LangChain integration
Five-minute path to wiring STACK into a LangChain agent. Result: your agent never holds raw credentials, every tool call goes through STACK with passport-bound scope, and every action lands in the audit log.
1. Install + sign in
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.)
2. Connect a service
Before wiring code, connect at least one service to your operator at getstack.run/services. OAuth providers (Slack, GitHub, Google, etc.) only ask the user once; STACK stores the token KMS-encrypted on your behalf and the agent never sees it.
3. Register the agent (one-time)
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="enforced", # auto-revoke on detector fire
)
print(f"Save this: {agent.id}") # e.g. agt_abc123Register once and keep agent.id -- you wire it into the runtime in step 4. accountability_mode is an agent property (not the passport): standard (audit only), logged (record warnings, don't block),enforced (auto-revoke on critical detector fires).
4. Open the runtime with a per-agent keypair
# 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.
5. Route LangChain tool calls through the STACK proxy
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.
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.
import time
result = stack.intents.submit_and_wait(
intent={
"type": "intent_claim",
"intent_type": "http_call",
"agent_id": "agt_abc123",
"named_intent": "stripe:refunds:create",
"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.
6. Wire the tool into a LangChain agent
from langchain.agents import AgentExecutor, create_openai_functions_agent
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a customer-support triage bot. Use the tools provided."),
("user", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
langchain_agent = create_openai_functions_agent(llm, [post_to_slack], prompt)
executor = AgentExecutor(agent=langchain_agent, tools=[post_to_slack], verbose=True)
executor.invoke({"input": "Acknowledge ticket #4821 in #support."})Place this executor.invoke(...) call inside the with stack.passports.mission(...) block from step 4 — that's how the tool sees mission in scope.
7. Kill switch + CI fallback
If the agent goes off-script, revoke its passport. Propagation is sub-60-second across all STACK surfaces.
stack.passports.revoke(mission.passport.jti, reason="off-script")For CI deploys (GitHub Actions, Lambda, etc.) where there is no browser: set STACK_API_KEY as a secret and the SDK falls back to the legacy sk_live_* path. The agent runtime can still use Stack(agent_id=...) -- enrollment uses the env-var key as the static bearer for the one-time dance, then signs subsequent requests with the agent's local keypair.
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.
Why bother
- No raw credentials in the LangChain process — prompt injection has nothing to leak
- Every tool call audit-logged with passport id, scope, and outcome
- Sub-60-second kill switch via stack.passports.revoke(jti) when the bot misbehaves
- Detector grid catches scope drift, credential bursts, post-checkout access in real time