chore: add the keycardai-temporal package - #262
Merged
Conversation
Port keycard-sandbox/poc-keycard-temporal (main plus the stacked PRs #7 to #11) into packages/temporal as a workspace sibling. Public surface is unchanged: @grant(resource[, subject_from=...[, impersonate=True]]), access(), KeycardInterceptor. Tokens are minted per activity execution into the SDK's AccessContext and never reach workflow history. Release plumbing only; the commit is deliberately unscoped so no package bumps on merge. The first release is a manually pushed 0.1.0-keycardai-temporal tag. Co-Authored-By: Larry Osakwe <larry@keycard.ai>
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
…r real The port dropped the PoC README section documenting the one-time zone prerequisites (application credential, resource under the Zone Provider, the dependency that app-only issuance needs, and the on-behalf-of anchor resource topology); restored without the workshop-specific parts. The retry-policy test asserted membership in a list it had just built; replaced with a live test environment run proving a worker configured without a subject_token_provider fails the workflow after exactly one attempt with ApplicationError type GrantConfigurationError.
…uth 0.26.0 The classifier in _raise_on_mint_error and the client-credentials path now read keycardai.oauth.PERMANENT_ERROR_CODES instead of a local copy of the same three codes. Retry classification is byte-identical; a pin test asserts the set agrees with OAuthProtocolError.retryable for every code, so drift between them fails this package's suite. Consuming the retryable property directly is deferred to an oauth follow-up: the exchange-path error dicts carry no retryability, and the property classifies some code-less failures (4xx without an OAuth body, config errors) differently than this package does today. Credential discovery now delegates to keycardai.oauth.server's discover_credential, with CredentialDiscoveryError wrapped into GrantConfigurationError so a startup misconfiguration stays in the worker-configuration error class. Two behavior changes ride the swap: - An environment that can build both a client secret and a workload identity is rejected as ambiguous instead of silently preferring the client secret. EKS IRSA injects AWS_WEB_IDENTITY_TOKEN_FILE into pods, so a client-secret worker on EKS must now set KEYCARD_APPLICATION_CREDENTIAL_TYPE=client_secret. - KEYCARD_APPLICATION_CREDENTIAL_TYPE wins over everything: the old code returned the client secret even when the variable named a workload identity type. Discovery also gains AZURE_FEDERATED_TOKEN_FILE and the client_secret and workload_identity type names; eks_workload_identity remains as an alias and now builds a WorkloadIdentity over a FileTokenSource.
The package imports PERMANENT_ERROR_CODES, discover_credential, and CredentialDiscoveryError, all of which shipped in keycardai-oauth 0.26.0. The workspace lock hides the gap in CI; a consumer installing against 0.23.x-0.25.x gets an ImportError.
Larry-Osakwe
approved these changes
Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports
keycard-sandbox/poc-keycard-temporalinto the monorepo aspackages/temporal(keycardai-temporal), a sibling ofkeycardai-mcp,keycardai-fastmcp, andkeycardai-a2a. The ported tree is the PoC'smainplus its five stacked open PRs (#7 one OAuth client per worker, #8 the SDK's sharedAccessContext, #9 anyApplicationCredential, #10 robust identity binding, #11 impersonation), taken in stack order. Kim's public surface is unchanged:Invariants preserved from the PoC: tokens live only in a
ContextVarfor one execution and never reach headers, arguments, return values, or history; no caching (oneAsyncClientper worker, one mint per activity call); mint failures raise before the activity body;access_denied/insufficient_authorization/invalid_clientbecomeApplicationError(type="KeycardAccessDenied", non_retryable=True), transient failures stay retryable, andGrantConfigurationErroris retryable by default but listable innon_retryable_error_types;keycardaiimports are wrapped inworkflow.unsafe.imports_passed_through(); sync activities work on the thread-pool executor, process pools are documented as unsupported.Changes relative to the PoC source are cosmetic only (
collections.abc.Callableimport for ruff,ruff format, docstring tweaks).subject_frommarkers and dotted paths resolve at decoration time; when type hints hold unresolvable forward references the marker scan is deferred to first execution and still fails closed (NameError, no mint) if they never resolve.Tests (hermetic, no zone)
tests/test_interceptor.py(46 tests) drives the interceptor chain directly withAsyncClientstubbed: per-call fresh tokens, one client shared across activities and interceptor instances, credential polymorphism (ClientSecret, assertion-style credentials, env discovery, EKS), every identity-binding strategy and its fail-closed cases, impersonation with no provider configured and provider never called, denial classified non-retryable, transient failures retryable, composition with@activity.defn.tests/test_history_hygiene.pyischeck_history.pyturned into a real test:WorkflowEnvironment.start_local()runs a workflow with an async on-behalf-of activity (Subject marker) and a sync impersonating activity on aThreadPoolExecutor, both under the realKeycardInterceptorand the workflow sandbox. It fetches the recorded history, walks every string including base64-decoded payload data, asserts the identity reference is present (positive control) and the JWT-shaped token, its segments, the session token, and the client secret are absent. Verified locally that returning the token from an activity makes it fail. The Temporal dev server binary is downloaded bytemporalioon first use (network in CI; no zone).Repo conventions
The task description referenced a root
CLAUDE.mdscope table and per-package.cz.toml; this repo has neither. Following the actualkeycardai-langchainprecedent, the commitizen block (version0.1.0,tag_format = "${version}-keycardai-temporal", scopedchangelog_pattern,bump_pattern,bump_map,bump_map_major_version_zero) lives inpackages/temporal/pyproject.toml, and the scope row was added toDEVELOPER.md.scripts/test_bump_increment.pyregisters the package (so the generic sibling-isolation tests cover it) and addstest_temporal_bumps_only_on_its_own_scope. Dependencies:temporalio>=1.32.0(what the PoC pinned) andkeycardai-oauth>=0.23.0as a workspace source.Release plan
chore: add the keycardai-temporal package), so merging bumps and releases nothing (bump_patternonly matches(keycardai-temporal)scopes; covered by the new bump-scope test).0.1.0-keycardai-temporal, after the PyPI side is ready..github/workflows/release.yml, triggered by tags matching*-keycardai-temporal(added in this PR), which resolves the tag topackages/temporalviascripts/changelog.py package, builds it withuv build, and publishes withuv publishunder thepypi-releaseenvironment using OIDC trusted publishing. Register the pending publisher on PyPI for projectkeycardai-temporalwith repositorykeycardai/python-sdk, workflowrelease.yml, environmentpypi-releasebefore pushing the tag; PyPI accepts pending publishers for not-yet-existing projects, so no manual upload is needed.uv-dynamic-versioningreports a0.0.0.postN.dev0local version for the package, exactly as the other packages did before their first tag.Out of scope, unchanged: the spec instance, the MongoDB partner repo, Kim's sandbox repo.
Link to Devin session: https://app.devin.ai/sessions/bc5a82199c744c44a247beea89a41b65
Open in Devin Desktop: https://app.devin.ai/desktop/session/bc5a82199c744c44a247beea89a41b65?variant=devin
Requested by: @Larry-Osakwe
Update (Sep 1): swapped local workarounds for keycardai-oauth 0.26.0
Merged main and deleted the two pieces of local code that 0.26.0 replaces:
_PERMANENT_DENIAL_CODEStable is gone; the classifier readskeycardai.oauth.PERMANENT_ERROR_CODES(the same three codes). Retry classification is byte-identical, and a new pin test asserts the set agrees withOAuthProtocolError.retryablefor every code, so drift between them fails this package's suite. Consuming theretryableproperty directly is deferred to an oauth follow-up: the exchange-path error dicts carry no retryability, and the property classifies some code-less failures differently than this package does today._discover_credentialis gone; the interceptor callskeycardai.oauth.server.discover_credential()and wrapsCredentialDiscoveryErrorintoGrantConfigurationError, so a startup misconfiguration stays in the worker-configuration error class.Two discovery behavior changes ride the swap:
AWS_WEB_IDENTITY_TOKEN_FILEinto pods automatically, so a client-secret worker on EKS must setKEYCARD_APPLICATION_CREDENTIAL_TYPE=client_secret. Documented in the README.KEYCARD_APPLICATION_CREDENTIAL_TYPEnow wins over everything; the old code returned the client secret even when the variable named a workload identity type.Tests: 51 (was 48). New: ambiguity rejection with the type-pin fix, type-switch precedence, and the set/property equivalence pin.
test_eks_workload_identity_discovered_from_envbecametest_workload_identity_discovered_from_env(discovery returnsWorkloadIdentity;eks_workload_identitystays as a legacy alias).