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: theroleas the tool applied it, which may differ from the one requested, anidfor the grant if the tool has one (Steward sends it back on revoke), andpending: Truewhile 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: therolesthe identity holds now, and whether a grant is stillpending.
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
inputssupports[],inanddict();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
roleyou were given, can be used as they are. Returning nothing is an empty response. - Inputs.
integration.inputsandinputshold the values that are not sensitive, keyed by input name; numbers arrive as floats. Sensitive values arrive inintegration.secrets(and a resource's or application'ssecrets), a plaindict-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 appliedrole), 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
kindandname, which Steward stores and sends on every later call.provisionreceives them with theinputsand returns only theoutputs, such as a URL or an id the tool assigned. An identity hasexternal_id,name,ulidandattrs; those that are optional report whether they were set withHasField. - Variables. An application's variables each carry
sensitive,sealedandlocked. 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)
| File | Size | Uploaded | |
|---|---|---|---|
| treno_dev_steward_sdk-0.1.0rc1.tar.gz | 11.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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