Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions copaw/src/copaw_worker/matrix_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -1001,11 +1001,14 @@ async def _upload_file(self, file_ref: str) -> Optional[str]:
"MatrixChannel: uploaded %s → %s", path.name, resp.content_uri
)
return resp.content_uri
logger.warning("MatrixChannel: upload failed: %s", resp)
logger.warning(
"MatrixChannel: upload returned %s (expected UploadResponse) for %s",
type(resp).__name__, file_ref,
)
return None
except Exception as exc:
logger.warning(
"MatrixChannel: upload error for %s: %s", file_ref, exc
logger.exception(
"MatrixChannel: upload exception for %s: %s", file_ref, exc
)
return None

Expand Down Expand Up @@ -1458,6 +1461,18 @@ async def send_media(
logger.warning(
"MatrixChannel: send_media upload failed for %s", file_ref
)
# Send fallback notice so the user knows the file didn't arrive
try:
path_str = file_ref.removeprefix("file://")
_fname = os.path.basename(path_str) or "file"
await self._client.room_send(
room_id, "m.room.message",
{"msgtype": "m.notice",
"body": f"\u26a0\ufe0f File upload failed: {_fname}"},
ignore_unverified_devices=True,
)
except Exception:
pass
return

# Build and send the Matrix room event
Expand Down Expand Up @@ -1495,3 +1510,12 @@ async def send_media(
logger.exception(
"MatrixChannel: send_media failed for %s: %s", room_id, exc
)
try:
await self._client.room_send(
room_id, "m.room.message",
{"msgtype": "m.notice",
"body": f"\u26a0\ufe0f File delivery failed: {filename}"},
ignore_unverified_devices=True,
)
except Exception:
pass
4 changes: 3 additions & 1 deletion manager/agent/TOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

Each skill has a full `SKILL.md` in `skills/<name>/`. The `description` field in each SKILL.md tells the system when to load it.

Available skills: `task-management`, `task-coordination`, `git-delegation-management`, `worker-management`, `project-management`, `channel-management`, `matrix-server-management`, `mcp-server-management`, `model-switch`, `worker-model-switch`, `file-sync-management`
Available skills: `task-management`, `task-coordination`, `git-delegation-management`, `worker-management`, `project-management`, `channel-management`, `matrix-server-management`, `mcp-server-management`, `model-switch`, `worker-model-switch`, `file-sync-management`, `send-file`

> **Shared skills** (in `shared-skills/<name>/`): `send-file` — available to both Manager and Workers.

## Cross-Skill Combos

Expand Down
57 changes: 57 additions & 0 deletions manager/agent/shared-skills/send-file/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: send-file
description: Send a local file as a Matrix attachment to a chat room. Use when someone asks you to send, share, or transfer a file.
assign_when: Agent needs to send files to users via Matrix
---

# Send File

Send a local file as a Matrix attachment so the recipient can download it directly from the chat.

## Usage

**Manager:**
```bash
bash /opt/hiclaw/agent/shared-skills/send-file/scripts/send-file.sh <file_path> <room_id>
```

**Worker:**
```bash
bash ~/skills/send-file/scripts/send-file.sh <file_path> <room_id>
```

| Parameter | Description |
|-----------|-------------|
| `file_path` | Absolute path to the local file to send |
| `room_id` | Matrix room ID (e.g., `!abc123:domain`) — the target room |

On success, prints the `mxc://` URI. On failure, prints an error to stderr and exits non-zero.

## Examples

### Send a file you just created

```bash
echo "# Report" > /tmp/report.md
bash ~/skills/send-file/scripts/send-file.sh /tmp/report.md '!roomid:domain'
```

### Download from OSS/MinIO then send

```bash
mc cp ${HICLAW_STORAGE_PREFIX}/shared/tasks/task-123/output.csv /tmp/output.csv
bash ~/skills/send-file/scripts/send-file.sh /tmp/output.csv '!roomid:domain'
```

### Send a task result

```bash
bash ~/skills/send-file/scripts/send-file.sh ~/shared/tasks/task-123/result.md '!roomid:domain'
```

## Important

- The file must exist locally before sending — download it first if it's on OSS/MinIO
- MIME type is auto-detected from the file content
- Credentials are auto-resolved from environment variables or `openclaw.json`
- Do NOT paste file contents as text when asked to "send" a file — use this script to send as a proper downloadable attachment
149 changes: 149 additions & 0 deletions manager/agent/shared-skills/send-file/scripts/send-file.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/bin/bash
# send-file.sh — Upload a local file to Matrix and send as m.file attachment
#
# Usage: send-file.sh <file_path> <room_id>
#
# Credentials (checked in order):
# 1. MATRIX_ACCESS_TOKEN env var
# 2. accessToken from openclaw.json (auto-detected path)
#
# Matrix server: HICLAW_MATRIX_SERVER env var (required)
#
# Output: mxc:// URI on success
# Exit: 0 on success, 1 on failure

set -euo pipefail

# ============================================================
# Arguments
# ============================================================
if [ $# -lt 2 ]; then
echo "Usage: send-file.sh <file_path> <room_id>" >&2
exit 1
fi

FILE_PATH="$1"
ROOM_ID="$2"

# ============================================================
# Validate file
# ============================================================
if [ ! -f "${FILE_PATH}" ]; then
echo "ERROR: File not found: ${FILE_PATH}" >&2
exit 1
fi

if [ ! -r "${FILE_PATH}" ]; then
echo "ERROR: File not readable: ${FILE_PATH}" >&2
exit 1
fi

FILENAME=$(basename "${FILE_PATH}")
FILE_SIZE=$(stat -c%s "${FILE_PATH}" 2>/dev/null || stat -f%z "${FILE_PATH}" 2>/dev/null || echo 0)

# ============================================================
# Resolve Matrix server URL
# ============================================================
MATRIX_URL="${HICLAW_MATRIX_SERVER:-}"
if [ -z "${MATRIX_URL}" ]; then
echo "ERROR: HICLAW_MATRIX_SERVER environment variable not set" >&2
exit 1
fi

# ============================================================
# Resolve access token
# ============================================================
TOKEN="${MATRIX_ACCESS_TOKEN:-}"

if [ -z "${TOKEN}" ]; then
# Try to find openclaw.json (Worker environments)
OPENCLAW_JSON=""

# CoPaw Worker: ~/.copaw-worker/<name>/openclaw.json
if [ -z "${OPENCLAW_JSON}" ]; then
for f in ~/.copaw-worker/*/openclaw.json; do
[ -f "$f" ] && OPENCLAW_JSON="$f" && break
done
fi

# OpenClaw Worker: ~/openclaw.json or ~/.openclaw/openclaw.json
[ -z "${OPENCLAW_JSON}" ] && [ -f ~/openclaw.json ] && OPENCLAW_JSON=~/openclaw.json
[ -z "${OPENCLAW_JSON}" ] && [ -f ~/.openclaw/openclaw.json ] && OPENCLAW_JSON=~/.openclaw/openclaw.json

if [ -n "${OPENCLAW_JSON}" ]; then
TOKEN=$(jq -r '.channels.matrix.accessToken // empty' "${OPENCLAW_JSON}" 2>/dev/null)
fi
fi

# Manager environment: try MANAGER_MATRIX_TOKEN
if [ -z "${TOKEN}" ]; then
TOKEN="${MANAGER_MATRIX_TOKEN:-}"
fi

if [ -z "${TOKEN}" ]; then
echo "ERROR: No Matrix access token found. Set MATRIX_ACCESS_TOKEN or ensure openclaw.json is available." >&2
exit 1
fi

# ============================================================
# Detect MIME type
# ============================================================
MIME_TYPE=$(file --brief --mime-type "${FILE_PATH}" 2>/dev/null || echo "application/octet-stream")

# ============================================================
# Step 1: Upload file to Matrix media repo
# ============================================================
UPLOAD_RESP=$(curl -sf -X POST \
"${MATRIX_URL}/_matrix/media/v3/upload?filename=$(printf '%s' "${FILENAME}" | jq -sRr @uri)" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: ${MIME_TYPE}" \
--data-binary "@${FILE_PATH}" 2>&1) || {
echo "ERROR: File upload failed" >&2
echo "${UPLOAD_RESP}" >&2
exit 1
}

MXC_URI=$(echo "${UPLOAD_RESP}" | jq -r '.content_uri // empty' 2>/dev/null)
if [ -z "${MXC_URI}" ]; then
echo "ERROR: Upload succeeded but no content_uri in response" >&2
echo "${UPLOAD_RESP}" >&2
exit 1
fi

# ============================================================
# Step 2: Send m.file event to room
# ============================================================
TXN_ID="sf-$(date +%s%N)"
ROOM_ENC="${ROOM_ID//!/%21}"

SEND_RESP=$(curl -sf -X PUT \
"${MATRIX_URL}/_matrix/client/v3/rooms/${ROOM_ENC}/send/m.room.message/${TXN_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H 'Content-Type: application/json' \
-d "$(jq -n \
--arg body "${FILENAME}" \
--arg url "${MXC_URI}" \
--arg mime "${MIME_TYPE}" \
--argjson size "${FILE_SIZE}" \
'{
msgtype: "m.file",
body: $body,
url: $url,
info: { mimetype: $mime, size: $size }
}')" 2>&1) || {
echo "ERROR: Failed to send file event to room" >&2
echo "${SEND_RESP}" >&2
exit 1
}

EVENT_ID=$(echo "${SEND_RESP}" | jq -r '.event_id // empty' 2>/dev/null)
if [ -z "${EVENT_ID}" ]; then
echo "ERROR: Send succeeded but no event_id in response" >&2
echo "${SEND_RESP}" >&2
exit 1
fi

# ============================================================
# Success
# ============================================================
echo "${MXC_URI}"
13 changes: 13 additions & 0 deletions manager/agent/skills/worker-management/scripts/create-worker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,19 @@ if [ -d "${WORKER_AGENT_SRC}" ]; then
|| log " WARNING: Failed to push ${_skill_name} skill"
done
fi

# Push shared skills (used by both Manager and Workers)
if [ -d "/opt/hiclaw/agent/shared-skills" ]; then
for _skill_dir in /opt/hiclaw/agent/shared-skills/*/; do
[ ! -d "${_skill_dir}" ] && continue
_skill_name=$(basename "${_skill_dir}")
log " Pushing shared skill ${_skill_name} to worker MinIO..."
mc mirror "${_skill_dir}" \
"${HICLAW_STORAGE_PREFIX}/agents/${WORKER_NAME}/skills/${_skill_name}/" --overwrite \
|| log " WARNING: Failed to push shared skill ${_skill_name}"
done
fi

log " Worker agent files pushed"
else
log " WARNING: worker-agent directory not found at ${WORKER_AGENT_SRC}"
Expand Down
1 change: 1 addition & 0 deletions manager/scripts/init/start-manager-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,7 @@ bash "$RENDER" /root/manager-workspace/copaw-worker-agent
bash "$RENDER" /opt/hiclaw/agent/worker-skills
bash "$RENDER" /opt/hiclaw/agent/worker-agent
bash "$RENDER" /opt/hiclaw/agent/copaw-worker-agent
bash "$RENDER" /opt/hiclaw/agent/shared-skills
log "Agent doc templates rendered"

# Cloud mode: start background file sync (workspace ↔ OSS) and initial push
Expand Down
12 changes: 12 additions & 0 deletions manager/scripts/init/upgrade-builtins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,18 @@ if [ -d "${WORKER_AGENT_SRC}" ] && mc alias ls hiclaw > /dev/null 2>&1; then
done
fi

# Push shared skills (used by both Manager and Workers)
if [ -d "${AGENT_SRC}/shared-skills" ]; then
for _skill_dir in "${AGENT_SRC}/shared-skills"/*/; do
[ ! -d "${_skill_dir}" ] && continue
_skill_name=$(basename "${_skill_dir}")
mc mirror "${_skill_dir}" \
"${HICLAW_STORAGE_PREFIX}/agents/${_worker_name}/skills/${_skill_name}/" --overwrite 2>/dev/null \
&& log " Updated shared skill: ${_skill_name}" \
|| log " WARNING: Failed to sync shared skill ${_skill_name}"
done
fi

# Push assigned worker-skills (on-demand skills from registry)
for _skill_name in $(jq -r --arg w "${_worker_name}" \
'.workers[$w].skills // [] | .[]' "${REGISTRY}" 2>/dev/null); do
Expand Down
Loading
Loading