Chio/Docs

BuildAgent SDKsnew

Agent-Framework SDKs

Wrap CrewAI, AutoGen, or LlamaIndex tools with sidecar authorization and signed receipts.

Prerequisites

A running Chio sidecar. Each adapter talks to it over the SDK client at 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_id and a server_id. Before the tool body runs, it calls ChioClient.evaluate_tool_call(capability_id, tool_server, tool_name, parameters) and inspects the returned ChioReceipt. A non-allow receipt raises ChioToolError before 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 own CapabilityToken via ChioClient.create_capability, so a researcher scoped to search cannot invoke write even if the model hallucinates the call.
  • Delegation attenuation. Agent-to-agent handoff mints a child token through ChioClient.attenuate_capability. The child scope is child ⊆ parent; broadening scope raises ChioValidationError.
  • Redaction. Arguments are run through chio_adapter_base.redact.redact_args with RedactionPolicy.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

terminal
uv pip install chio-crewai
uv pip install chio-autogen
uv pip install chio-llamaindex
# each also works under plain pip install
PackageWrapsFramework pin
chio-crewaicrewai.tools.BaseTool, crewai.Crewcrewai>=0.80,<1
chio-autogenregister_function, GroupChatpyautogen>=0.2,<0.3
chio-llamaindexFunctionTool, QueryEngineToolllama-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.

crew.py
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.

crew.py
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.

group_chat.py
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.

group_chat.py
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.

agent.py
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:

  1. 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 a ChioToolError whose guard is CollectionScopeGuard.
  2. 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.

agent.py
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

AdapterWrapper typesScoping unitDelegation entry point
CrewAIChioBaseTool, ChioCrewagent roleChioCrew.attenuate_for_delegation
AutoGenChioFunctionRegistry, ChioGroupChatagent name / roleattenuate_for_handoff, register_nested_chats_with_attenuation
LlamaIndexChioFunctionTool, ChioQueryEngineTool, ChioAgentRunnerwhole agent runnerChioAgentRunner.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