Skip to content

Commit e6da057

Browse files
committed
Remove safety feature and rename prompts to use md format.
1 parent c01d5b8 commit e6da057

35 files changed

Lines changed: 83 additions & 694 deletions

README.md

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,23 @@ A Python port of the Emacs [gptel-agent-harness](https://github.com/beacoder/gpt
2626
delivered in the original call order.
2727
- **Default agent prompts** — the main agent and sub-agents each get a
2828
distinct default system prompt bundled with the package
29-
(`prompts/agent.txt`, `prompts/subagent.txt`), with YAML frontmatter
29+
(`prompts/agent.md`, `prompts/subagent.md`), with YAML frontmatter
3030
stripped and the `{{SKILLS}}` placeholder filled from the discovered
3131
skill directory. The main prompt is prefixed with the project context
3232
files and the task-completion rules; sub-agents get only their own
3333
prompt. `/init` `/review` `/explain` and custom commands
34-
(`prompts/commands/*.txt`) run with their own prompt for that run.
34+
(`prompts/commands/*.md`) run with their own prompt for that run.
3535
- **Plan / Build modes** — plan mode is read-only except the per-session
3636
plan file; PlanExit switches back to build with an "execute the plan"
3737
prompt; sub-agents in plan mode receive the read-only reminder.
38-
- **Safety** — forbidden paths (default `/mnt/`), catastrophic/destructive/
39-
dangerous bash pattern tiers, per-session allow/deny memory, 300s command
40-
timeout, and a plan-mode read-only bash whitelist.
4138
- **Sessions** — auto-saved after every response to
4239
`~/.local/share/python-agent-harness/sessions/`, LLM-generated titles
4340
(one-shot per session, fired when the agent loop finishes; the file is
4441
renamed to `<title>_<TS>.md`), `/restore` (with `--latest`) and
4542
`/sessions` TUI commands.
4643
- **Commands**`/init` (create/update AGENTS.md), `/review` (uncommitted
4744
changes / commit / branch / PR), `/summary`, `/explain` and custom
48-
commands from `prompts/commands/*.txt` — all TUI slash commands.
45+
commands from `prompts/commands/*.md` — all TUI slash commands.
4946
Tool availability: `/init`/`/review` may use **all
5047
tools except PlanExit** (the PlanExit tool is hidden for the run,
5148
including for spawned sub-agents); custom commands may use all tools
@@ -56,7 +53,7 @@ A Python port of the Emacs [gptel-agent-harness](https://github.com/beacoder/gpt
5653
(or Alt+Enter) to submit, and **Tab completion** (Tab to complete,
5754
Shift+Tab to cycle backwards): the first token starting with `/`
5855
completes against the slash commands (builtins + custom
59-
`prompts/commands/*.txt`); after a slash command's space, Tab
56+
`prompts/commands/*.md`); after a slash command's space, Tab
6057
completes paths relative to the project dir (absolute and `~` paths
6158
work too; directories get a trailing `/` to keep drilling). In plain
6259
messages, any token containing `/` or starting with `~` (e.g.
@@ -66,8 +63,7 @@ A Python port of the Emacs [gptel-agent-harness](https://github.com/beacoder/gpt
6663
- **TUI** — rich live interface with a pinned status bar (mode, context
6764
usage, spinner), streaming assistant output, tool-result previews, a
6865
pinned Todos panel (sub-agent lists shown with a `sub:` label), and
69-
numbered-choice questions for Bash approval / Question tool /
70-
PlanExit confirmations.
66+
numbered-choice questions for Question tool / PlanExit confirmations.
7167
- **Diff rendering** — Edit/Write tool calls capture a unified diff of
7268
the file change and render it inline (red/green) in the TUI, so file
7369
edits are visible without leaving the app.
@@ -141,7 +137,7 @@ TUI slash commands: `/plan` `/build` `/init` `/review` `/explain`
141137
code and `/summary` appends a conversation summary (both TUI-only);
142138
`/sessions` lists saved sessions and `/restore [path|title|--latest]`
143139
restores one (`/restore` matches sessions by title substring). Custom
144-
commands from `prompts/commands/*.txt` are TUI slash commands too
140+
commands from `prompts/commands/*.md` are TUI slash commands too
145141
(TUI-only — no CLI subcommand is registered for them).
146142

147143
Input editing: type your message, press **Enter** for a new line, and
@@ -160,7 +156,6 @@ python_agent_harness/
160156
├── client.py OpenAI-compatible streaming client (httpx)
161157
├── models.py Message / ToolCall / ToolSpec data classes
162158
├── token_estimator.py CJK-aware token estimation + calibration
163-
├── safety.py path guards + bash policy tiers
164159
├── planmode.py build/plan mode + plan file lifecycle
165160
├── prompts.py prompt loading + system prompt assembly
166161
├── session_store.py session persistence + titles
@@ -187,7 +182,7 @@ Python ≥ 3.11 is required (CI runs 3.11 / 3.12 / 3.13).
187182
- [x] Tool-result sanitization (None → error placeholder)
188183
- [x] Compaction: frame, resume last request
189184
- [x] Plan mode: read-only + plan-file writes only
190-
- [x] Bash tiers: catastrophic → plan gate → destructive → dangerous → run
185+
- [x] Bash: Ctrl-C process-group kill
191186
- [x] Session metadata round-trip and title sanitization
192187
- [x] One-shot LLM title generation after the agent loop finishes
193188
- [x] Ctrl-C cancel: stale workers can't clobber the next run's history

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
55
[project]
66
name = "python-agent-harness"
77
version = "0.1.0"
8-
description = "Python agent execution harness: agent loop, tools, safety, cache, plan/build modes, sessions"
8+
description = "Python agent execution harness: agent loop, tools, cache, plan/build modes, sessions"
99
readme = "README.md"
1010
requires-python = ">=3.11"
1111
dependencies = [
@@ -21,4 +21,4 @@ python-agent-harness = "python_agent_harness.cli:main"
2121
include = ["python_agent_harness*"]
2222

2323
[tool.setuptools.package-data]
24-
python_agent_harness = ["prompts/*.txt", "prompts/commands/*.txt"]
24+
python_agent_harness = ["prompts/*.md", "prompts/commands/*.md"]

python_agent_harness/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def compact(self) -> bool:
168168
conversation = "\n\n".join(
169169
f"{m.role}: {m.text()}" for m in self.messages if m.text()
170170
)
171-
system = read_prompt_file("compact.txt")
171+
system = read_prompt_file("compact.md")
172172
resp, _ = self.session.client.chat_sync(
173173
[Message(role="user", content=conversation)], system=system
174174
)

python_agent_harness/agent_session.py

Lines changed: 17 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
"""AgentSession: the runtime hub wiring tools, safety, plan mode.
1+
"""AgentSession: the runtime hub wiring tools, plan mode.
22
3-
The session implements the ToolContext-facing API (path guards, bash
4-
verdicts, sub-agents, questions) and the
5-
agent-loop-facing API (client, calibrator, plan mode, auto-save,
6-
notifications). The TUI layer subclasses it to provide interactive
7-
confirmations.
3+
The session implements the ToolContext-facing API (sub-agents,
4+
questions) and the agent-loop-facing API (client, calibrator, plan
5+
mode, auto-save, notifications). The TUI layer subclasses it to
6+
provide interactive confirmations.
87
"""
98

109
from __future__ import annotations
@@ -17,7 +16,6 @@
1716
from .client import Client
1817
from .models import AgentMode
1918
from .planmode import PlanMode
20-
from .safety import BashPolicy, SafetyViolation, check_path
2119
from .session_store import SessionStore
2220
from .subagent import run_subagent
2321
from .token_estimator import TokenCalibrator
@@ -96,7 +94,6 @@ def __init__(
9694
self.registry = registry or Registry()
9795
self.calibrator = TokenCalibrator()
9896
self.plan_mode = PlanMode(project_dir)
99-
self.bash_policy = BashPolicy()
10097
self.tool_ctx = ToolContext(self)
10198
self._tool_diffs: dict[str, str] = {}
10299
# thread-local: parallel sub-agents each execute tools in their
@@ -145,7 +142,6 @@ def __init__(
145142
self.notify_fn: Callable[[str], None] | None = None
146143
self.confirm_fn: Callable[[str], bool] | None = None
147144
self.ask_fn: Callable[[list[dict]], str] | None = None
148-
self.bash_approval_fn: Callable[[str], tuple[bool, str]] | None = None
149145

150146
# ------------------------------------------------------------------
151147
# notifications
@@ -184,7 +180,7 @@ def tool_specs(self, exclude: tuple[str, ...] = ()) -> list:
184180
def execute_tool(
185181
self, name: str, args: dict[str, Any], call_id: str | None = None
186182
) -> str:
187-
"""Execute a tool with safety integration.
183+
"""Execute a tool.
188184
189185
``call_id`` (when given) lets Edit/Write attach a unified diff
190186
for the TUI to render; retrieve it afterwards with
@@ -196,14 +192,6 @@ def execute_tool(
196192
if blocked:
197193
return blocked
198194

199-
if name in ("Write", "Edit", "Insert", "Mkdir"):
200-
path = self._tool_path(name, args)
201-
if path:
202-
try:
203-
check_path(path, name)
204-
except SafetyViolation as e:
205-
return str(e)
206-
207195
self._active_call.call_id = call_id
208196
try:
209197
result = self.registry.execute(name, args, self.tool_ctx)
@@ -225,7 +213,10 @@ def take_diff(self, call_id: str) -> str | None:
225213

226214
def _plan_blocked(self, name: str, args: dict[str, Any]) -> str | None:
227215
if name == "Bash":
228-
return None # handled by bash policy below
216+
return (
217+
"Error: blocked by plan mode (read-only phase); "
218+
"Bash is disabled — use Read/Glob/Grep for read-only access"
219+
)
229220
path = self._tool_path(name, args)
230221
if path and path != self.plan_mode.plan_file:
231222
return (
@@ -248,54 +239,6 @@ def _tool_path(self, name: str, args: dict[str, Any]) -> str | None:
248239
# ------------------------------------------------------------------
249240
# ToolContext-facing API
250241
# ------------------------------------------------------------------
251-
def guard_path(self, path: str, tool_name: str) -> None:
252-
check_path(path, tool_name)
253-
254-
def verify_bash(self, command: str) -> str | None:
255-
"""Return an error string to deliver, or None to run.
256-
257-
The interactive approval prompt is serialized: parallel tool
258-
rounds may reach CONFIRM simultaneously, but the user can only
259-
answer one question at a time. Command *execution* stays
260-
parallel — the lock is released before the process starts.
261-
"""
262-
with self._interactive_lock:
263-
self.bash_policy.plan_mode = self.plan_mode.is_plan
264-
verdict = self.bash_policy.verdict(command)
265-
if verdict != "CONFIRM":
266-
return verdict
267-
if self.bash_approval_fn:
268-
run, answer = self.bash_approval_fn(command)
269-
else:
270-
run, answer = self._ask_via_tui(command)
271-
if answer == "allow":
272-
self.bash_policy.record(command, "allow")
273-
return None
274-
if answer == "deny":
275-
self.bash_policy.record(command, "deny")
276-
return "Error: Bash command rejected by user approval (denied for this session)."
277-
if run:
278-
return None
279-
return "Error: Bash command rejected by user approval."
280-
281-
def _ask_via_tui(self, command: str) -> tuple[bool, str]:
282-
prompt = (
283-
"Dangerous Bash command:\n\n"
284-
f"{command}\n\n"
285-
"Run it? [y]es / [n]o / [a]lways allow (session) / [d]eny (session)"
286-
)
287-
if self.ask_fn is None:
288-
# headless fallback: run once (matches confirm-tool-calls opt-out)
289-
return True, "run"
290-
answer = self.ask_fn([{"question": prompt}])
291-
if answer.startswith("a"):
292-
return True, "allow"
293-
if answer.startswith("d"):
294-
return False, "deny"
295-
if answer.startswith("y") or "Yes" in answer:
296-
return True, "run"
297-
return False, "run"
298-
299242
def update_todos(self, todos: list[dict]) -> None:
300243
"""Store TODOS so the pinned TUI panel shows the current list."""
301244
self.todos = list(todos)
@@ -376,9 +319,9 @@ def _mode_prompts(self) -> dict[str, str]:
376319
from .prompts import read_prompt_file
377320

378321
return {
379-
"plan": read_prompt_file("plan.txt"),
380-
"plan-mode": read_prompt_file("plan-mode.txt"),
381-
"build-switch": read_prompt_file("build-switch.txt"),
322+
"plan": read_prompt_file("plan.md"),
323+
"plan-mode": read_prompt_file("plan-mode.md"),
324+
"build-switch": read_prompt_file("build-switch.md"),
382325
}
383326

384327
# ------------------------------------------------------------------
@@ -408,7 +351,7 @@ def auto_save(self, messages: list, system: str | None) -> None:
408351
self.log(f"auto-save failed: {e}")
409352

410353
def generate_session_title(self) -> None:
411-
"""Generate a title from the first real user message (title.txt).
354+
"""Generate a title from the first real user message (title.md).
412355
413356
Mirrors gptel-agent-harness--generate-session-title: one-shot per
414357
session (guarded by store.title / title_pending); on success the
@@ -432,7 +375,7 @@ def generate_session_title(self) -> None:
432375
from .prompts import read_prompt_file
433376
from .models import Message as Msg
434377

435-
system = read_prompt_file("title.txt")
378+
system = read_prompt_file("title.md")
436379
resp, _ = self.client.chat_sync(
437380
[Msg(role="user", content=first)],
438381
system=system,
@@ -523,7 +466,7 @@ def compact_conversation(self) -> tuple[bool, str]:
523466
self.compacting = True
524467
try:
525468
conversation = self._conversation_text(messages)
526-
system = read_prompt_file("compact.txt")
469+
system = read_prompt_file("compact.md")
527470
resp, _ = self.client.chat_sync(
528471
[Msg(role="user", content=conversation)], system=system
529472
)
@@ -562,7 +505,7 @@ def summarize_conversation(self) -> str:
562505
if not messages:
563506
return "Nothing to summarize."
564507
conversation = self._conversation_text(messages)
565-
system = read_prompt_file("summary.txt")
508+
system = read_prompt_file("summary.md")
566509
try:
567510
resp, _ = self.client.chat_sync(
568511
[Msg(role="user", content=conversation)], system=system

python_agent_harness/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
run [project] interactive TUI agent session (default)
55
config [--init] show effective LLM config / write a template file
66
7-
Custom commands (prompts/commands/*.txt) — like init, review,
7+
Custom commands (prompts/commands/*.md) — like init, review,
88
sessions, restore and summary/explain — are TUI slash commands only;
99
they are NOT registered as CLI subcommands.
1010

python_agent_harness/commands.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
Tool availability per command:
1010
- init/review: all tools EXCEPT PlanExit (they are one-shot runs that
1111
must not end in a plan/build handoff)
12-
- custom commands (prompts/commands/*.txt): all tools, incl. PlanExit
12+
- custom commands (prompts/commands/*.md): all tools, incl. PlanExit
1313
- compact/summary: no tools at all (direct chat_sync calls, like the
1414
session-title generation)
1515
"""
@@ -89,7 +89,7 @@ def prepare(
8989
def initialize_command() -> SessionCommand:
9090
return SessionCommand(
9191
name="initialize",
92-
prompt_file="initialize.txt",
92+
prompt_file="initialize.md",
9393
kickoff="Analyze the repository at ${path} and create/update AGENTS.md.\n",
9494
buffer_name="*gptel-agent-init:*",
9595
status=" Initializing...",
@@ -101,7 +101,7 @@ def initialize_command() -> SessionCommand:
101101
def review_command() -> SessionCommand:
102102
return SessionCommand(
103103
name="review",
104-
prompt_file="review.txt",
104+
prompt_file="review.md",
105105
kickoff="Review the requested code changes.",
106106
buffer_name="*gptel-agent-review*",
107107
status=" Reviewing...",
@@ -144,7 +144,7 @@ def load_custom_commands() -> list[SessionCommand]:
144144
if not COMMANDS_DIR.is_dir():
145145
return []
146146
commands = []
147-
for f in sorted(COMMANDS_DIR.glob("*.txt")):
147+
for f in sorted(COMMANDS_DIR.glob("*.md")):
148148
name = custom_name(f.name)
149149
if not name:
150150
continue

0 commit comments

Comments
 (0)