BuildAgent SDKsnew
Agent-Framework SDKs
Wrap CrewAI, AutoGen, or LlamaIndex tools with sidecar authorization and signed receipts.
Prerequisites
http://127.0.0.1:9090 by default. All three packages depend on chio-sdk-python and chio-adapter-base, and require Python 3.11 or newer. See Capabilities for the capability-token model used by these adapters and Receipts for the receipt produced for each governed call.Shared model
The three adapters wrap different framework objects. Their governance mechanism is identical, and all three build on the same chio-sdk-python types: ChioScope, ToolGrant, CapabilityToken, and the async ChioClient.
- Intercept. The wrapper holds a
capability_idand aserver_id. Before the tool body runs, it callsChioClient.evaluate_tool_call(capability_id, tool_server, tool_name, parameters)and inspects the returnedChioReceipt. A non-allow receipt raisesChioToolErrorbefore the underlying function executes. - Per-role scoping. A crew, group chat, or agent runner accepts a mapping from role to
ChioScope. Each role is receives its ownCapabilityTokenviaChioClient.create_capability, so a researcher scoped tosearchcannot invokewriteeven if the model hallucinates the call. - Delegation attenuation. Agent-to-agent handoff mints a child token through
ChioClient.attenuate_capability. The child scope ischild ⊆ parent; broadening scope raisesChioValidationError. - Redaction. Arguments are run through
chio_adapter_base.redact.redact_argswithRedactionPolicy.chio_default()before parameters cross into the sidecar, so secret-bearing fields do not enter the receipt log. The underlying executor still receives the original, unredacted arguments.
Install
uv pip install chio-crewai
uv pip install chio-autogen
uv pip install chio-llamaindex
# each also works under plain pip install| Package | Wraps | Framework pin |
|---|---|---|
chio-crewai | crewai.tools.BaseTool, crewai.Crew | crewai>=0.80,<1 |
chio-autogen | register_function, GroupChat | pyautogen>=0.2,<0.3 |
chio-llamaindex | FunctionTool, QueryEngineTool | llama-index-core>=0.11,<1 |
AutoGen version line
chio-autogen targets the classic pyautogen 0.2.x line because it exposes the stable ConversableAgent / GroupChat / register_function API the adapter intercepts. The newer autogen-agentchat 0.4+ redesign pivots to an async actor model without a drop-in GroupChat.CrewAI
ChioBaseTool is a crewai.tools.BaseTool subclass whose _run is gated on an allow verdict. Supply an executor callable to wrap an existing function, or subclass and override _execute. ChioCrew is a crewai.Crew subclass that takes a per-role capability_scope mapping and mints a scoped token for each role.
import asyncio
from chio_crewai import ChioBaseTool, ChioCrew
from chio_sdk.client import ChioClient
from chio_sdk.models import ChioScope, Operation, ToolGrant
from crewai import Agent, Task
def grant(name: str) -> ToolGrant:
return ToolGrant(server_id="tools-srv", tool_name=name, operations=[Operation.INVOKE])
search_tool = ChioBaseTool(
name="search",
description="Search the web",
server_id="tools-srv",
executor=lambda q: {"results": [f"hit for {q!r}"]},
)
write_tool = ChioBaseTool(
name="write",
description="Write a file",
server_id="tools-srv",
executor=lambda path, content: {"ok": True, "path": path},
)
researcher = Agent(role="researcher", goal="Find facts", backstory="Careful.",
tools=[search_tool, write_tool])
writer = Agent(role="writer", goal="Write prose", backstory="Terse.",
tools=[search_tool, write_tool])
task = Task(description="Research then write.", expected_output="A brief.", agent=researcher)
async def main() -> None:
async with ChioClient("http://127.0.0.1:9090") as chio:
crew = ChioCrew(
capability_scope={
"researcher": ChioScope(grants=[grant("search")]),
"writer": ChioScope(grants=[grant("write")]),
},
chio_client=chio,
agents=[researcher, writer],
tasks=[task],
)
await crew.provision_capabilities()
print(crew.kickoff())
asyncio.run(main())provision_capabilities() mints one token per role and rewrites each agent's ChioBaseTool instances in place. The researcher can call search but any attempt to call write is denied; the writer has the inverse scope.
Delegation attenuation
attenuate_for_delegation mints a narrower child token when one role hands off to another and rebinds the delegate's tools to it.
child = await crew.attenuate_for_delegation(
delegator_role="writer",
delegate_role="editor",
new_scope=ChioScope(grants=[grant("write")]), # strict subset of writer
)AutoGen
ChioFunctionRegistry wraps each function registered on an agent's function_map with a Chio allow gate, preserving the sync/async contract AutoGen dispatches on. ChioGroupChat and ChioGroupChatManager subclass AutoGen's GroupChat / GroupChatManager and carry the per-role scope. Enforcement lives in the registry; the manager mints tokens and rebinds registries when a role changes.
from autogen import ConversableAgent
from chio_autogen import (
ChioFunctionRegistry, ChioGroupChat, ChioGroupChatManager, attach_registry,
)
from chio_sdk.models import ChioScope, Operation, ToolGrant
def grant(name: str) -> ToolGrant:
return ToolGrant(server_id="tools-srv", tool_name=name, operations=[Operation.INVOKE])
researcher = ConversableAgent(name="researcher", llm_config=False)
writer = ConversableAgent(name="writer", llm_config=False)
r_registry = ChioFunctionRegistry(agent=researcher, chio_client=chio, server_id="tools-srv")
@r_registry.as_decorator()
def search(query: str) -> str:
"""Search the web."""
return f"hits for {query!r}"
attach_registry(researcher, r_registry)
# ... a matching writer registry registers a write() function ...
groupchat = ChioGroupChat(
capability_scope={
"researcher": ChioScope(grants=[grant("search")]),
"writer": ChioScope(grants=[grant("write")]),
},
agents=[researcher, writer],
messages=[],
max_round=6,
)
manager = ChioGroupChatManager(groupchat=groupchat, chio_client=chio, llm_config=False)
await manager.provision_capabilities()The manager also exposes ensure_function_in_scope(role, function_name), a local check that refuses a cross-role dispatch before the sidecar is even reached. ChioGroupChat derives an agent's role from its name by default; pass role_key="role" to match CrewAI-style labels.
Nested chat attenuation
AutoGen supports nested chats where an agent spawns a sub-conversation. register_nested_chats_with_attenuation mints an attenuated token, rebinds each child recipient's registry to it, then delegates to AutoGen's native register_nested_chats.
from chio_autogen import register_nested_chats_with_attenuation
child_token = await register_nested_chats_with_attenuation(
parent_agent=researcher,
child_configs=[{"recipient": editor, "message": "handoff", "max_turns": 2}],
parent_capability=manager.token_for("researcher"),
child_scope=ChioScope(grants=[grant("search")]), # strict subset
chio_client=chio,
)For an initiate_chats queue instead of agent-level nesting, use attenuate_for_initiate_chats, which rebinds each recipient and sender in the queue to the child token.
LlamaIndex
ChioFunctionTool subclasses FunctionTool and gates call / acall. ChioQueryEngineTool subclasses QueryEngineTool and adds a vector-collection scope that LlamaIndex itself has no notion of. ChioAgentRunner mints one token for the agent and binds it to each governed tool on the runner.
from chio_llamaindex import ChioAgentRunner, ChioFunctionTool, ChioQueryEngineTool
from chio_sdk.models import ChioScope, Constraint, Operation, ToolGrant
def search_grant() -> ToolGrant:
return ToolGrant(server_id="tools-srv", tool_name="search_documents",
operations=[Operation.INVOKE])
def rag_grant() -> ToolGrant:
return ToolGrant(
server_id="rag-srv",
tool_name="query_prod-docs",
operations=[Operation.INVOKE],
constraints=[Constraint(type="memory_store_allowlist", value="prod-docs")],
)
search_tool = ChioFunctionTool(
fn=search_documents,
name="search_documents",
description="Search the document index",
server_id="tools-srv",
)
rag_tool = ChioQueryEngineTool(
query_engine=index.as_query_engine(),
collection="prod-docs",
server_id="rag-srv",
)
chio_runner = ChioAgentRunner(
runner=runner,
capability_scope=ChioScope(grants=[search_grant(), rag_grant()]),
chio_client=chio,
agent_name="analyst",
)
await chio_runner.provision_capability()
response = runner.chat("Summarise the Q4 filings.")Collection scoping
ChioQueryEngineTool is bound to a single collection and enforces two checks for each query:
- Client-side allowlist. The tool reads
Constraint(type="memory_store_allowlist")entries from the capability scope and allows the call when the bound collection is in the set. This runs before the retriever or the sidecar. When an allowlist is configured but the collection is absent, it fails closed with aChioToolErrorwhose guard isCollectionScopeGuard. - Sidecar evaluation. The collection and query string are forwarded to the sidecar under
parameters={"query": ..., "collection": ...}, so kernel policy can veto on any field independent of the client-side check.
Pass allowed_collections=[...] as a shortcut when you do not want to thread a full ChioScope onto the tool.
tool = ChioQueryEngineTool(
query_engine=engine,
collection="prod-docs",
allowed_collections=["prod-docs", "qa-docs"],
capability_id="cap-analyst",
server_id="rag-srv",
)
# Narrow to a child capability for a nested or helper agent.
child = await chio_runner.attenuate(new_scope=ChioScope(grants=[search_grant()]))Both tool types accept raise_on_deny=False to return an error ToolOutput instead of raising, which some LlamaIndex planners prefer to feed back to the model.
Adapter Comparison
| Adapter | Wrapper types | Scoping unit | Delegation entry point |
|---|---|---|---|
| CrewAI | ChioBaseTool, ChioCrew | agent role | ChioCrew.attenuate_for_delegation |
| AutoGen | ChioFunctionRegistry, ChioGroupChat | agent name / role | attenuate_for_handoff, register_nested_chats_with_attenuation |
| LlamaIndex | ChioFunctionTool, ChioQueryEngineTool, ChioAgentRunner | whole agent runner | ChioAgentRunner.attenuate |
Error types
All adapters raise the same denial type, ChioToolError (error code TOOL_DENIED), which carries the sidecar verdict: tool_name, server_id, guard, reason, and receipt_id. Call to_dict() for a JSON-serializable view. Each package also defines its own configuration error, raised before any tool is dispatched:
ChioCrewConfigError(CREW_CONFIG_ERROR) — empty scope map, missing scope for a role, delegator without a minted token.ChioAutogenConfigError(AUTOGEN_CONFIG_ERROR) — invalid GroupChat wiring, missing registry, unregisterable agent.ChioLlamaIndexConfigError(LLAMAINDEX_CONFIG_ERROR) — empty collection, missing capability before delegation, no runner.
Next Steps
- LangChain and Provider SDKs · the hosted-edge variant for LangChain, Anthropic, and OpenAI clients
- Delegate Between Agents · the attenuation model these adapters build on
- Capabilities · scopes, grants, and the
child ⊆ parentinvariant - Receipts · signed output from governed calls