Wrap STACK proxy calls as LangChain tools. The provider credential stays in STACK. The tool call carries Passport scope and produces an audit entry.
pip install langchain langchain-openai getstack
npx -y @getstackrun/cli auth loginSTACK's Python SDK ships on PyPI as getstack. The CLI's auth login opens a browser, runs the OAuth Device flow, and stores a refresh token at ~/.stack/credentials.json. The Python SDK reads it automatically -- no STACK_API_KEY env var needed for local dev. (CI without a browser keeps the env-var path -- see step 7.)
Before wiring code, connect at least one service to your operator at getstack.run/services. Complete provider OAuth in Connect. STACK stores the resulting token KMS-encrypted; the agent does not receive it.
from getstack import Stack
stack = Stack() # reads ~/.stack/credentials.json automatically
agent = stack.agents.register(
name="my-langchain-agent",
description="Customer-support triage bot",
accountability_mode="logged", # full audit + detectors, no per-call gate
key_mode="customer_managed", # runtime authenticates as this agent
)
print(f"Save this: {agent.id}") # e.g. agt_abc123
# Authorize the agent for the service you connected in step 2.
# The grant is the authorization -- a mission can only narrow it,
# never substitute for it.
conn = next(c for c in stack.services.list() if c["provider"] == "slack")
stack.services.grant_agent_access(
agent_id=agent.id,
service_connection_id=conn["id"],
scopes=conn["scopes"], # or a narrower subset
)Register once and keep agent.id; you wire it into the runtime in step 4. Customer-managed mode is required for Stack(agent_id=...), because that client enrolls and uses a local agent keypair. Without the grant, issuing a passport that declares slack fails with a 403. accountability_mode is an agent property (not the passport): standard (audit only), logged (checkpoints + detectors, critical flags surface at review), enforced (strictest: every proxied call additionally requires a pre-approved Intent -- see 5b).
# In your agent's runtime code (the script LangChain calls into):
from getstack import Stack
# agent_id binds this Stack instance to the agent's local Ed25519
# keypair. First run generates the keypair + enrolls the public half;
# subsequent runs sign every API call with a fresh 60-second JWT.
# Your operator key never touches this process.
stack = Stack(agent_id="agt_abc123")
with stack.passports.mission(
agent_id="agt_abc123",
intent="Triage and acknowledge new support tickets",
services=["slack"],
checkpoint_interval="5m",
) as mission:
# mission.token is the passport JWT for proxied calls
# mission.log(...) records each tool action
# checkpoints fire automatically every 5m
# checkout fires automatically when the block exits
...The context manager owns the passport lifecycle so your code never manages JTIs by hand. If the block raises, checkout still fires with the failure reason as the summary.
LangChain tools that hit external APIs should call the STACK proxy instead of the upstream directly. The proxy injects the credential server-side; your agent process never sees the token.
from langchain_core.tools import tool
@tool
def post_to_slack(channel: str, text: str) -> dict:
"""Post a message to a Slack channel via STACK's credential proxy."""
response = mission.proxy(
service="slack",
url="https://slack.com/api/chat.postMessage",
method="POST",
body={"channel": channel, "text": text},
)
return response.bodymission.proxy() auto-attaches the passport JWT, logs the tool call into the mission's checkpoint buffer, and returns a ProxyResponse with .status, .headers, and .body. Pass full URLs — the proxy validates them and rejects relative paths.
For a durable non-GET job, pass on_operation_id to persist the generated operation ID before network I/O. Reuse it only for the unchanged request and call stack.proxy.get_operation() after response loss. See Proxy operation recovery.
Register the agent with accountability_mode="enforced" and this gate becomes mandatory: the proxy refuses any call that does not carry an approval id. Submit the Intent, wait for the operator's approval, then thread the returned id on the producer call. Rejection auto-revokes the passport.
import time
result = stack.intents.submit_and_wait(
intent={
"type": "intent_claim",
"intent_type": "http_call",
"agent_id": "agt_abc123",
"named_intent": "stripe.create_refund",
"target": "stripe",
"action": "POST /v1/refunds",
"parameters": {
"url": "https://api.stripe.com/v1/refunds",
"method": "POST",
"body": {"charge": "ch_abc", "amount": 4200},
},
"estimated_cost": {"wallet_cents": 0, "tokens": None, "gas_gwei": None},
"accountability": "enforced",
"reason": "Customer requested refund on ticket #4821",
"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":
raise RuntimeError(f"Refund blocked: {result['final']['status']}")
mission.proxy(
service="stripe",
url="https://api.stripe.com/v1/refunds",
method="POST",
body={"charge": "ch_abc", "amount": 4200},
approval_id=result["final"]["id"],
)The gate checks the call shape against the approved Intent (service, method, URL host, body). Mismatch returns 403 with metadata.gate_reason="call_mismatch". stack.intents.simulate(...) is the pre-check that runs without a human round-trip.
from langchain.agents import create_agent
langchain_agent = create_agent(
model="openai:gpt-4o-mini",
tools=[post_to_slack],
system_prompt="You are a customer-support triage agent. Use the approved tools.",
)
langchain_agent.invoke({
"messages": [{
"role": "user",
"content": "Acknowledge ticket #4821 in #support.",
}]
})Place this langchain_agent.invoke(...) call inside the with stack.passports.mission(...) block from step 4 — that's how the tool sees mission in scope.
If the agent goes off-script, revoke its passport. Propagation is enforced on the next STACK-verified call.
stack.passports.revoke(mission.passport.jti, reason="off-script")For a first headless deployment, set STACK_API_KEY in the deployment's secret store. It is an operator-wide credential and is used only to authorize the one-time enrollment; after that, Stack(agent_id=...) signs requests with the agent's local keypair. Remove the operator key from the runtime once enrollment has completed.
Full SDK reference: /docs/sdk/python. Underlying proxy contract: /docs/api/proxy. Tracking the run: /docs/concepts/audit. Auth model: /docs/security/stack-auth + /docs/security/agent-keys.