Skip to content

feat(durable): add wait_for_approval and the approvals client - #389

Merged
subnetmarco merged 19 commits into
mainfrom
feat/durable-approvals
Oct 8, 2026
Merged

subnetmarco merged 19 commits into
mainfrom
feat/durable-approvals

Conversation

@subnetmarco

@subnetmarco subnetmarco commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Change

Durable workflows can wait for a person's approval. Tracks https://github.com/Kong/volcano-hosting/pull/1678.

  • ctx.wait_for_approval(name, *, title, description=None, details=None, timeout=None) suspends the execution until a person approves or denies, or the timeout passes, and returns an ApprovalDecision. Registration runs inside the callback submitter, so a replay never registers twice; a timeout resolves to status="expired".
  • Registration retries network errors, 404, 429, 5xx and 409 approval_not_ready within one 30 s deadline. Each attempt is bounded as a whole (connect, headers, and body) by min(10 s, remaining). The request is validated against the 64 KiB body limit before the callback opens.
  • name must be printable ASCII of at most 237 characters, since it is the runtime's operation name, and NUL is refused anywhere in the text or details. details is copied when checked, so a later change by the caller cannot alter what was validated. A host lookup that stalls is bounded by its attempt's deadline.
  • client.durable.approvals: list, get, stats, approve, deny. A project access token is refused deciding with PermissionDeniedError.
  • The decision parser trusts only status; comment, decided_by, and decided_at read as empty when malformed, so a recorded decision never fails a replay. decided_at must be an RFC 3339 date-time, matching JavaScript.
  • The runtime wrapper is built once per process; each approval reads its execution from the runtime context.
  • OpenAPI operations synced from Hosting.

Verification

  • poe checks: pass. Policy, audit, generated client, ruff, mypy, basedpyright, 2615 tests at 100% line and branch coverage, package check, and all four tox environments.
  • test_realtime_presence_sync_coalesces_latest_backpressured_state is timing-sensitive under heavy load and also fails intermittently on main; it passed in the final run.

Cross-language impact

Classify this change: public facade / wire contract / shared behavior.

Merge order

  1. SDK releases: a maintainer merges and releases the JS SDK 1.16 (feat(durable): add waitForApproval and the approvals client volcano-sdk-js#315), Python SDK 0.14 (feat(durable): add wait_for_approval and the approvals client #389), and the Ruby SDK (feat(durable): list, read, and decide durable approvals volcano-sdk-ruby#341). Hosting's E2E fixtures install those versions from the registries. Until Hosting deploys, the new approval methods get 404.
  2. CLI (feat(durable): add durable approvals commands volcano-cli#282), with a one-time maintainer override of localmode-e2e. That suite needs a nightly local-mode image with approvals, which exists only once Hosting merges. Hosting's merge queue runs its CLI E2E against CLI main, so the CLI goes first.
  3. Hosting (https://github.com/Kong/volcano-hosting/pull/1678).
  4. Dashboard (https://github.com/Kong/volcano-web/pull/836, https://github.com/Kong/volcano-web/pull/837, then https://github.com/Kong/volcano-web/pull/832) and agent skills (docs(durable): teach agents to request and decide durable approvals volcano-skills#57).

subnetmarco and others added 12 commits October 6, 2026 17:40
Regenerates the internal client from the Hosting spec with the durable approval request, list, get, stats, approve, and deny operations, and requires the operations the SDK calls.

Co-authored-by: Cursor <cursoragent@cursor.com>
DurableContext.wait_for_approval registers an approval with Volcano from the runtime's callback submitter and suspends until a person decides. A denial or a timeout returns an ApprovalDecision rather than raising. @durable reads the execution reference from the invocation and passes it to child, map, and parallel contexts.

client.durable.approvals lists, reads, summarizes, approves, and denies approvals. HTTP 403 now raises PermissionDeniedError, a subclass of AuthenticationError.

Co-authored-by: Cursor <cursoragent@cursor.com>
Retry registration on network errors, timeouts, 404, 429, any 5xx, and
409 approval_not_ready, on the JS SDK's delay schedule within one
30-second deadline that also caps each attempt's timeout. A 409
approval_closed now counts as registered, so an approval whose timeout
passed first resumes with the expired decision instead of failing the
execution.

Refuse text that cannot be encoded as JSON and approvals over 64 KiB
(with room for the longest callback id) before the callback opens, and
send the exact bytes that were measured. Out-of-range approval timeouts
raise TypeError, as ctx.wait does.

Correct the guide: approvals have no name filter, and a refusal reaches
the handler as the runtime's CallbackSubmitterError.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…eniently

Co-authored-by: Cursor <cursoragent@cursor.com>
…from its context

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…avaScript SDK does

Co-authored-by: Cursor <cursoragent@cursor.com>
subnetmarco and others added 2 commits October 7, 2026 15:07
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

@subnetmarco subnetmarco left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed at 424d6f4. The core holds up against the runtime and Hosting:

  • Registration runs in the checkpointed submitter, and a retried POST is idempotent by callback id.
  • The runtime's own retry is disabled, and running out of the deadline fails the execution instead of hanging.
  • Only CallbackTimeoutError becomes expired, and PermissionDeniedError subclasses AuthenticationError.
  • The 64 KiB check measures the exact UTF-8 bytes sent, and the retry policy matches JS.

ruff, mypy, basedpyright, and the generated-client check pass. pytest gives 2626 passed and 1 failed, and the failure comes from FORCE_COLOR=0 in the local shell.

Release order: Hosting's E2E pins volcano-sdk-python~=0.14.0, so this needs to be released before Hosting merges (details on Kong/volcano-hosting#1678).

Comment thread src/volcano_sdk/_durable_approval_registration.py Outdated
Comment thread src/volcano_sdk/_durable_approval_registration.py Outdated
Comment thread src/volcano_sdk/_durable_approval_registration.py Outdated
Comment thread src/volcano_sdk/_durable_approval_registration.py
subnetmarco and others added 4 commits October 8, 2026 08:37
Co-authored-by: Cursor <cursoragent@cursor.com>
Names are the runtime's operation name, so they hold to printable ASCII and 237 characters. NUL is refused before the callback opens, details are copied when checked, and a stalled host lookup no longer outlasts its attempt.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	openapi/openapi.yaml
… wins

Co-authored-by: Cursor <cursoragent@cursor.com>
@subnetmarco
subnetmarco marked this pull request as ready for review October 8, 2026 19:05
@subnetmarco
subnetmarco requested a review from a team as a code owner October 8, 2026 19:05
@subnetmarco
subnetmarco added this pull request to the merge queue Oct 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 8, 2026
@subnetmarco
subnetmarco added this pull request to the merge queue Oct 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 8, 2026
@subnetmarco
subnetmarco added this pull request to the merge queue Oct 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 8, 2026
@subnetmarco
subnetmarco added this pull request to the merge queue Oct 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 8, 2026
@subnetmarco
subnetmarco added this pull request to the merge queue Oct 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 8, 2026
@subnetmarco
subnetmarco added this pull request to the merge queue Oct 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 8, 2026
A command normally arrives within a millisecond. The 0.2 s wait failed the
cancelled-unsubscribe tests whenever a loaded runner stalled the loop, which
made mutation stats collection fail on random modules.

Co-authored-by: Cursor <cursoragent@cursor.com>
@subnetmarco
subnetmarco enabled auto-merge October 8, 2026 23:25
@subnetmarco
subnetmarco added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 4a590b4 Oct 8, 2026
82 checks passed
@subnetmarco
subnetmarco deleted the feat/durable-approvals branch October 8, 2026 23:50
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