sophhub 0.4.26 → 0.4.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/config-manager/skill.json +16 -0
- package/skills/config-manager/src/SKILL.md +56 -0
- package/skills/config-manager/src/pyproject.toml +6 -0
- package/skills/config-manager/src/scripts/common.py +118 -0
- package/skills/config-manager/src/scripts/list_config.py +63 -0
- package/skills/config-manager/src/scripts/toggle_skill.py +125 -0
- package/skills/config-manager/src/scripts/toggle_tool.py +129 -0
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "config-manager",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"types": ["store"],
|
|
5
|
+
"displayName": "配置管理",
|
|
6
|
+
"description": "管理 Sophclaw Agent 的 skill 和 tool 运行时开关。当用户要求启用/禁用某个 skill、开启/关闭某个 tool、查看当前 agent 的 skill 或 tool 配置、调整 agent 能力边界时使用。通过安全脚本修改 openclaw.json,自动备份并校验。",
|
|
7
|
+
"changelog": [
|
|
8
|
+
{
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"date": "2026-05-21",
|
|
11
|
+
"changes": ["初次提交:list_config、toggle_skill、toggle_tool 三大功能"]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"createdAt": "2026-05-21",
|
|
15
|
+
"updatedAt": "2026-05-21"
|
|
16
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: config-manager
|
|
3
|
+
description: 管理 Sophclaw Agent 的 skill 和 tool 运行时开关。当用户要求启用/禁用某个 skill、开启/关闭某个 tool、查看当前 agent 的 skill 或 tool 配置、调整 agent 能力边界时使用。所有修改通过安全脚本执行,自动备份并校验。不要绕过本 skill 直接编辑 openclaw.json。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Config Manager
|
|
7
|
+
|
|
8
|
+
安全地管理 Sophclaw Agent 的 `skills` 和 `tools.deny` 配置。所有修改必须通过本 skill 的脚本执行,严禁直接编辑 `openclaw.json`。
|
|
9
|
+
|
|
10
|
+
## 用法
|
|
11
|
+
|
|
12
|
+
### 1. 先查看当前状态
|
|
13
|
+
|
|
14
|
+
任何修改前,必须先列出当前配置:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv run {baseDir}/scripts/list_config.py --agent-id "{agent_id}"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 2. 执行变更
|
|
21
|
+
|
|
22
|
+
确认操作后执行。不确定时可加 `--dry-run` 仅预览不写入。
|
|
23
|
+
|
|
24
|
+
**开关 skill**:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv run {baseDir}/scripts/toggle_skill.py --agent-id "{agent_id}" --skill-name "{name}" --action enable|disable [--dry-run]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**开关 tool**(enable = 从 deny 列表移除,disable = 加入 deny 列表):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv run {baseDir}/scripts/toggle_tool.py --agent-id "{agent_id}" --tool-name "{name}" --action enable|disable [--dry-run]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 3. 反馈结果
|
|
37
|
+
|
|
38
|
+
解析脚本输出的 JSON:
|
|
39
|
+
|
|
40
|
+
| 字段 | 含义 |
|
|
41
|
+
|------|------|
|
|
42
|
+
| `changed: true` | 变更已执行 |
|
|
43
|
+
| `changed: false` | 已是目标状态,无需变更 |
|
|
44
|
+
| `dry_run: true` | 预览模式,需去掉 --dry-run 再次执行 |
|
|
45
|
+
| `backup` | 备份文件路径 |
|
|
46
|
+
| `error` | 错误信息 |
|
|
47
|
+
|
|
48
|
+
变更成功时告知用户备份位置,失败时说明原因和建议。
|
|
49
|
+
|
|
50
|
+
## 注意事项
|
|
51
|
+
|
|
52
|
+
- 严禁直接编辑 `openclaw.json`,所有修改必须通过本 skill 脚本执行
|
|
53
|
+
- 默认只操作当前对话的 agent,除非用户明确指定其他 agent_id 并确认
|
|
54
|
+
- 一次只操作一个 skill 或 tool
|
|
55
|
+
- 不修改 `skills` 和 `tools.deny` 以外的字段(model、workspace、identity 等)
|
|
56
|
+
- 用户未确认时不执行写入操作
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from shutil import copy2
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def default_openclaw_config_path() -> Path:
|
|
13
|
+
raw = os.environ.get("OPENCLAW_CONFIG_PATH", "~/.openclaw/openclaw.json")
|
|
14
|
+
return Path(os.path.expanduser(raw)).resolve()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def openclaw_root(config_path: Path) -> Path:
|
|
18
|
+
return config_path.expanduser().resolve().parent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_json(path: Path) -> dict[str, Any]:
|
|
22
|
+
with path.open("r", encoding="utf-8") as f:
|
|
23
|
+
return json.load(f)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def dump_json(path: Path, data: dict[str, Any]) -> None:
|
|
27
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
with path.open("w", encoding="utf-8") as f:
|
|
29
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
30
|
+
f.write("\n")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_openclaw_config(config_path: Path | None = None) -> dict[str, Any]:
|
|
34
|
+
path = config_path or default_openclaw_config_path()
|
|
35
|
+
if not path.is_file():
|
|
36
|
+
raise FileNotFoundError(f"找不到 openclaw.json: {path}\n请确认 OpenClaw 已正确安装,或设置 OPENCLAW_CONFIG_PATH 环境变量指向正确的配置文件路径。")
|
|
37
|
+
return load_json(path)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def list_agent_ids(config: dict[str, Any]) -> list[str]:
|
|
41
|
+
agents = config.get("agents", {}).get("list", [])
|
|
42
|
+
if not isinstance(agents, list):
|
|
43
|
+
return []
|
|
44
|
+
return [a["id"] for a in agents if isinstance(a, dict) and a.get("id")]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def find_agent_entry(config: dict[str, Any], agent_id: str) -> dict[str, Any] | None:
|
|
48
|
+
agents = config.get("agents", {}).get("list", [])
|
|
49
|
+
for agent in agents:
|
|
50
|
+
if isinstance(agent, dict) and agent.get("id") == agent_id:
|
|
51
|
+
return agent
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def ensure_agent_entry(config: dict[str, Any], agent_id: str) -> dict[str, Any]:
|
|
56
|
+
entry = find_agent_entry(config, agent_id)
|
|
57
|
+
if entry is None:
|
|
58
|
+
installed = list_agent_ids(config)
|
|
59
|
+
if installed:
|
|
60
|
+
hint = f"当前已安装的 agent: {', '.join(installed)}"
|
|
61
|
+
else:
|
|
62
|
+
hint = "当前 openclaw.json 中尚未安装任何 agent(agents.list 为空或不存在)。请先通过 agent-install 安装 agent。"
|
|
63
|
+
raise ValueError(f"Agent '{agent_id}' 不存在。{hint}")
|
|
64
|
+
return entry
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def backup_config(config_path: Path, backup_root: Path | None = None) -> Path:
|
|
68
|
+
if backup_root is None:
|
|
69
|
+
backup_root = openclaw_root(config_path) / "backups"
|
|
70
|
+
backup_root.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
72
|
+
backup_path = backup_root / f"openclaw-{timestamp}.json"
|
|
73
|
+
copy2(config_path, backup_path)
|
|
74
|
+
_cleanup_old_backups(backup_root, keep=10)
|
|
75
|
+
return backup_path
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _cleanup_old_backups(backup_root: Path, keep: int) -> None:
|
|
79
|
+
backups = sorted(
|
|
80
|
+
[p for p in backup_root.iterdir() if p.is_file() and p.name.startswith("openclaw-")],
|
|
81
|
+
key=lambda p: p.stat().st_mtime,
|
|
82
|
+
)
|
|
83
|
+
if len(backups) > keep:
|
|
84
|
+
for p in backups[: len(backups) - keep]:
|
|
85
|
+
p.unlink()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def validate_json(data: dict[str, Any]) -> bool:
|
|
89
|
+
try:
|
|
90
|
+
json.loads(json.dumps(data, ensure_ascii=False))
|
|
91
|
+
return True
|
|
92
|
+
except (json.JSONDecodeError, TypeError):
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def make_result(
|
|
97
|
+
action: str,
|
|
98
|
+
agent_id: str,
|
|
99
|
+
field: str,
|
|
100
|
+
before: Any,
|
|
101
|
+
after: Any,
|
|
102
|
+
changed: bool,
|
|
103
|
+
backup_path: str | None = None,
|
|
104
|
+
dry_run: bool = False,
|
|
105
|
+
message: str = "",
|
|
106
|
+
) -> dict[str, Any]:
|
|
107
|
+
return {
|
|
108
|
+
"action": action,
|
|
109
|
+
"agent_id": agent_id,
|
|
110
|
+
"field": field,
|
|
111
|
+
"before": before,
|
|
112
|
+
"after": after,
|
|
113
|
+
"changed": changed,
|
|
114
|
+
"dry_run": dry_run,
|
|
115
|
+
"backup": backup_path,
|
|
116
|
+
"message": message,
|
|
117
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
118
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from common import dump_json, find_agent_entry, load_openclaw_config, ensure_agent_entry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def list_config(agent_id: str, config_path: Path | None = None) -> dict:
|
|
13
|
+
config = load_openclaw_config(config_path)
|
|
14
|
+
entry = ensure_agent_entry(config, agent_id)
|
|
15
|
+
|
|
16
|
+
skills = entry.get("skills", [])
|
|
17
|
+
if not isinstance(skills, list):
|
|
18
|
+
skills = []
|
|
19
|
+
|
|
20
|
+
tools = entry.get("tools", {})
|
|
21
|
+
if not isinstance(tools, dict):
|
|
22
|
+
tools = {}
|
|
23
|
+
tools_deny = tools.get("deny", [])
|
|
24
|
+
if not isinstance(tools_deny, list):
|
|
25
|
+
tools_deny = []
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
"agent_id": agent_id,
|
|
29
|
+
"skills": skills,
|
|
30
|
+
"tools_deny": tools_deny,
|
|
31
|
+
"model": entry.get("model"),
|
|
32
|
+
"workspace": entry.get("workspace"),
|
|
33
|
+
"identity": entry.get("identity"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main() -> int:
|
|
38
|
+
parser = argparse.ArgumentParser(description="列出 Agent 当前的 skills 和 tools 状态")
|
|
39
|
+
parser.add_argument("--agent-id", required=True, help="Agent ID")
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--config",
|
|
42
|
+
help="openclaw.json 路径(默认 ~/.openclaw/openclaw.json)",
|
|
43
|
+
)
|
|
44
|
+
args = parser.parse_args()
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
config_path = Path(args.config).expanduser().resolve() if args.config else None
|
|
48
|
+
result = list_config(args.agent_id, config_path)
|
|
49
|
+
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
|
|
50
|
+
sys.stdout.write("\n")
|
|
51
|
+
return 0
|
|
52
|
+
except (FileNotFoundError, ValueError) as e:
|
|
53
|
+
error = {
|
|
54
|
+
"error": str(e),
|
|
55
|
+
"agent_id": args.agent_id,
|
|
56
|
+
}
|
|
57
|
+
json.dump(error, sys.stdout, indent=2, ensure_ascii=False)
|
|
58
|
+
sys.stdout.write("\n")
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from common import (
|
|
10
|
+
backup_config,
|
|
11
|
+
dump_json,
|
|
12
|
+
load_openclaw_config,
|
|
13
|
+
ensure_agent_entry,
|
|
14
|
+
make_result,
|
|
15
|
+
validate_json,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def toggle_skill(
|
|
20
|
+
agent_id: str,
|
|
21
|
+
skill_name: str,
|
|
22
|
+
action: str,
|
|
23
|
+
config_path: Path | None = None,
|
|
24
|
+
dry_run: bool = False,
|
|
25
|
+
) -> dict:
|
|
26
|
+
resolved_path = config_path or Path("~/.openclaw/openclaw.json").expanduser().resolve()
|
|
27
|
+
config = load_openclaw_config(resolved_path)
|
|
28
|
+
entry = ensure_agent_entry(config, agent_id)
|
|
29
|
+
skills: list[str] = list(entry.get("skills", []) or [])
|
|
30
|
+
|
|
31
|
+
if action == "enable":
|
|
32
|
+
if skill_name in skills:
|
|
33
|
+
return make_result(
|
|
34
|
+
"enable_skill",
|
|
35
|
+
agent_id,
|
|
36
|
+
"skills",
|
|
37
|
+
before=skills,
|
|
38
|
+
after=skills,
|
|
39
|
+
changed=False,
|
|
40
|
+
message=f"skill '{skill_name}' 已经处于启用状态,无需操作",
|
|
41
|
+
)
|
|
42
|
+
new_skills = skills + [skill_name]
|
|
43
|
+
result = make_result(
|
|
44
|
+
"enable_skill",
|
|
45
|
+
agent_id,
|
|
46
|
+
"skills",
|
|
47
|
+
before=skills,
|
|
48
|
+
after=new_skills,
|
|
49
|
+
changed=True,
|
|
50
|
+
dry_run=dry_run,
|
|
51
|
+
message=f"将启用 skill: {skill_name}",
|
|
52
|
+
)
|
|
53
|
+
elif action == "disable":
|
|
54
|
+
if skill_name not in skills:
|
|
55
|
+
return make_result(
|
|
56
|
+
"disable_skill",
|
|
57
|
+
agent_id,
|
|
58
|
+
"skills",
|
|
59
|
+
before=skills,
|
|
60
|
+
after=skills,
|
|
61
|
+
changed=False,
|
|
62
|
+
message=f"skill '{skill_name}' 已经处于禁用状态,无需操作",
|
|
63
|
+
)
|
|
64
|
+
new_skills = [s for s in skills if s != skill_name]
|
|
65
|
+
result = make_result(
|
|
66
|
+
"disable_skill",
|
|
67
|
+
agent_id,
|
|
68
|
+
"skills",
|
|
69
|
+
before=skills,
|
|
70
|
+
after=new_skills,
|
|
71
|
+
changed=True,
|
|
72
|
+
dry_run=dry_run,
|
|
73
|
+
message=f"将禁用 skill: {skill_name}",
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
raise ValueError(f"无效的 action: {action},请使用 enable 或 disable")
|
|
77
|
+
|
|
78
|
+
if dry_run or not result["changed"]:
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
entry["skills"] = new_skills
|
|
82
|
+
if not validate_json(config):
|
|
83
|
+
entry["skills"] = skills
|
|
84
|
+
raise ValueError("配置 JSON 校验失败,变更已回滚,openclaw.json 未被修改")
|
|
85
|
+
|
|
86
|
+
backup_path = backup_config(resolved_path)
|
|
87
|
+
dump_json(resolved_path, config)
|
|
88
|
+
result["backup"] = str(backup_path)
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main() -> int:
|
|
93
|
+
parser = argparse.ArgumentParser(description="启用或禁用 Agent 的某个 skill")
|
|
94
|
+
parser.add_argument("--agent-id", required=True, help="Agent ID")
|
|
95
|
+
parser.add_argument("--skill-name", required=True, help="skill 名称")
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--action", required=True, choices=["enable", "disable"], help="enable=启用, disable=禁用"
|
|
98
|
+
)
|
|
99
|
+
parser.add_argument("--dry-run", action="store_true", help="仅预览变更,不实际写入")
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--config",
|
|
102
|
+
help="openclaw.json 路径(默认 ~/.openclaw/openclaw.json)",
|
|
103
|
+
)
|
|
104
|
+
args = parser.parse_args()
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
config_path = Path(args.config).expanduser().resolve() if args.config else None
|
|
108
|
+
result = toggle_skill(args.agent_id, args.skill_name, args.action, config_path, args.dry_run)
|
|
109
|
+
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
|
|
110
|
+
sys.stdout.write("\n")
|
|
111
|
+
return 0
|
|
112
|
+
except (FileNotFoundError, ValueError) as e:
|
|
113
|
+
error = {
|
|
114
|
+
"error": str(e),
|
|
115
|
+
"agent_id": args.agent_id,
|
|
116
|
+
"skill_name": args.skill_name,
|
|
117
|
+
"action": args.action,
|
|
118
|
+
}
|
|
119
|
+
json.dump(error, sys.stdout, indent=2, ensure_ascii=False)
|
|
120
|
+
sys.stdout.write("\n")
|
|
121
|
+
return 1
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from common import (
|
|
10
|
+
backup_config,
|
|
11
|
+
dump_json,
|
|
12
|
+
load_openclaw_config,
|
|
13
|
+
ensure_agent_entry,
|
|
14
|
+
make_result,
|
|
15
|
+
validate_json,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def toggle_tool(
|
|
20
|
+
agent_id: str,
|
|
21
|
+
tool_name: str,
|
|
22
|
+
action: str,
|
|
23
|
+
config_path: Path | None = None,
|
|
24
|
+
dry_run: bool = False,
|
|
25
|
+
) -> dict:
|
|
26
|
+
resolved_path = config_path or Path("~/.openclaw/openclaw.json").expanduser().resolve()
|
|
27
|
+
config = load_openclaw_config(resolved_path)
|
|
28
|
+
entry = ensure_agent_entry(config, agent_id)
|
|
29
|
+
|
|
30
|
+
entry.setdefault("tools", {})
|
|
31
|
+
if not isinstance(entry["tools"], dict):
|
|
32
|
+
entry["tools"] = {}
|
|
33
|
+
tools_deny: list[str] = list(entry["tools"].get("deny", []) or [])
|
|
34
|
+
|
|
35
|
+
if action == "enable":
|
|
36
|
+
if tool_name not in tools_deny:
|
|
37
|
+
return make_result(
|
|
38
|
+
"enable_tool",
|
|
39
|
+
agent_id,
|
|
40
|
+
"tools.deny",
|
|
41
|
+
before=tools_deny,
|
|
42
|
+
after=tools_deny,
|
|
43
|
+
changed=False,
|
|
44
|
+
message=f"tool '{tool_name}' 已经处于启用状态(不在 deny 列表中),无需操作",
|
|
45
|
+
)
|
|
46
|
+
new_deny = [t for t in tools_deny if t != tool_name]
|
|
47
|
+
result = make_result(
|
|
48
|
+
"enable_tool",
|
|
49
|
+
agent_id,
|
|
50
|
+
"tools.deny",
|
|
51
|
+
before=tools_deny,
|
|
52
|
+
after=new_deny,
|
|
53
|
+
changed=True,
|
|
54
|
+
dry_run=dry_run,
|
|
55
|
+
message=f"将启用 tool: {tool_name}(从 deny 列表移除)",
|
|
56
|
+
)
|
|
57
|
+
elif action == "disable":
|
|
58
|
+
if tool_name in tools_deny:
|
|
59
|
+
return make_result(
|
|
60
|
+
"disable_tool",
|
|
61
|
+
agent_id,
|
|
62
|
+
"tools.deny",
|
|
63
|
+
before=tools_deny,
|
|
64
|
+
after=tools_deny,
|
|
65
|
+
changed=False,
|
|
66
|
+
message=f"tool '{tool_name}' 已经处于禁用状态(已在 deny 列表中),无需操作",
|
|
67
|
+
)
|
|
68
|
+
new_deny = tools_deny + [tool_name]
|
|
69
|
+
result = make_result(
|
|
70
|
+
"disable_tool",
|
|
71
|
+
agent_id,
|
|
72
|
+
"tools.deny",
|
|
73
|
+
before=tools_deny,
|
|
74
|
+
after=new_deny,
|
|
75
|
+
changed=True,
|
|
76
|
+
dry_run=dry_run,
|
|
77
|
+
message=f"将禁用 tool: {tool_name}(加入 deny 列表)",
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError(f"无效的 action: {action},请使用 enable 或 disable")
|
|
81
|
+
|
|
82
|
+
if dry_run or not result["changed"]:
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
entry["tools"]["deny"] = new_deny
|
|
86
|
+
if not validate_json(config):
|
|
87
|
+
entry["tools"]["deny"] = tools_deny
|
|
88
|
+
raise ValueError("配置 JSON 校验失败,变更已回滚,openclaw.json 未被修改")
|
|
89
|
+
|
|
90
|
+
backup_path = backup_config(resolved_path)
|
|
91
|
+
dump_json(resolved_path, config)
|
|
92
|
+
result["backup"] = str(backup_path)
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main() -> int:
|
|
97
|
+
parser = argparse.ArgumentParser(description="启用或禁用 Agent 的某个 tool")
|
|
98
|
+
parser.add_argument("--agent-id", required=True, help="Agent ID")
|
|
99
|
+
parser.add_argument("--tool-name", required=True, help="tool 名称")
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--action", required=True, choices=["enable", "disable"], help="enable=启用(移出 deny), disable=禁用(加入 deny)"
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument("--dry-run", action="store_true", help="仅预览变更,不实际写入")
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"--config",
|
|
106
|
+
help="openclaw.json 路径(默认 ~/.openclaw/openclaw.json)",
|
|
107
|
+
)
|
|
108
|
+
args = parser.parse_args()
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
config_path = Path(args.config).expanduser().resolve() if args.config else None
|
|
112
|
+
result = toggle_tool(args.agent_id, args.tool_name, args.action, config_path, args.dry_run)
|
|
113
|
+
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
|
|
114
|
+
sys.stdout.write("\n")
|
|
115
|
+
return 0
|
|
116
|
+
except (FileNotFoundError, ValueError) as e:
|
|
117
|
+
error = {
|
|
118
|
+
"error": str(e),
|
|
119
|
+
"agent_id": args.agent_id,
|
|
120
|
+
"tool_name": args.tool_name,
|
|
121
|
+
"action": args.action,
|
|
122
|
+
}
|
|
123
|
+
json.dump(error, sys.stdout, indent=2, ensure_ascii=False)
|
|
124
|
+
sys.stdout.write("\n")
|
|
125
|
+
return 1
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
raise SystemExit(main())
|