Builds on the v0.8.2 runtime. Four workstreams, all behind the §15 MCP door:
- Group rooms (Phase 1): migration 0026; N-way threads repo with a DM/room
count-guard; chat.send {room} + room.create/invite/leave tools; RoomMessage
-> room.message SSE; /api/claw-chat/rooms* APIs; Observer room badge.
- Per-claw door identity: door caller_agent resolves the X-ZeroClaw-Agent
header (set by the fork) to the specific claw, falling back to roster[0].
- Gated delegation bridge (Phase 3): clawmates__delegate door tool drives a
sibling via the existing /ws/chat ZeroClawDriveExecutor (not A2A); self-deny,
per-workspace hourly budget, audit trail, untrusted-banner result. Native
in-daemon delegation stays off (it would bypass the door).
- A2A tenant ingress (Phase 2): migration 0027 (workspace_a2a + a2a_tokens);
runtime_provision enable_a2a_server/publish_claw; routes/a2a.rs tenant-aware
proxy (per-workspace tokens, injected internal bearer, daemon stays internal,
cards URL-rewritten to the cm-api edge); a2a.invoked taxonomy.
Tests: cm-db room repos, cm-runtime chat tools, door units. sqlx cache updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
132 lines
4.0 KiB
Rust
132 lines
4.0 KiB
Rust
use axum::extract::{Path, Query, State};
|
|
use axum::Json;
|
|
use cm_db::repo::threads::{Thread, ThreadMessage};
|
|
use cm_domain::AgentId;
|
|
use serde::Deserialize;
|
|
use serde_json::{json, Value};
|
|
use uuid::Uuid;
|
|
|
|
use crate::routes::claws::workspace_agent;
|
|
use crate::{ApiError, AppState, Authed};
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct ThreadsQuery {
|
|
#[serde(rename = "clawId")]
|
|
claw_id: AgentId,
|
|
}
|
|
|
|
/// GET /api/claw-chat/threads?clawId= — the inter-agent inbox (§7.2).
|
|
pub async fn threads(
|
|
State(state): State<AppState>,
|
|
Authed(user): Authed,
|
|
Query(query): Query<ThreadsQuery>,
|
|
) -> Result<Json<Vec<Thread>>, ApiError> {
|
|
let agent = workspace_agent(&state, &user, query.claw_id).await?;
|
|
Ok(Json(
|
|
cm_db::repo::threads::list_for_agent(&state.pool, agent.id).await?,
|
|
))
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct MessagesQuery {
|
|
#[serde(rename = "clawId")]
|
|
claw_id: AgentId,
|
|
#[serde(rename = "threadId")]
|
|
thread_id: Uuid,
|
|
}
|
|
|
|
/// GET /api/claw-chat/messages?clawId=&threadId= — thread detail (§7.2).
|
|
pub async fn messages(
|
|
State(state): State<AppState>,
|
|
Authed(user): Authed,
|
|
Query(query): Query<MessagesQuery>,
|
|
) -> Result<Json<Vec<ThreadMessage>>, ApiError> {
|
|
let agent = workspace_agent(&state, &user, query.claw_id).await?;
|
|
if !cm_db::repo::threads::is_participant(&state.pool, query.thread_id, agent.id).await? {
|
|
return Err(ApiError::NotFound);
|
|
}
|
|
Ok(Json(
|
|
cm_db::repo::threads::messages(&state.pool, query.thread_id).await?,
|
|
))
|
|
}
|
|
|
|
/// Confirms a thread is in the caller's workspace (room API scoping).
|
|
async fn workspace_thread(
|
|
state: &AppState,
|
|
user: &cm_auth::AuthedUser,
|
|
thread_id: Uuid,
|
|
) -> Result<(), ApiError> {
|
|
match cm_db::repo::threads::workspace_of(&state.pool, thread_id).await? {
|
|
Some(ws) if ws == user.workspace_id.as_uuid() => Ok(()),
|
|
_ => Err(ApiError::NotFound),
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct CreateRoomBody {
|
|
subject: String,
|
|
members: Vec<AgentId>,
|
|
}
|
|
|
|
/// POST /api/claw-chat/rooms — create an N-way group room (operator action).
|
|
pub async fn create_room(
|
|
State(state): State<AppState>,
|
|
Authed(user): Authed,
|
|
Json(body): Json<CreateRoomBody>,
|
|
) -> Result<Json<Value>, ApiError> {
|
|
// Every member must be a claw in the caller's workspace.
|
|
for id in &body.members {
|
|
workspace_agent(&state, &user, *id).await?;
|
|
}
|
|
let thread_id = cm_db::repo::threads::create_room(
|
|
&state.pool,
|
|
user.workspace_id,
|
|
&body.subject,
|
|
None,
|
|
&body.members,
|
|
)
|
|
.await?;
|
|
Ok(Json(json!({ "threadId": thread_id })))
|
|
}
|
|
|
|
/// GET /api/claw-chat/rooms — list the workspace's group rooms.
|
|
pub async fn rooms(
|
|
State(state): State<AppState>,
|
|
Authed(user): Authed,
|
|
) -> Result<Json<Vec<Thread>>, ApiError> {
|
|
Ok(Json(
|
|
cm_db::repo::threads::list_rooms_for_workspace(&state.pool, user.workspace_id).await?,
|
|
))
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct AddParticipantBody {
|
|
#[serde(rename = "clawId")]
|
|
claw_id: AgentId,
|
|
}
|
|
|
|
/// POST /api/claw-chat/rooms/{threadId}/participants — add a claw to a room.
|
|
pub async fn add_participant(
|
|
State(state): State<AppState>,
|
|
Authed(user): Authed,
|
|
Path(thread_id): Path<Uuid>,
|
|
Json(body): Json<AddParticipantBody>,
|
|
) -> Result<Json<Value>, ApiError> {
|
|
workspace_thread(&state, &user, thread_id).await?;
|
|
workspace_agent(&state, &user, body.claw_id).await?;
|
|
cm_db::repo::threads::add_participant(&state.pool, thread_id, body.claw_id, None).await?;
|
|
Ok(Json(json!({ "ok": true })))
|
|
}
|
|
|
|
/// DELETE /api/claw-chat/rooms/{threadId}/participants/{clawId} — remove a claw.
|
|
pub async fn remove_participant(
|
|
State(state): State<AppState>,
|
|
Authed(user): Authed,
|
|
Path((thread_id, claw_id)): Path<(Uuid, AgentId)>,
|
|
) -> Result<Json<Value>, ApiError> {
|
|
workspace_thread(&state, &user, thread_id).await?;
|
|
workspace_agent(&state, &user, claw_id).await?;
|
|
cm_db::repo::threads::remove_participant(&state.pool, thread_id, claw_id).await?;
|
|
Ok(Json(json!({ "ok": true })))
|
|
}
|