Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6a2ad76
feat: add Tool Script Safety Guard
lll-peanut Jul 1, 2026
1333d95
feat: add Tool Script Safety Guard
lll-peanut Jul 1, 2026
8d12bb2
style: fix flake8 warnings in safety module (unused imports, f-string)
lll-peanut Jul 1, 2026
6a4495d
fix: add AST Python scanner and shlex Bash scanner, fix whitelist_com…
lll-peanut Jul 19, 2026
fa799a4
docs: add cd-to-root notice in README quick start
lll-peanut Jul 19, 2026
a347cc7
docs: remove duplicate README, add cd note inline in CLI section
lll-peanut Jul 19, 2026
999e96e
docs: fix pip install comment breaking copy-paste
lll-peanut Jul 19, 2026
44b08fd
docs: fix CLI and test code blocks for one-click run
lll-peanut Jul 19, 2026
532b3da
docs: merge cd and commands into single code block per section
lll-peanut Jul 19, 2026
fca6cbd
chore: update example reports
lll-peanut Jul 19, 2026
b7f0d3c
fix: block oversized script bypass — DENY instead of needs_human_revi…
lll-peanut Jul 19, 2026
6fec1b3
fix: address code review issues in Tool Safety Guard
lll-peanut Jul 19, 2026
0305e68
fix: address second-round code review issues in Tool Safety Guard
lll-peanut Jul 19, 2026
99cffb9
fix: enforce max_script_bytes, fix decorator silent skip, rm flag par…
lll-peanut Jul 19, 2026
4f07d1d
fix: filter type None crash and blocklist Bash bypass
lll-peanut Jul 19, 2026
f88f898
fix: double-escape regex bypass and multi-field scan
lll-peanut Jul 19, 2026
88e78cf
fix: block_on_review switch, dead code removal, \n in regex
lll-peanut Jul 19, 2026
3f4731f
fix: echo bypass, VAR=val extraction, multi-cmd analysis
lll-peanut Jul 19, 2026
5c79d2f
fix: resolve W504 line break after binary operator CI error
lll-peanut Jul 19, 2026
896e877
fix: safety_wrapper fail-closed by default (require_script=True)
lll-peanut Jul 19, 2026
b1a85b9
fix: echo "$(...)" command substitution bypass
lll-peanut Jul 19, 2026
6a4bdfa
fix: python alias pollution, pipe splitting, dynamic exec bypass, dd …
lll-peanut Jul 19, 2026
fee73a2
fix: blocklist_commands enforcement, &-split, scanner cache, W605
lll-peanut Jul 19, 2026
074fd91
fix: allow_patterns audit log, risk_level recalc, redirect/background…
lll-peanut Jul 20, 2026
276f9f8
fix: pass command_args/env to decorator and filter, W504 line break
lll-peanut Jul 20, 2026
9a11238
fix: URL whitelist bypass via userinfo@host, scan_input mutation
lll-peanut Jul 20, 2026
a9dd1df
fix: /dev/null redirect false positive, AST domain extractor @ bypass
lll-peanut Jul 20, 2026
841bb9e
fix: CI compliance — yapf formatting and flake8 for all PR files
lll-peanut Jul 20, 2026
ba6dc17
test: patch coverage up
lll-peanut Jul 20, 2026
b811d60
fix: review issues + 96% patch coverage (~500 tests, 16 files)
lll-peanut Jul 20, 2026
a3676f1
fix: audit process-safe writes (fcntl), tail-read, lock LRU, oversize…
lll-peanut Jul 20, 2026
e579bc1
fix: critical AST bypass + fail-open hardening + bash prefix coverage
lll-peanut Jul 20, 2026
78757f0
fix: re.compile/getattr false positive DENY, unused variable
lll-peanut Jul 20, 2026
fa456cd
fix: audit lock LRU removal, getattr 3-arg safe, re.compile exempt
lll-peanut Jul 20, 2026
7120ab2
fix: 3-arg getattr bypass + policy race + re.compile W504
lll-peanut Jul 20, 2026
4f3e6a6
fix: builtins subscript bypass, /bin/rm normalize, filter args-as-lis…
lll-peanut Jul 20, 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
3 changes: 2 additions & 1 deletion .github/code_review/scripts/post_inline_comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ def main():
if position is not None:
target_line = end_line

target = "%s:%s-%s" % (path, start_line, end_line) if end_line and end_line > start_line else "%s:%s" % (path, start_line)
target = "%s:%s-%s" % (path, start_line,
end_line) if end_line and end_line > start_line else "%s:%s" % (path, start_line)

if position is None:
print("skip finding without diff position: %s" % target)
Expand Down
3 changes: 1 addition & 2 deletions examples/dynamic_subagent/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ def create_minimal_agent() -> LlmAgent:
tools=workspace_tools + [
DynamicSubAgentTool(
# Stream the sub-agent's execution to the parent consumer.
agent_config=SubAgentConfig(forward_events=True),
),
agent_config=SubAgentConfig(forward_events=True), ),
],
)

Expand Down
6 changes: 2 additions & 4 deletions examples/dynamic_subagent/agent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ def get_model_config() -> tuple[str, str, str]:
url = os.getenv('TRPC_AGENT_BASE_URL', '')
model_name = os.getenv('TRPC_AGENT_MODEL_NAME', '')
if not api_key or not url or not model_name:
raise ValueError(
'TRPC_AGENT_API_KEY, TRPC_AGENT_BASE_URL, and '
'TRPC_AGENT_MODEL_NAME must be set in environment variables'
)
raise ValueError('TRPC_AGENT_API_KEY, TRPC_AGENT_BASE_URL, and '
'TRPC_AGENT_MODEL_NAME must be set in environment variables')
return api_key, url, model_name
22 changes: 10 additions & 12 deletions examples/dynamic_subagent/run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ async def run_demo(mode: str):
user_content = Content(parts=[Part.from_text(text=query)])
print("\U0001F916 Assistant: ", end="", flush=True)
async for event in runner.run_async(
user_id=user_id,
session_id=current_session_id,
new_message=user_content,
user_id=user_id,
session_id=current_session_id,
new_message=user_content,
):
# Forwarded sub-agent execution events (SubAgentConfig
# forward_events=True). These are partial progress events carrying
Expand All @@ -146,15 +146,11 @@ async def run_demo(mode: str):
if part.thought:
continue
if part.function_call:
print(
f"\n\n\U0001F527 [Invoke Tool:: {part.function_call.name}"
f"{_truncate(part.function_call.args)}]\n"
)
print(f"\n\n\U0001F527 [Invoke Tool:: {part.function_call.name}"
f"{_truncate(part.function_call.args)}]\n")
elif part.function_response:
print(
f"\n\U0001F4CA [Tool Result: "
f"{_truncate(part.function_response.response)}]\n"
)
print(f"\n\U0001F4CA [Tool Result: "
f"{_truncate(part.function_response.response)}]\n")

print(f"\n{'─' * 60}\n")

Expand All @@ -164,7 +160,9 @@ async def run_demo(mode: str):
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="DynamicSubAgentTool demo")
parser.add_argument(
"--mode", choices=["minimal", "bounded"], default="minimal",
"--mode",
choices=["minimal", "bounded"],
default="minimal",
help="minimal: workspace tools + dynamic_subagent; bounded: only dynamic_subagent",
)
args = parser.parse_args()
Expand Down
14 changes: 5 additions & 9 deletions examples/goal_tools/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,12 @@ def _create_model() -> LLMModel:
def on_retry(event: RetryEvent) -> None:
"""Observability callback: called every time the retry intercepts a premature final."""
if event.reason == "blocked":
print(
f" ⚡ [Goal retry] Premature final intercepted "
f"(attempt {event.attempt_number}/{event.max_retries}). "
f"Objective: {event.goal.objective!r}"
)
print(f" ⚡ [Goal retry] Premature final intercepted "
f"(attempt {event.attempt_number}/{event.max_retries}). "
f"Objective: {event.goal.objective!r}")
else:
print(
f" ⚠️ [Goal retry] Budget exhausted ({event.max_retries} retries). "
f"Letting final response through."
)
print(f" ⚠️ [Goal retry] Budget exhausted ({event.max_retries} retries). "
f"Letting final response through.")


def create_goal_agent(work_dir: str | None = None) -> LlmAgent:
Expand Down
6 changes: 2 additions & 4 deletions examples/goal_tools/agent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ def get_model_config() -> tuple[str, str, str]:
url = os.getenv("TRPC_AGENT_BASE_URL", "")
model_name = os.getenv("TRPC_AGENT_MODEL_NAME", "")
if not api_key or not url or not model_name:
raise ValueError(
"TRPC_AGENT_API_KEY, TRPC_AGENT_BASE_URL, and TRPC_AGENT_MODEL_NAME "
"must be set in environment variables"
)
raise ValueError("TRPC_AGENT_API_KEY, TRPC_AGENT_BASE_URL, and TRPC_AGENT_MODEL_NAME "
"must be set in environment variables")
return api_key, url, model_name
1 change: 0 additions & 1 deletion examples/goal_tools/agent/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@
"""Prompts for the Goal tools demo agent."""

INSTRUCTION = """You are a rigorous engineering assistant that can work toward session goals. """

35 changes: 15 additions & 20 deletions examples/goal_tools/run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@
APP_NAME = "goal_agent_demo"
USER_ID = "demo_user"


# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------


def _summarise_tool_response(name: str, resp: object) -> str:
if not isinstance(resp, dict):
return str(resp)
Expand Down Expand Up @@ -97,14 +97,14 @@ async def _run_turn(
# Tool calls/responses arrive as non-partial events and are printed immediately.
# Model text may arrive as streaming partial chunks; we fall back to partial
# collection so pure-text responses are always visible.
streaming_text: list[str] = [] # accumulated from partial text chunks
final_text: list[str] = [] # text from the last non-partial event
streaming_text: list[str] = [] # accumulated from partial text chunks
final_text: list[str] = [] # text from the last non-partial event

user_content = Content(parts=[Part.from_text(text=query)])
async for event in runner.run_async(
user_id=USER_ID,
session_id=session_id,
new_message=user_content,
user_id=USER_ID,
session_id=session_id,
new_message=user_content,
):
if not event.content or not event.content.parts:
continue
Expand Down Expand Up @@ -213,17 +213,13 @@ async def case1_model_sets_goal(work_dir: str) -> None:
# 目标与 prompt 差异化,观察模型是否能够正确理解目标并执行任务
"在当前目录创建 notes/ 目录,其中包含两个文件:\n"
" - summary.txt:用三句话描述 Python 异步编程的核心概念\n"
" - example.py:一个可运行的 asyncio 示例(包含 main 协程和 asyncio.run 调用)"
)
" - example.py:一个可运行的 asyncio 示例(包含 main 协程和 asyncio.run 调用)")

CASE2_TURNS = [
(
"执行任务(用户设置目标)",
# 宿主已通过 start_goal() 设置了目标,消息里不提及任何 goal 工具。
"请在当前目录创建 notes/ 目录,在其中写文件:\n"
"summary.txt:用三句话描述 Python 异步编程的核心概念\n"
)
]
CASE2_TURNS = [(
"执行任务(用户设置目标)",
# 宿主已通过 start_goal() 设置了目标,消息里不提及任何 goal 工具。
"请在当前目录创建 notes/ 目录,在其中写文件:\n"
"summary.txt:用三句话描述 Python 异步编程的核心概念\n")]


async def case2_user_sets_goal(work_dir: str) -> None:
Expand Down Expand Up @@ -270,10 +266,8 @@ async def case2_user_sets_goal(work_dir: str) -> None:
print(f"🎯 Goal pre-injected by host:")
print(f" objective: {goal.objective!r}")
print(f" status: {goal.status.value}")
print(
"\n📌 Note: goal is active from the first token.\n"
" The agent does NOT call create_goal.\n"
)
print("\n📌 Note: goal is active from the first token.\n"
" The agent does NOT call create_goal.\n")

for label, query in CASE2_TURNS:
await _run_turn(runner, session_id=session_id, label=label, query=query, agent_name=agent.name)
Expand All @@ -285,6 +279,7 @@ async def case2_user_sets_goal(work_dir: str) -> None:
# Entry point
# ---------------------------------------------------------------------------


async def main() -> None:
work_dir = os.getcwd()
await case1_model_sets_goal(work_dir)
Expand Down
44 changes: 24 additions & 20 deletions examples/spawn_subagent/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,28 +53,27 @@ def create_default_agent() -> LlmAgent:
description="Coding assistant with spawn_subagent in zero-config mode.",
model=_create_model(),
instruction=INSTRUCTION,
tools=[ReadTool(), GlobTool(), GrepTool(),
SpawnSubAgentTool(
# Stream the sub-agent's execution to the parent consumer.
agent_config=SubAgentConfig(forward_events=True),
)],
tools=[
ReadTool(),
GlobTool(),
GrepTool(),
SpawnSubAgentTool(
# Stream the sub-agent's execution to the parent consumer.
agent_config=SubAgentConfig(forward_events=True), )
],
)


_SECURITY_AUDITOR = SubAgentArchetype(
name="security-auditor",
description=(
"Specialized security auditor for code vulnerability analysis. "
"Use this for ANY security-related task: code audits, secret "
"detection, auth review. Checks for OWASP Top 10 risks, CWE "
"patterns, rates severity, and produces structured reports."
),
instruction=(
"You are a security auditor. Review the relevant code for security "
"issues: injection risks, hardcoded secrets, unsafe API usage, "
"missing authentication/authorization checks. Report findings "
"concisely with severity (low/medium/high/critical). Do NOT modify files."
),
description=("Specialized security auditor for code vulnerability analysis. "
"Use this for ANY security-related task: code audits, secret "
"detection, auth review. Checks for OWASP Top 10 risks, CWE "
"patterns, rates severity, and produces structured reports."),
instruction=("You are a security auditor. Review the relevant code for security "
"issues: injection risks, hardcoded secrets, unsafe API usage, "
"missing authentication/authorization checks. Report findings "
"concisely with severity (low/medium/high/critical). Do NOT modify files."),
tools=(ReadTool, GlobTool, GrepTool),
)

Expand All @@ -92,7 +91,9 @@ def create_code_agent() -> LlmAgent:
model=_create_model(),
instruction=INSTRUCTION,
tools=[
ReadTool(), GlobTool(), GrepTool(),
ReadTool(),
GlobTool(),
GrepTool(),
SpawnSubAgentTool(
agents=[_SECURITY_AUDITOR, EXPLORE_AGENT, PLAN_AGENT],
# Stream the sub-agent's execution to the parent consumer.
Expand All @@ -117,9 +118,12 @@ def create_md_agent() -> LlmAgent:
model=_create_model(),
instruction=INSTRUCTION,
tools=[
ReadTool(), GlobTool(), GrepTool(),
ReadTool(),
GlobTool(),
GrepTool(),
SpawnSubAgentTool(
agents=[EXPLORE_AGENT, PLAN_AGENT], agent_paths=[_AGENTS_PATH],
agents=[EXPLORE_AGENT, PLAN_AGENT],
agent_paths=[_AGENTS_PATH],
# Stream the sub-agent's execution to the parent consumer.
agent_config=SubAgentConfig(forward_events=True),
),
Expand Down
6 changes: 2 additions & 4 deletions examples/spawn_subagent/agent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ def get_model_config() -> tuple[str, str, str]:
url = os.getenv('TRPC_AGENT_BASE_URL', '')
model_name = os.getenv('TRPC_AGENT_MODEL_NAME', '')
if not api_key or not url or not model_name:
raise ValueError(
'TRPC_AGENT_API_KEY, TRPC_AGENT_BASE_URL, and '
'TRPC_AGENT_MODEL_NAME must be set in environment variables'
)
raise ValueError('TRPC_AGENT_API_KEY, TRPC_AGENT_BASE_URL, and '
'TRPC_AGENT_MODEL_NAME must be set in environment variables')
return api_key, url, model_name
25 changes: 15 additions & 10 deletions examples/spawn_subagent/run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
if EXAMPLE_DIR not in sys.path:
sys.path.insert(0, EXAMPLE_DIR)


def _truncate(text: str, max_len: int = 200) -> str:
"""Truncate long tool output for display."""
if not isinstance(text, str):
Expand Down Expand Up @@ -99,8 +100,10 @@ def _print_subagent_progress(payload: dict) -> None:
"accept a 'user_id' parameter, and report which files they are in "
"and what they do.",
],
"code": _SHARED_AGENT_QUERIES,
"md": _SHARED_AGENT_QUERIES,
"code":
_SHARED_AGENT_QUERIES,
"md":
_SHARED_AGENT_QUERIES,
}


Expand Down Expand Up @@ -139,9 +142,9 @@ async def run_demo(mode: str):
user_content = Content(parts=[Part.from_text(text=query)])
print("\U0001F916 Assistant: ", end="", flush=True)
async for event in runner.run_async(
user_id=user_id,
session_id=current_session_id,
new_message=user_content,
user_id=user_id,
session_id=current_session_id,
new_message=user_content,
):
# Forwarded sub-agent execution events (SubAgentConfig
# forward_events=True). These are partial progress events carrying
Expand All @@ -165,7 +168,9 @@ async def run_demo(mode: str):
if part.thought:
continue
if part.function_call:
print(f"\n\n\U0001F527 [Invoke Tool:: {part.function_call.name}{(_truncate(part.function_call.args))}]\n")
tool_info = f"[Invoke Tool:: {part.function_call.name}"
tool_info += f"{(_truncate(part.function_call.args))}]"
print(f"\n\n\U0001F527 {tool_info}\n")
elif part.function_response:
print(f"\n\U0001F4CA [Tool Result: {_truncate(part.function_response.response)}]\n")

Expand All @@ -176,10 +181,10 @@ async def run_demo(mode: str):

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="SpawnSubAgentTool demo")
parser.add_argument(
"--mode", choices=["default", "code", "md"], default="default",
help="Which agent configuration to run (default: default)"
)
parser.add_argument("--mode",
choices=["default", "code", "md"],
default="default",
help="Which agent configuration to run (default: default)")
args = parser.parse_args()

os.chdir(SAMPLE_REPO)
Expand Down
Loading
Loading