Integrations AutoGen
autogen v0.4+

Resolve vault credentials inside an AutoGen tool function.

Microsoft AutoGen v0.4 (autogen-agentchat) defines tools as plain async Python functions passed directly to AssistantAgent. The deniable pattern resolves the credential inside the tool boundary, makes the privileged HTTPS call, and returns only a narrowed DTO to the agent. The key never enters the message history the model sees.

Jump to code ↓

Prerequisites

Python: AssistantAgent + async tool function

Show code — ~32 lines, python
# pip install autogen-agentchat autogen-ext[openai] deny-sh requests
import asyncio, os, requests, json
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
from deny_sh import vault_get

async def get_invoice(id: str) -> str:
    """Look up a Stripe invoice by id and return status and amount due."""
    # resolve the credential INSIDE the tool boundary
    stripe_key = vault_get("stripe-prod", os.environ["VAULT_PW"])
    r = requests.get(
        f"https://api.stripe.com/v1/invoices/{id}",
        headers={"Authorization": f"Bearer {stripe_key}"},
    )
    body = r.json()
    # narrowed DTO: never return raw upstream response bodies
    return json.dumps({
        "id": body.get("id"),
        "amount_due": body.get("amount_due"),
        "status": body.get("status"),
    })

model_client = OpenAIChatCompletionClient(model="gpt-4o")

agent = AssistantAgent(
    name="billing_agent",
    model_client=model_client,
    tools=[get_invoice],
    system_message="You are a billing assistant. Use tools to look up invoices.",
)

async def main():
    result = await agent.run(task="Look up invoice in_1Abc123XYZ")
    print(result.messages[-1].content)

asyncio.run(main())

Why this is the deniable shape

The Stripe key is resolved and consumed entirely inside get_invoice's scope. AutoGen's AssistantAgent infers the tool schema from the function signature and docstring — it sees the input args (id: str) and the narrowed return string. The OpenAIChatCompletionClient sees the same. The key never crosses any wire that isn't deny.sh → your server → Stripe. The narrowed DTO prevents leaking raw upstream response headers or error stacks into the model's message history.

Streaming variant with run_stream

For long-running agentic tasks, swap agent.run() for agent.run_stream() and consume the async iterator. The deniable shape is identical — credential resolution stays inside the tool boundary regardless of streaming mode.

Show code — ~16 lines, python
from autogen_agentchat.base import TaskResult
from autogen_agentchat.messages import AgentEvent, ChatMessage

async def main_stream():
    async for event in agent.run_stream(task="Look up invoice in_1Abc123XYZ"):
        if isinstance(event, (AgentEvent, ChatMessage)):
            print(f"[{event.source}] {event.content}", flush=True)
        elif isinstance(event, TaskResult):
            print(f"\nFinal: {event.messages[-1].content}")
            break

asyncio.run(main_stream())

Multi-tenant: factory pattern

For per-tenant agent platforms, build a factory that captures each tenant's vault password and deny.sh API key in a closure. Construct a fresh AssistantAgent per request so tool functions are scoped to that tenant only — no shared credential fallback is possible.

Show code — ~22 lines, python
def build_agent_for_tenant(tenant_pw: str, tenant_api_key: str) -> AssistantAgent:
    async def get_invoice(id: str) -> str:
        """Look up a Stripe invoice for this tenant by id."""
        # tenant_pw and tenant_api_key captured in closure — never shared
        stripe_key = vault_get(
            "stripe-prod",
            tenant_pw,
            api_key=tenant_api_key,
        )
        r = requests.get(
            f"https://api.stripe.com/v1/invoices/{id}",
            headers={"Authorization": f"Bearer {stripe_key}"},
        )
        body = r.json()
        return json.dumps({
            "id": body.get("id"),
            "amount_due": body.get("amount_due"),
            "status": body.get("status"),
        })

    return AssistantAgent(
        name="billing_agent",
        model_client=OpenAIChatCompletionClient(model="gpt-4o"),
        tools=[get_invoice],
        system_message="You are a billing assistant.",
    )

# per-request: resolve tenant credentials from your own store, then build
agent = build_agent_for_tenant(tenant_pw=pw, tenant_api_key=key)
result = await agent.run(task="Look up invoice in_1Abc123XYZ")

Each tenant's vault entries are cryptographically isolated. A compromise of one tenant's vault password exposes only that tenant's labels — not any other tenant's credentials or the platform operator's own keys.

Troubleshooting

The first five errors a launch-day developer hits when copy-pasting the snippets above. Click to expand the fix.

ModuleNotFoundError: No module named 'autogen_agentchat'

You are importing from the old v0.2 package (import autogen / from autogen import ConversableAgent), which installs as pyautogen. AutoGen v0.4 restructured into separate packages and the top-level autogen namespace no longer ships AssistantAgent or the new agent-chat primitives.

Fix: pip install autogen-agentchat autogen-ext[openai]. Update all imports to from autogen_agentchat.agents import AssistantAgent and from autogen_ext.models.openai import OpenAIChatCompletionClient. Remove any pyautogen or old autogen pins from your requirements file to avoid conflicts.

ModuleNotFoundError: No module named 'autogen_ext'

autogen-agentchat was installed but the model-client extension package was not. OpenAIChatCompletionClient lives in autogen-ext, which is a separate install with an optional dependency group for OpenAI.

Fix: pip install "autogen-ext[openai]". The [openai] extra pulls in openai>=1.3 and the required async transport. If you need Anthropic or Azure instead, use [anthropic] or [azure] respectively.

Agent never calls the tool — loops on text replies instead

AssistantAgent infers the tool call schema from the function's type hints and docstring. If either is missing, AutoGen cannot generate a valid JSON schema for the tool and the model never receives a usable tool definition — so it answers in plain text.

Fix: ensure every tool function has (a) typed parameters (id: str), (b) a typed return annotation (-> str), and (c) a non-empty docstring. The docstring becomes the tool's description field; vague or absent descriptions cause the model to ignore the tool even when it is registered.

KeyError: 'VAULT_PW' or KeyError: 'DENY_API_KEY'

The tool function reads os.environ["VAULT_PW"] at call time. If the environment variable is not set in the process running the agent, the lookup raises KeyError the first time the model invokes the tool.

Fix: set VAULT_PW and DENY_API_KEY in your server environment (Fly secrets, Render env vars, a .env loaded with python-dotenv before agent construction). Never pass them as part of system_message or task — that puts the value inside the LLM context window and defeats the deniable shape.

vault_get error: label_not_found when the vault entry exists

A wrong vault password returns label_not_found, not invalid_password. That is deliberate: the wire shape is identical so an attacker cannot enumerate which labels exist by probing passwords. If you are in a multi-tenant setup, the most common cause is passing the platform operator's vault password instead of the tenant-specific password captured in the closure.

Fix: confirm the password passed to vault_get is the same one used in vault_store for that label. If the password has been rotated, all labels stored under the old password must be re-stored under the new one — old ciphertexts will not decrypt with a new password regardless of the label name matching.

Production checklist