P3 backend: files, skills, routines, claw chat with LIVE taint plumbing

- tc-files: BlobStore trait + LocalBlobStore (traversal-proof keys); wired
  through Runtime (config storage.data_dir in deployments)
- File tools: files.write/files.list (workspace-internal) + files.delete
  (gated FileDeletion — tested: file survives pending, gone after approve);
  GET /api/openclaw/files + /api/shared-drive/files (drive/agent scoped)
- Skills: catalog/library + idempotent install with counter, uninstall;
  GET /api/skills[?clawId=], POST install/uninstall
- tc-scheduler: croner cron math (clock-controlled tests), SKIP LOCKED
  claim-and-advance firing REAL runs into dedicated ' name' sessions
  (reused, exactly-once), paused routines skipped; routines API + agent
  tool routine.schedule; loop spawned in server
- Claw chat: 1:1 threads, chat.send enforcing the target's Other-Claws
  policy, chat.inbox whose output carries inter_agent taint; the run loop
  now ACCUMULATES taint from tool outputs into LoopState, classifies with
  it, and stamps steps + approvals — a poisoned inbox followed by
  email.send produces an approval whose taint_sources says inter_agent
- ScriptedProvider scenario selection now keys on the most recent marker
  (session history kept earlier markers alive)

132 Rust tests green.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-10 05:28:50 -05:00
co-authored by Claude Fable 5
parent ea5162ac65
commit 67f918439c
69 changed files with 3651 additions and 183 deletions
+116
View File
@@ -0,0 +1,116 @@
//! Built-in tools. Each tool declares its effects (spec §15); the gate
//! policy decides from those declarations whether human approval is
//! required before execution. Tool outputs may declare a taint source —
//! untrusted content the run loop tracks (§15 untrusted-by-default).
mod chat;
mod clock;
mod email;
mod files;
mod routine;
use std::collections::HashMap;
use std::sync::Arc;
use serde_json::Value;
use sqlx::PgPool;
use tc_domain::{AgentId, WorkspaceId};
use tc_files::BlobStore;
use tc_llm::ToolDescriptor;
use tc_tools::{Effect, TaintSource};
pub use chat::{ChatInbox, ChatSend};
pub use clock::ClockNow;
pub use email::EmailSend;
pub use files::{FilesDelete, FilesList, FilesWrite};
pub use routine::RoutineSchedule;
/// Execution context handed to tools: who is acting, for which tenant.
#[derive(Clone)]
pub struct ToolContext {
pub pool: PgPool,
pub workspace_id: WorkspaceId,
pub agent_id: AgentId,
pub blob: Arc<dyn BlobStore>,
}
#[async_trait::async_trait]
pub trait Tool: Send + Sync {
fn descriptor(&self) -> ToolDescriptor;
/// Declared effects; the gate policy classifies from these.
fn effects(&self) -> &'static [Effect];
/// Taint carried by this tool's output, if any (e.g. inter-agent
/// messages, web content). `None` for trusted workspace data.
fn output_taint(&self) -> Option<TaintSource> {
None
}
/// The exact human-facing preview for approval cards (§10).
fn preview(&self, input: &Value) -> Value {
input.clone()
}
async fn execute(&self, ctx: &ToolContext, input: Value) -> Result<Value, String>;
}
pub struct ToolRegistry {
tools: HashMap<String, Arc<dyn Tool>>,
}
impl Default for ToolRegistry {
fn default() -> Self {
let mut registry = ToolRegistry {
tools: HashMap::new(),
};
registry.register(Arc::new(ClockNow));
registry.register(Arc::new(EmailSend));
registry.register(Arc::new(FilesWrite));
registry.register(Arc::new(FilesList));
registry.register(Arc::new(FilesDelete));
registry.register(Arc::new(RoutineSchedule));
registry.register(Arc::new(ChatSend));
registry.register(Arc::new(ChatInbox));
registry
}
}
impl ToolRegistry {
pub fn register(&mut self, tool: Arc<dyn Tool>) {
self.tools.insert(tool.descriptor().name, tool);
}
pub fn descriptors(&self) -> Vec<ToolDescriptor> {
let mut all: Vec<ToolDescriptor> = self.tools.values().map(|t| t.descriptor()).collect();
all.sort_by(|a, b| a.name.cmp(&b.name));
all
}
/// Declared effects of a tool; unknown tools have none (they cannot
/// execute anything — `execute` fails for them).
pub fn effects_of(&self, name: &str) -> &'static [Effect] {
self.tools.get(name).map(|t| t.effects()).unwrap_or(&[])
}
/// Taint the tool's output carries, if any.
pub fn output_taint_of(&self, name: &str) -> Option<TaintSource> {
self.tools.get(name).and_then(|t| t.output_taint())
}
/// The approval-card preview for a tool input.
pub fn preview_of(&self, name: &str, input: &Value) -> Value {
self.tools
.get(name)
.map(|t| t.preview(input))
.unwrap_or_else(|| input.clone())
}
pub async fn execute(
&self,
ctx: &ToolContext,
name: &str,
input: Value,
) -> Result<Value, String> {
match self.tools.get(name) {
Some(tool) => tool.execute(ctx, input).await,
None => Err(format!("unknown tool: {name}")),
}
}
}