Adapters
Adapters are how nodes touch the world. Each adapter crate follows the same
layout (src/ + tests/) and registers into the engine’s AdapterRegistry
by name.
The Adapter trait (flow-execution)
Section titled “The Adapter trait (flow-execution)”#[async_trait]pub trait Adapter: Send + Sync { fn name(&self) -> &str; async fn execute(&self, node: &FlowNode) -> Result<NodeOutput, AdapterError>;
async fn execute_with_events( &self, node: &FlowNode, ctx: &AdapterCtx, ) -> Result<NodeOutput, AdapterError> { self.execute(node).await }
fn descriptor(&self) -> AdapterDescriptor; // actions + field schemas}
pub struct NodeOutput { pub status: String, pub payload: serde_json::Value }name() matches node.data.adapter. The executor looks the adapter up by
name in the registry. execute_with_events lets streaming adapters emit
live log events, while non-streaming adapters keep the basic API. The
descriptor() feeds the DSL spec generator, so adapter reference pages are
generated from the live action surface. It also serves as the model-facing
tool schema source. Every action becomes a callable tool in the AI tool
loop, so an adapter written once serves both flow nodes and agent turns.
Shipped adapters
Section titled “Shipped adapters”| Crate | Actions |
|---|---|
flow-adapter-shell | run-command, git, npm, pnpm, cargo, kubectl, curl. Output is streamed via NodeLog. |
flow-adapter-fs | read-file, write-file, edit-file, delete-file, glob, grep, list-dir. Every path is confined to the workspace. |
flow-adapter-cli | One vendor-neutral cli-tool action that runs any CLI named by the node’s bin, driven by a catalog commandTree. |
flow-adapter-utility | Log, set-variable, email-style utilities. |
Placeholder slots (ssh, zosmf, mock) register as MockAdapters. They
run but return synthetic payloads.
Sandboxing
Section titled “Sandboxing”- Filesystem:
flow_security::confine_pathjails every fs path to the node’sworkspaceRoot. Symlink and..escapes are rejected. - Shell: the always-on lightweight rails are a pinned cwd, an environment
allow-list, an output cap, and a timeout. On top of those, each node can
opt in to an OS sandbox through its
capabilities. macOS uses asandbox-execprofile, Linux falls back to the rails, and Windows is audit-only. Every invocation writes one JSON line to the shell audit log.
AI providers (flow-adapter-ai)
Section titled “AI providers (flow-adapter-ai)”This is not an Adapter. It is a provider registry behind one request type:
pub trait CloudAiProvider { // "local" llama-server + cloud providers async fn invoke(&self, req: &CloudAiRequest) -> Result<CloudAiResponse, CloudAiError>; async fn invoke_stream(/* token streaming */); async fn invoke_tools(&self, req, tools: &[ToolSpec], dispatcher: &dyn ToolDispatcher, max_iters: usize) -> Result<CloudAiResponse, CloudAiError>; async fn embed(&self, req: &CloudAiRequest) -> Result<EmbeddingResponse, CloudAiError>;}The registry dispatches per request by capability, covering the reasoning
toggle, vision, the tool loop, embeddings, and classification. The local
provider talks to the managed llama-server over an OpenAI-compatible API,
and reasoning maps to the server’s thinking toggle.
Adding a provider is a contained change. You implement CloudAiProvider, add
response-parsing tests, and register it. Nothing else in the platform changes.
Writing a new adapter
Section titled “Writing a new adapter”- New crate
flow-adapter-<name>in the workspace, implementingAdapter(+ integration tests intests/). - Register it in
FlowApp::new. - Describe its actions in the descriptor. The DSL spec, validation, and the AI tool loop pick them up from there. Keep names vendor-neutral.