AutoGen integration

Five-minute path to wiring STACK into Microsoft AutoGen agents. AutoGen's function-calling pattern maps cleanly to STACK: every function the agent can invoke goes through the credential proxy with a passport-bound scope, and every call lands in the audit log.

Examples below use AutoGen 0.2 (PyPI package pyautogen) — the version with the AssistantAgent / UserProxyAgent / register_function pattern. Microsoft is shipping a successor under autogen-agentchat (0.4+); the STACK wiring concept is identical but the AutoGen-side imports differ.

1. Install

bash
pip install pyautogen getstack

STACK's Python SDK ships on PyPI as getstack.

2. Connect a service

Connect at least one upstream service at getstack.run/services. For this example, connect GitHub.

3. Register the agent + open a mission

bash
# One-time, in your shell:
npx -y @getstackrun/cli auth login
python
from getstack import Stack

# Step a: Sign in as developer to register the agent (one-time).
stack = Stack()  # reads ~/.stack/credentials.json
agent = stack.agents.register(
    name="autogen-assistant",
    description="GitHub triage bot",
    accountability_mode="enforced",
)
print(f"Save: {agent.id}")  # e.g. agt_xyz

# Step b: In the runtime, switch to agent-keypair mode.
agent_stack = Stack(agent_id=agent.id)

mission_cm = agent_stack.passports.mission(
    agent_id=agent.id,
    intent="Triage open issues in acme/sandbox",
    services=["github"],
    checkpoint_interval="5m",
)

mission_cm is a context manager. We keep a reference to it so the AutoGen functions defined below can capture mission in closure scope once we enter the block.

4. STACK-proxied functions

python
def make_github_functions(mission):
    """Define AutoGen-callable functions that proxy through STACK."""

    def create_github_issue(title: str, body: str) -> dict:
        """Create a GitHub issue in acme/sandbox."""
        response = mission.proxy(
            service="github",
            url="https://api.github.com/repos/acme/sandbox/issues",
            method="POST",
            body={"title": title, "body": body},
        )
        if not response.ok():
            raise RuntimeError(f"GitHub API failed: {response.status}")
        return response.body

    def list_github_issues(state: str = "open") -> list[dict]:
        """List GitHub issues in acme/sandbox."""
        response = mission.proxy(
            service="github",
            url="https://api.github.com/repos/acme/sandbox/issues",
            method="GET",
            query={"state": state},
        )
        if not response.ok():
            raise RuntimeError(f"GitHub API failed: {response.status}")
        return response.body

    return create_github_issue, list_github_issues

mission.proxy(...) attaches the passport JWT, logs the tool call into the mission's checkpoint buffer, and validates the URL. Pass full URLs — the proxy rejects relative paths.

5. Wire into AutoGen

python
from autogen import AssistantAgent, UserProxyAgent, register_function

with mission_cm as mission:
    create_github_issue, list_github_issues = make_github_functions(mission)

    llm_config = {"model": "gpt-4o-mini", "api_key": os.environ["OPENAI_API_KEY"]}

    assistant = AssistantAgent(
        name="github_helper",
        system_message=(
            "You triage GitHub issues for acme/sandbox. You can list and "
            "create issues. Your scope is bound to that one repo."
        ),
        llm_config=llm_config,
    )

    user_proxy = UserProxyAgent(
        name="user",
        human_input_mode="NEVER",
        code_execution_config=False,
    )

    register_function(
        create_github_issue,
        caller=assistant,
        executor=user_proxy,
        description="Create a GitHub issue in acme/sandbox",
    )
    register_function(
        list_github_issues,
        caller=assistant,
        executor=user_proxy,
        description="List GitHub issues in acme/sandbox",
    )

    user_proxy.initiate_chat(
        assistant,
        message="List open issues, then file one for the title-typo bug we discussed.",
    )

# Mission auto-checks-out here. Post-hoc detectors run on the trajectory.

5b. Pre-execution approval gate

Enforced-mode agents can route Intents through STACK's approval queue before executing. Submit, wait, then thread the returned approval id on the producer call. Rejection auto-revokes the passport.

python
import time

def make_gated_github_close(mission, agent_id):
    def close_github_issue(issue_number: int, reason: str) -> dict:
        result = stack.intents.submit_and_wait(
            intent={
                "type": "intent_claim",
                "intent_type": "http_call",
                "agent_id": agent_id,
                "named_intent": "github:issues:update",
                "target": "github",
                "action": "PATCH /repos/acme/sandbox/issues",
                "parameters": {
                    "url": f"https://api.github.com/repos/acme/sandbox/issues/{issue_number}",
                    "method": "PATCH",
                    "body": {"state": "closed", "state_reason": "completed"},
                },
                "estimated_cost": {"wallet_cents": 0, "tokens": None, "gas_gwei": None},
                "accountability": "enforced",
                "reason": reason,
                "requires": [],
                "user_subject": None,
                "mission_ref": None,
                "submitted_at": int(time.time() * 1000),
            },
            passport_token=mission.token,
            timeout_seconds=300,
        )
        if result["final"]["status"] != "approved":
            return {"error": f"closure blocked: {result['final']['status']}"}

        response = mission.proxy(
            service="github",
            url=f"https://api.github.com/repos/acme/sandbox/issues/{issue_number}",
            method="PATCH",
            body={"state": "closed", "state_reason": "completed"},
            approval_id=result["final"]["id"],
        )
        return response.body

    return close_github_issue

Register alongside the other functions in step 5. The gate checks the call shape against the approved Intent; mismatch returns 403 with metadata.gate_reason="call_mismatch". stack.intents.simulate(...) is the pre-check that runs without a human round-trip.

6. What happens when the agent goes off-script

If the LLM hallucinates a call to a different repo or a different provider, the proxy denies it (passport scope is bound to github service for this agent) and the credential_outside_scope detector fires. In enforced mode the passport is auto-revoked; subsequent function calls raise. The trace surfaces in the dashboard /activity feed and /audit.

7. Kill switch

If you need to revoke during the run (e.g. the LLM's output looks adversarial), call:

python
stack.passports.revoke(mission.passport.jti, reason="off-script")

Propagation is sub-60-second. Subsequent mission.proxy(...) calls fail closed.

Full SDK reference: /docs/sdk/python. For multi-agent AutoGen flows where one agent hands off to another, register each as a separate STACK agent (one passport each, scope per role) — see the CrewAI guide for the same per-agent pattern.

Why bother

  • AutoGen functions are LLM-routed; STACK puts a hard fence around what each function can actually call
  • Scope is enforced server-side — the LLM cannot fabricate its way past it
  • Detectors catch wrong-repo / overrunning action volume / drift from the original intent
  • Hash-chained audit log of every function call, exportable for compliance review
stack | docs