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:
co-authored by
Claude Fable 5
parent
ea5162ac65
commit
67f918439c
@@ -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}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user