mcp door: extend service-session bearer to per-topic + per-loop spawns
Per-team runtimes had their MCP bearer swap wired in the prior slice, but per-topic (research_container::spawn) and per-loop (spawn_loop) containers still baked the stale template bearer and 401'd every tools/list. Same fix, extended: mint a workspace-owner service session via runtime_provision::mint_workspace_service_token and inject via prewrite_daemon_config_with_risk. Move mint_workspace_service_token from topology_worker into runtime_provision so all three spawn call sites share the helper. Callers updated: routes/research.rs (start_topic), routes/research_setup.rs (prepare_topic_runtime), routes/loops.rs (ensure_loop_container). Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
6504ed6062
commit
657b666219
@@ -187,6 +187,7 @@ pub async fn spawn(
|
|||||||
topic_id: Uuid,
|
topic_id: Uuid,
|
||||||
repo_host_path: &Path,
|
repo_host_path: &Path,
|
||||||
state_host_path: &Path,
|
state_host_path: &Path,
|
||||||
|
mcp_bearer: Option<&str>,
|
||||||
) -> Result<SpawnedContainer, String> {
|
) -> Result<SpawnedContainer, String> {
|
||||||
let name = container_name_for(topic_id);
|
let name = container_name_for(topic_id);
|
||||||
let gateway_url = format!("http://{name}:42617");
|
let gateway_url = format!("http://{name}:42617");
|
||||||
@@ -225,7 +226,7 @@ pub async fn spawn(
|
|||||||
// connect. These containers live on the private clawmates_core
|
// connect. These containers live on the private clawmates_core
|
||||||
// docker network and only accept traffic from the API server, so
|
// docker network and only accept traffic from the API server, so
|
||||||
// disabling pairing here is safe.
|
// disabling pairing here is safe.
|
||||||
prewrite_daemon_config(state_host_path)?;
|
prewrite_daemon_config_with_risk(state_host_path, None, mcp_bearer)?;
|
||||||
|
|
||||||
let mut mounts = vec![
|
let mut mounts = vec![
|
||||||
Mount {
|
Mount {
|
||||||
@@ -447,6 +448,7 @@ pub async fn spawn_loop(
|
|||||||
docker: &Docker,
|
docker: &Docker,
|
||||||
loop_id: Uuid,
|
loop_id: Uuid,
|
||||||
state_host_path: &Path,
|
state_host_path: &Path,
|
||||||
|
mcp_bearer: Option<&str>,
|
||||||
) -> Result<SpawnedContainer, String> {
|
) -> Result<SpawnedContainer, String> {
|
||||||
let name = loop_container_name_for(loop_id);
|
let name = loop_container_name_for(loop_id);
|
||||||
let gateway_url = format!("http://{name}:42617");
|
let gateway_url = format!("http://{name}:42617");
|
||||||
@@ -475,7 +477,7 @@ pub async fn spawn_loop(
|
|||||||
.map_err(|e| format!("mkdir {}: {e}", state_host_path.display()))?;
|
.map_err(|e| format!("mkdir {}: {e}", state_host_path.display()))?;
|
||||||
// Same pairing bypass as `spawn` — per-loop containers are
|
// Same pairing bypass as `spawn` — per-loop containers are
|
||||||
// ephemeral, on a private docker network, and freshly created.
|
// ephemeral, on a private docker network, and freshly created.
|
||||||
prewrite_daemon_config(state_host_path)?;
|
prewrite_daemon_config_with_risk(state_host_path, None, mcp_bearer)?;
|
||||||
|
|
||||||
let mounts = vec![Mount {
|
let mounts = vec![Mount {
|
||||||
target: Some("/zeroclaw-data".into()),
|
target: Some("/zeroclaw-data".into()),
|
||||||
|
|||||||
@@ -245,7 +245,24 @@ async fn ensure_loop_container(pool: &PgPool, workspace_id: Uuid, loop_id: Uuid)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let state_root = loop_state_root().join(loop_id.to_string()).join("state");
|
let state_root = loop_state_root().join(loop_id.to_string()).join("state");
|
||||||
let spawned = match crate::research_container::spawn_loop(&docker, loop_id, &state_root).await {
|
let mcp_bearer = crate::runtime_provision::mint_workspace_service_token(
|
||||||
|
pool,
|
||||||
|
cm_domain::WorkspaceId::from(workspace_id),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("loops::ensure_loop_container({loop_id}): mint MCP bearer failed: {e}");
|
||||||
|
e
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
let spawned = match crate::research_container::spawn_loop(
|
||||||
|
&docker,
|
||||||
|
loop_id,
|
||||||
|
&state_root,
|
||||||
|
mcp_bearer.as_deref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("loops::ensure_loop_container({loop_id}): spawn failed: {e}");
|
eprintln!("loops::ensure_loop_container({loop_id}): spawn failed: {e}");
|
||||||
|
|||||||
@@ -772,7 +772,25 @@ pub async fn start_topic(
|
|||||||
if let Some(repo_path) = repo_path_for_container {
|
if let Some(repo_path) = repo_path_for_container {
|
||||||
match crate::research_container::connect() {
|
match crate::research_container::connect() {
|
||||||
Ok(docker) => {
|
Ok(docker) => {
|
||||||
match crate::research_container::spawn(&docker, id, &repo_path, &state_root).await {
|
let mcp_bearer = crate::runtime_provision::mint_workspace_service_token(
|
||||||
|
&state.pool,
|
||||||
|
user.workspace_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("start_topic: mint MCP bearer failed: {e}");
|
||||||
|
e
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
match crate::research_container::spawn(
|
||||||
|
&docker,
|
||||||
|
id,
|
||||||
|
&repo_path,
|
||||||
|
&state_root,
|
||||||
|
mcp_bearer.as_deref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(spawned) => {
|
Ok(spawned) => {
|
||||||
if let Err(e) = cm_db::repo::research_topics::set_zeroclaw_container(
|
if let Err(e) = cm_db::repo::research_topics::set_zeroclaw_container(
|
||||||
&state.pool,
|
&state.pool,
|
||||||
|
|||||||
@@ -93,7 +93,25 @@ pub async fn prepare_topic_runtime(pool: &PgPool, workspace_id: Uuid, topic_id:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match crate::research_container::spawn(&docker, topic_id, &repo_path, &state_root).await {
|
let mcp_bearer = crate::runtime_provision::mint_workspace_service_token(
|
||||||
|
pool,
|
||||||
|
cm_domain::WorkspaceId::from(workspace_id),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("prepare_topic_runtime({topic_id}): mint MCP bearer failed: {e}");
|
||||||
|
e
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
match crate::research_container::spawn(
|
||||||
|
&docker,
|
||||||
|
topic_id,
|
||||||
|
&repo_path,
|
||||||
|
&state_root,
|
||||||
|
mcp_bearer.as_deref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(spawned) => {
|
Ok(spawned) => {
|
||||||
if let Err(e) = cm_db::repo::research_topics::set_zeroclaw_container(
|
if let Err(e) = cm_db::repo::research_topics::set_zeroclaw_container(
|
||||||
pool,
|
pool,
|
||||||
|
|||||||
@@ -11,8 +11,29 @@
|
|||||||
//! prompt; the claw's rich `system_prompt` remains its chat-path identity.
|
//! prompt; the claw's rich `system_prompt` remains its chat-path identity.
|
||||||
//! Injecting per-claw persona into runtime turns is a fast-follow.
|
//! Injecting per-claw persona into runtime turns is a fast-follow.
|
||||||
|
|
||||||
|
use cm_domain::WorkspaceId;
|
||||||
|
use sqlx::PgPool;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Mint a long-lived session token for the workspace owner. Used by
|
||||||
|
/// internal service callers (per-team + per-topic + per-loop ZeroClaw
|
||||||
|
/// runtimes hitting our clawmates_door MCP endpoint) without threading
|
||||||
|
/// a real user session through the runtime template.
|
||||||
|
pub async fn mint_workspace_service_token(
|
||||||
|
pool: &PgPool,
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let owner = cm_db::repo::users::owner_of_workspace(pool, workspace_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("owner_of_workspace: {e}"))?;
|
||||||
|
let auth = cm_auth::AuthService::new(pool.clone());
|
||||||
|
let token = auth
|
||||||
|
.mint_service_session(owner, time::Duration::days(30))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("mint_service_session: {e}"))?;
|
||||||
|
Ok(token.secret().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// The runtime agent alias for a claw id.
|
/// The runtime agent alias for a claw id.
|
||||||
pub fn claw_alias(claw_id: Uuid) -> String {
|
pub fn claw_alias(claw_id: Uuid) -> String {
|
||||||
format!("claw_{}", claw_id.simple())
|
format!("claw_{}", claw_id.simple())
|
||||||
|
|||||||
@@ -681,7 +681,7 @@ async fn try_team_gateway_url(
|
|||||||
// clawmates_door MCP calls pass cm-auth (the static bearer baked
|
// clawmates_door MCP calls pass cm-auth (the static bearer baked
|
||||||
// into the template config isn't a valid auth_sessions row and
|
// into the template config isn't a valid auth_sessions row and
|
||||||
// gets 401'd, leaving every agent with 0 tools).
|
// gets 401'd, leaving every agent with 0 tools).
|
||||||
let mcp_bearer = mint_workspace_service_token(pool, workspace_id)
|
let mcp_bearer = crate::runtime_provision::mint_workspace_service_token(pool, workspace_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
eprintln!("try_team_gateway_url: mint MCP bearer failed for team {team_id}: {e}");
|
eprintln!("try_team_gateway_url: mint MCP bearer failed for team {team_id}: {e}");
|
||||||
@@ -720,21 +720,3 @@ async fn try_team_gateway_url(
|
|||||||
Some(spawned.gateway_url)
|
Some(spawned.gateway_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mint a long-lived session token for the workspace owner. Used to
|
|
||||||
/// authorize internal service callers (e.g. per-team ZeroClaw runtimes
|
|
||||||
/// hitting our clawmates_door MCP endpoint) without threading a real
|
|
||||||
/// user session through the runtime template.
|
|
||||||
async fn mint_workspace_service_token(
|
|
||||||
pool: &PgPool,
|
|
||||||
workspace_id: WorkspaceId,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let owner = cm_db::repo::users::owner_of_workspace(pool, workspace_id)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("owner_of_workspace: {e}"))?;
|
|
||||||
let auth = cm_auth::AuthService::new(pool.clone());
|
|
||||||
let token = auth
|
|
||||||
.mint_service_session(owner, time::Duration::days(30))
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("mint_service_session: {e}"))?;
|
|
||||||
Ok(token.secret().to_string())
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user