mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 22:40:45 +08:00
修复 Admin Skills 安装区块不显示,并支持卸载 _workspace/public lane。
同时补齐 _workspace/public 目录约定、公共技能免绑定规则,并为 run_command 增加默认禁用开关与回归测试。 Made-with: Cursor
This commit is contained in:
parent
da2413afad
commit
5d67303d22
11 changed files with 118 additions and 5 deletions
|
|
@ -355,11 +355,32 @@ def uninstall_skill(
|
|||
ec, rt = _classify_install_detail("name_required")
|
||||
return SkillInstallResult(ok=False, name="", target_dir="", detail="name_required", error_code=ec, retryable=rt)
|
||||
root = Path(skills_root).resolve() if skills_root else default_skills_root()
|
||||
candidates = [root / name, root / "_workspace" / name]
|
||||
target = next((p for p in candidates if p.exists() and p.is_dir()), None)
|
||||
# Prefer manifest directory resolution to handle workspace lanes and public lane.
|
||||
target: Path | None = None
|
||||
try:
|
||||
manifests = list(discover_workspace_skill_manifests(root))
|
||||
m = next((x for x in manifests if str(x.name or "").strip() == name), None)
|
||||
if m and str(m.skill_dir or "").strip():
|
||||
target = Path(str(m.skill_dir)).resolve()
|
||||
except Exception:
|
||||
target = None
|
||||
if target is None:
|
||||
# Fallback to legacy lane guesses.
|
||||
candidates = [root / name, root / "_workspace" / name, root / "_workspace" / "public" / name]
|
||||
target = next((p for p in candidates if p.exists() and p.is_dir()), None)
|
||||
if target is None:
|
||||
ec, rt = _classify_install_detail("not_found")
|
||||
return SkillInstallResult(ok=False, name=name, target_dir="", detail="not_found", error_code=ec, retryable=rt)
|
||||
# Safety: only allow uninstall within the configured skills root.
|
||||
try:
|
||||
root_r = root.resolve()
|
||||
ws_r = (root_r / "_workspace").resolve()
|
||||
if not (str(target).startswith(str(root_r)) or str(target).startswith(str(ws_r))):
|
||||
ec, rt = _classify_install_detail("runtime_error")
|
||||
return SkillInstallResult(ok=False, name=name, target_dir=str(target), detail="remove_failed:path_restricted", error_code=ec, retryable=rt)
|
||||
except Exception:
|
||||
ec, rt = _classify_install_detail("runtime_error")
|
||||
return SkillInstallResult(ok=False, name=name, target_dir=str(target), detail="remove_failed:path_restricted", error_code=ec, retryable=rt)
|
||||
try:
|
||||
shutil.rmtree(target)
|
||||
except Exception as exc:
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@ def allowed_workspace_skill_names_for_role(*, store: Any, role: str) -> set[str]
|
|||
r = str(role or "").strip().lower()
|
||||
if not r:
|
||||
return set()
|
||||
from oclaw.runtime.skills import discover_public_workspace_skill_names
|
||||
|
||||
public = discover_public_workspace_skill_names()
|
||||
mapping = normalize_skill_role_binding(
|
||||
mapping_raw=load_skill_role_binding_dict(store),
|
||||
valid_skill_names=_all_installed_skill_names(store),
|
||||
|
|
@ -101,7 +104,7 @@ def allowed_workspace_skill_names_for_role(*, store: Any, role: str) -> set[str]
|
|||
inherit_mgr = True
|
||||
mgr = {str(x).strip() for x in (mapping.get("manager") or []) if str(x).strip()} if inherit_mgr else set()
|
||||
sp = {str(x).strip() for x in (mapping.get(r) or []) if str(x).strip()}
|
||||
return mgr | sp
|
||||
return mgr | sp | public
|
||||
|
||||
|
||||
def _all_installed_skill_names(store: Any) -> set[str]:
|
||||
|
|
|
|||
|
|
@ -148,6 +148,28 @@ def discover_workspace_skill_manifests(skills_root: str | Path | None = None) ->
|
|||
return tuple(out)
|
||||
|
||||
|
||||
def discover_public_workspace_skill_names(skills_root: str | Path | None = None) -> set[str]:
|
||||
"""Skill names under `<skills_root>/_workspace/public/<skill>/SKILL.md`.
|
||||
|
||||
These skills are treated as public and do not require role binding.
|
||||
"""
|
||||
base = Path(skills_root).resolve() if skills_root else default_skills_root()
|
||||
public_root = (base / "_workspace" / "public").resolve()
|
||||
if not public_root.exists() or not public_root.is_dir():
|
||||
return set()
|
||||
out: set[str] = set()
|
||||
try:
|
||||
for md in public_root.rglob("SKILL.md"):
|
||||
if not md.is_file():
|
||||
continue
|
||||
m = load_skill_manifest(md.parent)
|
||||
if m and str(m.name or "").strip():
|
||||
out.add(str(m.name).strip())
|
||||
except Exception:
|
||||
return set()
|
||||
return out
|
||||
|
||||
|
||||
def _tool_origin(tool: "ToolSpec") -> str:
|
||||
nm = str(getattr(tool, "name", "") or "")
|
||||
if nm.startswith("mcp__"):
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
用于官方/手工管理的稳定技能(安装、维护、评审都在这层)。
|
||||
- **自写目录**:`oclaw/runtime/skills/_workspace/<skill_name>/`
|
||||
用于 agent 自写/自动安装技能,和主目录隔离,避免混放。
|
||||
- **公共目录**:`oclaw/runtime/skills/_workspace/public/<skill_name>/`
|
||||
放在这里的 skill 默认对所有人可用,不需要做角色绑定(适合通用能力)。
|
||||
|
||||
说明:
|
||||
- `auto_install_skill_from_payload` 产物默认落在 `_workspace` 下。
|
||||
|
|
|
|||
16
runtime/skills/_workspace/public/README.md
Normal file
16
runtime/skills/_workspace/public/README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
本目录用于 **公共 skills**。
|
||||
|
||||
约定:
|
||||
- 路径:`oclaw/runtime/skills/_workspace/public/<skill_name>/SKILL.md`
|
||||
- 放在这里的 skill **默认对所有人可用**,不需要在 Admin 里做角色绑定。
|
||||
|
||||
注意:
|
||||
- 仍会受 skill 本身的 `disable_model_invocation`、租户禁用列表等策略影响。
|
||||
本目录用于 **公共 skills**。
|
||||
|
||||
约定:
|
||||
- 路径:`oclaw/runtime/skills/_workspace/public/<skill_name>/SKILL.md`
|
||||
- 放在这里的 skill **默认对所有人可用**,不需要在 Admin 里做角色绑定。
|
||||
|
||||
注意:
|
||||
- 仍会受 skill 本身的 `disable_model_invocation`、租户禁用列表等策略影响。
|
||||
|
|
@ -70,6 +70,31 @@ class LocalAdapter:
|
|||
cmd = str(command or "").strip()
|
||||
if not cmd:
|
||||
return {"ok": False, "error_code": "command_required", "error": "command_required"}
|
||||
# Safety gate: disabled by default unless explicitly enabled.
|
||||
try:
|
||||
def _truthy(v: str | None) -> bool:
|
||||
return str(v or "").strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
enabled: bool | None = None
|
||||
# Prefer DB setting when available.
|
||||
dbp = str(os.getenv("OPS_ASSISTANT_DB_PATH") or "").strip()
|
||||
if dbp:
|
||||
try:
|
||||
from oclaw.platform.persistence.sqlite_store import SqliteStore
|
||||
|
||||
store = SqliteStore(dbp)
|
||||
raw_db = str(store.get_setting("AIA_ENABLE_RUN_COMMAND") or "").strip()
|
||||
if raw_db:
|
||||
enabled = _truthy(raw_db)
|
||||
except Exception:
|
||||
enabled = None
|
||||
if enabled is None:
|
||||
raw_env = str(os.getenv("AIA_ENABLE_RUN_COMMAND") or "").strip()
|
||||
enabled = _truthy(raw_env) if raw_env else False
|
||||
if not enabled:
|
||||
return {"ok": False, "error_code": "disabled", "error": "disabled"}
|
||||
except Exception:
|
||||
return {"ok": False, "error_code": "disabled", "error": "disabled"}
|
||||
try:
|
||||
timeout_s = max(1, min(int(timeout or 30), 600))
|
||||
# run_command never follows adapter cd state.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue