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
109from __future__ import annotations
1716from .client import Client
1817from .models import AgentMode
1918from .planmode import PlanMode
20- from .safety import BashPolicy , SafetyViolation , check_path
2119from .session_store import SessionStore
2220from .subagent import run_subagent
2321from .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
0 commit comments