The package provides synchronous and asynchronous clients. It requires Python 3.9+ and uses httpx for HTTP requests.
pip install getstackSign in once on your development machine:
npx @getstackrun/cli auth loginfrom getstack import Stack
stack = Stack()
agents = stack.agents.list()
# Open a Mission for one agent and issue its Passport in one call.
started = stack.missions.start(
agent_id="agt_support",
mission={"intent_summary": "Post the daily support update", "intent_services": ["slack"]},
)
token = started["passport"]["token"]
# Completing the Mission revokes its Passports.
stack.missions.complete(started["mission_id"])Stack() creates an API client and can use the local OAuth profile written by the CLI. A fleet worker uses Stack(agent_id=agent_id) to sign requests with its locally stored key. AsyncStack exposes the same methods with await.
Connect the service first and name an enrolled provisioner in the policy. wait_for_approval opens the returned approval URL and waits for the operator's decision. create_worker returns Stack(agent_id=agent_id), or AsyncStack for the async example. Enrollment stores the worker's key locally and needs no separate operator approval.
from getstack.errors import StackError
def setup_fleet(*, operator, provisioner, policy, policy_key, worker_name,
enrollment_key, create_worker, wait_for_approval):
try:
activated = operator.fleets.activate(policy, idempotency_key=policy_key)
except StackError as error:
approval = error.details
if (error.code != "GOVERNANCE_APPROVAL_REQUIRED"
or not approval.get("approval_id") or not approval.get("approval_url")):
raise
wait_for_approval(approval)
activated = operator.fleets.activate(policy, idempotency_key=policy_key,
governance_approval_id=approval["approval_id"])
enrolled = provisioner.fleets.enroll_local(activated["id"],
name=worker_name, idempotency_key=enrollment_key)
worker = create_worker(enrolled["agent_id"])
session = worker.fleets.session()
return {"policy_id": activated["id"], "agent_id": enrolled["agent_id"],
"worker": worker, "session": session}
from getstack.errors import StackError
async def setup_fleet(*, operator, provisioner, policy, policy_key, worker_name,
enrollment_key, create_worker, wait_for_approval):
try:
activated = await operator.fleets.activate(policy, idempotency_key=policy_key)
except StackError as error:
approval = error.details
if (error.code != "GOVERNANCE_APPROVAL_REQUIRED"
or not approval.get("approval_id") or not approval.get("approval_url")):
raise
await wait_for_approval(approval)
activated = await operator.fleets.activate(policy, idempotency_key=policy_key,
governance_approval_id=approval["approval_id"])
enrolled = await provisioner.fleets.enroll_local(activated["id"],
name=worker_name, idempotency_key=enrollment_key)
worker = create_worker(enrolled["agent_id"])
session = await worker.fleets.session()
return {"policy_id": activated["id"], "agent_id": enrolled["agent_id"],
"worker": worker, "session": session}
import hashlib
import json
from getstack.errors import StackError
def run_fleet_mission_example(stack, request, provider_url="https://slack.com/api/chat.postMessage"):
started = stack.missions.start(request)
repeated = stack.missions.start(request)
assert repeated["mission_id"] == started["mission_id"]
operation_id = hashlib.sha256(json.dumps([started["mission_id"], "support-update"]).encode()).hexdigest()
response = stack.proxy.request("slack", provider_url, "POST",
body={"channel": "C123", "text": "Support update"},
passport_token=started["passport"]["token"], authority_request_id=operation_id)
assert response.status == 200
try:
stack.proxy.request("slack", provider_url, "POST", body={"channel": "OTHER", "text": "Support update"},
passport_token=started["passport"]["token"], authority_request_id=operation_id + "_denied")
except StackError as error:
assert error.status_code == 403
else:
raise AssertionError("Expected channel restriction")
before = stack.missions.get(started["mission_id"])
renewed = stack.missions.renew(started["mission_id"])
after = stack.missions.get(renewed["mission_id"])
assert renewed["mission_id"] == started["mission_id"]
assert after["fleet"]["limits"] == before["fleet"]["limits"]
assert after["fleet"]["usage"] == before["fleet"]["usage"]
completed = stack.missions.complete(started["mission_id"], output={"done": True})
assert completed["terminal_state"] == "completed_successful"
stopped = stack.missions.start({**request, "idempotency_key": request["idempotency_key"] + "-stop"})
stack.missions.revoke(stopped["mission_id"], reason="operator_initiated_clean")
try:
stack.missions.renew(stopped["mission_id"])
except StackError:
pass
else:
raise AssertionError("Expected revoked Mission")
return {"mission_id": started["mission_id"], "operation_id": operation_id, "completed": True, "revoked": True}
import hashlib
import json
from getstack.errors import StackError
async def run_fleet_mission_example(stack, request, provider_url="https://slack.com/api/chat.postMessage"):
started = await stack.missions.start(request)
repeated = await stack.missions.start(request)
assert repeated["mission_id"] == started["mission_id"]
operation_id = hashlib.sha256(json.dumps([started["mission_id"], "support-update"]).encode()).hexdigest()
response = await stack.proxy.request("slack", provider_url, "POST",
body={"channel": "C123", "text": "Support update"},
passport_token=started["passport"]["token"], authority_request_id=operation_id)
assert response.status == 200
try:
await stack.proxy.request("slack", provider_url, "POST", body={"channel": "OTHER", "text": "Support update"},
passport_token=started["passport"]["token"], authority_request_id=operation_id + "_denied")
except StackError as error:
assert error.status_code == 403
else:
raise AssertionError("Expected channel restriction")
before = await stack.missions.get(started["mission_id"])
renewed = await stack.missions.renew(started["mission_id"])
after = await stack.missions.get(renewed["mission_id"])
assert renewed["mission_id"] == started["mission_id"]
assert after["fleet"]["limits"] == before["fleet"]["limits"]
assert after["fleet"]["usage"] == before["fleet"]["usage"]
completed = await stack.missions.complete(started["mission_id"], output={"done": True})
assert completed["terminal_state"] == "completed_successful"
stopped = await stack.missions.start({**request, "idempotency_key": request["idempotency_key"] + "-stop"})
await stack.missions.revoke(stopped["mission_id"], reason="operator_initiated_clean")
try:
await stack.missions.renew(stopped["mission_id"])
except StackError:
pass
else:
raise AssertionError("Expected revoked Mission")
return {"mission_id": started["mission_id"], "operation_id": operation_id, "completed": True, "revoked": True}
stack.missions starts and manages server-side Missions. The with stack.passports.mission(agent_id, intent, services) context manager opens a Mission and its Passport in one call (POST /v1/missions/start), completes the Mission on exit and revokes it if the block raises. Pass mission_id="ms_..." to bind an existing Mission instead; the helper then leaves that Mission open.
stack.fleets.list()
stack.fleets.list_page(page=1, limit=20)
stack.fleets.get(policy_id)
stack.fleets.membership(agent_id)
stack.fleets.members_page(policy_id, page=1, limit=20)
stack.fleets.activate(policy, idempotency_key=key, governance_approval_id=approval_id)
stack.fleets.revise(policy_id, policy, expected_revision=revision, idempotency_key=key, governance_approval_id=approval_id)
stack.fleets.members(policy_id)
stack.fleets.revoke_member(policy_id, agent_id)
stack.fleets.revoke(policy_id)
stack.fleets.enroll_local(policy_id, name=name, idempotency_key=key)
stack.fleets.enroll_workload_local(policy_id, name=name, idempotency_key=key)
stack.fleets.session(fleet_policy_id)
stack.missions.start(agent_id="agt_...", mission={"intent_summary": "...", "intent_services": [...]}, passport={...})
stack.missions.start(fleet_request) # fleet form: dict with fleet_policy_id
stack.missions.open("summary", agent_id="agt_...", intent_services=[...], expires_in_seconds=3600)
stack.missions.renew(mission_id)
stack.missions.complete(mission_id, output={"done": True})
stack.missions.revoke(mission_id, reason="operator_initiated_clean")The operator form of missions.start returns a dict with mission_id, claim_id, expires_at and passport (token, jti, expires_at). passport is optional and takes issue fields such as scopes, ttl_seconds and intent. missions.open assigns the Mission to agent_id when given. Completing or revoking a Mission revokes every Passport bound to it, for any revoke reason.
stack.agents.register("name", description="optional")
stack.agents.list()
stack.agents.get("agt_...")
stack.agents.update("agt_...", status="suspended")
stack.agents.delete("agt_...")mission = stack.missions.open("Post the daily support update",
agent_id="agt_...", intent_services=["slack"])
passport = stack.passports.issue(
agent_id="agt_...",
mission_ids=[mission["mission_id"]],
authority_binding_ids=["abn_..."],
)
print(passport.token)mission_ids is required and holds exactly one live Mission ID; the SDK raises ValueError otherwise. The Mission must be assigned to the agent or unassigned. The Passport never outlives its Mission, and refresh and delegation fail once the Mission is completed, revoked or expired.
The issue helper attaches active bindings and the Passport receives opaque authority references. Authority management and the native receiver protocol use the Authority REST API. The async client accepts the same authority_binding_ids argument.
# Publishing
stack.skills.publish(
name="my-skill",
description="...",
input_schema={...},
output_schema={...},
execution_mode="sealed",
)
# Browsing
skills = stack.skills.browse(query="financial analysis", tags="finance")
# Invoking
result = stack.skills.invoke(
"skl_...",
agent_id="agt_...",
input={"data": "..."},
)
# Polling
completed = stack.skills.poll(result.id, timeout_seconds=60)
# Requests
stack.skills.post_request(description="I need...", tags=["finance"])
stack.skills.suggest_composition("sreq_...")providers = stack.identity.list_providers()
# Start a provider shown as available for this deployment.
provider_key = providers[0].key
session = stack.identity.initiate_verification(provider_key)
stack.identity.complete_verification(provider_key, session.session_ref)
claims = stack.identity.list_claims()
stack.identity.revoke_claim("clm_...")
stack.identity.replace_service_requirement(service_id, requirement_or_none)import os
# Local OAuth profile created by the CLI
stack = Stack()
# Operator API key from the runtime secret store
stack = Stack(api_key=os.environ["STACK_API_KEY"])
# Existing dashboard session
stack = Stack.from_session(session_token)# Drop-offs
dof = stack.dropoffs.create(from_agent_id="agt_a", to_agent_id="agt_b", schema={...})
# Each agent passes the Passport from its own stack.missions.start(...).
stack.dropoffs.deposit(dof["id"], data={...}, agent_id="agt_a", passport_token=producer_token)
stack.dropoffs.collect(dof["id"], agent_id="agt_b", passport_token=consumer_token)
# Proxy
resp = stack.proxy.request(service="slack", method="POST",
url="https://slack.com/api/chat.postMessage",
body={"channel": "C0123", "text": "hi"},
passport_token=passport.token,
on_operation_id=save_operation_id)
operation = stack.proxy.get_operation(resp.operation_id,
passport_token=passport.token,
include_response=True)
# Content scan — run prompt-injection detector on retrieved content
result = stack.scan.scan(
content=email_body,
context="email",
source=sender_address,
)
if result["verdict"] == "critical":
raise RuntimeError(f"Indirect injection caught: {result['match']['pattern_id']}")
stack.scan.usage() # monthly quota
# Delivery methods (email, SMS or webhook; each is verified before it delivers)
dest = stack.delivery_methods.add("webhook", "https://…")
stack.delivery_methods.send_verification_code(dest["id"])
stack.delivery_methods.verify(dest["id"], code="123456")
stack.delivery_methods.test(dest["id"])
# Security events
events = stack.security_events.list(limit=50)A notification rule routes events to verified delivery methods. The Python client has no helper for creating one; send POST /v1/notifications/channels with destination_ids, events and an optional min_severity. See Notifications API.
Non-GET proxy calls generate an operation ID before network I/O. Durable workers should use on_operation_id to persist it, then reuse authority_request_id only for the unchanged request. Sync and async clients both provide get_operation() for recovery. See Proxy operation recovery.
# Tail recent entries — newest-first. Pass since (epoch ms) for
# incremental polling; pass agent_id or passport_jti to narrow.
data = stack.audit.list(limit=20, agent_id="agt_support")
for entry in data["entries"]:
print(entry["action"], entry["outcome"])
watermark = data["max_timestamp"]
# Anchor the chain head externally to prove no row was rewritten later.
head = stack.audit.chain_head()
# Walk the chain and verify every entry's hash + link integrity.
result = stack.audit.verify_chain()
assert result["valid"], result.get("first_break")
# Checkout reviews
pending = stack.reviews.list(status="flagged")
stack.reviews.decide(checkout_id="cout_abc", decision="approved",
notes="notion use was ancillary")Cascade revokes write one passport.revoke entry plus onepassport.revoke_cascade per child — filter bypassport_jti= to see the chain for a single passport. The Python client has no export helper; use GET /v1/audit/export for date-range exports.
For async applications, import AsyncStack. All service methods are awaitable; the service surface mirrors the sync client exactly.
from getstack import AsyncStack
stack = AsyncStack(api_key="sk_live_...")
async with stack:
agent = await stack.agents.register("my-agent")
mission = await stack.missions.open("...", agent_id=agent.id, intent_services=[...])
passport = await stack.passports.issue(agent_id=agent.id, mission_ids=[mission["mission_id"]],
intent="...", services=[...])
# ...
await stack.missions.complete(mission["mission_id"]) # also revokes the Passportfrom getstack import Stack, NotFoundError, ForbiddenError
try:
agent = stack.agents.get("agt_nonexistent")
except NotFoundError:
print("Agent not found")
except ForbiddenError:
print("Access denied")