Skip to content

Rust #325: Waterline v1 workflow import - #768

Merged
rmcdaniel merged 16 commits into
mainfrom
rust-325-waterline-import
Oct 26, 2026
Merged

rmcdaniel merged 16 commits into
mainfrom
rust-325-waterline-import

Conversation

@rmcdaniel

@rmcdaniel rmcdaniel commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Rust #325: native now serves POST /api/workflows/import/waterline-v1 as published Server 2.11.1 does (LegacyV1ProjectionController / LegacyV1Projection), and reads and refuses projected runs as PHP does. After this, no run operation route remains missing from the native port. This is the next single activation after #764, and main (2dd58d25, with #764) is merged in.

What it does

  • Import.
    • Laravel validation of source_id, workflow and dry_run, then PHP's normalization refusals in PHP's order.
    • The identity is deterministic (mappedIdentity). Opaque payloads, unsupported fields, events in PHP's order, a SHA-256 dedupe key over PHP's canonical JSON, and PHP's history_size_bytes.
    • already_projected, namespace collision, storage inconsistency, changed source and native identity collision are decided from the run and instance that hold the identity in any namespace, since ids are global.
    • One transaction writes the instance, run, history and summary of one standalone, read-only run. It writes no links, references to other rows, signing keys or tasks.
  • Reads. Describe, run describe, history, the runs list, the workflow list and history export report the projection as PHP does, with migration_projection.
  • Refusals. Signal, query, update, cancel, terminate, repair, archive and redrive refuse a projected run with v1_projection_read_only before reading their bodies (a query after its input validates). Cooperative cancellation refuses with legacy_v1_operation_not_supported.
  • Changes that reach ordinary runs.
    • The export checksum and signature use PHP's canonical JSON, so non-ASCII text is escaped as PHP escapes it and PHP's importer verifies a native export.
    • List times use PHP's toJSON with six fractional digits.
    • A history event without a recorded time reads as null.

Security notes, from the #764 review

Review fixes (head 66767200)

Security's review found two gaps that published 2.11.1 shares (#769, #770):

  • Appends are read-only.
    • A message stream append to a projection is refused with 409 v1_projection_read_only.
    • The refusal happens at the route before the body is read, in the service before parsing, and in the store under its lock.
    • Malformed input is not counted against a projection.
  • Imports are bounded.
    • An import is bounded by DW_V2_LIMIT_HISTORY_TRANSACTION_SIZE and _BYTES.
    • The count is checked from the source's row counts before any row is copied or any event is built. History bytes are added up event by event and stop at the first event over the limit.
    • A refusal is 409 v1_projection_too_large with limit {dimension, used, limit}, returned before any row is read or written.
  • The settings are honored and visible.
    • Both settings are now honored in config_audit.
    • Operator metrics, health and backend capabilities publish their active values.

The answer, with every control, is #768 (comment).

Qualification

  • Waterline-import fixture, recorded from published 2.11.1 (tests/Fixtures/ServerParity/waterline-import.json, with its contract and probe): passes against native on SQLite, PostgreSQL 17, MySQL 8.0 and MariaDB 10.11, recorded with the original commit. Hosted CI reruns it on every database.
  • After merging main (2dd58d25), locally in the pinned CI image:
  • The unserved-routes fixture drops the Waterline route.

The one merge conflict was in docs/rust-server-port.md. This branch removes the "Run operations" missing section, and #764's embedded-import notes moved into the parity-surface paragraph.

🤖 Generated with Claude Code

rmcdaniel and others added 11 commits October 25, 2026 02:19
…published 2.11.1

POST /api/workflows/import/waterline-v1 (LegacyV1ProjectionController and
LegacyV1Projection) from v1 flow-detail templates, in the probe's own
namespaces:

- The request gates, Laravel validation, and each normalization refusal,
  alone and together.
- operator_id precedence over id, and a dry run that writes nothing.
- The projection with idempotent, trimmed, changed and cross-namespace
  repeats.
- Every v1 status, timestamp offsets and unparseable times, unusable rows,
  signal-name fallbacks, and the fewest accepted fields.
- A native identity collision from another namespace.
- The projected runs through describe, run, runs, history, history export
  and list.
- The read-only refusal of signal, query, update, cancel, terminate,
  repair, archive, redrive and cooperative cancellation, on running and
  completed projections.

Projected identities are hashes of the source id, which carries the
recording prefix, so the probe computes them as mappedIdentity does and
records them, their event ids, dedupe keys, export checksums and
import-time sort keys as labels. Import and export times are placeholders.
Status projections get distinct source times, so the list order does not
depend on hashed ids.

Two export details are made target- and prefix-independent:

- The summary's history size is recorded without the bytes the prefix
  contributes.
- The probe recomputes each export's checksum over PHP's canonical JSON of
  the bundle and records whether it matches. Timeline, wait and
  projection-source reporting belong to the general history export and are
  left out.

Recorded three times under prefixes of 2, 7 and 9 characters, identical.

Stream items and close are left out until native serves those routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native now serves POST /api/workflows/import/waterline-v1 as published
Server 2.11.1 does (LegacyV1ProjectionController and LegacyV1Projection),
and reads and refuses projected runs as PHP does.

Import:

- Laravel validation of source_id, workflow and dry_run, then each
  normalization refusal in PHP's order, with the first error's reason,
  message and remediation at the top of the report.
- The deterministic identity (mappedIdentity), the opaque payloads, the
  unsupported fields, and events ordered by boundary, whole-second time
  (untimed first) and source order. The dedupe key is SHA-256 over PHP's
  canonical JSON, and history_size_bytes is PHP's json_encode length of
  each event. A unit test pins both against PHP's own output.
- already_projected, namespace collision, storage inconsistency, changed
  source and native identity collision, decided from the run and instance
  holding the identity in any namespace (ids are global).
- One transaction writes the instance, run, history and summary. Ordinary
  date columns keep the source's wall-clock time as Eloquent stores it;
  history times go through the UTC cast.

Reads and refusals:

- Describe and run describe report the projection as formatRun does
  without a control-plane description: the stored codec, no routing,
  toJSON progress time, every action off, action_blocked_reason and
  migration_projection. A failed projection carries the terminal failure
  PHP derives from its activity failure events.
- History carries migration_projection. The runs list leaves the memo out.
  The workflow list orders a projection by its summary sort time and
  accepts runs without a start time or queue.
- The history export carries migration_projection inside its integrity
  block.
- Signal, query, update, cancel, terminate, repair and archive refuse a
  projected current run with v1_projection_read_only before reading their
  bodies; a query once its input passes validation; redrive on its source
  run. Cooperative cancellation refuses with
  legacy_v1_operation_not_supported.

Three changes reach ordinary runs:

- The export checksum and signature are computed over PHP's canonical
  JSON. Non-ASCII text is now escaped as PHP escapes it, so PHP's importer
  verifies a native export.
- List times use PHP's toJSON with six fractional digits.
- A history event without a recorded time reads as null.

Qualified locally: the waterline-import fixture passes against native on
SQLite, PostgreSQL 17, MySQL 8.0 and MariaDB 10.11; every shared fixture
passes against native on SQLite; and the SQLite all-targets suite passes
(269 unit, 454 execution).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	.github/workflows/server-parity.yml
# Conflicts:
#	.github/workflows/server-parity.yml
Records the Waterline v1 import route as served as well: 132 of the 175
published route methods on this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-325-waterline-import

Native now serves 140 route methods: main's 138 plus both imports. The
comparator node test list keeps both sides' tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-325-waterline-import

The comparator node test list keeps both sides' tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both published 2.11.1 cross-namespace defects the workflow-import fixture
bounds are now Server #765. The contract reads only each defect's
published steps, so the added issue field changes no comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…line-import

# Conflicts:
#	docs/rust-server-port.md
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security review of exact head a197d6eb85bb3d8a410da72501fe6cde9ad8ffd8 requests changes before merge.

The new read-only projection guard omits POST /api/workflows/{workflow_id}/message-streams/{stream_name}/messages. For a projected run whose imported status is pending, running, or waiting, the current path is:

Runtime::append_message() → message_streams::append_message() → deliver_runtime_signal().

That path treats the projected run as active and can create inbound-stream/item rows, a signal command/history event, and a workflow task. Invalid append input can also call record_malformed_message() and persist stream diagnostics. This gives a projection advertised as “standalone, read-only, no tasks” a native execution path.

The published PHP MessageStreamController / MessageStreamService path has the same missing LegacyV1Projection fence, so reproducing it is not safe parity. Record and coordinate that public defect with the PHP owner.

Before this PR can merge:

  • Refuse message-stream append for a projected current run before reading or validating the request body, consistently with the other projection operation fences.
  • Add a second guard under the locked service/storage transaction so direct/internal callers cannot bypass the HTTP middleware.
  • Add valid- and invalid-body controls for an active projection proving the response is v1_projection_read_only and that stream rows/items, signal/command/history rows, tasks, run/instance counters, and summaries are byte-for-byte unchanged.
  • Include a direct service/storage call control, not only an HTTP-route test.
  • Preserve ordinary message-stream behavior and retain read-only diagnostics.

No Rust tests were run on the lab host. This finding is from the exact source tree and call-path review; hosted checks are still running.

@rmcdaniel

Copy link
Copy Markdown
Member Author

A second exact-head blocker is unbounded import amplification.

normalize() accepts every array member from logs, signals, and exceptions, builds and sorts one projected event per member, and computes the complete encoded history size. waterline_write() then performs one history-row insert per event in a single transaction. There is no hard limit on any source-row count, the projected event count, or history_size_bytes.

The 2 MiB HTTP payload limit is not an adequate transaction bound: an array of tiny objects can contain hundreds of thousands of members, which this path expands into a very large in-memory projection and a long locked sequence of inserts. The published PHP LegacyV1Projection path has the same gap.

Add explicit public hard ceilings for projected event count and total projected history bytes, shared with the PHP contract. Refuse before existing-row discovery and before opening the write transaction. Add exact-limit and one-over controls on SQLite, PostgreSQL, MySQL, and MariaDB proving that refusal writes no instance, run, history, summary, task, link, stream, or signal row. The response should make the exceeded dimension and remediation machine-readable.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Clarification to the unbounded-import blocker: the limit-check ordering is part of the boundary.

A check performed after events() or after the current history_size_bytes fold would protect the database transaction but would still allow the attacker-controlled cloning, event construction, sort, and complete encoding that create the in-memory/CPU amplification. The count-derived ceiling must be checked with overflow-safe arithmetic immediately after the three source row lists are identified and before unsupported_fields, for_hash, events(), sorting, or per-event payload cloning. The projected count must include the start event and any terminal event.

Then construct/encode only within that bounded count and accumulate projected history bytes with checked arithmetic, stopping on the first event that would exceed the byte ceiling. Do not first build an unbounded event vector or a complete over-limit encoded history merely to discover that it is too large. The existing HTTP body limit remains an independent outer bound, not a substitute for either projection limit.

Please include source-level controls that distinguish an early count refusal from a post-construction refusal, in addition to the exact-limit/one-over database no-write matrix already requested.

rmcdaniel and others added 2 commits October 25, 2026 23:04
Security's review of #768 found two gaps that published 2.11.1 shares
(Server #769, #770):

- A message stream append to a projection delivered a signal and created a
  workflow task. Native now refuses it with 409 v1_projection_read_only at
  the route before the body is read, in the service before parsing, and
  again in the store under its lock. Malformed input is not counted
  against a projection.
- An import had no size limit. Native now applies
  DW_V2_LIMIT_HISTORY_TRANSACTION_SIZE and _BYTES to a projection, which is
  one transaction writing its history. The event count is known from the
  source's row counts and is checked before any row is copied or any event
  is built; history bytes are added up event by event and the first event
  past the limit stops the measure. A refusal is a 409 report with reason
  v1_projection_too_large and limit {dimension, used, limit}, before any
  row is read or written.

Both limits are now honored settings: config_audit lists
HISTORY_TRANSACTION_SIZE as honored and HISTORY_TRANSACTION_BYTES as a
native-honored name with its legacy alias. Operator metrics, the health
snapshot and the backend capabilities publish their active values (0 when
disabled) instead of a fixed 5000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Twenty-four appends race the import that creates a Waterline v1
projection on a four-thread runtime. Each answers 404 instance_not_found
or 409 v1_projection_read_only, and afterwards only the projection's own
rows exist: the store decides under the lock every durable transaction
takes, before the duplicate-replay update and the new item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Answer to Security's review of a197d6eb:

New exact head: 66767200024bf09fcfd6ba505b823834b1f7c942. Both defects also exist in published 2.11.1 and are filed for the General owner as #769 (stream appends) and #770 (import size).

Message stream appends to a projection

The append (POST /api/workflows/{id}/message-streams/{stream}/messages) is refused with 409 v1_projection_read_only at three points:

  1. Route. read_only_guard matches the append before the body is read, so a valid body, an invalid body and a non-object body all answer the same 409.
  2. Service. Runtime::append_message checks the current run before parsing the body.
  3. Store. append_message checks again inside its transaction, under the lock every durable transaction takes, before the duplicate-replay update and before the new item. Its outcome cannot differ from the state it then writes. record_malformed_message counts nothing against a projection.

The refusal carries the read-only body (operation: message_stream_append) with the stream's fields (accepted: false, outcome: rejected, stream_name).

Controls

  • Execution: valid, invalid and non-object appends leave every table unchanged:

    • instances, runs, history, summaries, tasks;
    • commands, signal records, inbound streams and items.

    The run's describe is unchanged too.

  • Lib (direct calls): the service refuses valid and invalid bodies, a direct store append is refused by the store's own check, and a direct malformed count writes nothing.

  • Concurrency: 24 appends race the import that creates the projection, on a 4-thread runtime. Each answers 404 instance_not_found (before the import commits) or 409 v1_projection_read_only (after). Afterwards only the projection's own rows exist: no stream, item, signal, command or task.

Import size

A projection is one transaction writing its history, so native applies the completion-budget limits: DW_V2_LIMIT_HISTORY_TRANSACTION_SIZE (default 5,000 events) and _BYTES (default 16 MiB). 0 disables a limit.

  • Count first. In normalize_bounded, right after the identity checks, the projected count comes from the source's row counts: the start event, one event per log, signal and exception, and the terminal event when the status has one. It is checked before any row is copied, before unsupported_fields, the dedupe hash, events() or the sort.
  • Then bytes. History bytes (json_encode of each event as PHP holds it) are added up event by event with saturating arithmetic. The first event past the limit stops the measure.
  • Refusal. A 409 report with status: rejected, reason: v1_projection_too_large and limit: {dimension, used, limit}, returned before any existing row is read or written.

Controls

  • Source level (TooLarge.encoded is how many events were built and measured):
    • The exact count is admitted.
    • One over is refused with encoded: 0, so no event was built.
    • The exact byte total is admitted.
    • One byte short is measured to the last event.
    • A limit below the first event stops after measuring that one event.
  • Database:
    • Events: 3 is admitted with the limit at 3. 4 and 501 are refused, with no row written.
    • Bytes: the size is measured from a stored projection's export. The exact value is admitted and one less is refused, with no row written.

Shared with #766

These now honor both settings, so the review of #766 applies here too:

  • config_audit lists HISTORY_TRANSACTION_SIZE as Honored, and HISTORY_TRANSACTION_BYTES as a native-honored name with its legacy alias. Strict mode starts with either set, and legacy names give only the deprecation warning.
  • Operator metrics, the health snapshot and the backend capabilities (configured and effective) publish the active values instead of a fixed 5000. Controlled with 4321/987654, disabled values (0) and the defaults.
  • The port doc's environment counts are corrected: 164 contract settings, of which native honors 49, ignores 51 and refuses 64 non-default values.

#766 adds the row limit to the same surfaces.

Local qualification at 66767200024bf09fcfd6ba505b823834b1f7c942

  • SQLite: cargo test --lib --test execution passes, 293 unit and 505 execution tests, before the race test was added.
  • PostgreSQL 17, MySQL 8.0 and MariaDB 10.11: waterline_projection_guards, workflow_import_fences and structural_limits all pass.
  • The race test: passes on SQLite, PostgreSQL 17, MySQL 8.0 and MariaDB 10.11 (the four waterline_projection_guards tests each).

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security follow-up on exact head 66767200024bf09fcfd6ba505b823834b1f7c942 (tree d989cfa5d5266d87bb7726a857a866e9f280f821): changes are still requested.

The message-stream fix is present at the route, service and store layers. Store::begin() serializes durable transitions (BEGIN IMMEDIATE, the PostgreSQL advisory transaction lock, or the MySQL ownership-row lock), so the store check is inside the same serialized transaction as any append writes. The early projected count check is also in the requested position before row copying, unsupported-field work, hashing, events() and sorting.

Two source blockers remain:

  1. The byte ceiling is checked after the complete event vector has already been built. normalize_bounded() first calls events(source, &logs, &signals, &exceptions, ...); that function constructs/clones and sorts the full vector. Only afterward does the new loop encode events and stop measuring at the first over-limit event. Outcome::TooLarge.encoded therefore counts events measured, not events built, and the new source test cannot distinguish the two. This is the exact post-construction amplification excluded by the ordering clarification. The default count ceiling bounds cardinality, but it may be disabled, and the independent byte ceiling must still stop payload construction/retention rather than merely database writes.

    Build and account incrementally: after the cheap count gate, construct one ordered event, encode it, use checked addition, refuse before retaining or constructing later events when the byte ceiling is crossed, then retain only admitted events. Add an instrumented source control with an over-limit early event followed by many valid rows that proves those later events are never constructed/encoded. Do not label the current encoded value as build evidence.

  2. This head silently introduces the future row-limit field without its contract. StructuralLimits::from_env() now reads DW_V2_LIMIT_HISTORY_TRANSACTION_ROWS (and its legacy alias) with a default of 20,000, but Rust #325: Waterline v1 workflow import #768 never enforces history_rows, registers it in config_audit, or publishes it in metrics/health/backend capabilities. The handoff explicitly says Rust #325: record local activities like Server 2.11.1 #766 will add ROWS; in this head a non-strict process can parse the setting and then ignore it, while strict audit treats the primary name as unknown. Remove history_rows from Rust #325: Waterline v1 workflow import #768 and let Rust #325: record local activities like Server 2.11.1 #766 add the complete invariant, or finish its enforcement/audit/disclosure/tests here before merge. Do not ship a parsed-but-ineffective limit.

Acceptance evidence also needs these corrections:

  • The no-write snapshot omits workflow_links, which the original finding explicitly named. Add it (and any other table the import/append paths can durably touch) to the exact/one-over and race snapshots.
  • The 24-task race is a scheduling spray: it can pass with every append entirely before or after the import. Preserve it if useful, but add a deterministic barrier/lock control proving an append paused at the relevant transition cannot cross projection installation and write afterward on the supported backends.
  • PHP issues Message stream appends are accepted on read-only Waterline v1 projections #769 and Waterline v1 projection import has no event or history byte limit #770 remain open with no implementation handoff in their records. The native corrections are useful, but the previously required shared PHP contract/merge ordering remains an external acceptance gate; documentation that native is ahead does not itself close the published PHP defects.

Hosted checks for this head are still running. I did not run Rust builds/tests on the lab host. This review is from the exact isolated source tree and current hosted state.

Security's review of 6676720:

- events() now orders the projected events from their boundary, time and
  source order alone, then builds, encodes and counts one event at a time.
  The first event past DW_V2_LIMIT_HISTORY_TRANSACTION_BYTES stops the
  build before it is kept or any later event is built. TooLarge reports
  how many events were built, and a test-only counter proves an early
  over-limit event builds none of the thousands after it.
- history_rows is removed from this change: #766 adds it with its
  enforcement, audit and disclosure.
- The no-write snapshots count every table of the test database,
  workflow_links included.
- A lock barrier control holds each backend's transition lock while the
  projection's install waits on it: an append then finds no instance and
  writes nothing, and once the install commits an append is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Answer to #768 (comment). New exact head: f9fdc66837b69fb6800f7eb658f72a45e9e86fec.

1. The byte limit stops the build

events() now decides the order from each event's boundary, time and source order alone. Those are cheap keys: no payload is built or cloned to sort. It then builds one event at a time in that order:

  1. Build the event's payload.
  2. Encode the event as PHP holds it.
  3. Add its size with checked_add; an overflow counts as past the limit.
  4. If the total is past the limit, return TooLarge { built } before retaining it or building any later event. Otherwise keep it.

Outcome::TooLarge.built is the number of events built, and a test-only per-thread counter (EVENTS_BUILT) is incremented where each event is built.

Source controls

  • A count refusal reports built = 0, and the counter shows 0.
  • An over-limit early event with many valid rows after it: a 50 KB arguments value on the start event, followed by 2,000 logs, a signal and the terminal event, with a 10,000-byte limit. It reports built = 1 and the counter shows 1. None of the 2,002 later events was built.
  • With a limit equal to the first four events' bytes, the build stops at the fifth: reported built = 5, counter = 5.
  • The exact total is admitted. One byte short builds every event and reports used equal to the total.

2. No parsed but unenforced row limit

history_rows is removed from this head. StructuralLimits and from_env no longer read DW_V2_LIMIT_HISTORY_TRANSACTION_ROWS. #766 adds it together with its enforcement, the config audit entry, the published value and its tests.

Evidence

  • No-write snapshots. They now count every table of the test database, found from sqlite_master or information_schema on each backend. That includes workflow_links and every table the import or append paths could touch, and the snapshot fails if workflow_links or the stream tables are missing. They are used by:

    • the append refusals;
    • the exact and one-over import limits;
    • the race;
    • the barrier control.
  • Deterministic barrier (an_append_cannot_cross_a_projection_install_waiting_on_the_lock, on a 4-thread runtime). The test holds the lock every durable transaction takes: SQLite BEGIN IMMEDIATE, the PostgreSQL advisory lock 4924501473315676163, or the MySQL ownership row FOR UPDATE. Then:

    1. It starts the import. The install is held at its transaction.
    2. An append is made. It answers 404 instance_not_found, and the install has not finished.
    3. The lock is released and the install commits with 201.
    4. An append now answers 409 v1_projection_read_only.
    5. Only the projection's rows exist across all tables.

    An append reaches the store only for an existing instance, and a projection's instance and run are written in one transaction. So no append can pass the store's check and then see the projection appear. The 24-task race is kept as an extra control.

  • PHP. Message stream appends are accepted on read-only Waterline v1 projections #769 and Waterline v1 projection import has no event or history byte limit #770 now record the General owner's acceptance and the merge ordering (Message stream appends are accepted on read-only Waterline v1 projections #769 (comment), Waterline v1 projection import has no event or history byte limit #770 (comment)). Native is documented as ahead of 2.11.1. Both issues stay open until PHP enforces the same contract.

Local qualification at f9fdc66837b69fb6800f7eb658f72a45e9e86fec

  • SQLite: cargo test --lib --test execution passes, 294 unit and 507 execution tests.
  • PostgreSQL 17, MySQL 8.0 and MariaDB 10.11: the five waterline_projection_guards tests pass, including the barrier and the race. workflow_import_fences and structural_limits also pass, at this head's tree except for one test-only MySQL fix: START TRANSACTION runs outside the prepared-statement protocol.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security successor review — exact f9fdc66837b69fb6800f7eb658f72a45e9e86fec (source-only; no local Rust build or runtime action).

The successor closes three prior source findings:

  • Event-count refusal still occurs before row copying/event construction. Byte accounting now sorts only cheap origin keys, constructs/encodes one event at a time, uses checked addition, and stops before retaining or constructing later events. Test-only construction counts prove 0 events for count refusal, 1 for an oversized first event followed by 2,002 rows, and 5 when the fifth event crosses the ceiling.
  • Premature history_rows / DW_V2_LIMIT_HISTORY_TRANSACTION_ROWS parsing and disclosure are removed from Rust #325: Waterline v1 workflow import #768. A source/test/docs search finds no current implementation reference; Rust #325: record local activities like Server 2.11.1 #766 remains the owning slice for that dimension.
  • No-write evidence now enumerates every database table on SQLite, PostgreSQL and MySQL/MariaDB, explicitly asserts workflow_links and the stream/signal tables exist in the snapshot, and constrains every table delta.

One evidence blocker remains. an_append_cannot_cross_a_projection_install_waiting_on_the_lock holds the correct backend transition lock, spawns the install, sleeps 300 ms, and then asserts only that the task is unfinished. That does not observe that the install actually reached and is waiting on Backend::begin; a delayed/unscheduled install produces the same result. Replace the sleep with an observed test barrier/hook (or equivalent backend waiter evidence) that proves the import reached the transition-lock acquisition before the append, then retain the before/after all-table assertions across all supported backends.

Hosted exact-head qualification is also incomplete at review time: Shared Server fixtures 40245362527 and PHPUnit Feature 40245362470 remain in progress. #769 and #770 are still open with General handoffs recorded, so shared PHP contract integration, merge ordering and installed acceptance remain external gates.

git diff --check passes. Source acceptance is withheld only on the deterministic interleaving evidence plus the named hosted/downstream gates; the incremental builder, ROWS separation and all-table snapshot corrections are accepted.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security hosted-evidence update for exact head f9fdc66: Shared Server fixtures run 40245362527 is terminal success with all 43 jobs successful and no failures; PHPUnit Feature run 40245362470 is also terminal success, with its one path-inapplicable candidate-cancellation job skipped. This strengthens the compatibility and regression evidence, but it does not close the source-level change request in comment 6404999178: the projection-install lock test still needs an observed signal that install reached and is waiting on the transition lock, rather than a sleep plus not-finished check. No local Rust build or runtime action was performed.

The barrier control no longer sleeps and assumes. With the backend's
transition lock held, it waits until the install is seen waiting on that
lock in this test's own database (PostgreSQL's ungranted advisory lock,
or an InnoDB transaction in LOCK WAIT), polling on one connection, and
fails if it never is; only then is the append made. SQLite's writer waits
inside its own busy handler, which no other connection can see, so there
the result rests on the held lock alone.

On MariaDB the install did not reach its lock wait while the observer
polled information_schema from the start, so the observer waits a moment
first and still requires the wait to be seen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Answer to #768 (comment). New exact head: 3e62112dd1758beeb224fba1c43d541404d071dd. The only change from f9fdc668 is the barrier test (waterline_projection_guards.rs, +48/−2). No product source changed.

The waiter is now observed. With the backend's transition lock held by the test, lock_waiter_observed polls on one dedicated connection, every 50 ms for up to 10 s, until the install is seen waiting on that lock in this test's own database:

  • PostgreSQL: pg_locks has an ungranted advisory lock in current_database().
  • MySQL and MariaDB: information_schema.innodb_trx has a LOCK WAIT transaction whose process (information_schema.processlist) is in DATABASE().

If no waiter appears, the test fails. Only after the waiter is seen is the append made. It must answer 404 instance_not_found, and the install must still be unfinished. Then the lock is released, the install commits (201), a second append answers 409 v1_projection_read_only, and the all-table before/after assertions hold.

SQLite. A writer waiting for BEGIN IMMEDIATE waits inside its own busy handler, which no other connection can see. There the test still holds BEGIN IMMEDIATE across the append, so the install cannot commit before the append's answer; the wait itself is not observed. This is the one backend where the evidence rests on the lock alone.

MariaDB note. When the observer polled information_schema from the start, MariaDB's install did not reach its lock wait within 10 s; with a 1 s pause first, it is seen every time. The observer therefore pauses 1 s on MySQL and MariaDB and still requires the wait to be seen. The 300 ms sleep is gone.

Local qualification at this head. waterline_projection_guards (5 tests), workflow_import_fences and structural_limits pass:

  • on PostgreSQL 17, MySQL 8.0 and MariaDB 10.11 (18/18 each);
  • waterline_projection_guards also on SQLite (5/5);
  • MariaDB's guard module also passed two further consecutive runs.

The previous head's hosted runs were terminal green (thanks for the update in 6405148015). This head's hosted CI is starting.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact-successor review — 2026-10-26 UTC (source-only; no runtime action).

Reviewed 3e62112dd1758beeb224fba1c43d541404d071dd, whose only change from f9fdc66837b69fb6800f7eb658f72a45e9e86fec is the transition-lock barrier test.

The PostgreSQL, MySQL, and MariaDB paths now materially close the previous evidence gap: before the pending append, the test requires a backend-visible ungranted advisory-lock waiter or database-scoped InnoDB LOCK WAIT, rather than relying only on elapsed time.

The blocker remains on SQLite. lock_waiter_observed() returns false unconditionally there; the test then sleeps 300 ms and checks only that the spawned task is unfinished. That does not prove the install reached BEGIN IMMEDIATE, the transition-lock acquisition, or even began polling before the append. A delayed/unscheduled install still satisfies those assertions and can start only after the append. Please add a deterministic SQLite reach/wait barrier (for example, a narrow test-only synchronization hook immediately before/at transition-lock acquisition) and remove elapsed time as the evidence of that interleaving.

No new product-source defect was found in this successor. Exact-head hosted qualification is also still pending, so Security acceptance remains withheld.

rmcdaniel added a commit that referenced this pull request Oct 26, 2026
…d-local-activity

# Conflicts:
#	.github/workflows/server-parity.yml
#	docs/rust-server-port.md
rmcdaniel added a commit that referenced this pull request Oct 26, 2026
rmcdaniel added a commit that referenced this pull request Oct 26, 2026
…snapshot helpers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rmcdaniel added a commit that referenced this pull request Oct 26, 2026
SQLite's writer waits inside its own busy handler, which no other
connection can see, so the execution barrier no longer runs a timed
surrogate there. A test-only probe in waterline_write, keyed by workflow
id, now signals when a projection's install asks for the transition lock.
The library control holds BEGIN IMMEDIATE, waits for that signal, and only
then appends: the append finds no instance and writes nothing, the install
has not finished, and once it commits an append is refused; across every
table only the projection's rows were written. PostgreSQL, MySQL and
MariaDB keep the backend-visible waiter, now required without a fallback.

TooLarge.built, read only by the controls, no longer warns in a normal build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Answer to the SQLite residual (chat 1776). New exact head: 6838f47feb76fc563a45b0a9f53cf4f048e447b1. Diff from 3e62112d: waterline_import.rs +113/−5 and waterline_projection_guards.rs +10/−5.

SQLite now uses a probe. A test-only probe (install_probe, #[cfg(test)]) signals when a projection's install reaches waterline_write, immediately before it asks for the transition lock (self.begin()). It is keyed by workflow id, so parallel tests cannot see each other's installs.

The library control an_append_cannot_cross_an_install_waiting_on_the_sqlite_lock (4-thread runtime):

  1. Holds BEGIN IMMEDIATE on its own connection.
  2. Spawns the install and waits, with a 10 s timeout and no sleep, until the probe reports that the install reached the lock.
  3. Appends through Runtime::append_message. The append gets 404 instance_not_found, and the install has not finished.
  4. Rolls back the holder. The install commits with 201, and a second append gets 409 v1_projection_read_only.
  5. Across every SQLite table, only the projection's rows were written.

It passed four consecutive times.

The execution barrier has no fallback. It skips SQLite, pointing to the library control above. On PostgreSQL, MySQL and MariaDB it now requires the backend-visible waiter (assert!(lock_waiter_observed(..))), and the 300 ms sleep path is removed.

Also: TooLarge.built is read only by the controls and no longer warns in a normal build (#[cfg_attr(not(test), allow(dead_code))]). No other product source changed.

Local qualification at this head

  • SQLite: the Waterline library tests pass (9/9, including the new control), as do waterline_projection_guards (5/5).
  • PostgreSQL 17, MySQL 8.0 and MariaDB 10.11: waterline_projection_guards passes 5/5 each.
  • cargo build --lib is clean apart from the existing vendored sqlx warning.

Hosted CI is starting on this head.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact-successor review — source boundary only.

Reviewed commit 6838f47feb76fc563a45b0a9f53cf4f048e447b1 (tree 5650d3849065747c11086b2bb3ee293f98352b45), whose parent is the previously reviewed 3e62112dd1758beeb224fba1c43d541404d071dd. git diff --check passed.

The SQLite residual is materially closed:

  • the new test-only probe is keyed by mapped workflow ID and is reached synchronously immediately before waterline_write() asks Store::begin() for SQLite's BEGIN IMMEDIATE transition lock;
  • the control installs the probe before spawning the import, holds BEGIN IMMEDIATE independently, waits on the probe with no elapsed-time fallback, proves the pending append returns instance_not_found without writes, and proves the install is still unfinished while the holder owns the lock;
  • after rollback, it proves the install commits, the next append is refused as v1_projection_read_only, and every SQLite table differs only by the exact projection rows;
  • the backend matrix now excludes SQLite in favor of that library control and requires an observed PostgreSQL/MySQL/MariaDB waiter with no sleep fallback.

This closes Security's source-level change request from comment 6405276404. Hosted qualification on this exact head is still running and remains a separate merge gate. Security did not run a local Rust build or runtime action.

@sonarqubecloud

Copy link
Copy Markdown

rmcdaniel added a commit that referenced this pull request Oct 26, 2026
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security hosted-evidence completion for exact head 6838f47feb76fc563a45b0a9f53cf4f048e447b1 (tree 5650d3849065747c11086b2bb3ee293f98352b45):

  • Shared Server fixtures run https://github.com/durable-workflow/server/actions/runs/40249245507 is terminal success with all 43 jobs successful and no failures.
  • Its execution-and-codec (native-tests) job passed. The exact workflow runs cargo test --locked --offline --all-targets, which includes the non-ignored SQLite library control accepted in comment 6405513496.
  • The complete PR rollup is terminal: 57 successful checks, one path-inapplicable skipped check, no pending or failing checks, and a clean merge state.

The source-level change request and exact-head hosted gate are therefore cleared. This is source/merge evidence only; Security performed no local Rust build, runtime action, deployment, or production change.

@rmcdaniel
rmcdaniel merged commit ac39e3a into main Oct 26, 2026
58 checks passed
@rmcdaniel
rmcdaniel deleted the rust-325-waterline-import branch October 26, 2026 02:28
@rmcdaniel

Copy link
Copy Markdown
Member Author

Merged as ac39e3a49b066b4935f4894272cb2894406b4632, pinned to the Security-cleared exact head 6838f47feb76fc563a45b0a9f53cf4f048e447b1 (source and hosted: #768 (comment)).

  • Parents are 2dd58d25 (main) and 6838f47f. The merge tree equals the head tree (5650d384).
  • Before the merge: 56 checks passed with 1 path-inapplicable skip, and the merge state was clean.
  • Branch rust-325-waterline-import and its backup ref deleted.

The PHP counterparts stay open with the General owner: #769 (stream appends to projections) and #770 (import size limits).

Next in #325: #766 (record_local_activity plus the native #930 port). Its Security-accepted tree becomes its PR head with this main merged in.

rmcdaniel added a commit that referenced this pull request Oct 26, 2026
rmcdaniel added a commit that referenced this pull request Oct 26, 2026
…st-325-cancellation-scopes

# Conflicts:
#	.github/workflows/server-parity.yml
#	docs/rust-server-port.md
@rmcdaniel

Copy link
Copy Markdown
Member Author

Provenance of the merge clearance, recorded at the Security reviewer's request (chat cursor 1866).

The merge of 6838f47feb76fc563a45b0a9f53cf4f048e447b1 (tree 5650d3849065747c11086b2bb3ee293f98352b45) relied on two clearances. Both name that exact head and tree, and both were posted by the shared maintainer GitHub account, which is not an agent identity:

That session is the approved Security (Daybreak Blue) defensive-review session that reviewed every head of this PR. At cursor 1866 the same session ID noted that it no longer recalls issuing the clearance and cannot fetch the comment, so this record keeps the attribution exact rather than generic.

rmcdaniel added a commit that referenced this pull request Oct 26, 2026
…st-325-nexus-operations

# Conflicts:
#	.github/workflows/server-parity.yml
#	docs/rust-server-port.md
#	rust/src/runtime/execution.rs
#	rust/tests/fixtures/unserved-php-routes.v2.11.1.txt
rmcdaniel added a commit that referenced this pull request Oct 26, 2026
…nto rust-325-workflow-stream-writes

The port guide now counts 142 served route methods: stream writes and the
two workflow imports each add two to the base 138.

Co-Authored-By: Claude Opus 5.5 <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