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
1 change: 1 addition & 0 deletions changelog/current.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Record image-affecting changes to `manager/`, `worker/`, `copaw/`, `openclaw-bas

**Bug Fixes**

- **OpenHuman AgentTeams runtime contract**: OpenHuman workers now consume terminal `AGENTTEAMS_*` identity, storage, Matrix, gateway, and controller settings; the controller again preserves their dedicated workspace and `openhuman` runtime label after backend normalization, and Helm selects the renamed worker image.
- **Legacy Team channel policy**: Legacy Team reconciliation now writes final Matrix channel allow-lists to member runtime config and re-adds the Team Leader to the Manager allow-list so controller integration tests observe durable policy state.
- **Sandbox worker-deps hardening**: Sandbox-backed Workers now prepare controller-owned worker-deps env/token/data material before claim creation, recycle stale SandboxClaims and bound Sandboxes on runtime-affecting changes, and use bounded ServiceAccount token projection for built-in SandboxClaim mounts.
- **CLI AgentTeams auth env**: The `hiclaw` CLI now discovers `AGENTTEAMS_CONTROLLER_URL`, `AGENTTEAMS_AUTH_TOKEN`, `AGENTTEAMS_AUTH_TOKEN_FILE`, and `AGENTTEAMS_CLUSTER_ID` while preserving legacy `HICLAW_*` fallbacks, so Manager and Worker containers can use the terminal env names for controller calls.
Expand Down
2 changes: 1 addition & 1 deletion helm/hiclaw/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ worker:
repository: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/agentteams-hermes-worker
tag: "" # defaults to global.imageTag
openhuman:
repository: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/hiclaw-openhuman-worker
repository: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/agentteams-openhuman-worker
tag: "" # defaults to global.imageTag
defaultRuntime: "openclaw"
resources:
Expand Down
4 changes: 3 additions & 1 deletion hiclaw-controller/internal/backend/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ func (d *DockerBackend) Create(ctx context.Context, req CreateRequest) (*WorkerR

// Infer WorkingDir from HOME env if not set
if req.WorkingDir == "" {
if home, ok := req.Env["HOME"]; ok {
if req.Runtime == RuntimeOpenHuman {
req.WorkingDir = "/home/openhuman/.openhuman"
} else if home, ok := req.Env["HOME"]; ok {
req.WorkingDir = home
}
}
Expand Down
84 changes: 75 additions & 9 deletions hiclaw-controller/internal/backend/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,13 @@ func TestDockerCreatePullsImage(t *testing.T) {
}

// captureCreateImagesServer is a minimal Docker mock that records the Image
// field of every POST /containers/create request. Other endpoints return the
// minimum responses required to make DockerBackend.Create succeed.
// and WorkingDir fields of every POST /containers/create request. Other
// endpoints return the minimum responses required to make DockerBackend.Create
// succeed.
type capturedCreateBodies struct {
srv *httptest.Server
images []string
srv *httptest.Server
images []string
workingDirs []string
}

func (c *capturedCreateBodies) lastImage() string {
Expand All @@ -257,6 +259,13 @@ func (c *capturedCreateBodies) lastImage() string {
return c.images[len(c.images)-1]
}

func (c *capturedCreateBodies) lastWorkingDir() string {
if len(c.workingDirs) == 0 {
return ""
}
return c.workingDirs[len(c.workingDirs)-1]
}

func captureCreateImagesServer(t *testing.T) *capturedCreateBodies {
t.Helper()
captured := &capturedCreateBodies{}
Expand All @@ -271,6 +280,11 @@ func captureCreateImagesServer(t *testing.T) *capturedCreateBodies {
if img, ok := body["Image"].(string); ok {
captured.images = append(captured.images, img)
}
if workingDir, ok := body["WorkingDir"].(string); ok {
captured.workingDirs = append(captured.workingDirs, workingDir)
} else {
captured.workingDirs = append(captured.workingDirs, "")
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"Id": "sha256-test"})
})
Expand Down Expand Up @@ -442,11 +456,13 @@ func TestDockerCreateResolvesImageFromRuntime(t *testing.T) {
}{
{"explicit_copaw_uses_copaw_image", RuntimeCopaw, "", "agentteams/copaw-worker:latest"},
{"explicit_hermes_uses_hermes_image", RuntimeHermes, "", "agentteams/hermes-worker:latest"},
{"explicit_openhuman_uses_openhuman_image", RuntimeOpenHuman, "", "agentteams/openhuman-worker:latest"},
{"explicit_qwenpaw_uses_qwenpaw_image", RuntimeQwenPaw, "", "agentteams/qwenpaw-worker:latest"},
{"explicit_openclaw_uses_worker_image", RuntimeOpenClaw, "", "agentteams/worker-agent:latest"},
{"empty_runtime_with_no_fallback_uses_worker_image", "", "", "agentteams/worker-agent:latest"},
{"empty_runtime_with_copaw_fallback_uses_copaw_image", "", RuntimeCopaw, "agentteams/copaw-worker:latest"},
{"empty_runtime_with_hermes_fallback_uses_hermes_image", "", RuntimeHermes, "agentteams/hermes-worker:latest"},
{"empty_runtime_with_openhuman_fallback_uses_openhuman_image", "", RuntimeOpenHuman, "agentteams/openhuman-worker:latest"},
{"empty_runtime_with_qwenpaw_fallback_uses_qwenpaw_image", "", RuntimeQwenPaw, "agentteams/qwenpaw-worker:latest"},
{"explicit_runtime_overrides_fallback", RuntimeOpenClaw, RuntimeHermes, "agentteams/worker-agent:latest"},
}
Expand All @@ -457,11 +473,12 @@ func TestDockerCreateResolvesImageFromRuntime(t *testing.T) {

b := &DockerBackend{
config: DockerConfig{
WorkerImage: "agentteams/worker-agent:latest",
CopawWorkerImage: "agentteams/copaw-worker:latest",
HermesWorkerImage: "agentteams/hermes-worker:latest",
QwenPawWorkerImage: "agentteams/qwenpaw-worker:latest",
DefaultNetwork: "hiclaw-net",
WorkerImage: "agentteams/worker-agent:latest",
CopawWorkerImage: "agentteams/copaw-worker:latest",
HermesWorkerImage: "agentteams/hermes-worker:latest",
OpenHumanWorkerImage: "agentteams/openhuman-worker:latest",
QwenPawWorkerImage: "agentteams/qwenpaw-worker:latest",
DefaultNetwork: "hiclaw-net",
},
containerPrefix: "agentteams-worker-",
client: &http.Client{
Expand All @@ -483,3 +500,52 @@ func TestDockerCreateResolvesImageFromRuntime(t *testing.T) {
})
}
}

func TestDockerCreateRuntimeWorkingDir(t *testing.T) {
const home = "/root/hiclaw-fs/agents/x"
cases := []struct {
name string
runtime string
explicit string
wantWorkingDir string
}{
{"openclaw_uses_home", RuntimeOpenClaw, "", home},
{"copaw_uses_home", RuntimeCopaw, "", home},
{"hermes_uses_home", RuntimeHermes, "", home},
{"openhuman_uses_dedicated_workspace", RuntimeOpenHuman, "", "/home/openhuman/.openhuman"},
{"empty_runtime_uses_home", "", "", home},
{"explicit_working_dir_wins", RuntimeOpenHuman, "/custom/workspace", "/custom/workspace"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
captured := captureCreateImagesServer(t)
defer captured.srv.Close()
b := &DockerBackend{
config: DockerConfig{
WorkerImage: "agentteams/worker-agent:latest",
CopawWorkerImage: "agentteams/copaw-worker:latest",
HermesWorkerImage: "agentteams/hermes-worker:latest",
OpenHumanWorkerImage: "agentteams/openhuman-worker:latest",
DefaultNetwork: "hiclaw-net",
},
containerPrefix: "agentteams-worker-",
client: &http.Client{
Transport: &testTransport{serverURL: captured.srv.URL},
},
}

_, err := b.Create(context.Background(), CreateRequest{
Name: "x",
Runtime: tc.runtime,
WorkingDir: tc.explicit,
Env: map[string]string{"HOME": home},
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
if got := captured.lastWorkingDir(); got != tc.wantWorkingDir {
t.Fatalf("create body WorkingDir = %q, want %q", got, tc.wantWorkingDir)
}
})
}
}
3 changes: 3 additions & 0 deletions hiclaw-controller/internal/backend/interface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ func TestResolveRuntime(t *testing.T) {
{"explicit_over_empty_fallback", RuntimeOpenClaw, "", RuntimeOpenClaw},
{"empty_uses_fallback_hermes", "", RuntimeHermes, RuntimeHermes},
{"empty_uses_fallback_copaw", "", RuntimeCopaw, RuntimeCopaw},
{"empty_uses_fallback_openhuman", "", RuntimeOpenHuman, RuntimeOpenHuman},
{"empty_uses_fallback_qwenpaw", "", RuntimeQwenPaw, RuntimeQwenPaw},
{"empty_and_no_fallback_uses_openclaw", "", "", RuntimeOpenClaw},
{"explicit_openclaw_preserved", RuntimeOpenClaw, RuntimeHermes, RuntimeOpenClaw},
{"explicit_hermes_preserved", RuntimeHermes, RuntimeCopaw, RuntimeHermes},
{"explicit_openhuman_preserved", RuntimeOpenHuman, RuntimeCopaw, RuntimeOpenHuman},
{"explicit_qwenpaw_preserved", RuntimeQwenPaw, RuntimeCopaw, RuntimeQwenPaw},
}
for _, tc := range cases {
Expand All @@ -38,6 +40,7 @@ func TestValidRuntime(t *testing.T) {
{RuntimeOpenClaw, true},
{RuntimeCopaw, true},
{RuntimeHermes, true},
{RuntimeOpenHuman, true},
{RuntimeQwenPaw, true},
{"unknown", false},
}
Expand Down
4 changes: 4 additions & 0 deletions hiclaw-controller/internal/backend/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ func (k *K8sBackend) Create(ctx context.Context, req CreateRequest) (*WorkerResu
req.Env = map[string]string{}
}
req.Env["HOME"] = req.WorkingDir
case req.Runtime == RuntimeOpenHuman:
req.WorkingDir = "/home/openhuman/.openhuman"
default:
// Both openclaw and hermes use the same workspace layout:
// HOME == WorkingDir == /root/hiclaw-fs/agents/<name> (== MinIO
Expand Down Expand Up @@ -744,6 +746,8 @@ func defaultRuntime(runtime string) string {
return RuntimeCopaw
case RuntimeHermes:
return RuntimeHermes
case RuntimeOpenHuman:
return RuntimeOpenHuman
case RuntimeQwenPaw:
return RuntimeQwenPaw
default:
Expand Down
35 changes: 20 additions & 15 deletions hiclaw-controller/internal/backend/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -618,8 +618,8 @@ func TestK8sWithPrefixStop(t *testing.T) {

// TestK8sCreateRuntimeWorkingDir verifies WorkingDir / HOME defaulting per
// runtime. The hermes runtime now shares the openclaw layout: WorkingDir ==
// HOME == /root/hiclaw-fs/agents/<name> (== MinIO mirror root). Only copaw
// now shares the same layout.
// HOME == /root/hiclaw-fs/agents/<name> (== MinIO mirror root). OpenHuman
// keeps the dedicated workspace baked into its image.
func TestK8sCreateRuntimeWorkingDir(t *testing.T) {
cases := []struct {
name string
Expand All @@ -630,18 +630,20 @@ func TestK8sCreateRuntimeWorkingDir(t *testing.T) {
{"openclaw", RuntimeOpenClaw, "/root/hiclaw-fs/agents/x", "/root/hiclaw-fs/agents/x"},
{"hermes", RuntimeHermes, "/root/hiclaw-fs/agents/x", "/root/hiclaw-fs/agents/x"},
{"copaw", RuntimeCopaw, "/root/hiclaw-fs/agents/x", "/root/hiclaw-fs/agents/x"},
{"openhuman", RuntimeOpenHuman, "/home/openhuman/.openhuman", ""},
{"empty_default", "", "/root/hiclaw-fs/agents/x", "/root/hiclaw-fs/agents/x"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
client := newFakeK8sCoreClient()
b := NewK8sBackendWithClient(client, K8sConfig{
Namespace: "hiclaw",
WorkerImage: "agentteams/worker-agent:latest",
CopawWorkerImage: "agentteams/copaw-worker:latest",
HermesWorkerImage: "agentteams/hermes-worker:latest",
WorkerCPU: "1000m",
WorkerMemory: "2Gi",
Namespace: "hiclaw",
WorkerImage: "agentteams/worker-agent:latest",
CopawWorkerImage: "agentteams/copaw-worker:latest",
HermesWorkerImage: "agentteams/hermes-worker:latest",
OpenHumanWorkerImage: "agentteams/openhuman-worker:latest",
WorkerCPU: "1000m",
WorkerMemory: "2Gi",
}, "agentteams-worker-", nil)

if _, err := b.Create(context.Background(), CreateRequest{
Expand Down Expand Up @@ -685,25 +687,28 @@ func TestK8sCreateResolvesImageFromRuntime(t *testing.T) {
}{
{"explicit_copaw", RuntimeCopaw, "", "agentteams/copaw-worker:latest", RuntimeCopaw},
{"explicit_hermes", RuntimeHermes, "", "agentteams/hermes-worker:latest", RuntimeHermes},
{"explicit_openhuman", RuntimeOpenHuman, "", "agentteams/openhuman-worker:latest", RuntimeOpenHuman},
{"explicit_qwenpaw", RuntimeQwenPaw, "", "agentteams/qwenpaw-worker:latest", RuntimeQwenPaw},
{"explicit_openclaw", RuntimeOpenClaw, "", "agentteams/worker-agent:latest", RuntimeOpenClaw},
{"empty_no_fallback", "", "", "agentteams/worker-agent:latest", RuntimeOpenClaw},
{"empty_with_copaw_fallback", "", RuntimeCopaw, "agentteams/copaw-worker:latest", RuntimeCopaw},
{"empty_with_hermes_fallback", "", RuntimeHermes, "agentteams/hermes-worker:latest", RuntimeHermes},
{"empty_with_openhuman_fallback", "", RuntimeOpenHuman, "agentteams/openhuman-worker:latest", RuntimeOpenHuman},
{"empty_with_qwenpaw_fallback", "", RuntimeQwenPaw, "agentteams/qwenpaw-worker:latest", RuntimeQwenPaw},
{"explicit_overrides_fallback", RuntimeOpenClaw, RuntimeHermes, "agentteams/worker-agent:latest", RuntimeOpenClaw},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
client := newFakeK8sCoreClient()
b := NewK8sBackendWithClient(client, K8sConfig{
Namespace: "hiclaw",
WorkerImage: "agentteams/worker-agent:latest",
CopawWorkerImage: "agentteams/copaw-worker:latest",
HermesWorkerImage: "agentteams/hermes-worker:latest",
QwenPawWorkerImage: "agentteams/qwenpaw-worker:latest",
WorkerCPU: "1000m",
WorkerMemory: "2Gi",
Namespace: "hiclaw",
WorkerImage: "agentteams/worker-agent:latest",
CopawWorkerImage: "agentteams/copaw-worker:latest",
HermesWorkerImage: "agentteams/hermes-worker:latest",
OpenHumanWorkerImage: "agentteams/openhuman-worker:latest",
QwenPawWorkerImage: "agentteams/qwenpaw-worker:latest",
WorkerCPU: "1000m",
WorkerMemory: "2Gi",
}, "agentteams-worker-", nil)

if _, err := b.Create(context.Background(), CreateRequest{
Expand Down
12 changes: 6 additions & 6 deletions openhuman/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# ---------------------------------------------------------------------------
# OpenHuman Worker — HiClaw-integrated multi-stage Docker build
# OpenHuman Worker - AgentTeams-integrated multi-stage Docker build
#
# Based on the upstream OpenHuman Dockerfile with the following changes:
# - Enables `channel-matrix` feature flag for native Matrix support
# - Installs mc (MinIO Client) for file sync with HiClaw centralized storage
# - Installs mc (MinIO Client) for file sync with AgentTeams centralized storage
# - Installs jq for JSON processing in entrypoint scripts
# - Copies HiClaw shared scripts and OpenHuman worker entrypoint
# - Copies AgentTeams shared scripts and OpenHuman worker entrypoint
# - Copies OpenHuman worker agent template (AGENTS.md, skills)
#
# License notice:
Expand All @@ -14,7 +14,7 @@
# The GPL-3.0 license text is preserved at /usr/share/licenses/openhuman/LICENSE
# inside the built image.
#
# Build context: HiClaw repo root (to access shared/lib/ and manager/agent/).
# Build context: AgentTeams repo root (to access shared/lib/ and manager/agent/).
# OpenHuman upstream source is fetched via git inside the builder stage —
# no `openhuman-src/` directory is required in the build context.
#
Expand Down Expand Up @@ -100,7 +100,7 @@ WORKDIR /build/openhuman-src
RUN cargo build --release --bin openhuman-core --features channel-matrix

# ==========================================================================
# Stage 3: Minimal runtime image with HiClaw integration
# Stage 3: Minimal runtime image with AgentTeams integration
# ==========================================================================
FROM debian:bookworm-slim AS runtime

Expand Down Expand Up @@ -149,7 +149,7 @@ RUN mkdir -p /home/openhuman/.openhuman/skills \
/home/openhuman/.openhuman/agent-config \
&& chown -R openhuman:openhuman /home/openhuman

# Copy HiClaw shared scripts
# Copy AgentTeams shared scripts
COPY shared/lib/ /opt/hiclaw/scripts/lib/
RUN chmod +x /opt/hiclaw/scripts/lib/*.sh 2>/dev/null || true

Expand Down
Loading
Loading