sophhub 0.2.0 → 0.2.2
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/compact-context/skill.json +20 -0
- package/skills/compact-context/src/SKILL.md +133 -0
- package/skills/compact-context/src/scripts/check.sh +381 -0
- package/skills/compact-context/src/scripts/set-keep-recent.mjs +1337 -0
- package/skills/compact-context/src/scripts/setup.sh +96 -0
- package/skills/feishu-notes-assistant-universal/skill.json +20 -0
- package/skills/feishu-notes-assistant-universal/src/README.md +55 -0
- package/skills/feishu-notes-assistant-universal/src/SKILL.md +159 -0
- package/skills/feishu-notes-assistant-universal/src/bin/linux-amd64/lark-cli-openclaw +0 -0
- package/skills/feishu-notes-assistant-universal/src/bin/linux-arm64/lark-cli-openclaw +0 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/_resolve_lark_cli.py +58 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_meeting_minutes.py +462 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_notes_crud.py +547 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/openclaw_notes_crud_test.py +181 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/run_meeting_minutes.py +80 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/run_meeting_minutes.sh +5 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/run_note_crud.py +32 -0
- package/skills/feishu-notes-assistant-universal/src/scripts/run_note_crud.sh +5 -0
- package/skills/image-classify/skill.json +5 -5
- package/skills/image-classify/src/SKILL.md +60 -67
- package/skills/image-classify/src/scripts/face_search.py +400 -15
- package/skills/image-classify/src/scripts/send_dm_message.py +332 -0
- package/skills/md2pdf-converter/skill.json +20 -0
- package/skills/md2pdf-converter/src/SKILL.md +244 -0
- package/skills/md2pdf-converter/src/_meta.json +6 -0
- package/skills/md2pdf-converter/src/scripts/generate_emoji_mapping.py +74 -0
- package/skills/md2pdf-converter/src/scripts/md2pdf-local.sh +291 -0
- package/skills/sophnet-bot-client/skill.json +20 -0
- package/skills/sophnet-bot-client/src/SKILL.md +255 -0
- package/skills/sophnet-bot-client/src/pyproject.toml +13 -0
- package/skills/sophnet-bot-client/src/scripts/__init__.py +0 -0
- package/skills/sophnet-bot-client/src/scripts/bot_client_proxy.py +165 -0
- package/skills/sophnet-bot-client/src/scripts/bot_client_safe.sh +29 -0
- package/skills/sophnet-bot-client/src/scripts/bot_client_setup.py +502 -0
- package/skills/sophnet-bot-client/src/tests/__init__.py +0 -0
- package/skills/sophnet-bot-client/src/tests/test_bot_client_proxy.py +255 -0
- package/skills/sophnet-bot-client/src/tests/test_bot_client_setup.py +679 -0
- package/skills/sophnet-bot-client/src/uv.lock +8 -0
- package/skills/sophnet-docx/skill.json +20 -0
- package/skills/sophnet-docx/src/SKILL.md +463 -0
- package/skills/sophnet-docx/src/package-lock.json +208 -0
- package/skills/sophnet-docx/src/package.json +16 -0
- package/skills/sophnet-docx/src/pyproject.toml +11 -0
- package/skills/sophnet-docx/src/scripts/__init__.py +1 -0
- package/skills/sophnet-docx/src/scripts/accept_changes.py +135 -0
- package/skills/sophnet-docx/src/scripts/comment.py +318 -0
- package/skills/sophnet-docx/src/scripts/ensure_uv_env.sh +68 -0
- package/skills/sophnet-docx/src/scripts/office/helpers/__init__.py +0 -0
- package/skills/sophnet-docx/src/scripts/office/helpers/merge_runs.py +199 -0
- package/skills/sophnet-docx/src/scripts/office/helpers/simplify_redlines.py +197 -0
- package/skills/sophnet-docx/src/scripts/office/pack.py +159 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/sophnet-docx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/sophnet-docx/src/scripts/office/soffice.py +183 -0
- package/skills/sophnet-docx/src/scripts/office/unpack.py +132 -0
- package/skills/sophnet-docx/src/scripts/office/validate.py +111 -0
- package/skills/sophnet-docx/src/scripts/office/validators/__init__.py +15 -0
- package/skills/sophnet-docx/src/scripts/office/validators/base.py +847 -0
- package/skills/sophnet-docx/src/scripts/office/validators/docx.py +446 -0
- package/skills/sophnet-docx/src/scripts/office/validators/pptx.py +275 -0
- package/skills/sophnet-docx/src/scripts/office/validators/redlining.py +247 -0
- package/skills/sophnet-docx/src/scripts/templates/comments.xml +3 -0
- package/skills/sophnet-docx/src/scripts/templates/commentsExtended.xml +3 -0
- package/skills/sophnet-docx/src/scripts/templates/commentsExtensible.xml +3 -0
- package/skills/sophnet-docx/src/scripts/templates/commentsIds.xml +3 -0
- package/skills/sophnet-docx/src/scripts/templates/people.xml +3 -0
- package/skills/sophnet-docx/src/scripts/upload_file.sh +96 -0
- package/skills/sophnet-docx/src/uv.lock +320 -0
- package/skills/sophnet-pdf/skill.json +20 -0
- package/skills/sophnet-pdf/src/SKILL.md +413 -0
- package/skills/sophnet-pdf/src/forms.md +297 -0
- package/skills/sophnet-pdf/src/pyproject.toml +14 -0
- package/skills/sophnet-pdf/src/reference.md +612 -0
- package/skills/sophnet-pdf/src/scripts/check_bounding_boxes.py +65 -0
- package/skills/sophnet-pdf/src/scripts/check_fillable_fields.py +11 -0
- package/skills/sophnet-pdf/src/scripts/convert_pdf_to_images.py +33 -0
- package/skills/sophnet-pdf/src/scripts/create_validation_image.py +37 -0
- package/skills/sophnet-pdf/src/scripts/enhance_tutorial.py +558 -0
- package/skills/sophnet-pdf/src/scripts/ensure_uv_env.sh +68 -0
- package/skills/sophnet-pdf/src/scripts/extract_form_field_info.py +122 -0
- package/skills/sophnet-pdf/src/scripts/extract_form_structure.py +115 -0
- package/skills/sophnet-pdf/src/scripts/extract_pdf_content.py +35 -0
- package/skills/sophnet-pdf/src/scripts/fill_fillable_fields.py +98 -0
- package/skills/sophnet-pdf/src/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/skills/sophnet-pdf/src/scripts/upload_file.sh +88 -0
- package/skills/sophnet-pdf/src/uv.lock +537 -0
- package/skills/sophnet-xlsx/skill.json +20 -0
- package/skills/sophnet-xlsx/src/SKILL.md +399 -0
- package/skills/sophnet-xlsx/src/pyproject.toml +11 -0
- package/skills/sophnet-xlsx/src/scripts/ensure_uv_env.sh +68 -0
- package/skills/sophnet-xlsx/src/scripts/office/helpers/__init__.py +0 -0
- package/skills/sophnet-xlsx/src/scripts/office/helpers/merge_runs.py +199 -0
- package/skills/sophnet-xlsx/src/scripts/office/helpers/simplify_redlines.py +197 -0
- package/skills/sophnet-xlsx/src/scripts/office/pack.py +159 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/sophnet-xlsx/src/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/sophnet-xlsx/src/scripts/office/soffice.py +183 -0
- package/skills/sophnet-xlsx/src/scripts/office/unpack.py +132 -0
- package/skills/sophnet-xlsx/src/scripts/office/validate.py +111 -0
- package/skills/sophnet-xlsx/src/scripts/office/validators/__init__.py +15 -0
- package/skills/sophnet-xlsx/src/scripts/office/validators/base.py +847 -0
- package/skills/sophnet-xlsx/src/scripts/office/validators/docx.py +446 -0
- package/skills/sophnet-xlsx/src/scripts/office/validators/pptx.py +275 -0
- package/skills/sophnet-xlsx/src/scripts/office/validators/redlining.py +247 -0
- package/skills/sophnet-xlsx/src/scripts/recalc.py +184 -0
- package/skills/sophnet-xlsx/src/scripts/upload_file.sh +96 -0
- package/skills/sophnet-xlsx/src/uv.lock +319 -0
- package/skills/wechat-article-publisher/skill.json +20 -0
- package/skills/wechat-article-publisher/src/SKILL.md +60 -0
- package/skills/wechat-article-publisher/src/config.json +7 -0
- package/skills/wechat-article-publisher/src/pyproject.toml +12 -0
- package/skills/wechat-article-publisher/src/scripts/publish_wechat.py +825 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Permission-aware Bot API proxy.
|
|
2
|
+
|
|
3
|
+
Checks agent permissions before sending messages through the Bot API.
|
|
4
|
+
Credentials are stored in admin_dir (invisible to sub-agents).
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python scripts/bot_client_proxy.py --agent-id <id> send --message "..."
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
def _find_manager_scripts() -> str:
|
|
17
|
+
"""Locate sophnet-customer-agent-manager/scripts at runtime."""
|
|
18
|
+
skill_name = "sophnet-customer-agent-manager"
|
|
19
|
+
subdir = "scripts"
|
|
20
|
+
candidates = [
|
|
21
|
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", skill_name, subdir),
|
|
22
|
+
os.path.join(os.path.expanduser("~"), ".openclaw", "skills", skill_name, subdir),
|
|
23
|
+
os.path.join("/app", "skills", skill_name, subdir),
|
|
24
|
+
]
|
|
25
|
+
for path in candidates:
|
|
26
|
+
resolved = os.path.realpath(path)
|
|
27
|
+
if os.path.isdir(resolved):
|
|
28
|
+
return resolved
|
|
29
|
+
return os.path.realpath(candidates[0])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
sys.path.insert(0, _find_manager_scripts())
|
|
33
|
+
from permission_store import _DEFAULT_ADMIN_DIR, get_permission
|
|
34
|
+
|
|
35
|
+
_BOT_CREDENTIALS_FILENAME = "bot_credentials.json"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _output(data: dict) -> None:
|
|
39
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
40
|
+
sys.exit(0 if data.get("status") == "ok" else 1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_credentials(admin_dir: str, agent_id: str) -> dict | None:
|
|
44
|
+
path = os.path.join(admin_dir, agent_id, _BOT_CREDENTIALS_FILENAME)
|
|
45
|
+
if not os.path.isfile(path):
|
|
46
|
+
return None
|
|
47
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
48
|
+
return json.load(f)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def send_message(
|
|
52
|
+
agent_id: str,
|
|
53
|
+
message: str,
|
|
54
|
+
admin_dir: str = _DEFAULT_ADMIN_DIR,
|
|
55
|
+
) -> dict:
|
|
56
|
+
"""Send a message through Bot API with permission checking."""
|
|
57
|
+
permission = get_permission(admin_dir, agent_id)
|
|
58
|
+
if permission is None:
|
|
59
|
+
return {
|
|
60
|
+
"status": "error",
|
|
61
|
+
"action": "bot_client_send",
|
|
62
|
+
"message": f"未找到 agent '{agent_id}' 的权限记录",
|
|
63
|
+
"fix": "请先使用 agent_manager_cli.py create 创建该 agent",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
credentials = _load_credentials(admin_dir, agent_id)
|
|
67
|
+
if credentials is None:
|
|
68
|
+
return {
|
|
69
|
+
"status": "error",
|
|
70
|
+
"action": "bot_client_send",
|
|
71
|
+
"message": f"未找到 agent '{agent_id}' 的 Bot API 凭据",
|
|
72
|
+
"fix": "请先执行 bot_client_setup.py setup --mode proxy",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
url = credentials["url"]
|
|
76
|
+
secret = credentials["secret"]
|
|
77
|
+
remote_agent_id = credentials["remote_agent_id"]
|
|
78
|
+
|
|
79
|
+
# Use agent_id as a stable chatId so the server always resolves to the
|
|
80
|
+
# same sessionKey. Previously we saved the server's sessionKey and sent
|
|
81
|
+
# it back as chatId, but sessionKey is a composite key that already
|
|
82
|
+
# contains chatId — feeding it back caused infinite key growth and a new
|
|
83
|
+
# session on every request.
|
|
84
|
+
payload = {
|
|
85
|
+
"agentId": remote_agent_id,
|
|
86
|
+
"senderId": agent_id,
|
|
87
|
+
"text": message,
|
|
88
|
+
"chatId": agent_id,
|
|
89
|
+
"senderPermissions": {
|
|
90
|
+
"operations": permission.get("operations", {}),
|
|
91
|
+
"scope": permission.get("scope", {}),
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
[
|
|
98
|
+
"curl", "-s", "-X", "POST", url,
|
|
99
|
+
"-H", "Content-Type: application/json",
|
|
100
|
+
"-H", f"Authorization: Bearer {secret}",
|
|
101
|
+
"-d", json.dumps(payload, ensure_ascii=False),
|
|
102
|
+
],
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
timeout=120,
|
|
106
|
+
)
|
|
107
|
+
except subprocess.TimeoutExpired:
|
|
108
|
+
return {
|
|
109
|
+
"status": "error",
|
|
110
|
+
"action": "bot_client_send",
|
|
111
|
+
"message": "Bot API 请求超时 (120s)",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if result.returncode != 0:
|
|
115
|
+
return {
|
|
116
|
+
"status": "error",
|
|
117
|
+
"action": "bot_client_send",
|
|
118
|
+
"message": f"curl 请求失败 (exit {result.returncode})",
|
|
119
|
+
"stderr": result.stderr,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
response = json.loads(result.stdout)
|
|
124
|
+
except json.JSONDecodeError:
|
|
125
|
+
return {
|
|
126
|
+
"status": "error",
|
|
127
|
+
"action": "bot_client_send",
|
|
128
|
+
"message": "Bot API 返回了无法解析的响应",
|
|
129
|
+
"raw": result.stdout[:500],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"status": "ok",
|
|
134
|
+
"action": "bot_client_send",
|
|
135
|
+
"reply": response.get("reply", ""),
|
|
136
|
+
"sessionKey": response.get("sessionKey", ""),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main():
|
|
141
|
+
parser = argparse.ArgumentParser(description="Permission-aware Bot API proxy")
|
|
142
|
+
parser.add_argument("--agent-id", required=True, help="Agent identifier")
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
"--admin-dir", default=_DEFAULT_ADMIN_DIR,
|
|
145
|
+
help="Admin directory (permissions + credentials)",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
149
|
+
|
|
150
|
+
send_parser = subparsers.add_parser("send", help="Send message via Bot API")
|
|
151
|
+
send_parser.add_argument("--message", required=True, help="Message to send")
|
|
152
|
+
|
|
153
|
+
args = parser.parse_args()
|
|
154
|
+
|
|
155
|
+
if args.command == "send":
|
|
156
|
+
result = send_message(
|
|
157
|
+
agent_id=args.agent_id,
|
|
158
|
+
message=args.message,
|
|
159
|
+
admin_dir=args.admin_dir,
|
|
160
|
+
)
|
|
161
|
+
_output(result)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
main()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Permission-aware wrapper for bot_client_proxy.py.
|
|
3
|
+
# Only allows Bot API proxy commands, no arbitrary shell/python execution.
|
|
4
|
+
# Designed to be exec-allowlisted alongside customer_proxy_safe.sh.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
PROXY_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
9
|
+
|
|
10
|
+
if [[ $# -lt 2 ]]; then
|
|
11
|
+
echo '{"status":"error","message":"Usage: bot_client_safe.sh --agent-id <id> send --message <msg>"}'
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
if [[ "$1" != "--agent-id" ]]; then
|
|
16
|
+
echo '{"status":"error","message":"First argument must be --agent-id"}'
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
AGENT_ID="$2"
|
|
21
|
+
shift 2
|
|
22
|
+
|
|
23
|
+
if [[ -z "${AGENT_ID}" || "${AGENT_ID}" == *";"* || "${AGENT_ID}" == *"|"* || "${AGENT_ID}" == *"&"* ]]; then
|
|
24
|
+
echo '{"status":"error","message":"Invalid agent-id"}'
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
cd "${PROXY_DIR}" && exec uv run --project . python scripts/bot_client_proxy.py \
|
|
29
|
+
--agent-id "${AGENT_ID}" "$@"
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""Bot API client setup — inject/remove remote collaboration section in sub-agent AGENTS.md.
|
|
2
|
+
|
|
3
|
+
Supports two modes:
|
|
4
|
+
- raw: injects curl templates directly (legacy, no permission enforcement)
|
|
5
|
+
- proxy: injects bot_client_safe.sh reference with programmatic permission checks
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python scripts/bot_client_setup.py setup \
|
|
9
|
+
--agent-id vip-reader \
|
|
10
|
+
--url https://remote:3000/bot-api/chat \
|
|
11
|
+
--secret abc123 \
|
|
12
|
+
--remote-agent-id main \
|
|
13
|
+
--mode proxy
|
|
14
|
+
|
|
15
|
+
python scripts/bot_client_setup.py remove --agent-id vip-reader
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import stat
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
_DEFAULT_WORKSPACE_ROOT = os.path.join(
|
|
25
|
+
os.path.expanduser("~"), ".openclaw", "workspace-customer-agents"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_DEFAULT_ADMIN_DIR = os.path.join(
|
|
29
|
+
os.path.expanduser("~"), ".config", "sophnet-customer-admin"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_DEFAULT_EXEC_APPROVALS = os.path.join(
|
|
33
|
+
os.path.expanduser("~"), ".openclaw", "exec-approvals.json"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def _ensure_executable(path: str) -> None:
|
|
37
|
+
"""Ensure a file has execute permission."""
|
|
38
|
+
try:
|
|
39
|
+
current = os.stat(path).st_mode
|
|
40
|
+
if not (current & stat.S_IXUSR):
|
|
41
|
+
os.chmod(path, current | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
42
|
+
except OSError:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _find_bot_client_safe_sh() -> str:
|
|
47
|
+
"""Locate bot_client_safe.sh at runtime.
|
|
48
|
+
|
|
49
|
+
Search order:
|
|
50
|
+
1. Alongside this script (source/development)
|
|
51
|
+
2. clawhub runtime install path
|
|
52
|
+
3. Docker pre-built path
|
|
53
|
+
"""
|
|
54
|
+
skill_name = "sophnet-bot-client"
|
|
55
|
+
filename = "bot_client_safe.sh"
|
|
56
|
+
candidates = [
|
|
57
|
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), filename),
|
|
58
|
+
os.path.join(os.path.expanduser("~"), ".openclaw", "skills", skill_name, "scripts", filename),
|
|
59
|
+
os.path.join("/app", "skills", skill_name, "scripts", filename),
|
|
60
|
+
]
|
|
61
|
+
for path in candidates:
|
|
62
|
+
if os.path.isfile(path):
|
|
63
|
+
resolved = os.path.realpath(path)
|
|
64
|
+
_ensure_executable(resolved)
|
|
65
|
+
return resolved
|
|
66
|
+
return os.path.realpath(candidates[0])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_BOT_CLIENT_SAFE_SH = _find_bot_client_safe_sh()
|
|
70
|
+
|
|
71
|
+
SECTION_MARKER_START = "## 远程协作 — Bot API Client"
|
|
72
|
+
SECTION_MARKER_END = "<!-- END 远程协作 -->"
|
|
73
|
+
|
|
74
|
+
CREDENTIALS_FILENAME = "bot_credentials.json"
|
|
75
|
+
SESSION_FILENAME = "bot_session.txt"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _workspace_path(agent_id: str, workspace_root: str) -> str:
|
|
79
|
+
return os.path.join(workspace_root, agent_id)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _agents_md_path(agent_id: str, workspace_root: str) -> str:
|
|
83
|
+
return os.path.join(_workspace_path(agent_id, workspace_root), "AGENTS.md")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _data_dir(agent_id: str, workspace_root: str) -> str:
|
|
87
|
+
return os.path.join(_workspace_path(agent_id, workspace_root), "data")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def generate_section(
|
|
91
|
+
url: str,
|
|
92
|
+
remote_agent_id: str,
|
|
93
|
+
agent_id: str,
|
|
94
|
+
credentials_path: str,
|
|
95
|
+
) -> str:
|
|
96
|
+
"""Generate the remote collaboration section for AGENTS.md (raw mode)."""
|
|
97
|
+
return f"""{SECTION_MARKER_START}
|
|
98
|
+
|
|
99
|
+
你可以通过 Bot API 与远程系统对话。
|
|
100
|
+
|
|
101
|
+
### 连接信息
|
|
102
|
+
|
|
103
|
+
- **凭据文件**: `{credentials_path}`
|
|
104
|
+
- **远端 Agent**: `{remote_agent_id}`
|
|
105
|
+
|
|
106
|
+
### 发送消息
|
|
107
|
+
|
|
108
|
+
使用以下命令发送消息并自动管理会话:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# 读取凭据
|
|
112
|
+
BOT_URL=$(cat {credentials_path} | python3 -c "import sys,json; print(json.load(sys.stdin)['url'])")
|
|
113
|
+
BOT_SECRET=$(cat {credentials_path} | python3 -c "import sys,json; print(json.load(sys.stdin)['secret'])")
|
|
114
|
+
|
|
115
|
+
# 发送请求(chatId 使用固定的 agent_id 以保持会话稳定)
|
|
116
|
+
RESPONSE=$(curl -s -X POST "$BOT_URL" \\
|
|
117
|
+
-H "Content-Type: application/json" \\
|
|
118
|
+
-H "Authorization: Bearer $BOT_SECRET" \\
|
|
119
|
+
-d "$(python3 -c "
|
|
120
|
+
import json, sys
|
|
121
|
+
print(json.dumps({{
|
|
122
|
+
'agentId': '{remote_agent_id}',
|
|
123
|
+
'senderId': '{agent_id}',
|
|
124
|
+
'text': sys.argv[1],
|
|
125
|
+
'chatId': '{agent_id}'
|
|
126
|
+
}}))
|
|
127
|
+
" "<你要发送的消息>")")
|
|
128
|
+
|
|
129
|
+
# 提取回复
|
|
130
|
+
echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('reply','(无回复)'))"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 会话管理
|
|
134
|
+
|
|
135
|
+
1. **多轮对话**:chatId 固定为你的 agent_id,服务端自动维护同一会话
|
|
136
|
+
2. **无需手动管理**:每次请求自动进入同一会话上下文
|
|
137
|
+
|
|
138
|
+
### 错误处理
|
|
139
|
+
|
|
140
|
+
- 如果 curl 返回非 JSON(网络错误/超时),检查 URL 是否可达
|
|
141
|
+
- 如果收到 401/403,说明 secret 已过期,联系管理员更新凭据文件
|
|
142
|
+
- 如果 `reply` 字段为空,检查 `RESPONSE` 中的完整 JSON 以获取错误信息
|
|
143
|
+
|
|
144
|
+
### 权限约束
|
|
145
|
+
|
|
146
|
+
**通过 Bot API 发送的请求必须遵守你本地相同的权限限制。**
|
|
147
|
+
|
|
148
|
+
- 如果你本地被禁止执行某项操作(如 delete、update),则**禁止**通过 Bot API 请求远端 agent 代为执行
|
|
149
|
+
- Bot API 仅用于在你的权限范围内与远端系统协作,不得用于绕过自身权限
|
|
150
|
+
- 违反此约束等同于直接执行被禁止的操作
|
|
151
|
+
|
|
152
|
+
### 使用场景
|
|
153
|
+
|
|
154
|
+
当本地命令无法满足需求,或用户明确要求通过远程系统执行时,在你的权限范围内使用上述命令发送自然语言请求。
|
|
155
|
+
|
|
156
|
+
{SECTION_MARKER_END}"""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def generate_proxy_section(
|
|
160
|
+
remote_agent_id: str,
|
|
161
|
+
agent_id: str,
|
|
162
|
+
wrapper_path: str,
|
|
163
|
+
) -> str:
|
|
164
|
+
"""Generate the remote collaboration section for AGENTS.md (proxy mode).
|
|
165
|
+
|
|
166
|
+
Unlike raw mode, this does NOT expose curl/python3 commands or credentials.
|
|
167
|
+
All communication goes through the permission-checking proxy.
|
|
168
|
+
"""
|
|
169
|
+
return f"""{SECTION_MARKER_START}
|
|
170
|
+
|
|
171
|
+
你可以通过 Bot API 与远程系统对话。
|
|
172
|
+
|
|
173
|
+
### 连接信息
|
|
174
|
+
|
|
175
|
+
- **远端 Agent**: `{remote_agent_id}`
|
|
176
|
+
- 凭据和会话由系统自动管理
|
|
177
|
+
|
|
178
|
+
### 发送消息
|
|
179
|
+
|
|
180
|
+
使用以下命令发送消息:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
{wrapper_path} \\
|
|
184
|
+
--agent-id "{agent_id}" send --message "<你要发送的消息>"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 错误处理
|
|
188
|
+
|
|
189
|
+
- 如果返回 `权限记录` 相关错误,说明 agent 未正确注册,联系管理员
|
|
190
|
+
- 如果返回 `凭据` 相关错误,说明 Bot API 连接未配置,联系管理员
|
|
191
|
+
- 如果返回 `curl 请求失败`,检查远端服务是否可用
|
|
192
|
+
|
|
193
|
+
### 权限约束
|
|
194
|
+
|
|
195
|
+
**你的权限由系统强制执行,无法绕过。** 超出权限范围的操作会被代理自动拒绝。
|
|
196
|
+
|
|
197
|
+
- **禁止**使用 curl、python3 或任何其他方式直接访问 Bot API
|
|
198
|
+
- **禁止**直接读取凭据文件
|
|
199
|
+
- 所有远程通信**必须**通过上述命令
|
|
200
|
+
|
|
201
|
+
{SECTION_MARKER_END}"""
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _find_section(content: str) -> tuple[int, int] | None:
|
|
205
|
+
"""Find the start and end positions of the remote collaboration section.
|
|
206
|
+
|
|
207
|
+
Returns (start, end) character indices, or None if not found.
|
|
208
|
+
"""
|
|
209
|
+
start = content.find(SECTION_MARKER_START)
|
|
210
|
+
if start == -1:
|
|
211
|
+
return None
|
|
212
|
+
end = content.find(SECTION_MARKER_END, start)
|
|
213
|
+
if end == -1:
|
|
214
|
+
return None
|
|
215
|
+
end += len(SECTION_MARKER_END)
|
|
216
|
+
return (start, end)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _make_writable(path: str) -> int | None:
|
|
220
|
+
"""If file is read-only, make it writable. Returns original mode or None."""
|
|
221
|
+
if not os.path.exists(path):
|
|
222
|
+
return None
|
|
223
|
+
current_mode = os.stat(path).st_mode
|
|
224
|
+
if not (current_mode & stat.S_IWUSR):
|
|
225
|
+
os.chmod(path, current_mode | stat.S_IWUSR)
|
|
226
|
+
return current_mode
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _protect_file(path: str) -> None:
|
|
231
|
+
"""Set file to read-only (444)."""
|
|
232
|
+
os.chmod(path, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _add_to_exec_allowlist(
|
|
236
|
+
agent_id: str,
|
|
237
|
+
pattern: str,
|
|
238
|
+
exec_approvals_path: str = _DEFAULT_EXEC_APPROVALS,
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Add an entry to the agent's exec allowlist (idempotent)."""
|
|
241
|
+
data: dict = {"version": 1, "agents": {}}
|
|
242
|
+
if os.path.exists(exec_approvals_path):
|
|
243
|
+
with open(exec_approvals_path, "r", encoding="utf-8") as f:
|
|
244
|
+
data = json.load(f)
|
|
245
|
+
|
|
246
|
+
agents = data.setdefault("agents", {})
|
|
247
|
+
agent_entry = agents.get(agent_id)
|
|
248
|
+
if agent_entry is None:
|
|
249
|
+
agent_entry = {"security": "allowlist", "ask": "off", "allowlist": []}
|
|
250
|
+
agents[agent_id] = agent_entry
|
|
251
|
+
allowlist = agent_entry.setdefault("allowlist", [])
|
|
252
|
+
|
|
253
|
+
abs_pattern = os.path.abspath(pattern)
|
|
254
|
+
for entry in allowlist:
|
|
255
|
+
if entry.get("pattern") == abs_pattern:
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
allowlist.append({"pattern": abs_pattern})
|
|
259
|
+
|
|
260
|
+
with open(exec_approvals_path, "w", encoding="utf-8") as f:
|
|
261
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def setup(
|
|
265
|
+
agent_id: str,
|
|
266
|
+
url: str,
|
|
267
|
+
secret: str,
|
|
268
|
+
remote_agent_id: str,
|
|
269
|
+
workspace_root: str = _DEFAULT_WORKSPACE_ROOT,
|
|
270
|
+
mode: str = "raw",
|
|
271
|
+
admin_dir: str = _DEFAULT_ADMIN_DIR,
|
|
272
|
+
exec_approvals_path: str = _DEFAULT_EXEC_APPROVALS,
|
|
273
|
+
) -> dict:
|
|
274
|
+
"""Set up bot client for a sub-agent.
|
|
275
|
+
|
|
276
|
+
mode="raw": credentials in workspace/data, raw curl in AGENTS.md
|
|
277
|
+
mode="proxy": credentials in admin_dir, safe proxy in AGENTS.md + exec allowlist
|
|
278
|
+
"""
|
|
279
|
+
workspace = _workspace_path(agent_id, workspace_root)
|
|
280
|
+
if not os.path.isdir(workspace):
|
|
281
|
+
return {
|
|
282
|
+
"status": "error",
|
|
283
|
+
"action": "setup_bot_client",
|
|
284
|
+
"message": f"Workspace not found: {workspace}",
|
|
285
|
+
"fix": "请先使用 customer-agent create 创建子代理",
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
agents_md = _agents_md_path(agent_id, workspace_root)
|
|
289
|
+
if not os.path.isfile(agents_md):
|
|
290
|
+
return {
|
|
291
|
+
"status": "error",
|
|
292
|
+
"action": "setup_bot_client",
|
|
293
|
+
"message": f"AGENTS.md not found: {agents_md}",
|
|
294
|
+
"fix": "Workspace 目录不完整,请重新创建子代理",
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
credentials_data = {
|
|
298
|
+
"url": url,
|
|
299
|
+
"secret": secret,
|
|
300
|
+
"remote_agent_id": remote_agent_id,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if mode == "proxy":
|
|
304
|
+
# Credentials go to admin_dir/<agent_id>/ (invisible to sub-agent)
|
|
305
|
+
creds_dir = os.path.join(admin_dir, agent_id)
|
|
306
|
+
os.makedirs(creds_dir, exist_ok=True)
|
|
307
|
+
credentials_abs = os.path.join(creds_dir, CREDENTIALS_FILENAME)
|
|
308
|
+
with open(credentials_abs, "w", encoding="utf-8") as f:
|
|
309
|
+
json.dump(credentials_data, f, indent=2, ensure_ascii=False)
|
|
310
|
+
|
|
311
|
+
wrapper_path = os.path.abspath(_BOT_CLIENT_SAFE_SH)
|
|
312
|
+
section = generate_proxy_section(remote_agent_id, agent_id, wrapper_path)
|
|
313
|
+
else:
|
|
314
|
+
# Legacy: credentials in workspace/data/
|
|
315
|
+
data_dir = _data_dir(agent_id, workspace_root)
|
|
316
|
+
if not os.path.exists(data_dir):
|
|
317
|
+
os.makedirs(data_dir, exist_ok=True)
|
|
318
|
+
credentials_path = os.path.join("data", CREDENTIALS_FILENAME)
|
|
319
|
+
credentials_abs = os.path.join(workspace, credentials_path)
|
|
320
|
+
with open(credentials_abs, "w", encoding="utf-8") as f:
|
|
321
|
+
json.dump(credentials_data, f, indent=2, ensure_ascii=False)
|
|
322
|
+
|
|
323
|
+
section = generate_section(url, remote_agent_id, agent_id, credentials_path)
|
|
324
|
+
|
|
325
|
+
# Read existing AGENTS.md (handle read-only files)
|
|
326
|
+
original_mode = _make_writable(agents_md)
|
|
327
|
+
try:
|
|
328
|
+
with open(agents_md, "r", encoding="utf-8") as f:
|
|
329
|
+
content = f.read()
|
|
330
|
+
|
|
331
|
+
bounds = _find_section(content)
|
|
332
|
+
if bounds is not None:
|
|
333
|
+
start, end = bounds
|
|
334
|
+
new_content = content[:start].rstrip("\n") + "\n\n" + section + content[end:]
|
|
335
|
+
else:
|
|
336
|
+
new_content = content.rstrip("\n") + "\n\n" + section + "\n"
|
|
337
|
+
|
|
338
|
+
with open(agents_md, "w", encoding="utf-8") as f:
|
|
339
|
+
f.write(new_content)
|
|
340
|
+
finally:
|
|
341
|
+
if mode == "proxy":
|
|
342
|
+
_protect_file(agents_md)
|
|
343
|
+
elif original_mode is not None:
|
|
344
|
+
os.chmod(agents_md, original_mode)
|
|
345
|
+
|
|
346
|
+
# In proxy mode, add bot_client_safe.sh to exec allowlist
|
|
347
|
+
if mode == "proxy":
|
|
348
|
+
_add_to_exec_allowlist(
|
|
349
|
+
agent_id, _BOT_CLIENT_SAFE_SH, exec_approvals_path,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
result = {
|
|
353
|
+
"status": "ok",
|
|
354
|
+
"action": "setup_bot_client",
|
|
355
|
+
"agent_id": agent_id,
|
|
356
|
+
"url": url,
|
|
357
|
+
"remote_agent_id": remote_agent_id,
|
|
358
|
+
"mode": mode,
|
|
359
|
+
"workspace": workspace,
|
|
360
|
+
"credentials_file": credentials_abs,
|
|
361
|
+
}
|
|
362
|
+
if mode == "proxy":
|
|
363
|
+
result["exec_allowlist_updated"] = True
|
|
364
|
+
return result
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def remove(
|
|
368
|
+
agent_id: str,
|
|
369
|
+
workspace_root: str = _DEFAULT_WORKSPACE_ROOT,
|
|
370
|
+
admin_dir: str = _DEFAULT_ADMIN_DIR,
|
|
371
|
+
) -> dict:
|
|
372
|
+
"""Remove bot client configuration from a sub-agent.
|
|
373
|
+
|
|
374
|
+
1. Remove remote collaboration section from AGENTS.md
|
|
375
|
+
2. Delete credentials and session files from both workspace/data and admin_dir
|
|
376
|
+
"""
|
|
377
|
+
workspace = _workspace_path(agent_id, workspace_root)
|
|
378
|
+
if not os.path.isdir(workspace):
|
|
379
|
+
return {
|
|
380
|
+
"status": "error",
|
|
381
|
+
"action": "remove_bot_client",
|
|
382
|
+
"message": f"Workspace not found: {workspace}",
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
agents_md = _agents_md_path(agent_id, workspace_root)
|
|
386
|
+
removed_section = False
|
|
387
|
+
|
|
388
|
+
if os.path.isfile(agents_md):
|
|
389
|
+
original_mode = _make_writable(agents_md)
|
|
390
|
+
try:
|
|
391
|
+
with open(agents_md, "r", encoding="utf-8") as f:
|
|
392
|
+
content = f.read()
|
|
393
|
+
|
|
394
|
+
bounds = _find_section(content)
|
|
395
|
+
if bounds is not None:
|
|
396
|
+
start, end = bounds
|
|
397
|
+
new_content = content[:start].rstrip("\n") + content[end:].lstrip("\n")
|
|
398
|
+
if new_content and not new_content.endswith("\n"):
|
|
399
|
+
new_content += "\n"
|
|
400
|
+
with open(agents_md, "w", encoding="utf-8") as f:
|
|
401
|
+
f.write(new_content)
|
|
402
|
+
removed_section = True
|
|
403
|
+
finally:
|
|
404
|
+
if original_mode is not None:
|
|
405
|
+
os.chmod(agents_md, original_mode)
|
|
406
|
+
|
|
407
|
+
# Clean up data files from workspace/data (raw mode)
|
|
408
|
+
removed_files = []
|
|
409
|
+
for filename in (CREDENTIALS_FILENAME, SESSION_FILENAME):
|
|
410
|
+
filepath = os.path.join(workspace, "data", filename)
|
|
411
|
+
if os.path.isfile(filepath):
|
|
412
|
+
os.remove(filepath)
|
|
413
|
+
removed_files.append(filename)
|
|
414
|
+
|
|
415
|
+
# Clean up data files from admin_dir (proxy mode)
|
|
416
|
+
admin_agent_dir = os.path.join(admin_dir, agent_id)
|
|
417
|
+
for filename in (CREDENTIALS_FILENAME, SESSION_FILENAME):
|
|
418
|
+
filepath = os.path.join(admin_agent_dir, filename)
|
|
419
|
+
if os.path.isfile(filepath):
|
|
420
|
+
os.remove(filepath)
|
|
421
|
+
if filename not in removed_files:
|
|
422
|
+
removed_files.append(filename)
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
"status": "ok",
|
|
426
|
+
"action": "remove_bot_client",
|
|
427
|
+
"agent_id": agent_id,
|
|
428
|
+
"removed_section": removed_section,
|
|
429
|
+
"removed_files": removed_files,
|
|
430
|
+
"workspace": workspace,
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def main() -> None:
|
|
435
|
+
parser = argparse.ArgumentParser(
|
|
436
|
+
description="Bot API client setup for sub-agents"
|
|
437
|
+
)
|
|
438
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
439
|
+
|
|
440
|
+
# setup command
|
|
441
|
+
setup_parser = subparsers.add_parser("setup", help="Configure remote connection")
|
|
442
|
+
setup_parser.add_argument("--agent-id", required=True, help="Sub-agent ID")
|
|
443
|
+
setup_parser.add_argument("--url", required=True, help="Bot API URL")
|
|
444
|
+
setup_parser.add_argument("--secret", required=True, help="API secret/token")
|
|
445
|
+
setup_parser.add_argument(
|
|
446
|
+
"--remote-agent-id", required=True,
|
|
447
|
+
help="Remote target agent ID (should be the calling agent's own ID for permission inheritance)"
|
|
448
|
+
)
|
|
449
|
+
setup_parser.add_argument(
|
|
450
|
+
"--mode", choices=["raw", "proxy"], default="proxy",
|
|
451
|
+
help="proxy (default): inject safe proxy with permission checks; raw (legacy): inject curl templates",
|
|
452
|
+
)
|
|
453
|
+
setup_parser.add_argument(
|
|
454
|
+
"--workspace-root", default=_DEFAULT_WORKSPACE_ROOT, help=argparse.SUPPRESS
|
|
455
|
+
)
|
|
456
|
+
setup_parser.add_argument(
|
|
457
|
+
"--admin-dir", default=_DEFAULT_ADMIN_DIR, help=argparse.SUPPRESS
|
|
458
|
+
)
|
|
459
|
+
setup_parser.add_argument(
|
|
460
|
+
"--exec-approvals-path", default=_DEFAULT_EXEC_APPROVALS,
|
|
461
|
+
help=argparse.SUPPRESS,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# remove command
|
|
465
|
+
remove_parser = subparsers.add_parser("remove", help="Remove remote connection")
|
|
466
|
+
remove_parser.add_argument("--agent-id", required=True, help="Sub-agent ID")
|
|
467
|
+
remove_parser.add_argument(
|
|
468
|
+
"--workspace-root", default=_DEFAULT_WORKSPACE_ROOT, help=argparse.SUPPRESS
|
|
469
|
+
)
|
|
470
|
+
remove_parser.add_argument(
|
|
471
|
+
"--admin-dir", default=_DEFAULT_ADMIN_DIR, help=argparse.SUPPRESS
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
args = parser.parse_args()
|
|
475
|
+
|
|
476
|
+
if args.command == "setup":
|
|
477
|
+
result = setup(
|
|
478
|
+
agent_id=args.agent_id,
|
|
479
|
+
url=args.url,
|
|
480
|
+
secret=args.secret,
|
|
481
|
+
remote_agent_id=args.remote_agent_id,
|
|
482
|
+
workspace_root=args.workspace_root,
|
|
483
|
+
mode=args.mode,
|
|
484
|
+
admin_dir=args.admin_dir,
|
|
485
|
+
exec_approvals_path=args.exec_approvals_path,
|
|
486
|
+
)
|
|
487
|
+
elif args.command == "remove":
|
|
488
|
+
result = remove(
|
|
489
|
+
agent_id=args.agent_id,
|
|
490
|
+
workspace_root=args.workspace_root,
|
|
491
|
+
admin_dir=args.admin_dir,
|
|
492
|
+
)
|
|
493
|
+
else:
|
|
494
|
+
parser.print_help()
|
|
495
|
+
sys.exit(1)
|
|
496
|
+
|
|
497
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
498
|
+
sys.exit(0 if result["status"] == "ok" else 1)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
if __name__ == "__main__":
|
|
502
|
+
main()
|
|
File without changes
|