Chio/Docs
LOGIN · JOIN

BuildAgent SDKs

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). 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 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.

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:

sdks/python/chio-sdk-python/src/chio_sdk/client.py463-470python
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

Every example on this page provisions scopes and wires the adapters correctly, and every symbol in them is real. What none of them does at this commit is reach an allow. Treat this page's allow paths as the wiring contract rather than as observed behaviour, and expect a 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

terminalbash
uv pip install chio-crewai
uv pip install chio-autogen
uv pip install chio-llamaindex
# each also works under plain pip install

The 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.

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.

crewai_example.pypython
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.

crewai_example.pypython
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.

autogen_example.pypython
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.

autogen_example.pypython
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.

llamaindex_example.pypython
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.

llamaindex_example.pypython
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 / roleChioGroupChatManager.attenuate_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) 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