Repository navigation
Description
Decision
Native Server 3.0 is released. The CLI moves from PHP to Rust and ships as a CLI 2.x minor release: 2.5.0, or the next free minor after #61's 2.4.0. The operator directed the rewrite; General approved the scope below on 2026-11-03.
Version decision (General, 2026-11-03, reversing the earlier 3.0.0 plan). The rewrite keeps every command, flag, JSON shape and exit code, so semver makes it a minor release. A major version would also break PHP Server compatibility. Published PHP Server 2.11.8 advertises client_compatibility.clients.cli.supported_versions: ">=2.0.0,<3.0.0", and the CLI warns on every command when its version is outside that range. No further PHP Server releases are planned, so the range will not widen. Native Server does not constrain the CLI version. General will ask Server to advertise a client-compatibility block in 3.1.0 with ranges that admit the 2.x line.
Users keep the same dw command, install path and automation contract. Nobody should need to change a script, profile or pipeline because the implementation language changed.
Scope
- Same repository, same
dwbinary name, same release channels, installers, Homebrew formula anddw upgradepath. - The Rust CLI depends on the published
durable-workflowcrate from sdk-rust, pinned to the latest qualified release. Missing client capabilities are filed as sdk-rust issues. Client code is not copied into the CLI. - The PHP implementation (PHAR and static-PHP binaries) is in maintenance until the Rust release ships: security and correctness fixes only. After that the PHAR gets a published deprecation date.
- The release notes state plainly that the implementation changed (a static Rust binary instead of the PHAR or static-PHP). Asset names and install channels stay the same, so
dw upgrademoves 2.3.x users with no action on their part.
Acceptance
- Command parity. Every command group in the 2.x CLI exists with the same names, arguments, options, environment variables and profile config file. Any intentional difference is listed in the release's upgrade notes with a reason.
- Automation contract.
--output=jsonand--output=jsonlproduce the same envelopes, validated against the publishedschemas/outputmanifests. Exit codes keep their current categories (usage, network, authentication, not-found, server, timeout, compatibility). - Parity harness. A repository command runs the PHP 2.x and Rust binaries against published Server 2.11.8 (PHP) and native Server 3.0.5. It compares stdout JSON shapes, exit codes and stderr categories after normalizing timestamps and ids. Stderr must also match on PHP 2.11.8, so the Rust 2.x build triggers no compatibility warning there. It runs in CI for each migrated slice.
- Correctness contracts carry over. Fixes made to the PHP CLI during the rewrite are reproduced in Rust with the same tests. That includes Preserve the selected run identity during bulk cancellation #59 (bulk cancel targets the exact selected run) and the human-output sanitization from Expose command keys with safe human output and exact JSON values #61.
- Release integrity from day one. The Rust release workflow keeps exact-source builds,
SHA256SUMS, SLSA attestations and public-download verification. It also uploads every asset before publication for stable releases and prereleases, so immutable releases (Make published CLI releases immutable without breaking asset publication #64) can be enabled without weakening the PHAR path. Code from the release (installers, the publisheddw) runs only in read-only jobs, never in the job that holds write or OIDC permissions (Run post-release verification outside the privileged release job (MEDIUM) #75). - Installed qualification. Before the Rust release is called delivered, the published binaries are installed through each installer and pass the harness against the published Servers above.
Plan
Each slice is a draft PR labelled agent:cli with Security review (or General while Security is out).
- Preserve the selected run identity during bulk cancellation #59 fixed in PHP and released as 2.3.4.
- Rust skeleton: Cargo crate, profile/config loading, output envelopes, exit codes, and the parity harness with one command wired through.
- Slices: workflow, namespace and schedule first, then the remaining groups (activity, worker, task-queue, search-attribute, schema, system, env, server, storage, query/workflow tasks, service-call, bridge, debug/doctor/watch/upgrade).
- Release workflow for the Rust binaries, then the 2.x minor release and installed qualification.
Activity
rmcdaniel commentedon Nov 3, 2026
Known issue (General's decision, 2026-11-03): the raw worker commands (
workflow-task:polland related) in the 2.x CLI fail against native Server 3.0.x because native omits the worker-protocolprotocol_versionbody field. The fix ships in Server 3.1.0 (durable-workflow/server#1001). No client workaround goes into the PHAR. The Rust CLI keeps the same body check, so its parity harness runs those commands against 3.1.0 or later.[-]Rewrite the CLI in Rust for 3.0.0 with full 2.x parity[/-][+]Rewrite the CLI in Rust as a 2.x minor release with full parity[/+]rmcdaniel commentedon Nov 4, 2026
Version decision updated in the body: the Rust CLI ships as a 2.x minor (2.5.0 or the next free minor), not 3.0.0. PHP Server 2.11.8 advertises CLI support
>=2.0.0,<3.0.0and won't be re-released, so a 3.0.0 CLI would warn on every command against PHP 2.x Servers. The rewrite is non-breaking, so semver makes it a minor anyway.rmcdaniel commentedon Nov 4, 2026
Step 2 is merged: #76 (
b460f0e1) adds the Rust skeleton, the 2.x command catalog and the parity harness.env:*,server:health, and 2.x command lookup/listings.General's requirements before any Rust release (2.5.0):
--helpparity;Next slice: the compatibility preflight and
server:info.rmcdaniel commentedon Nov 4, 2026
Progress: #79 is merged (
28e3a04c). It ports the compatibility preflight, session and--versionwarnings, exit-8 refusals,server:info/cluster:infoand PHP-exact JSON encoding, and CI compares the compatibility checks against the 2.x PHP classes. #80 (per-command--helpparity) is in review.Recorded follow-up from #79's review:
php::float_to_jsonrounds ties at 1e14 and above half-up, where PHP uses half-to-even (for example1149865714285714.2). Fix before 2.5.0.rmcdaniel commentedon Nov 4, 2026
Planning note for the remaining command slices: path encoding of IDs.
Most 2.x workflow commands put IDs and names into the URL without encoding them. Affected:
"/workflows/{$workflowId}/runs/{$runId}").Only five commands use
rawurlencode, for exampleactivity:describe. In 2.x, an ID containing/,?,#,%or a space therefore reaches a different route or query, or fails.The Rust port builds paths through the SDK's segment encoder (sdk-rust#183, which is
rawurlencode-compatible). For those IDs it sends the route the user meant, so it will differ from 2.x on exactly those inputs. For IDs made only of unreserved characters (A-Za-z0-9-._~), the requests are byte-identical.Proposal: encode every segment and list this as an intentional difference in the release notes. The alternative is to reproduce the unencoded paths. I'll add parity cases that keep IDs to unreserved characters, plus one case per affected command that records the difference.
rmcdaniel commentedon Nov 4, 2026
Decision (General, 2026-11-03): encode every path segment, and list it as an intentional difference in the 2.5.0 release notes. Do not reproduce the 2.x unencoded paths: an unencoded
../or?in an ID is a path-injection risk, not only a wrong route.Condition: parity cases against both published Servers (PHP 2.11.8 and native 3.0.5) with IDs containing
/, a space,%,?and#. Each must reach the right workflow, or get a clean 404 or validation refusal; it must never reach a different route. Server mishandling of%2Fgets filed against server with a repro, not worked around in the CLI.Probe results today (both Servers, raw HTTP):
/, space,%,?,#,@or~inworkflow_idvalidation_failedvalidation_failed:or.inworkflow_idGET /api/workflows/a%20b,a%25b,a%3Fb,a%23b,%2E%2Einstance_not_found, decoded idGET /api/workflows/cli%3Aenccli:enccli:encGET /api/workflows/a%2Fb,..%2Fhealthworkflow_ida/b/../health%2F. Apache's defaultAllowEncodedSlashes Offcauses it, and it is filed as server#1013./in a workflow ID, the impact is the missing JSON refusal, not an unreachable workflow.:is a valid ID character that the Rust CLI sends as%3Awhile 2.x sent it raw. Both Servers decode it to the same workflow, so the request is equivalent.Plan for the slices: each ported command that takes an ID or name in its path gets parity cases against both Servers:
/, space,%,?and#IDs, which expect the clean 404 or 422; the PHP%2Fcase is recorded as an expected difference citing server#1013;:ID that reaches the real workflow.The same applies to other path segments (signal, query and update names, schedule and activity IDs) as those commands land.
rmcdaniel commentedon Nov 4, 2026
cli#81 merged as ae2c3c6, with a tree equal to the cleared head 1d35507. Review nits carried to the next slice:
SHELL_VERBOSITYwith PHP(int)semantics (-1x→ quiet).$SHELLwith a trailing slash the waybasenamedoes.format_status: split and lowercase ASCII only.--output=-hvalidation order;dw -- -h.rmcdaniel commentedon Nov 4, 2026
Two more items to decide in the command slices, both raised by sdk-rust#183's clearance:
Policy::none); a 3xx comes back as a RawResponse. The 2.x Server client used Symfony HttpClient's default: it follows up to 20 redirects and dropsAuthorizationwhen the host changes. Plan: report a 3xx as an error and never re-send the bearer token to theLocation. Record it as an intentional difference, with a parity case using the mock Server.--tls-verify=false. 2.x prints no warning. The SDK logs onewarnon thedurable_workflow::tlstarget. Plan: install no tracing subscriber by default, so stderr stays byte-identical to 2.x; revisit if Security wants a visible warning.rmcdaniel commentedon Nov 4, 2026
Release workflow decisions (General, 2026-11-03). All three defaults were accepted, with conditions:
dw-*.dependencies.jsonbecomes a Cargo.lock inventory under a new schema id.release-rust.ymlwith a two-way version gate.release-rust.ymlreleases only versions >= 2.5.0;release.ymlrefuses them.release.ymlrefusal merges before any 2.5.0 tag exists.release.ymlretires after 2.5.0.dw upgradeis a release gate. The critical path is the published 2.x upgrade code (latest stable: 2.3.5) fetching 2.5.0 under the asset names it already expects.upgradecommand, with parity.dw upgradeto a 2.5.0 candidate release, and confirm the binary is checksum-verified and runs.rmcdaniel commentedon Nov 4, 2026
2.5.0 notes review (General, 2026-11-03). The wording is approved. Additions due before the tag:
Get-FileHashagainst SHA256SUMS, attestation, copy; no iex (cli#85).--tls-verify=falsemust warn on stderr (security requirement). 2.x printed no warning, so list this as an intentional difference. This replaces my earlier "no subscriber" plan.dw upgrade" only if the release-gate e2e check (published 2.3.5 → candidate, all 3 platforms) passes. Name any failing platform explicitly.74 remaining items
Port server:start-dev launch and signal lifecycle to Rust
Support --tls-verify=false against HTTPS in the Rust CLI
Port workflow:describe, show-run, history and history-export to Rust
Port the schedule commands to the Rust CLI
Port workflow cancel, terminate, archive, redrive, repair and migrate…
Port workflow:query, start, signal and update to Rust
Port search attributes, storage, task queues and workers to Rust
Port activity, workflow-task and query-task commands to Rust
Port system, service-call, bridge, debug and watch commands to Rust
Run the Rust CLI against published PHP and native Servers in CI
List the ported commands' intentional differences in the 2.5.0 notes
Prepare 2.5.0-rc.3, the first feature-complete release candidate
Prepare 2.5.0 from the rc.3 source
Require live-Server parity and update the README for 2.5.0
Publish the 2.3.x PHAR end-of-support date and refresh distribution docs