Relationships
#4474 Slackwater: bearer-only HTTP route for workflow actions
Opened by skunk-ape · 11/6/2026· Shipped 11/6/2026
Part of #4459. Needs #4460, which has shipped.
Problem
An integration has to hold a WebSocket per person to act on a workflow. Most apps handle a short request, such as a Slack button press, and want a short HTTP call in return.
Serve already has an HTTP request route, but it can't simply take these actions. It accepts the dashboard's session cookie, which is safe only for reads, and a rule test pins its types to reads. Actions need their own path.
Scope
A new bearer-only path, POST /api/v1/action, with its own list of allowed types:
| Action | Type |
|---|---|
| Start a run | workflow.run |
| Resume a run | workflow.resume |
| Run status | workflow.history.get (by full run ID) |
| List approvals | workflow.approvals |
| Approve | workflow.approve |
| Reject | workflow.reject |
| Signal | workflow.signal |
workflow.run.summary was replaced in the table because it reports totals per workflow and takes no run ID. workflow.resume was added because an approved run otherwise needs --auto-resume to continue.
Each action goes through the same handleMessage path as the WebSocket, so validation, authorization, audit and the client recorded by #4460 stay the same.
Rules
- Bearer only. No
dashboardSessionis passed to this path, and a request with only a cookie is refused. Add this tointegration/serve_http_request_rules_test.ts, which must also keep pinning the existing route to reads. - Only listed types run. Anything else is refused, and the rule test fails if a type is added without being listed.
- Only listed fields are accepted. Each type has a strict payload, so a caller can't claim an identity or held locks, or turn off checks. Fields serve sets itself are never taken from the caller.
- Per-person rate limit. Use
limitPrincipalRate: true, keyed on the person rather than the app, so one person's clicks can't use up everyone's allowance. Failed credentials are counted per token name and address, so a bad token can't lock out the token's real holder. - Followers: an action sent to a follower works, using the caller's own credentials, and records the same principal and client as on the leader. Extend
integration/serve_follower_requests_rules_test.ts. - Errors can be told apart, and refusals reveal nothing. Not authenticated, refused by a grant, not found, already decided and rate limited each have a stable code and HTTP status. A caller with no grant learns nothing about whether a run or gate exists, or its state.
- A repeated start is safe. A start requires an idempotency key, scoped to the person, client and workflow. A retry gets the first start's run ID and no second run; the same key with different inputs is refused.
- A repeated decision is safe. A double click or a retried request can't record a second decision on a gate, and gets the already-decided error, including when the same step name is used in another job.
- A decision answers one attempt of a gate. Approve and reject name the attempt with the gate's
suspendedAt, required on this route and optional on the WebSocket and CLI. A decision for an earlier attempt of a gate that has been asked again is refused. - A repeated resume is safe. This route only resumes a suspended run, so a retry can't re-run a failed run's steps.
Check while building
- Starting a run outlives the request. A run started here must not stop when the caller disconnects. Slack gives an app about 3 seconds to respond, so starting returns once the run is accepted, and the caller then reads status.
- A run that pauses at a gate has to be reported as paused, not as a hang or a failure. #4438 is this bug in swamp-lib, so don't copy it.
- Span routes: add the new path to the span route table, as the rule test requires.
Out of scope
swamp-lib helpers for these calls, the event stream, integration registration and ceilings. Those are separate items under #4459.
Done when
- Start, resume, status, approvals, approve, reject and signal work over HTTP with a bearer token.
- Audit events and run records match the same action over the WebSocket, and any difference is shown.
- A cookie is refused, and types and fields not on the lists are refused.
- Actions work through a follower, and record the same principal and client as on the leader.
- Requests are rate limited per person.
- A run keeps going after the caller disconnects.
- A retried start starts one run.
- A repeated decision gets a clear error and records nothing new, and so does a decision for an earlier attempt of a re-opened gate.
- Grant conditions apply here exactly as over the WebSocket.
- The route is documented in the serve guide, and swamp-uat covers it against the compiled binary.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.