Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,8 @@ To verify the loop:

This setup shares session evidence and generated skills with the group. `group_id` is a namespace, not an access-control boundary; use filesystem/share permissions and separate roots for different trust groups. Automated privacy redaction is not provided by this setup. The default publish mode is `direct`; use the `validated` flow above if clients should review candidates before publication.

For Cloudflare R2, use the **account endpoint**, such as `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`, with `sharing.backend s3`, `sharing.bucket <bucket-name>`, and `sharing.region auto`. Do not append the bucket to `sharing.endpoint`: boto3 sends the bucket separately. SkillClaw rejects that configuration before making a storage request. See the [R2 S3 API documentation](https://developers.cloudflare.com/r2/api/s3/api/).

## Quick Reference

**Operational model** — Every user has their own `skillclaw` process and `~/.skillclaw/config.yaml`. Each group maps to a `group_id`; one evolve server watches that namespace, turns sessions into skills, and writes them back. Joining a group only requires the correct sharing config on the client side.
Expand Down
2 changes: 2 additions & 0 deletions assets/README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,8 @@ Bob 使用相同配置,只把 `sharing.local_root` 改为 `/Volumes/skillclaw-

同组用户共享会话证据和生成的技能。`group_id` 只是命名空间,不是权限隔离;不同信任组应使用独立共享根目录和文件系统/共享访问权限。本例没有自动隐私脱敏。默认发布模式为 `direct`;若需先让客户端验证候选技能,请使用上文的 `validated` 流程。

Cloudflare R2 应使用**账号级 endpoint**,例如 `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,并配置 `sharing.backend s3`、`sharing.bucket <bucket-name>`、`sharing.region auto`。不要把 bucket 拼到 `sharing.endpoint` 后面;boto3 会单独发送 bucket。SkillClaw 会在访问存储前拒绝这种错误配置。参见 [R2 S3 API 文档](https://developers.cloudflare.com/r2/api/s3/api/)。

## 快速参考

**运行关系** — 每个用户有自己的 `skillclaw` 进程和 `~/.skillclaw/config.yaml`;每个群组对应一个 `group_id`,由一个 Evolver 持续读取会话、产出技能、写回存储。加入群组只需正确配置共享存储,无需部署服务端。
Expand Down
9 changes: 9 additions & 0 deletions skillclaw/object_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import os
from pathlib import Path
from typing import Iterator
from urllib.parse import urlsplit


class ObjectInfo:
Expand Down Expand Up @@ -123,6 +124,14 @@ def __init__(
region: str = "",
session_token: str = "",
) -> None:
parsed_endpoint = urlsplit(endpoint)
if (parsed_endpoint.hostname or "").endswith(".r2.cloudflarestorage.com"):
if parsed_endpoint.path not in {"", "/"}:
raise ValueError(
"Cloudflare R2 endpoint must be the account endpoint without a bucket/path suffix "
"(https://<ACCOUNT_ID>.r2.cloudflarestorage.com). "
"Set the bucket separately in sharing.bucket / EVOLVE_STORAGE_BUCKET."
)
try:
import boto3
except ImportError as exc:
Expand Down
41 changes: 41 additions & 0 deletions tests/test_object_store_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from unittest.mock import Mock

import boto3
import pytest

from skillclaw.object_store import S3ObjectStore


@pytest.mark.parametrize("path", ["/my-bucket", "/my-bucket/", "/prefix/data"])
def test_r2_rejects_bucket_suffix_before_opening_session(monkeypatch, path):
session = Mock()
monkeypatch.setattr(boto3.session, "Session", session)
with pytest.raises(ValueError, match="without a bucket/path suffix"):
S3ObjectStore(
endpoint="https://account.r2.cloudflarestorage.com" + path,
bucket="my-bucket",
access_key_id="",
secret_access_key="",
)
session.assert_not_called()


@pytest.mark.parametrize(
"endpoint",
[
"https://account.r2.cloudflarestorage.com",
"https://account.eu.r2.cloudflarestorage.com/",
"https://s3.example.test/proxy",
"",
],
)
def test_valid_endpoints_preserve_bucket_and_url(monkeypatch, endpoint):
client = Mock()
session = Mock(return_value=Mock(client=Mock(return_value=client)))
monkeypatch.setattr(boto3.session, "Session", session)
store = S3ObjectStore(endpoint=endpoint, bucket="my-bucket", access_key_id="", secret_access_key="")
expected = {"endpoint_url": endpoint} if endpoint else {}
session.return_value.client.assert_called_once_with("s3", **expected)
client.get_paginator.return_value.paginate.return_value = [{"Contents": [{"Key": "a"}]}]
assert [obj.key for obj in store.iter_objects("prefix/")] == ["a"]
client.get_paginator.return_value.paginate.assert_called_once_with(Bucket="my-bucket", Prefix="prefix/")
Loading