Skip to content

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.

#[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.

CrateActions
flow-adapter-shellrun-command, git, npm, pnpm, cargo, kubectl, curl. Output is streamed via NodeLog.
flow-adapter-fsread-file, write-file, edit-file, delete-file, glob, grep, list-dir. Every path is confined to the workspace.
flow-adapter-cliOne vendor-neutral cli-tool action that runs any CLI named by the node’s bin, driven by a catalog commandTree.
flow-adapter-utilityLog, set-variable, email-style utilities.

Placeholder slots (ssh, zosmf, mock) register as MockAdapters. They run but return synthetic payloads.

  • Filesystem: flow_security::confine_path jails every fs path to the node’s workspaceRoot. 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 a sandbox-exec profile, Linux falls back to the rails, and Windows is audit-only. Every invocation writes one JSON line to the shell audit log.

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.

  1. New crate flow-adapter-<name> in the workspace, implementing Adapter (+ integration tests in tests/).
  2. Register it in FlowApp::new.
  3. Describe its actions in the descriptor. The DSL spec, validation, and the AI tool loop pick them up from there. Keep names vendor-neutral.