Skip to content

Feature: Bot/Gateway parity with hermes-agent + openclaw — expanded defaults, self-improving skill_manage, per-session workspace sandbox #1499

@MervinPraison

Description

@MervinPraison

Overview

Bring PraisonAI's bot / gateway default agent up to parity with two reference implementations — hermes-agent (~/hermes-agent) and OpenClaw (~/openclaw) — by shipping (1) a richer default tool set, (2) a full self-improving "skills" write path (skill_manage), and (3) a per-bot / per-session workspace sandbox that scopes every file-touching tool to a single directory with read/write/read-only/none access modes.

After #1498 the bot gets a small set of safe defaults (search_web, web_crawl, schedule_*, store/search_memory, store/search_learning). Hermes-agent and OpenClaw ship file I/O, shell / code execution, planning (todo), delegation / subagents, session history search, and skill management out of the box — all scoped to an explicit workspace directory. PraisonAI currently has none of those gated tools wired into bot defaults, no skill_manage tool, and no workspaceDir concept (file tools fall back to os.getcwd() of the daemon, which is not per-session).

This issue delivers all three gaps in one coordinated change, preserving backward compatibility and the "safe by default" invariant.


Background

Hermes-agent (~/hermes-agent)

Default core toolset (~/hermes-agent/toolsets.py, _HERMES_CORE_TOOLS, lines 31-63) — shared across CLI and every messaging-platform bot:

Category Tools
Web web_search, web_extract
Terminal / process terminal, process
File (workspace-scoped) read_file, write_file, patch, search_files
Vision / image vision_analyze, image_generate
Skills (self-improving) skills_list, skill_view, skill_manage
Browser browser_navigate, browser_snapshot, browser_click, browser_type, …
Planning / memory todo, memory, session_search, clarify
Code exec / delegation execute_code, delegate_task
Cron / messaging cronjob, send_message
Home Assistant ha_* (gated on env)

Skills lifecycle (~/hermes-agent/tools/skill_manager_tool.py) — the self-improving agent concept:

skill_manage(action, name, ...) →  create | edit | patch | delete | write_file | remove_file
  • Skills live at ~/.hermes/skills/<name>/SKILL.md + references/, templates/, scripts/, assets/
  • Every write goes through skills_guard.scan_skill (prompt-injection scan, rollback on block)
  • Atomic writes via temp-file + os.replace
  • path_security.validate_within_dir containment check + has_traversal_component guard
  • MAX_NAME_LENGTH=64, MAX_SKILL_CONTENT_CHARS=100_000, MAX_SKILL_FILE_BYTES=1_048_576
  • Fuzzy patch action shared with file-patch tool (fuzzy_find_and_replace)
  • After write, clears cached system prompt so next turn sees the new skill

Workspace scoping: weaker — relies on terminal.cwd + optional Docker sandbox. Skills dir is the only hard-enforced boundary.

OpenClaw (~/openclaw)

Default bot toolset (~/openclaw/src/agents/openclaw-tools.ts:230-303) — every tool factory is passed an explicit workspaceDir:

canvas, nodes, cron, message, tts, image_generate, music_generate, video_generate, gateway, agents_list, update_plan, sessions_list, sessions_history, sessions_send, sessions_yield, sessions_spawn, subagents, session_status, web_search, web_fetch, image (vision), pdf, + plugin tools

Sandbox tool policy (~/openclaw/src/agents/sandbox/constants.ts:13-38):

DEFAULT_TOOL_ALLOW = ["exec","process","read","write","edit","apply_patch","image","sessions_*","subagents","session_status"]
DEFAULT_TOOL_DENY  = ["browser","canvas","nodes","cron","gateway", ...CHANNEL_IDS]

Workspace system — the piece PraisonAI is missing:

  • DEFAULT_AGENT_WORKSPACE_DIR constant + normalizeWorkspaceDir() / resolveWorkspaceRoot() (~/openclaw/src/agents/workspace-dir.ts) — refuses filesystem root, expands ~, defaults to process.cwd().
  • DEFAULT_SANDBOX_WORKSPACE_ROOT = path.join(STATE_DIR, "sandboxes") — per-session sandbox copies live here (~/openclaw/src/agents/sandbox/constants.ts:5).
  • ensureSandboxWorkspaceLayout() (~/openclaw/src/agents/sandbox/context.ts:26-82) decides the effective workspace based on:
    • cfg.scope ∈ {"shared", "session", "agent"}resolveSandboxScopeKey(...)
    • cfg.workspaceAccess ∈ {"rw", "ro", "none"}rw uses the real agent dir, ro/none use a copied sandbox dir
  • Every file/image/pdf tool factory receives workspaceDir + optional sandbox: { root, bridge } + fsPolicy: ToolFsPolicy. Tools that read or write outside the workspace are rejected upstream.
  • applyNodesToolWorkspaceGuard() wraps any tool that can touch the filesystem to enforce containment.
  • Skills are synced into the sandbox via syncSkillsToWorkspace when scope is non-rw.

PraisonAI — current state

Bot defaults (src/praisonai-agents/praisonaiagents/bots/config.py:53-58 + src/praisonai/praisonai/bots/_defaults.py):

default_tools = [
    "search_web", "web_crawl",
    "schedule_add", "schedule_list", "schedule_remove",
    "store_memory", "search_memory",
    "store_learning", "search_learning",
]

No file I/O, no shell, no planning, no delegation, no session_search, no skill management — explicitly excluded as "destructive" in _get_default_safe_tools.

Skills system (src/praisonai-agents/praisonaiagents/skills/) is read/activate only:

  • SkillManager (manager.py) exposes discover, add_skill, get_skill, activate, invoke, to_prompt, get_instructions, load_resources, clear.
  • tools/skill_tools.py exposes run_skill_script (execute only, @require_approval("critical")).
  • There is no create_skill / edit_skill / patch_skill / delete_skill / write_skill_file — the agent cannot turn successful runs into reusable procedural memory.

Workspace — there is no bot/gateway workspace concept:

  • FileTools._validate_path (src/praisonai-agents/praisonaiagents/tools/file_tools.py:25-56) uses os.getcwd() as the allowed root — which in bot mode is wherever the daemon was started (often / or the user's home).
  • BotConfig has no workspace_dir / workspace_access / workspace_scope fields.
  • SkillTools.__init__(working_directory=None) defaults to os.getcwd() — same issue.
  • No per-user, per-channel, or per-session workspace isolation.

Architecture Analysis

Key file locations

File Purpose Notes
src/praisonai-agents/praisonaiagents/bots/config.py BotConfig dataclass (default_tools, auto_approve_tools) Needs workspace_* fields
src/praisonai/praisonai/bots/_defaults.py apply_bot_smart_defaults + _get_default_safe_tools Needs expanded default set + workspace-scoped file tools
src/praisonai-agents/praisonaiagents/tools/file_tools.py FileTools.read_file / write_file / list_files / copy_file / delete_file _validate_path hard-codes os.getcwd() — needs injectable root
src/praisonai-agents/praisonaiagents/tools/skill_tools.py SkillTools.run_skill_script Extend with skill_manage equivalent
src/praisonai-agents/praisonaiagents/skills/manager.py SkillManager (read/activate) Add write API (create, edit, patch, delete, write_file, remove_file)
src/praisonai-agents/praisonaiagents/skills/discovery.py Skill discovery Source of truth for skill dirs
src/praisonai-agents/praisonaiagents/tools/path_overlap.py Path helpers Good place for validate_within_dir port
src/praisonai/praisonai/bots/bot.py Bot class — wires BotConfigAgent Needs to construct workspace-scoped tool factories
src/praisonai/praisonai/gateway/server.py WebSocketGateway Same defaults as Bot (per #1498 fix)

Control flow

Incoming message
  → Bot / WebSocketGateway
    → apply_bot_smart_defaults(agent, config)
        → resolves config.default_tools → tool instances
        → [NEW] constructs WorkspaceScope(root, access, scope) from config
        → [NEW] instantiates file/skill/shell tools with workspace=WorkspaceScope
    → agent.chat(message)
       → tool call → tool.run(..., workspace=scope) → path containment enforced

Gap Analysis Summary

Critical gaps

Gap Impact Effort
No workspace concept for bot/gateway File tools read/write arbitrary host paths; can't safely enable file tools by default M
No skill_manage write API Agent can load skills but cannot author or evolve them — "self-improving" concept is incomplete M
Default tool set too narrow for useful bots Users must manually wire read_file, write_file, edit_file, todo, subagents S
FileTools._validate_path coupled to os.getcwd() Blocks workspace scoping; wrong root in daemon mode S

Feature gaps (hermes/openclaw → praisonai)

Feature Hermes OpenClaw Praisonai today Proposed default
read_file ✅ (read) ✅ tool exists, not defaulted default (workspace-scoped)
write_file ✅ (write) ✅ tool exists, not defaulted default (workspace-scoped)
edit_file / patch ✅ (edit, apply_patch) partial (no fuzzy patch tool) default (new tool)
search_files missing default (new tool)
execute_code / terminal ✅ (exec, process) shell_tools exists, gated opt-in (auto-approve off)
todo planning ✅ (update_plan) missing in defaults default
delegate_task / subagents subagent_tool exists, not defaulted default
session_search / sessions_history missing default
skills_list / skill_view via SKILL.md partial (via SkillManager only) default (new tools)
skill_manage (write) ✅ (edit-in-workspace) missing default (new tool)
web_search / web_extract ✅ in defaults keep default
vision_analyze / image_generate tools exist opt-in
Workspace sandbox (rw/ro/none) partial (Docker only) ✅ full missing ship full

Proposed Implementation

Phase 1 — Workspace scope (foundation)

Add an explicit Workspace dataclass in the core SDK and thread it through bot config + every file-touching tool.

# src/praisonai-agents/praisonaiagents/workspace/__init__.py  (NEW)
from dataclasses import dataclass
from pathlib import Path
from typing import Literal, Optional

WorkspaceAccess = Literal["rw", "ro", "none"]
WorkspaceScope  = Literal["shared", "session", "user", "agent"]

@dataclass(frozen=True)
class Workspace:
    root: Path                       # absolute, must exist, refuses "/"
    access: WorkspaceAccess = "rw"   # ro = read-only; none = copy-on-write sandbox
    scope:  WorkspaceScope  = "session"
    session_key: Optional[str] = None

    def contains(self, path: str | Path) -> bool: ...
    def resolve(self, path: str | Path) -> Path:
        """Resolve relative paths against root; reject '..' traversal and escapes."""
    @classmethod
    def from_config(cls, cfg: "BotConfig", *, session_key: str) -> "Workspace": ...

BotConfig additions (src/praisonai-agents/praisonaiagents/bots/config.py):

workspace_dir: Optional[str] = None        # default: ~/.praisonai/workspaces/<bot>/<session_key>
workspace_access: WorkspaceAccess = "rw"   # rw (default) | ro | none
workspace_scope:  WorkspaceScope  = "session"  # shared | session | user | agent

File tools refactorFileTools becomes workspace-aware (backward compatible):

class FileTools:
    def __init__(self, workspace: Workspace | None = None):
        self._workspace = workspace  # None = legacy os.getcwd() behaviour

    def _validate_path(self, filepath: str) -> str:
        if self._workspace is not None:
            return str(self._workspace.resolve(filepath))
        # existing os.getcwd()-based behaviour

Phase 2 — Skill write API (self-improving agent)

Extend SkillManager with the hermes skill_manage surface:

# src/praisonai-agents/praisonaiagents/skills/manager.py  (extend)
class SkillManager:
    def create_skill(self, name, content, category=None) -> dict: ...
    def edit_skill(self, name, content) -> dict: ...          # full rewrite
    def patch_skill(self, name, old_string, new_string,
                    file_path=None, replace_all=False) -> dict:
        """Fuzzy find-and-replace; shares engine with file edit tool."""
    def delete_skill(self, name) -> dict: ...
    def write_skill_file(self, name, file_path, file_content) -> dict:
        """file_path must be under references/ templates/ scripts/ assets/"""
    def remove_skill_file(self, name, file_path) -> dict: ...

Expose as an agent tool (src/praisonai-agents/praisonaiagents/tools/skill_tools.py):

class SkillTools:
    def __init__(self, workspace: Workspace | None = None,
                 skills_dir: str | None = None):
        self._workspace = workspace
        self._skills_dir = Path(skills_dir or "~/.praisonai/skills").expanduser()

    @require_approval(risk_level="low")  # auto-approvable for bots
    def skill_manage(self, action: str, name: str, *,
                     content=None, category=None, file_path=None,
                     file_content=None, old_string=None,
                     new_string=None, replace_all=False) -> str:
        """Hermes-compatible schema. Dispatches to SkillManager.*"""

Security guardrails (port from hermes skills_guard):

  • Name regex ^[a-z0-9][a-z0-9._-]*$, max 64 chars
  • Max SKILL.md 100_000 chars, max supporting file 1 MiB
  • .. traversal rejection + validate_within_dir(path, SKILLS_DIR) containment
  • Atomic write via tempfile.mkstemp + os.replace
  • Optional prompt-injection scan hook (pluggable; noop until skills_guard ported)
  • Rollback on scan block

Phase 3 — Expanded bot defaults

Update _get_default_safe_tools (src/praisonai/praisonai/bots/_defaults.py) + BotConfig.default_tools:

default_tools = [
    # Web (existing)
    "search_web", "web_crawl",
    # Memory / learning (existing)
    "store_memory", "search_memory",
    "store_learning", "search_learning",
    # Scheduling (existing)
    "schedule_add", "schedule_list", "schedule_remove",
    # Files — NEW (workspace-scoped, safe by construction)
    "read_file", "write_file", "edit_file", "list_files", "search_files",
    # Planning — NEW
    "todo_add", "todo_list", "todo_update",
    # Skills (self-improving) — NEW
    "skills_list", "skill_view", "skill_manage",
    # Delegation — NEW
    "delegate_task",
    # Session — NEW
    "session_search",
]

All file tools are constructed with the bot's Workspace, so enabling them by default is safe — the agent physically cannot read or write outside workspace_dir.

execute_command / shell_command remain opt-in (listed in destructive_tools set).


Files to Create / Modify

New files

File Purpose
src/praisonai-agents/praisonaiagents/workspace/__init__.py Workspace dataclass + helpers
src/praisonai-agents/praisonaiagents/workspace/protocols.py WorkspaceProtocol, FsPolicyProtocol
src/praisonai-agents/praisonaiagents/skills/guard.py scan_skill, should_allow_install (port from hermes)
src/praisonai-agents/praisonaiagents/tools/edit_tool.py Fuzzy patch tool shared by file + skill edits
src/praisonai-agents/tests/unit/workspace/test_workspace.py Containment, .. rejection, rw/ro/none modes
src/praisonai-agents/tests/unit/skills/test_skill_manage.py create/edit/patch/delete/write_file/remove_file + security
src/praisonai/tests/unit/bots/test_workspace_sandbox.py File tool scoping end-to-end in bot defaults
src/praisonai/tests/integration/bots/test_skill_manage_daemon.py Real agentic test: bot creates skill, uses it next turn
examples/python/bots/self_improving_bot.py End-to-end skill_manage demo

Modified files

File Change
src/praisonai-agents/praisonaiagents/bots/config.py Add workspace_dir, workspace_access, workspace_scope; expand default_tools; add fields to to_dict
src/praisonai-agents/praisonaiagents/tools/file_tools.py Accept optional workspace; _validate_path uses workspace when set
src/praisonai-agents/praisonaiagents/tools/skill_tools.py Add skill_manage, skill_view, skills_list methods; workspace-aware
src/praisonai-agents/praisonaiagents/skills/manager.py Add create_skill, edit_skill, patch_skill, delete_skill, write_skill_file, remove_skill_file
src/praisonai-agents/praisonaiagents/skills/__init__.py Export new API + SkillManagerWriteMixin
src/praisonai-agents/praisonaiagents/tools/__init__.py Export edit_file, search_files, todo_*, skill_manage, skill_view, skills_list, session_search
src/praisonai/praisonai/bots/_defaults.py Build Workspace from config; construct file / skill tools with workspace; expand default list; move new file tools out of destructive_tools
src/praisonai/praisonai/bots/bot.py Resolve default workspace_dir~/.praisonai/workspaces/<bot_name>/<session_key>; create dir on bot start
src/praisonai/praisonai/gateway/server.py Same workspace wiring for WebSocketGateway (keeps #1498 parity)
docs/features/bots.mdx (PraisonAIDocs) Document workspace_* fields, new defaults, skill_manage
docs/concepts/skills.mdx Document self-improving workflow

Technical Considerations

  • Dependencies: no new runtime deps. Fuzzy patch reuses existing difflib. Skill scan can reuse ast + regex patterns already present for tools/skill_bridge.py.
  • Performance: Workspace.resolve() calls Path.resolve() once per tool call — negligible. No module-level imports added to praisonaiagents.__init__; skills write path is lazy-loaded (only loaded when skill_manage is invoked). Verified import time budget < 200 ms.
  • Backward compat: All new fields default to current behaviour. FileTools() with no workspace works exactly as today (os.getcwd()). SkillManager without write calls is unchanged. Existing BotConfig(default_tools=[…]) still honoured.
  • Safe by default: Workspace root defaults to ~/.praisonai/workspaces/<bot>/<session>not the daemon's cwd. File tools, while now in default_tools, can only touch the workspace. skill_manage is auto-approvable because the write surface is ~/.praisonai/skills/ + the workspace, nothing else. execute_command remains opt-in.
  • Multi-agent safety: Workspace is frozen, passed by value. Each bot session gets its own instance keyed by session_key. No shared mutable state.
  • Async-safe: All new tool methods follow existing sync/async patterns. File I/O remains sync (wrapped by existing asyncio.to_thread shim used elsewhere in the SDK).

Acceptance Criteria

Workspace

  • praisonaiagents.workspace.Workspace(root, access, scope) exists and is exported
  • Workspace.resolve("foo.txt") returns absolute path inside root; Workspace.resolve("../etc/passwd") raises ValueError
  • BotConfig.workspace_dir / workspace_access / workspace_scope round-trip through to_dict
  • Bot() with no workspace_dir auto-creates ~/.praisonai/workspaces/<bot_name>/<session_key>/ on start
  • File tool called from a bot cannot read or write outside its workspace (test with path traversal)

Skills (self-improving)

  • SkillManager.create_skill(name, content) writes ~/.praisonai/skills/<name>/SKILL.md atomically with valid frontmatter
  • SkillManager.patch_skill(name, old_string, new_string) performs fuzzy replace; rejects non-unique match without replace_all
  • SkillManager.write_skill_file(name, "references/api.md", content) accepts allowed subdirs; rejects arbitrary paths
  • Name regex, size limits, traversal rejection enforced; atomic write verified (no partial file on crash)
  • After skill_manage write, next SkillManager.to_prompt() includes the new skill (cache busted)
  • Tool schema skill_manage is function-call compatible with hermes schema (actions: create/edit/patch/delete/write_file/remove_file)

Bot defaults

Real agentic test (required)

from praisonaiagents import Agent
from praisonaiagents.bots import Bot, BotConfig

bot = Bot(name="scribe", config=BotConfig(workspace_dir="./scratch"))
# No tools passed → smart defaults apply
r1 = bot.chat("Save a skill called 'weekly-report' that produces a markdown summary from a CSV path")
r2 = bot.chat("Use the weekly-report skill on ./data/jan.csv")
assert "weekly-report" in r2  # agent created and then invoked its own skill

Implementation Notes

Key files to read first

  1. ~/hermes-agent/tools/skill_manager_tool.py (789 lines) — reference implementation of skill_manage + security model
  2. ~/openclaw/src/agents/sandbox/context.ts (256 lines) — reference implementation of workspace scope/access modes
  3. ~/openclaw/src/agents/workspace-dir.ts (21 lines) — minimal workspace root normalization
  4. src/praisonai-agents/praisonaiagents/skills/manager.py (262 lines) — existing read-only SkillManager to extend
  5. src/praisonai-agents/praisonaiagents/tools/file_tools.py (441 lines) — existing FileTools to make workspace-aware
  6. src/praisonai/praisonai/bots/_defaults.py (post-fix: Bot has zero tools in daemon/gateway mode — smart defaults + auto-approve now run #1498) — existing smart-defaults injection point

Critical integration points

  1. _get_default_safe_tools(config) must call a new _resolve_with_workspace(tool_names, workspace) that instantiates FileTools(workspace=...), SkillTools(workspace=...) etc. rather than returning globally-shared tool functions.
  2. Bot.start() must materialise the workspace dir before the first agent turn.
  3. apply_bot_smart_defaults(agent, config) must set agent._workspace = workspace so any future tool the user adds can introspect it.
  4. SkillManager.create_skill must call clear_skills_system_prompt_cache() equivalent so the next turn's prompt includes the new skill (hermes pattern, tools/skill_manager_tool.py:668-672).
  5. fix: Bot has zero tools in daemon/gateway mode — smart defaults + auto-approve now run #1498 invariant: WebSocket gateway and Bot must share the same apply_bot_smart_defaults code path — do not fork.

Testing commands

# Unit — workspace
pytest src/praisonai-agents/tests/unit/workspace/ -v

# Unit — skills write path
pytest src/praisonai-agents/tests/unit/skills/test_skill_manage.py -v

# Unit — bot defaults
pytest src/praisonai/tests/unit/bots/test_workspace_sandbox.py -v

# Integration — daemon skill_manage round trip
pytest src/praisonai/tests/integration/bots/test_skill_manage_daemon.py -v

# Import-time budget
python -c "import time; t=time.time(); import praisonaiagents; print(f'{(time.time()-t)*1000:.0f}ms')"
# → must be < 200 ms

# Real agentic
python examples/python/bots/self_improving_bot.py

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingclaudeAuto-trigger Claude analysisdocumentationImprovements or additions to documentationperformancesecurity

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions