Skip to content

Add transactions, AEAD encryption, file modes, and TOML support - #676

Merged
logbie merged 14 commits into
mainfrom
claude/transactions-encryption-permissions-toml-cpsmw9
Jul 31, 2026
Merged

Add transactions, AEAD encryption, file modes, and TOML support#676
logbie merged 14 commits into
mainfrom
claude/transactions-encryption-permissions-toml-cpsmw9

Conversation

@logbie

@logbie logbie commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR closes four related issues (#664#667) filed by a user building a configuration subsystem in WFL. Together they address critical gaps: atomic database operations, secret encryption, file permission control, and configuration file parsing.

Key Changes

#664: Database Transactions

  • Problem: BEGIN/ROLLBACK sent as SQL statements landed on different pooled connections, silently failing to roll back writes. In-memory SQLite (single connection) masked the bug; production databases exposed it.
  • Solution:
    • New in transaction on db: ... end transaction block syntax that pins a single connection for the entire block
    • Transactions commit on normal exit, roll back on error
    • Reject raw transaction-control SQL (BEGIN, COMMIT, ROLLBACK, etc.) sent through query/execute with a clear error message
    • Added DbTransaction enum and db_transactions map to IoClient to track open transactions per handle
    • Comprehensive test coverage in tests/database_transaction_test.rs and TestPrograms/database_transaction_test.wfl using file-backed SQLite (not in-memory) to catch the original defect

#665: Authenticated Encryption (AEAD)

  • Problem: secure_random_bytes of 32 minted keys with no way to use them for storing secrets that must be recovered later
  • Solution:
    • seal of <plaintext> and <key> — XChaCha20-Poly1305 encryption with automatic nonce handling
    • unseal of <sealed> and <key> — decryption with authentication tag verification
    • Self-describing versioned format (wflseal1: prefix) for future algorithm changes
    • Nonce freshness guaranteed per message (no state/counter needed)
    • Tests in tests/crypto_seal_test.rs and TestPrograms/crypto_seal_test.wfl covering round-trip, tampering detection, and oracle resistance

#666: File Permissions

  • Problem: No way to set or verify file modes; a program writing a secret config file could not enforce 0600 or verify it was enforced
  • Solution:
    • file_mode of <path> — read permissions as 4-character octal string ("0600", "0644", etc.)
    • set_file_mode of <path> and <mode> — set permissions with strict octal parsing
    • Unix: real POSIX semantics
    • Windows: explicit unsupported error (never silent no-op)
    • Tests in tests/filesystem_mode_test.rs and TestPrograms/file_mode_test.wfl with platform-aware assertions

#667: TOML Support

  • Problem: No TOML parsing; config files are typically TOML, not JSON
  • Solution:
    • parse_toml of <text> — read TOML into WFL objects/lists/scalars
    • stringify_toml of <object> — write WFL object back to TOML
    • stringify_toml_pretty of <object> — formatted output
    • Mirrors JSON module surface (parse_json, stringify_json, stringify_json_pretty) for consistency
    • Dates/times preserved as text (TOML's offset/local distinction does not map cleanly to WFL types)
    • nothing/null values omitted from tables (TOML has no null); error in arrays (no way to leave holes)
    • New src/stdlib/toml.rs module with full type mapping
    • Tests in tests/toml_test.rs and TestPrograms/toml_test.wfl

Implementation Details

  • Transaction safety: DbTransaction enum mirrors DbPool (Postgres/MySql/Sqlite variants); DbTarget enum routes statements to pool or transaction
  • Nonce handling: XChaCha20-Poly1305 uses 24-byte random nonce per seal; no reuse risk
  • Mode parsing: Strict 3–4 octal

https://claude.ai/code/session_015mrGwj9ef5QPoDfodampdL


Open in Devin Review

Summary by CodeRabbit

  • New Features
    • Added scoped database transactions with automatic commit and rollback.
    • Added authenticated encryption helpers for protecting recoverable secrets.
    • Added TOML parsing and serialization, including pretty formatting.
    • Added file permission inspection and updates.
  • Bug Fixes
    • Raw transaction-control SQL is now rejected outside transaction blocks.
    • Improved transaction isolation and lifecycle handling.
  • Documentation
    • Added comprehensive guidance for transactions, encryption, TOML, and file permissions.
  • Tests
    • Added broad coverage for success cases, invalid input, platform differences, concurrency, and rollback behavior.

claude added 7 commits July 31, 2026 06:53
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
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
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
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
…gh 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
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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: affd7eb82e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/interpreter/mod.rs Outdated
Comment on lines +2550 to +2551
if let Some(tx) = transactions.get_mut(handle_id) {
return database::run_query(database::DbTarget::Transaction(tx), sql, params).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep transactions scoped to the initiating handler

Under main loop concurrently, all handler futures share this IoClient, so while one handler has a transaction open for a global database handle, an unrelated handler's ordinary query on that handle is automatically executed inside the first handler's transaction. Its writes can therefore be committed or rolled back by the wrong request; transaction ownership must include the handler/execution context rather than only handle_id.

AGENTS.md reference: AGENTS.md:L148-L151

Useful? React with 👍 / 👎.

Comment thread src/interpreter/mod.rs Outdated
Comment on lines +2549 to +2551
let mut transactions = self.db_transactions.lock().await;
if let Some(tx) = transactions.get_mut(handle_id) {
return database::run_query(database::DbTarget::Transaction(tx), sql, params).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid holding the global transaction mutex during database I/O

When a transactional query is slow or stalled, this awaits the entire database operation while holding db_transactions; every concurrent db_query, db_execute, begin, commit, rollback, and close—including operations on unrelated handles—must wait for it. In a concurrent web loop, one slow database can therefore wedge otherwise independent requests, so transactions need per-handle synchronization or ownership that does not retain the global map lock across I/O.

AGENTS.md reference: AGENTS.md:L148-L151

Useful? React with 👍 / 👎.

Comment thread src/interpreter/mod.rs Outdated
Comment on lines +2510 to +2513
self.db_transactions
.lock()
.await
.insert(handle_id.to_string(), tx);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reserve transaction handles atomically before beginning

When two concurrent handlers enter a transaction on the same handle, both can pass the earlier contains_key check while their asynchronous database::begin calls are in flight, after which the second insertion replaces and drops the first transaction. Subsequent statements can then run on the wrong transaction, and one handler's commit leaves the other with "No transaction is open"; reserve the handle under one atomic state transition before awaiting the database.

AGENTS.md reference: AGENTS.md:L138-L144

Useful? React with 👍 / 👎.

Comment thread src/interpreter/database.rs Outdated
Comment on lines +181 to +188
let first_word: String = sql
.trim_start()
.chars()
.take_while(|c| c.is_ascii_alphabetic())
.collect();

if first_word.is_empty() {
return Ok(());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip SQL comments before checking transaction controls

If transaction-control SQL begins with a normal SQL comment, such as -- start transaction\nBEGIN or /* transaction */ BEGIN, first_word is empty and this returns success. The BEGIN then goes through the pool—the exact path this guard is intended to prevent—so later statements may use different connections and writes intended for rollback can survive; inspect the first SQL token after whitespace and comments.

Useful? React with 👍 / 👎.

Comment thread src/stdlib/toml.rs
::toml::Value::Integer(i) => Value::Number(i as f64),
::toml::Value::Float(f) => Value::Number(f),
::toml::Value::Boolean(b) => Value::Bool(b),
::toml::Value::Datetime(dt) => Value::Text(Arc::from(dt.to_string())),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve TOML date and time types when stringifying

For any TOML date, time, or datetime, parsing converts the value to ordinary WFL text, and wfl_to_toml later serializes every text value as a quoted TOML string. Thus parsing and rewriting released = 2026-07-31 produces released = "2026-07-31", silently changing the field's TOML type; retain enough type information to emit a TOML datetime again.

Useful? React with 👍 / 👎.

Comment thread src/stdlib/toml.rs
fn toml_to_wfl(value: ::toml::Value) -> Value {
match value {
::toml::Value::String(s) => Value::Text(Arc::from(s)),
::toml::Value::Integer(i) => Value::Number(i as f64),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve or reject TOML integers outside exact f64 range

Valid TOML integers above 2^53 are not exactly representable as WFL's f64 number, so this cast silently changes values—for example, 9007199254740993 becomes 9007199254740992. A parse/edit/stringify cycle can therefore corrupt IDs or configuration values; preserve large integers in an exact representation or reject them instead of rounding.

Useful? React with 👍 / 👎.

@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 8 potential issues.

Open in Devin Review

Comment thread src/stdlib/crypto.rs
Comment on lines +1037 to +1045
fn hex_to_bytes(hex: &str) -> Option<Vec<u8>> {
if !hex.len().is_multiple_of(2) {
return None;
}
(0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok())
.collect()
}

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.

🔴 Programs crash outright when a non-English character is used in an encryption key or sealed value

The key and sealed text are chopped into two-byte pieces (&hex[i..i + 2] at src/stdlib/crypto.rs:1043) without checking they are plain ASCII, so any value containing an accented or non-Latin character makes the whole program crash instead of reporting a bad key.

Impact: A user-supplied or misconfigured key/sealed string containing a multi-byte character aborts the running program (and any server it is hosting) rather than raising a catchable error.

Mechanism: byte-index slicing of a UTF-8 string at a non-character boundary

hex_to_bytes only validates that hex.len() (a byte count) is even, then slices &hex[i..i + 2] for each even byte offset. For a string such as "€a" the length is 4 bytes, so the loop runs, but byte offset 2 lands in the middle of the 3-byte , and Rust's str indexing panics with "byte index 2 is not a char boundary".

Both entry points are reachable from WFL source: parse_seal_key (src/stdlib/crypto.rs:1050) passes the caller's key straight in, and native_unseal (src/stdlib/crypto.rs:1165) passes the hex body of the sealed value. Every other malformed input is handled by returning None → a clean RuntimeError, so this is the one path that escapes the module's own fail-closed contract.

Suggested change
fn hex_to_bytes(hex: &str) -> Option<Vec<u8>> {
if !hex.len().is_multiple_of(2) {
return None;
}
(0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok())
.collect()
}
fn hex_to_bytes(hex: &str) -> Option<Vec<u8>> {
let bytes = hex.as_bytes();
if !bytes.len().is_multiple_of(2) {
return None;
}
bytes
.chunks_exact(2)
.map(|pair| {
let text = std::str::from_utf8(pair).ok()?;
u8::from_str_radix(text, 16).ok()
})
.collect()
}
Open in Devin Review

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

Comment thread src/interpreter/mod.rs Outdated
Comment on lines +2548 to +2556
{
let mut transactions = self.db_transactions.lock().await;
if let Some(tx) = transactions.get_mut(handle_id) {
return database::run_query(database::DbTarget::Transaction(tx), sql, params).await;
}
}
let pool = self.get_database(handle_id).await?;
database::run_query(database::DbTarget::Pool(&pool), sql, params).await
}

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.

🔴 A slow database statement inside a transaction freezes every other database operation in the program

The shared record of open transactions is locked for the entire time a statement runs (self.db_transactions.lock().await at src/interpreter/mod.rs:2549 and src/interpreter/mod.rs:2567), so while one slow statement inside a transaction is in flight, every other database read or write in the program — even on a completely different database — has to wait.

Impact: Under concurrent request handling one slow transactional query stalls unrelated handlers that touch any database.

Mechanism: tokio mutex guard held across the SQL await

In db_query/db_execute the guard returned by db_transactions.lock().await is still alive when database::run_query(...).await / run_execute(...).await is entered, because the return happens inside the if let Some(tx) = transactions.get_mut(handle_id) borrow. Every other call to db_query/db_execute, and begin_transaction/commit_transaction/rollback_transaction/close_database (src/interpreter/mod.rs:2478, 2500, 2522, 2533), must first acquire that same mutex, so they all block for the duration of the in-transaction statement.

The non-transactional path is unaffected (the guard is dropped at the end of the block before get_database), so the stall is specific to the new transaction routing. Docs/04-advanced-features/databases.md and tests/database_transaction_test.rs::transaction_does_not_block_other_handles_on_the_same_file assert that another handle stays usable, but that test is sequential and cannot observe the contention.

A fix is to keep the map lock short: take the transaction out of the map (or wrap each DbTransaction in its own per-handle mutex/Rc<RefCell<...>>) so the shared map is not held while awaiting SQL.

Prompt for agents
In src/interpreter/mod.rs, IoClient::db_query and IoClient::db_execute route statements to an open transaction by locking the shared `db_transactions` tokio Mutex and then awaiting the SQL call while still holding the guard. Because every other database entry point (db_query, db_execute, begin_transaction, commit_transaction, rollback_transaction, close_database) locks the same map, a long-running statement inside a transaction serializes all database activity in the interpreter, including work on unrelated handles. Under `main loop concurrently:` this means one slow handler blocks unrelated handlers, which the repository's testing policy (§11.3) explicitly targets. Restructure so the map lock is only held long enough to look up/borrow the per-handle transaction — e.g. temporarily remove the DbTransaction from the map for the duration of the statement and put it back afterwards (taking care to restore it on the error path), or store each transaction behind its own per-handle lock so the shared map guard is released before awaiting.
Open in Devin Review

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

Comment on lines +1133 to +1138
Statement::TransactionStatement { db, body, .. } => {
self.mark_used_in_expression(db, usages);
for statement in body {
self.mark_used_variables(statement, usages);
}
}

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.

🟡 Static analysis silently skips everything written inside a transaction block

Two of the analyzer's tree walks were never taught about the new transaction block (src/analyzer/static_analyzer.rs:1133 adds only the usage walk), so declarations and function calls written inside such a block are invisible to the unused-variable check and to the insecure-random-seeding security lint.

Impact: A call to the unsafe random-seeding builtin placed inside a transaction block escapes the security warning that would otherwise block it.

Mechanism: two sibling walkers missing the new statement arm

mark_used_variables gained a Statement::TransactionStatement arm, but:

  • collect_calls_in_statement (src/analyzer/static_analyzer.rs:199-300) has no arm, so rng_security_ingredients / check_insecure_rng_seeding never see random_seed or the security-sensitive crypto builtins called inside in transaction on db: ... end transaction.
  • collect_variable_declarations (src/analyzer/static_analyzer.rs:880-930) has no arm either, even though its comment says it deliberately mirrors mark_used_variables; variables declared inside the block are therefore never tracked for the unused-variable diagnostic.

Both are false negatives (no spurious warnings), but the first is a security lint bypass.

Prompt for agents
src/analyzer/static_analyzer.rs has three AST walkers that must stay in sync for block-bearing statements: collect_calls_in_statement (used by rng_security_ingredients / the insecure-RNG-seeding lint), collect_variable_declarations (unused-variable analysis), and mark_used_variables. This PR added a Statement::TransactionStatement arm only to mark_used_variables, so calls and declarations inside `in transaction on db: ... end transaction` are invisible to the other two — notably letting a `random_seed` call inside a transaction block evade the security lint. Add matching recursion into the transaction body in the other two walkers.
Open in Devin Review

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

Comment on lines +180 to +204
pub fn reject_transaction_control_sql(sql: &str) -> Result<(), String> {
let first_word: String = sql
.trim_start()
.chars()
.take_while(|c| c.is_ascii_alphabetic())
.collect();

if first_word.is_empty() {
return Ok(());
}

let lowered = first_word.to_ascii_lowercase();
if TRANSACTION_CONTROL_KEYWORDS.contains(&lowered.as_str()) {
return Err(format!(
"'{first_word}' controls a transaction, and sending it through query/execute does \
not work: each statement runs on its own pooled connection, so the transaction \
would not cover the statements you meant it to. Use a transaction block instead:\n\
\n in transaction on db:\n execute db with \"...\"\n end transaction\n\
\nThe block holds one connection for its whole body, commits when it finishes, \
and rolls back if anything inside it fails."
));
}

Ok(())
}

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.

🟡 Existing programs that manage transactions with SQL text now fail where they previously worked

Statements whose first word is one of the transaction-control words are now refused outright (reject_transaction_control_sql at src/interpreter/database.rs:180), which breaks programs that used this pattern successfully against an in-memory database, with no deprecation period.

Impact: A working program that wraps writes in hand-written transaction statements stops running after upgrading, instead of being warned first.

Why the "no working program could depend on it" argument is incomplete

The PR justifies the hard rejection on the grounds that pooled connections made the old pattern a no-op. That is true for pooled backends, but connect caps in-memory SQLite at a single connection (src/interpreter/database.rs:99-103), so for sqlite::memory: — the configuration most WFL tests and examples use — BEGIN/COMMIT/ROLLBACK through execute genuinely worked. Those programs now raise a runtime error.

AGENTS.md/CLAUDE.md state that backward compatibility is sacred and that existing WFL programs must not break "without the documented deprecation path" in GOVERNANCE.md; the change ships as a straight rejection with no deprecation window. Consider warning (or accepting on single-connection SQLite) for a release before erroring, or documenting the break through the governance deprecation process.

Open in Devin Review

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

Comment thread src/stdlib/crypto.rs
Comment on lines +1214 to +1216
// Authenticated encryption
env.define_native("seal", native_seal);
env.define_native("unseal", native_unseal);

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.

🔍 New security-relevant builtins are not in the insecure-RNG lint's sensitive list

seal/unseal are added to the crypto module but not to SECURITY_SENSITIVE_BUILTINS (src/analyzer/static_analyzer.rs:117-134), which drives the "program seeds the RNG and then does crypto" lint. In practice the nonce comes from rand::rng() (the OS CSPRNG), exactly like secure_random_bytes, so random_seed does not weaken it — but secure_random_bytes and hmac_sha256 are on that list for the same reason, so the omission is an inconsistency worth deciding on deliberately.

Open in Devin Review

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

Comment thread src/interpreter/mod.rs Outdated
Comment on lines +7321 to +7331
match outcome {
Ok((value, control_flow)) => {
// The block finished without an error. `break`, `continue`
// and `return` are ordinary exits, not failures, so they
// commit too — the work inside completed.
self.io_client
.commit_transaction(&handle)
.await
.map_err(|e| RuntimeError::new(e, *line, *column))?;
Ok((value, control_flow))
}

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.

🔍 Control-flow exits from a transaction block commit, including exit

The commit path takes any Ok((value, control_flow)) (src/interpreter/mod.rs:7322-7331), which the docs describe for break/continue/return. Statement::ExitStatement also propagates as a non-error control flow, so exit inside a transaction block commits the partial work before the program stops. That may be the intended reading of "only errors roll back", but it is not covered by the docs or tests and is the one exit where a user is most likely to expect abandonment rather than commit.

Open in Devin Review

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

Comment thread src/stdlib/crypto.rs
Comment on lines +1037 to +1045
fn hex_to_bytes(hex: &str) -> Option<Vec<u8>> {
if !hex.len().is_multiple_of(2) {
return None;
}
(0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok())
.collect()
}

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.

🟥 Malformed hex input to seal/unseal panics the interpreter instead of failing closed

hex_to_bytes (src/stdlib/crypto.rs:1037-1045) validates only that the byte length is even and then slices the string at fixed two-byte offsets. Untrusted text — a key from configuration, or a sealed value read from a file, database, or HTTP request — containing a multi-byte UTF-8 character causes a panic ("byte index is not a char boundary") rather than the documented uniform unseal error. Both parse_seal_key (src/stdlib/crypto.rs:1050) and native_unseal (src/stdlib/crypto.rs:1165) feed caller-controlled text into it, so an attacker who controls a stored/sealed blob can crash the process.

Open in Devin Review

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

Comment thread src/builtins.rs
Comment on lines +56 to +58
// Authenticated encryption (implemented in stdlib/crypto.rs)
"seal",
"unseal",

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.

🟨 New encryption builtins are not covered by the insecure-RNG-seeding security lint

SECURITY_SENSITIVE_BUILTINS (src/analyzer/static_analyzer.rs:117-134) drives the lint that blocks a program which both calls random_seed and performs a cryptographic operation. The new seal/unseal builtins were added to src/builtins.rs and src/stdlib/typechecker.rs but not to this list, so a program that seeds the general RNG and then performs authenticated encryption is no longer flagged. Additionally, collect_calls_in_statement was not taught about the new transaction block, so any security-sensitive call written inside in transaction on db: ... end transaction escapes the lint entirely.

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: 5 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: 59c6d4ee-3087-43f7-ae97-fee0615d0dfd

📥 Commits

Reviewing files that changed from the base of the PR and between f4f72dc and 6511986.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • fuzz/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • CHANGELOG.md
  • Cargo.toml
  • Docs/04-advanced-features/databases.md
  • Docs/05-standard-library/crypto-module.md
  • History/dev-diary/2026/2026-07-31-transactions-aead-file-modes-toml.md
  • TestPrograms/crypto_seal_test.wfl
  • src/analyzer/static_analyzer.rs
  • src/builtins.rs
  • src/interpreter/mod.rs
  • src/stdlib/crypto.rs
  • src/stdlib/typechecker.rs
  • src/typechecker/mod.rs
  • tests/crypto_seal_test.rs
  • tests/database_transaction_test.rs
  • tests/transaction_analyzer_walk_test.rs
  • tests/transaction_completion_type_test.rs
  • tests/transaction_handler_scope_test.rs
📝 Walkthrough

Walkthrough

This PR adds scoped database transactions, authenticated encryption, file-mode operations, and TOML parsing and serialization. It updates parser, interpreter, standard-library registration, typechecking, documentation, and tests for these features.

Changes

Database transactions

Layer / File(s) Summary
Transaction syntax and semantic contracts
src/parser/..., src/analyzer/..., src/typechecker/mod.rs, src/linter/mod.rs, Docs/04-advanced-features/databases.md, Docs/reference/*
Adds contextual transaction parsing, transaction AST handling, semantic checks, analyzer traversal, nesting checks, and transaction documentation.
Scoped transaction execution
src/interpreter/database.rs, src/interpreter/mod.rs
Adds scoped transaction storage, pinned connections, commit and rollback handling, SQL-control rejection, lifecycle checks, and transaction-aware database routing.
Transaction behavior validation
tests/database_transaction_test.rs, tests/transaction_handler_scope_test.rs, tests/transaction_analyzer_walk_test.rs, TestPrograms/database_transaction_test.wfl, CHANGELOG.md, Engineering/evidence/*, History/dev-diary/*
Covers atomicity, control flow, SQL guards, concurrency, handler isolation, analyzer traversal, and documented behavior.

Authenticated encryption

Layer / File(s) Summary
Seal and unseal implementation
Cargo.toml, src/stdlib/crypto.rs, src/builtins.rs, src/stdlib/typechecker.rs, src/analyzer/static_analyzer.rs, Docs/05-standard-library/crypto-module.md
Adds XChaCha20-Poly1305 seal and unseal with optional context, versioned output, validation, and sensitive-buffer cleanup.
Encryption behavior validation
tests/crypto_seal_test.rs, TestPrograms/crypto_seal_test.wfl
Tests round trips, nonce freshness, context binding, tampering, malformed input, invalid keys, Unicode, and failure handling.

Filesystem modes

Layer / File(s) Summary
File-mode implementation and contracts
src/stdlib/filesystem.rs, src/builtins.rs, src/stdlib/typechecker.rs, Docs/05-standard-library/filesystem-module.md
Adds file_mode and set_file_mode with strict octal validation, Unix permission updates, and Windows-specific behavior.
File-mode behavior validation
tests/filesystem_mode_test.rs, TestPrograms/file_mode_test.wfl
Tests permission formatting, Unix mode changes, malformed modes, missing paths, and Windows behavior.

TOML support

Layer / File(s) Summary
TOML conversion and registration
Cargo.toml, src/stdlib/toml.rs, src/stdlib/mod.rs, src/builtins.rs, src/stdlib/typechecker.rs, Docs/05-standard-library/toml-module.md, Docs/05-standard-library/index.md, Docs/05-standard-library/overview.md
Adds TOML parsing, compact and pretty serialization, WFL value conversion, table-root validation, registration, contracts, and library documentation.
TOML behavior validation
tests/toml_test.rs, TestPrograms/toml_test.wfl, History/dev-diary/*
Tests scalar and nested values, arrays, duplicate keys, round trips, omission rules, empty documents, date conversion, and numeric precision.

Estimated code review effort: 5 (Critical) | ~120 minutes

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 and concisely summarizes the pull request's four primary features.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% 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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/transactions-encryption-permissions-toml-cpsmw9

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.

…nd 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

logbie commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed c8a9612 addressing the review. Most of it was right, and two findings were more serious than anything I'd caught myself.

Fixed

  • hex_to_bytes panicked on non-ASCII input — it validated byte length was even, then sliced the str at fixed two-byte offsets, landing inside a multi-byte character. Keys and sealed values are exactly the untrusted text this module promises to fail closed on, so one accented character in a stored blob could abort a running server. Decoded over bytes now; tests for a non-ASCII key, a non-ASCII sealed value, and a multi-byte character inside an otherwise well-formed key.
  • Transactions were owned by a handle, not by the handler that opened one — under main loop concurrently: an unrelated request's ordinary execute was enrolled in another request's transaction and rolled back with it. Now keyed by (scope, handle), where the scope is handler-local state swapped per poll by InstalledRunState, alongside the existing count-loop/call-stack isolation. task_local! wouldn't work here — handlers are futures in a FuturesUnordered on one thread, not separate tasks. tests/transaction_handler_scope_test.rs drives two real requests over a socket; with the scope key removed it fails with both rows gone.
  • Map lock held across the SQL await — transactions moved behind their own per-handle lock; the map's lock is now held only long enough to clone an Arc.
  • begin check-then-insert race — the handle is reserved under one atomic step before the round-trip, and the reservation is removed if begin fails.
  • Leading SQL comment defeated the transaction guard-- go\nBEGIN read as an empty first word. Comments are skipped before the first token.
  • Analyzer walkerscollect_calls_in_statement and collect_variable_declarations had no arm for the new statement, so random_seed inside a transaction block escaped the insecure-RNG lint. Both fixed; seal/unseal added to SECURITY_SENSITIVE_BUILTINS.
  • exit inside a transaction committed — it now rolls back, matching what already happens when a program ends with a transaction open. break/continue/return still commit.

On backward compatibility — this one I got wrong and the review was right. I'd claimed the rejection broke nothing "because the old behavior did not work". In-memory SQLite is capped at one connection, so under sqlite::memory: it genuinely did work; I confirmed by building main and watching a rollback correctly discard its row. It still ships as a hard error, deliberately: the same program silently loses writes against any pooled backend, so the behavior being removed is "passes in development, corrupts data in production". Now recorded under Removed in the changelog with the reasoning stated outright, rather than implied away.

Documented rather than fixed (both now stated plainly and pinned with tests, replacing a doc claim that the round trip was exact):

  • TOML dates round-trip as strings, not dates. Preserving the type needs a tagged value in WFL's model — out of scope here, and silently lossy was the actual bug.
  • Integers above 2⁵³ are rounded. That's WFL's f64 number model, identical for JSON; fixing it is a language-wide change, not a TOML one.

Also: regenerated fuzz/Cargo.lock (the fuzz job builds --locked and my new deps weren't in it), and the transaction arm is now awaited behind a Box::pinexecute_statement is a plain async fn, so every await in every arm enlarges the single state machine each level of statement recursion keeps on the stack.

Verification: 2051 tests passing (0 failed), clippy clean at -D warnings, integration gate 134/0, docs validator 21/21, hygiene static + working-tree clean.

Still watching the Windows concurrent_disconnect_burst_test stack overflow — it passed on main's last run and doesn't reproduce on Linux, so I'm checking whether this push clears it before drawing a conclusion.


Generated by Claude Code

logbie commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

CI status on c8a9612 — everything green except one job, and that failure is not from this PR.

Green: Build/Test/Clippy, Run WFL Programs (both OSes), Database Tests, Fuzz targets compile, Repository Hygiene (both OSes), Check formatting, CodeQL, config-lint, claude-review.

Red: Integration Tests (Linux) — response_stream_backpressure_test::test_early_chunk_is_visible_before_the_body_completes, with stream transport failed instead of ending cleanly: error decoding response body. (Integration Tests (Windows) shows cancelled, not failed — matrix fail-fast off the Linux job.)

Why I'm not fixing it here. It's flaky, and the evidence is on this same commit: Build, Test, Clippy runs cargo test --workspace and passed, while Integration Tests runs cargo test --test '*' and failed. Same code, same test, two different outcomes in the same CI run. Locally it's 8/8 clean in isolation and clean across two full-suite runs.

It's also untouched by this PR — no database, crypto, TOML or filesystem code is on that path. My changes to swap_run_state only run on the concurrent-handler path (IsolatedHandler); that test uses a serial main loop.

The likely mechanism, for whoever picks it up: the handler does close outbreak, the loop exits, interpret() returns, and the server thread's Runtime is then dropped — which aborts the in-flight connection task. If the terminating chunk hasn't reached the client yet, the client sees a truncated body instead of clean EOF. That would be a real clean-shutdown gap (testing.md §11.3) in the streaming work from #641, not a test-only defect — which is exactly why I'm not papering over it by adding a sleep to the test. That would be manufactured green under §8.2.

Happy to fix it properly in a separate change if wanted. I don't have permission to re-run failed jobs from here, so re-running this one needs a maintainer.

For the record on the earlier Windows failure I flagged: Integration Tests (Windows) was already failing on 438780a, the commit this branch was cut from. The repair landed on main in 5f0356d5, which is now merged in here — and Run WFL Programs (Windows) passes on this commit.


Generated by Claude Code

claude added 2 commits July 31, 2026 09:29
…ncryption-permissions-toml-cpsmw9

# Conflicts:
#	Cargo.toml
#	fuzz/Cargo.lock
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 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main (5826723) and resolved the conflicts — pushed as f4f72dc.

Two conflicts, both in dependency files:

Cargo.toml — the only one needing a judgement call. Both sides changed the same block: this branch added toml next to a warp pinned with features = ["tls"], while #679 removed that feature to drop the vulnerable rustls 0.22 chain and route TLS through tokio-rustls 0.26. Resolved by keeping both: main's TLS change is a security fix and is preserved exactly as written, with toml added alongside. The old features = ["tls"] pin and its comment are gone, not resurrected.

fuzz/Cargo.lock — generated, so regenerated from main's version rather than hand-merged. fuzz/ is a separate workspace and its CI job builds --locked, so it needs both the new toml dependency and #679's TLS change; committed separately in f4f72dc.

Re-verified against the merged tree rather than assuming the merge was harmless, since #679 touches the web server this branch also runs tests against:

  • cargo test --all2057 passed, 0 failed (includes web_server_tls_test, which fix: remove vulnerable legacy TLS dependency chain #679 modified)
  • clippy -D warnings — clean
  • integration gate — 134 passed, 0 failed
  • docs validator — 21/21
  • cargo check --locked --manifest-path fuzz/Cargo.toml — clean
  • repo hygiene, static + working-tree — clean
  • cargo fmt --check — clean

Generated by Claude Code

logbie commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Integration Tests (Windows) failed on f4f72dc with STATUS_STACK_OVERFLOW, this time in execute_file_test (previously concurrent_disconnect_burst_test). I measured it rather than assuming, because a failure that appears on a PR and moves between test binaries looks exactly like a regression.

It isn't one. Reproducing the Windows condition on Linux with RUST_MIN_STACK — building normally first, then running the built binary under a constrained stack — gives identical results for origin/main and this branch:

Stack origin/main this branch
1 MB overflow overflow
1.2 MB overflow overflow
1.4 MB overflow overflow
1.6 MB overflow overflow
1.8 MB overflow overflow
2 MB pass pass

Same threshold at every size. execute_file_test needs ~2 MB of stack on main today, with nothing from this PR involved. It also already failed this way on main at 438780a, before this branch existed.

The mechanism is that execute_statement is a plain async fn recursing through execute_block, so every await in every arm enlarges one state machine that is paid per nesting level. That is why I boxed this PR's new TransactionStatement arm — and the measurement above confirms the branch is no worse than main despite adding a block-bearing statement.

Filed as #681 with the full reproduction and two suggested directions (box the remaining large arms; set RUST_MIN_STACK for the Windows job as an interim CI mitigation). I've kept it out of this PR: fixing it means touching arms unrelated to #664#667, and adding the CI env var here would quietly paper over a real interpreter limit that affects deeply nested user programs too.

Happy to take #681 next if you want it prioritised — it's the thing currently making the Windows gate flap.


Generated by Claude Code

@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: 13

🧹 Nitpick comments (7)
src/stdlib/crypto.rs (2)

1167-1170: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the extra indentation from the error text.

The continuation line inside the format string carries five extra spaces. Those spaces reach the user in the rendered message. Align the continuation with the other error strings in this module.

✏️ Proposed change
             format!(
                 "unseal: this is not a sealed value. Expected text beginning with \
-                     '{SEAL_V1_PREFIX}', as produced by `seal`."
+                 '{SEAL_V1_PREFIX}', as produced by `seal`."
             ),
🤖 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 `@src/stdlib/crypto.rs` around lines 1167 - 1170, Update the unseal error
message in the format! call to remove the extra leading spaces from the
continued string literal, so the rendered error text has no unintended
indentation and matches the alignment used by other error strings in the module.

1111-1114: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

An absent context and an empty context are the same value.

context.as_deref().unwrap_or("") maps None and Some("") to the same associated data. A value sealed by seal of secret and key therefore opens under unseal of sealed and key and "", and a value sealed with "" opens with the context omitted. The documentation presents the context as a binding, so the equivalence should be explicit.

Two options:

  • Document the equivalence in Docs/05-standard-library/crypto-module.md and add a test that asserts it.
  • Distinguish the two cases by prefixing the AAD with a presence marker. That changes the sealed-value contract, so it needs a format version bump.

Keep the current behavior if it is intentional. State it either way.

🤖 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 `@src/stdlib/crypto.rs` around lines 1111 - 1114, Decide and document the
intended equivalence between absent and empty context in the sealing/unsealing
API. If intentional, document it in crypto-module.md and add a test covering
both directions; otherwise, distinguish None from Some("") using a versioned
sealed-value format and update corresponding seal/unseal handling.
src/builtins.rs (1)

517-520: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Give the TOML arities their own section header.

Line 520 registers the TOML functions under the === JSON FUNCTIONS === header. Every other family in this match has its own header. Add one so the grouping stays readable.

♻️ Proposed change
         // === JSON FUNCTIONS ===
         // Single argument functions
         "parse_json" | "stringify_json" | "stringify_json_pretty" => 1,
+
+        // === TOML FUNCTIONS ===
+        // Single argument functions
         "parse_toml" | "stringify_toml" | "stringify_toml_pretty" => 1,
🤖 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 `@src/builtins.rs` around lines 517 - 520, In the arity match near the JSON
function registrations, add a dedicated TOML functions section header before the
parse_toml/stringify_toml arm, keeping the existing JSON header associated only
with the JSON functions.
tests/crypto_seal_test.rs (1)

204-237: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add one test that asserts the failure messages are identical.

Lines 3-6 state that every failure must be "rejected the same way", and src/stdlib/crypto.rs funnels hex, length, nonce, decrypt, and UTF-8 failures into a single failed() message. No test asserts that. A later change that splits those messages would still pass every test here.

expect_unseal_failure already returns the error string, so the check is cheap.

💚 Proposed additional test
/// The uniform-failure property itself: a tampered ciphertext, a tampered tag,
/// and a truncated blob must be indistinguishable from each other.
#[tokio::test]
async fn every_unseal_failure_reports_the_same_message() {
    let sealed = seal_once("top secret").await;
    let body_len = sealed.split_once(':').unwrap().1.len();

    let tampered_ciphertext = flip_hex_digit(&sealed, 60);
    let tampered_tag = flip_hex_digit(&sealed, body_len - 1);
    let truncated = sealed[..sealed.len() - 8].to_string();

    let first = expect_unseal_failure(&tampered_ciphertext, "must fail").await;
    for (blob, what) in [
        (tampered_tag, "tampered tag"),
        (truncated, "truncated blob"),
    ] {
        let other = expect_unseal_failure(&blob, "must fail").await;
        assert_eq!(
            first, other,
            "a {what} must be indistinguishable from a tampered ciphertext, \
             or `unseal` becomes an oracle"
        );
    }
}

As per coding guidelines: "Classify behavioral-change risk before testing; concurrency, cancellation, lifecycle, streaming, untrusted input, crypto/secrets, and backward compatibility are R3 and require negative or failure-path tests plus risk-triggered coverage."

🤖 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/crypto_seal_test.rs` around lines 204 - 237, Add a Tokio test alongside
the existing tampering tests that compares the error strings returned by
expect_unseal_failure for tampered ciphertext, tampered tag, and a truncated
sealed blob. Assert every failure message is identical, preserving the uniform
failed() behavior across untrusted-input failure paths.

Source: Coding guidelines

tests/transaction_handler_scope_test.rs (3)

130-130: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The interleaving depends on sleep margins.

The transaction holds for 600 ms and the client waits 200 ms before it sends /plain. On a loaded CI machine, /plain can arrive after the transaction has already rolled back. Both assertions still pass in that case, so the test silently stops exercising the concurrent window that the module comment describes.

Consider a readiness signal instead of fixed sleeps, for example a /tx-ready marker row that the harness polls before it sends /plain, or a longer hold inside the transaction.

🤖 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/transaction_handler_scope_test.rs` at line 130, Replace the fixed
sleep-based coordination around the transaction hold in the concurrent test with
a deterministic readiness signal, such as polling for a `/tx-ready` marker
emitted while the transaction is active, before sending `/plain`. Ensure the
marker is produced only after the transaction has acquired the intended state,
preserving the test’s concurrent interleaving assertions.

126-135: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a response on the transaction success path.

The /tx handler responds only in the when error: arm. If the transaction ever completes without an error, the handler sends no response. The client at Line 150 uses reqwest, which applies no request timeout by default, so tx.await at Line 173 would block instead of failing. A regression in transaction failure handling then appears as a hang, not as a test failure.

Respond after end transaction as well, and assert the failure text as today.

♻️ Proposed success-path response
                     try:
                         in transaction on db:
                             store a as execute db with "INSERT INTO writes (tag) VALUES ('rolled-back')"
                             wait for 600 milliseconds
                             store boom as execute db with "INSERT INTO no_such_table (x) VALUES (1)"
                         end transaction
+                        respond to req with "tx-committed"
                     when error:
                         respond to req with "tx-rolled-back"
                     end try
🤖 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/transaction_handler_scope_test.rs` around lines 126 - 135, Update the
/tx handler’s transaction flow so it responds after end transaction on the
success path, while retaining the existing "tx-rolled-back" response in the when
error arm. Keep the test assertion validating the failure response text.

89-99: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Bound the server join with a timeout.

shutdown discards the result of the /shutdown request and then joins the server thread without a bound. If the request fails or the WFL loop does not exit, server.join() blocks forever. The test then hangs until the harness kills it, which gives a weak failure signal.

Wrap the join in tokio::time::timeout and fail with a clear message.

♻️ Proposed bounded shutdown
 async fn shutdown(port: u16, server: std::thread::JoinHandle<()>) {
     let _ = reqwest::Client::new()
         .get(format!("http://127.0.0.1:{port}/shutdown"))
         .send()
         .await;
-    match tokio::task::spawn_blocking(move || server.join()).await {
-        Ok(Ok(())) => {}
-        Ok(Err(panic)) => std::panic::resume_unwind(panic),
-        Err(join_err) => panic!("server join task failed: {join_err}"),
+    let join = tokio::task::spawn_blocking(move || server.join());
+    match tokio::time::timeout(Duration::from_secs(30), join).await {
+        Ok(Ok(Ok(()))) => {}
+        Ok(Ok(Err(panic))) => std::panic::resume_unwind(panic),
+        Ok(Err(join_err)) => panic!("server join task failed: {join_err}"),
+        Err(_) => panic!("server thread did not exit within 30s after /shutdown"),
     }
 }
🤖 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/transaction_handler_scope_test.rs` around lines 89 - 99, Update
shutdown to check the /shutdown request result and wrap the spawn_blocking
server.join() operation in tokio::time::timeout with a suitable duration.
Preserve panic propagation for thread and blocking-task failures, and panic with
a clear timeout message when the server does not terminate promptly.
🤖 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 `@Docs/04-advanced-features/databases.md`:
- Around line 163-181: Update the “Do not send BEGIN or COMMIT through execute”
section to state that the transaction-control guard rejects both query and
execute APIs. Add a query example or otherwise explicitly document query
rejection, while preserving the existing explanation of pooled connections and
accepted SQL containing these words.

In `@Docs/05-standard-library/crypto-module.md`:
- Around line 583-587: Update the WFL example around seal/unseal to initialize
project_key and api_token before sealing, retain the successful unseal
demonstration, and wrap the wrong-context unseal in a try/when error block so
validation exits successfully while showing the refusal. Add the displayed token
and refusal message as demonstrated, then validate the example with the required
WFL tools and docs-example tracking workflow.

In `@src/analyzer/static_analyzer.rs`:
- Around line 237-244: Update collect_calls_in_statement to handle
Statement::DatabaseQueryStatement by traversing its sql and parameters
expressions, including the corresponding execute operands, so calls are detected
both directly and inside Statement::TransactionStatement bodies. Add a
regression test covering random_seed and seal/unseal calls in these database
expressions.

In `@src/interpreter/mod.rs`:
- Around line 2486-2503: Update close_database to reject closing a handle when
any entry in db_transactions has the same handle_id, regardless of scope,
instead of only checking the (scope, handle_id) key. Preserve scope for
diagnostics if needed and report the scope holding the open transaction while
retaining the existing error behavior.
- Around line 6467-6483: Update the rollback-failure branch in the transaction
error handling to append the rollback message while preserving the original
err.kind, rather than constructing a RuntimeError with the default General kind.
Keep the original line and column and ensure Cancelled, Timeout, ResourceLimit,
and other error kinds continue driving handler classification correctly.
- Around line 6435-6442: The transaction registry must be drained when handlers
or interpreter runs are abandoned. Add a scope-synchronous cleanup helper that
removes entries from db_transactions and rolls back each live DbTransaction,
then invoke it from IsolatedHandler::drop, OutboundStreamCleanup::drop, and
interpret_inner’s pre-run cleanup while preserving normal commit/rollback paths.

In `@src/stdlib/toml.rs`:
- Around line 118-193: Update the TOML function registrations in register_toml
so stringify_toml and stringify_toml_pretty require map(Text, Any) inputs. Keep
parse_toml unchanged, and align both stringification overloads with
wfl_to_toml_document’s required top-level table shape so non-map values are
rejected during type checking.

In `@src/stdlib/typechecker.rs`:
- Around line 349-370: Update register_toml so stringify_toml and
stringify_toml_pretty are registered only with map(Type::Text, Type::Any) as
their argument type, matching wfl_to_toml_document’s top-level table
requirement. Retain Nothing, Boolean, Number, Text, and list(Type::Any) only as
nested TOML value types in the descriptive logic, not as function overloads.

In `@src/typechecker/mod.rs`:
- Around line 5853-5874: Update the TransactionStatement arm in
check_statement_types to call check_statement_block_with_completion(body) and
assign its result to self.current_statement_completion. Preserve the existing
database-type validation and shared-scope behavior, while ensuring the
transaction statement reports the body’s completion type instead of retaining
Type::Nothing.

In `@TestPrograms/crypto_seal_test.wfl`:
- Around line 24-28: Update the test assertion in “the sealed value does not
contain the plaintext” so it verifies that sealed does not contain the plaintext
string “correct-horse-battery-staple”; preserve the existing prefix assertion if
desired, but ensure the test’s stated property is explicitly checked.

In `@tests/database_transaction_test.rs`:
- Around line 593-612: Update the comment above the formatted test program to
remove the claim that `main loop concurrently` runs interleaved bodies, since
this test executes top-level statements serially. Retain the explanation that
the unrelated query must progress while the transaction remains open, which
would otherwise cause a timeout or deadlock, and leave the test code unchanged.

In `@tests/transaction_analyzer_walk_test.rs`:
- Around line 11-14: Remove the unused StaticAnalyzer symbol from the
static_analyzer import in transaction_analyzer_walk_test.rs, retaining
rng_security_ingredients and all other imports unchanged.
- Around line 60-75: Update calls_in_the_transaction_header_are_collected so the
transaction header expression itself contains one of the tracked calls,
replacing the bare db reference with an appropriate call expression while
retaining a separate tracked call in the body or following statement. Adjust
assertions as needed so the test specifically fails when the header expression
is not walked.

---

Nitpick comments:
In `@src/builtins.rs`:
- Around line 517-520: In the arity match near the JSON function registrations,
add a dedicated TOML functions section header before the
parse_toml/stringify_toml arm, keeping the existing JSON header associated only
with the JSON functions.

In `@src/stdlib/crypto.rs`:
- Around line 1167-1170: Update the unseal error message in the format! call to
remove the extra leading spaces from the continued string literal, so the
rendered error text has no unintended indentation and matches the alignment used
by other error strings in the module.
- Around line 1111-1114: Decide and document the intended equivalence between
absent and empty context in the sealing/unsealing API. If intentional, document
it in crypto-module.md and add a test covering both directions; otherwise,
distinguish None from Some("") using a versioned sealed-value format and update
corresponding seal/unseal handling.

In `@tests/crypto_seal_test.rs`:
- Around line 204-237: Add a Tokio test alongside the existing tampering tests
that compares the error strings returned by expect_unseal_failure for tampered
ciphertext, tampered tag, and a truncated sealed blob. Assert every failure
message is identical, preserving the uniform failed() behavior across
untrusted-input failure paths.

In `@tests/transaction_handler_scope_test.rs`:
- Line 130: Replace the fixed sleep-based coordination around the transaction
hold in the concurrent test with a deterministic readiness signal, such as
polling for a `/tx-ready` marker emitted while the transaction is active, before
sending `/plain`. Ensure the marker is produced only after the transaction has
acquired the intended state, preserving the test’s concurrent interleaving
assertions.
- Around line 126-135: Update the /tx handler’s transaction flow so it responds
after end transaction on the success path, while retaining the existing
"tx-rolled-back" response in the when error arm. Keep the test assertion
validating the failure response text.
- Around line 89-99: Update shutdown to check the /shutdown request result and
wrap the spawn_blocking server.join() operation in tokio::time::timeout with a
suitable duration. Preserve panic propagation for thread and blocking-task
failures, and panic with a clear timeout message when the server does not
terminate promptly.
🪄 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: 89172486-68af-41cf-ab8c-6918549280a8

📥 Commits

Reviewing files that changed from the base of the PR and between 5826723 and f4f72dc.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • fuzz/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (38)
  • CHANGELOG.md
  • Cargo.toml
  • Docs/04-advanced-features/databases.md
  • Docs/05-standard-library/crypto-module.md
  • Docs/05-standard-library/filesystem-module.md
  • Docs/05-standard-library/index.md
  • Docs/05-standard-library/overview.md
  • Docs/05-standard-library/toml-module.md
  • Docs/reference/keyword-reference.md
  • Docs/reference/reserved-keywords.md
  • Engineering/evidence/red-664-667-transactions-aead-modes-toml.txt
  • History/dev-diary/2026/2026-07-31-transactions-aead-file-modes-toml.md
  • TestPrograms/crypto_seal_test.wfl
  • TestPrograms/database_transaction_test.wfl
  • TestPrograms/file_mode_test.wfl
  • TestPrograms/toml_test.wfl
  • src/analyzer/mod.rs
  • src/analyzer/static_analyzer.rs
  • src/builtins.rs
  • src/interpreter/database.rs
  • src/interpreter/mod.rs
  • src/linter/mod.rs
  • src/parser/ast.rs
  • src/parser/mod.rs
  • src/parser/stmt/database.rs
  • src/parser/stmt/mod.rs
  • src/stdlib/crypto.rs
  • src/stdlib/filesystem.rs
  • src/stdlib/mod.rs
  • src/stdlib/toml.rs
  • src/stdlib/typechecker.rs
  • src/typechecker/mod.rs
  • tests/crypto_seal_test.rs
  • tests/database_transaction_test.rs
  • tests/filesystem_mode_test.rs
  • tests/toml_test.rs
  • tests/transaction_analyzer_walk_test.rs
  • tests/transaction_handler_scope_test.rs

Comment thread Docs/04-advanced-features/databases.md Outdated
Comment thread Docs/05-standard-library/crypto-module.md
Comment thread src/analyzer/static_analyzer.rs
Comment thread src/interpreter/mod.rs
Comment thread src/interpreter/mod.rs
Comment on lines +6435 to +6442
self.io_client
.begin_transaction(self.tx_scope.get(), &handle)
.await
.map_err(|e| RuntimeError::new(e, line, column))?;

// Statements inside share the enclosing scope, like `try:`, so a
// variable set inside the block is still readable after it.
let outcome = self.execute_block(body, Rc::clone(&env)).await;

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: find every reader/writer of the transaction registry and any scope-based cleanup.
set -euo pipefail

rg -n -C 6 'db_transactions' --type=rust
rg -n -C 4 'tx_scope' --type=rust
# Cleanup sites that already reclaim handler-owned resources.
rg -n -C 6 'fn drop' --type=rust -g 'src/interpreter/mod.rs'

Repository: WebFirstLanguage/wfl

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "tracked interpreter files:"
git ls-files | rg '(^|/)interpreter/mod\.rs$|src/interpreter' || true

echo
echo "search db_transactions:"
rg -n -C 8 'db_transactions' . || true

echo
echo "search tx_scope:"
rg -n -C 4 'tx_scope' . || true

echo
echo "search begin/commit/rollback_transaction:"
rg -n -C 6 'begin_transaction|commit_transaction|rollback_transaction' . || true

echo
echo "grep cleanup/drop in src/interpreter/mod.rs if present:"
if [ -f src/interpreter/mod.rs ]; then
  rg -n -C 8 'fn drop|impl .*Drop|cleanup|reclaim|drain|isolate|handler' src/interpreter/mod.rs || true
fi

Repository: WebFirstLanguage/wfl

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "IsolatedHandler and interpreter lifecycle context:"
sed -n '830,1030p' src/interpreter/mod.rs
sed -n '6080,6165p' src/interpreter/mod.rs

echo
echo "transaction implementation context:"
sed -n '2514,2610p' src/interpreter/mod.rs
sed -n '6430,6490p' src/interpreter/mod.rs
sed -n '7450,7465p' src/interpreter/mod.rs

echo
echo "all close_database callsites:"
rg -n -C 3 'close_database\(self\.tx_scope\.get\(\)' src/interpreter/mod.rs

Repository: WebFirstLanguage/wfl

Length of output: 22488


Roll back abandoned transactions when the handler disappears.

IoClient::begin_transaction already reserves (scope, handle) before awaiting the handler; normal completion paths commit or rollback, but IsolatedHandler::drop, OutboundStreamCleanup::drop, and interpret_inner's pre-run cleanup do not drain db_transactions. Add a scope-synchronous rollback step that removes those entries and rolls back any live DbTransaction, so cancelled or dropped handlers do not leave pinned pool connections reachable from the registry.

🤖 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 `@src/interpreter/mod.rs` around lines 6435 - 6442, The transaction registry
must be drained when handlers or interpreter runs are abandoned. Add a
scope-synchronous cleanup helper that removes entries from db_transactions and
rolls back each live DbTransaction, then invoke it from IsolatedHandler::drop,
OutboundStreamCleanup::drop, and interpret_inner’s pre-run cleanup while
preserving normal commit/rollback paths.

Comment thread src/typechecker/mod.rs
Comment thread TestPrograms/crypto_seal_test.wfl
Comment thread tests/database_transaction_test.rs Outdated
Comment on lines +11 to +14
use wfl::analyzer::Analyzer;
use wfl::analyzer::static_analyzer::{StaticAnalyzer, rng_security_ingredients};
use wfl::lexer::lex_wfl_with_positions;
use wfl::parser::Parser;

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the public surface used by this test.
fd -t f 'static_analyzer.rs' src | while IFS= read -r f; do
  echo "=== $f ==="
  rg -n 'pub fn rng_security_ingredients|pub struct|seed_site|security_site' "$f"
done
# Check whether StaticAnalyzer is referenced anywhere in this test file.
rg -n 'StaticAnalyzer' tests/transaction_analyzer_walk_test.rs
# Check the check_unused_variables signature.
rg -n --type=rust -C2 'fn check_unused_variables' src

Repository: WebFirstLanguage/wfl

Length of output: 1578


Remove the unused StaticAnalyzer import.

StaticAnalyzer is imported in tests/transaction_analyzer_walk_test.rs, but this file only uses rng_security_ingredients. Since imports are checked by Clippy with unused_imports, keep only the symbol required by this test.

The other static-analyzer symbols are available under src/analyzer/static_analyzer.rs.

🧹 Proposed import fix
-use wfl::analyzer::static_analyzer::{StaticAnalyzer, rng_security_ingredients};
+use wfl::analyzer::static_analyzer::rng_security_ingredients;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
use wfl::analyzer::Analyzer;
use wfl::analyzer::static_analyzer::{StaticAnalyzer, rng_security_ingredients};
use wfl::lexer::lex_wfl_with_positions;
use wfl::parser::Parser;
use wfl::analyzer::Analyzer;
use wfl::analyzer::static_analyzer::rng_security_ingredients;
use wfl::lexer::lex_wfl_with_positions;
use wfl::parser::Parser;
🤖 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/transaction_analyzer_walk_test.rs` around lines 11 - 14, Remove the
unused StaticAnalyzer symbol from the static_analyzer import in
transaction_analyzer_walk_test.rs, retaining rng_security_ingredients and all
other imports unchanged.

Source: Coding guidelines

Comment thread tests/transaction_analyzer_walk_test.rs Outdated
…s, 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

logbie commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the CodeRabbit round in e90c10d. Four were bugs I'd introduced, two of them fallout from the transaction-scoping fix in the previous round — a good reminder that a fix is a change like any other.

Fixed

  • close_database only checked the caller's scope. Keying by (scope, handle) meant the guard that refuses to close a pool with a live transaction stopped seeing other scopes' transactions — so a handler could close the pool out from under another handler's transaction, exactly what the guard exists to prevent. Now matches the handle across all scopes; the scope parameter is dropped 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 it explicitly mirrors — uses check_statement_block_with_completion. Reproduced first: an action ending in a transaction is inferred to return nothing, so store n as call row_count / n times 2 gives "Cannot perform Multiply operation on Nothing and Number" on a program that runs fine. Pinned in tests/transaction_completion_type_test.rs, with the try: shape alongside as a baseline.
  • stringify_toml's static contract contradicted its runtime and my own docs. I'd registered the JSON value set, so stringify_toml of 42 type-checked then failed at runtime, while 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, which select when clauses and drive concurrent-handler classification. Now uses with_kind.
  • collect_calls_in_statement ignored DatabaseQueryStatement — a random_seed or seal call in a SQL string or bound parameter escaped the security lint, transaction or not.
  • Docs: the guard applies to query as well as execute (verified both); the crypto context example couldn't have passed validation (undefined api_token, deliberately-failing call outside try:) and now runs.
  • Tests: added the uniform-failure test — tampered ciphertext, tampered tag and truncated blob must be indistinguishable or unseal is an oracle; bounded the server join; added the missing success-path response; corrected a comment claiming concurrency a serial test doesn't exercise; made the "does not contain the plaintext" case assert that property.

One decided differently. Absent and empty context were the same associated data (unwrap_or("")). The suggestion was to document the equivalence; I made an empty context an error instead. Passing the argument means you intend to bind the value, so a context assembled from a config key that turned out to be missing should be reported rather than quietly producing an unbound ciphertext that looks bound. That matches how this module already treats probably-mistaken input — strict mode strings, exact key lengths — and removes the subtlety rather than explaining it.

One skipped. The StaticAnalyzer import in tests/transaction_analyzer_walk_test.rs is flagged as unused, but it's the trait providing check_unused_variables, and clippy runs -D warnings — an unused import couldn't have got this far.

Two scoped out, both worth their own change rather than being folded in here: rolling back transactions abandoned by a dropped handler (IsolatedHandler::drop and friends don't drain the registry — real, and larger than it looks since Drop is sync and rollback is async), and replacing the fixed sleeps in the handler-scope test with a readiness signal. Say the word and I'll take either.

Verification: 2064 tests passing (0 failed), clippy clean at -D warnings, integration gate 134/0, docs validator 21/21, fuzz --locked clean, hygiene static + working-tree clean.


Generated by Claude Code

claude and others added 3 commits July 31, 2026 10:16
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
@logbie
logbie merged commit ada9557 into main Jul 31, 2026
19 checks passed
@logbie
logbie deleted the claude/transactions-encryption-permissions-toml-cpsmw9 branch July 31, 2026 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants