Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ src/synthorg/
definitions/ # Per-namespace setting definitions (company, providers, memory, budget, security, coordination, observability, backup)
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)
templates/ # Pre-built company templates, personality presets, and builder
tools/ # Tool registry, built-in tools (file_system/, git, sandbox/, code_runner), git clone SSRF prevention (git_url_validator), MCP bridge (mcp/), role-based access, approval tool (request_human_approval)
tools/ # Tool registry, built-in tools (file_system/, git, sandbox/, code_runner), git clone SSRF prevention (git_url_validator), MCP bridge (mcp/), role-based access, approval tool (request_human_approval), tool factory (build_default_tools, build_default_tools_from_config)

web/ # Vue 3 + PrimeVue + Tailwind CSS dashboard
src/
Expand Down Expand Up @@ -196,7 +196,7 @@ site/ # Astro landing page (synthorg.io)
- **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
- **Variable name**: always `logger` (not `_logger`, not `log`)
- **Event names**: always use constants from the domain-specific module under `synthorg.observability.events` (e.g., `PROVIDER_CALL_START` from `events.provider`, `BUDGET_RECORD_ADDED` from `events.budget`, `CFO_ANOMALY_DETECTED` from `events.cfo`, `CONFLICT_DETECTED` from `events.conflict`, `MEETING_STARTED` from `events.meeting`, `MEETING_SCHEDULER_STARTED` from `events.meeting`, `MEETING_SCHEDULER_ERROR` from `events.meeting`, `MEETING_SCHEDULER_STOPPED` from `events.meeting`, `MEETING_PERIODIC_TRIGGERED` from `events.meeting`, `MEETING_EVENT_TRIGGERED` from `events.meeting`, `MEETING_PARTICIPANTS_RESOLVED` from `events.meeting`, `MEETING_NO_PARTICIPANTS` from `events.meeting`, `MEETING_NOT_FOUND` from `events.meeting`, `CLASSIFICATION_START` from `events.classification`, `CONSOLIDATION_START` from `events.consolidation`, `ORG_MEMORY_QUERY_START` from `events.org_memory`, `API_REQUEST_STARTED` from `events.api`, `API_REQUEST_COMPLETED` from `events.api`, `API_REQUEST_ERROR` from `events.api`, `API_ROUTE_NOT_FOUND` from `events.api`, `API_HEALTH_CHECK` from `events.api`, `API_COORDINATION_STARTED` from `events.api`, `API_COORDINATION_COMPLETED` from `events.api`, `API_COORDINATION_FAILED` from `events.api`, `API_COORDINATION_AGENT_RESOLVE_FAILED` from `events.api`, `API_CONTENT_NEGOTIATED` from `events.api`, `API_CORRELATION_FALLBACK` from `events.api`, `API_ACCEPT_PARSE_FAILED` from `events.api`, `API_WS_TICKET_ISSUED` from `events.api`, `API_WS_TICKET_CONSUMED` from `events.api`, `API_WS_TICKET_EXPIRED` from `events.api`, `API_WS_TICKET_INVALID` from `events.api`, `API_WS_TICKET_CLEANUP` from `events.api`, `CODE_RUNNER_EXECUTE_START` from `events.code_runner`, `DOCKER_EXECUTE_START` from `events.docker`, `MCP_INVOKE_START` from `events.mcp`, `SECURITY_EVALUATE_START` from `events.security`, `HR_HIRING_REQUEST_CREATED` from `events.hr`, `PERF_METRIC_RECORDED` from `events.performance`, `PERF_LLM_SAMPLE_STARTED` from `events.performance`, `PERF_LLM_SAMPLE_COMPLETED` from `events.performance`, `PERF_LLM_SAMPLE_FAILED` from `events.performance`, `PERF_OVERRIDE_SET` from `events.performance`, `PERF_OVERRIDE_CLEARED` from `events.performance`, `PERF_OVERRIDE_APPLIED` from `events.performance`, `PERF_OVERRIDE_EXPIRED` from `events.performance`, `TRUST_EVALUATE_START` from `events.trust`, `PROMOTION_EVALUATE_START` from `events.promotion`, `PROMPT_BUILD_START` from `events.prompt`, `MEMORY_RETRIEVAL_START` from `events.memory`, `MEMORY_BACKEND_CONNECTED` from `events.memory`, `MEMORY_ENTRY_STORED` from `events.memory`, `MEMORY_BACKEND_SYSTEM_ERROR` from `events.memory`, `MEMORY_RRF_FUSION_COMPLETE` from `events.memory`, `MEMORY_RRF_VALIDATION_FAILED` from `events.memory`, `AUTONOMY_ACTION_AUTO_APPROVED` from `events.autonomy`, `TIMEOUT_POLICY_EVALUATED` from `events.timeout`, `PERSISTENCE_AUDIT_ENTRY_SAVED` from `events.persistence`, `TASK_ENGINE_STARTED` from `events.task_engine`, `COORDINATION_STARTED` from `events.coordination`, `COORDINATION_FACTORY_BUILT` from `events.coordination`, `COMMUNICATION_DISPATCH_START` from `events.communication`, `COMPANY_STARTED` from `events.company`, `CONFIG_LOADED` from `events.config`, `CORRELATION_ID_CREATED` from `events.correlation`, `DECOMPOSITION_STARTED` from `events.decomposition`, `DELEGATION_STARTED` from `events.delegation`, `EXECUTION_LOOP_START` from `events.execution`, `CHECKPOINT_SAVED` from `events.checkpoint`, `PERSISTENCE_CHECKPOINT_SAVED` from `events.persistence`, `GIT_COMMAND_START` from `events.git`, `GIT_CLONE_URL_REJECTED` from `events.git`, `GIT_CLONE_SSRF_BLOCKED` from `events.git`, `GIT_CLONE_DNS_FAILED` from `events.git`, `GIT_CLONE_SSRF_DISABLED` from `events.git`, `PARALLEL_GROUP_START` from `events.parallel`, `PERSONALITY_LOADED` from `events.personality`, `QUOTA_CHECKED` from `events.quota`, `ROLE_ASSIGNED` from `events.role`, `ROUTING_STARTED` from `events.routing`, `SANDBOX_EXECUTE_START` from `events.sandbox`, `TASK_CREATED` from `events.task`, `TASK_ASSIGNMENT_STARTED` from `events.task_assignment`, `TASK_ROUTING_STARTED` from `events.task_routing`, `TEMPLATE_LOADED` from `events.template`, `TOOL_INVOKE_START` from `events.tool`, `TOOL_OUTPUT_WITHHELD` from `events.tool`, `WORKSPACE_CREATED` from `events.workspace`, `APPROVAL_GATE_ESCALATION_DETECTED` from `events.approval_gate`, `APPROVAL_GATE_ESCALATION_FAILED` from `events.approval_gate`, `APPROVAL_GATE_INITIALIZED` from `events.approval_gate`, `APPROVAL_GATE_RISK_CLASSIFIED` from `events.approval_gate`, `APPROVAL_GATE_RISK_CLASSIFY_FAILED` from `events.approval_gate`, `APPROVAL_GATE_CONTEXT_PARKED` from `events.approval_gate`, `APPROVAL_GATE_CONTEXT_PARK_FAILED` from `events.approval_gate`, `APPROVAL_GATE_PARK_TASKLESS` from `events.approval_gate`, `APPROVAL_GATE_RESUME_STARTED` from `events.approval_gate`, `APPROVAL_GATE_CONTEXT_RESUMED` from `events.approval_gate`, `APPROVAL_GATE_RESUME_FAILED` from `events.approval_gate`, `APPROVAL_GATE_RESUME_DELETE_FAILED` from `events.approval_gate`, `APPROVAL_GATE_RESUME_TRIGGERED` from `events.approval_gate`, `APPROVAL_GATE_NO_PARKED_CONTEXT` from `events.approval_gate`, `APPROVAL_GATE_LOOP_WIRING_WARNING` from `events.approval_gate`, `STAGNATION_CHECK_PERFORMED` from `events.stagnation`, `STAGNATION_DETECTED` from `events.stagnation`, `STAGNATION_CORRECTION_INJECTED` from `events.stagnation`, `STAGNATION_TERMINATED` from `events.stagnation`, `PERSISTENCE_AGENT_STATE_SAVED` from `events.persistence`, `PERSISTENCE_AGENT_STATE_FETCHED` from `events.persistence`, `PERSISTENCE_AGENT_STATE_ACTIVE_QUERIED` from `events.persistence`, `PERSISTENCE_AGENT_STATE_DELETED` from `events.persistence`, `SETTINGS_VALUE_SET` from `events.settings`, `SETTINGS_VALUE_DELETED` from `events.settings`, `SETTINGS_VALUE_RESOLVED` from `events.settings`, `SETTINGS_CACHE_INVALIDATED` from `events.settings`, `SETTINGS_ENCRYPTION_ERROR` from `events.settings`, `SETTINGS_VALIDATION_FAILED` from `events.settings`, `SETTINGS_NOTIFICATION_PUBLISHED` from `events.settings`, `SETTINGS_NOTIFICATION_FAILED` from `events.settings`, `SETTINGS_FETCH_FAILED` from `events.settings`, `SETTINGS_SET_FAILED` from `events.settings`, `SETTINGS_DELETE_FAILED` from `events.settings`, `SETTINGS_NOT_FOUND` from `events.settings`, `SETTINGS_REGISTRY_DUPLICATE` from `events.settings`, `SETTINGS_CONFIG_PATH_MISS` from `events.settings`). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`
- **Event names**: always use constants from the domain-specific module under `synthorg.observability.events` (e.g., `PROVIDER_CALL_START` from `events.provider`, `BUDGET_RECORD_ADDED` from `events.budget`, `CFO_ANOMALY_DETECTED` from `events.cfo`, `CONFLICT_DETECTED` from `events.conflict`, `MEETING_STARTED` from `events.meeting`, `MEETING_SCHEDULER_STARTED` from `events.meeting`, `MEETING_SCHEDULER_ERROR` from `events.meeting`, `MEETING_SCHEDULER_STOPPED` from `events.meeting`, `MEETING_PERIODIC_TRIGGERED` from `events.meeting`, `MEETING_EVENT_TRIGGERED` from `events.meeting`, `MEETING_PARTICIPANTS_RESOLVED` from `events.meeting`, `MEETING_NO_PARTICIPANTS` from `events.meeting`, `MEETING_NOT_FOUND` from `events.meeting`, `CLASSIFICATION_START` from `events.classification`, `CONSOLIDATION_START` from `events.consolidation`, `ORG_MEMORY_QUERY_START` from `events.org_memory`, `API_REQUEST_STARTED` from `events.api`, `API_REQUEST_COMPLETED` from `events.api`, `API_REQUEST_ERROR` from `events.api`, `API_ROUTE_NOT_FOUND` from `events.api`, `API_HEALTH_CHECK` from `events.api`, `API_COORDINATION_STARTED` from `events.api`, `API_COORDINATION_COMPLETED` from `events.api`, `API_COORDINATION_FAILED` from `events.api`, `API_COORDINATION_AGENT_RESOLVE_FAILED` from `events.api`, `API_CONTENT_NEGOTIATED` from `events.api`, `API_CORRELATION_FALLBACK` from `events.api`, `API_ACCEPT_PARSE_FAILED` from `events.api`, `API_WS_TICKET_ISSUED` from `events.api`, `API_WS_TICKET_CONSUMED` from `events.api`, `API_WS_TICKET_EXPIRED` from `events.api`, `API_WS_TICKET_INVALID` from `events.api`, `API_WS_TICKET_CLEANUP` from `events.api`, `CODE_RUNNER_EXECUTE_START` from `events.code_runner`, `DOCKER_EXECUTE_START` from `events.docker`, `MCP_INVOKE_START` from `events.mcp`, `SECURITY_EVALUATE_START` from `events.security`, `HR_HIRING_REQUEST_CREATED` from `events.hr`, `PERF_METRIC_RECORDED` from `events.performance`, `PERF_LLM_SAMPLE_STARTED` from `events.performance`, `PERF_LLM_SAMPLE_COMPLETED` from `events.performance`, `PERF_LLM_SAMPLE_FAILED` from `events.performance`, `PERF_OVERRIDE_SET` from `events.performance`, `PERF_OVERRIDE_CLEARED` from `events.performance`, `PERF_OVERRIDE_APPLIED` from `events.performance`, `PERF_OVERRIDE_EXPIRED` from `events.performance`, `TRUST_EVALUATE_START` from `events.trust`, `PROMOTION_EVALUATE_START` from `events.promotion`, `PROMPT_BUILD_START` from `events.prompt`, `MEMORY_RETRIEVAL_START` from `events.memory`, `MEMORY_BACKEND_CONNECTED` from `events.memory`, `MEMORY_ENTRY_STORED` from `events.memory`, `MEMORY_BACKEND_SYSTEM_ERROR` from `events.memory`, `MEMORY_RRF_FUSION_COMPLETE` from `events.memory`, `MEMORY_RRF_VALIDATION_FAILED` from `events.memory`, `AUTONOMY_ACTION_AUTO_APPROVED` from `events.autonomy`, `TIMEOUT_POLICY_EVALUATED` from `events.timeout`, `PERSISTENCE_AUDIT_ENTRY_SAVED` from `events.persistence`, `TASK_ENGINE_STARTED` from `events.task_engine`, `COORDINATION_STARTED` from `events.coordination`, `COORDINATION_FACTORY_BUILT` from `events.coordination`, `COMMUNICATION_DISPATCH_START` from `events.communication`, `COMPANY_STARTED` from `events.company`, `CONFIG_LOADED` from `events.config`, `CORRELATION_ID_CREATED` from `events.correlation`, `DECOMPOSITION_STARTED` from `events.decomposition`, `DELEGATION_STARTED` from `events.delegation`, `EXECUTION_LOOP_START` from `events.execution`, `CHECKPOINT_SAVED` from `events.checkpoint`, `PERSISTENCE_CHECKPOINT_SAVED` from `events.persistence`, `GIT_COMMAND_START` from `events.git`, `GIT_CLONE_URL_REJECTED` from `events.git`, `GIT_CLONE_SSRF_BLOCKED` from `events.git`, `GIT_CLONE_DNS_FAILED` from `events.git`, `GIT_CLONE_SSRF_DISABLED` from `events.git`, `PARALLEL_GROUP_START` from `events.parallel`, `PERSONALITY_LOADED` from `events.personality`, `QUOTA_CHECKED` from `events.quota`, `ROLE_ASSIGNED` from `events.role`, `ROUTING_STARTED` from `events.routing`, `SANDBOX_EXECUTE_START` from `events.sandbox`, `TASK_CREATED` from `events.task`, `TASK_ASSIGNMENT_STARTED` from `events.task_assignment`, `TASK_ROUTING_STARTED` from `events.task_routing`, `TEMPLATE_LOADED` from `events.template`, `TOOL_INVOKE_START` from `events.tool`, `TOOL_OUTPUT_WITHHELD` from `events.tool`, `TOOL_FACTORY_BUILT` from `events.tool`, `WORKSPACE_CREATED` from `events.workspace`, `APPROVAL_GATE_ESCALATION_DETECTED` from `events.approval_gate`, `APPROVAL_GATE_ESCALATION_FAILED` from `events.approval_gate`, `APPROVAL_GATE_INITIALIZED` from `events.approval_gate`, `APPROVAL_GATE_RISK_CLASSIFIED` from `events.approval_gate`, `APPROVAL_GATE_RISK_CLASSIFY_FAILED` from `events.approval_gate`, `APPROVAL_GATE_CONTEXT_PARKED` from `events.approval_gate`, `APPROVAL_GATE_CONTEXT_PARK_FAILED` from `events.approval_gate`, `APPROVAL_GATE_PARK_TASKLESS` from `events.approval_gate`, `APPROVAL_GATE_RESUME_STARTED` from `events.approval_gate`, `APPROVAL_GATE_CONTEXT_RESUMED` from `events.approval_gate`, `APPROVAL_GATE_RESUME_FAILED` from `events.approval_gate`, `APPROVAL_GATE_RESUME_DELETE_FAILED` from `events.approval_gate`, `APPROVAL_GATE_RESUME_TRIGGERED` from `events.approval_gate`, `APPROVAL_GATE_NO_PARKED_CONTEXT` from `events.approval_gate`, `APPROVAL_GATE_LOOP_WIRING_WARNING` from `events.approval_gate`, `STAGNATION_CHECK_PERFORMED` from `events.stagnation`, `STAGNATION_DETECTED` from `events.stagnation`, `STAGNATION_CORRECTION_INJECTED` from `events.stagnation`, `STAGNATION_TERMINATED` from `events.stagnation`, `PERSISTENCE_AGENT_STATE_SAVED` from `events.persistence`, `PERSISTENCE_AGENT_STATE_FETCHED` from `events.persistence`, `PERSISTENCE_AGENT_STATE_ACTIVE_QUERIED` from `events.persistence`, `PERSISTENCE_AGENT_STATE_DELETED` from `events.persistence`, `SETTINGS_VALUE_SET` from `events.settings`, `SETTINGS_VALUE_DELETED` from `events.settings`, `SETTINGS_VALUE_RESOLVED` from `events.settings`, `SETTINGS_CACHE_INVALIDATED` from `events.settings`, `SETTINGS_ENCRYPTION_ERROR` from `events.settings`, `SETTINGS_VALIDATION_FAILED` from `events.settings`, `SETTINGS_NOTIFICATION_PUBLISHED` from `events.settings`, `SETTINGS_NOTIFICATION_FAILED` from `events.settings`, `SETTINGS_FETCH_FAILED` from `events.settings`, `SETTINGS_SET_FAILED` from `events.settings`, `SETTINGS_DELETE_FAILED` from `events.settings`, `SETTINGS_NOT_FOUND` from `events.settings`, `SETTINGS_REGISTRY_DUPLICATE` from `events.settings`, `SETTINGS_CONFIG_PATH_MISS` from `events.settings`). Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`

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

This list of example event constants is becoming very long and is difficult to navigate. To improve readability and make it easier to check if an event is already listed, please consider sorting the constants alphabetically within the e.g., (...) block.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
- **Structured kwargs**: always `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
Expand Down
3 changes: 3 additions & 0 deletions src/synthorg/observability/events/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
TOOL_PERMISSION_CHECKER_CREATED: Final[str] = "tool.permission.checker_created"
TOOL_PERMISSION_FILTERED: Final[str] = "tool.permission.filtered"

# ── Factory events ──────────────────────────────────────────────
TOOL_FACTORY_BUILT: Final[str] = "tool.factory.built"

# ── File system tool events ──────────────────────────────────────
TOOL_FS_READ: Final[str] = "tool.fs.read"
TOOL_FS_WRITE: Final[str] = "tool.fs.write"
Expand Down
3 changes: 3 additions & 0 deletions src/synthorg/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
ToolPermissionDeniedError,
)
from .examples.echo import EchoTool
from .factory import build_default_tools, build_default_tools_from_config
from .file_system import (
BaseFileSystemTool,
DeleteFileTool,
Expand Down Expand Up @@ -86,4 +87,6 @@
"ToolPermissionDeniedError",
"ToolRegistry",
"WriteFileTool",
"build_default_tools",
"build_default_tools_from_config",
]
119 changes: 119 additions & 0 deletions src/synthorg/tools/factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Tool factory — instantiate built-in workspace tools with config-driven parameters.

Provides ``build_default_tools`` (core factory) and
``build_default_tools_from_config`` (convenience wrapper that
extracts parameters from a ``RootConfig``). Both return
``tuple[BaseTool, ...]`` so callers can extend before wrapping
in a ``ToolRegistry``.
"""

from typing import TYPE_CHECKING

from synthorg.observability import get_logger
from synthorg.observability.events.tool import TOOL_FACTORY_BUILT
from synthorg.tools.file_system import (
DeleteFileTool,
EditFileTool,
ListDirectoryTool,
ReadFileTool,
WriteFileTool,
)
from synthorg.tools.git_tools import (
GitBranchTool,
GitCloneTool,
GitCommitTool,
GitDiffTool,
GitLogTool,
GitStatusTool,
)

if TYPE_CHECKING:
from pathlib import Path

from synthorg.config.schema import RootConfig
from synthorg.tools.base import BaseTool
from synthorg.tools.git_url_validator import GitCloneNetworkPolicy
from synthorg.tools.sandbox.protocol import SandboxBackend

logger = get_logger(__name__)


def build_default_tools(
*,
workspace: Path,
git_clone_policy: GitCloneNetworkPolicy | None = None,
sandbox: SandboxBackend | None = None,
) -> tuple[BaseTool, ...]:
"""Instantiate all built-in workspace tools.

Returns a sorted tuple of tool instances ready for use or
extension before wrapping in a ``ToolRegistry``.

Args:
workspace: Absolute path to the agent workspace root.
git_clone_policy: Network policy for git clone SSRF
prevention. ``None`` uses the default (block all
private IPs, empty hostname allowlist).
sandbox: Optional sandbox backend for git subprocess
isolation.

Returns:
Tuple of ``BaseTool`` instances sorted by name.
"""
fs_tools: list[BaseTool] = [
ReadFileTool(workspace_root=workspace),
WriteFileTool(workspace_root=workspace),
EditFileTool(workspace_root=workspace),
ListDirectoryTool(workspace_root=workspace),
DeleteFileTool(workspace_root=workspace),
]

git_tools: list[BaseTool] = [
GitStatusTool(workspace=workspace, sandbox=sandbox),
GitLogTool(workspace=workspace, sandbox=sandbox),
GitDiffTool(workspace=workspace, sandbox=sandbox),
GitBranchTool(workspace=workspace, sandbox=sandbox),
GitCommitTool(workspace=workspace, sandbox=sandbox),
GitCloneTool(
workspace=workspace,
sandbox=sandbox,
network_policy=git_clone_policy,
),
]

result = tuple(sorted(fs_tools + git_tools, key=lambda t: t.name))

logger.info(
TOOL_FACTORY_BUILT,
tool_count=len(result),
tools=tuple(t.name for t in result),
)

return result
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated


def build_default_tools_from_config(
*,
workspace: Path,
config: RootConfig,
sandbox: SandboxBackend | None = None,
) -> tuple[BaseTool, ...]:
"""Build default tools using parameters from a ``RootConfig``.

Convenience wrapper that extracts ``config.git_clone`` and
delegates to :func:`build_default_tools`.

Args:
workspace: Absolute path to the agent workspace root.
config: Validated root configuration.
sandbox: Optional sandbox backend for git subprocess
isolation.

Returns:
Tuple of ``BaseTool`` instances sorted by name.
"""
return build_default_tools(
workspace=workspace,
git_clone_policy=config.git_clone,
sandbox=sandbox,
)
Loading
Loading