Skip to content
Open
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,8 @@ mpa-create.config*.yaml
# PiAgent release archives are downloaded and verified during image builds.
examples/piagent_with_mcp/vendor/pi-*.tar.gz
.gstack/

# Local Managed Agents verification state; never part of an upstream change.
examples/16_self_host_sandbox/.local-managed-agents/
examples/16_self_host_sandbox/deploy_records/
examples/16_self_host_sandbox/soak_results_*/
7 changes: 7 additions & 0 deletions config.yaml.full
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ tool:
app_id:
app_secret:
transport: ws # `ws` | `webhook`
streaming: false
show_thinking: false
show_tool_calls: false
show_tool_results: false
separate_tool_call_cards: false
separate_thinking_card: false
create_topic: false
# [optional] for Volcengine Lake AI Service https://www.volcengine.com/product/las
mobile_use:
tool_id: #https://console.volcengine.com/ACEP
Expand Down
47 changes: 47 additions & 0 deletions docker/managed-agents/Dockerfile.worker
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
FROM python:3.12-slim-bookworm

# Build-only indexes: never export package credentials into the runtime image.
ARG PIP_INDEX_URL=https://pypi.org/simple
ARG UV_DEFAULT_INDEX=https://pypi.org/simple

ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
UV_LINK_MODE=copy \
MANAGED_AGENT_WORK_CONCURRENCY=500 \
MANAGED_AGENT_WORKDIR=/workspace \
MANAGED_AGENT_READY_FILE=/tmp/managed-agent-worker-ready \
PATH=/app/.venv/bin:$PATH

WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:0.11.28 /uv /uvx /bin/
COPY pyproject.toml uv.lock README.md LICENSE ./
COPY veadk ./veadk
COPY docker/managed-agents/verify_anthropic_sdk.py /tmp/verify_anthropic_sdk.py

# Export pinned versions and hashes; an index override changes downloads,
# never the locked dependency versions. No adjacent SDK wheel is needed.
RUN --mount=type=cache,id=veadk-managed-agents-uv-python312,target=/root/.cache/uv,sharing=locked \
uv export --quiet --frozen --no-dev --extra sandbox \
--no-emit-project --output-file /tmp/requirements.txt \
&& uv venv /app/.venv \
&& PIP_INDEX_URL="$PIP_INDEX_URL" UV_DEFAULT_INDEX="$UV_DEFAULT_INDEX" \
UV_INDEX_URL="$UV_DEFAULT_INDEX" \
uv pip install --python /app/.venv/bin/python --no-deps --require-hashes \
-r /tmp/requirements.txt \
&& PIP_INDEX_URL="$PIP_INDEX_URL" UV_DEFAULT_INDEX="$UV_DEFAULT_INDEX" \
UV_INDEX_URL="$UV_DEFAULT_INDEX" \
uv pip install --python /app/.venv/bin/python --no-deps . \
&& python /tmp/verify_anthropic_sdk.py \
&& rm /tmp/requirements.txt /tmp/verify_anthropic_sdk.py \
&& python -c "from veadk.runtime.managed_agents.dispatcher import EnvironmentWorkDispatcher; from anthropic.lib.tools.agent_toolset import beta_agent_toolset_20260401"

COPY docker/managed-agents/run-runtime.sh /app/run-runtime.sh
COPY docker/managed-agents/run-claimed.sh /opt/gem/run.sh
RUN useradd --create-home --uid 10001 veadk \
&& mkdir -p /workspace \
&& chmod 0755 /app/run-runtime.sh /opt/gem/run.sh \
&& chown veadk:veadk /workspace

USER 10001:10001
EXPOSE 8080
CMD ["bash", "/app/run-runtime.sh"]
21 changes: 21 additions & 0 deletions docker/managed-agents/Dockerfile.worker.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
**
!pyproject.toml
!uv.lock
!README.md
!LICENSE
!veadk/
!veadk/**
!docker/
!docker/managed-agents/
!docker/managed-agents/run-runtime.sh
!docker/managed-agents/run-claimed.sh
!docker/managed-agents/verify_anthropic_sdk.py
**/.env
**/.env.*
**/*.env
**/__pycache__
**/*.pyc
frontend
veadk/webui
**/deploy_records
**/soak_results_*
128 changes: 128 additions & 0 deletions docker/managed-agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Managed Agents Worker build

[中文版](README.zh.md)

This image runs the installed `veadk.runtime.managed_agents.worker` package and
the AgentKit health listener. The dispatcher and Session adapters ship inside
VeADK; no neighboring repository, private SDK wheel, frontend build, or cloud
deployment is required to build the image.

## Build and verify locally

From the repository root:

```bash
uv sync --extra dev --extra sandbox --extra extensions
uv run --extra dev --extra sandbox pytest tests/runtime/managed_agents tests/integrations/mpa
bash docker/managed-agents/build.sh veadk-managed-agents-worker:local
docker run --rm --entrypoint python veadk-managed-agents-worker:local \
-m veadk.runtime.managed_agents.worker --help
```

The build script defaults to `--load`; publication requires an explicit
`--push` argument and a registry/tag selected by the operator. Building an image
does not update an AgentKit Runtime, Tool, Session, or Kubernetes Deployment.
`MANAGED_AGENTS_PLATFORM` overrides the default `linux/amd64` build platform.

## Dependency and SDK pitfalls

Older source images copied a wheel from
`examples/16_self_host_sandbox/.docker-dist/anthropic-*.whl`, built from an adjacent
SDK checkout, and imported `anthropic.lib.environments._dispatcher`. A normal
Anthropic 1.3.0 installation provides Managed Sessions and native tools but does
not provide that private dispatcher. An installed package could import successfully
yet fail to start a Worker. The new dispatcher uses generated Work APIs and the
existing account model-work polling protocol instead; the Docker build validates
its import and native-tool availability before the image is considered built.

Images default to `https://pypi.org/simple`. If an approved package mirror does
not contain every version in `uv.lock`, use a complete mirror through build args;
do not loosen pins, remove hashes, or regenerate the lock to hide missing packages:

```bash
PIP_INDEX_URL=https://packages.example.com/simple \
UV_DEFAULT_INDEX=https://packages.example.com/simple \
bash docker/managed-agents/build.sh veadk-managed-agents-worker:local
```

Indexes are build inputs, not runtime environment variables. This helper accepts
unauthenticated mirror URLs and rejects user info, query strings and fragments
before invoking Docker. Authenticated mirrors require a separately configured
BuildKit secret integration; never put credentials in build arguments, image tags,
source, or logs. Frozen export preserves dependency hashes. The component uses
its own named, locked BuildKit cache rather than sharing an unrelated image
build's cache. This dedicated Worker image installs the sandbox extra. Feishu and
optional data integrations use a separate SDK installation with the extensions
extra; development and Codex packages are not included in the Worker image.

## Runtime configuration

Configure gateway/credential/identity settings through the platform's secret and
environment mechanisms. A minimal worker needs `ANTHROPIC_BASE_URL`, a Runtime
credential (`ANTHROPIC_ENVIRONMENT_KEY` or configured Runtime Identity), and its
Work scope. Account workers default to `MANAGED_AGENT_WORK_SCOPE=account` and
`MA_AGENT_LOOP_RUNTIME_TYPE=agentkit_runtime`; environment workers must set
`MANAGED_AGENT_WORK_SCOPE=environment` and `ANTHROPIC_ENVIRONMENT_ID` explicitly.
Session creation additionally requires a configured Agent ID. Session snapshots
remain authoritative for model, tools, Skills, MCP and credentials.

`MANAGED_AGENT_WORK_CONCURRENCY` defaults to 500 in the image. It counts Work
handlers, including waits, and does not enable simultaneous turns in one Session.
The worker ready file appears only after a valid successful Work poll, and is
removed while draining. HTTP health endpoints alone do not prove worker readiness.
The entry point supervises worker/listener together; failure of either stops both.

For an AgentKit Tool executing an already claimed Work, the image also retains
`/opt/gem/run.sh`. It selects `MANAGED_AGENT_ENTRYPOINT_MODE=claimed`, uses injected
Work/Session/lease configuration, and defaults tool execution to local inside the
Tool's isolation. The regular image command selects continuous worker mode.

The process runs as UID/GID 10001. Mount a writable `/workspace` and `/tmp` when
using a read-only filesystem. Native bash must execute within the deployment's OS
isolation; path-confined file tools do not provide a shell security sandbox.

The [Kubernetes example](k8s/worker.yaml) references an operator-created Secret;
replace the image placeholder before applying. No secret values or real deployment
identifiers are supplied. Kubernetes, AgentKit and live model acceptance require
separate deployment authorization and are not proven by local tests/builds.

## Official SDK extension strategy

The current image installs the unchanged official PyPI `anthropic==1.3.0` selected
by `uv.lock`; it does **not** install the historical adjacent custom wheel.
The locked SHA256 matches the [official release](https://pypi.org/project/anthropic/1.3.0/).
The installer validates artifact hashes, and `verify_anthropic_sdk.py` checks every
installed SDK Python file against wheel RECORD, rejecting changed, missing and
unrecorded files. RECORD checking is an integrity check, not an independent origin
proof if the wheel and its RECORD are both replaced; the lock hash supplies that boundary.

VeADK uses composition rather than subclassing or globally patching the SDK:

| Behavior | Implementation |
| --- | --- |
| environment Work poll | official `beta.environments.work.with_raw_response.poll`; inspect HTTP 204 before validating Work |
| account model Work poll | public `AsyncAnthropic.get` for `/v1/model-work/poll`, a VeADK gateway extension, not a Claude API endpoint |
| ack / heartbeat / stop and Session events | official generated SDK APIs |
| account/runtime routing headers | public `default_headers` / `extra_headers` |
| Runtime Identity rotation | public HTTP client request/response hooks; preserve Work-scoped Bearer alongside gateway identity |
| native tool execution | official `agent_toolset` factory and public `ToolError` |

The official SDK documents [raw responses, custom requests and HTTP clients](https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/python#advanced-usage).
Its `EnvironmentWorker` has no arbitrary VeADK handler callback; subclassing would
require overriding private execution methods. No SDK source files or methods are
replaced. The per-Work client is a copy with inherited auth cleared; setting its
public `api_key=None` is necessary because SDK 1.3.0 `copy(api_key=None)` inherits
the parent key.

Two SDK internals remain version-sensitive: `_skills._download_and_extract` is
shipped by the official package and preserves fail-fast Skill download semantics
(the public helper skips failures); the tool decorator's `_context_manager` is
read for additive cleanup. VeADK performs cleanup locally to avoid the SDK helper
logging raw exception text. Real SDK archive, tool, auth and dispatcher tests cover
these boundaries. A locked official SDK does not require a custom SDK fork.

For a local VKE Worker with no Session Identity reference, explicitly set
`MANAGED_AGENT_MODEL_CREDENTIAL_SOURCE=environment` and supply `MODEL_AGENT_API_KEY`
through a dedicated Kubernetes Secret. The default is `session`; AgentKit and any
Session with a provider reference always require Identity and never fall back.
This choice verifies configured model credentials, not cloud Identity delivery.
109 changes: 109 additions & 0 deletions docker/managed-agents/README.zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Managed Agents Worker 构建

[English](README.md)

镜像运行安装后的 `veadk.runtime.managed_agents.worker` 包与 AgentKit 健康监听器。
dispatcher 和 Session 适配器包含在 VeADK 内,构建不依赖相邻仓库、私有 SDK wheel、
前端构建或线上部署。

## 本地构建与验证

在仓库根目录运行:

```bash
uv sync --extra dev --extra sandbox --extra extensions
uv run --extra dev --extra sandbox pytest tests/runtime/managed_agents tests/integrations/mpa
bash docker/managed-agents/build.sh veadk-managed-agents-worker:local
docker run --rm --entrypoint python veadk-managed-agents-worker:local \
-m veadk.runtime.managed_agents.worker --help
```

构建脚本默认 `--load`。发布必须显式增加 `--push`,并由操作者选择镜像仓库/tag。
镜像构建不更新 AgentKit Runtime、Tool、Session 或 Kubernetes Deployment。
`MANAGED_AGENTS_PLATFORM` 可覆盖默认 `linux/amd64` 平台。

## 依赖与 SDK 注意事项

旧来源镜像复制相邻 SDK checkout 构建的
`examples/16_self_host_sandbox/.docker-dist/anthropic-*.whl`,并导入
`anthropic.lib.environments._dispatcher`。普通 Anthropic 1.3.0 安装包含 Managed
Sessions 与原生工具,但不包含该私有 dispatcher,导致包导入成功而 Worker 启动失败。
新 dispatcher 使用生成的 Work API 与既有账号级模型 Work 轮询协议;Docker 构建结束
前验证其导入与原生工具可用性。

镜像默认使用 `https://pypi.org/simple`。如果批准的镜像源没有 `uv.lock` 中全部版本,
通过 build args 使用完整镜像源;不能放宽版本、删除哈希或重建锁文件掩盖缺失:

```bash
PIP_INDEX_URL=https://packages.example.com/simple \
UV_DEFAULT_INDEX=https://packages.example.com/simple \
bash docker/managed-agents/build.sh veadk-managed-agents-worker:local
```

包源只作为构建输入,不写入运行时环境。本脚本只接受无认证镜像源,调用 Docker
前拒绝用户名密码、查询字符串与 fragment。认证镜像源必须单独配置 BuildKit secret
集成;凭据不能写入 build args、镜像 tag、源码或日志。冻结导出保留依赖哈希。组件使用
独立命名且锁定的 BuildKit 缓存,避免与无关镜像构建共用缓存。专用 Worker 镜像只
安装 sandbox extra。飞书和可选数据集成使用安装 extensions extra 的独立 SDK 环境;
Worker 镜像不包含开发或 Codex 包。

## 运行时配置

通过平台 Secret 和环境配置提供网关、凭据及 Identity 信息。最小 Worker 需要
`ANTHROPIC_BASE_URL`、Runtime 凭据(`ANTHROPIC_ENVIRONMENT_KEY` 或配置后的
Runtime Identity)与 Work 作用域。账号 Worker 默认
`MANAGED_AGENT_WORK_SCOPE=account` 和 `MA_AGENT_LOOP_RUNTIME_TYPE=agentkit_runtime`;
Environment Worker 必须显式设置 `MANAGED_AGENT_WORK_SCOPE=environment` 与
`ANTHROPIC_ENVIRONMENT_ID`。创建 Session 还需要配置 Agent ID。Session 快照是模型、
工具、Skills、MCP 与凭据的权威来源。

镜像默认 `MANAGED_AGENT_WORK_CONCURRENCY=500`,它限制包含等待时间的 Work handler
数量,不允许同一 Session 同时执行多个轮次。只有成功完成合法 Work 轮询后才创建
就绪文件,排空时删除。HTTP 健康端点不能单独证明 Worker 就绪。启动入口同时监督
Worker 与监听器,任一进程停止会终止另一个。

对于执行已领取 Work 的 AgentKit Tool,镜像保留 `/opt/gem/run.sh`,选择
`MANAGED_AGENT_ENTRYPOINT_MODE=claimed`,使用注入的 Work/Session/租约配置,并默认
在 Tool 隔离中本地执行工具。镜像常规命令选择持续 Worker 模式。

进程以 UID/GID 10001 运行。使用只读文件系统时,挂载可写 `/workspace` 与 `/tmp`。
原生 bash 必须在部署提供的 OS 隔离中执行;文件路径限制不能替代 shell 安全沙箱。

[Kubernetes 示例](k8s/worker.yaml) 引用操作者创建的 Secret;应用前替换镜像占位符。
模板不提供密钥值或真实部署标识。Kubernetes、AgentKit 和真实模型验收需要独立部署
授权,本地测试或构建不能证明线上验收。

## 官方 SDK 扩展方案

当前镜像安装 `uv.lock` 选定的 **未修改的官方 PyPI `anthropic==1.3.0`**,不会安装
历史相邻仓库的定制 wheel。锁定 SHA256 与[官方发行版](https://pypi.org/project/anthropic/1.3.0/)
一致。安装器验证发行物哈希,`verify_anthropic_sdk.py` 再将所有已安装 SDK Python
文件与 wheel RECORD 对比,拒绝文件被修改、缺失或额外覆盖。RECORD 是完整性校验,
若 wheel 与 RECORD 同时被替换则不能独立证明来源;此边界由锁定发行物哈希保障。

VeADK 采用组合,无需继承或全局修改 SDK 方法:

| 行为 | 实现 |
| --- | --- |
| 环境级 Work 轮询 | 官方 `beta.environments.work.with_raw_response.poll`,先检查 HTTP 204 再校验 Work |
| 账号级模型 Work 轮询 | 公开 `AsyncAnthropic.get` 请求 `/v1/model-work/poll`;这是 VeADK 网关扩展,非 Claude API 端点 |
| ack / heartbeat / stop 与 Session 事件 | 官方 SDK 生成接口 |
| 账号/Runtime 路由头 | 公开 `default_headers` / `extra_headers` |
| Runtime Identity 轮转 | 公开 HTTP client 请求/响应 hooks,保留 Work Bearer 与网关 Identity 双重认证 |
| 原生工具执行 | 官方 `agent_toolset` 工厂与公开 `ToolError` |

官方文档说明了[原始响应、自定义请求及 HTTP client](https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/python#advanced-usage)。
官方 `EnvironmentWorker` 没有任意 VeADK handler 回调;继承需要覆盖私有执行方法。
本实现不覆盖 SDK 源码或方法。每个 Work 使用清除父认证的客户端副本;必须设置
副本公开属性 `api_key=None`,因为 SDK 1.3.0 的 `copy(api_key=None)` 会继承父 Key。

仍有两个版本敏感的 SDK 内部接口:官方发行包内的 `_skills._download_and_extract`
保留 Skill 下载失败即失败的语义,公开 helper 会跳过失败;工具 decorator 的
`_context_manager` 用于额外的退出清理。VeADK 在本地执行清理,避免 SDK helper
直接输出异常文本。真实 SDK 的归档、工具、认证与 dispatcher 测试覆盖这些边界。
锁定官方 SDK 版本不等于需要定制 SDK fork。

本地 VKE Worker 没有 Session Identity 引用时,可显式设置
`MANAGED_AGENT_MODEL_CREDENTIAL_SOURCE=environment`,通过独立 Kubernetes Secret
提供 `MODEL_AGENT_API_KEY`。默认 `session`;AgentKit 及带 provider 引用的 Session
始终使用 Identity,不会兜底。此路径证明配置的模型凭据,不证明云 Identity 下发。
51 changes: 51 additions & 0 deletions docker/managed-agents/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"

if [[ $# -lt 1 || $# -gt 2 ]]; then
echo "usage: $0 IMAGE[:TAG] [--push]" >&2
exit 2
fi
image="$1"
output="--load"
if [[ $# == 2 ]]; then
if [[ "$2" != "--push" ]]; then
echo "unknown option: $2" >&2
exit 2
fi
output="--push"
fi

PIP_INDEX_URL="${PIP_INDEX_URL:-https://pypi.org/simple}"
UV_DEFAULT_INDEX="${UV_DEFAULT_INDEX:-${UV_INDEX_URL:-$PIP_INDEX_URL}}"

# Build arguments can survive in image/build metadata. Do not pass credentials.
PIP_INDEX_URL="$PIP_INDEX_URL" UV_DEFAULT_INDEX="$UV_DEFAULT_INDEX" python3 - <<'PY'
import os
import sys
from urllib.parse import urlsplit

for name in ("PIP_INDEX_URL", "UV_DEFAULT_INDEX"):
try:
value = urlsplit(os.environ[name])
valid = (
value.scheme in {"http", "https"}
and value.hostname
and not value.username
and not value.password
and not value.query
and not value.fragment
)
except ValueError:
valid = False
if not valid:
sys.exit("Package indexes must be HTTP(S) URLs without credentials, query strings or fragments")
PY

exec docker buildx build --platform "${MANAGED_AGENTS_PLATFORM:-linux/amd64}" \
"$output" --file "$SCRIPT_DIR/Dockerfile.worker" --tag "$image" \
--build-arg PIP_INDEX_URL="$PIP_INDEX_URL" \
--build-arg UV_DEFAULT_INDEX="$UV_DEFAULT_INDEX" \
"$REPO_ROOT"
Loading