Skip to content

feat(durable): read and stop executions with application credentials - #422

Open
swkeever wants to merge 5 commits into
mainfrom
skeever/durable-execution-data-plane
Open

swkeever wants to merge 5 commits into
mainfrom
skeever/durable-execution-data-plane

Conversation

@swkeever

@swkeever swkeever commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Change

A Python application can now read and stop the durable executions it started, with the credential start() already uses: the active session, then a service key, then the anon key. Before this, get() and stop() called only the owner routes, which need a platform token.

client.durable.get_from_application(function_name, execution_id, *, client_token=None)  # -> DurableExecution
client.durable.stop_from_application(function_name, execution_id, *, client_token=None) # -> DurableExecution
  • They call GET /durable-functions/{functionId}/executions/{executionId} and POST .../stop.
  • client_token goes out as X-Volcano-Client-Token when given and is omitted otherwise. It is checked the way durable.messages checks it: a non-empty printable ASCII string with no surrounding whitespace, at most 128 characters.
  • client_token goes out with every credential, including a service key: a service key scoped without * reaches an execution only with its token. A key holding *, the default, reaches any execution, including a pending one.
  • 404 raises NotFoundError, which is also what a credential that cannot reach the execution gets. A 409 on a stop raises ConflictError, and only a service key holding * gets it.
  • A 503 raises ServerError. The SDK reads Retry-After only on a 429 (RateLimitedError.retry_after), so the header a throttled read sends with its 503 is not surfaced. The docs say a ServerError from a stop can follow a stop that was already sent, so read before stopping again.
  • The SDK has no async client, so there are no async variants.
  • The owner-scoped get(), list() and stop() are unchanged. These no longer say that reading or stopping needs the owner's token: their docstrings, the start() docstring, the DurableExecution.client_token comment, docs/functions.md and the README.
  • The header check moves from _durable_messages to _durable_arguments.header_value, so both facades share it. Its behavior is unchanged.
  • Purely additive.

Depends on Kong/volcano-hosting#2364, merged as 29807384cf.

Coordination branch: skeever/durable-execution-data-plane.

Contract snapshot

openapi/openapi.yaml takes only the durable sections #2364 shipped: the start path, the two application paths, the DurableClientToken parameter, and the DurableExecution schema. They are copied verbatim from the public bundle scripts/ci/openapi-bundle.sh builds from Hosting main 1ccff82a16, which equals its docs/public/api-reference/openapi.yaml, and now match it exactly. The rest of Hosting's drift is left to the automated sync PR (#2). The client was regenerated with uv run python scripts/generate_openapi.py. The generator's required-operation list adds the two new operations.

Verification

On 5d3cdd4, with uv 0.12.17, the version CI pins:

  • poe policy, generated, lint, format-check, types (mypy and basedpyright), audit, package-check: pass.

  • poe test and poe coverage: 3,042 passed, 1 failed; coverage 100%. The failure is test_realtime.py::test_realtime_presence_sync_coalesces_latest_backpressured_state, a 0.2 s timing assertion this PR does not touch, which fails intermittently on this machine at load average ~58 and passed in CI on 61fbf9c.

  • poe mutation, run per changed module on 61fbf9c, left no survivors. 5d3cdd4 changes only docstrings, docs, the spec text, and tests, so it is left to CI's per-module mutation jobs:

    Module Killed Rejected by type checks
    durable 206 308
    _durable_messages 146 189
    _transport_execution 44 157
    _durable_arguments 24 12

    models changed only in a comment.

New tests in src/volcano_sdk/_tests/test_durable.py cover:

  • the request path, method and Authorization through the generated transport over a mock HTTP transport, with X-Volcano-Client-Token sent when given and absent when not;
  • X-Volcano-Client-Token sent over the wire with a session, a service key, and an anon key;
  • which credential is used: session, service key, or anon key;
  • response mapping;
  • 404, from the facade and over the wire, and a stop's 409;
  • argument checks before transport.

Deferred to CI: package-extras (tox). Not run: cloud E2E and the live staging sdk-contract runner.

Cross-language impact

Classification: public facade and wire contract.

  • Requirement IDs and affected behavior: none yet. tests/sdk-contract has no requirement for reading or stopping from an application. A shared scenario belongs in the coordinated Hosting change.
  • Companion PRs: Ruby feat(durable): read and stop executions with application credentials volcano-sdk-ruby#366. JavaScript (getFromApplication, stopFromApplication) is on the same branch name in Kong/volcano-sdk-js; it had no PR when this one opened.
  • Canonical scenarios, bindings and active/staged copies: not changed here. Hosting owns them.
  • Public examples updated: README.md and docs/functions.md.
  • Native checks and shared acceptance evidence: native checks above. Shared acceptance has not run; Staging Validation runs it after the SDKs merge.

Coordination

Companion PRs, all on skeever/durable-execution-data-plane: Kong/volcano-sdk-js#357, #422, Kong/volcano-sdk-ruby#366.

Merge order:

  1. The server support, Kong/volcano-hosting#2364. Merged.
  2. These three SDK PRs.
  3. A hosting PR that adds the shared tests/sdk-contract scenarios for both routes.

🤖 Generated with Claude Code

Apply Kong/volcano-hosting#2364's change to the bundled public spec
(docs/public/api-reference/openapi.yaml, 82a97eb58c..6377c22be0) and
regenerate the client with scripts/generate_openapi.py. Only that delta is
taken; the rest of Hosting's drift stays with the automated sync PR.
Add durable.get_from_application and durable.stop_from_application. They
call the data-plane routes from Kong/volcano-hosting#2364 with the
credential start uses, and send the execution's client token as
X-Volcano-Client-Token when given. The owner-scoped get, list and stop
are unchanged.

Share the messages facade's header check through _durable_arguments,
and stop saying in docstrings and docs that reading or stopping needs
the owner's token.
Copy the start path, the two application execution paths, the
DurableClientToken parameter and the DurableExecution schema from Hosting
main 1ccff82a16's public bundle, and regenerate the client.

Hosting #2364 shipped scoped service keys: only a key holding `*` reaches
any execution, including a pending one; a key scoped without `*` needs the
execution's client token. Say so in the facade docstrings, README and
docs, and note that a ServerError from a stop can follow a stop already
sent. Pin that the client token goes out with every credential over the
wire, including a service key.
@swkeever
swkeever marked this pull request as ready for review October 29, 2026 21:02
@swkeever
swkeever requested a review from a team as a code owner October 29, 2026 21:02
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

AI review: no P0-P2 findings

Reviewed 8bbda67. Run log

This PR adds durable.get_from_application and durable.stop_from_application. They call the new data-plane read and stop routes with the same credential start() uses, and send client_token as X-Volcano-Client-Token when one is given. The client-token header check moves into _durable_arguments unchanged, the regenerated client and the copied OpenAPI sections are included, and the docs are updated. The change is additive and low risk. The earlier finding on private-function visibility errors is fixed: the docstring, README and docs now say a private function raises NotFoundError and only an anonymous key on an authenticated function raises PermissionDeniedError.

Not checked: The AI review read the diff, the code, and the PR's text; it ran no tests or builds and read nothing outside this repository.

  • Nobody has compared the copied openapi.yaml sections with the public bundle built from Hosting main 1ccff82a16, and that copy decides whether the routes, headers and status codes match the server.
  • Nobody has checked that the deployed Hosting routes enforce the documented client-token, scoped-key, visibility and pending-execution rules, or return the documented statuses.
  • The two new routes have not been run against a live server; the shared tests/sdk-contract scenarios for them are planned for a later Hosting PR.
  • Confirm the copied durable OpenAPI sections match Kong/volcano-hosting’s public bundle at 1ccff82a16.
  • Verify deployed Hosting routes enforce the documented ownership, visibility, client-token, scoped-service-key, and pending-execution rules.

Human review: needed

  • Should existing execution client tokens authorize result access and cancellation through the application routes exposed in src/volcano_sdk/durable.py and openapi/openapi.yaml?

The AI review found no P0-P2 problems at 8bbda67, so the code needs no second review for bugs. Decide the points above: approve to accept them, or request changes.

Suggested reviewer: subnetmarco (most recent commits to durable.py, the durable docs and openapi.yaml). Any collaborator with write access other than the author can approve.

@github-actions
github-actions Bot requested a review from subnetmarco October 29, 2026 21:05

@swkeever swkeever left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guided review of 5d3cdd4.

1. Should execution client tokens used for messaging also authorize result access and cancellation through the new methods?

Accept. Hosting's shipped contract (main at a525bc6477, from #2364) makes the client token one per-execution credential for reading, stopping, and messaging. DurableExecution.client_token in api/openapi.yaml says it lets a browser read the execution, stop it, and read and send its messages, and lets a scoped service key read and stop it; durable-functions.md and durable-messages.md say the same. The server enforces this, not the SDK: in internal/handlers/durable_application_access.go, the read, stop, and messages routes all clear the caller through clearDurableApplicationRequest and Target.Admits, and read and stop differ from messages only in requiring the token from a service key scoped without *. These methods only forward X-Volcano-Client-Token, and the copied spec sections are identical to Hosting's public bundle at a525bc6477.

Docstring fix (non-blocking): see the suggestion on get_from_application. "Anything else raises NotFoundError" leaves out the visibility check that docs/functions.md states. For example, an anon key with the token on an authenticated function raises PermissionDeniedError.

Parity fix (non-blocking follow-up): JS (Kong/volcano-sdk-js#357) exposes the throttled 503's Retry-After as error.retryAfter. Python and Ruby (Kong/volcano-sdk-ruby#366) read Retry-After only on a 429. Reading it on a 503 as well, in response_payload in _transport_response.py and in Ruby's Transport, would set ServerError.retry_after and close the gap.


Generated by the guided-review skill. Install it with one command:

sh -c 'd=$(mktemp -d) && gh repo clone Kong/volcano-conventions "$d" -- --depth 1 -q && make -C "$d" install-skills guided-review pr-review explain-pr; s=$?; rm -rf "$d"; exit $s'

Then ask your coding agent for a guided review of a PR or issue URL. Run the command again to update.

Comment thread src/volcano_sdk/durable.py Resolved
…ication reads

A visibility refusal raises PermissionDeniedError rather than NotFoundError,
and a service key scoped without * also needs functions.invoke.
Comment thread src/volcano_sdk/durable.py Resolved
Comment thread src/volcano_sdk/durable.py Resolved
A private function answers every credential but a service key with 404,
so it raises NotFoundError. Only a missing functions.invoke or an anonymous
key on an authenticated function raises PermissionDeniedError.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant