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
+82
View File
@@ -0,0 +1,82 @@
//! Blob storage behind the three file drives (spec §7.4). The local
//! filesystem implementation serves dev and the air-gapped target; an
//! S3-compatible implementation slots in behind the same trait for cloud.
use std::path::{Component, Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum BlobError {
#[error("blob not found")]
NotFound,
#[error("invalid blob key: {0}")]
InvalidKey(String),
#[error("storage io: {0}")]
Io(String),
}
#[async_trait::async_trait]
pub trait BlobStore: Send + Sync {
async fn put(&self, key: &str, bytes: &[u8]) -> Result<(), BlobError>;
async fn get(&self, key: &str) -> Result<Vec<u8>, BlobError>;
async fn delete(&self, key: &str) -> Result<(), BlobError>;
}
/// Filesystem-backed store rooted at a data directory.
pub struct LocalBlobStore {
root: PathBuf,
}
impl LocalBlobStore {
pub fn new(root: PathBuf) -> LocalBlobStore {
LocalBlobStore { root }
}
/// Resolves a key strictly below the root: rejects absolute paths and
/// any `..`/`.` components so keys can never escape the data dir.
fn resolve(&self, key: &str) -> Result<PathBuf, BlobError> {
let path = Path::new(key);
if path.is_absolute() || key.is_empty() {
return Err(BlobError::InvalidKey(key.to_owned()));
}
for component in path.components() {
match component {
Component::Normal(_) => {}
_ => return Err(BlobError::InvalidKey(key.to_owned())),
}
}
Ok(self.root.join(path))
}
}
#[async_trait::async_trait]
impl BlobStore for LocalBlobStore {
async fn put(&self, key: &str, bytes: &[u8]) -> Result<(), BlobError> {
let path = self.resolve(key)?;
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| BlobError::Io(e.to_string()))?;
}
tokio::fs::write(&path, bytes)
.await
.map_err(|e| BlobError::Io(e.to_string()))
}
async fn get(&self, key: &str) -> Result<Vec<u8>, BlobError> {
let path = self.resolve(key)?;
match tokio::fs::read(&path).await {
Ok(bytes) => Ok(bytes),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(BlobError::NotFound),
Err(e) => Err(BlobError::Io(e.to_string())),
}
}
async fn delete(&self, key: &str) -> Result<(), BlobError> {
let path = self.resolve(key)?;
match tokio::fs::remove_file(&path).await {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(BlobError::NotFound),
Err(e) => Err(BlobError::Io(e.to_string())),
}
}
}