Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

treno-dev-steward-sdk

The Steward SDK for Python. Today it holds the part for writing Steward plugins, imported from steward_sdk.plugin. A plugin is one integration: it declares what it needs to connect to a system and what it manages there, and implements the calls Steward makes. The SDK handles everything else: the gRPC services, the handshake with the runner, the health check, and routing each call to your handler by kind.

pip install treno-dev-steward-sdk
from steward_sdk.plugin import create_plugin

Requires Python 3.10 or newer. The package ships its type hints.

Start a plugin

steward plugin init my-plugin --language python
cd my-plugin
python -m venv .venv && source .venv/bin/activate
pip install -e .
python src/plugin.py

steward plugin init takes its templates from the SDK release, so the project always matches the SDK it depends on. To work on the SDK and its templates together, point it at a local checkout with --sdk-path /path/to/steward-sdk. The template lives next to the SDKs, in ../templates/python.

A plugin

This is the plugin that steward plugin init --language python generates, with a resource that can be provisioned and given access to. The block below is filled in from the template by python scripts/readme.py, so the two never differ.

# This is the file you edit. It declares what your plugin offers and implements the calls Steward
# makes. The gRPC plumbing, the handshake and the health check live in the Steward SDK.
#
# A plugin is one integration, like a provider. Declare it and each kind of resource once, with
# its handlers attached: Steward's forms and the capabilities of each kind (provisioning,
# discovery, access) follow from what you write here, and every call is routed to the right
# handler by kind. If your plugin needs more than one tool, take the credentials for each as inputs.
#
# Each handler is annotated with the request and response types the SDK exports, named for the
# entity and the call (ResourceProvisionRequest, ResourceProvisionResponse). A request is the
# generated message, so your editor completes its fields; a response is a message or a plain dict
# with the fields you have something to say about. Handlers may be async or plain functions.

from steward_sdk.plugin import (
    IntegrationValidateRequest,
    IntegrationValidateResponse,
    ResourceDeprovisionRequest,
    ResourceDeprovisionResponse,
    ResourceGetAccessRequest,
    ResourceGetAccessResponse,
    ResourceGrantAccessRequest,
    ResourceGrantAccessResponse,
    ResourceProvisionRequest,
    ResourceProvisionResponse,
    ResourceRevokeAccessRequest,
    ResourceRevokeAccessResponse,
    create_plugin,
)


# Check that the inputs and credentials work. Return an error per input the user can fix.
async def validate(request: IntegrationValidateRequest) -> IntegrationValidateResponse:
    errors = []

    if not request.integration.secrets.get("token"):
        errors.append({"field": "token", "message": "An API token is required."})

    return {"errors": errors}


# Create a resource called `request.name`. Idempotent: if it already exists, succeed anyway. Return
# what it produced, as declared in `outputs`. The request also has `integration` and `inputs`.
async def provision(request: ResourceProvisionRequest) -> ResourceProvisionResponse:
    base_url = request.integration.inputs["base_url"]

    return {"outputs": {"url": f"{base_url}/items/{request.name}"}}


# Remove a resource. Idempotent: succeed if it is already gone. The request has `integration` and
# `resource` (`kind`, `name`).
async def deprovision(request: ResourceDeprovisionRequest) -> ResourceDeprovisionResponse:
    return {}


# Give an identity a role on the resource. Idempotent. Return the role as the tool applied it, an
# `id` for the grant if the tool has one, and `pending: True` if the person has to act first, such
# as accepting an invitation. The request has `integration`, `resource`, `identity` (`external_id`,
# `name`, `attrs`) and `role`.
async def grant_access(request: ResourceGrantAccessRequest) -> ResourceGrantAccessResponse:
    return {"role": request.role}


# Take a role away. Idempotent: succeed if the identity does not have it.
async def revoke_access(request: ResourceRevokeAccessRequest) -> ResourceRevokeAccessResponse:
    return {}


# Report the roles the identity holds on the resource now, and whether a grant is still pending.
async def get_access(request: ResourceGetAccessRequest) -> ResourceGetAccessResponse:
    return {"roles": [], "pending": False}


# The integration is one connection to the tool. Steward builds its form from `inputs`, so it is
# also the documentation people see. Mark credentials `sensitive`: they arrive in
# `request.integration.secrets`, the other inputs in `request.integration.inputs`.
plugin = create_plugin(
    name="my-plugin",
    version="0.1.0",
    title="My plugin",
    description="Connects Steward to My plugin.",
    # What is required to create an integration from this plugin.
    inputs=[
        {"name": "base_url", "label": "Base URL", "type": "string", "required": True},
        {"name": "token", "label": "API token", "type": "string", "required": True, "sensitive": True},
    ],
    validate=validate,
)

# A kind of resource this integration manages.
plugin.resource(
    kind="item",
    title="Item",
    description="An example resource.",
    # What a resource of this kind takes besides its name, which Steward stores with the kind.
    inputs=[{"name": "description", "label": "Description", "type": "string"}],
    outputs=[{"name": "url", "label": "URL", "type": "string"}],
    # What can be granted on this kind: the tool's roles, and the permissions each contains. Writing
    # the three access handlers is what makes the kind accept access.
    roles=[
        {"name": "read", "title": "Read", "permissions": [{"name": "view"}]},
        {"name": "write", "title": "Write", "permissions": [{"name": "view"}, {"name": "edit"}]},
    ],
    provision=provision,
    deprovision=deprovision,
    grant_access=grant_access,
    revoke_access=revoke_access,
    get_access=get_access,
)

plugin.serve()

A plugin needs one create_plugin(...), any number of plugin.resource(...) and plugin.application(...) declarations, and a final plugin.serve(). If it needs more than one tool, for example both Cloudflare and AWS, take the credentials for each as inputs.

What you declare

The integration, in create_plugin:

Argument
name, version Required.
title, description Shown in Steward.
inputs What is required to create an integration, credentials included.
validate Checks the inputs and credentials. Returns {"errors": [{"field": ..., "message": ...}]}.
roles, permissions, grant_access, revoke_access, get_access Access to the integration as a whole, such as membership of an organization.

A resource, with plugin.resource(kind=..., ...), and an application, with plugin.application(kind=..., ...), both have a kind that is unique within the plugin, a title, a description, inputs and outputs. Resources also take roles and permissions. Applications take sources ("git", "image").

Inputs and outputs are declared like variables:

{"name": "visibility", "label": "Visibility", "type": "select", "options": ["private", "public"], "default": "private"}

type is one of string, number, boolean, select, list (of strings) or map. Mark a credential "sensitive": True and it is never shown back once set.

Access

Access exists at two levels, each with the same three handlers, grant_access, revoke_access and get_access:

  • The integration, in create_plugin: membership of the tool as a whole, such as an organization or a site. Steward grants this first, then access to the resources.
  • A resource kind, in plugin.resource: a role on one resource, such as a repository. The generated plugin above shows this.

For the integration it looks like this:

async def grant_access(request: IntegrationGrantAccessRequest) -> IntegrationGrantAccessResponse:
    identity = request.identity
    email = identity.external_id or plain(identity.attrs)["email"]
    invitation = await invite(request.integration, email, request.role.name)

    return {"id": invitation.id, "role": request.role, "pending": True}  # an invitation is not access until accepted


plugin = create_plugin(
    name="github",
    version="0.1.0",
    inputs=[...],
    roles=[{"name": "member", "title": "Member"}, {"name": "owner", "title": "Owner"}],
    grant_access=grant_access,
    revoke_access=revoke_access,
    get_access=get_access,
)

invite stands for a call to the tool's own API.

What you declare with roles (and, for tools that expose them, permissions) is what Steward offers for assignment. A role has a name and the permissions it contains, and a permission is a name with an optional level: {"name": "pull_requests", "level": "read"}, or just {"name": "s3:GetObject"}. Steward can also build its own roles from the permissions you declare.

Handlers receive the identity (external_id, name, attrs) and the role to apply, and a resource call also gets the resource (kind, name). They return:

  • grant_access: the role as the tool applied it, which may differ from the one requested, an id for the grant if the tool has one (Steward sends it back on revoke), and pending: True while the person has to act first, such as accepting an invitation.
  • revoke_access: nothing. It succeeds if the identity did not have the role.
  • get_access: the roles the identity holds now, and whether a grant is still pending.

Steward works out overlap between roles before it asks you to revoke one, so you can remove the role you are given in full.

Capabilities follow from your handlers

You never list capabilities. The SDK reads them from the handlers you pass:

You pass The kind can
provision, deprovision be created and removed (provisioning)
list be discovered (optional: Steward keeps track of what it creates, so list is only for finding resources that already exist)
grant_access, revoke_access, get_access have access granted (resources and the integration)
create, delete, deploy, set_variables, list be run as an application

A call for something you did not pass fails with UNIMPLEMENTED.

Handlers

A handler takes the request and returns the response. It may be async def or a plain function (a plain one runs in a thread, so blocking calls do not stall the plugin).

  • Requests are the generated protobuf messages, so fields are snake_case and your editor completes them. A message the runner did not send reads as empty. A Struct such as inputs supports [], in and dict(); plain(message) turns any message into a plain dict.
  • Responses are a response message or a plain dict with the fields you have something to say about (snake_case or camelCase). Messages inside it, such as the role you were given, can be used as they are. Returning nothing is an empty response.
  • Inputs. integration.inputs and inputs hold the values that are not sensitive, keyed by input name; numbers arrive as floats. Sensitive values arrive in integration.secrets (and a resource's or application's secrets), a plain dict-like of strings. Never store them.
  • Idempotent, and no report of changes. Make calls that change something idempotent: creating what exists, or removing what is gone, simply succeeds. Return the result of the change (the outputs, the applied role), not a description of what changed: Steward keeps the state and works out the difference itself.
  • Resources and identities. A resource is identified by its kind and name, which Steward stores and sends on every later call. provision receives them with the inputs and returns only the outputs, such as a URL or an id the tool assigned. An identity has external_id, name, ulid and attrs; those that are optional report whether they were set with HasField.
  • Variables. An application's variables each carry sensitive, sealed and locked. Refuse to change or remove a locked variable.

Errors

Raise a PluginError with a gRPC status code to report a specific failure:

from steward_sdk.plugin import PluginError, StatusCode

raise PluginError(StatusCode.FAILED_PRECONDITION, "APP_KEY is locked and cannot be removed")

Common codes: INVALID_ARGUMENT, NOT_FOUND, FAILED_PRECONDITION, UNIMPLEMENTED. Any other exception is reported as INTERNAL, with its traceback on stderr. A call for an unknown kind is answered with NOT_FOUND by the SDK.

Logging

Write logs to stderr (the logging module does by default). Standard output is reserved for the one handshake line the runner reads.

Developing the SDK

The messages and gRPC services are generated from ../proto with buf (buf.gen.yaml, using buf's remote plugins, so it needs network access), and the generated code is not committed. The plugin versions in buf.gen.yaml decide the minimum protobuf and grpcio the package requires.

python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
python scripts/generate.py          # the contract into src/steward_sdk/_gen
python scripts/readme.py            # refill the example above from ../templates/python
python scripts/readme.py --check    # fail if the example is out of date (for CI)
python -m build                     # the wheel and sdist, with the generated code

Metadata

Release files for treno-dev-steward-sdk 0.1.0rc1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for treno-dev-steward-sdk 0.1.0rc1
File Size Uploaded
treno_dev_steward_sdk-0.1.0rc1.tar.gz 11.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for treno-dev-steward-sdk 0.1.0rc1
File Interpreter ABI Platform
treno_dev_steward_sdk-0.1.0rc1-py3-none-any.whl Python 3 none any Details

Total release size: 22.9 kB

Release files / treno_dev_steward_sdk-0.1.0rc1.tar.gz

Download URL treno_dev_steward_sdk-0.1.0rc1.tar.gz
Size 11.3 kB
Tags Source
SHA-256 checksum
How to use checksums
59b086839ee00b7d213aef99811dce70c6b14c30ee1bde5b0b0eb59ef1959cbf
BLAKE2b-256 checksum
How to use checksums
c931c33f063fd873f8c10e291dce974980b6b1a3ea645d83f3d1a170a46c814e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release files / treno_dev_steward_sdk-0.1.0rc1-py3-none-any.whl

Download URL treno_dev_steward_sdk-0.1.0rc1-py3-none-any.whl
Size 11.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d0bbcdef52f3cd9d032da33bca10e374c5b75a811a39194cb00eccd89c9fe8f1
BLAKE2b-256 checksum
How to use checksums
3f94a83684edce381d58982c914a92abef657067d00f7e1536fc112f83e820da
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0rc1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page