The Python SDK for the saas accounts API — the Python twin of
saas-sdk-go. A versioned,
published client that solutions depend on instead of regenerating their own
bindings.
Everything ships under a single top-level package, saas_sdk, so installing it
never claims a generic name (saas, buf, datasource) in the consumer's
import namespace.
Packages:
-
saas_sdk._gen— the generated protobuf message bindings (*_pb2), from the accounts public proto at the ref recorded inSOURCE.txt. Only message types are generated; the runtime owns the Connect transport, so there are no client/server stubs. The bindings embed only the accounts protos this SDK exposes (datasource.proto,work_contexts.proto) — thebuf.validate/saas.policycustom options, thegoogle.apiHTTP annotations, and their shared descriptors are stripped during generation, so this SDK never registers a shared proto into the global descriptor pool (which would collide with a sibling saas SDK). Thegoogle.protobufwell-known types stay runtime types. -
saas_sdk.datasource— a gateway-bound facade over those types. It names the Connect procedure and routes the call through the solution runtime'sGateway.unary(procedure, request, response_type)seam, so a solution connects a GitHub datasource collection in a few lines:from saas_sdk import datasource ds = datasource.new(gateway) source = ds.add_github_source( org_id=org, repo="codefly-dev/module-saas-starter", paths=["docs"], collection="handbook", access_token=token, ) ds.sync(org, source.id)
gatewayis any value exposingunary(procedure, request, response_type)— whichsolution_runtime.Gatewayalready satisfies. The runtime stays datasource-agnostic and takes no dependency on this SDK. -
saas_sdk.work_context— both halves of thex-codefly-work-contextfeature. Mint side, a delegated caller mints a short-lived context at turn start and stamps it on outgoing calls. Callee side, a service verifies a presented context. See below.
A Work Context is a signed, delegated-authority capability carried in the
x-codefly-work-context header. saas_sdk.work_context carries the two sides a
solution needs: the mint-side client to the accounts authority, and the
callee-side verifier. Only the signer itself stays authority-side in
sdk-go.
The wire format is not a JWT. A token is
base64url(JSON payload).base64url(Ed25519 sig), the payload is a fixed
snake_case JSON object, and key_id lives inside the payload. Field order,
uint64-as-decimal-string encoding, scope canonicalization, base64url, and
Ed25519 signing are pinned byte-for-byte to sdk-go: the wire golden in
tests/fixtures/work_context_wire_golden.json is byte-identical to sdk-go's
TestWorkContextWireGolden, and CI in both repos verifies the same token.
A solution acting on a user's behalf mints a context at turn start and stamps it on every outgoing call, so a downstream service can verify the delegated authority:
from saas_sdk import work_context
wc = work_context.new(gateway)
parent = wc.start_task(
bearer=user_bearer, org_id=org, task_id=task, session_id=session,
audience="accounts",
scopes=[work_context.pb.WorkContextScope(resource_kind="evidence", actions=["read"])],
)
ctx = wc.exchange_audience(bearer=user_bearer, parent=parent, audience="evidence", scopes=scopes)
work_context.attach(headers, ctx) # per outgoing requestEvery mint RPC is owner-bound, so the user's bearer is passed on each call
rather than relying on the gateway's ambient service identity; the mint client
therefore needs a gateway whose unary takes that bearer. A context lives
5 minutes by default and at most 15 (DEFAULT_TTL / MAX_TTL); renew extends
work running past the cap. Mint failures raise WorkContextMintError, distinct
from a verification failure.
from saas_sdk import work_context as wc
verifier = wc.JWKSVerifier(
"https://accounts.codefly.dev/v1/auth/.well-known/jwks.json",
)
verifier.refresh() # optional: fail closed at boot if key discovery is down
token = wc.token_from_headers(request.headers)
claims = verifier.verify(token, wc.Expectations(
issuer="https://accounts.codefly.dev/work-context",
audience="warden.evidence",
))
wc.require_scope(claims, wc.ScopeRequirement("repository", "write", "repo-warden"))JWKSVerifier discovers Ed25519 keys through the published JWKS endpoint,
caches them, and — rotation-aware — refreshes once per cache generation on an
unknown key_id. Verifier is the same check against a fixed set of keys.
Verification enforces the two-segment shape, the signature, structural
validation (including the proto's min_len constraints on optional fields and
monotonic scope attenuation), the time window, and the caller's expectations,
raising WorkContextError (or WorkContextDenied from require_scope) on any
failure. When the key set itself cannot be fetched or read, JWKSVerifier raises
WorkContextUnavailable, a WorkContextError subclass, so a consumer can report
an authority outage as unavailable rather than as an invalid capability. Replay policy is reported on the claims; enforcing single-use
consumption still requires a durable replay store the caller owns.
saas_sdk.authorization_revision implements Accounts' current revision protocol
for already verified WorkContext claims. It projects the owner and every
actor's original scopes, including resource IDs and revision. The consumer keeps
signature/lifetime verification, action/resource policy and public error mapping.
from saas_sdk.authorization_revision import MeshGRPCClient, RevisionDenied, RevisionUnavailable
# The host:port Codefly resolved for accounts' private `rest` endpoint.
client = MeshGRPCClient(resolved_accounts_rest_address, current_internal_credential)
try:
await client.check(verified_claims)
finally:
await client.aclose()A current host serves this RPC as cleartext HTTP/2 (h2c) multiplexed onto accounts'
private rest endpoint, and leaves in-cluster transport security to the service
mesh (module-saas-starter module/INTERNAL_TRANSPORT.md). MeshGRPCClient dials
that resolved host:port and adds no TLS; it refuses a URL, a path or credentials
in the address. Use it only where the mesh enforces mutual TLS on the hop.
GRPCClient("https://…", …) remains for a deployment that terminates server TLS on
accounts itself.
Install saas-sdk-python[revision-grpc] for either gRPC client. Where an
endpoint explicitly serves Connect, install [revision-connect] and select
ConnectClient; selecting that compatibility transport is a composition choice,
never an automatic fallback. revision_request(claims) needs neither networking
extra and returns the canonical generated protobuf request.
Each check loads the current internal service credential through an async callback
and makes one query. It does not cache authorization, retry, inherit environment
proxies, follow redirects or forward caller bearer tokens, cookies or Work Context
headers. One three-second budget covers credential loading, connection and the
complete response; caller cancellation propagates. The callback and any injected
fixture transport must cooperate with cancellation. tls_context selects verified
trust roots; TLS verification cannot be disabled. Connect's explicit transport
injection exists for fixtures and is trusted to implement its own transport
security; production should use the default transport.
Only gRPC PermissionDenied/FailedPrecondition, or their coherent Connect
status/code pairs, raise RevisionDenied. Credential, transport and malformed
response failures raise RevisionUnavailable. Responses must be the canonical
empty message, and error strings omit remote details and credentials. Neither
error grants authority; consumers explicitly map them onto their public API.
The projection fixture at tests/fixtures/authorization_revision_projection.json
is shared byte-for-byte with the Go SDK's
authorizationrevision/testdata/projection.json. Owner/actor scope projection is
therefore verified independently of transport and consumer policy.
This SDK's release version tracks the saas-starter module version
(module/module.package.codefly.yaml), recorded in SOURCE.txt alongside the
proto ref the bindings were generated from. The protobuf runtime floor is
>=5.29.3 (the gencode line the bindings are built with), so the SDK installs
alongside consumers still on protobuf 5.x.
The proto source of truth is module-saas-starter/module/services/accounts/proto.
From a checkout of that repo:
scripts/generate.sh <module-saas-starter>/module/services/accounts/protoThat builds a descriptor image for datasource.proto, strips the custom options
and shared-proto dependencies (scripts/strip_options.py), and regenerates
src/saas_sdk/_gen/datasource_pb2.py. Never hand-edit _gen/.
from saas_sdk import datasource # the facade
from saas_sdk._gen import datasource_pb2 # the message types