Skip to content

feat: implement core tool categories and granular sub-constraints#1101

Merged
Aureliolo merged 18 commits intomainfrom
feat/tool-system
Apr 7, 2026
Merged

feat: implement core tool categories and granular sub-constraints#1101
Aureliolo merged 18 commits intomainfrom
feat/tool-system

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

Implements #1034 (core tool categories) and #220 (granular tool access sub-constraints) as a single cohesive change. Adds 6 new tools across 3 categories, a shared SSRF prevention layer, and a granular sub-constraint enforcement pipeline integrated into the existing tool invoker.

Tool Categories (#1034)

Web tools (tools/web/)

  • HttpRequestTool: GET/POST/PUT/DELETE via httpx with SSRF prevention (shared NetworkPolicy, IP blocklist, DNS validation). Redirects disabled to prevent SSRF bypass. Response truncation at configurable max bytes.
  • WebSearchTool: Vendor-agnostic search via WebSearchProvider protocol -- no concrete implementation shipped (inject via MCP bridge or custom provider).
  • HtmlParserTool: Text/links/metadata extraction via stdlib html.parser. Strips script/style tags. Operates on pre-fetched content (no HTTP).

Database tools (tools/database/)

  • SqlQueryTool: Parameterized SQL execution via aiosqlite. Read-only by default with defense-in-depth: statement keyword classification AND SQLite URI-based ?mode=ro. Query timeout via asyncio.wait_for. Table name validation regex for PRAGMA queries.
  • SchemaInspectTool: list_tables (sqlite_master) + describe_table (PRAGMA table_info with safe identifier validation).

Terminal tools (tools/terminal/)

  • ShellCommandTool: Sandboxed command execution via SandboxBackend delegation. Command allow/blocklist. Working directory support. Output truncation. Returns error when no sandbox configured.

Shared infrastructure

  • network_validator.py: Extracted SSRF blocklist and DNS validation from git_url_validator (backward compatible). NetworkPolicy model reusable across tool categories. Case-insensitive scheme validation.

Granular Sub-Constraints (#220)

Models (sub_constraints.py)

  • Five constraint dimension enums: FileSystemScope, NetworkMode, GitAccess, CodeExecutionIsolation, TerminalAccess
  • ToolSubConstraints frozen Pydantic model with per-level defaults matching operations.md section 11.2
  • get_sub_constraints() resolution with custom override support

Enforcement (sub_constraint_enforcer.py)

  • SubConstraintEnforcer checks network (blocks WEB when NONE), terminal (blocks TERMINAL when NONE), git (blocks push for LOCAL_ONLY/READ_AND_BRANCH, blocks clone for LOCAL_ONLY), and requires_approval (escalation for matching action type prefixes)
  • Integrated into ToolPermissionChecker (optional sub_constraints param)
  • Wired into ToolInvoker pipeline between permission check and param validation

Agent model

  • ToolPermissions gains sub_constraints: ToolSubConstraints | None field for per-agent overrides

Integration

  • Tool factory extended with _build_web/database/terminal_tools builders
  • RootConfig gains optional web, database, terminal config fields
  • Event constants for web, database, terminal, sub_constraint domains
  • New dependency: httpx==0.28.1 (async HTTP client)

Security Highlights

  • SSRF: shared IP blocklist (IPv4+IPv6), DNS resolution validation, scheme restriction, redirect disabled, fail-closed on unparseable IPs
  • SQL injection: parameterized queries + table name regex + SQLite read-only URI mode
  • Command injection: sandbox delegation + allow/blocklist (documented as best-effort safety net, sandbox is primary defense)
  • Sub-constraints: network=NONE blocks web tools, terminal=NONE blocks shell tools, git=LOCAL_ONLY blocks clone+push

Test Plan

  • 170+ new unit tests across 22 test files
  • All 14,827 existing unit tests continue to pass
  • Pre-reviewed by 10 agents, 25 findings addressed (security fixes, conventions, docs)

Review Coverage

  • code-reviewer, security-reviewer, type-design-analyzer, silent-failure-hunter, pr-test-analyzer, async-concurrency-reviewer, conventions-enforcer, logging-audit, issue-resolution-verifier, docs-consistency

Closes #1034
Closes #220

Copilot AI review requested due to automatic review settings April 6, 2026 23:10
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 6, 2026 23:11 — with GitHub Actions Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 6, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 7912db9.
Ensure that dependencies are being submitted on PR branches. Re-running this action after a short time may resolve the issue. See the documentation for more information and troubleshooting advice.

OpenSSF Scorecard

PackageVersionScoreDetails
pip/httpx 0.28.1 UnknownUnknown

Scanned Files

  • pyproject.toml

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 6, 2026

Codecov Report

❌ Patch coverage is 90.49180% with 87 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.03%. Comparing base (31e7273) to head (7912db9).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/synthorg/tools/terminal/shell_command.py 79.16% 12 Missing and 3 partials ⚠️
src/synthorg/tools/web/http_request.py 84.14% 12 Missing and 1 partial ⚠️
src/synthorg/tools/database/sql_query.py 88.04% 7 Missing and 4 partials ⚠️
src/synthorg/tools/database/schema_inspect.py 87.83% 8 Missing and 1 partial ⚠️
src/synthorg/tools/network_validator.py 90.21% 6 Missing and 3 partials ⚠️
src/synthorg/tools/web/html_parser.py 93.79% 5 Missing and 3 partials ⚠️
src/synthorg/tools/web/web_search.py 86.53% 6 Missing and 1 partial ⚠️
src/synthorg/tools/sub_constraints.py 92.85% 3 Missing and 1 partial ⚠️
src/synthorg/tools/web/base_web_tool.py 83.33% 3 Missing and 1 partial ⚠️
src/synthorg/tools/terminal/base_terminal_tool.py 92.85% 2 Missing and 1 partial ⚠️
... and 3 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1101      +/-   ##
==========================================
+ Coverage   89.00%   89.03%   +0.02%     
==========================================
  Files         802      824      +22     
  Lines       46993    47903     +910     
  Branches     4715     4821     +106     
==========================================
+ Hits        41828    42650     +822     
- Misses       4285     4352      +67     
- Partials      880      901      +21     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces new tool categories for web, database, and terminal operations, including robust SSRF prevention for web tools and granular sub-constraint enforcement for all tools. My review identified a high-severity DNS rebinding vulnerability in the HTTP request tool, a medium-severity path injection risk in the SQL query tool's URI construction, and a performance concern regarding connection pooling in the HTTP request tool.

Comment on lines +187 to +195
async with httpx.AsyncClient() as client:
response = await client.request(
method=method,
url=url,
headers=headers,
content=body,
timeout=timeout,
follow_redirects=False,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The current implementation is vulnerable to DNS rebinding (TOCTOU). While _validate_url performs DNS resolution and checks the resulting IPs against the blocklist, httpx.AsyncClient().request will perform its own DNS resolution when it receives the hostname. An attacker could return a public IP during validation and a private/internal IP during the actual request. To mitigate this, the tool should pin the request to the validated IP addresses found in the validation object (e.g., by using a custom transport or rewriting the URL to use the IP and setting the Host header).

) -> ToolExecutionResult:
"""Execute the query against SQLite (inner coroutine for timeout wrapping)."""
if self._config.read_only:
db_uri = f"file:{self._config.database_path}?mode=ro"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The SQLite URI construction is vulnerable to path injection or breakage if database_path contains special characters like ? or #. Since uri=True is passed to aiosqlite.connect, the path component must be properly URI-encoded to ensure it is interpreted correctly as a file path rather than part of the query string.

Suggested change
db_uri = f"file:{self._config.database_path}?mode=ro"
import urllib.parse
db_uri = f"file:{urllib.parse.quote(str(self._config.database_path))}?mode=ro"

A ``ToolExecutionResult`` with the response.
"""
try:
async with httpx.AsyncClient() as client:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instantiating a new httpx.AsyncClient for every request is inefficient as it prevents connection pooling. If this tool is invoked frequently by an agent, the overhead of establishing new TCP/TLS connections for every call will significantly impact performance. Consider managing a persistent client instance or reusing one from a shared context.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements core tool categories (web/database/terminal) and adds a granular “sub-constraints” layer to gate tool invocation beyond category-level permissions, including shared SSRF defenses and new observability domains.

Changes:

  • Added built-in Web tools (HTTP requests, HTML parsing, provider-backed search) plus shared SSRF network validation.
  • Added Database tools (SQLite query + schema inspection) and Terminal tool (sandboxed shell execution) with new config wiring and event domains.
  • Implemented sub-constraint models + enforcement and integrated them into ToolPermissionChecker and ToolInvoker, with extensive unit tests.

Reviewed changes

Copilot reviewed 53 out of 57 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
uv.lock Adds httpx dependency lock entry.
pyproject.toml Pins httpx==0.28.1.
docs/design/operations.md Updates design doc to reflect sub-constraint enforcement layer.
CLAUDE.md Updates repo documentation to include new tool categories and events.
src/synthorg/tools/network_validator.py New shared SSRF/DNS/IP validation utilities and NetworkPolicy.
src/synthorg/tools/web/base_web_tool.py Introduces common WEB tool base with URL validation helper.
src/synthorg/tools/web/http_request.py Adds HTTP request tool using httpx + SSRF validation + truncation.
src/synthorg/tools/web/html_parser.py Adds HTML parsing/extraction tool (text/links/metadata).
src/synthorg/tools/web/web_search.py Adds provider-agnostic web search tool and protocol.
src/synthorg/tools/web/config.py Adds web tools configuration model.
src/synthorg/tools/web/init.py Exposes web tool public API exports.
src/synthorg/tools/database/base_db_tool.py Adds common DATABASE tool base.
src/synthorg/tools/database/config.py Adds database configuration models.
src/synthorg/tools/database/sql_query.py Adds SQLite query execution tool with read-only defaults.
src/synthorg/tools/database/schema_inspect.py Adds schema inspection tool (list tables / describe table).
src/synthorg/tools/database/init.py Exposes database tool public API exports.
src/synthorg/tools/terminal/base_terminal_tool.py Adds common TERMINAL tool base with allow/blocklist logic.
src/synthorg/tools/terminal/config.py Adds terminal tool configuration model.
src/synthorg/tools/terminal/shell_command.py Adds sandbox-delegated shell command tool.
src/synthorg/tools/terminal/init.py Exposes terminal tool public API exports.
src/synthorg/tools/sub_constraints.py Adds sub-constraint enums, model, and per-access-level defaults.
src/synthorg/tools/sub_constraint_enforcer.py Adds enforcement logic for sub-constraints on invocations.
src/synthorg/tools/permissions.py Integrates sub-constraint enforcement into permission checking.
src/synthorg/tools/invoker.py Enforces sub-constraints in the invocation pipeline.
src/synthorg/tools/factory.py Adds tool builders for new categories and config-based wiring.
src/synthorg/tools/git_url_validator.py Reuses shared SSRF blocklist from network_validator.
src/synthorg/tools/init.py Re-exports new tools/configs in top-level tools package.
src/synthorg/observability/events/web.py Adds web event constants.
src/synthorg/observability/events/database.py Adds database event constants.
src/synthorg/observability/events/terminal.py Adds terminal event constants.
src/synthorg/observability/events/sub_constraint.py Adds sub-constraint event constants.
src/synthorg/core/agent.py Adds per-agent ToolPermissions.sub_constraints field.
src/synthorg/config/schema.py Adds optional web/database/terminal config fields to RootConfig.
src/synthorg/config/defaults.py Adds default entries for new config fields.
tests/unit/observability/test_events.py Extends event-domain discovery expectations.
tests/unit/tools/test_sub_constraints.py Tests sub-constraint enums/defaults/resolution.
tests/unit/tools/test_sub_constraint_enforcer.py Tests enforcement logic for each constraint dimension.
tests/unit/tools/test_permissions_sub_constraints.py Tests permission checker integration with sub-constraints.
tests/unit/tools/test_invoker_sub_constraints.py Tests invoker pipeline sub-constraint enforcement behavior.
tests/unit/tools/test_factory.py Updates expected tool names list.
tests/unit/tools/test_factory_sandbox_wiring.py Updates expected tool counts for new categories.
tests/unit/tools/test_factory_new_categories.py Adds factory tests for web/database/terminal categories.
tests/unit/tools/test_network_validator.py Adds comprehensive SSRF/network validator tests.
tests/unit/tools/web/conftest.py Adds shared fixtures for web tool tests.
tests/unit/tools/web/test_http_request.py Adds HttpRequestTool unit tests.
tests/unit/tools/web/test_html_parser.py Adds HtmlParserTool unit tests.
tests/unit/tools/web/test_web_search.py Adds WebSearchTool unit tests.
tests/unit/tools/terminal/conftest.py Adds shared fixtures for terminal tool tests.
tests/unit/tools/terminal/test_config.py Adds terminal config unit tests.
tests/unit/tools/terminal/test_shell_command.py Adds ShellCommandTool unit tests.
tests/unit/tools/database/conftest.py Adds shared fixtures for database tool tests.
tests/unit/tools/database/test_config.py Adds database config unit tests.
tests/unit/tools/database/test_sql_query.py Adds SqlQueryTool unit tests.
tests/unit/tools/database/test_schema_inspect.py Adds SchemaInspectTool unit tests.
tests/unit/tools/web/init.py Test package init.
tests/unit/tools/terminal/init.py Test package init.
tests/unit/tools/database/init.py Test package init.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +154 to +161
# Write protection
is_write = keyword in _WRITE_PREFIXES
if is_write and self._config.read_only:
logger.warning(
DB_WRITE_BLOCKED,
keyword=keyword,
database=self._config.database_path,
)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read-only enforcement is bypassable for statements like PRAGMA/WITH (and any unclassified keyword) because is_write only checks _WRITE_PREFIXES. This contradicts the comment above and allows potentially mutating PRAGMAs in read-only mode. Consider treating any statement not explicitly in a small read-only allowlist (e.g., SELECT/EXPLAIN) as write/blocked when read_only=True, or explicitly classify PRAGMA/WITH as write-unsafe.

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +129
super().__init__(
name="sql_query",
description=(
"Execute SQL queries against a database. "
"Read-only by default; write queries require "
"explicit configuration."
),
parameters_schema=dict(_PARAMETERS_SCHEMA),
action_type=ActionType.DB_QUERY,
config=config,
)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tool’s action_type is always ActionType.DB_QUERY, but the docstring says write queries should use ActionType.DB_MUTATE for escalation. As written, write operations won’t be classified as db:mutate, which can undermine approval/risk policy. Consider setting the action type based on the classified statement (or otherwise surfacing a per-invocation action_type to the security pipeline).

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +133
async def _list_tables(self) -> ToolExecutionResult:
"""List all tables in the database."""
async with aiosqlite.connect(self._config.database_path) as db:
cursor = await db.execute(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
)
rows = await cursor.fetchall()
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SchemaInspectTool claims “Always read-only”, but it connects with aiosqlite.connect(self._config.database_path) which can create a missing DB file and opens it writable even when the connection config is read-only. Consider using SQLite URI mode=ro (and failing if the file doesn’t exist) to make the read-only guarantee enforceable.

Copilot uses AI. Check for mistakes.
Comment on lines +211 to +213
if url.startswith("-"):
return False
url_lower = url.lower()
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_allowed_http_scheme() checks url.startswith("-") without stripping leading whitespace, so inputs like " -http://…" won’t be caught by the flag-injection guard (and leading whitespace may also affect subsequent parsing/validation). Consider normalizing with lstrip() (or rejecting leading whitespace/control chars) before scheme checks.

Suggested change
if url.startswith("-"):
return False
url_lower = url.lower()
normalized_url = url.lstrip()
if normalized_url.startswith("-"):
return False
url_lower = normalized_url.lower()

Copilot uses AI. Check for mistakes.
Comment on lines +379 to +406
normalized = hostname.lower()
is_https = url.startswith("https://")

port: int | None = None
try:
raw_port = urlparse(url).port
except ValueError:
logger.warning(
WEB_SSRF_BLOCKED,
hostname=normalized,
reason="malformed_port",
)
return f"Invalid port in URL: {url!r}"
if raw_port is not None and raw_port <= 0:
logger.warning(
WEB_SSRF_BLOCKED,
hostname=normalized,
port=raw_port,
reason="invalid_port",
)
return f"Invalid port in URL: {raw_port!r}"
if raw_port is not None:
port = raw_port
elif is_https:
port = 443
else:
port = 80

Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_url_host() determines HTTPS via url.startswith("https://"), which is case-sensitive. Since is_allowed_http_scheme() allows uppercase schemes (it lowercases), URLs like "HTTPS://example.com" will be treated as HTTP (wrong port and is_https). Consider using urlparse(url).scheme.casefold() for scheme detection and default port selection.

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +157
"""Enforce network mode constraint.

``NONE`` blocks all tools in network-requiring categories.
``ALLOWLIST_ONLY`` is enforced at the transport level (not here),
but tools in network-requiring categories are allowed through.
"""
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NetworkMode.ALLOWLIST_ONLY is documented as “enforced at the transport level”, but there’s no implementation using ToolSubConstraints.network_allowlist (it’s currently unused in the codebase). As a result, RESTRICTED mode still allows any public host. Consider wiring network_allowlist into the web transport (e.g., build a NetworkPolicy allowlist from it) or enforcing host:port checks here before allowing WEB tools.

Copilot uses AI. Check for mistakes.
Comment on lines +194 to +205
all_tools: list[BaseTool] = [
*_build_file_system_tools(workspace=workspace),
*_build_git_tools(
workspace=workspace,
git_clone_policy=git_clone_policy,
sandbox=sandbox,
),
)
*_build_web_tools(
network_policy=web_network_policy,
search_provider=web_search_provider,
),
*_build_terminal_tools(
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RootConfig.web is documented as “None = no web tools”, but build_default_tools_from_config() always builds web tools regardless of whether config.web is set (it only uses it to optionally supply a policy). This makes the configuration contract ambiguous. Consider either (a) skipping web tool creation when config.web is None, or (b) updating the schema field description/defaults to reflect that web tools are enabled by default.

Copilot uses AI. Check for mistakes.
)
web: WebToolsConfig | None = Field(
default=None,
description="Web tool configuration (None = no web tools)",
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The web field description says “None = no web tools”, but the tool factory currently constructs web tools even when this is None. Either the description should be updated (e.g., “None = default web config”) or the factory should treat None as a disable flag to match this docstring.

Suggested change
description="Web tool configuration (None = no web tools)",
description="Web tool configuration (None = default web config)",

Copilot uses AI. Check for mistakes.
from synthorg.tools.web.config import WebToolsConfig
from synthorg.tools.web.html_parser import HtmlParserTool
from synthorg.tools.web.http_request import HttpRequestTool
from synthorg.tools.web.web_search import SearchResult, WebSearchProvider, WebSearchTool
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import line likely exceeds the configured Ruff line length (88) and will fail linting. Consider splitting the imported names across multiple lines (or using parentheses) to satisfy the formatter/linter.

Suggested change
from synthorg.tools.web.web_search import SearchResult, WebSearchProvider, WebSearchTool
from synthorg.tools.web.web_search import (
SearchResult,
WebSearchProvider,
WebSearchTool,
)

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +100
command: str = arguments["command"]
working_dir: str | None = arguments.get("working_directory")
timeout: float = arguments.get("timeout") or self._config.default_timeout

if not command.strip():
return ToolExecutionResult(
content="Empty command",
is_error=True,
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

working_directory is documented as “relative to workspace”, but the implementation passes Path(working_dir) directly to the sandbox without enforcing that it’s relative (or within the workspace) at the tool layer. If a sandbox backend is configured without strict workspace enforcement, this could allow escaping the intended directory boundary. Consider rejecting absolute paths and normalizing/validating that the resolved path stays within the sandbox/workspace root before calling the backend.

Copilot uses AI. Check for mistakes.
@Aureliolo
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds three new tool categories—web, database, and terminal—implementing HTTP requests (using httpx), HTML parsing, web search provider integration, SQLite schema inspection, and SQL query execution. Introduces centralized SSRF/DNS validation (tools/network_validator), granular sub-constraint enums and defaults (tools/sub_constraints), and a SubConstraintEnforcer with integration into ToolPermissionChecker and ToolInvoker for per-invocation gating. Expands observability with new event modules (events.web, events.database, events.terminal, events.sub_constraint), updates config/schema/defaults to expose web, database, and terminal, extends package exports, and adds comprehensive unit and integration tests.

Suggested labels

autorelease: tagged

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 40.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: implementation of core tool categories and granular sub-constraints as listed in linked issues #1034 and #220.
Description check ✅ Passed The description is comprehensive, well-organized, and directly related to the changeset. It outlines the implementation of three tool categories, shared infrastructure, granular sub-constraints, and integration points with detailed security highlights.
Linked Issues check ✅ Passed The changeset fully implements both linked issues: #1034 (all three tool categories with specified features) and #220 (granular sub-constraints with per-level defaults and custom override support) are comprehensively addressed through 6 new tools, shared SSRF infrastructure, constraint models, enforcer integration, and test coverage.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #1034 and #220 objectives. The only tangentially related change is updating git_url_validator to use shared network_validator blocklist, which is a supported factoring of SSRF prevention infrastructure mentioned in the PR description.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/unit/tools/test_factory.py (1)

45-45: ⚠️ Potential issue | 🟡 Minor

Update docstring to reflect new tool count.

The docstring says "11 built-in tools" but _EXPECTED_TOOL_NAMES now contains 14 tools.

📝 Proposed fix
-        """Factory returns all 11 built-in tools sorted by name."""
+        """Factory returns all 14 built-in tools sorted by name."""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/tools/test_factory.py` at line 45, Update the docstring in
tests/unit/tools/test_factory.py to match the current expected tool count:
change the phrase "Factory returns all 11 built-in tools sorted by name." to
reflect 14 built-in tools (or use a count derived from _EXPECTED_TOOL_NAMES);
locate the test function or module containing that docstring and update it to
"14 built-in tools" or make it dynamic by referencing _EXPECTED_TOOL_NAMES so
the docstring remains accurate when the list changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/tools/database/config.py`:
- Around line 44-51: Add validation to ensure the default_connection value
exists in the connections mapping: implement a pydantic root_validator (or
`@validator` that accesses other fields) for the model that checks that the
'default_connection' string is one of the keys of the 'connections' dict and
raises a ValueError if not (handle the case where connections is empty
appropriately). Use the existing field names 'connections' and
'default_connection' in the validator so misconfiguration is caught at model
creation rather than later in tool resolution.

In `@src/synthorg/tools/database/schema_inspect.py`:
- Around line 112-125: Change the broad "except Exception" in the schema
inspection block to catch database-specific errors only (use "except
aiosqlite.Error as exc") so only SQLite-related failures are handled; update
imports to include aiosqlite if missing, keep the existing logger.warning call
(DB_SCHEMA_INSPECT_FAILED, action=action, error=str(exc)) and the
ToolExecutionResult return unchanged, and do not swallow system exceptions like
KeyboardInterrupt or SystemExit.

In `@src/synthorg/tools/database/sql_query.py`:
- Around line 254-262: Remove the redundant list() wrapping around await
cursor.fetchall() in the async function that assigns to rows; change the
assignment to use rows = await cursor.fetchall() so rows is directly the list
returned by cursor.fetchall(), leaving the subsequent logger.info and
ToolExecutionResult code (keyword, row_count handling) unchanged and referencing
the same rows variable.

In `@src/synthorg/tools/factory.py`:
- Around line 298-318: The code rebuilds sandbox backends multiple times when
sandbox_backends is None (calls to build_sandbox_backends before resolving
different categories); refactor by computing a single local backends variable
once (e.g., compute resolved_backends = sandbox_backends or
build_sandbox_backends(config=config.sandboxing, workspace=workspace)) and then
pass resolved_backends into resolve_sandbox_for_category for both the version
control and terminal resolution paths (references: sandbox_backends,
build_sandbox_backends, resolve_sandbox_for_category, config.sandboxing,
terminal_sandbox) so backends are built at most once per call.

In `@src/synthorg/tools/invoker.py`:
- Around line 199-204: The call to
self._permission_checker.check_sub_constraints passes dict(tool_call.arguments)
which is only a shallow copy and may allow nested mutable values to be mutated
by sub-constraint evaluation; replace the shallow copy with a deep copy (use
copy.deepcopy) of tool_call.arguments before passing it to
_permission_checker.check_sub_constraints so mutations cannot leak into later
validation/execution, keeping the call signature and arguments (tool.name,
tool.category, tool.action_type) unchanged.

In `@src/synthorg/tools/sub_constraint_enforcer.py`:
- Around line 124-138: The current tuple eagerly evaluates all checks by calling
self._check_network, self._check_terminal, self._check_git, and
self._check_requires_approval up front; change this to lazy evaluation so each
check is invoked only as you iterate. Replace the tuple of direct calls with a
generator/iterable of callables or a generator expression that calls each check
(e.g., yield or call each lambda) and iterate over it, invoking each check in
turn and returning the first non-None violation; refer to the methods
_check_network, _check_terminal, _check_git, and _check_requires_approval and
the local variable checks/violation to locate and update the code.

In `@src/synthorg/tools/sub_constraints.py`:
- Around line 236-241: The code directly indexes _LEVEL_SUB_CONSTRAINTS with
access_level which can raise a KeyError if a new ToolAccessLevel isn't present;
change this to fetch with a safe default (e.g.,
_LEVEL_SUB_CONSTRAINTS.get(access_level, DEFAULT_SUB_CONSTRAINTS or an empty
list)) and add a defensive logger.warning or logger.debug indicating an unknown
access_level before falling back, then continue to call
logger.debug(SUB_CONSTRAINT_RESOLVED, access_level=access_level.value,
source="level_default") as before; update references to _LEVEL_SUB_CONSTRAINTS,
access_level, and SUB_CONSTRAINT_RESOLVED in the same block.

In `@src/synthorg/tools/terminal/base_terminal_tool.py`:
- Around line 66-85: The command blocklist currently uses substring matching via
the _is_command_blocked method (it checks if pattern.lower() in normalized), so
update the docstring for the command_blocklist field in the terminal config (the
command_blocklist declaration) to explicitly state patterns are matched as
substrings anywhere in the command (not just as prefixes), note this differs
from the allowlist's prefix matching, and add a brief example and warning about
single-word patterns like "halt"/"shutdown"/"reboot" potentially matching
unintended strings; keep the wording concise and include guidance to use more
specific patterns if exact command matching is desired.

In `@src/synthorg/tools/terminal/config.py`:
- Around line 27-40: Update the command_blocklist in config.py (symbol:
command_blocklist) to include broader/regex-safe patterns and additional
dangerous commands: replace simple substrings like "rm -rf /" with regex
patterns that account for flags (e.g.,
r"\\brm\\s+(-[\\w-\\s]+\\s*)*/?\\s*/\\b"), add patterns for recursive
chmod/chown (e.g., r"\\bchmod\\s+-R\\b", r"\\bchown\\s+-R\\b"), mv to /dev/null
(e.g., r"\\bmv\\s+/.+\\s+/dev/null\\b"), and other dangerous variants (mkfs, dd,
fork bomb, shutdown/reboot/poweroff variants); ensure the Field default uses
these regex strings and update any matching/validation code to use re.search on
each pattern rather than simple substring checks so the new patterns are
effective, and keep in mind to adopt an allowlist approach for production
deployments.

In `@src/synthorg/tools/terminal/shell_command.py`:
- Around line 198-206: The truncation currently slices output to
self._config.max_output_bytes then appends a marker, which can push the result
past the limit; fix by precomputing the truncation marker string (e.g.,
trunc_marker = f"\n\n[Truncated: output exceeded
{self._config.max_output_bytes:,} bytes]"), then when len(output) >
self._config.max_output_bytes set truncated = True and slice the original output
to self._config.max_output_bytes - len(trunc_marker) (use max(0, ...) to avoid
negative slice lengths) and append trunc_marker so the final output length never
exceeds self._config.max_output_bytes; update references to output, truncated,
and self._config.max_output_bytes accordingly.

In `@src/synthorg/tools/web/html_parser.py`:
- Around line 169-179: HtmlParserTool.__init__ currently doesn't accept or
forward network_policy and request_timeout to BaseWebTool; add optional
parameters network_policy=None and request_timeout=None to
HtmlParserTool.__init__ and pass them through in the super().__init__ call (or
alternatively change the base class to a simpler BaseTool and update the
docstring to state this tool makes no HTTP requests) so the behavior is
explicit; update the constructor signature and the super call referencing
HtmlParserTool.__init__ and BaseWebTool to reflect the chosen approach and
adjust the docstring to note whether network settings are ignored.

In `@src/synthorg/tools/web/http_request.py`:
- Around line 209-227: The code currently uses response.text and len(content)
which counts characters and decodes the entire body into memory, so change the
logic in the HTTP request handling to read and truncate by bytes: stream the
response bytes (e.g., using response.iter_content(chunk_size=4096) or access
response.content up to _max_response_bytes) accumulate at most
self._max_response_bytes bytes, set truncated=True if more bytes exist, then
decode only the collected bytes to a string (use errors='replace' or 'ignore')
for logging and use the byte length for content_length; update uses of
response.text, content, truncated, and the logger.info(WEB_REQUEST_SUCCESS, ...)
call accordingly so truncation and the "[Truncated: response exceeded ...
bytes]" message reflect bytes not characters and avoid loading the full body
into memory.

In `@src/synthorg/tools/web/web_search.py`:
- Line 119: Replace the shallow copy used when constructing parameters_schema
with a deep, read-only copy: use copy.deepcopy(_PARAMETERS_SCHEMA) and wrap the
result with types.MappingProxyType so nested dicts like "properties" aren't
shared or mutable; update the import to include copy and MappingProxyType and
change the construction site where parameters_schema=dict(_PARAMETERS_SCHEMA) is
used (referencing _PARAMETERS_SCHEMA and the parameters_schema argument) to
create MappingProxyType(copy.deepcopy(_PARAMETERS_SCHEMA)).

In `@tests/unit/tools/database/test_sql_query.py`:
- Around line 34-35: Add the missing pytest unit marker to the test by
decorating the test_classify method with `@pytest.mark.unit` (and ensure pytest is
imported if not already); this mirrors other tests in the file and keeps the
test suite markers consistent while the test still asserts
_classify_statement(query) == expected.

In `@tests/unit/tools/test_factory_new_categories.py`:
- Around line 48-60: The test test_custom_network_policy builds a custom
NetworkPolicy and passes it to build_default_tools but never asserts the policy
was applied; update the test to fetch the http_request tool (as done via
next(...) and HttpRequestTool) and add an assertion that the tool's
network_policy (or the appropriate property on the HttpRequestTool instance)
equals the policy instance or has the same settings (e.g.,
block_private_ips=False) to verify build_default_tools applied the custom
NetworkPolicy.

In `@tests/unit/tools/test_network_validator.py`:
- Around line 36-39: Replace the broad pytest.raises(Exception) in test_frozen
with the specific pydantic ValidationError: update the test case (test_frozen)
to expect ValidationError from pydantic when attempting to mutate
NetworkPolicy.block_private_ips, import ValidationError from pydantic at the top
of the test module, and make the same replacement for the similar assertions
around lines 57-60 so all Pydantic field validation checks use
pytest.raises(ValidationError) instead of Exception.

In `@tests/unit/tools/test_sub_constraint_enforcer.py`:
- Around line 40-44: The test_frozen currently expects a generic Exception when
mutating the frozen Pydantic model; update test_frozen to expect
pydantic.errors.ValidationError specifically by replacing
pytest.raises(Exception) with pytest.raises(ValidationError) and add the
appropriate import for ValidationError from pydantic at the top of the test
file; keep the test target SubConstraintViolation and the mutation line
unchanged so the failure still arises from the frozen behavior.
- Around line 161-174: The test test_local_only_blocks_clone currently calls
enforcer.check with ActionType.VCS_COMMIT which is incorrect for a clone/read
operation; update the enforcer.check invocation in test_local_only_blocks_clone
to use ActionType.VCS_READ (or add a concise inline comment explaining why
VCS_COMMIT is intentionally used) so the action type matches the semantic intent
for a clone, keeping references to SubConstraintEnforcer,
ToolSubConstraints(git=GitAccess.LOCAL_ONLY), and the "git_clone" call
consistent.

---

Outside diff comments:
In `@tests/unit/tools/test_factory.py`:
- Line 45: Update the docstring in tests/unit/tools/test_factory.py to match the
current expected tool count: change the phrase "Factory returns all 11 built-in
tools sorted by name." to reflect 14 built-in tools (or use a count derived from
_EXPECTED_TOOL_NAMES); locate the test function or module containing that
docstring and update it to "14 built-in tools" or make it dynamic by referencing
_EXPECTED_TOOL_NAMES so the docstring remains accurate when the list changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: db74c08f-e17e-49b5-8366-737b9c304970

📥 Commits

Reviewing files that changed from the base of the PR and between c845d22 and c01b62f.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (56)
  • CLAUDE.md
  • docs/design/operations.md
  • pyproject.toml
  • src/synthorg/config/defaults.py
  • src/synthorg/config/schema.py
  • src/synthorg/core/agent.py
  • src/synthorg/observability/events/database.py
  • src/synthorg/observability/events/sub_constraint.py
  • src/synthorg/observability/events/terminal.py
  • src/synthorg/observability/events/web.py
  • src/synthorg/tools/__init__.py
  • src/synthorg/tools/database/__init__.py
  • src/synthorg/tools/database/base_db_tool.py
  • src/synthorg/tools/database/config.py
  • src/synthorg/tools/database/schema_inspect.py
  • src/synthorg/tools/database/sql_query.py
  • src/synthorg/tools/factory.py
  • src/synthorg/tools/git_url_validator.py
  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/network_validator.py
  • src/synthorg/tools/permissions.py
  • src/synthorg/tools/sub_constraint_enforcer.py
  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/terminal/__init__.py
  • src/synthorg/tools/terminal/base_terminal_tool.py
  • src/synthorg/tools/terminal/config.py
  • src/synthorg/tools/terminal/shell_command.py
  • src/synthorg/tools/web/__init__.py
  • src/synthorg/tools/web/base_web_tool.py
  • src/synthorg/tools/web/config.py
  • src/synthorg/tools/web/html_parser.py
  • src/synthorg/tools/web/http_request.py
  • src/synthorg/tools/web/web_search.py
  • tests/unit/observability/test_events.py
  • tests/unit/tools/database/__init__.py
  • tests/unit/tools/database/conftest.py
  • tests/unit/tools/database/test_config.py
  • tests/unit/tools/database/test_schema_inspect.py
  • tests/unit/tools/database/test_sql_query.py
  • tests/unit/tools/terminal/__init__.py
  • tests/unit/tools/terminal/conftest.py
  • tests/unit/tools/terminal/test_config.py
  • tests/unit/tools/terminal/test_shell_command.py
  • tests/unit/tools/test_factory.py
  • tests/unit/tools/test_factory_new_categories.py
  • tests/unit/tools/test_factory_sandbox_wiring.py
  • tests/unit/tools/test_invoker_sub_constraints.py
  • tests/unit/tools/test_network_validator.py
  • tests/unit/tools/test_permissions_sub_constraints.py
  • tests/unit/tools/test_sub_constraint_enforcer.py
  • tests/unit/tools/test_sub_constraints.py
  • tests/unit/tools/web/__init__.py
  • tests/unit/tools/web/conftest.py
  • tests/unit/tools/web/test_html_parser.py
  • tests/unit/tools/web/test_http_request.py
  • tests/unit/tools/web/test_web_search.py

Comment on lines +254 to +262
)

rows = list(await cursor.fetchall())
if not rows:
logger.info(DB_QUERY_SUCCESS, keyword=keyword, row_count=0)
return ToolExecutionResult(
content="Query returned no results.",
metadata={"keyword": keyword, "row_count": 0},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Minor: Redundant list() wrapping.

cursor.fetchall() already returns a list. The list() wrapper on line 256 is unnecessary.

♻️ Remove redundant list()
-            rows = list(await cursor.fetchall())
+            rows = await cursor.fetchall()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
)
rows = list(await cursor.fetchall())
if not rows:
logger.info(DB_QUERY_SUCCESS, keyword=keyword, row_count=0)
return ToolExecutionResult(
content="Query returned no results.",
metadata={"keyword": keyword, "row_count": 0},
)
)
rows = await cursor.fetchall()
if not rows:
logger.info(DB_QUERY_SUCCESS, keyword=keyword, row_count=0)
return ToolExecutionResult(
content="Query returned no results.",
metadata={"keyword": keyword, "row_count": 0},
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/database/sql_query.py` around lines 254 - 262, Remove the
redundant list() wrapping around await cursor.fetchall() in the async function
that assigns to rows; change the assignment to use rows = await
cursor.fetchall() so rows is directly the list returned by cursor.fetchall(),
leaving the subsequent logger.info and ToolExecutionResult code (keyword,
row_count handling) unchanged and referencing the same rows variable.

Comment on lines +124 to +138
checks = (
self._check_network(tool_name, category),
self._check_terminal(tool_name, category),
self._check_git(tool_name, action_type),
self._check_requires_approval(tool_name, action_type),
)
for violation in checks:
if violation is not None:
logger.warning(
SUB_CONSTRAINT_DENIED,
tool_name=tool_name,
constraint=violation.constraint,
reason=violation.reason,
)
return violation
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider lazy evaluation for minor efficiency gain.

All _check_* methods are called eagerly when constructing the tuple (lines 124-128), even though iteration stops at the first violation. This is functionally correct but performs unnecessary work when an early check fails.

♻️ Lazy evaluation alternative
-        checks = (
-            self._check_network(tool_name, category),
-            self._check_terminal(tool_name, category),
-            self._check_git(tool_name, action_type),
-            self._check_requires_approval(tool_name, action_type),
-        )
-        for violation in checks:
-            if violation is not None:
+        check_fns = (
+            lambda: self._check_network(tool_name, category),
+            lambda: self._check_terminal(tool_name, category),
+            lambda: self._check_git(tool_name, action_type),
+            lambda: self._check_requires_approval(tool_name, action_type),
+        )
+        for check_fn in check_fns:
+            violation = check_fn()
+            if violation is not None:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
checks = (
self._check_network(tool_name, category),
self._check_terminal(tool_name, category),
self._check_git(tool_name, action_type),
self._check_requires_approval(tool_name, action_type),
)
for violation in checks:
if violation is not None:
logger.warning(
SUB_CONSTRAINT_DENIED,
tool_name=tool_name,
constraint=violation.constraint,
reason=violation.reason,
)
return violation
check_fns = (
lambda: self._check_network(tool_name, category),
lambda: self._check_terminal(tool_name, category),
lambda: self._check_git(tool_name, action_type),
lambda: self._check_requires_approval(tool_name, action_type),
)
for check_fn in check_fns:
violation = check_fn()
if violation is not None:
logger.warning(
SUB_CONSTRAINT_DENIED,
tool_name=tool_name,
constraint=violation.constraint,
reason=violation.reason,
)
return violation
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/sub_constraint_enforcer.py` around lines 124 - 138, The
current tuple eagerly evaluates all checks by calling self._check_network,
self._check_terminal, self._check_git, and self._check_requires_approval up
front; change this to lazy evaluation so each check is invoked only as you
iterate. Replace the tuple of direct calls with a generator/iterable of
callables or a generator expression that calls each check (e.g., yield or call
each lambda) and iterate over it, invoking each check in turn and returning the
first non-None violation; refer to the methods _check_network, _check_terminal,
_check_git, and _check_requires_approval and the local variable checks/violation
to locate and update the code.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

♻️ Duplicate comments (1)
src/synthorg/tools/web/http_request.py (1)

200-227: ⚠️ Potential issue | 🟠 Major

The response cap still doesn't cap memory usage.

Switching to response.content fixed the byte/char accounting, but AsyncClient.request() still buffers the entire body before this code truncates it. A large response can therefore exhaust memory even when _max_response_bytes is small. Use streaming (client.stream(...) / response.aiter_bytes()) and stop reading once the budget is hit.

In httpx 0.28.1, does AsyncClient.request() buffer the full response body by default, and what async streaming API should be used to stop after N bytes (for example, client.stream(...) with response.aiter_bytes())?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/web/http_request.py` around lines 200 - 227, The current
implementation uses httpx.AsyncClient.request which buffers the entire response
before you truncate, risking OOM; replace the request call with
httpx.AsyncClient.stream and iterate response.aiter_bytes() (or aiter_raw()
chunks) inside the same async with, accumulate up to self._max_response_bytes
and stop reading further once the budget is hit, then decode the accumulated
bytes to produce content; keep the same exception handling
(TimeoutException/HTTPError) and return ToolExecutionResult as before, and
ensure the stream/response is properly closed when you break early.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/tools/database/config.py`:
- Around line 46-49: The connections mapping currently allows blank/whitespace
keys; update its type to use NotBlankStr for keys so invalid names are rejected
at model creation: change the annotated type from dict[str,
DatabaseConnectionConfig] to dict[NotBlankStr, DatabaseConnectionConfig] and
import NotBlankStr from core.types, keeping the Field(...) as-is; ensure this
aligns with the existing default_connection constraint and run validations to
confirm blank keys are now rejected.

In `@src/synthorg/tools/database/schema_inspect.py`:
- Around line 127-177: Both _list_tables and _describe_table open and query the
DB without honoring DatabaseConnectionConfig.query_timeout, so slow/locked DBs
can hang; modify these methods to wrap the async DB operations (the
aiosqlite.connect + execute + fetchall sequence) in an asyncio.wait_for using
the configured query_timeout (same pattern used by SqlQueryTool) and log/return
a ToolExecutionResult error on timeout; ensure you reference
self._config.query_timeout and keep the URI construction and safety check
(_SAFE_IDENTIFIER_RE) but move the actual DB calls into the timed block and
handle asyncio.TimeoutError similarly to other DB errors.

In `@src/synthorg/tools/database/sql_query.py`:
- Around line 39-53: The current _WRITE_PREFIXES tuple permits file-affecting
SQLite statements (e.g., ATTACH, DETACH, VACUUM) whenever read_only=False, which
allows arbitrary DB files to be touched; create a separate blacklist constant
(e.g., _FILE_AFFECTING_PREFIXES) containing "ATTACH", "DETACH", and "VACUUM"
(and any variants like "VACUUM INTO") and update the validation logic that
inspects SQL prefixes (the code paths that currently consult _WRITE_PREFIXES) to
always reject statements starting with any _FILE_AFFECTING_PREFIXES regardless
of the read_only flag; ensure both places where prefix checks occur (the
_WRITE_PREFIXES definition and the other occurrence around lines 158-175) are
updated to reference the new blacklist so file-oriented statements remain
blocked even when writable.

In `@src/synthorg/tools/factory.py`:
- Around line 120-142: The factory _build_database_tools currently builds tools
only for DatabaseConfig.default_connection (or the first entry), ignoring other
entries in DatabaseConfig.connections; update it to iterate over all
config.connections items and instantiate a SqlQueryTool and SchemaInspectTool
for each connection (using each connection's DatabaseConnectionConfig) so every
configured connection is exposed as tools; ensure returned tuple includes one
pair per connection (optionally include the connection name in tool construction
or wrapping if the tools support/need it) rather than only the default.
- Around line 270-274: The current assignment uses truthiness so an explicitly
provided empty mapping for sandbox_backends gets ignored; change the logic to
honor any supplied sandbox_backends (including {} or MappingProxyType({})) by
checking if sandbox_backends is not None and only calling
build_sandbox_backends(config.sandboxing, workspace) when sandbox_backends is
None; update the assignment that sets resolved_backends (and any related uses)
to prefer the provided sandbox_backends when sandbox_backends is not None,
otherwise call build_sandbox_backends.

In `@src/synthorg/tools/invoker.py`:
- Around line 207-221: The code treats requires_approval like a hard deny by
returning early instead of parking the escalation and letting the normal
security flow run; change the branch handling violation.requires_approval to
append a structured record to self._pending_escalations (including tool_call,
violation, tool_call.id/name and any correlation id) and produce a non-fatal
ToolResult (e.g., is_error=False or a special pending flag) or otherwise avoid
returning early so that _check_security() still executes; keep the existing
logger.warning(TOOL_SECURITY_ESCALATED, ...) but remove the immediate return and
ensure whatever caller logic expects a pending escalation can resume the tool
call from the entry points that inspect _pending_escalations and call
_check_security().
- Around line 199-204: Replace the direct copy.deepcopy call used when calling
_permission_checker.check_sub_constraints with the guarded helper
_safe_deepcopy_args so deepcopy failures are handled consistently; specifically,
before invoking _permission_checker.check_sub_constraints(tool.name,
tool.category, tool.action_type, ...), call self._safe_deepcopy_args(tool_call)
(or otherwise obtain the safely-copied arguments via that helper), check for a
ToolResult indicating TOOL_INVOKE_DEEPCOPY_ERROR and return it if present, and
only pass the successful deep-copied dict to
_permission_checker.check_sub_constraints to avoid raising on non-copyable
values.

In `@src/synthorg/tools/network_validator.py`:
- Around line 103-109: The current model_validator method _normalize_allowlist
mutates a frozen instance with object.__setattr__; instead implement
normalization before instance construction by changing the validator to run in
"before" mode and operate on the input mapping: replace
`@model_validator`(mode="after") _normalize_allowlist with a
`@model_validator`(mode="before") function that accepts the incoming values dict,
lowercases and deduplicates values from the "hostname_allowlist" key into a
tuple, sets that tuple back into the values dict, and returns the modified
values; remove any use of object.__setattr__ so no mutation of the constructed
object occurs.
- Around line 350-441: validate_url_host is too large and mixes parsing, port
normalization, allowlist/master-switch checks, literal-IP handling, and DNS
resolution; split it into small helpers to keep the main function under 50
lines. Extract responsibilities into helper functions such as
parse_and_normalize_hostname(url) that calls extract_hostname and determines
is_https, determine_port(url, is_https) that validates and returns port or an
error string, check_allowlist_and_master_switch(normalized, policy, is_https,
port) that returns _ok or an error as appropriate, handle_literal_ip(normalized,
is_https, port) that uses ipaddress.ip_address and is_blocked_ip, and
perform_dns_check(normalized, timeout) that wraps resolve_and_check and returns
either the resolved IPs or an error string; then rewrite validate_url_host to
call those helpers in sequence and return early based on their results,
preserving uses of _ok, resolve_and_check, is_blocked_ip, extract_hostname, and
policy.hostname_allowlist / policy.block_private_ips /
policy.dns_resolution_timeout.
- Around line 408-410: The allowlist branch returns early without performing DNS
resolution, leaving resolved_ips empty and bypassing the module's
TOCTOU/rebinding protections; change the early-return logic in the block that
checks if normalized is in policy.hostname_allowlist to perform the same
hostname resolution used elsewhere in this module (populate resolved_ips using
the existing resolver code/path and apply any canonicalization the function
uses), then call _ok with the normalized name, port, is_https flag and the
populated resolved_ips (i.e., preserve the same resolved_ips handling as the
non-allowlist path) so allowlisted hostnames are still pinned to their resolved
IPs before returning.

In `@src/synthorg/tools/sub_constraint_enforcer.py`:
- Around line 102-129: The check() method only evaluates network, terminal, git,
and requires_approval and ignores ToolSubConstraints.file_system,
.code_execution, and .network_allowlist as well as the arguments param; fix by
reading the ToolSubConstraints for the invoked tool (use the existing tool_name
lookup that returns ToolSubConstraints), add/ call new helper methods (e.g.,
_check_filesystem(tool_name, category, arguments),
_enforce_code_execution(tool_name, category),
_enforce_network_allowlist(tool_name, category)) or extend check() to: (1)
enforce FileSystemScope by validating paths from arguments with PathValidator
and rejecting accesses outside WORKSPACE_ONLY/PROJECT_DIRECTORY/FULL semantics,
(2) enforce code_execution by selecting/forcing the appropriate runner or
rejecting non-containerized runs when code_execution == CONTAINERIZED, and (3)
enforce per-agent network_allowlist by passing the allowlist into the network
transport or rejecting disallowed destinations; update check() to include these
checks (and return SubConstraintViolation on failure) and remove the unused noqa
on arguments once implemented.

In `@src/synthorg/tools/sub_constraints.py`:
- Around line 193-222: When get_sub_constraints receives custom_constraints for
a built-in access_level, do not return it verbatim; instead load the built-in
defaults for that access_level (the existing spec defaults used for
SANDBOXED/PROJECT/ETC) and merge only the explicitly provided override fields
from custom_constraints into those defaults so unspecified fields keep the
built-in values. Update get_sub_constraints to detect when access_level !=
ToolAccessLevel.CUSTOM, perform a field-wise merge/copy from the default
ToolSubConstraints for that access_level with non-None/custom values overriding,
and adjust the logger call (e.g., use source="merged" or similar) before
returning the resulting ToolSubConstraints.

In `@src/synthorg/tools/terminal/shell_command.py`:
- Around line 205-221: The code currently truncates the combined `output` string
by character count instead of bytes; change the truncation to operate on UTF-8
encoded bytes: after combining `result.stdout` and `result.stderr` into
`output`, encode it to `out_bytes = output.encode("utf-8")`, compute `marker =
f"\n\n[Truncated: output exceeded {self._config.max_output_bytes:,} bytes]"` and
`marker_bytes = marker.encode("utf-8")`, check `if len(out_bytes) >
self._config.max_output_bytes:` then slice `out_bytes = out_bytes[: max(0,
self._config.max_output_bytes - len(marker_bytes))]`, decode back with `output =
out_bytes.decode("utf-8", errors="replace")`, append `marker` and set `truncated
= True`; update references to `output`, `marker`, `truncated`, and keep using
`self._config.max_output_bytes`.
- Around line 113-121: Allowlist check currently inspects the raw shell string,
which is bypassable via shell chaining; modify the validation in the execution
path that calls bash -c so it either (A) parses the command into argv (e.g., via
shlex.split) and validates the parsed executable/first argument against
self._config.command_allowlist (use the same logic as _is_command_allowed but
applied to the first token), or (B) if you must keep passing through a shell,
explicitly reject any shell metacharacters (like ;, &&, |, $, `, >, <, \\n) when
a command_allowlist is set; update the code paths around _is_command_allowed and
where the subprocess is invoked so ToolExecutionResult is returned with
is_error=True when validation fails.
- Around line 162-180: The code currently only rejects absolute
working_directory values but lets relative paths like "../../" through; update
the validation where working_dir is converted to cwd (the Path(working_dir)
block) to resolve the path against the workspace root and ensure it stays inside
the workspace before calling self._sandbox.execute. Concretely, compute resolved
= (workspace_root / working_dir).resolve() and compare to
workspace_root.resolve() using resolved.is_relative_to(workspace_root.resolve())
(or equivalent containment check) and return a ToolExecutionResult with
is_error=True and a clear message if the resolved path is outside the workspace;
keep the existing error for absolute paths, and then pass the validated cwd into
self._sandbox.execute.

In `@src/synthorg/tools/web/http_request.py`:
- Around line 272-278: In _pin_url, avoid emitting malformed IPv6 netlocs and
stop mutating the caller's headers: when building pinned_netloc, detect IPv6
literals (e.g., pinned_ip contains ':') and wrap them in brackets (use
f"[{pinned_ip}]" when needed) before appending port_suffix; do not modify the
incoming headers dict — create a new_headers = dict(headers) (or {} if headers
is None), set new_headers["Host"] = parsed.hostname or "" if missing, and return
the rewritten URL together with the new_headers (adjust the function
return/signature accordingly) so callers receive a fresh headers mapping.

In `@src/synthorg/tools/web/web_search.py`:
- Around line 144-151: The current broad "except Exception" around the call to
self._provider.search in the web search routine swallows system errors; change
it to explicitly handle provider-level errors and re-raise system-level
exceptions. Replace the bare except with either an except catching the
provider-specific exception type(s) raised by WebSearchProvider.search (e.g.,
ProviderError) or, if unknown, keep except Exception as exc but immediately
re-raise for critical system exceptions (MemoryError, RecursionError,
KeyboardInterrupt, SystemExit) before logging and returning the
ToolExecutionResult; reference self._provider.search, WEB_SEARCH_FAILED,
logger.warning and ToolExecutionResult when making the change.

In `@tests/unit/tools/database/test_sql_query.py`:
- Around line 15-36: Remove the duplicate pytest marker on the test method: keep
a single `@pytest.mark.unit` and delete the other one so the decorator list for
test_classify only contains one `@pytest.mark.unit` plus the existing
`@pytest.mark.parametrize`; ensure the test still calls _classify_statement(query)
== expected and that `@pytest.mark.parametrize` remains immediately above the def
test_classify definition.

---

Duplicate comments:
In `@src/synthorg/tools/web/http_request.py`:
- Around line 200-227: The current implementation uses httpx.AsyncClient.request
which buffers the entire response before you truncate, risking OOM; replace the
request call with httpx.AsyncClient.stream and iterate response.aiter_bytes()
(or aiter_raw() chunks) inside the same async with, accumulate up to
self._max_response_bytes and stop reading further once the budget is hit, then
decode the accumulated bytes to produce content; keep the same exception
handling (TimeoutException/HTTPError) and return ToolExecutionResult as before,
and ensure the stream/response is properly closed when you break early.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2c8698de-5ffe-4f09-a570-feb744d03e8d

📥 Commits

Reviewing files that changed from the base of the PR and between c01b62f and 396eead.

📒 Files selected for processing (21)
  • src/synthorg/config/schema.py
  • src/synthorg/tools/database/config.py
  • src/synthorg/tools/database/schema_inspect.py
  • src/synthorg/tools/database/sql_query.py
  • src/synthorg/tools/factory.py
  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/network_validator.py
  • src/synthorg/tools/sub_constraint_enforcer.py
  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/terminal/config.py
  • src/synthorg/tools/terminal/shell_command.py
  • src/synthorg/tools/web/__init__.py
  • src/synthorg/tools/web/http_request.py
  • src/synthorg/tools/web/web_search.py
  • tests/integration/tools/test_factory_integration.py
  • tests/unit/tools/database/test_sql_query.py
  • tests/unit/tools/test_factory.py
  • tests/unit/tools/test_factory_new_categories.py
  • tests/unit/tools/test_network_validator.py
  • tests/unit/tools/test_sub_constraint_enforcer.py
  • tests/unit/tools/web/test_html_parser.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations—Python 3.14 has PEP 649 native lazy annotations.
Use PEP 758 except syntax: except A, B: (no parentheses)—ruff enforces this on Python 3.14.
Type hints: all public functions required, mypy strict mode enforced.
Docstrings: Google style, required on public classes/functions (enforced by ruff D rules).
Create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.
For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Use allow_inf_nan=False in all ConfigDict declarations to reject NaN/Inf in numeric fields at validation time.
Use @computed_field for derived values instead of storing + validating redundant fields (e.g. TokenUsage.total_tokens).
Use NotBlankStr (from core.types) for all identifier/name fields—including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants—instead of manual whitespace validators.
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.
Line length: 88 characters (ruff enforced).
Functions: < 50 lines, files < 800 lines.
Handle errors explicitly, never silently swallow.
Validate at system boundaries (user input, external APIs, config files).
Variable name: always logger (not _logger, not log).
All error paths must log at WARNING or ERROR with context before raising.
All state transitions must log at INFO.
DEBUG for object creation, internal flow, entry/exit of key functions.
Pure data models, enums, and re-exports do NOT need logging.

Files:

  • tests/integration/tools/test_factory_integration.py
  • tests/unit/tools/test_factory.py
  • src/synthorg/config/schema.py
  • tests/unit/tools/web/test_html_parser.py
  • src/synthorg/tools/invoker.py
  • tests/unit/tools/database/test_sql_query.py
  • tests/unit/tools/test_factory_new_categories.py
  • src/synthorg/tools/terminal/config.py
  • src/synthorg/tools/database/config.py
  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/database/schema_inspect.py
  • tests/unit/tools/test_network_validator.py
  • src/synthorg/tools/database/sql_query.py
  • src/synthorg/tools/web/__init__.py
  • src/synthorg/tools/web/http_request.py
  • src/synthorg/tools/factory.py
  • src/synthorg/tools/terminal/shell_command.py
  • tests/unit/tools/test_sub_constraint_enforcer.py
  • src/synthorg/tools/web/web_search.py
  • src/synthorg/tools/sub_constraint_enforcer.py
  • src/synthorg/tools/network_validator.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Markers: @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, @pytest.mark.slow.
Async: asyncio_mode = "auto"—no manual @pytest.mark.asyncio needed.
Timeout: 30 seconds per test (global in pyproject.toml—do not add per-file pytest.mark.timeout(30) markers; non-default overrides like timeout(60) are allowed).
Parametrize: Prefer @pytest.mark.parametrize for testing similar cases.
Tests must use test-provider, test-small-001, etc. (not real vendor names).
Property-based testing: Python uses Hypothesis (@given + @settings).
Hypothesis profiles configured in tests/conftest.py: ci (deterministic, max_examples=10 + derandomize=True), dev (1000 examples), fuzz (10,000 examples, no deadline), extreme (500,000 examples, no deadline). Controlled via HYPOTHESIS_PROFILE env var.
Never skip, dismiss, or ignore flaky tests—always fix them fully and fundamentally. For timing-sensitive tests, mock time.monotonic() and asyncio.sleep() to make them deterministic. For tasks that must block indefinitely until cancelled, use asyncio.Event().wait() instead of asyncio.sleep(large_number).

Files:

  • tests/integration/tools/test_factory_integration.py
  • tests/unit/tools/test_factory.py
  • tests/unit/tools/web/test_html_parser.py
  • tests/unit/tools/database/test_sql_query.py
  • tests/unit/tools/test_factory_new_categories.py
  • tests/unit/tools/test_network_validator.py
  • tests/unit/tools/test_sub_constraint_enforcer.py

⚙️ CodeRabbit configuration file

Test files do not require Google-style docstrings on classes or functions -- ruff D rules are only enforced on src/. A bare @settings() decorator with no arguments on Hypothesis property tests is a no-op and should not be suggested -- the HYPOTHESIS_PROFILE env var controls example counts via registered profiles, which @given() honors automatically.

Files:

  • tests/integration/tools/test_factory_integration.py
  • tests/unit/tools/test_factory.py
  • tests/unit/tools/web/test_html_parser.py
  • tests/unit/tools/database/test_sql_query.py
  • tests/unit/tools/test_factory_new_categories.py
  • tests/unit/tools/test_network_validator.py
  • tests/unit/tools/test_sub_constraint_enforcer.py
src/synthorg/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/synthorg/**/*.py: Every module with business logic MUST have: from synthorg.observability import get_logger then logger = get_logger(name)
Never use import logging / logging.getLogger() / print() in application code. Exception: observability/setup.py, observability/sinks.py, observability/syslog_handler.py, and observability/http_handler.py may use stdlib logging and print(..., file=sys.stderr).
Event names: always use constants from domain-specific modules under synthorg.observability.events (e.g., API_REQUEST_STARTED from events.api). Import directly: from synthorg.observability.events. import EVENT_CONSTANT
Structured kwargs: always logger.info(EVENT, key=value)—never logger.info("msg %s", val)
All provider calls go through BaseCompletionProvider which applies retry + rate limiting automatically. Never implement retry logic in driver subclasses or calling code.
RetryConfig and RateLimiterConfig are set per-provider in ProviderConfig.
Retryable errors (is_retryable=True): RateLimitError, ProviderTimeoutError, ProviderConnectionError, ProviderInternalError.
Non-retryable errors raise immediately without retry.
RetryExhaustedError signals that all retries failed—the engine layer catches this to trigger fallback chains.
Rate limiter respects RateLimitError.retry_after from providers—automatically pauses future requests.

Files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/terminal/config.py
  • src/synthorg/tools/database/config.py
  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/database/schema_inspect.py
  • src/synthorg/tools/database/sql_query.py
  • src/synthorg/tools/web/__init__.py
  • src/synthorg/tools/web/http_request.py
  • src/synthorg/tools/factory.py
  • src/synthorg/tools/terminal/shell_command.py
  • src/synthorg/tools/web/web_search.py
  • src/synthorg/tools/sub_constraint_enforcer.py
  • src/synthorg/tools/network_validator.py
src/**/*.py

⚙️ CodeRabbit configuration file

This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.

Files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/terminal/config.py
  • src/synthorg/tools/database/config.py
  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/database/schema_inspect.py
  • src/synthorg/tools/database/sql_query.py
  • src/synthorg/tools/web/__init__.py
  • src/synthorg/tools/web/http_request.py
  • src/synthorg/tools/factory.py
  • src/synthorg/tools/terminal/shell_command.py
  • src/synthorg/tools/web/web_search.py
  • src/synthorg/tools/sub_constraint_enforcer.py
  • src/synthorg/tools/network_validator.py
🧠 Learnings (47)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Security: SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies: disabled/weighted/per-category/milestone), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/security/**/*.py : Security package (security/): SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T20:42:41.219Z
Learning: Dependency review: license allow-list (permissive + weak-copyleft), per-package GPL exemptions for dev-only tool deps, PR comment summaries.
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 BaseModel, model_validator, computed_field, ConfigDict.

Applied to files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. For derived values use `computed_field` instead of storing + validating redundant fields. Use `NotBlankStr` (from `core.types`) for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.

Applied to files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T18:42:17.990Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:42:17.990Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`

Applied to files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (via `model_copy(update=...)`) for runtime state that evolves

Applied to files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/terminal/config.py
  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.

Applied to files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/terminal/config.py
  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state

Applied to files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/terminal/config.py
  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 with adopted conventions: use computed_field for derived values instead of storing + validating redundant fields; use NotBlankStr from core.types for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators.

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/security/**/*.py : Security package (security/): SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume)

Applied to files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/terminal/config.py
  • src/synthorg/tools/sub_constraints.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/security/**/*.py : Security module includes SecOps agent, rule engine (soft-allow/hard-deny), audit log, output scanner, risk classifier, autonomy levels (4 strategies), timeout policies.

Applied to files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/sub_constraints.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/**/*.py : Set `RetryConfig` and `RateLimiterConfig` per-provider in `ProviderConfig`.

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-04-06T20:42:41.219Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T20:42:41.219Z
Learning: Applies to src/synthorg/**/*.py : RetryConfig and RateLimiterConfig are set per-provider in ProviderConfig.

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...

Applied to files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/web/__init__.py
  • src/synthorg/tools/factory.py
📚 Learning: 2026-03-31T21:07:37.469Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T21:07:37.469Z
Learning: Applies to src/synthorg/providers/**/*.py : Set `RetryConfig` and `RateLimiterConfig` per-provider in `ProviderConfig`

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to src/synthorg/providers/**/*.py : Providers: LLM provider abstraction (LiteLLM adapter), auth types (api_key/oauth/custom_header/none), presets (PROVIDER_PRESETS), runtime CRUD (ProviderManagementService with asyncio.Lock serialization), hot-reload via AppState swap.

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-16T19:13:36.562Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T19:13:36.562Z
Learning: Applies to src/synthorg/providers/**/*.py : RetryConfig and RateLimiterConfig are set per-provider in ProviderConfig.

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for `dict`/`list` fields

Applied to files:

  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-01T09:58:27.410Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:58:27.410Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for dict/list fields in frozen Pydantic models

Applied to files:

  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : For non-Pydantic internal collections (registries, `BaseTool`), use `copy.deepcopy()` at construction and wrap with `MappingProxyType` for read-only enforcement

Applied to files:

  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).

Applied to files:

  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-01T09:09:43.948Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:09:43.948Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at construction and `MappingProxyType` wrapping for read-only enforcement in non-Pydantic internal collections (registries, BaseTool)

Applied to files:

  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Immutability: create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.

Applied to files:

  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-06T20:42:41.219Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T20:42:41.219Z
Learning: Applies to **/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).

Applied to files:

  • src/synthorg/tools/invoker.py
  • tests/unit/tools/test_sub_constraint_enforcer.py
  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-06T20:42:41.219Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T20:42:41.219Z
Learning: Applies to **/*.py : Create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.

Applied to files:

  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to tests/**/*.py : Test markers: pytest.mark.unit, pytest.mark.integration, pytest.mark.e2e, pytest.mark.slow. Coverage: 80% minimum (enforced in CI).

Applied to files:

  • tests/unit/tools/database/test_sql_query.py
📚 Learning: 2026-04-02T09:04:54.709Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T09:04:54.709Z
Learning: Applies to tests/**/*.py : Use pytest markers: `pytest.mark.unit`, `pytest.mark.integration`, `pytest.mark.e2e`, `pytest.mark.slow`

Applied to files:

  • tests/unit/tools/database/test_sql_query.py
📚 Learning: 2026-04-06T20:42:41.219Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T20:42:41.219Z
Learning: Applies to tests/**/*.py : Markers: pytest.mark.unit, pytest.mark.integration, pytest.mark.e2e, pytest.mark.slow.

Applied to files:

  • tests/unit/tools/database/test_sql_query.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to tests/**/*.py : Test markers: `pytest.mark.unit`, `pytest.mark.integration`, `pytest.mark.e2e`, `pytest.mark.slow`. Coverage: 80% minimum. Async: `asyncio_mode = 'auto'` — no manual `pytest.mark.asyncio` needed. Timeout: 30 seconds per test. Parallelism: `pytest-xdist` via `-n auto` — ALWAYS include `-n auto` when running pytest, never run tests sequentially.

Applied to files:

  • tests/unit/tools/database/test_sql_query.py
📚 Learning: 2026-04-01T09:37:49.451Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:37:49.451Z
Learning: Applies to **/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models with `model_copy(update=...)` for runtime state that evolves

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-04-02T11:49:21.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T11:49:21.202Z
Learning: Applies to **/*.py : Use Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`) with `allow_inf_nan=False` in all `ConfigDict` declarations

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-04-01T15:36:39.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T15:36:39.993Z
Learning: Applies to src/**/*.py : Use Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`). Use `allow_inf_nan=False` in all `ConfigDict` declarations to reject `NaN`/`Inf` in numeric fields at validation time

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to **/*.py : Config vs runtime state: use frozen Pydantic models for config/identity; separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state. Never mix static config fields with mutable runtime fields in one model.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-26T15:18:16.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-26T15:18:16.848Z
Learning: Applies to src/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. Use `computed_field` for derived values instead of storing redundant fields. Use `NotBlankStr` for all identifier/name fields.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-17T11:41:02.964Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T11:41:02.964Z
Learning: Applies to src/**/*.py : Models: Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`). Use `computed_field` for derived values instead of storing + validating redundant fields. Use `NotBlankStr` for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Handle errors explicitly, never silently swallow. Validate at system boundaries (user input, external APIs, config files).

Applied to files:

  • src/synthorg/tools/database/schema_inspect.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Handle errors explicitly—never silently swallow exceptions.

Applied to files:

  • src/synthorg/tools/database/schema_inspect.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Handle errors explicitly, never silently swallow exceptions

Applied to files:

  • src/synthorg/tools/database/schema_inspect.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Handle errors explicitly; never silently swallow exceptions

Applied to files:

  • src/synthorg/tools/database/schema_inspect.py
📚 Learning: 2026-03-16T20:14:00.937Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T20:14:00.937Z
Learning: Applies to **/*.py : Validate: at system boundaries (user input, external APIs, config files).

Applied to files:

  • tests/unit/tools/test_network_validator.py
📚 Learning: 2026-03-19T07:13:44.964Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:13:44.964Z
Learning: Applies to **/*.py : Validate at system boundaries (user input, external APIs, config files)

Applied to files:

  • tests/unit/tools/test_network_validator.py
📚 Learning: 2026-04-06T20:42:41.219Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T20:42:41.219Z
Learning: Applies to **/*.py : Validate at system boundaries (user input, external APIs, config files).

Applied to files:

  • tests/unit/tools/test_network_validator.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers

Applied to files:

  • src/synthorg/tools/web/__init__.py
📚 Learning: 2026-04-06T20:42:41.219Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T20:42:41.219Z
Learning: Version locations: pyproject.toml ([tool.commitizen].version), src/synthorg/__init__.py (__version__).

Applied to files:

  • src/synthorg/tools/web/__init__.py
📚 Learning: 2026-04-02T07:18:02.381Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T07:18:02.381Z
Learning: Applies to {pyproject.toml,src/synthorg/__init__.py} : Update version in `pyproject.toml` (`[tool.commitizen].version`) and `src/synthorg/__init__.py` (`__version__`)

Applied to files:

  • src/synthorg/tools/web/__init__.py
📚 Learning: 2026-04-01T06:12:03.047Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T06:12:03.047Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Line length: 88 characters (enforced by ruff)

Applied to files:

  • src/synthorg/tools/web/__init__.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to docker/Dockerfile.sandbox : Docker sandbox: `synthorg-sandbox` — Python 3.14 + Node.js + git, non-root (UID 10001), agent code execution sandbox

Applied to files:

  • src/synthorg/tools/terminal/shell_command.py
🔇 Additional comments (10)
tests/unit/tools/test_sub_constraint_enforcer.py (1)

288-301: Good precedence coverage.

This locks in the short-circuit order, so future constraint additions cannot silently change which policy wins.

src/synthorg/tools/terminal/config.py (2)

27-45: Blocklist uses substring matching with known bypass potential.

The default blocklist patterns are vulnerable to minor variations as previously noted. For example, "rm -rf /" won't match rm -rf / (extra whitespace) or rm -r -f / (separated flags). The current implementation in base_terminal_tool.py uses simple substring matching.

However, the docstring clearly documents the matching behavior, and the sandbox backend provides the primary security boundary. This blocklist serves as defense-in-depth rather than the sole protection layer.


8-56: Well-structured frozen config model with sensible defaults.

The TerminalConfig model follows project conventions:

  • frozen=True and allow_inf_nan=False in ConfigDict
  • Appropriate Field constraints (gt=0, le=600.0)
  • Clear attribute documentation
  • Reasonable defaults (1 MiB output limit, 30s timeout with 10-minute max)
src/synthorg/tools/web/web_search.py (1)

102-124: Constructor properly uses copy.deepcopy for schema immutability.

The parameters_schema=copy.deepcopy(_PARAMETERS_SCHEMA) at line 120 correctly addresses the past review concern about shallow copying. The nested dicts are now properly isolated.

tests/integration/tools/test_factory_integration.py (1)

15-15: Tool count correctly updated for new default tools.

The increase from 11 to 14 accounts for the three new tools added by default: http_request, html_parser, and shell_command. This aligns with the factory behavior where web_search requires an explicit provider and database tools require DatabaseConfig.

tests/unit/tools/test_factory.py (1)

19-34: Expected tool names correctly updated with alphabetical ordering.

The new tools (html_parser, http_request, shell_command) are correctly positioned in the alphabetically sorted tuple, and the docstring at line 45 accurately reflects the new count of 14.

src/synthorg/tools/web/__init__.py (1)

1-21: Clean package re-exports with complete __all__ definition.

The package init properly exposes the web tools public API. The __all__ list matches all imports, providing a clear contract for consumers.

src/synthorg/config/schema.py (1)

714-725: New tool configuration fields correctly added to RootConfig.

The three new optional fields follow established patterns in the model. The distinction in default behavior is correctly documented:

  • web and terminal: None = use default configuration (tools enabled)
  • database: None = no database tools (explicit opt-in required)

This aligns with the factory behavior and security posture (database access requires explicit configuration).

tests/unit/tools/database/test_sql_query.py (1)

42-158: Comprehensive test coverage for SqlQueryTool.

The test suite covers:

  • Statement classification including edge cases (comments, whitespace)
  • Read-only enforcement with parametrized write operations
  • Write mode behavior
  • Query execution including empty queries, no results, parameterized queries, and invalid SQL

Good use of @pytest.mark.parametrize for the write-blocked tests.

tests/unit/tools/web/test_html_parser.py (1)

1-119: Test suite structure and fixture are correct.

The test file is well-organized with comprehensive coverage across all three extraction modes (text, links, metadata), edge cases, and default behavior. All tests are properly marked with @pytest.mark.unit, and the html_tool fixture is correctly defined in tests/unit/tools/web/conftest.py.

Comment on lines +120 to +142
def _build_database_tools(
*,
config: DatabaseConfig,
) -> tuple[BaseTool, ...]:
"""Instantiate the built-in database tools for each configured connection."""
from synthorg.tools.database import SchemaInspectTool, SqlQueryTool # noqa: PLC0415

if not config.connections:
return ()

# Use the default connection, or first available
conn_name = config.default_connection
conn_config: DatabaseConnectionConfig | None = config.connections.get(conn_name)
if conn_config is None and config.connections:
conn_name = next(iter(config.connections))
conn_config = config.connections[conn_name]
if conn_config is None:
return ()

return (
SqlQueryTool(config=conn_config),
SchemaInspectTool(config=conn_config),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Non-default database connections are silently ignored.

DatabaseConfig.connections is a mapping, but the factory only ever builds one SqlQueryTool/SchemaInspectTool pair for default_connection (or the first entry). Any additional configured connection is unreachable from the tool surface. Either emit one tool per connection or add a connection argument to the database tools.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/factory.py` around lines 120 - 142, The factory
_build_database_tools currently builds tools only for
DatabaseConfig.default_connection (or the first entry), ignoring other entries
in DatabaseConfig.connections; update it to iterate over all config.connections
items and instantiate a SqlQueryTool and SchemaInspectTool for each connection
(using each connection's DatabaseConnectionConfig) so every configured
connection is exposed as tools; ensure returned tuple includes one pair per
connection (optionally include the connection name in tool construction or
wrapping if the tools support/need it) rather than only the default.

Comment on lines +162 to +180
if working_dir:
cwd = Path(working_dir)
if cwd.is_absolute():
return ToolExecutionResult(
content=(
f"Absolute working_directory not allowed: {working_dir!r}. "
"Use a path relative to the workspace."
),
is_error=True,
)
else:
cwd = None

try:
result = await self._sandbox.execute(
command="bash",
args=("-c", command),
cwd=cwd,
timeout=timeout,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -t f . src/synthorg/tools/sandbox src/synthorg/tools/terminal \
  | xargs -r rg -n -C3 '\bcwd\b|\bworkspace\b|\bresolve\(\)|\bis_relative_to\b|\bdef execute\b'

Repository: Aureliolo/synthorg

Length of output: 32301


🏁 Script executed:

# Find where ShellCommandTool gets initialized and what context it has access to
rg -n "class ShellCommandTool" src/synthorg/tools/terminal/shell_command.py

Repository: Aureliolo/synthorg

Length of output: 106


🏁 Script executed:

# Check SubprocessSandboxConfig and DockerSandboxConfig definitions
fd -t f . src/synthorg/tools/sandbox -name "*.py" | xargs rg -n "class.*SandboxConfig|workspace_only.*=" | head -30

Repository: Aureliolo/synthorg

Length of output: 1334


🏁 Script executed:

# Check if shell_command.py has any workspace context or sandbox config access
rg -n "workspace|_sandbox" src/synthorg/tools/terminal/shell_command.py | head -20

Repository: Aureliolo/synthorg

Length of output: 618


🏁 Script executed:

# Check if DockerSandboxConfig also has workspace_only
rg -A5 "class DockerSandboxConfig" src/synthorg/tools/sandbox/docker_config.py

Repository: Aureliolo/synthorg

Length of output: 274


🏁 Script executed:

# Check the BaseTerminalTool or ShellCommandTool constructor to see what context is available
rg -B5 -A15 "class ShellCommandTool\|def __init__" src/synthorg/tools/terminal/shell_command.py | head -50

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

# Check if sandbox has workspace property exposed
rg -n "workspace" src/synthorg/tools/sandbox/protocol.py

Repository: Aureliolo/synthorg

Length of output: 282


🏁 Script executed:

# Check the SandboxBackend protocol or actual implementations for workspace property
rg -B2 -A2 "def workspace|property.*workspace" src/synthorg/tools/sandbox/

Repository: Aureliolo/synthorg

Length of output: 772


🏁 Script executed:

# Check if ShellCommandTool can access sandbox.workspace
cat -n src/synthorg/tools/terminal/shell_command.py | sed -n '140,185p'

Repository: Aureliolo/synthorg

Length of output: 1915


🏁 Script executed:

# Verify the actual validation happening in subprocess_sandbox.py _validate_cwd
cat -n src/synthorg/tools/sandbox/subprocess_sandbox.py | sed -n '321,342p'

Repository: Aureliolo/synthorg

Length of output: 994


🏁 Script executed:

# Verify what happens when we pass a relative path with .. to the subprocess sandbox
# Check if resolve() properly handles relative paths
python3 << 'EOF'
from pathlib import Path
import tempfile

# Simulate workspace and user input
with tempfile.TemporaryDirectory() as tmpdir:
    workspace = Path(tmpdir)
    
    # Test case 1: Simple relative path (should be allowed)
    user_input = "subdir"
    cwd = Path(user_input)
    try:
        rel = cwd.resolve().relative_to(workspace.resolve())
        print(f"✓ '{user_input}' resolves to: {rel} (ALLOWED)")
    except ValueError as e:
        print(f"✗ '{user_input}' rejected: {e}")
    
    # Test case 2: Path with .. that escapes (should be rejected)
    user_input = "../../../../etc"
    cwd = Path(user_input)
    try:
        rel = cwd.resolve().relative_to(workspace.resolve())
        print(f"✓ '{user_input}' resolves to: {rel} (ALLOWED)")
    except ValueError as e:
        print(f"✗ '{user_input}' rejected: CORRECTLY CAUGHT")
    
    # Test case 3: Check what resolve() returns for relative paths without workspace context
    user_input = "../../.."
    cwd = Path(user_input)
    print(f"Relative path '{user_input}' resolves to absolute: {cwd.resolve()}")
EOF

Repository: Aureliolo/synthorg

Length of output: 301


🏁 Script executed:

# Check if DockerSandboxConfig inherits workspace_only or has its own default
cat -n src/synthorg/tools/sandbox/docker_config.py | head -50

Repository: Aureliolo/synthorg

Length of output: 2325


🏁 Script executed:

# Check if DockerSandboxConfig has workspace_only setting
rg -n "workspace_only" src/synthorg/tools/sandbox/docker_config.py

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

# Look for test cases that verify .. escapes are blocked
rg -B3 -A3 "\.\./\.\." tests/unit/tools/ tests/integration/tools/

Repository: Aureliolo/synthorg

Length of output: 6681


🏁 Script executed:

# Verify the actual flow: does shell_command pass unresolved Path or should it resolve first?
# Check how sandbox.execute is supposed to work with relative paths
rg -B5 -A10 "def execute" src/synthorg/tools/sandbox/protocol.py

Repository: Aureliolo/synthorg

Length of output: 575


🏁 Script executed:

# Check how GitStatusTool and other file system tools validate paths at the boundary
rg -B5 -A15 "escapes workspace" src/synthorg/tools/

Repository: Aureliolo/synthorg

Length of output: 1851


🏁 Script executed:

# Check if there are any tests for shell_command.py path traversal
rg -n "ShellCommandTool\|test_shell_command\|working_dir" tests/unit/tools/terminal/ tests/integration/tools/terminal/

Repository: Aureliolo/synthorg

Length of output: 135


🏁 Script executed:

# Check PathValidator - how do other tools do validation?
rg -B10 -A10 "class PathValidator" src/synthorg/tools/

Repository: Aureliolo/synthorg

Length of output: 1592


🏁 Script executed:

# Check the full PathValidator.validate method to see the exact pattern
cat -n src/synthorg/tools/file_system/_path_validator.py | sed -n '1,80p'

Repository: Aureliolo/synthorg

Length of output: 3311


🏁 Script executed:

# Verify the exact validation logic used
rg -B3 -A8 "def validate\(" src/synthorg/tools/file_system/_path_validator.py

Repository: Aureliolo/synthorg

Length of output: 444


🏁 Script executed:

# Check if workspace is accessible in ShellCommandTool
rg -B5 -A5 "def __init__|self._sandbox|self\._workspace" src/synthorg/tools/terminal/shell_command.py

Repository: Aureliolo/synthorg

Length of output: 1392


Validate working_directory against workspace boundary at the tool boundary.

This only rejects absolute paths. Relative paths like ../../.. bypass the check and are forwarded to the sandbox, making containment dependent on backend-specific configuration that may not be enforced consistently (e.g., DockerSandbox has no workspace_only flag).

Add path validation at this system boundary following the pattern used by file system tools: check that the resolved path remains within the workspace root using is_relative_to(), not just rejection of absolute paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/terminal/shell_command.py` around lines 162 - 180, The
code currently only rejects absolute working_directory values but lets relative
paths like "../../" through; update the validation where working_dir is
converted to cwd (the Path(working_dir) block) to resolve the path against the
workspace root and ensure it stays inside the workspace before calling
self._sandbox.execute. Concretely, compute resolved = (workspace_root /
working_dir).resolve() and compare to workspace_root.resolve() using
resolved.is_relative_to(workspace_root.resolve()) (or equivalent containment
check) and return a ToolExecutionResult with is_error=True and a clear message
if the resolved path is outside the workspace; keep the existing error for
absolute paths, and then pass the validated cwd into self._sandbox.execute.

Comment on lines +144 to +151
try:
results = await self._provider.search(query, max_results)
except Exception as exc:
logger.warning(WEB_SEARCH_FAILED, query=query, error=str(exc))
return ToolExecutionResult(
content=f"Web search failed: {exc}",
is_error=True,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider narrowing the exception type for provider errors.

The bare except Exception catch is broad. If the WebSearchProvider protocol implementations can raise specific exceptions, consider catching those explicitly or at minimum re-raising system errors.

♻️ Suggested refinement
         try:
             results = await self._provider.search(query, max_results)
-        except Exception as exc:
+        except builtins.MemoryError, RecursionError:
+            raise
+        except Exception as exc:
             logger.warning(WEB_SEARCH_FAILED, query=query, error=str(exc))
             return ToolExecutionResult(
                 content=f"Web search failed: {exc}",
                 is_error=True,
             )

Per project conventions, system errors like MemoryError and RecursionError should propagate rather than being swallowed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/web/web_search.py` around lines 144 - 151, The current
broad "except Exception" around the call to self._provider.search in the web
search routine swallows system errors; change it to explicitly handle
provider-level errors and re-raise system-level exceptions. Replace the bare
except with either an except catching the provider-specific exception type(s)
raised by WebSearchProvider.search (e.g., ProviderError) or, if unknown, keep
except Exception as exc but immediately re-raise for critical system exceptions
(MemoryError, RecursionError, KeyboardInterrupt, SystemExit) before logging and
returning the ToolExecutionResult; reference self._provider.search,
WEB_SEARCH_FAILED, logger.warning and ToolExecutionResult when making the
change.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/synthorg/tools/database/config.py (2)

49-52: ⚠️ Potential issue | 🟡 Minor

Use NotBlankStr for connection-name keys.

connections currently accepts blank/whitespace keys (dict[str, ...]). Line 49 should constrain keys to NotBlankStr so invalid identifiers are rejected during config validation.

Suggested fix
-    connections: dict[str, DatabaseConnectionConfig] = Field(
+    connections: dict[NotBlankStr, DatabaseConnectionConfig] = Field(
         default_factory=dict,
         description="Named database connections",
     )

As per coding guidelines "Use NotBlankStr (from core.types) for all identifier/name fields—including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants—instead of manual whitespace validators."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/database/config.py` around lines 49 - 52, Change the type
for the connections mapping to use NotBlankStr for keys so blank/whitespace
connection names are rejected: replace dict[str, DatabaseConnectionConfig] with
dict[NotBlankStr, DatabaseConnectionConfig] on the connections Field (and
add/import NotBlankStr from core.types at top of the module) to ensure
validation uses the canonical identifier type.

58-70: ⚠️ Potential issue | 🟠 Major

Fail fast when default_connection is not configured.

Line 65 only logs a warning, but downstream tool construction can still hard-fail with KeyError when indexing config.connections[selected_connection] (see src/synthorg/tools/factory.py:120-140). This should be rejected at model validation time instead of deferred.

Suggested fix
     `@model_validator`(mode="after")
     def _validate_default_connection(self) -> Self:
-        """Warn when default_connection is missing from connections.
-
-        Does not raise -- the tool factory falls back to the first
-        available connection when the default is not found.
-        """
+        """Validate that default_connection exists in connections."""
         if self.connections and self.default_connection not in self.connections:
-            logger.warning(
-                "database_config_default_missing",
-                default_connection=self.default_connection,
-                available=sorted(self.connections),
-            )
+            logger.error(
+                "database_config_default_missing",
+                default_connection=self.default_connection,
+                available=sorted(self.connections),
+            )
+            raise ValueError(
+                "default_connection must match one of the configured connections"
+            )
         return self

As per coding guidelines "Validate at system boundaries (user input, external APIs, config files)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/database/config.py` around lines 58 - 70, The current
_validate_default_connection (decorated with `@model_validator`(mode="after"))
only logs a warning when default_connection is missing from connections, but
this allows downstream KeyError; change the validator to reject the model by
raising a validation error (e.g., ValueError or pydantic.ValidationError) when
self.connections is set and self.default_connection is not present, including
the invalid default_connection and available connections in the error message so
model construction fails fast; update any tests/consumers expecting a warning
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/tools/database/config.py`:
- Around line 66-67: Replace the literal event name
"database_config_default_missing" with the domain event constant (e.g.,
DATABASE_CONFIG_DEFAULT_MISSING) from synthorg.observability.events.database:
import the constant at top of src/synthorg/tools/database/config.py and pass
that constant to logger.warning in place of the raw string; ensure the import
uses the exact constant name (DATABASE_CONFIG_DEFAULT_MISSING) and update the
logger call in the same function where logger.warning currently uses the
literal.

---

Duplicate comments:
In `@src/synthorg/tools/database/config.py`:
- Around line 49-52: Change the type for the connections mapping to use
NotBlankStr for keys so blank/whitespace connection names are rejected: replace
dict[str, DatabaseConnectionConfig] with dict[NotBlankStr,
DatabaseConnectionConfig] on the connections Field (and add/import NotBlankStr
from core.types at top of the module) to ensure validation uses the canonical
identifier type.
- Around line 58-70: The current _validate_default_connection (decorated with
`@model_validator`(mode="after")) only logs a warning when default_connection is
missing from connections, but this allows downstream KeyError; change the
validator to reject the model by raising a validation error (e.g., ValueError or
pydantic.ValidationError) when self.connections is set and
self.default_connection is not present, including the invalid default_connection
and available connections in the error message so model construction fails fast;
update any tests/consumers expecting a warning accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 62e87347-5d5a-42a1-9010-3a73d8d36d68

📥 Commits

Reviewing files that changed from the base of the PR and between 396eead and 5f9939a.

📒 Files selected for processing (1)
  • src/synthorg/tools/database/config.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations—Python 3.14 has PEP 649 native lazy annotations.
Use PEP 758 except syntax: except A, B: (no parentheses)—ruff enforces this on Python 3.14.
Type hints: all public functions required, mypy strict mode enforced.
Docstrings: Google style, required on public classes/functions (enforced by ruff D rules).
Create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.
For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Use allow_inf_nan=False in all ConfigDict declarations to reject NaN/Inf in numeric fields at validation time.
Use @computed_field for derived values instead of storing + validating redundant fields (e.g. TokenUsage.total_tokens).
Use NotBlankStr (from core.types) for all identifier/name fields—including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants—instead of manual whitespace validators.
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.
Line length: 88 characters (ruff enforced).
Functions: < 50 lines, files < 800 lines.
Handle errors explicitly, never silently swallow.
Validate at system boundaries (user input, external APIs, config files).
Variable name: always logger (not _logger, not log).
All error paths must log at WARNING or ERROR with context before raising.
All state transitions must log at INFO.
DEBUG for object creation, internal flow, entry/exit of key functions.
Pure data models, enums, and re-exports do NOT need logging.

Files:

  • src/synthorg/tools/database/config.py
src/synthorg/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/synthorg/**/*.py: Every module with business logic MUST have: from synthorg.observability import get_logger then logger = get_logger(name)
Never use import logging / logging.getLogger() / print() in application code. Exception: observability/setup.py, observability/sinks.py, observability/syslog_handler.py, and observability/http_handler.py may use stdlib logging and print(..., file=sys.stderr).
Event names: always use constants from domain-specific modules under synthorg.observability.events (e.g., API_REQUEST_STARTED from events.api). Import directly: from synthorg.observability.events. import EVENT_CONSTANT
Structured kwargs: always logger.info(EVENT, key=value)—never logger.info("msg %s", val)
All provider calls go through BaseCompletionProvider which applies retry + rate limiting automatically. Never implement retry logic in driver subclasses or calling code.
RetryConfig and RateLimiterConfig are set per-provider in ProviderConfig.
Retryable errors (is_retryable=True): RateLimitError, ProviderTimeoutError, ProviderConnectionError, ProviderInternalError.
Non-retryable errors raise immediately without retry.
RetryExhaustedError signals that all retries failed—the engine layer catches this to trigger fallback chains.
Rate limiter respects RateLimitError.retry_after from providers—automatically pauses future requests.

Files:

  • src/synthorg/tools/database/config.py
src/**/*.py

⚙️ CodeRabbit configuration file

This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.

Files:

  • src/synthorg/tools/database/config.py
🧠 Learnings (18)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Security: SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies: disabled/weighted/per-category/milestone), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume).
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 BaseModel, model_validator, computed_field, ConfigDict.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (via `model_copy(update=...)`) for runtime state that evolves

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T18:42:17.990Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:42:17.990Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. For derived values use `computed_field` instead of storing + validating redundant fields. Use `NotBlankStr` (from `core.types`) for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-04-02T11:49:21.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T11:49:21.202Z
Learning: Applies to **/*.py : Use Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`) with `allow_inf_nan=False` in all `ConfigDict` declarations

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-04-01T09:37:49.451Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:37:49.451Z
Learning: Applies to **/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models with `model_copy(update=...)` for runtime state that evolves

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-04-01T15:36:39.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T15:36:39.993Z
Learning: Applies to src/**/*.py : Use Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`). Use `allow_inf_nan=False` in all `ConfigDict` declarations to reject `NaN`/`Inf` in numeric fields at validation time

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-04-06T20:42:41.219Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T20:42:41.219Z
Learning: Applies to **/*.py : Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Use allow_inf_nan=False in all ConfigDict declarations to reject NaN/Inf in numeric fields at validation time.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-26T15:18:16.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-26T15:18:16.848Z
Learning: Applies to src/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. Use `computed_field` for derived values instead of storing redundant fields. Use `NotBlankStr` for all identifier/name fields.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-17T11:41:02.964Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T11:41:02.964Z
Learning: Applies to src/**/*.py : Models: Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`). Use `computed_field` for derived values instead of storing + validating redundant fields. Use `NotBlankStr` for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-04-06T20:42:41.219Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T20:42:41.219Z
Learning: Applies to **/*.py : Use NotBlankStr (from core.types) for all identifier/name fields—including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants—instead of manual whitespace validators.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use `NotBlankStr` from `core.types` for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-31T20:07:03.035Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:07:03.035Z
Learning: Applies to **/*.py : Use `NotBlankStr` (from `core.types`) for all identifier/name fields, including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants, instead of manual whitespace validators

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to **/*.py : Models: Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Use computed_field for derived values instead of storing + validating redundant fields. Use NotBlankStr (from core.types) for all identifier/name fields — including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants — instead of manual whitespace validators.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 with adopted conventions: use computed_field for derived values instead of storing + validating redundant fields; use NotBlankStr from core.types for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators.

Applied to files:

  • src/synthorg/tools/database/config.py

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 7, 2026 06:42 — with GitHub Actions Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/synthorg/tools/invoker.py (1)

210-224: ⚠️ Potential issue | 🟠 Major

Approval-required sub-constraints still behave like hard denies.

The requires_approval=True branch returns an error ToolResult without appending to _pending_escalations. Compare with _check_security() (lines 298-312), which appends an EscalationInfo before returning when the verdict is ESCALATE. Without this tracking, sub-constraint escalations cannot be parked and resumed through the approval flow.

To align with existing security escalation behavior, append to _pending_escalations when violation.requires_approval is True:

Proposed fix
         if violation.requires_approval:
+            from synthorg.engine.approval_gate_models import (  # noqa: PLC0415
+                EscalationInfo,
+            )
+
+            # Generate a unique approval_id for sub-constraint escalations
+            approval_id = f"sub-constraint-{tool_call.id}"
+            self._pending_escalations.append(
+                EscalationInfo(
+                    approval_id=approval_id,
+                    tool_call_id=tool_call.id,
+                    tool_name=tool_call.name,
+                    action_type=tool.action_type,
+                    risk_level=ApprovalRiskLevel.HIGH,
+                    reason=violation.reason,
+                ),
+            )
             logger.warning(
                 TOOL_SECURITY_ESCALATED,
                 tool_call_id=tool_call.id,
                 tool_name=tool_call.name,
                 reason=violation.reason,
+                approval_id=approval_id,
             )
             return ToolResult(
                 tool_call_id=tool_call.id,
                 content=(
                     f"Sub-constraint escalation: {violation.reason}. "
                     "Human approval required."
                 ),
                 is_error=True,
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/invoker.py` around lines 210 - 224, The branch handling
violation.requires_approval currently logs and returns a ToolResult error but
fails to record the escalation for later approval; update the block in
invoker.py so that before returning you append an EscalationInfo to
_pending_escalations (mirroring the behavior in _check_security), constructing
the EscalationInfo with the tool_call (or tool_call.id/name), violation
details/reason and any needed metadata, then return the error ToolResult as
before; ensure you reference TOOL_SECURITY_ESCALATED/logger.warning and use the
same EscalationInfo shape used elsewhere so this sub-constraint escalation can
be parked and resumed via the approval flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/tools/web/http_request.py`:
- Around line 62-67: The schema for the "timeout" property currently allows 0
which in httpx means "no timeout"; update the "timeout" schema (the "timeout"
property object) to either set "minimum" to a positive value (e.g., 1) or add
"exclusiveMinimum": 0 to forbid zero, and/or adjust the "description" to
explicitly state that 0 means "no timeout" if you intend to allow it; modify the
"timeout" object accordingly so callers aren't confused by 0's special meaning.
- Around line 146-153: Duplicate logging occurs because _validate_url(...)
already emits WEB_SSRF_BLOCKED; remove the extra
logger.warning(WEB_SSRF_BLOCKED, ...) call in the SSRF validation block inside
the function that returns the ToolExecutionResult so you only return the error
ToolExecutionResult (content=f"URL blocked: {validation}", is_error=True) and
rely on _validate_url to log the block; keep the rest of the block intact and do
not alter _validate_url itself.

---

Duplicate comments:
In `@src/synthorg/tools/invoker.py`:
- Around line 210-224: The branch handling violation.requires_approval currently
logs and returns a ToolResult error but fails to record the escalation for later
approval; update the block in invoker.py so that before returning you append an
EscalationInfo to _pending_escalations (mirroring the behavior in
_check_security), constructing the EscalationInfo with the tool_call (or
tool_call.id/name), violation details/reason and any needed metadata, then
return the error ToolResult as before; ensure you reference
TOOL_SECURITY_ESCALATED/logger.warning and use the same EscalationInfo shape
used elsewhere so this sub-constraint escalation can be parked and resumed via
the approval flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: db5568da-e808-4b86-ae21-8d1303bcd5a8

📥 Commits

Reviewing files that changed from the base of the PR and between 5f9939a and 1cbce67.

📒 Files selected for processing (4)
  • src/synthorg/tools/factory.py
  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/web/http_request.py
  • tests/unit/tools/database/test_sql_query.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Web
  • GitHub Check: Build Backend
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations—Python 3.14 has PEP 649 native lazy annotations.
Use PEP 758 except syntax: except A, B: (no parentheses)—ruff enforces this on Python 3.14.
Type hints: all public functions required, mypy strict mode enforced.
Docstrings: Google style, required on public classes/functions (enforced by ruff D rules).
Create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.
For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Use allow_inf_nan=False in all ConfigDict declarations to reject NaN/Inf in numeric fields at validation time.
Use @computed_field for derived values instead of storing + validating redundant fields (e.g. TokenUsage.total_tokens).
Use NotBlankStr (from core.types) for all identifier/name fields—including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants—instead of manual whitespace validators.
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.
Line length: 88 characters (ruff enforced).
Functions: < 50 lines, files < 800 lines.
Handle errors explicitly, never silently swallow.
Validate at system boundaries (user input, external APIs, config files).
Variable name: always logger (not _logger, not log).
All error paths must log at WARNING or ERROR with context before raising.
All state transitions must log at INFO.
DEBUG for object creation, internal flow, entry/exit of key functions.
Pure data models, enums, and re-exports do NOT need logging.

Files:

  • tests/unit/tools/database/test_sql_query.py
  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/web/http_request.py
  • src/synthorg/tools/factory.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Markers: @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, @pytest.mark.slow.
Async: asyncio_mode = "auto"—no manual @pytest.mark.asyncio needed.
Timeout: 30 seconds per test (global in pyproject.toml—do not add per-file pytest.mark.timeout(30) markers; non-default overrides like timeout(60) are allowed).
Parametrize: Prefer @pytest.mark.parametrize for testing similar cases.
Tests must use test-provider, test-small-001, etc. (not real vendor names).
Property-based testing: Python uses Hypothesis (@given + @settings).
Hypothesis profiles configured in tests/conftest.py: ci (deterministic, max_examples=10 + derandomize=True), dev (1000 examples), fuzz (10,000 examples, no deadline), extreme (500,000 examples, no deadline). Controlled via HYPOTHESIS_PROFILE env var.
Never skip, dismiss, or ignore flaky tests—always fix them fully and fundamentally. For timing-sensitive tests, mock time.monotonic() and asyncio.sleep() to make them deterministic. For tasks that must block indefinitely until cancelled, use asyncio.Event().wait() instead of asyncio.sleep(large_number).

Files:

  • tests/unit/tools/database/test_sql_query.py

⚙️ CodeRabbit configuration file

Test files do not require Google-style docstrings on classes or functions -- ruff D rules are only enforced on src/. A bare @settings() decorator with no arguments on Hypothesis property tests is a no-op and should not be suggested -- the HYPOTHESIS_PROFILE env var controls example counts via registered profiles, which @given() honors automatically.

Files:

  • tests/unit/tools/database/test_sql_query.py
src/synthorg/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/synthorg/**/*.py: Every module with business logic MUST have: from synthorg.observability import get_logger then logger = get_logger(name)
Never use import logging / logging.getLogger() / print() in application code. Exception: observability/setup.py, observability/sinks.py, observability/syslog_handler.py, and observability/http_handler.py may use stdlib logging and print(..., file=sys.stderr).
Event names: always use constants from domain-specific modules under synthorg.observability.events (e.g., API_REQUEST_STARTED from events.api). Import directly: from synthorg.observability.events. import EVENT_CONSTANT
Structured kwargs: always logger.info(EVENT, key=value)—never logger.info("msg %s", val)
All provider calls go through BaseCompletionProvider which applies retry + rate limiting automatically. Never implement retry logic in driver subclasses or calling code.
RetryConfig and RateLimiterConfig are set per-provider in ProviderConfig.
Retryable errors (is_retryable=True): RateLimitError, ProviderTimeoutError, ProviderConnectionError, ProviderInternalError.
Non-retryable errors raise immediately without retry.
RetryExhaustedError signals that all retries failed—the engine layer catches this to trigger fallback chains.
Rate limiter respects RateLimitError.retry_after from providers—automatically pauses future requests.

Files:

  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/web/http_request.py
  • src/synthorg/tools/factory.py
src/**/*.py

⚙️ CodeRabbit configuration file

This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.

Files:

  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/web/http_request.py
  • src/synthorg/tools/factory.py
🧠 Learnings (18)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/security/**/*.py : Security package (security/): SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume)
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to tests/**/*.py : Test markers: pytest.mark.unit, pytest.mark.integration, pytest.mark.e2e, pytest.mark.slow. Coverage: 80% minimum (enforced in CI).

Applied to files:

  • tests/unit/tools/database/test_sql_query.py
📚 Learning: 2026-04-02T09:04:54.709Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T09:04:54.709Z
Learning: Applies to tests/**/*.py : Use pytest markers: `pytest.mark.unit`, `pytest.mark.integration`, `pytest.mark.e2e`, `pytest.mark.slow`

Applied to files:

  • tests/unit/tools/database/test_sql_query.py
📚 Learning: 2026-04-06T20:42:41.219Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T20:42:41.219Z
Learning: Applies to tests/**/*.py : Markers: pytest.mark.unit, pytest.mark.integration, pytest.mark.e2e, pytest.mark.slow.

Applied to files:

  • tests/unit/tools/database/test_sql_query.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to tests/**/*.py : Test markers: `pytest.mark.unit`, `pytest.mark.integration`, `pytest.mark.e2e`, `pytest.mark.slow`. Coverage: 80% minimum. Async: `asyncio_mode = 'auto'` — no manual `pytest.mark.asyncio` needed. Timeout: 30 seconds per test. Parallelism: `pytest-xdist` via `-n auto` — ALWAYS include `-n auto` when running pytest, never run tests sequentially.

Applied to files:

  • tests/unit/tools/database/test_sql_query.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to tests/**/*.py : Prefer `pytest.mark.parametrize` for testing similar cases.

Applied to files:

  • tests/unit/tools/database/test_sql_query.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/security/**/*.py : Security package (security/): SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume)

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for `dict`/`list` fields

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-04-01T09:58:27.410Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:58:27.410Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for dict/list fields in frozen Pydantic models

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : For non-Pydantic internal collections (registries, `BaseTool`), use `copy.deepcopy()` at construction and wrap with `MappingProxyType` for read-only enforcement

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-04-01T09:09:43.948Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:09:43.948Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at construction and `MappingProxyType` wrapping for read-only enforcement in non-Pydantic internal collections (registries, BaseTool)

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-04-06T20:42:41.219Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T20:42:41.219Z
Learning: Applies to **/*.py : Create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Immutability: create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-04-06T20:42:41.219Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T20:42:41.219Z
Learning: Applies to **/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/api/**/*.py : Authentication uses JWT + API key. Approval gate integration for high-risk operations.

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/security/**/*.py : Security module includes SecOps agent, rule engine (soft-allow/hard-deny), audit log, output scanner, risk classifier, autonomy levels (4 strategies), timeout policies.

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...

Applied to files:

  • src/synthorg/tools/factory.py
🔇 Additional comments (18)
tests/unit/tools/database/test_sql_query.py (5)

1-8: LGTM!

Imports are appropriate. Testing the private _classify_statement helper directly is a reasonable approach for unit testing internal classification logic.


12-35: LGTM!

Comprehensive parameterized test covering all SQL statement types, edge cases with comments, whitespace handling, and empty inputs. Good use of @pytest.mark.parametrize as recommended by guidelines.


41-78: LGTM!

Tests correctly verify application-level read-only enforcement. The test_select_allowed properly sets up the database before querying, and test_write_blocked_in_read_only covers all write statement types with appropriate assertion on the "blocked" message.


84-100: LGTM!

Test correctly verifies that write operations succeed when read_only=False. The assertion for "affected" in the content aligns with expected behavior for successful INSERT statements.


106-157: LGTM!

Execution tests comprehensively cover:

  • Empty query handling (returns error before DB connection)
  • Empty result sets from valid queries
  • Parameterized queries (SQL injection prevention)
  • Invalid SQL error handling

Assertions correctly align with the implementation's error messages per the source code.

src/synthorg/tools/invoker.py (2)

424-427: LGTM!

The integration correctly invokes sub-constraint checking after the permission check and before parameter validation, returning early on any violation. This placement ensures sub-constraints are enforced before any tool execution proceeds.


187-201: LGTM — deepcopy guard applied correctly.

The method now uses _safe_deepcopy_args() to safely copy tool_call.arguments, handling non-copyable values gracefully and emitting TOOL_INVOKE_DEEPCOPY_ERROR on failure. This addresses the earlier concern about unguarded copy.deepcopy() calls.

src/synthorg/tools/factory.py (7)

120-142: Non-default database connections are still silently ignored.

This function only builds tools for a single connection (the default or first available). Additional entries in DatabaseConfig.connections produce no tools and no warning. If this is intentional (e.g., to avoid tool name collisions), consider documenting it in the docstring or emitting a debug log noting skipped connections.


38-52: LGTM! New imports for web, database, and terminal tool support.

TYPE_CHECKING imports properly guard runtime-only type hints.


92-117: LGTM! Web tools builder follows established patterns.

Conditionally including WebSearchTool only when a provider is available is the right approach.


145-153: LGTM! Terminal tools builder is clean and simple.


156-228: LGTM! Extended function properly integrates new tool categories.

The conditional database tool inclusion and the final sorting maintain deterministic behavior.


270-284: LGTM! Sandbox backend resolution refactored correctly.

The is not None check (line 273) properly handles explicit empty mappings, and resolved_backends is computed once and reused for both VERSION_CONTROL and TERMINAL categories, addressing the previous review concerns.


286-302: LGTM! Graceful degradation for missing terminal sandbox.

Catching KeyError and logging a warning while allowing terminal tools to operate without a sandbox is appropriate for resilience.

src/synthorg/tools/web/http_request.py (4)

1-29: LGTM! Module setup follows project conventions.

Imports, logger setup, and event constant usage align with coding guidelines.


74-115: LGTM! Class initialization is well-documented and follows conventions.

The constructor properly copies _PARAMETERS_SCHEMA to avoid mutation, and the docstring includes a usage example.


222-228: Byte-based truncation is correctly implemented.

The code now properly truncates by bytes using response.content and decodes with errors="replace" to handle truncation at UTF-8 boundaries. This addresses the previous review concern about character vs byte counting.

Note: response.content still buffers the entire response before truncation. For very large responses, consider using response.aiter_bytes() to stream and truncate without full buffering. However, with a 1MB default limit, this is likely acceptable.


254-296: LGTM! IPv6 handling and immutability concerns addressed.

The method now correctly brackets IPv6 literals in the netloc (lines 281-288) and returns a copied headers dict (line 290) rather than mutating the caller's input. This addresses both concerns from the previous review.

Comment on lines +62 to +67
"timeout": {
"type": "number",
"description": "Request timeout in seconds",
"minimum": 0,
"maximum": 300,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider setting minimum to a positive value or exclusiveMinimum: 0.

A timeout of exactly 0 in httpx means "no timeout" rather than "instant timeout," which may confuse callers expecting 0 to mean immediate failure. If intentional, add a note in the description.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/web/http_request.py` around lines 62 - 67, The schema for
the "timeout" property currently allows 0 which in httpx means "no timeout";
update the "timeout" schema (the "timeout" property object) to either set
"minimum" to a positive value (e.g., 1) or add "exclusiveMinimum": 0 to forbid
zero, and/or adjust the "description" to explicitly state that 0 means "no
timeout" if you intend to allow it; modify the "timeout" object accordingly so
callers aren't confused by 0's special meaning.

@Aureliolo
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/synthorg/tools/database/config.py (1)

66-69: 🛠️ Refactor suggestion | 🟠 Major

Replace raw event name with the database event constant.

Line 67 still logs a literal ("database_config_default_missing"). Please import and use DATABASE_CONFIG_DEFAULT_MISSING from synthorg.observability.events.database for consistency and centralized event governance.

Proposed diff
 from synthorg.core.types import NotBlankStr  # noqa: TC001
 from synthorg.observability import get_logger
+from synthorg.observability.events.database import DATABASE_CONFIG_DEFAULT_MISSING
@@
         if self.connections and self.default_connection not in self.connections:
             logger.warning(
-                "database_config_default_missing",
+                DATABASE_CONFIG_DEFAULT_MISSING,
                 default_connection=self.default_connection,
                 available=sorted(self.connections),
             )

As per coding guidelines "Always use event name constants from domain-specific modules under synthorg.observability.events."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/database/config.py` around lines 66 - 69, Replace the
literal event name string with the shared constant: add an import for
DATABASE_CONFIG_DEFAULT_MISSING from synthorg.observability.events.database and
use that constant in the logger.warning call (the call referencing
self.default_connection and sorted(self.connections)); ensure the existing
logger.warning parameters remain unchanged and only the event name argument is
replaced with DATABASE_CONFIG_DEFAULT_MISSING.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/tools/invoker.py`:
- Around line 440-443: Update the invoke() docstring step list to reflect the
new ordering where _invoke_single() performs sub-constraint checks before
parameter validation: mention the "sub-constraint" stage immediately prior to
"parameter validation" in the invoke() flow description and ensure any
numbered/ordered steps and examples mirror this new sequence so invoke() and
_invoke_single() stay in sync for runbooks/debugging.

---

Duplicate comments:
In `@src/synthorg/tools/database/config.py`:
- Around line 66-69: Replace the literal event name string with the shared
constant: add an import for DATABASE_CONFIG_DEFAULT_MISSING from
synthorg.observability.events.database and use that constant in the
logger.warning call (the call referencing self.default_connection and
sorted(self.connections)); ensure the existing logger.warning parameters remain
unchanged and only the event name argument is replaced with
DATABASE_CONFIG_DEFAULT_MISSING.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 48d44fff-3462-48f0-952c-7470987eea7b

📥 Commits

Reviewing files that changed from the base of the PR and between 1cbce67 and 972d90f.

📒 Files selected for processing (2)
  • src/synthorg/tools/database/config.py
  • src/synthorg/tools/invoker.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations - Python 3.14 has PEP 649 native lazy annotations
Use except A, B: syntax (no parentheses) for exception handling in Python 3.14 per PEP 758 - ruff enforces this
Use 88-character line length (enforced by ruff) in Python code

Files:

  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/database/config.py
src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.py: Type hints required on all public functions; use mypy strict mode
Docstrings required on all public classes and functions using Google style
Create new objects, never mutate existing ones; for non-Pydantic internal collections, use copy.deepcopy() at construction and MappingProxyType wrapping for read-only enforcement
For dict/list fields in frozen Pydantic models, use copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization)
Use frozen Pydantic models for config/identity; use separate mutable-via-copy models for runtime state that evolves; never mix static config fields with mutable runtime fields in one model
Use Pydantic v2 with BaseModel, model_validator, computed_field, and ConfigDict
Use allow_inf_nan=False in all ConfigDict declarations to reject NaN/Inf in numeric fields at validation time
Use @computed_field for derived values instead of storing and validating redundant fields (e.g., TokenUsage.total_tokens)
Use NotBlankStr (from core.types) for all identifier/name fields - including optional and tuple variants - instead of manual whitespace validators
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g., multiple tool invocations, parallel agent calls); prefer structured concurrency over bare create_task
Functions should be less than 50 lines; files should be less than 800 lines
Handle errors explicitly; never silently swallow exceptions
Validate at system boundaries: user input, external APIs, and config files
Every module with business logic MUST import from synthorg.observability import get_logger and use logger = get_logger(__name__)
Never use import logging / logging.getLogger() / print() in application code; exceptions allowed only in observability/setup.py, observability/sinks.py, observability/syslog_handler.py, and observability/http_handler.py
Use variable name logger (not...

Files:

  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/database/config.py

⚙️ CodeRabbit configuration file

This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.

Files:

  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/database/config.py
🧠 Learnings (38)
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/security/**/*.py : Security package (security/): SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume)

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for `dict`/`list` fields

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-04-01T09:58:27.410Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:58:27.410Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for dict/list fields in frozen Pydantic models

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : For non-Pydantic internal collections (registries, `BaseTool`), use `copy.deepcopy()` at construction and wrap with `MappingProxyType` for read-only enforcement

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-04-01T09:09:43.948Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:09:43.948Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at construction and `MappingProxyType` wrapping for read-only enforcement in non-Pydantic internal collections (registries, BaseTool)

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-04-07T07:27:16.474Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T07:27:16.474Z
Learning: Applies to src/**/*.py : For `dict`/`list` fields in frozen Pydantic models, use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization)

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Immutability: create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-16T23:05:29.577Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T23:05:29.577Z
Learning: Applies to **/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/api/**/*.py : Authentication uses JWT + API key. Approval gate integration for high-risk operations.

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/security/**/*.py : Security module includes SecOps agent, rule engine (soft-allow/hard-deny), audit log, output scanner, risk classifier, autonomy levels (4 strategies), timeout policies.

Applied to files:

  • src/synthorg/tools/invoker.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 BaseModel, model_validator, computed_field, ConfigDict.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (via `model_copy(update=...)`) for runtime state that evolves

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. For derived values use `computed_field` instead of storing + validating redundant fields. Use `NotBlankStr` (from `core.types`) for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T18:42:17.990Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:42:17.990Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-04-07T07:27:16.474Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T07:27:16.474Z
Learning: Applies to src/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models for runtime state that evolves; never mix static config fields with mutable runtime fields in one model

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-04-02T11:49:21.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T11:49:21.202Z
Learning: Applies to **/*.py : Use Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`) with `allow_inf_nan=False` in all `ConfigDict` declarations

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-04-01T15:36:39.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T15:36:39.993Z
Learning: Applies to src/**/*.py : Use Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`). Use `allow_inf_nan=False` in all `ConfigDict` declarations to reject `NaN`/`Inf` in numeric fields at validation time

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-04-01T09:37:49.451Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:37:49.451Z
Learning: Applies to **/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models with `model_copy(update=...)` for runtime state that evolves

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-26T15:18:16.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-26T15:18:16.848Z
Learning: Applies to src/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. Use `computed_field` for derived values instead of storing redundant fields. Use `NotBlankStr` for all identifier/name fields.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-17T11:41:02.964Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T11:41:02.964Z
Learning: Applies to src/**/*.py : Models: Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`). Use `computed_field` for derived values instead of storing + validating redundant fields. Use `NotBlankStr` for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use `NotBlankStr` from `core.types` for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-04-07T07:27:16.474Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T07:27:16.474Z
Learning: Applies to src/**/*.py : Use `NotBlankStr` (from `core.types`) for all identifier/name fields - including optional and tuple variants - instead of manual whitespace validators

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to **/*.py : Models: Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Use computed_field for derived values instead of storing + validating redundant fields. Use NotBlankStr (from core.types) for all identifier/name fields — including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants — instead of manual whitespace validators.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-31T20:07:03.035Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:07:03.035Z
Learning: Applies to **/*.py : Use `NotBlankStr` (from `core.types`) for all identifier/name fields, including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants, instead of manual whitespace validators

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 with adopted conventions: use computed_field for derived values instead of storing + validating redundant fields; use NotBlankStr from core.types for all identifier/name fields (including optional and tuple variants) instead of manual whitespace validators.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-31T16:09:24.320Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T16:09:24.320Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from `synthorg.observability.events.<domain>` modules (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`); import directly and use in structured logging

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to src/synthorg/**/*.py : Always use event name constants from the domain-specific module under `synthorg.observability.events` in logging calls

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-19T11:33:01.580Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to src/synthorg/**/*.py : Use event constants from `synthorg.observability.events.<domain>` (e.g., `API_REQUEST_STARTED` from `events.api`); import directly and log with structured kwargs: `logger.info(EVENT, key=value)`, never interpolated strings

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Always use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`); import directly rather than using string literals

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-04-07T07:27:16.474Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T07:27:16.474Z
Learning: Applies to src/**/*.py : Always use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`)

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-18T21:23:23.586Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T21:23:23.586Z
Learning: Applies to src/synthorg/**/*.py : Event names: always use constants from the domain-specific module under synthorg.observability.events (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool). Import directly from synthorg.observability.events.<domain>.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Always use event name constants from domain-specific modules under `synthorg.observability.events` (e.g., `PROVIDER_CALL_START` from `events.provider`); import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to src/synthorg/**/*.py : Event names: always use constants from domain-specific modules under synthorg.observability.events (e.g., PROVIDER_CALL_START from events.provider, BUDGET_RECORD_ADDED from events.budget, etc.). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`.

Applied to files:

  • src/synthorg/tools/database/config.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use event name constants from synthorg.observability.events domain-specific modules (e.g., PROVIDER_CALL_START from events.provider). Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT.

Applied to files:

  • src/synthorg/tools/database/config.py
🔇 Additional comments (3)
src/synthorg/tools/invoker.py (1)

187-252: Good hardening of sub-constraint checks and escalation tracking.

Using _safe_deepcopy_args() here and recording EscalationInfo before returning closes the unsafe-copy gap and preserves approval context for resume flows.

Based on learnings: Use copy.deepcopy() at system boundaries (tool execution, serialization, delegation, persistence).

src/synthorg/tools/database/config.py (2)

22-56: Good use of strict frozen Pydantic config models.

ConfigDict(frozen=True, allow_inf_nan=False) and NotBlankStr usage here is aligned well for config safety and validation.


49-52: ⚠️ Potential issue | 🟠 Major

Add immutability enforcement to the connections dict field.

frozen=True prevents attribute reassignment but not nested dict mutation. Add a field validator to convert the dict to types.MappingProxyType or change the annotation to Mapping with appropriate validation, following the pattern already used elsewhere in the codebase (e.g., base.py for _parameters_schema).

⛔ Skipped due to learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T23:05:29.577Z
Learning: Applies to **/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T07:27:16.474Z
Learning: Applies to src/**/*.py : For `dict`/`list` fields in frozen Pydantic models, use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:09:43.948Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at construction and `MappingProxyType` wrapping for read-only enforcement in non-Pydantic internal collections (registries, BaseTool)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Immutability: create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.

Aureliolo added 10 commits April 7, 2026 15:21
- Use NotBlankStr for DatabaseConfig.connections keys per coding
  conventions (reject blank/whitespace connection names)
- Track sub-constraint escalations in _pending_escalations so
  requires_approval violations can be parked and resumed through
  the approval flow (aligns with security interceptor behavior)
- Add DB_CONFIG_DEFAULT_MISSING constant to events/database.py
- Replace literal event name string in DatabaseConfig validator
- Update invoke() docstring steps to include sub-constraint check
  (step 3) matching _invoke_single() execution order
- Use copy.deepcopy() for all _PARAMETERS_SCHEMA copies in tool
  constructors (schema_inspect, sql_query, shell_command, http_request,
  html_parser) to prevent shared nested dict mutation
- Fix test_read_and_branch_allows_clone to use VCS_READ (consistent
  with test_local_only_blocks_clone)
Design:
- Document all 6 sub-constraint dimensions in operations.md

Security:
- Block ATTACH/DETACH/VACUUM unconditionally (filesystem escape)
- Resolve DNS for allowlisted hosts (TOCTOU pinning)
- Block git_clone when network=NONE in sub-constraint enforcer
- Re-raise MemoryError/RecursionError in DNS resolver and web search
- Normalize Host header on all request paths (not just pin path)

Code quality:
- Pre-construction allowlist normalization (no frozen model mutation)
- Merge custom sub-constraints with level defaults (not replace)
- Remove duplicate TOOL_PERMISSION_CHECKER_CREATED log emission
- Cap web search results to max_results defensively
- Simplify conftest fixtures (direct Path typing)
- Fix blocklist test to use set membership instead of substring
- Assert both database tools absent in empty-connections test
- Fix requires_approval prefix from 'deployment' to 'deploy' to
  match actual action taxonomy (deploy:staging, deploy:production)
- Remove duplicate WEB_SSRF_BLOCKED log (base_web_tool already logs)
- Return normalized_headers on ValueError path in _pin_url
- Update test for new deploy prefix
- Fix _validate_working_dir return type to include Path
- Import Path at module level in shell_command.py
- Fix aiosqlite cursor.description type mismatch (always non-None
  in types but None at runtime for DML)
- Replace asynccontextmanager mock with proper class for error path
  (avoids unreachable yield in async generator)
The leading comment regex now matches -- comments at EOF (without
trailing newline) by making the newline optional: (?:\n|$).
- Move trailing \s* outside the alternation in _LEADING_COMMENT_RE
  so indentation between consecutive -- comments is consumed
- Snapshot cursor.description and close cursor before db.commit()
  for row-returning write statements (SQLite best practice)
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (4)
tests/unit/tools/web/test_http_request.py (1)

151-162: ⚠️ Potential issue | 🟡 Minor

Use result.error for SSRF rejection assertions.

These cases still inspect result.content, but the SSRF validation path returns its message in the error field. The test is checking the wrong payload here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/tools/web/test_http_request.py` around lines 151 - 162, The tests
assert SSRF rejection messages from HttpRequestTool.execute against
result.content but the SSRF validation path sets the message on result.error;
update both tests (test_private_ip_blocked and test_file_scheme_blocked) to
assert on result.error (e.g., assert result.is_error is True and assert
"blocked" in result.error.lower() / assert "scheme" in result.error.lower())
while leaving the HttpRequestTool.execute invocation and is_error check
unchanged so the assertions target the correct field.
src/synthorg/tools/web/web_search.py (1)

144-182: ⚠️ Potential issue | 🟠 Major

Validate provider results before rendering them.

WebSearchProvider is an external boundary, but this code only getattr()s fields on returned items. Dict results lose their real title/url/snippet, malformed objects still produce a success response, and SearchResult validation is skipped entirely. Normalize the sliced results with SearchResult.model_validate(...) (or equivalent coercion) before formatting.

As per coding guidelines "Validate at system boundaries: user input, external APIs, and config files."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/web/web_search.py` around lines 144 - 182, The code
currently formats provider results directly after calling self._provider.search
in WebSearchTool (or similar) which can accept dicts or malformed objects;
before slicing/formatting, normalize each item using SearchResult.model_validate
(or the project's equivalent coercion/constructor) to ensure title/url/snippet
fields exist and are typed, replacing or skipping invalid entries; validate the
results list returned by self._provider.search, build a new capped list of
validated SearchResult instances (or skip/log any that fail validation), then
use that validated list for the existing logging and ToolExecutionResult
creation so WEB_SEARCH_SUCCESS only reflects well-formed results.
src/synthorg/tools/sub_constraint_enforcer.py (1)

102-128: ⚠️ Potential issue | 🟠 Major

Three declared sub-constraints are still inert in this path.

check() only evaluates network, terminal, git, and requires_approval. file_system, code_execution, and network_allowlist never participate in the allow/deny decision here, and arguments stays unused, so those controls currently have no effect before SUB_CONSTRAINT_ENFORCED is logged. Please add explicit coverage for those dimensions once they are wired in.

As per coding guidelines "Validate at system boundaries: user input, external APIs, and config files."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/sub_constraint_enforcer.py` around lines 102 - 128, The
check method currently only runs _check_network, _check_terminal, _check_git,
and _check_requires_approval, leaving file_system, code_execution, and
network_allowlist inert; update the check(self, ...) implementation to also call
the corresponding validators (e.g., _check_filesystem(tool_name, category,
arguments), _check_code_execution(tool_name, category, arguments) and
_check_network_allowlist(tool_name, category, arguments) or whatever the
existing helper names are), include the returned SubConstraintViolation in the
existing for-tuple so the first non-None violation is returned, and ensure the
unused arguments parameter is passed into those checks for path/argument
validation before logging SUB_CONSTRAINT_ENFORCED.
src/synthorg/tools/network_validator.py (1)

263-272: 🧹 Nitpick | 🔵 Trivial

Exception handling uses isinstance check instead of PEP 758 comma syntax.

Lines 264-265 use isinstance(exc, MemoryError | RecursionError) for the system-error re-raise pattern. This works correctly, but the project convention documented in coding guidelines is except MemoryError, RecursionError: raise. Consider aligning with the convention for consistency, though the current approach is functionally equivalent.

♻️ Align with project convention
-    except Exception as exc:
-        if isinstance(exc, MemoryError | RecursionError):
-            raise
+    except MemoryError, RecursionError:
+        raise
+    except Exception as exc:
         logger.error(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/network_validator.py` around lines 263 - 272, Replace the
current runtime-type check inside the generic except Exception as exc block with
an explicit except clause that re-raises system-level errors: add an earlier
handler except (MemoryError, RecursionError): raise and keep the subsequent
except Exception as exc to perform the logger.error(WEB_DNS_FAILED,
hostname=hostname, reason=..., exc_info=True) and return DNS failure string;
this preserves hostname, logger and WEB_DNS_FAILED usage while aligning with the
project convention.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/config/defaults.py`:
- Around line 48-50: The code in build_default_tools_from_config currently
treats config.web and config.terminal being None as "disable" (skipping
WebSearchTool creation and terminal sandbox setup), which conflicts with the
schema comment that None should mean "use default config"; update
build_default_tools_from_config to treat None as a trigger to instantiate
defaults: when config.web is None call the default web factory (e.g.,
create_default_web_config()) and proceed to add WebSearchTool, and when
config.terminal is None call the default terminal factory (e.g.,
create_default_terminal_config()) and apply the sandbox/settings before adding
terminal tools; ensure references to WebSearchTool creation and terminal sandbox
setup in build_default_tools_from_config use the resulting config object
(defaulted or provided) rather than gating on is not None so defaults are
applied consistently.

In `@src/synthorg/config/schema.py`:
- Around line 714-725: The RootConfig class docstring is out of sync: the newly
added public attributes web, database, and terminal (defined as WebToolsConfig |
None, DatabaseConfig | None, and TerminalConfig | None) are not documented in
the RootConfig attribute list; update the RootConfig class docstring to add
brief entries for web ("Web tool configuration (None = default web config)"),
database ("Database tool configuration (None = no database tools)"), and
terminal ("Terminal tool configuration (None = default terminal config)") so the
class attribute list accurately reflects the fields.

In `@src/synthorg/tools/network_validator.py`:
- Around line 415-418: The allowlist branch currently swallows DNS resolution
failures by converting non-tuple results to an empty tuple, weakening IP-pinning
protection; update the block that calls resolve_and_check(normalized,
policy.dns_resolution_timeout) so that when the result is not a successful tuple
you log a warning (including the hostname normalized and the resolution
error/indicator) before returning _ok(normalized, port, is_https=is_https,
resolved_ips=()); use the existing module logger (or the file's standard logger)
and reference the symbols normalized, policy.hostname_allowlist,
resolve_and_check, and _ok to locate and change the code.

In `@src/synthorg/tools/sub_constraint_enforcer.py`:
- Around line 157-160: The _check_network() logic currently blocks all
ToolCategory.WEB tools when self._constraints.network == NetworkMode.NONE, which
also prevents non-networked tools like HtmlParserTool from running; modify the
check in sub_constraint_enforcer._check_network to test for tools that actually
perform external network actions (e.g., by checking a capability/flag or method
like has_external_network_access or an action enum on the tool instance) instead
of matching on ToolCategory.WEB, and keep the existing external-fetch checks
(using _NETWORK_CATEGORIES and _GIT_CLONE_TOOL_NAMES) for tools that truly
require outbound network access so HtmlParserTool and other purely local
web-parsing tools remain allowed under NetworkMode.NONE.

In `@src/synthorg/tools/sub_constraints.py`:
- Around line 163-170: The requires_approval entry for
ToolAccessLevel.RESTRICTED in ToolSubConstraints uses the prefix "deploy" which
is too broad; update the tuple to use "deploy:" (i.e., change "deploy" to
"deploy:") so that _check_requires_approval() which uses startswith(prefix) only
matches category-style actions like "deploy:staging" and not unrelated names;
locate the ToolSubConstraints initialization for ToolAccessLevel.RESTRICTED in
src/synthorg/tools/sub_constraints.py and update the requires_approval tuple
accordingly.

In `@src/synthorg/tools/terminal/base_terminal_tool.py`:
- Around line 79-84: The warning currently logs the full blocked command
(logger.warning with TERMINAL_COMMAND_BLOCKED and command=command); change these
calls to avoid emitting sensitive arguments by replacing the command=command
field with either the executable name (extract from the command string) or a
redacted fingerprint (e.g., hash or masked string), and include the matched
pattern/reason as-is; update all occurrences of logger.warning using
TERMINAL_COMMAND_BLOCKED (the call at the shown location plus the similar calls
around lines 114-118 and 124-128) to use the executable or fingerprint instead
of the raw command string so logs never contain full rejected command arguments.
- Around line 121-123: The current allowlist loop uses
normalized.startswith(prefix.lower()) which lets entries like "ls" match
"lscpu"; update the check to match command boundaries instead of raw prefixes:
for each prefix in self._config.command_allowlist, treat prefix_lower =
prefix.lower() and accept only if normalized equals prefix_lower or normalized
has prefix_lower followed by whitespace (or end-of-string). Implement this using
a boundary-aware test (e.g., a regex built with re.escape(prefix_lower) anchored
to the start and requiring (\s|$) after the escaped prefix) in the same method
where normalized and self._config.command_allowlist are used.

In `@src/synthorg/tools/web/base_web_tool.py`:
- Around line 41-42: The constructor for the web tool (the __init__ that accepts
request_timeout: float = 30.0) should validate request_timeout before assigning
it to the instance: check that request_timeout is a positive number (> 0) and
raise a ValueError with a clear message if not; update any assignment of
self.request_timeout in the class (e.g., in BaseWebTool.__init__) to occur only
after this validation so invalid timeouts fail fast at construction.

In `@tests/unit/tools/test_factory_new_categories.py`:
- Around line 13-16: The workspace fixture currently returns a non-existent
Path; modify the fixture function named workspace to create the directory before
returning it (e.g., call workspace_path.mkdir(parents=True, exist_ok=True) on
the Path you construct) so tests don't need to call workspace.mkdir()
themselves; keep the fixture signature (def workspace(tmp_path: Path) -> Path)
and return the created path.

---

Duplicate comments:
In `@src/synthorg/tools/network_validator.py`:
- Around line 263-272: Replace the current runtime-type check inside the generic
except Exception as exc block with an explicit except clause that re-raises
system-level errors: add an earlier handler except (MemoryError,
RecursionError): raise and keep the subsequent except Exception as exc to
perform the logger.error(WEB_DNS_FAILED, hostname=hostname, reason=...,
exc_info=True) and return DNS failure string; this preserves hostname, logger
and WEB_DNS_FAILED usage while aligning with the project convention.

In `@src/synthorg/tools/sub_constraint_enforcer.py`:
- Around line 102-128: The check method currently only runs _check_network,
_check_terminal, _check_git, and _check_requires_approval, leaving file_system,
code_execution, and network_allowlist inert; update the check(self, ...)
implementation to also call the corresponding validators (e.g.,
_check_filesystem(tool_name, category, arguments),
_check_code_execution(tool_name, category, arguments) and
_check_network_allowlist(tool_name, category, arguments) or whatever the
existing helper names are), include the returned SubConstraintViolation in the
existing for-tuple so the first non-None violation is returned, and ensure the
unused arguments parameter is passed into those checks for path/argument
validation before logging SUB_CONSTRAINT_ENFORCED.

In `@src/synthorg/tools/web/web_search.py`:
- Around line 144-182: The code currently formats provider results directly
after calling self._provider.search in WebSearchTool (or similar) which can
accept dicts or malformed objects; before slicing/formatting, normalize each
item using SearchResult.model_validate (or the project's equivalent
coercion/constructor) to ensure title/url/snippet fields exist and are typed,
replacing or skipping invalid entries; validate the results list returned by
self._provider.search, build a new capped list of validated SearchResult
instances (or skip/log any that fail validation), then use that validated list
for the existing logging and ToolExecutionResult creation so WEB_SEARCH_SUCCESS
only reflects well-formed results.

In `@tests/unit/tools/web/test_http_request.py`:
- Around line 151-162: The tests assert SSRF rejection messages from
HttpRequestTool.execute against result.content but the SSRF validation path sets
the message on result.error; update both tests (test_private_ip_blocked and
test_file_scheme_blocked) to assert on result.error (e.g., assert
result.is_error is True and assert "blocked" in result.error.lower() / assert
"scheme" in result.error.lower()) while leaving the HttpRequestTool.execute
invocation and is_error check unchanged so the assertions target the correct
field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7be66645-3f62-4f93-8c51-1b80a41d045b

📥 Commits

Reviewing files that changed from the base of the PR and between 47ebf8b and b27f3dc.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (57)
  • CLAUDE.md
  • docs/design/operations.md
  • pyproject.toml
  • src/synthorg/config/defaults.py
  • src/synthorg/config/schema.py
  • src/synthorg/core/agent.py
  • src/synthorg/observability/events/database.py
  • src/synthorg/observability/events/sub_constraint.py
  • src/synthorg/observability/events/terminal.py
  • src/synthorg/observability/events/web.py
  • src/synthorg/tools/__init__.py
  • src/synthorg/tools/database/__init__.py
  • src/synthorg/tools/database/base_db_tool.py
  • src/synthorg/tools/database/config.py
  • src/synthorg/tools/database/schema_inspect.py
  • src/synthorg/tools/database/sql_query.py
  • src/synthorg/tools/factory.py
  • src/synthorg/tools/git_url_validator.py
  • src/synthorg/tools/invoker.py
  • src/synthorg/tools/network_validator.py
  • src/synthorg/tools/permissions.py
  • src/synthorg/tools/sub_constraint_enforcer.py
  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/terminal/__init__.py
  • src/synthorg/tools/terminal/base_terminal_tool.py
  • src/synthorg/tools/terminal/config.py
  • src/synthorg/tools/terminal/shell_command.py
  • src/synthorg/tools/web/__init__.py
  • src/synthorg/tools/web/base_web_tool.py
  • src/synthorg/tools/web/config.py
  • src/synthorg/tools/web/html_parser.py
  • src/synthorg/tools/web/http_request.py
  • src/synthorg/tools/web/web_search.py
  • tests/integration/tools/test_factory_integration.py
  • tests/unit/observability/test_events.py
  • tests/unit/tools/database/__init__.py
  • tests/unit/tools/database/conftest.py
  • tests/unit/tools/database/test_config.py
  • tests/unit/tools/database/test_schema_inspect.py
  • tests/unit/tools/database/test_sql_query.py
  • tests/unit/tools/terminal/__init__.py
  • tests/unit/tools/terminal/conftest.py
  • tests/unit/tools/terminal/test_config.py
  • tests/unit/tools/terminal/test_shell_command.py
  • tests/unit/tools/test_factory.py
  • tests/unit/tools/test_factory_new_categories.py
  • tests/unit/tools/test_factory_sandbox_wiring.py
  • tests/unit/tools/test_invoker_sub_constraints.py
  • tests/unit/tools/test_network_validator.py
  • tests/unit/tools/test_permissions_sub_constraints.py
  • tests/unit/tools/test_sub_constraint_enforcer.py
  • tests/unit/tools/test_sub_constraints.py
  • tests/unit/tools/web/__init__.py
  • tests/unit/tools/web/conftest.py
  • tests/unit/tools/web/test_html_parser.py
  • tests/unit/tools/web/test_http_request.py
  • tests/unit/tools/web/test_web_search.py

Comment on lines +48 to +50
"web": None,
"database": None,
"terminal": None,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how config.web/database/terminal are interpreted by the factory.
rg -n -C4 'build_default_tools_from_config|config\.(web|database|terminal)|is not None|== None' src/synthorg/tools/factory.py

# Confirm new defaults in the config base layer.
rg -n -C2 '"web": None|"database": None|"terminal": None' src/synthorg/config/defaults.py

# Inspect schema defaults for these fields.
rg -n -C4 '\bweb\b|\bdatabase\b|\bterminal\b' src/synthorg/config/schema.py

Repository: Aureliolo/synthorg

Length of output: 4409


Inconsistency between schema documentation and factory implementation for web/terminal defaults.

The schema documents that config.web: None = "default web config" and config.terminal: None = "default terminal config", but build_default_tools_from_config() gates web policy extraction (line 309) and terminal sandbox setup (line 291) on is not None checks. When these are None, WebSearchTool is not added and terminal tools run without configured sandbox/settings. Clarify: should None values trigger a default factory configuration, or is the schema description inaccurate? (Database behavior is intentional per schema: None disables tools.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/config/defaults.py` around lines 48 - 50, The code in
build_default_tools_from_config currently treats config.web and config.terminal
being None as "disable" (skipping WebSearchTool creation and terminal sandbox
setup), which conflicts with the schema comment that None should mean "use
default config"; update build_default_tools_from_config to treat None as a
trigger to instantiate defaults: when config.web is None call the default web
factory (e.g., create_default_web_config()) and proceed to add WebSearchTool,
and when config.terminal is None call the default terminal factory (e.g.,
create_default_terminal_config()) and apply the sandbox/settings before adding
terminal tools; ensure references to WebSearchTool creation and terminal sandbox
setup in build_default_tools_from_config use the resulting config object
(defaulted or provided) rather than gating on is not None so defaults are
applied consistently.

Comment on lines +79 to +84
logger.warning(
TERMINAL_COMMAND_BLOCKED,
command=command,
pattern=pattern,
reason="blocklist_match",
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't log full blocked commands.

These warning events include the raw command string. Rejected commands often carry credentials or other sensitive args, so this leaks secrets into logs on the denial path. Log only the executable / matched rule, or a redacted fingerprint of the original command.

Also applies to: 114-118, 124-128

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/terminal/base_terminal_tool.py` around lines 79 - 84, The
warning currently logs the full blocked command (logger.warning with
TERMINAL_COMMAND_BLOCKED and command=command); change these calls to avoid
emitting sensitive arguments by replacing the command=command field with either
the executable name (extract from the command string) or a redacted fingerprint
(e.g., hash or masked string), and include the matched pattern/reason as-is;
update all occurrences of logger.warning using TERMINAL_COMMAND_BLOCKED (the
call at the shown location plus the similar calls around lines 114-118 and
124-128) to use the executable or fingerprint instead of the raw command string
so logs never contain full rejected command arguments.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/synthorg/tools/sub_constraint_enforcer.py (1)

124-128: ⚠️ Potential issue | 🟠 Major

These checks still leave three policy dimensions unenforced.

check() only runs network/terminal/git/requires_approval, and _check_network() only blocks NetworkMode.NONE. arguments is still unused here, so file_system, code_execution, and network_allowlist never participate in the decision; WORKSPACE_ONLY, ALLOWLIST_ONLY, and container-only policies are effectively declarative in this layer. Please either enforce those dimensions here or thread the relevant path/destination/runtime metadata into the downstream tool backends before returning success.

As per coding guidelines, "Validate at system boundaries: user input, external APIs, and config files".

Also applies to: 146-168

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/sub_constraint_enforcer.py` around lines 124 - 128, The
check() loop only calls _check_network, _check_terminal, _check_git, and
_check_requires_approval and ignores the passed arguments, so policy dimensions
file_system (WORKSPACE_ONLY), code_execution (container-only), and
network_allowlist (ALLOWLIST_ONLY) are not enforced; update check() to either
enforce those policies directly or propagate the needed runtime metadata to
downstream backends: inspect the incoming arguments param and call or augment
checks to validate file_system access (e.g., enforce WORKSPACE_ONLY by ensuring
file paths are within workspace), code_execution constraints (disallow
container-executed actions when not permitted), and network_allowlist (validate
destinations against network_allowlist) before returning success, or include
these fields in the object returned/passed to tool backends so methods like
_check_network/_check_terminal/_check_git receive and enforce network_allowlist,
file_system and code_execution constraints; ensure the functions named
_check_network, _check_terminal, _check_git, and _check_requires_approval are
updated to accept and use the relevant args/metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/tools/sub_constraints.py`:
- Around line 205-207: Update the param docstring for custom_constraints to
accurately describe the actual overlay semantics: state that when provided for a
built-in level the dict is merged/overlaid onto the level defaults (explicitly
set fields in custom_constraints override defaults while unspecified fields are
inherited), and that CUSTOM still requires a full constraints dict; reference
the custom_constraints parameter and the overlay behavior implemented in the
code around lines where defaults are combined (the overlay logic at the block
that applies fields onto level defaults) so the text matches the implementation;
if you intended full replacement instead, change the overlay logic to replace
rather than merge.
- Around line 119-148: The ToolSubConstraints model currently sets model_config
= ConfigDict(frozen=True, allow_inf_nan=False) which lets Pydantic drop unknown
fields; update this ConfigDict to include extra="forbid" so unknown/misspelled
sub-constraint keys are rejected (e.g., change ConfigDict(...) to include
extra="forbid"); adjust any tests or callers that pass through
get_sub_constraints() if they relied on silent ignoring to ensure they provide
valid fields.

In `@src/synthorg/tools/terminal/base_terminal_tool.py`:
- Line 77: Avoid calling command.split() twice: compute the split once into a
local variable (e.g., parts = command.split(maxsplit=1)) and then set executable
= parts[0] if parts else ""; apply the same change to the analogous occurrence
around line 112 (same pattern) so both places in BaseTerminalTool use a single
split call.
- Around line 89-92: The _SHELL_META_RE character-class is missing carriage
return and backslash which can allow obfuscation or line-continuation escapes;
update the regex in base_terminal_tool.py (symbol: _SHELL_META_RE) to include
'\r' and the backslash character inside the character class (properly escaped in
the pattern) so the compiled pattern also matches carriage returns and
backslashes alongside the existing metacharacters.

In `@src/synthorg/tools/web/web_search.py`:
- Around line 171-172: The bare except block labeled "except Exception:  # noqa:
S112  -- skip malformed items" is silently dropping malformed provider items;
update that block to log the malformed item at DEBUG level before continuing
(preserve the continue behavior). Use the module/class logger (e.g.,
logger.debug) and include the exception info (exc_info=True or similar) and the
offending item payload/identifier so operators can inspect faulty provider data
while still skipping it. Ensure the log message is concise and placed inside the
same except block that currently swallows the error.

---

Duplicate comments:
In `@src/synthorg/tools/sub_constraint_enforcer.py`:
- Around line 124-128: The check() loop only calls _check_network,
_check_terminal, _check_git, and _check_requires_approval and ignores the passed
arguments, so policy dimensions file_system (WORKSPACE_ONLY), code_execution
(container-only), and network_allowlist (ALLOWLIST_ONLY) are not enforced;
update check() to either enforce those policies directly or propagate the needed
runtime metadata to downstream backends: inspect the incoming arguments param
and call or augment checks to validate file_system access (e.g., enforce
WORKSPACE_ONLY by ensuring file paths are within workspace), code_execution
constraints (disallow container-executed actions when not permitted), and
network_allowlist (validate destinations against network_allowlist) before
returning success, or include these fields in the object returned/passed to tool
backends so methods like _check_network/_check_terminal/_check_git receive and
enforce network_allowlist, file_system and code_execution constraints; ensure
the functions named _check_network, _check_terminal, _check_git, and
_check_requires_approval are updated to accept and use the relevant
args/metadata.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a16bd451-bda9-4b0c-a0fd-51431cb99857

📥 Commits

Reviewing files that changed from the base of the PR and between b27f3dc and e0330ab.

📒 Files selected for processing (9)
  • src/synthorg/config/schema.py
  • src/synthorg/tools/network_validator.py
  • src/synthorg/tools/sub_constraint_enforcer.py
  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/terminal/base_terminal_tool.py
  • src/synthorg/tools/web/base_web_tool.py
  • src/synthorg/tools/web/web_search.py
  • tests/unit/tools/test_factory_new_categories.py
  • tests/unit/tools/test_sub_constraints.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Socket Security: Pull Request Alerts
  • GitHub Check: Deploy Preview
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations -- Python 3.14 has PEP 649
Use except A, B: syntax (no parentheses) for exception handling -- ruff enforces this on Python 3.14 (PEP 758)
All public functions require type hints; mypy strict mode enforcement
Docstrings must use Google style and are required on all public classes and functions (enforced by ruff D rules)
Use immutability patterns: create new objects, never mutate existing ones. For non-Pydantic internal collections, use copy.deepcopy() at construction plus MappingProxyType wrapping for read-only enforcement
Use frozen Pydantic models for config/identity; use separate mutable-via-copy models for runtime state evolution. Never mix static config fields with mutable runtime fields in one model
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Always use allow_inf_nan=False in ConfigDict; use @computed_field for derived values; use NotBlankStr for all identifier/name fields
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code. Prefer structured concurrency over bare create_task
Maintain line length of 88 characters (enforced by ruff)
Functions must be less than 50 lines; files must be less than 800 lines
Handle errors explicitly; never silently swallow exceptions
Validate at system boundaries: user input, external APIs, and config files
Variable name for logger must always be logger (not _logger, not log)
All Python source files must use ruff for linting and formatting with line length 88 characters

Files:

  • src/synthorg/config/schema.py
  • tests/unit/tools/test_sub_constraints.py
  • src/synthorg/tools/web/base_web_tool.py
  • src/synthorg/tools/terminal/base_terminal_tool.py
  • tests/unit/tools/test_factory_new_categories.py
  • src/synthorg/tools/sub_constraint_enforcer.py
  • src/synthorg/tools/network_validator.py
  • src/synthorg/tools/web/web_search.py
  • src/synthorg/tools/sub_constraints.py
src/synthorg/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/synthorg/**/*.py: Every module with business logic must have: from synthorg.observability import get_logger then logger = get_logger(__name__)
Never use import logging, logging.getLogger(), or print() in application code (exception: observability/setup.py, observability/sinks.py, observability/syslog_handler.py, observability/http_handler.py may use stdlib logging and print for handler/bootstrap code)
Use event name constants from synthorg.observability.events domain-specific modules (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool). Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT
Always use structured kwargs in logging: logger.info(EVENT, key=value) -- never logger.info("msg %s", val)
All error paths must log at WARNING or ERROR with context before raising
All state transitions must log at INFO level
Use DEBUG logging for object creation, internal flow, and entry/exit of key functions
All provider calls go through BaseCompletionProvider which applies retry and rate limiting automatically. Never implement retry logic in driver subclasses or calling code
Maintain 80% minimum test coverage (enforced in CI)
Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, comments, tests, or config examples. Use generic names: example-provider, example-large-001, example-medium-001, example-small-001
All Python code in src/synthorg/ must pass mypy strict mode type-checking
Pure data models, enums, and re-exports in Python do NOT need logging setup

Files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/web/base_web_tool.py
  • src/synthorg/tools/terminal/base_terminal_tool.py
  • src/synthorg/tools/sub_constraint_enforcer.py
  • src/synthorg/tools/network_validator.py
  • src/synthorg/tools/web/web_search.py
  • src/synthorg/tools/sub_constraints.py
src/**/*.py

⚙️ CodeRabbit configuration file

This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.

Files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/web/base_web_tool.py
  • src/synthorg/tools/terminal/base_terminal_tool.py
  • src/synthorg/tools/sub_constraint_enforcer.py
  • src/synthorg/tools/network_validator.py
  • src/synthorg/tools/web/web_search.py
  • src/synthorg/tools/sub_constraints.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: All tests must use markers: @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.e2e, @pytest.mark.slow
Use asyncio_mode = "auto" for async tests -- no manual @pytest.mark.asyncio needed
Global timeout of 30 seconds per test (configured in pyproject.toml). Non-default overrides like timeout(60) are allowed
Prefer @pytest.mark.parametrize for testing similar cases in Python tests
Tests must use test-provider, test-small-001, etc. instead of real vendor names
Use Hypothesis for property-based testing with @given and @settings. Configure via HYPOTHESIS_PROFILE env var: ci (10 examples, deterministic), dev (1000 examples), fuzz (10,000 examples, no deadline), extreme (500,000 examples)
When Hypothesis finds a failure, it is a real bug -- fix it fully and fundamentally. Add an explicit @example(...) decorator to the test for permanent coverage in CI
Never skip, dismiss, or ignore flaky tests -- always fix them fully and fundamentally. Mock time.monotonic() and asyncio.sleep() for determinism; use asyncio.Event().wait() for tasks that must block indefinitely
Test files and scripts/ may use relaxed ruff rules: print and deferred imports are allowed

Files:

  • tests/unit/tools/test_sub_constraints.py
  • tests/unit/tools/test_factory_new_categories.py

⚙️ CodeRabbit configuration file

Test files do not require Google-style docstrings on classes or functions -- ruff D rules are only enforced on src/. A bare @settings() decorator with no arguments on Hypothesis property tests is a no-op and should not be suggested -- the HYPOTHESIS_PROFILE env var controls example counts via registered profiles, which @given() honors automatically.

Files:

  • tests/unit/tools/test_sub_constraints.py
  • tests/unit/tools/test_factory_new_categories.py
🧠 Learnings (46)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Security: SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies: disabled/weighted/per-category/milestone), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/security/**/*.py : Security package (security/): SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume)
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 BaseModel, model_validator, computed_field, ConfigDict.

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. For derived values use `computed_field` instead of storing + validating redundant fields. Use `NotBlankStr` (from `core.types`) for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.

Applied to files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/network_validator.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (via `model_copy(update=...)`) for runtime state that evolves

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-15T18:42:17.990Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:42:17.990Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/security/**/*.py : Security package (security/): SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume)

Applied to files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/terminal/base_terminal_tool.py
  • src/synthorg/tools/sub_constraint_enforcer.py
  • src/synthorg/tools/sub_constraints.py
📚 Learning: 2026-03-20T11:18:48.128Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T11:18:48.128Z
Learning: Applies to src/synthorg/**/*.py : Set `RetryConfig` and `RateLimiterConfig` per-provider in `ProviderConfig`.

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/security/**/*.py : Security module includes SecOps agent, rule engine (soft-allow/hard-deny), audit log, output scanner, risk classifier, autonomy levels (4 strategies), timeout policies.

Applied to files:

  • src/synthorg/config/schema.py
  • src/synthorg/tools/sub_constraint_enforcer.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-31T21:07:37.469Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T21:07:37.469Z
Learning: Applies to src/synthorg/providers/**/*.py : Set `RetryConfig` and `RateLimiterConfig` per-provider in `ProviderConfig`

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to src/synthorg/providers/**/*.py : Providers: LLM provider abstraction (LiteLLM adapter), auth types (api_key/oauth/custom_header/none), presets (PROVIDER_PRESETS), runtime CRUD (ProviderManagementService with asyncio.Lock serialization), hot-reload via AppState swap.

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-16T19:13:36.562Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T19:13:36.562Z
Learning: Applies to src/synthorg/providers/**/*.py : RetryConfig and RateLimiterConfig are set per-provider in ProviderConfig.

Applied to files:

  • src/synthorg/config/schema.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, entry/exit of key functions.

Applied to files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, and key function entry/exit

Applied to files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
📚 Learning: 2026-03-31T20:07:03.035Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:07:03.035Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising; all state transitions must log at INFO; DEBUG for object creation, internal flow, entry/exit of key functions

Applied to files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
📚 Learning: 2026-03-17T06:43:14.114Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:43:14.114Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, entry/exit of key functions. Pure data models, enums, and re-exports do NOT need logging.

Applied to files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to src/ai_company/!(observability)/**/*.py : All error paths must log at WARNING or ERROR with context before raising.

Applied to files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
📚 Learning: 2026-03-15T16:55:07.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T16:55:07.730Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising.

Applied to files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
📚 Learning: 2026-04-07T13:20:05.336Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T13:20:05.336Z
Learning: Applies to src/synthorg/**/*.py : Maintain 80% minimum test coverage (enforced in CI)

Applied to files:

  • tests/unit/tools/test_factory_new_categories.py
📚 Learning: 2026-04-02T08:47:46.313Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T08:47:46.313Z
Learning: Run full pytest suite with coverage: `uv run python -m pytest tests/ -n auto --cov=synthorg --cov-fail-under=80`

Applied to files:

  • tests/unit/tools/test_factory_new_categories.py
📚 Learning: 2026-04-01T14:22:06.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T14:22:06.315Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Validate at system boundaries (user input, external APIs, config files)

Applied to files:

  • src/synthorg/tools/sub_constraint_enforcer.py
📚 Learning: 2026-03-16T20:14:00.937Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T20:14:00.937Z
Learning: Applies to **/*.py : Validate: at system boundaries (user input, external APIs, config files).

Applied to files:

  • src/synthorg/tools/sub_constraint_enforcer.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Security: SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies: disabled/weighted/per-category/milestone), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume).

Applied to files:

  • src/synthorg/tools/sub_constraint_enforcer.py
📚 Learning: 2026-03-19T07:13:44.964Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:13:44.964Z
Learning: Applies to **/*.py : Validate at system boundaries (user input, external APIs, config files)

Applied to files:

  • src/synthorg/tools/sub_constraint_enforcer.py
📚 Learning: 2026-04-07T13:20:05.336Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T13:20:05.336Z
Learning: Applies to **/*.py : Validate at system boundaries: user input, external APIs, and config files

Applied to files:

  • src/synthorg/tools/sub_constraint_enforcer.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).

Applied to files:

  • src/synthorg/tools/network_validator.py
  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-07T13:20:05.336Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T13:20:05.336Z
Learning: Applies to **/*.py : Functions must be less than 50 lines; files must be less than 800 lines

Applied to files:

  • src/synthorg/tools/network_validator.py
📚 Learning: 2026-04-01T06:12:03.047Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T06:12:03.047Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Keep functions under 50 lines and files under 800 lines

Applied to files:

  • src/synthorg/tools/network_validator.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Handle errors explicitly, never silently swallow. Validate at system boundaries (user input, external APIs, config files).

Applied to files:

  • src/synthorg/tools/network_validator.py
📚 Learning: 2026-04-01T09:09:43.948Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:09:43.948Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at construction and `MappingProxyType` wrapping for read-only enforcement in non-Pydantic internal collections (registries, BaseTool)

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : For non-Pydantic internal collections (registries, `BaseTool`), use `copy.deepcopy()` at construction and wrap with `MappingProxyType` for read-only enforcement

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for `dict`/`list` fields

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-01T09:58:27.410Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:58:27.410Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for dict/list fields in frozen Pydantic models

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Immutability: create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-07T13:20:05.336Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T13:20:05.336Z
Learning: Applies to **/*.py : Use immutability patterns: create new objects, never mutate existing ones. For non-Pydantic internal collections, use `copy.deepcopy()` at construction plus `MappingProxyType` wrapping for read-only enforcement

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-16T23:05:29.577Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T23:05:29.577Z
Learning: Applies to **/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-01T17:49:14.133Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T17:49:14.133Z
Learning: Applies to src/synthorg/{providers,engine}/**/*.py : Retryable errors are `RateLimitError`, `ProviderTimeoutError`, `ProviderConnectionError`, `ProviderInternalError`; non-retryable errors raise immediately; `RetryExhaustedError` signals all retries failed

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-07T13:20:05.336Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T13:20:05.336Z
Learning: Applies to src/synthorg/providers/**/*.py : Retryable errors (`is_retryable=True`): `RateLimitError`, `ProviderTimeoutError`, `ProviderConnectionError`, `ProviderInternalError`. Non-retryable errors raise immediately without retry

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-02T21:38:30.127Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T21:38:30.127Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Handle errors explicitly; never silently swallow exceptions

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Handle errors explicitly—never silently swallow exceptions.

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Handle errors explicitly; never silently swallow exceptions

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Handle errors explicitly, never silently swallow exceptions

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-16T19:13:34.746Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T19:13:34.746Z
Learning: Applies to src/synthorg/providers/**/*.py : Retryable errors (is_retryable=True): RateLimitError, ProviderTimeoutError, ProviderConnectionError, ProviderInternalError. Non-retryable errors raise immediately without retry. RetryExhaustedError signals that all retries failed — the engine layer catches this to trigger fallback chains.

Applied to files:

  • src/synthorg/tools/web/web_search.py
🔇 Additional comments (29)
src/synthorg/config/schema.py (3)

40-45: LGTM!

Type-only imports with # noqa: TC001 are appropriate here since these config models are used solely for Pydantic type annotations. The imports follow the existing pattern in the file.


579-583: Docstring now includes the new fields.

The RootConfig attribute list is updated to document web, database, and terminal. The descriptions accurately reflect the semantics: None enables default behavior for web/terminal tools, while None disables database tools (requiring explicit opt-in). This security-conscious asymmetry aligns with the PR's SSRF and SQL injection mitigation goals.


719-730: LGTM!

The three new configuration fields follow the established pattern:

  • Frozen Pydantic model with allow_inf_nan=False
  • Union type with None for optional config sections ✓
  • Descriptive Field() definitions ✓
  • Defaults to None aligning with defaults.py and factory wiring ✓

The intentional distinction—database requiring explicit configuration while web and terminal fall back to safe defaults—is a sound fail-closed posture for database tool access.

tests/unit/tools/test_factory_new_categories.py (5)

13-17: Fixture setup is clean and self-contained.

Creating the workspace inside the fixture keeps test setup consistent and removes per-test boilerplate.


23-62: Web factory coverage is strong.

This block checks default inclusion, provider-dependent behavior, and custom network-policy wiring in a focused way.


67-105: Database-tool gating assertions look complete.

Nice job covering default-off, configured-on, and empty-connections skip behavior for both sql_query and schema_inspect.


110-129: Terminal factory tests are well-targeted.

Both default creation and TerminalConfig propagation are asserted clearly.


135-147: Top-level factory contract checks are good guardrails.

Count and sorted-order assertions provide fast regression detection for default factory output.

src/synthorg/tools/terminal/base_terminal_tool.py (3)

1-21: LGTM!

Module setup correctly uses get_logger(__name__), imports the event constant from the appropriate domain module, and properly guards the type-only import behind TYPE_CHECKING.


24-65: LGTM!

The class correctly extends BaseTool with ToolCategory.TERMINAL, properly forwards all required parameters to the parent __init__, and the config property provides clean read-only access to the configuration.


124-134: Allowlist boundary handling looks correct.

The prefix matching now properly uses word boundaries (normalized == prefix_lower or normalized.startswith(prefix_lower + " ")), which prevents unintended matches like "ls" allowing "lscpu". The warning logging correctly outputs only the executable name rather than full command arguments.

src/synthorg/tools/network_validator.py (7)

1-30: LGTM!

Module setup is correct with proper imports, observability integration via get_logger(__name__), and comprehensive module-level docstring explaining the SSRF prevention purpose.


33-61: Comprehensive SSRF blocklist coverage.

The BLOCKED_NETWORKS tuple correctly covers all critical ranges: private (RFC 1918), loopback, link-local, CGNAT, test-nets, multicast, broadcast, and IPv6 equivalents including Teredo, 6to4, and ULA. The inclusion of 64:ff9b::/96 (NAT64) and 2002::/16 (6to4) is particularly important as these can encode IPv4 addresses that bypass naive checks.


67-114: LGTM!

NetworkPolicy correctly uses frozen Pydantic model with allow_inf_nan=False, bounded dns_resolution_timeout, and proper mode="before" validator for allowlist normalization without mutating the frozen instance.


145-172: Solid fail-closed IP validation with IPv6-mapped handling.

The is_blocked_ip function correctly:

  1. Blocks unparseable IPs (fail-closed)
  2. Unwraps IPv6-mapped IPv4 addresses before checking
  3. Logs blocked attempts with structured kwargs

263-272: System error propagation is now correctly implemented.

The exception handling properly re-raises MemoryError and RecursionError before the generic catch, following project conventions.


414-426: Allowlist DNS resolution now properly logs failures.

The allowlist branch correctly resolves DNS for IP pinning and logs a warning when resolution fails for allowlisted hosts before returning an empty tuple. This provides observability while maintaining the bypass behavior.


437-451: Literal IP handling correctly bypasses DNS resolution.

For literal IPs, the code skips DNS resolution (not needed) and directly checks against the blocklist. The try/except/else pattern cleanly separates the IP literal case from hostname resolution.

src/synthorg/tools/web/base_web_tool.py (3)

1-23: LGTM!

Module setup is correct with proper imports, ABC for abstract base, and observability integration.


33-65: LGTM!

Constructor properly validates request_timeout > 0 before use, sets the hardcoded ToolCategory.WEB, and defaults NetworkPolicy() when none provided. The validation addresses the system boundary principle.


72-94: Clean URL validation helper.

The _validate_url method correctly:

  1. Checks scheme first via is_allowed_http_scheme
  2. Logs blocked attempts with structured kwargs and descriptive reason
  3. Delegates host/IP validation to the centralized validate_url_host
src/synthorg/tools/web/web_search.py (7)

1-26: LGTM!

Module setup is correct with proper imports, copy for deep copying, runtime_checkable Protocol, and observability integration.


28-42: LGTM!

SearchResult is a proper frozen Pydantic model with allow_inf_nan=False, suitable for representing immutable search results.


44-67: LGTM!

WebSearchProvider protocol is correctly defined as runtime_checkable with an async search method. The docstring properly documents the expected return type.


102-124: LGTM!

Constructor properly uses copy.deepcopy(_PARAMETERS_SCHEMA) to avoid shared nested dict mutation, and stores the injected provider.


144-153: Proper exception handling with system error propagation.

The exception handling correctly re-raises MemoryError and RecursionError before the generic catch, following project conventions for system error propagation.


162-179: Good boundary validation for external provider results.

The code properly validates provider results at the system boundary:

  1. Caps results to max_results
  2. Coerces items to SearchResult via model_validate
  3. Silently skips malformed items (logged implicitly via empty validated list)
  4. Returns graceful "No results found." when all items fail validation

181-197: LGTM!

Result formatting is clean with proper structured logging of success metrics and well-formatted multiline output.

tests/unit/tools/test_sub_constraints.py (1)

158-193: Good regression coverage for merged overrides.

These assertions lock in the security-sensitive contract that built-in defaults survive partial overrides instead of being replaced wholesale.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/synthorg/tools/web/web_search.py (1)

114-123: 🛠️ Refactor suggestion | 🟠 Major

Make the copied schema immutable.

copy.deepcopy() fixes nested aliasing, but the schema handed to BaseWebTool is still mutable. That leaves the tool contract open to runtime mutation, which is the exact collection pattern the repo asks us to lock down for BaseTool-owned state.

Based on learnings: "For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction plus MappingProxyType wrapping for read-only enforcement."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/web/web_search.py` around lines 114 - 123, The
parameters_schema passed to BaseWebTool is currently a mutable deep copy of
_PARAMETERS_SCHEMA; wrap the deep-copied schema with a read-only
MappingProxyType before passing it to super() so the tool gets an immutable
mapping, e.g., replace parameters_schema=copy.deepcopy(_PARAMETERS_SCHEMA) with
parameters_schema=MappingProxyType(copy.deepcopy(_PARAMETERS_SCHEMA)); update
imports to include MappingProxyType and reference the constructor call in the
BaseWebTool subclass (the super().__init__ in this class) and the
_PARAMETERS_SCHEMA symbol when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/tools/terminal/base_terminal_tool.py`:
- Around line 67-88: Add DEBUG and INFO logs in _is_command_blocked to follow
the logging policy: log entry into the function with logger.debug including the
raw command and normalized/executable values, log the decision point for each
pattern check at DEBUG, and emit an INFO-level state transition when a command
is allowed or denied (e.g., on block: logger.info with executable, pattern,
reason="deny" before the existing logger.warning; on allow: logger.info with
executable, reason="allow" before returning False). Apply the same pattern to
the related validation functions referenced in the 95-135 range so all
entry/exit and allow/deny transitions use DEBUG for flow and INFO for state
changes.
- Around line 76-80: The blocklist comparison currently uses the raw normalized
string (variable normalized) so patterns with specific spacing like "rm -rf /"
can be bypassed with extra whitespace; update the normalization in the
BaseTerminalTool method to collapse all runs of whitespace
(spaces/tabs/newlines) into a single space (e.g., using a regex replace) after
strip(). Use that collapsed-normalized string for splitting into
parts/executable and for the loop that checks self._config.command_blocklist
(ensure you also lower-case both sides before the "in" check).

In `@src/synthorg/tools/web/web_search.py`:
- Around line 140-145: In execute(), validate and normalize max_results before
using it: ensure the value from arguments is an integer and within an acceptable
range (e.g. a positive non-zero upper bound), otherwise raise a clear ValueError
or coerce safely; then use the validated value (e.g. validated_max_results) when
calling self._provider.search(query, validated_max_results) and when slicing
list(results)[:validated_max_results] to avoid passing strings or negatives to
the provider or causing local TypeErrors in list slicing.
- Around line 142-152: The code currently logs and returns raw user query and
provider exception strings (see logger.info, logger.warning, and the
ToolExecutionResult content after calling self._provider.search); change these
to never emit raw PII/error text: replace logger.info(...) and
logger.warning(...) calls to log only a redacted fingerprint (e.g., hash or
truncated length) of query and the exception type (type(exc).__name__) plus any
safe metadata, and update returned ToolExecutionResult messages to a generic
failure message (no raw exc or query); apply the same redaction pattern to the
other occurrences noted (the other logger calls and ToolExecutionResult returns
referenced in the comment) so no raw query/error details are written to logs or
returned to callers.
- Around line 144-145: The tool calls self._provider.search(query, max_results)
without enforcing the provided network_policy; update the WebSearch tool to
preflight the network_policy (same deny/allow logic as in
src/synthorg/tools/web/http_request.py preflight) and block/raise before
invoking _provider.search so it fails closed, or change the provider interface
(e.g., provider.search) to accept a network_policy argument and ensure
implementations enforce it; specifically, perform the network_policy check in
the method that calls self._provider.search (or add a network_policy param to
Provider.search) and return/raise on deny so no outbound request can bypass the
policy.

---

Duplicate comments:
In `@src/synthorg/tools/web/web_search.py`:
- Around line 114-123: The parameters_schema passed to BaseWebTool is currently
a mutable deep copy of _PARAMETERS_SCHEMA; wrap the deep-copied schema with a
read-only MappingProxyType before passing it to super() so the tool gets an
immutable mapping, e.g., replace
parameters_schema=copy.deepcopy(_PARAMETERS_SCHEMA) with
parameters_schema=MappingProxyType(copy.deepcopy(_PARAMETERS_SCHEMA)); update
imports to include MappingProxyType and reference the constructor call in the
BaseWebTool subclass (the super().__init__ in this class) and the
_PARAMETERS_SCHEMA symbol when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8c905a34-aeef-4b30-b715-81d6193afffe

📥 Commits

Reviewing files that changed from the base of the PR and between e0330ab and 7807bf0.

📒 Files selected for processing (3)
  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/terminal/base_terminal_tool.py
  • src/synthorg/tools/web/web_search.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations -- Python 3.14 has PEP 649
Use except A, B: syntax (no parentheses) for exception handling -- ruff enforces this on Python 3.14 (PEP 758)
All public functions require type hints; mypy strict mode enforcement
Docstrings must use Google style and are required on all public classes and functions (enforced by ruff D rules)
Use immutability patterns: create new objects, never mutate existing ones. For non-Pydantic internal collections, use copy.deepcopy() at construction plus MappingProxyType wrapping for read-only enforcement
Use frozen Pydantic models for config/identity; use separate mutable-via-copy models for runtime state evolution. Never mix static config fields with mutable runtime fields in one model
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict). Always use allow_inf_nan=False in ConfigDict; use @computed_field for derived values; use NotBlankStr for all identifier/name fields
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code. Prefer structured concurrency over bare create_task
Maintain line length of 88 characters (enforced by ruff)
Functions must be less than 50 lines; files must be less than 800 lines
Handle errors explicitly; never silently swallow exceptions
Validate at system boundaries: user input, external APIs, and config files
Variable name for logger must always be logger (not _logger, not log)
All Python source files must use ruff for linting and formatting with line length 88 characters

Files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/web/web_search.py
src/synthorg/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/synthorg/**/*.py: Every module with business logic must have: from synthorg.observability import get_logger then logger = get_logger(__name__)
Never use import logging, logging.getLogger(), or print() in application code (exception: observability/setup.py, observability/sinks.py, observability/syslog_handler.py, observability/http_handler.py may use stdlib logging and print for handler/bootstrap code)
Use event name constants from synthorg.observability.events domain-specific modules (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool). Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT
Always use structured kwargs in logging: logger.info(EVENT, key=value) -- never logger.info("msg %s", val)
All error paths must log at WARNING or ERROR with context before raising
All state transitions must log at INFO level
Use DEBUG logging for object creation, internal flow, and entry/exit of key functions
All provider calls go through BaseCompletionProvider which applies retry and rate limiting automatically. Never implement retry logic in driver subclasses or calling code
Maintain 80% minimum test coverage (enforced in CI)
Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, comments, tests, or config examples. Use generic names: example-provider, example-large-001, example-medium-001, example-small-001
All Python code in src/synthorg/ must pass mypy strict mode type-checking
Pure data models, enums, and re-exports in Python do NOT need logging setup

Files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/web/web_search.py
src/**/*.py

⚙️ CodeRabbit configuration file

This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.

Files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/web/web_search.py
🧠 Learnings (33)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Security: SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies: disabled/weighted/per-category/milestone), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume).
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, entry/exit of key functions.

Applied to files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, and key function entry/exit

Applied to files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
📚 Learning: 2026-03-31T20:07:03.035Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:07:03.035Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising; all state transitions must log at INFO; DEBUG for object creation, internal flow, entry/exit of key functions

Applied to files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
📚 Learning: 2026-03-17T06:43:14.114Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:43:14.114Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, entry/exit of key functions. Pure data models, enums, and re-exports do NOT need logging.

Applied to files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to src/ai_company/!(observability)/**/*.py : All error paths must log at WARNING or ERROR with context before raising.

Applied to files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
📚 Learning: 2026-03-15T16:55:07.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T16:55:07.730Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising.

Applied to files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/security/**/*.py : Security package (security/): SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume)

Applied to files:

  • src/synthorg/tools/terminal/base_terminal_tool.py
  • src/synthorg/tools/sub_constraints.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...

Applied to files:

  • src/synthorg/tools/sub_constraints.py
📚 Learning: 2026-04-02T11:49:21.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T11:49:21.202Z
Learning: Applies to **/*.py : Use Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`) with `allow_inf_nan=False` in all `ConfigDict` declarations

Applied to files:

  • src/synthorg/tools/sub_constraints.py
📚 Learning: 2026-04-01T15:36:39.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T15:36:39.993Z
Learning: Applies to src/**/*.py : Use Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`). Use `allow_inf_nan=False` in all `ConfigDict` declarations to reject `NaN`/`Inf` in numeric fields at validation time

Applied to files:

  • src/synthorg/tools/sub_constraints.py
📚 Learning: 2026-04-07T13:20:05.336Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T13:20:05.336Z
Learning: Applies to **/*.py : Use Pydantic v2 (`BaseModel`, `model_validator`, `computed_field`, `ConfigDict`). Always use `allow_inf_nan=False` in ConfigDict; use `computed_field` for derived values; use `NotBlankStr` for all identifier/name fields

Applied to files:

  • src/synthorg/tools/sub_constraints.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).

Applied to files:

  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-16T23:05:29.577Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T23:05:29.577Z
Learning: Applies to **/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).

Applied to files:

  • src/synthorg/tools/sub_constraints.py
  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. For derived values use `computed_field` instead of storing + validating redundant fields. Use `NotBlankStr` (from `core.types`) for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.

Applied to files:

  • src/synthorg/tools/sub_constraints.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.

Applied to files:

  • src/synthorg/tools/sub_constraints.py
📚 Learning: 2026-04-07T13:20:05.336Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T13:20:05.336Z
Learning: Applies to **/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models for runtime state evolution. Never mix static config fields with mutable runtime fields in one model

Applied to files:

  • src/synthorg/tools/sub_constraints.py
📚 Learning: 2026-03-26T15:18:16.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-26T15:18:16.848Z
Learning: Applies to src/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. Use `computed_field` for derived values instead of storing redundant fields. Use `NotBlankStr` for all identifier/name fields.

Applied to files:

  • src/synthorg/tools/sub_constraints.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 BaseModel, model_validator, computed_field, ConfigDict.

Applied to files:

  • src/synthorg/tools/sub_constraints.py
📚 Learning: 2026-04-01T09:09:43.948Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:09:43.948Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at construction and `MappingProxyType` wrapping for read-only enforcement in non-Pydantic internal collections (registries, BaseTool)

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : For non-Pydantic internal collections (registries, `BaseTool`), use `copy.deepcopy()` at construction and wrap with `MappingProxyType` for read-only enforcement

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for `dict`/`list` fields

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-01T09:58:27.410Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:58:27.410Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for dict/list fields in frozen Pydantic models

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Immutability: create new objects, never mutate existing ones. For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement.

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-07T13:20:05.336Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T13:20:05.336Z
Learning: Applies to **/*.py : Use immutability patterns: create new objects, never mutate existing ones. For non-Pydantic internal collections, use `copy.deepcopy()` at construction plus `MappingProxyType` wrapping for read-only enforcement

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-01T17:49:14.133Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T17:49:14.133Z
Learning: Applies to src/synthorg/{providers,engine}/**/*.py : Retryable errors are `RateLimitError`, `ProviderTimeoutError`, `ProviderConnectionError`, `ProviderInternalError`; non-retryable errors raise immediately; `RetryExhaustedError` signals all retries failed

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-07T13:20:05.336Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T13:20:05.336Z
Learning: Applies to src/synthorg/providers/**/*.py : Retryable errors (`is_retryable=True`): `RateLimitError`, `ProviderTimeoutError`, `ProviderConnectionError`, `ProviderInternalError`. Non-retryable errors raise immediately without retry

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-04-02T21:38:30.127Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T21:38:30.127Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Handle errors explicitly; never silently swallow exceptions

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Handle errors explicitly—never silently swallow exceptions.

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Handle errors explicitly; never silently swallow exceptions

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Handle errors explicitly, never silently swallow exceptions

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-16T19:13:34.746Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T19:13:34.746Z
Learning: Applies to src/synthorg/providers/**/*.py : Retryable errors (is_retryable=True): RateLimitError, ProviderTimeoutError, ProviderConnectionError, ProviderInternalError. Non-retryable errors raise immediately without retry. RetryExhaustedError signals that all retries failed — the engine layer catches this to trigger fallback chains.

Applied to files:

  • src/synthorg/tools/web/web_search.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to src/ai_company/!(observability)/**/*.py : Use DEBUG logging for object creation, internal flow, and entry/exit of key functions.

Applied to files:

  • src/synthorg/tools/web/web_search.py
🔇 Additional comments (8)
src/synthorg/tools/sub_constraints.py (6)

226-238: Inconsistent handling of unknown access levels in merge path remains.

Line 228 falls back to ToolSubConstraints() (permissive defaults) when access_level is not in the mapping, but lines 252-261 raise ValueError for the same scenario without custom constraints. If a new ToolAccessLevel enum value is added without updating _LEVEL_SUB_CONSTRAINTS, the merge path silently uses permissive defaults as the base—violating fail-closed security principles.

🛡️ Proposed fix for consistent error handling
         # Non-CUSTOM with overrides: merge into level defaults so
         # unset fields retain the secure baseline.
-        base = _LEVEL_SUB_CONSTRAINTS.get(access_level, ToolSubConstraints())
+        base = _LEVEL_SUB_CONSTRAINTS.get(access_level)
+        if base is None:
+            msg = f"No default sub-constraints for access level: {access_level.value}"
+            logger.warning(
+                SUB_CONSTRAINT_RESOLVED,
+                access_level=access_level.value,
+                source="error",
+                error=msg,
+            )
+            raise ValueError(msg)
         merged = {
             **base.model_dump(),
             **custom_constraints.model_dump(exclude_unset=True),

1-24: Module setup and imports look good.

Correct use of get_logger(__name__) for structured logging, proper event constant import from domain-specific module, and immutability support via MappingProxyType and Final. No forbidden patterns present.


27-95: Enum definitions are well-structured.

All five constraint dimension enums follow StrEnum conventions with comprehensive docstrings. The value naming is consistent and the granularity matches the design spec.


101-148: Model configuration follows all guidelines.

Properly configured with frozen=True, allow_inf_nan=False, and extra="forbid" to reject unknown fields at this security boundary. Using NotBlankStr for identifier tuples ensures no empty/whitespace-only entries.


151-187: Level defaults mapping is well-designed.

Immutable via MappingProxyType with Final type hint. The constraint progression from SANDBOXED (most restrictive) to ELEVATED (least restrictive) follows a sensible security hierarchy. The requires_approval prefixes correctly use the "category:" format.


240-267: Error handling and default resolution paths are correct.

The CUSTOM level without constraints properly raises ValueError with a clear message. Unknown access levels (without custom constraints) also correctly fail with ValueError. Logging follows structured kwargs convention with appropriate severity levels (warning for errors, debug for successful resolution).

src/synthorg/tools/terminal/base_terminal_tool.py (1)

117-134: Good improvement: blocked-command logs avoid raw command leakage

Using executable (instead of full command) in TERMINAL_COMMAND_BLOCKED materially reduces sensitive-arg exposure in denial logs. Nice hardening.

src/synthorg/tools/web/web_search.py (1)

166-170: Nice defensive normalization of provider items.

Coercing each provider item through SearchResult.model_validate(..., from_attributes=True) is the right hardening layer here. It keeps malformed provider payloads from breaking the whole tool call.

Comment on lines +67 to +88
def _is_command_blocked(self, command: str) -> bool:
"""Check if the command matches any blocklist pattern.

Args:
command: The command string to check.

Returns:
``True`` if the command is blocked.
"""
normalized = command.strip().lower()
parts = normalized.split(maxsplit=1)
executable = parts[0] if parts else ""
for pattern in self._config.command_blocklist:
if pattern.lower() in normalized:
logger.warning(
TERMINAL_COMMAND_BLOCKED,
executable=executable,
pattern=pattern,
reason="blocklist_match",
)
return True
return False
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add DEBUG/INFO logs for command-validation flow to match project logging policy

The key validation paths return without INFO/DEBUG flow logs on allowed outcomes. Add DEBUG for entry/decision flow and INFO for state transitions (allow/deny outcomes), not only WARNING on denials.

As per coding guidelines, "All state transitions must log at INFO level" and "Use DEBUG logging for object creation, internal flow, and entry/exit of key functions."

Also applies to: 95-135

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/terminal/base_terminal_tool.py` around lines 67 - 88, Add
DEBUG and INFO logs in _is_command_blocked to follow the logging policy: log
entry into the function with logger.debug including the raw command and
normalized/executable values, log the decision point for each pattern check at
DEBUG, and emit an INFO-level state transition when a command is allowed or
denied (e.g., on block: logger.info with executable, pattern, reason="deny"
before the existing logger.warning; on allow: logger.info with executable,
reason="allow" before returning False). Apply the same pattern to the related
validation functions referenced in the 95-135 range so all entry/exit and
allow/deny transitions use DEBUG for flow and INFO for state changes.

Comment on lines +140 to +145
max_results: int = arguments.get("max_results", 10)

logger.info(WEB_SEARCH_START, query=query, max_results=max_results)

try:
results = await self._provider.search(query, max_results)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate max_results inside execute().

max_results is trusted twice: it's forwarded to the provider at Line 145, then reused in list(results)[:max_results] at Line 164. If schema validation is bypassed and a caller passes "10" or -1, this either issues a bad outbound request or raises an uncaught TypeError locally. Re-check the type/range here instead of relying on schema metadata alone.

🛡️ Suggested hardening
-        query: str = arguments["query"]
-        max_results: int = arguments.get("max_results", 10)
+        query: str = arguments["query"]
+        raw_max_results = arguments.get("max_results", 10)
+        if type(raw_max_results) is not int or not 1 <= raw_max_results <= 100:
+            return ToolExecutionResult(
+                content="max_results must be an integer between 1 and 100.",
+                is_error=True,
+            )
+        max_results = raw_max_results

As per coding guidelines: "Validate at system boundaries: user input, external APIs, and config files."

Also applies to: 164-164

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/web/web_search.py` around lines 140 - 145, In execute(),
validate and normalize max_results before using it: ensure the value from
arguments is an integer and within an acceptable range (e.g. a positive non-zero
upper bound), otherwise raise a clear ValueError or coerce safely; then use the
validated value (e.g. validated_max_results) when calling
self._provider.search(query, validated_max_results) and when slicing
list(results)[:validated_max_results] to avoid passing strings or negatives to
the provider or causing local TypeErrors in list slicing.

Comment on lines +142 to +152
logger.info(WEB_SEARCH_START, query=query, max_results=max_results)

try:
results = await self._provider.search(query, max_results)
except MemoryError, RecursionError:
raise
except Exception as exc:
logger.warning(WEB_SEARCH_FAILED, query=query, error=str(exc))
return ToolExecutionResult(
content=f"Web search failed: {exc}",
is_error=True,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redact query/error details before logging or returning them.

query is raw user input and str(exc) comes from an external provider boundary. Writing those values to INFO/WARNING logs and echoing str(exc) back in Line 151 can leak PII, secrets, or provider internals into audit sinks and model-visible output. Log a redacted fingerprint/length plus error_type, and return a generic failure message instead.

Based on learnings: "Security: SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered)."

Also applies to: 156-157, 172-176, 181-181, 194-198

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/web/web_search.py` around lines 142 - 152, The code
currently logs and returns raw user query and provider exception strings (see
logger.info, logger.warning, and the ToolExecutionResult content after calling
self._provider.search); change these to never emit raw PII/error text: replace
logger.info(...) and logger.warning(...) calls to log only a redacted
fingerprint (e.g., hash or truncated length) of query and the exception type
(type(exc).__name__) plus any safe metadata, and update returned
ToolExecutionResult messages to a generic failure message (no raw exc or query);
apply the same redaction pattern to the other occurrences noted (the other
logger calls and ToolExecutionResult returns referenced in the comment) so no
raw query/error details are written to logs or returned to callers.

Comment on lines +144 to +145
try:
results = await self._provider.search(query, max_results)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Apply network_policy before calling the provider.

This tool accepts a network_policy, but Line 145 delegates straight to the injected provider. Any provider that performs real HTTP calls can bypass the shared deny/allowlist logic here, unlike src/synthorg/tools/web/http_request.py:117-152, which preflights outbound traffic before making the request. Fail closed before invoking search(), or make the provider interface receive and enforce the policy explicitly.

Based on learnings: "Security: SecOps agent, rule engine (soft-allow/hard-deny, fail-closed)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/tools/web/web_search.py` around lines 144 - 145, The tool calls
self._provider.search(query, max_results) without enforcing the provided
network_policy; update the WebSearch tool to preflight the network_policy (same
deny/allow logic as in src/synthorg/tools/web/http_request.py preflight) and
block/raise before invoking _provider.search so it fails closed, or change the
provider interface (e.g., provider.search) to accept a network_policy argument
and ensure implementations enforce it; specifically, perform the network_policy
check in the method that calls self._provider.search (or add a network_policy
param to Provider.search) and return/raise on deny so no outbound request can
bypass the policy.

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 7, 2026 15:22 — with GitHub Actions Inactive
@Aureliolo Aureliolo merged commit 0611b53 into main Apr 7, 2026
28 of 29 checks passed
@Aureliolo Aureliolo deleted the feat/tool-system branch April 7, 2026 15:25
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 7, 2026 15:25 — with GitHub Actions Inactive
Aureliolo added a commit that referenced this pull request Apr 7, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.6.4](v0.6.3...v0.6.4)
(2026-04-07)


### Features

* analytics and metrics runtime pipeline
([#226](#226),
[#225](#225),
[#227](#227),
[#224](#224))
([#1127](#1127))
([ec57641](ec57641))
* engine intelligence -- quality signals, health monitoring, trajectory
scoring, coordination metrics
([#1099](#1099))
([aac2029](aac2029)),
closes [#697](#697)
[#707](#707)
[#705](#705)
[#703](#703)
* enterprise-grade auth -- HttpOnly cookie sessions, CSRF, lockout,
session limits
([#1102](#1102))
([d3022c7](d3022c7)),
closes [#1068](#1068)
* implement core tool categories and granular sub-constraints
([#1101](#1101))
([0611b53](0611b53)),
closes [#1034](#1034)
[#220](#220)
* memory evolution -- GraphRAG/consistency research +
SelfEditingMemoryStrategy
([#1036](#1036),
[#208](#208))
([#1129](#1129))
([a9acda3](a9acda3))
* security hardening -- sandbox, risk override, SSRF self-heal, DAST fix
([#1100](#1100))
([31e7273](31e7273)),
closes [#1098](#1098)
[#696](#696)
[#222](#222)
[#671](#671)


### Bug Fixes

* harden agent identity versioning post-review
([#1128](#1128))
([8eb2859](8eb2859)),
closes [#1076](#1076)


### Documentation

* engine architecture research
([#688](#688)
[#690](#690)
[#848](#848)
[#687](#687))
([#1114](#1114))
([59b31f9](59b31f9))


### Maintenance

* add .claudeignore and split CLAUDE.md for token optimization
([#1112](#1112))
([b0fbd18](b0fbd18))
* bump github.com/sigstore/protobuf-specs from 0.5.0 to 0.5.1 in /cli in
the all group
([#1106](#1106))
([73089c9](73089c9))
* bump jsdom from 29.0.1 to 29.0.2 in /site in the all group
([#1107](#1107))
([8e99dce](8e99dce))
* bump jsdom from 29.0.1 to 29.0.2 in /web in the all group
([#1108](#1108))
([ce8c749](ce8c749))
* bump python from `fb83750` to `6869258` in /docker/backend in the all
group ([#1104](#1104))
([4911726](4911726))
* bump python from `fb83750` to `6869258` in /docker/web in the all
group ([#1103](#1103))
([87bdf09](87bdf09))
* bump the all group across 1 directory with 4 updates
([#1111](#1111))
([f702464](f702464))
* bump the all group in /docker/sandbox with 2 updates
([#1105](#1105))
([05a91ca](05a91ca))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: implement core tool categories -- web, database, terminal/shell feat: implement granular tool access sub-constraints

3 participants