diff --git a/plugins/claude-code/skills/index.json b/plugins/claude-code/skills/index.json index f4fd4f5..818e12c 100644 --- a/plugins/claude-code/skills/index.json +++ b/plugins/claude-code/skills/index.json @@ -28,7 +28,7 @@ }, { "name": "volcano-durable", - "description": "Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, and durable function schedulers.", + "description": "Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, human approvals, polling, durable executions, idempotent starts, and durable function schedulers.", "path": "/skills/volcano-durable/SKILL.md" }, { @@ -73,7 +73,7 @@ }, { "name": "volcano-typescript", - "description": "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime and Durable types, Function invocation generics, OAuth providers, middleware types, and utility types.", + "description": "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime, Durable, and durable approval types, Function invocation generics, OAuth providers, middleware types, and utility types.", "path": "/skills/volcano-typescript/SKILL.md" } ] diff --git a/plugins/claude-code/skills/tests/skill-trigger-cases.json b/plugins/claude-code/skills/tests/skill-trigger-cases.json index 50b84de..7603439 100644 --- a/plugins/claude-code/skills/tests/skill-trigger-cases.json +++ b/plugins/claude-code/skills/tests/skill-trigger-cases.json @@ -139,6 +139,15 @@ { "skill": "volcano-durable", "signals": ["long-running", "workflow", "checkpointed", "waits", "Durable Functions"] } ] }, + { + "id": "durable-human-approval", + "prompt": "Build a refund workflow that pauses for a human approval before paying out using Volcano", + "expected": [ + { "skill": "volcano-sdk", "signals": ["Volcano"] }, + { "skill": "volcano-platform", "signals": ["Volcano"] }, + { "skill": "volcano-durable", "signals": ["human approval", "workflow"] } + ] + }, { "id": "project-log-reader", "prompt": "Build a server-side Volcano project logs reader with retained log search and pagination", diff --git a/plugins/claude-code/skills/volcano-durable/SKILL.md b/plugins/claude-code/skills/volcano-durable/SKILL.md index 66b0493..4261cc8 100644 --- a/plugins/claude-code/skills/volcano-durable/SKILL.md +++ b/plugins/claude-code/skills/volcano-durable/SKILL.md @@ -1,6 +1,6 @@ --- name: volcano-durable -description: Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, and durable function schedulers. +description: Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, human approvals, polling, durable executions, idempotent starts, and durable function schedulers. --- # Volcano Durable Functions Skill @@ -124,16 +124,129 @@ Python durable operations are synchronous. A step function receives its scope. | `ctx.step(name?, fn, options?)` | Run work and record its result. Retry policy and at-most-once behavior belong here. | | `ctx.wait(name?, duration)` | Suspend for at least one second without holding compute. | | `ctx.waitUntil(name?, check, options)` | Poll state until `options.until` passes. `initialState` is required. | +| `ctx.waitForApproval(name, options)` | Suspend until a person approves or denies, or the approval expires. Python takes keywords, not an options object: `ctx.wait_for_approval(name, *, title, description=None, details=None, timeout=None)`. | | `ctx.map(name?, items, fn, options?)` | Run one checkpointed child context per item. Set `concurrency` when required. | | `ctx.parallel(name?, branches, options?)` | Run independent checkpointed branches. | | `ctx.child(name?, fn)` | Group operations in a child context. | | `ctx.log` | Log with execution identifiers attached. | Each step attempt, wait, poll check, child, map item, and parallel branch uses a -durable operation. `durable get` shows the function's execution timeout and +durable operation, and each approval uses three. `durable get` shows the function's execution timeout and result retention. Read the plan limits documentation for operation allowance, operations per execution, and concurrency limits. +## Human approvals + +Use `waitForApproval` when a person must sign off before the workflow continues. +The execution suspends without holding compute and resumes with the decision. +Use `ctx.waitUntil` only for state your code can read, such as a payment +settling. Do not build your own approval table and poll it. + +```js +const { durable } = require('@volcano.dev/sdk/durable'); + +exports.handler = durable(async (input, ctx) => { + const quote = await ctx.step('quote', () => quoteShipping(input.order_id)); + + const decision = await ctx.waitForApproval('ship-order', { + title: `Ship order ${input.order_id}?`, + description: 'Express shipping is billed to the customer.', + details: { order_id: input.order_id, cost: quote.cost }, + timeout: '3d', + }); + if (!decision.approved) { + return { shipped: false, status: decision.status, comment: decision.comment }; + } + + await ctx.step('ship', () => ship(input.order_id)); + return { shipped: true, approved_by: decision.decidedBy?.email ?? null }; +}); +``` + +```python +from volcano_sdk.durable_authoring import durable + + +@durable +def handler(event, ctx): + order_id = event["order_id"] + quote = ctx.step("quote", lambda scope: quote_shipping(order_id)) + + decision = ctx.wait_for_approval( + "ship-order", + title=f"Ship order {order_id}?", + details={"order_id": order_id, "cost": quote["cost"]}, + timeout="3d", + ) + if not decision.approved: + return {"shipped": False, "status": decision.status} + + ctx.step("ship", lambda scope: ship(order_id)) + return {"shipped": True} +``` + +The decision has `approved`, `status`, `comment`, `decidedBy` (`{ id, email }` +or `null`), and `decidedAt`. Python uses attributes: `decision.decided_by` +(with `id` and `email`, or `None`) and `decision.decided_at`. + +| `status` | `approved` | Meaning | +|---|---|---| +| `approved` | `true` | A person approved, with an optional comment. | +| `denied` | `false` | A person denied, with an optional comment. | +| `expired` | `false` | `timeout` passed first. `comment` is empty; `decidedBy` and `decidedAt` are null. | + +A deny or an expiry is a return value, not an error. Branch on +`decision.approved` and handle `expired` explicitly. An approval whose execution +ends first (stopped, failed, or timed out) shows as `cancelled` to the owner; +the workflow never receives it. + +- `timeout` takes the same durations as `ctx.wait`. Without one, the approval + lasts until the execution's own timeout. +- Give each approval a short, stable, plain-ASCII name; it is the operation's + name in the execution's history, and the SDK refuses one longer than 237 + characters or outside printable ASCII. Replay returns the recorded decision + and never requests the approval twice. +- Approvals work inside `ctx.parallel`, `ctx.map`, and child contexts. An + execution can have at most 100 pending at once; the next request throws. +- Each approval is three durable operations: the approval, the wait for the + decision, and the step that registers the request. Inside `ctx.map` that + cost applies per item. +- Build `title` and `details` from input or step results. `title` holds up to + 200 characters, `description` 4000, and the whole request 64 KiB. +- `details` is shown to the person deciding and kept for a year. Do not put + secrets or credentials in it. + +### Who decides + +A person decides, in the dashboard under **Approvals**, or from the CLI. In +local mode the local user decides without logging in: + +```sh +volcano durable approvals list # pending, newest first +volcano durable approvals get +volcano durable approvals approve --comment "Checked stock" +volcano durable approvals deny --comment "Customer cancelled" +volcano durable approvals stats --since 30d +``` + +In cloud, the person runs `volcano login` and then the same commands under +`cloud`: + +```sh +volcano cloud durable approvals list +volcano cloud durable approvals approve --comment "Checked stock" +``` + +The SDK owner clients expose the same operations under +`durable.approvals`. Project access tokens can read approvals and stats but get +`403` on approve and deny. The MCP tools `list_durable_approvals` and +`get_durable_approval_stats` only read; MCP has no way to decide. + +Never approve or deny an approval yourself, including one your own workflow +requested during testing. List it, tell the user its ID and title, and let them +decide. Do not write code that decides approvals automatically; that defeats +the approval. + ## Start and manage executions from an application `start` accepts an application credential and returns an execution handle. An @@ -254,9 +367,8 @@ finish quickly while still suspending and replaying checkpoints. Instant waits cause more resumes per wall-clock minute than production, which helps expose a step that is unsafe to replay. Set `LOCAL_DURABLE_REAL_TIME=true` before `volcano start` when wait timing must be real. Local executions persist across -`volcano stop` and `volcano start`. Volcano does not expose externally completed -callbacks in local or cloud execution; use `ctx.waitUntil` to poll application -state instead. +`volcano stop` and `volcano start`. Approval timeouts always run in real time, +so test expiry locally with a short `timeout`. Local executions increment the same execution, operation, and compute counters as cloud executions. Inspect them through `GET /projects/{id}/usage`; the CLI @@ -328,6 +440,8 @@ start. `executions get` until terminal. - Deleting a durable function removes its execution history. Confirm before `durable delete`, `executions stop`, or `schedulers delete`. +- `approvals list`, `get`, and `stats` are safe to run. `approvals approve` and + `deny` are for the user alone; never run them. - Bound `logs --follow` with a timeout in agent-driven diagnostics. ## Troubleshooting @@ -343,6 +457,12 @@ cloud state. 5. A start during provisioning returns `409`; wait for `active`. 6. A `429` means a concurrency or durable allowance limit blocked the start. 7. A scheduler `403` can mean the project plan does not include schedulers. +8. An execution that stays `running` may be waiting on an approval. Check + `durable approvals list --execution `. +9. An approve or deny `403` means the credential is not a person's. A `409` + means the approval was already decided, expired, or cancelled. +10. `VOLCANO_PLATFORM_API_URL` is reserved. The platform sets it on durable + functions; a project variable with that name is rejected. ## Verification @@ -351,6 +471,7 @@ cloud state. - Run `volcano start` and `volcano durable deploy --all`. - Wait for local function status `active`. - Start one local execution with a unique idempotency name. +- If it waits on an approval, give the user its ID and ask them to decide. - Poll it to a terminal status and check its result. - Read runtime logs if the result is not `succeeded`. - After approved cloud deployment, repeat the execution check with @@ -359,6 +480,7 @@ cloud state. ## References - Hosting contract: `volcano-hosting/docs/public/functions/durable-functions.md` +- Approvals: `volcano-hosting/docs/public/functions/durable-approvals.md` - Local guide: `volcano-hosting/docs/public/guides/durable-functions-locally.md` - CLI contract: `volcano-cli/docs/durable-functions.md` - Command source: `volcano-cli/internal/cmd/durable/` diff --git a/plugins/claude-code/skills/volcano-sdk/SKILL.md b/plugins/claude-code/skills/volcano-sdk/SKILL.md index 9ac69f1..61270f7 100644 --- a/plugins/claude-code/skills/volcano-sdk/SKILL.md +++ b/plugins/claude-code/skills/volcano-sdk/SKILL.md @@ -46,7 +46,7 @@ Do NOT implement custom alternatives — no custom JWT auth, no ad-hoc database | User accounts or identity, email or password sign-up/sign-in, OAuth, sessions, anonymous users, password recovery, private or per-user data | `volcano-auth` | Auth application flows, session lifecycle, and common-error catalog | | Stored or persistent data, CRUD, records, todos, chat messages, polls, analytics, counters, click tracking, CMS content, feature flags, leaderboards, RLS | `volcano-database` | Query builder + every operator + RLS pattern + limitations (no joins / upserts / multi-statement tx) | | Volcano Functions, server-side or privileged logic, QR/PDF generators, secrets, outbound third-party APIs, orchestration, scheduled processing, file/image processing | `volcano-functions` | Invocation contract `{data, status, headers, version, error}`, Volcano Functions response shape, handler templates | -| Durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, execution schedulers | `volcano-durable` | Durable authoring contract, replay rules, cloud CLI lifecycle, execution status, schedulers, and safety | +| Durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, execution schedulers, human approvals or sign-off | `volcano-durable` | Durable authoring contract, replay rules, `waitForApproval`, cloud CLI lifecycle, execution status, schedulers, and safety | | Project logs, retained log search, pagination, activity buckets, structured log filters | `volcano-logs` | Project-token authentication, search cursors, filters, and activity counts | | Project locks, distributed leases, leader election, fencing tokens, backend worker coordination | `volcano-locks` | Renewable lock guards, direct lease control, fencing, and safe recovery | | Uploads, downloads, galleries, file sharing, buckets, paths, public/private files, visibility, resumable uploads | `volcano-storage` | Full storage API + access policies + resumable protocol + limits | diff --git a/plugins/claude-code/skills/volcano-typescript/SKILL.md b/plugins/claude-code/skills/volcano-typescript/SKILL.md index d2e34e9..31afbce 100644 --- a/plugins/claude-code/skills/volcano-typescript/SKILL.md +++ b/plugins/claude-code/skills/volcano-typescript/SKILL.md @@ -1,6 +1,6 @@ --- name: volcano-typescript -description: "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime and Durable types, Function invocation generics, OAuth providers, middleware types, and utility types." +description: "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime, Durable, and durable approval types, Function invocation generics, OAuth providers, middleware types, and utility types." --- # Volcano TypeScript Types Skill @@ -378,6 +378,32 @@ const execution: DurableExecution | undefined = data?.data[0]; `DurableExecution` uses the API's snake-case fields. Its `result` is `unknown`, so narrow it before use. +`ctx.waitForApproval` takes `WaitForApprovalOptions` and resolves with an +`ApprovalDecision`, both from the durable entry point: + +```ts +import type { ApprovalDecision, WaitForApprovalOptions } from '@volcano.dev/sdk/durable'; + +const request: WaitForApprovalOptions = { title: 'Ship order 4417?', timeout: '3d' }; +const decision: ApprovalDecision = await ctx.waitForApproval('ship-order', request); +// decision.status is 'approved' | 'denied' | 'expired'; decidedBy is null on expiry. +``` + +Reading and deciding approvals uses the main entry point's `DurableApproval`, +`DurableApprovalStatus`, `PaginatedDurableApprovals`, and `DurableApprovalStats`, +with `DurableApprovalListOptions` and `DurableApprovalStatsOptions` for filters: + +```ts +import type { DurableApproval, DurableApprovalListOptions } from '@volcano.dev/sdk'; + +const options: DurableApprovalListOptions = { status: 'pending', function: 'order-pipeline' }; +const { data } = await volcano.durable.approvals.list(projectId, options); +const approval: DurableApproval | undefined = data?.data[0]; +``` + +Like `DurableExecution`, `DurableApproval` uses snake-case fields +(`requested_at`, `expires_at`, `decision.decided_by`). + ## OAuth Types ```ts type OAuthProviderName = 'google' | 'github' | 'microsoft' | 'apple'; @@ -567,7 +593,7 @@ channel.onPostgresChanges('INSERT', 'public', 'posts', (change) => { | Storage | `StorageObject`, `StorageUploadResponse`, `StorageListResponse` | `storage.from(...)` operations | | Realtime | `PostgresChange`, `PresenceState`, `ConnectContext` | Channel callbacks | | Functions | `invoke(...)` generic params | Both ends of an invocation | -| Durable | `DurableHandler`, `DurableContext`, `DurableExecution` | Durable authoring and execution clients | +| Durable | `DurableHandler`, `DurableContext`, `DurableExecution`, `ApprovalDecision`, `DurableApproval` | Durable authoring, approvals, and execution clients | | OAuth | `OAuthProviderName`, `OAuthProvider` | Provider name validation | | Sessions | `AuthSession`, `SessionsResponse` | Multi-device session UI | | Middleware | `ServerClient`, `GetUserResult` | Next.js middleware/route handlers | diff --git a/plugins/claude-desktop/skills/index.json b/plugins/claude-desktop/skills/index.json index f4fd4f5..818e12c 100644 --- a/plugins/claude-desktop/skills/index.json +++ b/plugins/claude-desktop/skills/index.json @@ -28,7 +28,7 @@ }, { "name": "volcano-durable", - "description": "Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, and durable function schedulers.", + "description": "Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, human approvals, polling, durable executions, idempotent starts, and durable function schedulers.", "path": "/skills/volcano-durable/SKILL.md" }, { @@ -73,7 +73,7 @@ }, { "name": "volcano-typescript", - "description": "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime and Durable types, Function invocation generics, OAuth providers, middleware types, and utility types.", + "description": "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime, Durable, and durable approval types, Function invocation generics, OAuth providers, middleware types, and utility types.", "path": "/skills/volcano-typescript/SKILL.md" } ] diff --git a/plugins/claude-desktop/skills/tests/skill-trigger-cases.json b/plugins/claude-desktop/skills/tests/skill-trigger-cases.json index 50b84de..7603439 100644 --- a/plugins/claude-desktop/skills/tests/skill-trigger-cases.json +++ b/plugins/claude-desktop/skills/tests/skill-trigger-cases.json @@ -139,6 +139,15 @@ { "skill": "volcano-durable", "signals": ["long-running", "workflow", "checkpointed", "waits", "Durable Functions"] } ] }, + { + "id": "durable-human-approval", + "prompt": "Build a refund workflow that pauses for a human approval before paying out using Volcano", + "expected": [ + { "skill": "volcano-sdk", "signals": ["Volcano"] }, + { "skill": "volcano-platform", "signals": ["Volcano"] }, + { "skill": "volcano-durable", "signals": ["human approval", "workflow"] } + ] + }, { "id": "project-log-reader", "prompt": "Build a server-side Volcano project logs reader with retained log search and pagination", diff --git a/plugins/claude-desktop/skills/volcano-durable/SKILL.md b/plugins/claude-desktop/skills/volcano-durable/SKILL.md index 66b0493..4261cc8 100644 --- a/plugins/claude-desktop/skills/volcano-durable/SKILL.md +++ b/plugins/claude-desktop/skills/volcano-durable/SKILL.md @@ -1,6 +1,6 @@ --- name: volcano-durable -description: Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, and durable function schedulers. +description: Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, human approvals, polling, durable executions, idempotent starts, and durable function schedulers. --- # Volcano Durable Functions Skill @@ -124,16 +124,129 @@ Python durable operations are synchronous. A step function receives its scope. | `ctx.step(name?, fn, options?)` | Run work and record its result. Retry policy and at-most-once behavior belong here. | | `ctx.wait(name?, duration)` | Suspend for at least one second without holding compute. | | `ctx.waitUntil(name?, check, options)` | Poll state until `options.until` passes. `initialState` is required. | +| `ctx.waitForApproval(name, options)` | Suspend until a person approves or denies, or the approval expires. Python takes keywords, not an options object: `ctx.wait_for_approval(name, *, title, description=None, details=None, timeout=None)`. | | `ctx.map(name?, items, fn, options?)` | Run one checkpointed child context per item. Set `concurrency` when required. | | `ctx.parallel(name?, branches, options?)` | Run independent checkpointed branches. | | `ctx.child(name?, fn)` | Group operations in a child context. | | `ctx.log` | Log with execution identifiers attached. | Each step attempt, wait, poll check, child, map item, and parallel branch uses a -durable operation. `durable get` shows the function's execution timeout and +durable operation, and each approval uses three. `durable get` shows the function's execution timeout and result retention. Read the plan limits documentation for operation allowance, operations per execution, and concurrency limits. +## Human approvals + +Use `waitForApproval` when a person must sign off before the workflow continues. +The execution suspends without holding compute and resumes with the decision. +Use `ctx.waitUntil` only for state your code can read, such as a payment +settling. Do not build your own approval table and poll it. + +```js +const { durable } = require('@volcano.dev/sdk/durable'); + +exports.handler = durable(async (input, ctx) => { + const quote = await ctx.step('quote', () => quoteShipping(input.order_id)); + + const decision = await ctx.waitForApproval('ship-order', { + title: `Ship order ${input.order_id}?`, + description: 'Express shipping is billed to the customer.', + details: { order_id: input.order_id, cost: quote.cost }, + timeout: '3d', + }); + if (!decision.approved) { + return { shipped: false, status: decision.status, comment: decision.comment }; + } + + await ctx.step('ship', () => ship(input.order_id)); + return { shipped: true, approved_by: decision.decidedBy?.email ?? null }; +}); +``` + +```python +from volcano_sdk.durable_authoring import durable + + +@durable +def handler(event, ctx): + order_id = event["order_id"] + quote = ctx.step("quote", lambda scope: quote_shipping(order_id)) + + decision = ctx.wait_for_approval( + "ship-order", + title=f"Ship order {order_id}?", + details={"order_id": order_id, "cost": quote["cost"]}, + timeout="3d", + ) + if not decision.approved: + return {"shipped": False, "status": decision.status} + + ctx.step("ship", lambda scope: ship(order_id)) + return {"shipped": True} +``` + +The decision has `approved`, `status`, `comment`, `decidedBy` (`{ id, email }` +or `null`), and `decidedAt`. Python uses attributes: `decision.decided_by` +(with `id` and `email`, or `None`) and `decision.decided_at`. + +| `status` | `approved` | Meaning | +|---|---|---| +| `approved` | `true` | A person approved, with an optional comment. | +| `denied` | `false` | A person denied, with an optional comment. | +| `expired` | `false` | `timeout` passed first. `comment` is empty; `decidedBy` and `decidedAt` are null. | + +A deny or an expiry is a return value, not an error. Branch on +`decision.approved` and handle `expired` explicitly. An approval whose execution +ends first (stopped, failed, or timed out) shows as `cancelled` to the owner; +the workflow never receives it. + +- `timeout` takes the same durations as `ctx.wait`. Without one, the approval + lasts until the execution's own timeout. +- Give each approval a short, stable, plain-ASCII name; it is the operation's + name in the execution's history, and the SDK refuses one longer than 237 + characters or outside printable ASCII. Replay returns the recorded decision + and never requests the approval twice. +- Approvals work inside `ctx.parallel`, `ctx.map`, and child contexts. An + execution can have at most 100 pending at once; the next request throws. +- Each approval is three durable operations: the approval, the wait for the + decision, and the step that registers the request. Inside `ctx.map` that + cost applies per item. +- Build `title` and `details` from input or step results. `title` holds up to + 200 characters, `description` 4000, and the whole request 64 KiB. +- `details` is shown to the person deciding and kept for a year. Do not put + secrets or credentials in it. + +### Who decides + +A person decides, in the dashboard under **Approvals**, or from the CLI. In +local mode the local user decides without logging in: + +```sh +volcano durable approvals list # pending, newest first +volcano durable approvals get +volcano durable approvals approve --comment "Checked stock" +volcano durable approvals deny --comment "Customer cancelled" +volcano durable approvals stats --since 30d +``` + +In cloud, the person runs `volcano login` and then the same commands under +`cloud`: + +```sh +volcano cloud durable approvals list +volcano cloud durable approvals approve --comment "Checked stock" +``` + +The SDK owner clients expose the same operations under +`durable.approvals`. Project access tokens can read approvals and stats but get +`403` on approve and deny. The MCP tools `list_durable_approvals` and +`get_durable_approval_stats` only read; MCP has no way to decide. + +Never approve or deny an approval yourself, including one your own workflow +requested during testing. List it, tell the user its ID and title, and let them +decide. Do not write code that decides approvals automatically; that defeats +the approval. + ## Start and manage executions from an application `start` accepts an application credential and returns an execution handle. An @@ -254,9 +367,8 @@ finish quickly while still suspending and replaying checkpoints. Instant waits cause more resumes per wall-clock minute than production, which helps expose a step that is unsafe to replay. Set `LOCAL_DURABLE_REAL_TIME=true` before `volcano start` when wait timing must be real. Local executions persist across -`volcano stop` and `volcano start`. Volcano does not expose externally completed -callbacks in local or cloud execution; use `ctx.waitUntil` to poll application -state instead. +`volcano stop` and `volcano start`. Approval timeouts always run in real time, +so test expiry locally with a short `timeout`. Local executions increment the same execution, operation, and compute counters as cloud executions. Inspect them through `GET /projects/{id}/usage`; the CLI @@ -328,6 +440,8 @@ start. `executions get` until terminal. - Deleting a durable function removes its execution history. Confirm before `durable delete`, `executions stop`, or `schedulers delete`. +- `approvals list`, `get`, and `stats` are safe to run. `approvals approve` and + `deny` are for the user alone; never run them. - Bound `logs --follow` with a timeout in agent-driven diagnostics. ## Troubleshooting @@ -343,6 +457,12 @@ cloud state. 5. A start during provisioning returns `409`; wait for `active`. 6. A `429` means a concurrency or durable allowance limit blocked the start. 7. A scheduler `403` can mean the project plan does not include schedulers. +8. An execution that stays `running` may be waiting on an approval. Check + `durable approvals list --execution `. +9. An approve or deny `403` means the credential is not a person's. A `409` + means the approval was already decided, expired, or cancelled. +10. `VOLCANO_PLATFORM_API_URL` is reserved. The platform sets it on durable + functions; a project variable with that name is rejected. ## Verification @@ -351,6 +471,7 @@ cloud state. - Run `volcano start` and `volcano durable deploy --all`. - Wait for local function status `active`. - Start one local execution with a unique idempotency name. +- If it waits on an approval, give the user its ID and ask them to decide. - Poll it to a terminal status and check its result. - Read runtime logs if the result is not `succeeded`. - After approved cloud deployment, repeat the execution check with @@ -359,6 +480,7 @@ cloud state. ## References - Hosting contract: `volcano-hosting/docs/public/functions/durable-functions.md` +- Approvals: `volcano-hosting/docs/public/functions/durable-approvals.md` - Local guide: `volcano-hosting/docs/public/guides/durable-functions-locally.md` - CLI contract: `volcano-cli/docs/durable-functions.md` - Command source: `volcano-cli/internal/cmd/durable/` diff --git a/plugins/claude-desktop/skills/volcano-sdk/SKILL.md b/plugins/claude-desktop/skills/volcano-sdk/SKILL.md index 9ac69f1..61270f7 100644 --- a/plugins/claude-desktop/skills/volcano-sdk/SKILL.md +++ b/plugins/claude-desktop/skills/volcano-sdk/SKILL.md @@ -46,7 +46,7 @@ Do NOT implement custom alternatives — no custom JWT auth, no ad-hoc database | User accounts or identity, email or password sign-up/sign-in, OAuth, sessions, anonymous users, password recovery, private or per-user data | `volcano-auth` | Auth application flows, session lifecycle, and common-error catalog | | Stored or persistent data, CRUD, records, todos, chat messages, polls, analytics, counters, click tracking, CMS content, feature flags, leaderboards, RLS | `volcano-database` | Query builder + every operator + RLS pattern + limitations (no joins / upserts / multi-statement tx) | | Volcano Functions, server-side or privileged logic, QR/PDF generators, secrets, outbound third-party APIs, orchestration, scheduled processing, file/image processing | `volcano-functions` | Invocation contract `{data, status, headers, version, error}`, Volcano Functions response shape, handler templates | -| Durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, execution schedulers | `volcano-durable` | Durable authoring contract, replay rules, cloud CLI lifecycle, execution status, schedulers, and safety | +| Durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, execution schedulers, human approvals or sign-off | `volcano-durable` | Durable authoring contract, replay rules, `waitForApproval`, cloud CLI lifecycle, execution status, schedulers, and safety | | Project logs, retained log search, pagination, activity buckets, structured log filters | `volcano-logs` | Project-token authentication, search cursors, filters, and activity counts | | Project locks, distributed leases, leader election, fencing tokens, backend worker coordination | `volcano-locks` | Renewable lock guards, direct lease control, fencing, and safe recovery | | Uploads, downloads, galleries, file sharing, buckets, paths, public/private files, visibility, resumable uploads | `volcano-storage` | Full storage API + access policies + resumable protocol + limits | diff --git a/plugins/claude-desktop/skills/volcano-typescript/SKILL.md b/plugins/claude-desktop/skills/volcano-typescript/SKILL.md index d2e34e9..31afbce 100644 --- a/plugins/claude-desktop/skills/volcano-typescript/SKILL.md +++ b/plugins/claude-desktop/skills/volcano-typescript/SKILL.md @@ -1,6 +1,6 @@ --- name: volcano-typescript -description: "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime and Durable types, Function invocation generics, OAuth providers, middleware types, and utility types." +description: "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime, Durable, and durable approval types, Function invocation generics, OAuth providers, middleware types, and utility types." --- # Volcano TypeScript Types Skill @@ -378,6 +378,32 @@ const execution: DurableExecution | undefined = data?.data[0]; `DurableExecution` uses the API's snake-case fields. Its `result` is `unknown`, so narrow it before use. +`ctx.waitForApproval` takes `WaitForApprovalOptions` and resolves with an +`ApprovalDecision`, both from the durable entry point: + +```ts +import type { ApprovalDecision, WaitForApprovalOptions } from '@volcano.dev/sdk/durable'; + +const request: WaitForApprovalOptions = { title: 'Ship order 4417?', timeout: '3d' }; +const decision: ApprovalDecision = await ctx.waitForApproval('ship-order', request); +// decision.status is 'approved' | 'denied' | 'expired'; decidedBy is null on expiry. +``` + +Reading and deciding approvals uses the main entry point's `DurableApproval`, +`DurableApprovalStatus`, `PaginatedDurableApprovals`, and `DurableApprovalStats`, +with `DurableApprovalListOptions` and `DurableApprovalStatsOptions` for filters: + +```ts +import type { DurableApproval, DurableApprovalListOptions } from '@volcano.dev/sdk'; + +const options: DurableApprovalListOptions = { status: 'pending', function: 'order-pipeline' }; +const { data } = await volcano.durable.approvals.list(projectId, options); +const approval: DurableApproval | undefined = data?.data[0]; +``` + +Like `DurableExecution`, `DurableApproval` uses snake-case fields +(`requested_at`, `expires_at`, `decision.decided_by`). + ## OAuth Types ```ts type OAuthProviderName = 'google' | 'github' | 'microsoft' | 'apple'; @@ -567,7 +593,7 @@ channel.onPostgresChanges('INSERT', 'public', 'posts', (change) => { | Storage | `StorageObject`, `StorageUploadResponse`, `StorageListResponse` | `storage.from(...)` operations | | Realtime | `PostgresChange`, `PresenceState`, `ConnectContext` | Channel callbacks | | Functions | `invoke(...)` generic params | Both ends of an invocation | -| Durable | `DurableHandler`, `DurableContext`, `DurableExecution` | Durable authoring and execution clients | +| Durable | `DurableHandler`, `DurableContext`, `DurableExecution`, `ApprovalDecision`, `DurableApproval` | Durable authoring, approvals, and execution clients | | OAuth | `OAuthProviderName`, `OAuthProvider` | Provider name validation | | Sessions | `AuthSession`, `SessionsResponse` | Multi-device session UI | | Middleware | `ServerClient`, `GetUserResult` | Next.js middleware/route handlers | diff --git a/plugins/codex/skills/index.json b/plugins/codex/skills/index.json index f4fd4f5..818e12c 100644 --- a/plugins/codex/skills/index.json +++ b/plugins/codex/skills/index.json @@ -28,7 +28,7 @@ }, { "name": "volcano-durable", - "description": "Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, and durable function schedulers.", + "description": "Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, human approvals, polling, durable executions, idempotent starts, and durable function schedulers.", "path": "/skills/volcano-durable/SKILL.md" }, { @@ -73,7 +73,7 @@ }, { "name": "volcano-typescript", - "description": "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime and Durable types, Function invocation generics, OAuth providers, middleware types, and utility types.", + "description": "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime, Durable, and durable approval types, Function invocation generics, OAuth providers, middleware types, and utility types.", "path": "/skills/volcano-typescript/SKILL.md" } ] diff --git a/plugins/codex/skills/tests/skill-trigger-cases.json b/plugins/codex/skills/tests/skill-trigger-cases.json index 50b84de..7603439 100644 --- a/plugins/codex/skills/tests/skill-trigger-cases.json +++ b/plugins/codex/skills/tests/skill-trigger-cases.json @@ -139,6 +139,15 @@ { "skill": "volcano-durable", "signals": ["long-running", "workflow", "checkpointed", "waits", "Durable Functions"] } ] }, + { + "id": "durable-human-approval", + "prompt": "Build a refund workflow that pauses for a human approval before paying out using Volcano", + "expected": [ + { "skill": "volcano-sdk", "signals": ["Volcano"] }, + { "skill": "volcano-platform", "signals": ["Volcano"] }, + { "skill": "volcano-durable", "signals": ["human approval", "workflow"] } + ] + }, { "id": "project-log-reader", "prompt": "Build a server-side Volcano project logs reader with retained log search and pagination", diff --git a/plugins/codex/skills/volcano-durable/SKILL.md b/plugins/codex/skills/volcano-durable/SKILL.md index 66b0493..4261cc8 100644 --- a/plugins/codex/skills/volcano-durable/SKILL.md +++ b/plugins/codex/skills/volcano-durable/SKILL.md @@ -1,6 +1,6 @@ --- name: volcano-durable -description: Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, and durable function schedulers. +description: Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, human approvals, polling, durable executions, idempotent starts, and durable function schedulers. --- # Volcano Durable Functions Skill @@ -124,16 +124,129 @@ Python durable operations are synchronous. A step function receives its scope. | `ctx.step(name?, fn, options?)` | Run work and record its result. Retry policy and at-most-once behavior belong here. | | `ctx.wait(name?, duration)` | Suspend for at least one second without holding compute. | | `ctx.waitUntil(name?, check, options)` | Poll state until `options.until` passes. `initialState` is required. | +| `ctx.waitForApproval(name, options)` | Suspend until a person approves or denies, or the approval expires. Python takes keywords, not an options object: `ctx.wait_for_approval(name, *, title, description=None, details=None, timeout=None)`. | | `ctx.map(name?, items, fn, options?)` | Run one checkpointed child context per item. Set `concurrency` when required. | | `ctx.parallel(name?, branches, options?)` | Run independent checkpointed branches. | | `ctx.child(name?, fn)` | Group operations in a child context. | | `ctx.log` | Log with execution identifiers attached. | Each step attempt, wait, poll check, child, map item, and parallel branch uses a -durable operation. `durable get` shows the function's execution timeout and +durable operation, and each approval uses three. `durable get` shows the function's execution timeout and result retention. Read the plan limits documentation for operation allowance, operations per execution, and concurrency limits. +## Human approvals + +Use `waitForApproval` when a person must sign off before the workflow continues. +The execution suspends without holding compute and resumes with the decision. +Use `ctx.waitUntil` only for state your code can read, such as a payment +settling. Do not build your own approval table and poll it. + +```js +const { durable } = require('@volcano.dev/sdk/durable'); + +exports.handler = durable(async (input, ctx) => { + const quote = await ctx.step('quote', () => quoteShipping(input.order_id)); + + const decision = await ctx.waitForApproval('ship-order', { + title: `Ship order ${input.order_id}?`, + description: 'Express shipping is billed to the customer.', + details: { order_id: input.order_id, cost: quote.cost }, + timeout: '3d', + }); + if (!decision.approved) { + return { shipped: false, status: decision.status, comment: decision.comment }; + } + + await ctx.step('ship', () => ship(input.order_id)); + return { shipped: true, approved_by: decision.decidedBy?.email ?? null }; +}); +``` + +```python +from volcano_sdk.durable_authoring import durable + + +@durable +def handler(event, ctx): + order_id = event["order_id"] + quote = ctx.step("quote", lambda scope: quote_shipping(order_id)) + + decision = ctx.wait_for_approval( + "ship-order", + title=f"Ship order {order_id}?", + details={"order_id": order_id, "cost": quote["cost"]}, + timeout="3d", + ) + if not decision.approved: + return {"shipped": False, "status": decision.status} + + ctx.step("ship", lambda scope: ship(order_id)) + return {"shipped": True} +``` + +The decision has `approved`, `status`, `comment`, `decidedBy` (`{ id, email }` +or `null`), and `decidedAt`. Python uses attributes: `decision.decided_by` +(with `id` and `email`, or `None`) and `decision.decided_at`. + +| `status` | `approved` | Meaning | +|---|---|---| +| `approved` | `true` | A person approved, with an optional comment. | +| `denied` | `false` | A person denied, with an optional comment. | +| `expired` | `false` | `timeout` passed first. `comment` is empty; `decidedBy` and `decidedAt` are null. | + +A deny or an expiry is a return value, not an error. Branch on +`decision.approved` and handle `expired` explicitly. An approval whose execution +ends first (stopped, failed, or timed out) shows as `cancelled` to the owner; +the workflow never receives it. + +- `timeout` takes the same durations as `ctx.wait`. Without one, the approval + lasts until the execution's own timeout. +- Give each approval a short, stable, plain-ASCII name; it is the operation's + name in the execution's history, and the SDK refuses one longer than 237 + characters or outside printable ASCII. Replay returns the recorded decision + and never requests the approval twice. +- Approvals work inside `ctx.parallel`, `ctx.map`, and child contexts. An + execution can have at most 100 pending at once; the next request throws. +- Each approval is three durable operations: the approval, the wait for the + decision, and the step that registers the request. Inside `ctx.map` that + cost applies per item. +- Build `title` and `details` from input or step results. `title` holds up to + 200 characters, `description` 4000, and the whole request 64 KiB. +- `details` is shown to the person deciding and kept for a year. Do not put + secrets or credentials in it. + +### Who decides + +A person decides, in the dashboard under **Approvals**, or from the CLI. In +local mode the local user decides without logging in: + +```sh +volcano durable approvals list # pending, newest first +volcano durable approvals get +volcano durable approvals approve --comment "Checked stock" +volcano durable approvals deny --comment "Customer cancelled" +volcano durable approvals stats --since 30d +``` + +In cloud, the person runs `volcano login` and then the same commands under +`cloud`: + +```sh +volcano cloud durable approvals list +volcano cloud durable approvals approve --comment "Checked stock" +``` + +The SDK owner clients expose the same operations under +`durable.approvals`. Project access tokens can read approvals and stats but get +`403` on approve and deny. The MCP tools `list_durable_approvals` and +`get_durable_approval_stats` only read; MCP has no way to decide. + +Never approve or deny an approval yourself, including one your own workflow +requested during testing. List it, tell the user its ID and title, and let them +decide. Do not write code that decides approvals automatically; that defeats +the approval. + ## Start and manage executions from an application `start` accepts an application credential and returns an execution handle. An @@ -254,9 +367,8 @@ finish quickly while still suspending and replaying checkpoints. Instant waits cause more resumes per wall-clock minute than production, which helps expose a step that is unsafe to replay. Set `LOCAL_DURABLE_REAL_TIME=true` before `volcano start` when wait timing must be real. Local executions persist across -`volcano stop` and `volcano start`. Volcano does not expose externally completed -callbacks in local or cloud execution; use `ctx.waitUntil` to poll application -state instead. +`volcano stop` and `volcano start`. Approval timeouts always run in real time, +so test expiry locally with a short `timeout`. Local executions increment the same execution, operation, and compute counters as cloud executions. Inspect them through `GET /projects/{id}/usage`; the CLI @@ -328,6 +440,8 @@ start. `executions get` until terminal. - Deleting a durable function removes its execution history. Confirm before `durable delete`, `executions stop`, or `schedulers delete`. +- `approvals list`, `get`, and `stats` are safe to run. `approvals approve` and + `deny` are for the user alone; never run them. - Bound `logs --follow` with a timeout in agent-driven diagnostics. ## Troubleshooting @@ -343,6 +457,12 @@ cloud state. 5. A start during provisioning returns `409`; wait for `active`. 6. A `429` means a concurrency or durable allowance limit blocked the start. 7. A scheduler `403` can mean the project plan does not include schedulers. +8. An execution that stays `running` may be waiting on an approval. Check + `durable approvals list --execution `. +9. An approve or deny `403` means the credential is not a person's. A `409` + means the approval was already decided, expired, or cancelled. +10. `VOLCANO_PLATFORM_API_URL` is reserved. The platform sets it on durable + functions; a project variable with that name is rejected. ## Verification @@ -351,6 +471,7 @@ cloud state. - Run `volcano start` and `volcano durable deploy --all`. - Wait for local function status `active`. - Start one local execution with a unique idempotency name. +- If it waits on an approval, give the user its ID and ask them to decide. - Poll it to a terminal status and check its result. - Read runtime logs if the result is not `succeeded`. - After approved cloud deployment, repeat the execution check with @@ -359,6 +480,7 @@ cloud state. ## References - Hosting contract: `volcano-hosting/docs/public/functions/durable-functions.md` +- Approvals: `volcano-hosting/docs/public/functions/durable-approvals.md` - Local guide: `volcano-hosting/docs/public/guides/durable-functions-locally.md` - CLI contract: `volcano-cli/docs/durable-functions.md` - Command source: `volcano-cli/internal/cmd/durable/` diff --git a/plugins/codex/skills/volcano-sdk/SKILL.md b/plugins/codex/skills/volcano-sdk/SKILL.md index 9ac69f1..61270f7 100644 --- a/plugins/codex/skills/volcano-sdk/SKILL.md +++ b/plugins/codex/skills/volcano-sdk/SKILL.md @@ -46,7 +46,7 @@ Do NOT implement custom alternatives — no custom JWT auth, no ad-hoc database | User accounts or identity, email or password sign-up/sign-in, OAuth, sessions, anonymous users, password recovery, private or per-user data | `volcano-auth` | Auth application flows, session lifecycle, and common-error catalog | | Stored or persistent data, CRUD, records, todos, chat messages, polls, analytics, counters, click tracking, CMS content, feature flags, leaderboards, RLS | `volcano-database` | Query builder + every operator + RLS pattern + limitations (no joins / upserts / multi-statement tx) | | Volcano Functions, server-side or privileged logic, QR/PDF generators, secrets, outbound third-party APIs, orchestration, scheduled processing, file/image processing | `volcano-functions` | Invocation contract `{data, status, headers, version, error}`, Volcano Functions response shape, handler templates | -| Durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, execution schedulers | `volcano-durable` | Durable authoring contract, replay rules, cloud CLI lifecycle, execution status, schedulers, and safety | +| Durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, execution schedulers, human approvals or sign-off | `volcano-durable` | Durable authoring contract, replay rules, `waitForApproval`, cloud CLI lifecycle, execution status, schedulers, and safety | | Project logs, retained log search, pagination, activity buckets, structured log filters | `volcano-logs` | Project-token authentication, search cursors, filters, and activity counts | | Project locks, distributed leases, leader election, fencing tokens, backend worker coordination | `volcano-locks` | Renewable lock guards, direct lease control, fencing, and safe recovery | | Uploads, downloads, galleries, file sharing, buckets, paths, public/private files, visibility, resumable uploads | `volcano-storage` | Full storage API + access policies + resumable protocol + limits | diff --git a/plugins/codex/skills/volcano-typescript/SKILL.md b/plugins/codex/skills/volcano-typescript/SKILL.md index d2e34e9..31afbce 100644 --- a/plugins/codex/skills/volcano-typescript/SKILL.md +++ b/plugins/codex/skills/volcano-typescript/SKILL.md @@ -1,6 +1,6 @@ --- name: volcano-typescript -description: "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime and Durable types, Function invocation generics, OAuth providers, middleware types, and utility types." +description: "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime, Durable, and durable approval types, Function invocation generics, OAuth providers, middleware types, and utility types." --- # Volcano TypeScript Types Skill @@ -378,6 +378,32 @@ const execution: DurableExecution | undefined = data?.data[0]; `DurableExecution` uses the API's snake-case fields. Its `result` is `unknown`, so narrow it before use. +`ctx.waitForApproval` takes `WaitForApprovalOptions` and resolves with an +`ApprovalDecision`, both from the durable entry point: + +```ts +import type { ApprovalDecision, WaitForApprovalOptions } from '@volcano.dev/sdk/durable'; + +const request: WaitForApprovalOptions = { title: 'Ship order 4417?', timeout: '3d' }; +const decision: ApprovalDecision = await ctx.waitForApproval('ship-order', request); +// decision.status is 'approved' | 'denied' | 'expired'; decidedBy is null on expiry. +``` + +Reading and deciding approvals uses the main entry point's `DurableApproval`, +`DurableApprovalStatus`, `PaginatedDurableApprovals`, and `DurableApprovalStats`, +with `DurableApprovalListOptions` and `DurableApprovalStatsOptions` for filters: + +```ts +import type { DurableApproval, DurableApprovalListOptions } from '@volcano.dev/sdk'; + +const options: DurableApprovalListOptions = { status: 'pending', function: 'order-pipeline' }; +const { data } = await volcano.durable.approvals.list(projectId, options); +const approval: DurableApproval | undefined = data?.data[0]; +``` + +Like `DurableExecution`, `DurableApproval` uses snake-case fields +(`requested_at`, `expires_at`, `decision.decided_by`). + ## OAuth Types ```ts type OAuthProviderName = 'google' | 'github' | 'microsoft' | 'apple'; @@ -567,7 +593,7 @@ channel.onPostgresChanges('INSERT', 'public', 'posts', (change) => { | Storage | `StorageObject`, `StorageUploadResponse`, `StorageListResponse` | `storage.from(...)` operations | | Realtime | `PostgresChange`, `PresenceState`, `ConnectContext` | Channel callbacks | | Functions | `invoke(...)` generic params | Both ends of an invocation | -| Durable | `DurableHandler`, `DurableContext`, `DurableExecution` | Durable authoring and execution clients | +| Durable | `DurableHandler`, `DurableContext`, `DurableExecution`, `ApprovalDecision`, `DurableApproval` | Durable authoring, approvals, and execution clients | | OAuth | `OAuthProviderName`, `OAuthProvider` | Provider name validation | | Sessions | `AuthSession`, `SessionsResponse` | Multi-device session UI | | Middleware | `ServerClient`, `GetUserResult` | Next.js middleware/route handlers | diff --git a/plugins/cursor/skills/index.json b/plugins/cursor/skills/index.json index f4fd4f5..818e12c 100644 --- a/plugins/cursor/skills/index.json +++ b/plugins/cursor/skills/index.json @@ -28,7 +28,7 @@ }, { "name": "volcano-durable", - "description": "Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, and durable function schedulers.", + "description": "Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, human approvals, polling, durable executions, idempotent starts, and durable function schedulers.", "path": "/skills/volcano-durable/SKILL.md" }, { @@ -73,7 +73,7 @@ }, { "name": "volcano-typescript", - "description": "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime and Durable types, Function invocation generics, OAuth providers, middleware types, and utility types.", + "description": "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime, Durable, and durable approval types, Function invocation generics, OAuth providers, middleware types, and utility types.", "path": "/skills/volcano-typescript/SKILL.md" } ] diff --git a/plugins/cursor/skills/tests/skill-trigger-cases.json b/plugins/cursor/skills/tests/skill-trigger-cases.json index 50b84de..7603439 100644 --- a/plugins/cursor/skills/tests/skill-trigger-cases.json +++ b/plugins/cursor/skills/tests/skill-trigger-cases.json @@ -139,6 +139,15 @@ { "skill": "volcano-durable", "signals": ["long-running", "workflow", "checkpointed", "waits", "Durable Functions"] } ] }, + { + "id": "durable-human-approval", + "prompt": "Build a refund workflow that pauses for a human approval before paying out using Volcano", + "expected": [ + { "skill": "volcano-sdk", "signals": ["Volcano"] }, + { "skill": "volcano-platform", "signals": ["Volcano"] }, + { "skill": "volcano-durable", "signals": ["human approval", "workflow"] } + ] + }, { "id": "project-log-reader", "prompt": "Build a server-side Volcano project logs reader with retained log search and pagination", diff --git a/plugins/cursor/skills/volcano-durable/SKILL.md b/plugins/cursor/skills/volcano-durable/SKILL.md index 66b0493..4261cc8 100644 --- a/plugins/cursor/skills/volcano-durable/SKILL.md +++ b/plugins/cursor/skills/volcano-durable/SKILL.md @@ -1,6 +1,6 @@ --- name: volcano-durable -description: Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, and durable function schedulers. +description: Use for Volcano durable functions, long-running or resumable workflows, checkpointed steps, waits, human approvals, polling, durable executions, idempotent starts, and durable function schedulers. --- # Volcano Durable Functions Skill @@ -124,16 +124,129 @@ Python durable operations are synchronous. A step function receives its scope. | `ctx.step(name?, fn, options?)` | Run work and record its result. Retry policy and at-most-once behavior belong here. | | `ctx.wait(name?, duration)` | Suspend for at least one second without holding compute. | | `ctx.waitUntil(name?, check, options)` | Poll state until `options.until` passes. `initialState` is required. | +| `ctx.waitForApproval(name, options)` | Suspend until a person approves or denies, or the approval expires. Python takes keywords, not an options object: `ctx.wait_for_approval(name, *, title, description=None, details=None, timeout=None)`. | | `ctx.map(name?, items, fn, options?)` | Run one checkpointed child context per item. Set `concurrency` when required. | | `ctx.parallel(name?, branches, options?)` | Run independent checkpointed branches. | | `ctx.child(name?, fn)` | Group operations in a child context. | | `ctx.log` | Log with execution identifiers attached. | Each step attempt, wait, poll check, child, map item, and parallel branch uses a -durable operation. `durable get` shows the function's execution timeout and +durable operation, and each approval uses three. `durable get` shows the function's execution timeout and result retention. Read the plan limits documentation for operation allowance, operations per execution, and concurrency limits. +## Human approvals + +Use `waitForApproval` when a person must sign off before the workflow continues. +The execution suspends without holding compute and resumes with the decision. +Use `ctx.waitUntil` only for state your code can read, such as a payment +settling. Do not build your own approval table and poll it. + +```js +const { durable } = require('@volcano.dev/sdk/durable'); + +exports.handler = durable(async (input, ctx) => { + const quote = await ctx.step('quote', () => quoteShipping(input.order_id)); + + const decision = await ctx.waitForApproval('ship-order', { + title: `Ship order ${input.order_id}?`, + description: 'Express shipping is billed to the customer.', + details: { order_id: input.order_id, cost: quote.cost }, + timeout: '3d', + }); + if (!decision.approved) { + return { shipped: false, status: decision.status, comment: decision.comment }; + } + + await ctx.step('ship', () => ship(input.order_id)); + return { shipped: true, approved_by: decision.decidedBy?.email ?? null }; +}); +``` + +```python +from volcano_sdk.durable_authoring import durable + + +@durable +def handler(event, ctx): + order_id = event["order_id"] + quote = ctx.step("quote", lambda scope: quote_shipping(order_id)) + + decision = ctx.wait_for_approval( + "ship-order", + title=f"Ship order {order_id}?", + details={"order_id": order_id, "cost": quote["cost"]}, + timeout="3d", + ) + if not decision.approved: + return {"shipped": False, "status": decision.status} + + ctx.step("ship", lambda scope: ship(order_id)) + return {"shipped": True} +``` + +The decision has `approved`, `status`, `comment`, `decidedBy` (`{ id, email }` +or `null`), and `decidedAt`. Python uses attributes: `decision.decided_by` +(with `id` and `email`, or `None`) and `decision.decided_at`. + +| `status` | `approved` | Meaning | +|---|---|---| +| `approved` | `true` | A person approved, with an optional comment. | +| `denied` | `false` | A person denied, with an optional comment. | +| `expired` | `false` | `timeout` passed first. `comment` is empty; `decidedBy` and `decidedAt` are null. | + +A deny or an expiry is a return value, not an error. Branch on +`decision.approved` and handle `expired` explicitly. An approval whose execution +ends first (stopped, failed, or timed out) shows as `cancelled` to the owner; +the workflow never receives it. + +- `timeout` takes the same durations as `ctx.wait`. Without one, the approval + lasts until the execution's own timeout. +- Give each approval a short, stable, plain-ASCII name; it is the operation's + name in the execution's history, and the SDK refuses one longer than 237 + characters or outside printable ASCII. Replay returns the recorded decision + and never requests the approval twice. +- Approvals work inside `ctx.parallel`, `ctx.map`, and child contexts. An + execution can have at most 100 pending at once; the next request throws. +- Each approval is three durable operations: the approval, the wait for the + decision, and the step that registers the request. Inside `ctx.map` that + cost applies per item. +- Build `title` and `details` from input or step results. `title` holds up to + 200 characters, `description` 4000, and the whole request 64 KiB. +- `details` is shown to the person deciding and kept for a year. Do not put + secrets or credentials in it. + +### Who decides + +A person decides, in the dashboard under **Approvals**, or from the CLI. In +local mode the local user decides without logging in: + +```sh +volcano durable approvals list # pending, newest first +volcano durable approvals get +volcano durable approvals approve --comment "Checked stock" +volcano durable approvals deny --comment "Customer cancelled" +volcano durable approvals stats --since 30d +``` + +In cloud, the person runs `volcano login` and then the same commands under +`cloud`: + +```sh +volcano cloud durable approvals list +volcano cloud durable approvals approve --comment "Checked stock" +``` + +The SDK owner clients expose the same operations under +`durable.approvals`. Project access tokens can read approvals and stats but get +`403` on approve and deny. The MCP tools `list_durable_approvals` and +`get_durable_approval_stats` only read; MCP has no way to decide. + +Never approve or deny an approval yourself, including one your own workflow +requested during testing. List it, tell the user its ID and title, and let them +decide. Do not write code that decides approvals automatically; that defeats +the approval. + ## Start and manage executions from an application `start` accepts an application credential and returns an execution handle. An @@ -254,9 +367,8 @@ finish quickly while still suspending and replaying checkpoints. Instant waits cause more resumes per wall-clock minute than production, which helps expose a step that is unsafe to replay. Set `LOCAL_DURABLE_REAL_TIME=true` before `volcano start` when wait timing must be real. Local executions persist across -`volcano stop` and `volcano start`. Volcano does not expose externally completed -callbacks in local or cloud execution; use `ctx.waitUntil` to poll application -state instead. +`volcano stop` and `volcano start`. Approval timeouts always run in real time, +so test expiry locally with a short `timeout`. Local executions increment the same execution, operation, and compute counters as cloud executions. Inspect them through `GET /projects/{id}/usage`; the CLI @@ -328,6 +440,8 @@ start. `executions get` until terminal. - Deleting a durable function removes its execution history. Confirm before `durable delete`, `executions stop`, or `schedulers delete`. +- `approvals list`, `get`, and `stats` are safe to run. `approvals approve` and + `deny` are for the user alone; never run them. - Bound `logs --follow` with a timeout in agent-driven diagnostics. ## Troubleshooting @@ -343,6 +457,12 @@ cloud state. 5. A start during provisioning returns `409`; wait for `active`. 6. A `429` means a concurrency or durable allowance limit blocked the start. 7. A scheduler `403` can mean the project plan does not include schedulers. +8. An execution that stays `running` may be waiting on an approval. Check + `durable approvals list --execution `. +9. An approve or deny `403` means the credential is not a person's. A `409` + means the approval was already decided, expired, or cancelled. +10. `VOLCANO_PLATFORM_API_URL` is reserved. The platform sets it on durable + functions; a project variable with that name is rejected. ## Verification @@ -351,6 +471,7 @@ cloud state. - Run `volcano start` and `volcano durable deploy --all`. - Wait for local function status `active`. - Start one local execution with a unique idempotency name. +- If it waits on an approval, give the user its ID and ask them to decide. - Poll it to a terminal status and check its result. - Read runtime logs if the result is not `succeeded`. - After approved cloud deployment, repeat the execution check with @@ -359,6 +480,7 @@ cloud state. ## References - Hosting contract: `volcano-hosting/docs/public/functions/durable-functions.md` +- Approvals: `volcano-hosting/docs/public/functions/durable-approvals.md` - Local guide: `volcano-hosting/docs/public/guides/durable-functions-locally.md` - CLI contract: `volcano-cli/docs/durable-functions.md` - Command source: `volcano-cli/internal/cmd/durable/` diff --git a/plugins/cursor/skills/volcano-sdk/SKILL.md b/plugins/cursor/skills/volcano-sdk/SKILL.md index 9ac69f1..61270f7 100644 --- a/plugins/cursor/skills/volcano-sdk/SKILL.md +++ b/plugins/cursor/skills/volcano-sdk/SKILL.md @@ -46,7 +46,7 @@ Do NOT implement custom alternatives — no custom JWT auth, no ad-hoc database | User accounts or identity, email or password sign-up/sign-in, OAuth, sessions, anonymous users, password recovery, private or per-user data | `volcano-auth` | Auth application flows, session lifecycle, and common-error catalog | | Stored or persistent data, CRUD, records, todos, chat messages, polls, analytics, counters, click tracking, CMS content, feature flags, leaderboards, RLS | `volcano-database` | Query builder + every operator + RLS pattern + limitations (no joins / upserts / multi-statement tx) | | Volcano Functions, server-side or privileged logic, QR/PDF generators, secrets, outbound third-party APIs, orchestration, scheduled processing, file/image processing | `volcano-functions` | Invocation contract `{data, status, headers, version, error}`, Volcano Functions response shape, handler templates | -| Durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, execution schedulers | `volcano-durable` | Durable authoring contract, replay rules, cloud CLI lifecycle, execution status, schedulers, and safety | +| Durable functions, long-running or resumable workflows, checkpointed steps, waits, polling, durable executions, idempotent starts, execution schedulers, human approvals or sign-off | `volcano-durable` | Durable authoring contract, replay rules, `waitForApproval`, cloud CLI lifecycle, execution status, schedulers, and safety | | Project logs, retained log search, pagination, activity buckets, structured log filters | `volcano-logs` | Project-token authentication, search cursors, filters, and activity counts | | Project locks, distributed leases, leader election, fencing tokens, backend worker coordination | `volcano-locks` | Renewable lock guards, direct lease control, fencing, and safe recovery | | Uploads, downloads, galleries, file sharing, buckets, paths, public/private files, visibility, resumable uploads | `volcano-storage` | Full storage API + access policies + resumable protocol + limits | diff --git a/plugins/cursor/skills/volcano-typescript/SKILL.md b/plugins/cursor/skills/volcano-typescript/SKILL.md index d2e34e9..31afbce 100644 --- a/plugins/cursor/skills/volcano-typescript/SKILL.md +++ b/plugins/cursor/skills/volcano-typescript/SKILL.md @@ -1,6 +1,6 @@ --- name: volcano-typescript -description: "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime and Durable types, Function invocation generics, OAuth providers, middleware types, and utility types." +description: "Canonical TypeScript type definitions for the Volcano SDK: User, Session, AuthResponse, QueryBuilder, StorageObject, Realtime, Durable, and durable approval types, Function invocation generics, OAuth providers, middleware types, and utility types." --- # Volcano TypeScript Types Skill @@ -378,6 +378,32 @@ const execution: DurableExecution | undefined = data?.data[0]; `DurableExecution` uses the API's snake-case fields. Its `result` is `unknown`, so narrow it before use. +`ctx.waitForApproval` takes `WaitForApprovalOptions` and resolves with an +`ApprovalDecision`, both from the durable entry point: + +```ts +import type { ApprovalDecision, WaitForApprovalOptions } from '@volcano.dev/sdk/durable'; + +const request: WaitForApprovalOptions = { title: 'Ship order 4417?', timeout: '3d' }; +const decision: ApprovalDecision = await ctx.waitForApproval('ship-order', request); +// decision.status is 'approved' | 'denied' | 'expired'; decidedBy is null on expiry. +``` + +Reading and deciding approvals uses the main entry point's `DurableApproval`, +`DurableApprovalStatus`, `PaginatedDurableApprovals`, and `DurableApprovalStats`, +with `DurableApprovalListOptions` and `DurableApprovalStatsOptions` for filters: + +```ts +import type { DurableApproval, DurableApprovalListOptions } from '@volcano.dev/sdk'; + +const options: DurableApprovalListOptions = { status: 'pending', function: 'order-pipeline' }; +const { data } = await volcano.durable.approvals.list(projectId, options); +const approval: DurableApproval | undefined = data?.data[0]; +``` + +Like `DurableExecution`, `DurableApproval` uses snake-case fields +(`requested_at`, `expires_at`, `decision.decided_by`). + ## OAuth Types ```ts type OAuthProviderName = 'google' | 'github' | 'microsoft' | 'apple'; @@ -567,7 +593,7 @@ channel.onPostgresChanges('INSERT', 'public', 'posts', (change) => { | Storage | `StorageObject`, `StorageUploadResponse`, `StorageListResponse` | `storage.from(...)` operations | | Realtime | `PostgresChange`, `PresenceState`, `ConnectContext` | Channel callbacks | | Functions | `invoke(...)` generic params | Both ends of an invocation | -| Durable | `DurableHandler`, `DurableContext`, `DurableExecution` | Durable authoring and execution clients | +| Durable | `DurableHandler`, `DurableContext`, `DurableExecution`, `ApprovalDecision`, `DurableApproval` | Durable authoring, approvals, and execution clients | | OAuth | `OAuthProviderName`, `OAuthProvider` | Provider name validation | | Sessions | `AuthSession`, `SessionsResponse` | Multi-device session UI | | Middleware | `ServerClient`, `GetUserResult` | Next.js middleware/route handlers | diff --git a/sources/volcano-skills b/sources/volcano-skills index b0774cd..f96d5fa 160000 --- a/sources/volcano-skills +++ b/sources/volcano-skills @@ -1 +1 @@ -Subproject commit b0774cdb9f18daff5567da1842861e1a0468f963 +Subproject commit f96d5fa300795f65e9e76903724d2ad1c2902a96