oclaw/runtime/tools/public/skills_install_tool.py
oliver d14e9d3596 feat(persistence): PostgreSQL assistant store, chat persist fixes, gateway scripts
- Add SQLAlchemy Core repos, pg adapter/compat, assistant_store factory, Alembic bootstrap and migration/cutover scripts.

- Harden chat_message writes (NUL scrub for PG), turn_uuid on attempt failure, WS turn_runner fallbacks and gateway executed_turn_uuid init.

- start_gateway: log paths, PS7 stderr handling via cmd, background stdout/stderr redirect; runtime assistant_runtime_log_dir export.

- Ops: clear_all_chat_sessions with PG-only --postgresql and env-gated wipe; clear_postgres_chat_sessions.ps1.

- Tests: SA repos, pg compat, persist fallback, smoke env isolation; CI and docs touch-ups.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 09:33:50 +08:00

142 lines
5.1 KiB
Python

from __future__ import annotations
from pathlib import Path
from typing import Any
from svc.config.paths import db_path
from svc.persistence.sqlite_store import SqliteStore
from svc.persistence.assistant_store import get_assistant_store
from runtime.skill_installer import install_skill_from_registry_archive
from runtime.skills import default_skills_root
from runtime.skills_market import get_market_adapter, normalize_skill_market_provider_setting
from runtime.tools.base import ToolSpec
def _store() -> SqliteStore:
return get_assistant_store()
def _agent_workspace_skills_root() -> Path:
# Agent-origin installs are isolated under _workspace lane.
return default_skills_root() / "_workspace"
def skill_market_install_tool() -> ToolSpec:
def _handler(args: dict[str, Any]) -> dict[str, Any]:
payload = args if isinstance(args, dict) else {}
slug = str(payload.get("slug") or "").strip()
if not slug:
return {"ok": False, "error_code": "slug_required", "error": "slug_required"}
version = str(payload.get("version") or "").strip() or None
overwrite = bool(payload.get("overwrite"))
store = _store()
provider_arg = str(payload.get("provider") or "").strip()
if provider_arg:
provider = normalize_skill_market_provider_setting(provider_arg)
else:
provider = normalize_skill_market_provider_setting(str(store.get_setting("AIA_SKILL_MARKET_PROVIDER") or ""))
try:
adapter = get_market_adapter(provider)
archive_url, chosen_version = adapter.resolve_archive_url(slug=slug, version=version)
except Exception as exc:
return {
"ok": False,
"error_code": "market_resolve_failed",
"error": f"market_resolve_failed:{type(exc).__name__}",
"provider": provider,
"slug": slug,
}
if not str(archive_url or "").strip():
return {
"ok": False,
"error_code": "archive_url_unavailable",
"error": "archive_url_unavailable",
"provider": provider,
"slug": slug,
}
out = install_skill_from_registry_archive(
store=store,
archive_url=str(archive_url),
overwrite=overwrite,
skills_root=_agent_workspace_skills_root(),
)
return {
"ok": bool(out.ok),
"result": {
"name": out.name,
"target_dir": out.target_dir,
"detail": out.detail,
"error_code": out.error_code,
"retryable": bool(out.retryable),
},
"provider": provider,
"slug": slug,
"version": str(chosen_version or version or ""),
}
return ToolSpec(
name="skill_market_install",
description="Install a skill from configured market by slug/version.",
parameters={
"type": "object",
"properties": {
"slug": {"type": "string"},
"provider": {"type": "string", "description": "Optional provider override: clawhub or cocoloop."},
"version": {"type": "string"},
"overwrite": {"type": "boolean"},
},
"required": ["slug"],
"additionalProperties": False,
},
handler=_handler,
tags=frozenset({"skill", "installer", "market"}),
risk_level="medium",
timeout_s=120.0,
)
def skill_registry_install_tool() -> ToolSpec:
def _handler(args: dict[str, Any]) -> dict[str, Any]:
payload = args if isinstance(args, dict) else {}
archive_url = str(payload.get("archive_url") or "").strip()
if not archive_url:
return {"ok": False, "error_code": "archive_url_required", "error": "archive_url_required"}
overwrite = bool(payload.get("overwrite"))
out = install_skill_from_registry_archive(
store=_store(),
archive_url=archive_url,
overwrite=overwrite,
skills_root=_agent_workspace_skills_root(),
)
return {
"ok": bool(out.ok),
"result": {
"name": out.name,
"target_dir": out.target_dir,
"detail": out.detail,
"error_code": out.error_code,
"retryable": bool(out.retryable),
},
}
return ToolSpec(
name="skill_registry_install",
description="Install a skill from archive URL (registry/market artifact).",
parameters={
"type": "object",
"properties": {
"archive_url": {"type": "string"},
"overwrite": {"type": "boolean"},
},
"required": ["archive_url"],
"additionalProperties": False,
},
handler=_handler,
tags=frozenset({"skill", "installer", "registry"}),
risk_level="medium",
timeout_s=120.0,
)
__all__ = ["skill_market_install_tool", "skill_registry_install_tool"]