Skip to content

Rewrite the CLI in Rust as a 2.x minor release with full parity #72

Closed

Description

@rmcdaniel

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 dw binary name, same release channels, installers, Homebrew formula and dw upgrade path.
  • The Rust CLI depends on the published durable-workflow crate 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 upgrade moves 2.3.x users with no action on their part.

Acceptance

  1. 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.
  2. Automation contract. --output=json and --output=jsonl produce the same envelopes, validated against the published schemas/output manifests. Exit codes keep their current categories (usage, network, authentication, not-found, server, timeout, compatibility).
  3. 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.
  4. 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.
  5. 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 published dw) 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).
  6. 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).

  1. Preserve the selected run identity during bulk cancellation #59 fixed in PHP and released as 2.3.4.
  2. Rust skeleton: Cargo crate, profile/config loading, output envelopes, exit codes, and the parity harness with one command wired through.
  3. 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).
  4. Release workflow for the Rust binaries, then the 2.x minor release and installed qualification.

Related: #59, #60, #61, #64.

Activity

  1. added
    kind:featureA public product capability or experience is requested
    priority:P1High-priority product or release risk
    repo:cliOwned by the CLI repository
    status:in-progressApproved work is actively being implemented or validated
    on Nov 3, 2026
  2. rmcdaniel commented on Nov 3, 2026

    @rmcdaniel
    MemberAuthor

    Known issue (General's decision, 2026-11-03): the raw worker commands (workflow-task:poll and related) in the 2.x CLI fail against native Server 3.0.x because native omits the worker-protocol protocol_version body 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.

  3. changed the title [-]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[/+] on Nov 4, 2026
  4. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    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.0 and 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.

  5. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    Step 2 is merged: #76 (b460f0e1) adds the Rust skeleton, the 2.x command catalog and the parity harness.

    • Ported: option parsing, profiles, output modes, error envelope, exit codes, env:*, server:health, and 2.x command lookup/listings.
    • Parity: against published 2.3.5 on published PHP 2.11.8 and native 3.0.5, 72/73 cases match with 0 unexpected.
    • Accepted difference, for the release notes: 2.x prints its single-suggestion "Do you want to run …?" prompt to stdout even when stdin is not a terminal. The Rust build prompts only on a terminal.

    General's requirements before any Rust release (2.5.0):

    Next slice: the compatibility preflight and server:info.

  6. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    Progress: #79 is merged (28e3a04c). It ports the compatibility preflight, session and --version warnings, exit-8 refusals, server:info/cluster:info and PHP-exact JSON encoding, and CI compares the compatibility checks against the 2.x PHP classes. #80 (per-command --help parity) is in review.

    Recorded follow-up from #79's review: php::float_to_json rounds ties at 1e14 and above half-up, where PHP uses half-to-even (for example 1149865714285714.2). Fix before 2.5.0.

  7. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    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:

    • redrive, archive, history:export, query, signal and update;
    • watch ("/workflows/{$workflowId}/runs/{$runId}").

    Only five commands use rawurlencode, for example activity: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.

  8. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    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 %2F gets filed against server with a repro, not worked around in the CLI.

    Probe results today (both Servers, raw HTTP):

    Request PHP 2.11.8 native 3.0.5
    start with /, space, %, ?, #, @ or ~ in workflow_id 422 validation_failed 422 validation_failed
    start with : or . in workflow_id 201 201
    GET /api/workflows/a%20b, a%25b, a%3Fb, a%23b, %2E%2E 404 JSON instance_not_found, decoded id same
    GET /api/workflows/cli%3Aenc describes cli:enc describes cli:enc
    GET /api/workflows/a%2Fb, ..%2Fhealth 404 Apache HTML page (no JSON) 404 JSON, workflow_id a/b / ../health
    • No request reached another route on either Server.
    • The only mishandling is PHP Server's HTML 404 for %2F. Apache's default AllowEncodedSlashes Off causes it, and it is filed as server#1013.
    • Because neither Server accepts / 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 %3A while 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 %2F case is recorded as an expected difference citing server#1013;
    • a : 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.

  9. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    cli#81 merged as ae2c3c6, with a tree equal to the cleared head 1d35507. Review nits carried to the next slice:

    • Keep raw non-UTF-8 argv bytes (today they print as U+FFFD) before any command that takes a file-path argument is ported.
    • Parse SHELL_VERBOSITY with PHP (int) semantics (-1x → quiet).
    • Resolve a $SHELL with a trailing slash the way basename does.
    • format_status: split and lowercase ASCII only.
    • A mismatched closing style tag throws, as Symfony does.
    • Unclaimed differences: --output=-h validation order; dw -- -h.
  10. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    Two more items to decide in the command slices, both raised by sdk-rust#183's clearance:

    • Redirects. The SDK never follows them (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 drops Authorization when the host changes. Plan: report a 3xx as an error and never re-send the bearer token to the Location. 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 one warn on the durable_workflow::tls target. Plan: install no tracing subscriber by default, so stderr stays byte-identical to 2.x; revisit if Security wants a visible warning.
  11. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    Release workflow decisions (General, 2026-11-03). All three defaults were accepted, with conditions:

    1. dw.phar is dropped in 2.5.0.
      • Before the cut, neither the cli README nor durable-workflow.github.io may contain dw.phar download instructions. Grep both and send a docs PR.
      • The removal goes at the top of the release notes, with the replacement command.
      • All other asset names are unchanged. dw-*.dependencies.json becomes a Cargo.lock inventory under a new schema id.
    2. A separate release-rust.yml with a two-way version gate. release-rust.yml releases only versions >= 2.5.0; release.yml refuses them.
      • The release.yml refusal merges before any 2.5.0 tag exists.
      • A static test pins both gates.
      • The PR-only build and verify path holds no secrets and no id-token.
      • release.yml retires after 2.5.0.
    3. dw upgrade is 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.
      • Port the Rust upgrade command, with parity.
      • End-to-end check: install the latest published 2.x, run dw upgrade to a 2.5.0 candidate release, and confirm the binary is checksum-verified and runs.
      • Run it for both the PHAR-install and the binary-install paths, on linux x86_64, linux aarch64 and macOS arm64.
  12. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    2.5.0 notes review (General, 2026-11-03). The wording is approved. Additions due before the tag:

    • A Windows verified path: download, Get-FileHash against SHA256SUMS, attestation, copy; no iex (cli#85).
    • --tls-verify=false must warn on stderr (security requirement). 2.x printed no warning, so list this as an intentional difference. This replaces my earlier "no subscriber" plan.
    • Under intentional differences, once that slice lands: "3xx responses are reported as errors, and the token is never re-sent."
    • Keep "2.x standalone binaries upgrade with dw upgrade" only if the release-gate e2e check (published 2.3.5 → candidate, all 3 platforms) passes. Name any failing platform explicitly.
    • Cite the actual parity numbers from the final run in the version-bump PR.
    • PHAR-migration text sent to durable-workflow-github-io.
  13. 74 remaining items

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:featureA public product capability or experience is requestedpriority:P1High-priority product or release riskrepo:cliOwned by the CLI repositorystatus:in-progressApproved work is actively being implemented or validated

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions