Skip to content

fix: remove vulnerable legacy TLS dependency chain - #679

Merged
logbie merged 4 commits into
mainfrom
warden/dependabot-remediation
Jul 31, 2026
Merged

fix: remove vulnerable legacy TLS dependency chain#679
logbie merged 4 commits into
mainfrom
warden/dependabot-remediation

Conversation

@logbie

@logbie logbie commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • remove Warp 0.3.7's legacy TLS feature, which pinned Tokio-Rustls 0.25, Rustls 0.22.4, and vulnerable Rustls-WebPKI 0.102.8;
  • serve the unchanged Warp filters through Tokio-Rustls 0.26 / Rustls 0.23, with explicit ring configuration, TLS 1.2/1.3, HTTP/2 + HTTP/1.1 ALPN, peer-address propagation, actionable certificate/key validation, and tracked cancellation for every accepted connection task;
  • regenerate both Cargo lockfiles so the root and fuzz workspaces contain only Rustls-WebPKI 0.103.13;
  • update the extension lockfile from js-yaml 4.2.0 to 4.3.0 and the affected nested brace-expansion 5.0.7 copy to 5.0.9.

This addresses all ten Dependabot alerts open on 2026-07-31: root Cargo alerts 40/41/49/59, fuzz Cargo alerts 60/61/62/63, and npm alerts 67/68. GitHub will close them only after this reaches the default branch and Dependabot rescans.

Impact

WFL syntax and the public behavior of plain HTTP, secured HTTP, redirects, standalone WebSockets, request client_ip, port-zero binding, and close server are preserved. Invalid certificate/key pairs are now rejected before binding. Closing a secured server now also cancels accepted idle and ClientHello-stalled connections after the existing 50 ms response-flush allowance.

Test evidence

  • Risk class: R3 — TLS security boundary, protocol negotiation, concurrency, cancellation, and listener lifecycle.
  • Acceptance criteria → tests: tests/web_server_tls_test.rs covers occupied ports, malformed and mismatched keys, TLS 1.2/1.3, HTTP/2 ALPN plus HTTP/1.1 fallback, peer IP, stalled-handshake isolation, established/stalled connection cancellation, immediate rebind, and actual port-zero reporting. src/interpreter/tls.rs covers panic-safe task unregistration.
  • Red evidence: commit 2aa19de2 produced 11 passing TLS cases and a failing certificate/key-mismatch case because the legacy listener accepted the pair. Commit 0a90e770 added the lifecycle regression; before tracked cancellation it failed with Established TLS connection remained open after close server. The panic-cleanup unit initially retained one dead abort handle (left: 1, expected 0) before the RAII guard.
  • Unit/component: cargo test --lib panicking_tracked_task_is_unregistered -- --nocapture --test-threads=1; cargo test --all --test web_server_tls_test -- --test-threads=1 — 13/13.
  • Integration/contract: cargo test --all -j 2; cargo build --release -j 2; cargo check --manifest-path fuzz/Cargo.toml --all-targets -j 2; cargo fmt --all -- --check; cargo clippy --all-targets --all-features -- -D warnings.
  • End-to-end: the bounded repository integration runner discovered 154 programs: 130 passed, 0 failed, and 24 skipped under its expected-failure/timeout/CI-SKIP rules; scripts/run_web_tests.ps1 passed 2/2; forced docs validation passed 21/21 examples.
  • Security/dependencies: both root and fuzz cargo tree --locked -i rustls-webpki@0.102.8 correctly report no matching package; both resolve only 0.103.13 through Rustls 0.23.42. npm ci --ignore-scripts, npm run compile, and npm ls js-yaml brace-expansion --all pass with js-yaml 4.3.0 and the affected nested brace-expansion 5.0.9.
  • Coverage: the repository has no automated coverage gate; behavior, boundary, negative, concurrency, cancellation, and lifecycle coverage was added at the real-socket integration layer.
  • Platforms: Windows x86-64, Rust 1.97.0 (MSRV remains 1.94+), Node 24.14.0.
  • Not applicable / baseline limitations: the optional OpenSSL-generated WFL TLS fixture was skipped because OpenSSL is unavailable, while the Rust real-socket TLS suite ran all 13 cases. The extension's existing eslint src command has no usable lint target/config and blocks npm test; direct VS Code tests are 24 passing, 2 pending, with one pre-existing Windows CRLF assertion failure. Working-tree hygiene passed clean in the disposable worktree after commit; a root-worktree hygiene scan sees only the two preserved user-owned staged .worktrees entries. npm audit remains nonzero for broader legacy dev-tooling nodes that were not open alerts on the requested Dependabot dashboard.
  • Rollback/recovery: revert this PR to restore the old listener, understanding that doing so also restores the vulnerable Rustls 0.22/WebPKI 0.102 chain; forward repair is preferred for any post-merge regression.
  • Residual risk: no separate application-level hard cap was added for pre-HTTP TLS connections; active task cardinality remains bounded by OS accepted-connection resources. Dependabot dashboard closure cannot occur until merge/default-branch rescan.
  • Review: an independent R3 review exercised the final diff and approved it with no remaining actionable findings.

Implementation details and chronology are recorded in Engineering/plans/2026-07-31-dependabot-remediation.md and History/dev-diary/2026/2026-07-31-dependabot-tls-remediation.md.

Posted by the WFL repo warden (automated maintainer pass).


Open in Devin Review

Summary by CodeRabbit

  • Bug Fixes

    • Improved secure server reliability with support for TLS 1.2 and 1.3, HTTP/1.1, and HTTP/2.
    • Improved handling of stalled handshakes, invalid certificates or keys, occupied ports, and graceful shutdown.
    • Server errors and dynamically assigned ports are now reported more accurately.
  • Security

    • Updated TLS components and addressed dependency security alerts.
    • Updated the JavaScript YAML package override to a newer secure version.
  • Documentation

    • Added remediation and verification documentation for dependency and TLS updates.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

Open in Devin Review

Comment thread src/interpreter/tls.rs
Comment on lines +273 to +291
impl Accept for SecuredIncoming {
type Conn = SecuredStream;
type Error = io::Error;

fn poll_accept(
self: Pin<&mut Self>,
context: &mut Context<'_>,
) -> Poll<Option<Result<Self::Conn, Self::Error>>> {
let this = self.get_mut();
match ready!(Pin::new(&mut this.incoming).poll_accept(context)) {
Some(Ok(stream)) => Poll::Ready(Some(Ok(SecuredStream::new(
stream,
Arc::clone(&this.config),
)))),
Some(Err(error)) => Poll::Ready(Some(Err(error))),
None => Poll::Ready(None),
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟨 Secured listener has no TLS handshake timeout or connection cap

Each accepted TCP connection on the secured listener spawns a Hyper task whose TLS handshake is driven lazily (SecuredStream::poll_read at src/interpreter/tls.rs:48-67) with no handshake deadline and no application-level cap on concurrent pre-HTTP connections. A client that opens many sockets and never sends a ClientHello keeps tasks, sockets, and per-connection buffers alive indefinitely until the server is closed, so exhaustion is bounded only by OS file-descriptor limits.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@logbie, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 72aebcb0-4227-4fbc-a05b-3856ed5bb257

📥 Commits

Reviewing files that changed from the base of the PR and between 137e1c0 and 736beca.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • fuzz/Cargo.lock is excluded by !**/*.lock
  • vscode-extension/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • Cargo.toml
  • History/dev-diary/2026/2026-07-31-dependabot-tls-remediation.md
  • vscode-extension/package.json
📝 Walkthrough

Walkthrough

Changes

The PR replaces Warp’s legacy TLS feature with a custom Tokio-Rustls transport. It adds asynchronous handshakes, tracked connection cancellation, peer-address propagation, TLS configuration validation, expanded integration tests, and dependency remediation records.

TLS transport remediation

Layer / File(s) Summary
Dependencies and remediation records
Cargo.toml, Engineering/plans/..., History/dev-diary/..., vscode-extension/package.json
Rust dependencies now use Tokio-Rustls and Rustls TLS 1.2 support. The remediation plan and diary record dependency updates, TLS behavior, and verification results. The npm override updates js-yaml.
Rustls transport and connection lifecycle
src/interpreter/tls.rs
SecuredStream drives per-connection handshakes. Tracked Hyper tasks support cancellation, deregistration, and panic cleanup. SecuredIncoming binds listeners and loads validated Rustls configuration.
Secure server wiring and peer address propagation
src/interpreter/mod.rs, tests/web_server_tls_test.rs
Secure startup uses SecuredIncoming and inserts connection addresses into request extensions. Server completion and expected failure paths are now asserted.
TLS protocol and lifecycle validation
tests/web_server_tls_test.rs, History/dev-diary/...
Tests cover stalled handshakes, HTTP/2 and HTTP/1.1, TLS 1.2 and 1.3, credential errors, shutdown, occupied ports, and port-zero listeners.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TLSClient
  participant SecuredIncoming
  participant SecuredStream
  participant HyperServer
  TLSClient->>SecuredIncoming: connect to secure listener
  SecuredIncoming->>SecuredStream: accept TCP connection
  SecuredStream->>TLSClient: negotiate Rustls TLS and ALPN
  SecuredStream->>HyperServer: provide established stream and peer address
  HyperServer->>TLSClient: serve HTTP/2 or HTTP/1.1 request
  HyperServer->>SecuredIncoming: close server
  SecuredIncoming->>SecuredStream: cancel active and stalled connections
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: removing the vulnerable legacy TLS dependency chain.
Docstring Coverage ✅ Passed Docstring coverage is 86.67% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch warden/dependabot-remediation
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch warden/dependabot-remediation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/web_server_tls_test.rs (2)

607-614: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Confirm the reported ephemeral port is the actual listener.

The assertion proves the port is non-zero. It does not prove the listener bound that port. A connect to the reported port would prove the reported value and the bound socket agree.

♻️ Proposed addition
     assert_ne!(
         actual_port, 0,
         "A port-zero TLS listener must report the actual ephemeral port"
     );
+    tokio::time::timeout(
+        Duration::from_secs(2),
+        tokio::net::TcpStream::connect(("127.0.0.1", actual_port)),
+    )
+    .await
+    .expect("Connecting to the reported ephemeral port timed out")
+    .expect("The reported ephemeral port must accept connections");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/web_server_tls_test.rs` around lines 607 - 614, Extend the TLS server
port test after parsing actual_port to connect to the reported address and
assert the connection succeeds, proving it matches the listener’s bound
ephemeral port. Reuse the existing server address/setup symbols and retain the
non-zero assertion.

449-495: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert that no listener was bound when credentials are rejected.

Both cases prove the error message. Neither proves the socket was never bound. The PR objective states that invalid certificate/key pairs are rejected before binding, so make that observable.

Bind the port after each failing case. A successful bind proves the listener never claimed the port.

♻️ Proposed addition
     assert!(
         malformed_message.contains("contains no private key"),
         "Malformed-key error should explain the PEM requirement, got: {malformed_message}"
     );
+    std::net::TcpListener::bind(("127.0.0.1", 8222))
+        .expect("A rejected credential pair must not leave port 8222 bound");
     assert!(
         mismatch_message.contains(&cert_path)
             && mismatch_message.contains(&unrelated_key_path)
             && mismatch_message.contains("not a valid pair"),
         "Certificate/key mismatch should identify both invalid inputs, got: {mismatch_message}"
     );
+    std::net::TcpListener::bind(("127.0.0.1", 8223))
+        .expect("A rejected credential pair must not leave port 8223 bound");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/web_server_tls_test.rs` around lines 449 - 495, The test
test_tls_configuration_rejects_malformed_and_mismatched_keys only verifies error
messages but never confirms the listener port was never bound when credentials
fail validation. After both malformed_result and mismatch_result assertions, add
a check that attempts to bind to the same port (8222 and 8223 respectively, e.g.
via std::net::TcpListener::bind) and asserts the bind succeeds, proving no
listener was left claiming the port from the failed interpret call. Add this
verification after each of the two failure scenarios in the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@History/dev-diary/2026/2026-07-31-dependabot-tls-remediation.md`:
- Around line 80-99: Rename the “broader Green verification” section to
“Verification results.” Update the verification summary to identify the failing
npm lint check and pre-existing extension assertion as unresolved or explicitly
accepted failures, and state the release decision without claiming full
validation or Green status. Keep required failures visible rather than skipping,
retrying, or reclassifying them as successful checks.
- Around line 43-46: Update the TLS connection handling described by the
migration to enforce both a timeout for stalled pre-HTTP handshakes and a
configurable maximum number of in-flight handshakes, releasing capacity on
completion, failure, or expiry; add tests covering timeout expiry and subsequent
capacity recovery. If these controls are intentionally deferred, replace the
current scope statement with an explicit security risk acceptance naming an
owner and remediation deadline.

---

Nitpick comments:
In `@tests/web_server_tls_test.rs`:
- Around line 607-614: Extend the TLS server port test after parsing actual_port
to connect to the reported address and assert the connection succeeds, proving
it matches the listener’s bound ephemeral port. Reuse the existing server
address/setup symbols and retain the non-zero assertion.
- Around line 449-495: The test
test_tls_configuration_rejects_malformed_and_mismatched_keys only verifies error
messages but never confirms the listener port was never bound when credentials
fail validation. After both malformed_result and mismatch_result assertions, add
a check that attempts to bind to the same port (8222 and 8223 respectively, e.g.
via std::net::TcpListener::bind) and asserts the bind succeeds, proving no
listener was left claiming the port from the failed interpret call. Add this
verification after each of the two failure scenarios in the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0ed83aaf-e2fa-4a92-ad3b-0a4b1ecf5b2b

📥 Commits

Reviewing files that changed from the base of the PR and between 0096844 and 137e1c0.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • fuzz/Cargo.lock is excluded by !**/*.lock
  • vscode-extension/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • Cargo.toml
  • Engineering/plans/2026-07-31-dependabot-remediation.md
  • History/dev-diary/2026/2026-07-31-dependabot-tls-remediation.md
  • src/interpreter/mod.rs
  • src/interpreter/tls.rs
  • tests/web_server_tls_test.rs
  • vscode-extension/package.json

Comment on lines +43 to +46
The migration does not add a separate application-level cap for pre-HTTP TLS
connections. The tracker retains only currently active Hyper tasks and remains
bounded by the operating system's accepted-connection resources; adding a
configurable handshake limit or timeout is a separate hardening change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Bound stalled TLS handshakes before accepting this R3 change.

Lines 43-46 state that pre-HTTP TLS connections have no timeout or application-level limit. A client can open many sockets and withhold ClientHello, consuming file descriptors, task memory, and accepted-connection capacity until shutdown. Add a handshake timeout and a bounded in-flight handshake count, with tests for expiry and recovery. If this control remains out of scope, record an explicit security risk acceptance with an owner and deadline.

As per coding guidelines, asynchronous web and lifecycle changes require timeout and resource-limit coverage and failure-path tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@History/dev-diary/2026/2026-07-31-dependabot-tls-remediation.md` around lines
43 - 46, Update the TLS connection handling described by the migration to
enforce both a timeout for stalled pre-HTTP handshakes and a configurable
maximum number of in-flight handshakes, releasing capacity on completion,
failure, or expiry; add tests covering timeout expiry and subsequent capacity
recovery. If these controls are intentionally deferred, replace the current
scope statement with an explicit security risk acceptance naming an owner and
remediation deadline.

Source: Coding guidelines

Comment on lines +80 to +99
The broader Green verification was:

- `cargo fmt --all -- --check`;
- `cargo clippy --all-targets --all-features -- -D warnings`;
- `cargo test --all -j 2` (the unrestricted parallel compile exceeded this
Windows host's paging-file limit; two jobs completed the same suite);
- `cargo build --release -j 2`;
- `cargo check --manifest-path fuzz/Cargo.toml --all-targets`;
- 130 ordinary `TestPrograms` cases with the integration runner's timeout,
expected-failure, `--test`, exclusion, and `CI-SKIP` rules;
- `scripts/run_web_tests.ps1` (2/2; its optional OpenSSL-generated TLS fixture
was skipped because OpenSSL is unavailable, while the Rust TLS target ran
all 13 real-socket cases);
- `python scripts/validate_docs_examples.py` (19/19);
- `npm ci --ignore-scripts` and `npm run compile`.

The extension's existing `npm run lint` command cannot find an ESLint
configuration or a matching `src` target, so its `pretest` stops before the
test runner. Invoking the test runner directly produced 24 passing, 2 pending,
and one pre-existing Windows line-ending assertion failure. Static repository

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Do not label failed checks as Green.

Line 80 says “broader Green verification,” but Line 96 reports that npm run lint fails because no ESLint configuration exists, and Lines 98-99 report a failing extension assertion. Rename this section to Verification results, list these failures as unresolved or explicitly accepted pre-existing failures, and state the release decision. Do not claim full validation while required checks fail.

As per coding guidelines, required tests must remain failures and must not be skipped, retried, or presented as Green.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@History/dev-diary/2026/2026-07-31-dependabot-tls-remediation.md` around lines
80 - 99, Rename the “broader Green verification” section to “Verification
results.” Update the verification summary to identify the failing npm lint check
and pre-existing extension assertion as unresolved or explicitly accepted
failures, and state the release decision without claiming full validation or
Green status. Keep required failures visible rather than skipping, retrying, or
reclassifying them as successful checks.

Source: Coding guidelines

@logbie
logbie merged commit 5826723 into main Jul 31, 2026
19 checks passed
@logbie
logbie deleted the warden/dependabot-remediation branch July 31, 2026 09:10
logbie pushed a commit that referenced this pull request Jul 31, 2026
The merge with main combined this branch's `toml` dependency with #679's TLS
change (warp's legacy `tls` feature dropped in favour of tokio-rustls 0.26).
`fuzz/` is a separate workspace with its own lockfile and the fuzz CI job builds
it with `--locked`, so it has to carry both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mrGwj9ef5QPoDfodampdL
logbie added a commit that referenced this pull request Jul 31, 2026
* test: failing coverage for transactions, AEAD, file modes and TOML

Red evidence for #664, #665, #666 and #667. Tests only — no implementation.
Every suite fails because the feature does not exist:

  database_transaction_test  parse error on `in transaction on db:`
  crypto_seal_test           Undefined variable 'seal'
  filesystem_mode_test       Undefined variable 'file_mode' / 'set_file_mode'
  toml_test                  Undefined variable 'parse_toml'

The transaction suite is deliberately file-backed rather than in-memory:
`open database` hands out a five-connection pool, and in-memory SQLite is
special-cased to a single connection, which is exactly what hides #664 from
a test suite.

Adds the two dependencies the implementation will need (chacha20poly1305
for XChaCha20-Poly1305, toml for the parser) so the Red run compiles.

Refs #664, #665, #666, #667

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mrGwj9ef5QPoDfodampdL

* feat: seal and unseal, XChaCha20-Poly1305 authenticated encryption

Closes the gap in #665: the crypto stdlib could mint a key with
`secure_random_bytes of 32` but had nothing to encrypt with it, so a program
holding an API token that must be sent upstream later had only two options —
store it in plaintext, or not store it. Hashing cannot stand in, because a
stored credential has to be recoverable.

    store sealed as seal of token and key
    store token  as unseal of sealed and key

XChaCha20-Poly1305 rather than a 96-bit-nonce AEAD: the 192-bit nonce is wide
enough that a fresh random nonce per message is safe with no counter and no
caller-visible state, so the implementation owns nonce handling outright.
Nonce reuse is the sharpest edge in an AEAD API and a natural-language surface
is the last place to expose it.

An optional third argument supplies associated data, binding a ciphertext to
its context: `seal of token and key and "project:acme/api_key"`. The two- and
three-argument forms are the same call, so the simple form is a strict subset
of the expert one.

Sealed values are `wflseal1:` plus hex of nonce, ciphertext and tag —
self-describing, versioned so the algorithm can change later, and hex to match
the key that `secure_random_bytes` hands back. `unseal` fails closed and
reports every failure identically, so it cannot be used as an oracle to tell a
wrong key from a modified byte from a mismatched context.

Closes #665

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mrGwj9ef5QPoDfodampdL

* feat: file_mode and set_file_mode

Closes #666. A program that writes a secret could not restrict the file, and —
the half that matters just as much — could not verify it was restricted. The
best available mitigation was external, a UMask in a service unit, which is
invisible to the program and unverifiable from inside it. That made the common
check "refuse to start if this config is group- or world-readable" impossible
to express.

    store mode as file_mode of "config.toml"        // "0600"
    store ok   as set_file_mode of "config.toml" and "0600"

Unix gets real POSIX semantics. Mode strings are parsed strictly: three or four
octal digits, and anything else is an error rather than something quietly
masked into a mode the author did not intend.

Windows has no equivalent — modes do not map onto ACLs — so `file_mode` returns
a documented approximation from the read-only attribute and `set_file_mode`
raises an explicit unsupported error. A loud refusal beats a silent no-op that
leaves the caller believing a file is protected.

Closes #666

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mrGwj9ef5QPoDfodampdL

* feat: parse_toml, stringify_toml and stringify_toml_pretty

Closes #667. WFL could read JSON but not TOML, which is what a large share of
config files are actually written in. The reporter's options were to hand-roll
a TOML subset in WFL — fragile to own, and progressively more wrong as it meets
real TOML — or to change the file format; they changed the format and wrote
down the deviation from spec.

The surface mirrors the existing JSON one rather than the `to_toml` spelling
the issue sketched, so the two formats read the same way:

    store config as parse_toml of file_contents
    store out    as stringify_toml of config

Two places TOML is not JSON, handled explicitly rather than fudged:

  * A TOML document is always a table, so `stringify_toml` accepts only an
    object and says so, instead of emitting something that will not parse back.
  * TOML has no null. Absence is a missing key, so a `nothing` value is omitted
    when serializing a table. Inside an array there is no way to leave a hole,
    so that is an error rather than a silent change of length.

Whole numbers serialize as TOML integers, so a round-tripped config reads
`port = 8080` and not `port = 8080.0`.

Closes #667

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mrGwj9ef5QPoDfodampdL

* fix: atomic transaction blocks, and reject transaction SQL sent through execute

Closes #664, which was a silent data-integrity failure rather than a missing
feature. `open database` returns a five-connection pool and every query/execute
takes whichever connection is free, so a hand-written `BEGIN` … `ROLLBACK`
through `execute` ran each statement on a *different* connection: the rollback
undid nothing, the writes survived, and nothing errored. In-memory SQLite is
capped at one connection, so a program could pass its tests and lose data in
production.

Two halves, both needed.

A real construct, which holds one connection for the whole block:

    in transaction on db:
        execute db with "INSERT ..."
        execute db with "UPDATE ..."
    end transaction

It commits when the block finishes and rolls back if anything inside it fails.
Reads inside the block run on the same connection, so they see the block's own
uncommitted writes. `break`, `continue` and `return` are ordinary exits and
commit — only an error rolls back. Nesting on one handle, and closing a database
mid-transaction, are refused with an explanation rather than quietly doing
something surprising.

And a loud failure for the old workaround: `BEGIN`, `COMMIT`, `ROLLBACK`,
`START TRANSACTION`, `SAVEPOINT` and `RELEASE` through query/execute now raise
an error naming the block. Only the leading statement keyword is inspected, so a
column named `begin_at` or a value containing the word "commit" still runs. This
converts silent corruption into a message; no working program depended on the
old behavior, because the old behavior did not work.

`transaction` is deliberately not made a lexer keyword. It is recognized
positionally, after a leading `in` and after `end`, so programs already using
`transaction` as a variable name keep working. A statement beginning with `in`
was previously always a parse error, so the syntax claims nothing that was valid
before.

Also registers the six new builtins from #665/#666/#667 in the two catalogs in
builtins.rs and the static contracts in stdlib/typechecker.rs, which the
runtime-inventory test holds to the runtime registrations.

Closes #664

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mrGwj9ef5QPoDfodampdL

* docs: transactions, seal/unseal, file modes and TOML

Ships the documentation and end-to-end programs for #664, #665, #666 and #667
alongside the implementations.

Docs:
- Docs/04-advanced-features/databases.md gains a Transactions section covering
  the block, what it guarantees, the nesting and close-during restrictions, and
  why BEGIN/COMMIT through execute is now refused.
- Docs/05-standard-library/crypto-module.md documents seal and unseal, leading
  with the distinction the issue turned on: hashing verifies a value you are
  handed, and cannot stand in for a credential you must send upstream later.
- Docs/05-standard-library/filesystem-module.md documents file_mode and
  set_file_mode, including the refuse-to-start check that motivated them and the
  Windows behavior stated outright rather than implied.
- Docs/05-standard-library/toml-module.md is new; the index and overview gain it
  and the module counts move from 11 to 12.
- Both keyword references list `transaction` as a positional marker word that is
  never reserved, so the keyword total correctly stays at 181.

Every example in these pages was run against the release binary rather than
written from memory. Three claims did not survive that and were corrected: the
databases examples had used a bare `execute db with ...` statement, which the
parser reads as subprocess execution, so they now use the `store <name> as
execute ...` form the language actually has; and the TOML page had shown output
in the wrong key order. Writing TOML sorts keys alphabetically and drops
comments, which is deterministic but not faithful to a hand-written file, so the
page now says so.

TestPrograms:
- database_transaction_test.wfl, crypto_seal_test.wfl, file_mode_test.wfl and
  toml_test.wfl, run by the gated integration runner (it detects `describe` and
  adds --test, so a failed assertion exits nonzero).
- The transaction program uses a file-backed SQLite database on purpose;
  in-memory SQLite is capped at one connection and cannot observe #664 at all.
- The file-mode program runs on Windows too, where set_file_mode refuses by
  design, so it probes for support once and asserts whichever contract applies.

Adds a dev diary entry and CHANGELOG entries for all four issues.

Refs #664, #665, #666, #667

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mrGwj9ef5QPoDfodampdL

* fix: review findings — AEAD panic, transaction scoping, concurrency and lint gaps

Addresses the automated review on #676. Two findings were more serious than
anything the original change caught, and one contradicted a claim I had made.

**`seal`/`unseal` panicked on non-ASCII input.** `hex_to_bytes` checked that the
byte length was even and then sliced the `str` at fixed two-byte offsets, which
lands inside a multi-byte character and panics. Keys and sealed values are
exactly the untrusted text this module promises to fail closed on — they arrive
from config files, database rows and HTTP requests — so one accented character
in a stored blob could abort a running server. Decoded over bytes now.

**A transaction was owned by a database handle, not by the handler that opened
it.** Under `main loop concurrently:` handlers share one IoClient and can name
the same global handle, so an unrelated request running an ordinary `execute`
was silently enrolled in another request's transaction and had its write rolled
back with it. Transactions are keyed by `(scope, handle)`, where the scope is
handler-local state swapped per poll by `InstalledRunState` alongside the
existing count-loop and call-stack isolation. `tokio::task_local!` would not
work here: handlers are futures in a `FuturesUnordered` on one thread, not
separate tasks. The new test drives two real requests over a socket and asserts
the bystander's row survives; removing the scope key fails it with both rows
gone.

Also:

- The transaction map's lock was held across the SQL await, so one slow
  statement serialized every database operation in the program. Transactions now
  sit behind their own per-handle lock and the map's lock is held only long
  enough to clone an Arc.
- `begin` had a check-then-insert race in which a second concurrent begin could
  replace and drop a live transaction. The handle is reserved under one atomic
  step before the round-trip, and the reservation is removed if begin fails.
- A leading SQL comment defeated the transaction-control guard outright:
  `-- go\nBEGIN` read as an empty first word and went to the pool. Comments are
  skipped before the first token is read.
- Two of the analyzer's three AST walkers had no arm for the new statement, so a
  `random_seed` call inside a transaction block escaped the insecure-RNG
  security lint and declarations inside one were invisible to unused-variable
  analysis. `seal`/`unseal` join SECURITY_SENSITIVE_BUILTINS.
- `exit` inside a transaction committed its partial work, contradicting the rule
  that an abrupt stop discards it. It now rolls back, like a program that ends
  with a transaction still open. `break`/`continue`/`return` still commit.
- The transaction arm is awaited behind a `Box::pin`. `execute_statement` is a
  plain async fn, so every await in every arm enlarges the one state machine
  each level of statement recursion keeps on the stack.
- `fuzz/Cargo.lock` regenerated for the new dependencies; the fuzz job builds
  with `--locked`.

Documentation corrections, all verified against the binary rather than asserted:

- I wrote that rejecting hand-written transaction SQL broke nothing "because the
  old behavior did not work". That is wrong. In-memory SQLite is capped at one
  connection, so under `sqlite::memory:` the pattern genuinely worked — I
  confirmed it by building main and watching a rollback discard its row. It
  still ships as a hard error, deliberately, because the same program silently
  loses writes against any pooled backend; the changelog now records it under
  Removed and the docs say so outright instead of implying nothing is lost.
- TOML dates round-trip as strings, not dates, and integers above 2^53 are
  rounded by WFL's f64 numbers. The page had claimed the round trip was exact.
  Both are now stated and pinned with tests.

Refs #664, #665, #666, #667

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mrGwj9ef5QPoDfodampdL

* build: refresh fuzz lockfile for the merged dependency set

The merge with main combined this branch's `toml` dependency with #679's TLS
change (warp's legacy `tls` feature dropped in favour of tokio-rustls 0.26).
`fuzz/` is a separate workspace with its own lockfile and the fuzz CI job builds
it with `--locked`, so it has to carry both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mrGwj9ef5QPoDfodampdL

* fix: second review round — transaction scoping fallout, type contracts, lint gaps

CodeRabbit reviewed the merged branch. Four findings were bugs I introduced, two
of them fallout from the transaction-scoping fix in the previous round.

**`close_database` only checked the caller's scope.** Keying transactions by
`(scope, handle)` meant the guard that refuses to close a pool with a live
transaction no longer saw transactions belonging to other scopes — so a handler
could close the pool out from under another handler's transaction, which is the
outcome the guard exists to prevent. It now matches the handle across all scopes;
the scope parameter is gone from `close_database` since it was only misleading.

**The transaction block never reported its completion type.** It used
`check_statement_block`, which discards it, where `TryStatement` — the construct
this block explicitly mirrors — uses `check_statement_block_with_completion`. An
action whose body ends in a transaction was therefore inferred to return nothing,
making `store n as call row_count` / `n times 2` a spurious "Multiply on Nothing
and Number" error on a program that runs correctly. Reproduced before fixing;
`tests/transaction_completion_type_test.rs` pins it with the `try:` shape
alongside as a baseline. (The review also predicted a false error from a declared
return type — not reachable, the parser always sets `return_type: None`.)

**Static contracts for `stringify_toml` contradicted the runtime and the docs.**
The JSON value set was registered, so `stringify_toml of 42` type-checked and
then failed at runtime; `wfl_to_toml_document` accepts only a table, which the
TOML page states and a test already asserted. Narrowed to `map(Text, Any)`.

**A failed rollback flattened the error kind.** `RuntimeError::new` yields
`General`, discarding `Cancelled`/`Timeout`/`ResourceLimit` — kinds that select
`when` clauses and drive concurrent-handler classification, so a client
disconnect inside a transaction with a failing rollback would have been counted
as a structural handler failure. Uses `with_kind` and keeps `err.kind`.

Also:

- `collect_calls_in_statement` ignored `DatabaseQueryStatement`, so a
  `random_seed` or `seal` call in a SQL string or bound parameter escaped the
  security lint, transaction or not.
- Docs said the transaction-control guard applies to `execute`; it applies to
  `query` too, now stated and shown.
- The crypto context example could not have passed docs validation — undefined
  `api_token`, and a deliberately-failing call outside `try:`. Rewritten and run
  against the binary.
- An empty context is now refused rather than being a synonym for "no context".
  Both mapped to the same associated data, so a context built from a missing
  config key produced an unbound ciphertext that looked bound. The review
  suggested documenting the equivalence; erroring matches how the rest of this
  module treats probably-mistaken input and removes the ambiguity instead.
- Added the uniform-failure test the review asked for: tampered ciphertext,
  tampered tag and truncated blob must be indistinguishable, or `unseal` is an
  oracle.
- Test quality: bounded the server join in the handler-scope test, added its
  missing success-path response, corrected a comment claiming concurrency a
  serial test does not exercise, and made the "does not contain the plaintext"
  TestPrograms case assert that property.

Not changed: the review flagged the `StaticAnalyzer` import in
`tests/transaction_analyzer_walk_test.rs` as unused. It provides
`check_unused_variables`, and clippy runs `-D warnings`.

Refs #664, #665, #666, #667

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mrGwj9ef5QPoDfodampdL

* test: restore coverage for the transaction header expression walk

CodeRabbit's finding that `calls_in_the_transaction_header_are_collected` did
not actually test the header was auto-marked resolved by e90c10d, but that
commit *replaced* the weak test with two about database statement operands
rather than strengthening it. Net effect: the header walk in
`collect_calls_in_statement` lost its only test, weak as it was.

The header takes a full primary expression — `in transaction on call get_db:`
and `in transaction on get_db of 1:` both parse — so a security-sensitive call
really can sit there, and the line is reachable rather than defensive. The new
test puts the only crypto call in the header and keeps `random_seed` outside it,
so it fails if the walk stops descending. Verified as a genuine Red by removing
`collect_calls_in_expression(db, out)` and watching it fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mrGwj9ef5QPoDfodampdL

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant