BuildAgent SDKs
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). That call fails closed by construction, so nothing reaches the underlying function without a signed token: see the id-only gate below. - 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.
The id-only gate
All three adapters hold a capability id, not a signed capability token, and they all reach the sidecar through one method:
async def evaluate_tool_call(
self,
*,
capability_id: str,
tool_server: str,
tool_name: str,
parameters: dict[str, Any],
) -> NoReturn:The return type is the whole story. NoReturn means every path out of the body raises. It runs advisory evaluation for the audit receipt and the integrity check, then raises ChioDeniedError: on a hard deny from the advisory route, on a dropped observation, on HTTP 409 when advisory evaluation is disabled on the sidecar, and otherwise to report that an advisory observation is not execution authorization (sdks/python/chio-sdk-python/src/chio_sdk/client.py:463-547). The rule it enforces is in its own docstring: a capability id alone must never authorize execution.
Each adapter catches that and re-raises it as ChioToolError (chio-crewai/src/chio_crewai/tool.py:236-248, chio-llamaindex/src/chio_llamaindex/function_tool.py:259 and query_engine_tool.py:317, chio-autogen/src/chio_autogen/functions.py:252). So with the shipped ChioClient, a governed call never runs its tool body: the scope map decides which error a role gets, not whether it executes.
Every example here denies at this commit
ChioToolError from any governed call made through a capability id.The SDK ships two methods that do return. evaluate_tool_call_advisory returns a ChioReceipt for observability and carries no decision. evaluate_tool_call_mediated takes a complete signed CapabilityToken rather than an id, posts to the kernel-mediated /v1/evaluate route, and returns {"status", "receipt", "execution_nonce"} where status is authorized, deny or pending_approval (client.py:549 and :591). No adapter on this page calls either one.
Install
uv pip install chio-crewai
uv pip install chio-autogen
uv pip install chio-llamaindex
# each also works under plain pip installThe snippets below import chio_sdk directly for the client and the capability models. You do not install that separately: all three adapters declare chio-sdk-python>=0.1.0 in their own dependencies, and that distribution is what puts the chio_sdk module on the path.
| 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, so the researcher carries a search-only scope and the writer a write-only one. That is the scope contract; what the id-only client does with it is the gate above.
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 | ChioGroupChatManager.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) fires on an empty scope map, a missing scope for a role, or a delegator without a minted token.ChioAutogenConfigError(AUTOGEN_CONFIG_ERROR) fires on invalid GroupChat wiring, a missing registry, or an unregisterable agent.ChioLlamaIndexConfigError(LLAMAINDEX_CONFIG_ERROR) fires on an empty collection, a missing capability before delegation, or a missing 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