Skip to content

Rust #325: serve bridge adapter webhooks - #776

Merged
rmcdaniel merged 18 commits into
mainfrom
rust-325-bridge-adapters
Oct 26, 2026
Merged

rmcdaniel merged 18 commits into
mainfrom
rust-325-bridge-adapters

Conversation

@rmcdaniel

@rmcdaniel rmcdaniel commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Part of #325. Serves POST /api/bridge-adapters/webhook/{adapter} like published Server 2.11.1, fixture first.

Fixture (40a55cd5e, recorded from published 2.11.1 three times, identical). bridge-adapters covers:

  • adapter, body and action refusals;
  • start_workflow: new, duplicate, reject_duplicate, derived ID, unconfigured type, a type reserved for another workflow, another namespace;
  • signal_workflow and update_workflow: unknown, malformed, accepted, deduplicated.

One published defect is bounded rather than copied (Server #747). PHP's bridge update calls the control plane directly and skips external update admission, so an update to an SDK-worker workflow is always rejected workflow_definition_unavailable. The fixture records that answer under published_defects and expects the corrected one.

Native (176a5ef70):

  • The adapter name and event are validated as PHP validates them.
  • A start checks PHP's draining gate, then the reserved type and namespace.
  • Signals and updates carry PHP's bridge command context and are deduplicated by adapter, action, idempotency key and target name.
  • Every answer is the durable-workflow.v2.bridge-adapter-outcome.contract envelope.
  • A bridge update is admitted as the regular update route admits it, including that route's validator-declaration check.
  • Duplicate detection (5877358942) runs in the database: a per-dialect statement filters the command context on adapter, action, idempotency key and target name, as PHP's JSON-path query does, and reads at most one row. It no longer scans every accepted command of the workflow.

Fixture correction (ae97a3dd0). The fixture's worker now declares update_validators: []. Without it, published 2.11.1's regular route answers 409 update_validator_contract_missing for this run, so the expected 202 was not the regular route's answer. With it, the regular route answers 202 and the bridge path still records the #747 defect unchanged. Evidence: #747 comment 6408344149. No expectation changed.

Security hold successor (held at 587735894; Security cursor 2097 and rulings at 2105). Commit b480d7f9, with #775 (PR #778) merged in:

  • Namespace first. A start for an instance id that another namespace holds is refused unknown_target as "Workflow instance [id] is unavailable.", naming neither the type nor the namespace. Published PHP checks the type first and names it, so the fixture bounds the published answer as a second published defect (Server Bridge adapters: foreign-instance type disclosure, non-atomic dedupe and colliding update keys, unbounded adapter name #777).
  • Atomic deduplication. Each bridge signal or update is recorded with a fixed-length identity as its request_id: dw-bridge:v1: plus sha256 over the length-prefixed adapter, action, raw key, target field and target name. The duplicate check runs inside the command's own transaction, under the transition lock: the identity first, then the metadata of commands PHP recorded before takeover. An identity whose command records another tuple fails closed (409 bridge_identity_conflict). The unique (instance, command type, request id) index backs it, and a write that loses to it is answered only by the exact matching command. The raw key stays in the command metadata. The pool pre-check is gone, so a codec refusal is answered before a duplicate, as in PHP.
  • Reserved request ids. The regular signal and update routes, PHP and native, refuse caller request ids that start with dw-bridge: (case-insensitive), so no caller can claim a bridge identity first.
  • Adapter bound. The adapter name is at most 64 bytes, so a derived workflow id fits 128; a longer one is refused unsupported_routing before anything is persisted.
  • Update wait. The bridge update target takes the Update completion wait_timeout_seconds is caller-unbounded #775 ceiling (wait_timeout_seconds at most 60).

Second Security hold (867a2d5; Security cursors 2187, 2188, 2191 and 2197; PR comments 6414283246 and 6414404528), fixed in 34134f5e:

  • Any status under an identity. A command found under a bridge identity, accepted or rejected, must record the same adapter, action, raw key and target before any receipt is answered or replayed. This applies to the identity lookup and to the update's existing-receipt lookup. Before the prefix reservation, a regular caller could have stored a rejected update under the predictable identity, and it was replayed.
  • The conflict keeps the bridge envelope. 409 bridge_identity_conflict comes with the schema and version, accepted: false, outcome: rejected, and only the caller's own fields: adapter, action, key, redacted target and correlation. No stored metadata or error text is included. The in-transaction check and the lost-race exit give the same answer.
  • Starts are decided under the lock. The bridge no longer reads the draining gate or the instance reservation outside the transition lock. Store::bridge_start decides all three under the lock before anything is written, in the bridge's order: a draining queue, then an instance another namespace holds, then one reserved for another type. Each gives the envelope the old precheck gave (422 task_queue_draining or unknown_target). So a foreign instance, another type, or a queue that starts draining between a read and the write can no longer leak the namespace, return the other type as a duplicate, or bypass the envelope. The unknown-target status stays 422 because the published 2.11.1 fixture records 422 for it.

Tests:

  • Concurrent identical bridge signals and updates through two pools give one effect and one receipt (one 202, seven duplicates naming the same run).
  • The same raw key from another adapter, or for another target name, gives separate effects.
  • A regular route's dw-bridge: request id is refused.
  • The 64/65-byte adapter boundary.
  • PHP regressions for the reservation.
  • Each preclaimed identity is seeded by rewriting a regular command's request id: a rejected update (closed run), an accepted signal, and a signal command recorded as rejected. Each gets 409 with the exact bridge envelope, and no command or effect is added.
  • Starts against a foreign instance, another type and a draining queue get the exact envelopes, and no command or instance is added.
  • Three lock-queue tests use the real transition lock. The test holds it; a bridge start queues on it, then a second test-held lock, then a competitor: the same id started in another namespace, the same id started for another type, or a drain of the bridge's queue. On release the bridge is granted first. The second hold keeps the competitor out until any read the bridge made outside the lock has finished.
    • PostgreSQL's advisory lock and InnoDB's row lock grant waiters in order, so there the schedule is forced. Each test asserts the bridge's exact acceptance, the competitor's exact answer, and the stored state.
    • SQLite's busy retry has no queue, so there the test accepts either winner and asserts each side's exact answer and state.
  • Counterfactuals: without the in-transaction check, concurrent updates answer 8 acceptances; without both the check and the identity column, concurrent signals apply 8 times. Without the PHP reservation, the preclaim test answers 202.

Served routes: the webhook leaves unserved-php-routes.v2.11.1.txt. With current main merged in, native serves 146 of the 175 published route methods.

Bounds. The bridge adds no new storage or limits. Starts, signals and updates go through the same admission paths as the regular routes, and the update wait is bounded by #775.

PHP source. BridgeAdapterController::UNRESERVED_REQUEST_ID (not_regex:/^\s*dw-bridge:/i) is added to the request_id rule of the regular signal and update routes in WorkflowController. Nothing else in PHP app/ changes here; the update wait ceiling came with #778.

Qualification:

Source head 34134f5e (the second-hold fixes on 867a2d53, which merges main 68345fa5); bfd0ac6d and e29ca324 add only tests:

  • SQLite, all targets: lib 310 and execution 531 pass.
  • Native shared fixtures pass on SQLite, PostgreSQL 17, MySQL 8 and MariaDB 10.11: bridge-adapters, update-validation, state-updates, visibility-current-runs, one-signal, repeated-signal, signals-before-worker and message-streams.
  • PostgreSQL 17, MySQL 8 and MariaDB 10.11: 31/31 pass on each across bridge_adapters, command_requests, update_validation, update_admission, signal_claims and start_request.
  • Counterfactuals:
    • With the old accepted-only identity checks, the rejected update is replayed (422 run_not_active) on SQLite and PostgreSQL.
    • With the old bare lost-race error as well, the rejected-status signal gets {reason, message} without the envelope.
    • With the start decision outside the lock (a copy of the successor with the store's checks disabled), the seeded refusals leak the raw workflow_id_reserved_in_namespace 409 on SQLite and PostgreSQL.
  • Lock-queue tests, at e29ca324's test file:
    • On the successor: 3/3 on SQLite (8 runs), PostgreSQL 17, MySQL 8 and MariaDB 10.11. On the ordered backends the bridge is accepted first every time.
    • On the held 867a2d53: all three fail on PostgreSQL, MySQL and MariaDB. The foreign case gives a raw 409 workflow_id_reserved_in_namespace, the type case a 200 duplicate of the other type, and the drain case a raw 409 draining response.

Combined head e20bb446b (before the second hold), PHP, in the pinned server image: BridgeAdapterControllerTest 10, WorkflowControlPlaneTest 54, ServiceOperationUpdateWaitTest 4 and UpdateWaitConfigurationTest 3 pass. The second-hold fixes change no PHP.

Successor 23dfe993 (before #775's last two commits): PostgreSQL 17, MySQL 8 and MariaDB 10.11 pass 95/95 across bridge_adapters, command_requests, update_validation, update_admission, signal_claims and start_request.

Published PHP: published 2.11.1 records the fixture (with the corrected worker contract) and passes it through its published_defects branch, which bounds both published defects (#747 and #777). Its regular update route answers 202 for the same run.

Hosted CI runs at e29ca324.

🤖 Generated with Claude Code

rmcdaniel and others added 10 commits October 24, 2026 14:14
…ed 2.11.1

Refusals, start_workflow (new, duplicate, reject_duplicate, derived ID,
unconfigured type), and signal_workflow and update_workflow (unknown,
malformed, accepted, deduplicated). Recorded three times, identical. One
published defect is bounded: bridge updates skip external update admission.
POST /api/bridge-adapters/webhook/{adapter} validates the adapter name and
event, then starts, signals or updates a workflow with PHP's bridge command
context, deduplicating signals and updates by adapter, action, idempotency
key and target name. Starts check PHP's draining gate, then the reserved type
and namespace, before starting. Every answer is the bridge outcome envelope.
An update is admitted as the regular update route admits it, not refused as
published PHP refuses it (Server #747); the fixture bounds that defect. The
fixture also gains the reserved type and namespace steps, recorded three
times against published 2.11.1.
…ust-325-bridge-adapters

The runtime, router access table, comparator and probe keep both sides'
entries. Pull requests run without a path filter on main, so the old
pull_request paths list goes. Bridge adapter webhooks leave main's
unserved route list (native serves 139 of 175) and its missing list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main (worker session reads) and this branch (bridge adapter webhooks) each
added one family and each raised the table's length from 8 to 9; it now
holds 10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ust-325-bridge-adapters

Conflicts kept both sides: the router, delegates, modules, parity contract,
probe and workflow test list carry bridge adapters with the workflow
imports and record_local_activity. The port guide counts 141 served route
methods: bridge webhooks add one and the two imports two to the base 138.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bridge-adapters fixture expects a corrected bridge update to be
admitted as POST /api/workflows/{id}/update/bump admits it (Server #747).
Its worker declared update `bump` without `update_validators`, so the run's
durable contract never states whether `bump` has a validator, and published
2.11.1's regular route refuses it 409 update_validator_contract_missing.
Native already admits bridge updates through the regular route, so it
answered that refusal instead of the expected 202.

The worker now declares `update_validators: []`. On published 2.11.1 the
bridge path still records the #747 defect unchanged (the fixture passes on
PHP through its published_defects branch), and the regular route accepts the
same update with 202. Native passes on SQLite, PostgreSQL 17, MySQL 8 and
MariaDB 10.11. No expectation changed.

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

The port guide counts 144 served route methods: bridge webhooks add one and
cancellation scopes three to the base 140. The workflow test list carries
both contract tests.

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

Kept both sides: delegates, parity contract, probe and workflow test lists
carry bridge adapters with Nexus operation reads, and the port guide lists
both as served. It counts 146 served route methods: the base 140 plus
bridge webhooks, cancellation scopes and Nexus reads.

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

duplicateBridgeCommand read every accepted command of the kind for the
workflow, with its context, and matched adapter, action, idempotency key
and target name in memory, so each bridge signal or update scanned all of
a workflow's accepted commands. PHP filters on those JSON paths in SQL and
takes the latest. Native now does the same with a per-dialect statement
(PostgreSQL ->>, SQLite json_extract, MySQL/MariaDB binary
JSON_UNQUOTE(JSON_EXTRACT())), ordered as before and limited to one row.
The comparison stays exact and case-sensitive, as the in-memory match was.

The bridge-adapters fixture's duplicate and other-adapter cases pass on
SQLite, PostgreSQL 17, MySQL 8 and MariaDB 10.11.

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

Copy link
Copy Markdown
Member Author

Security hold on exact head 587735894218bfe3d6eddc716a398355cd155e7f, tree 713637265f452884bc26ccc3e6ea2c09c6b7a352.

The route/auth/body-boundary wiring and bound-parameter JSON queries are sound, but this head cannot activate the bridge route yet:

  1. instance_reservation reads another namespace's type, and start checks the type before the namespace. A caller who can guess a foreign workflow ID receives its durable type. Check namespace first and return the same generic unavailable/unknown-target refusal for a foreign instance. Do not preserve that PHP disclosure; file the shared PHP defect and update the fixture.
  2. Bridge dedupe is a pool read followed by a separate signal/update transaction. Two identical signals can both pass and both apply. The update fallback is not correctly bridge-scoped either: raw idempotency_key becomes a request ID whose uniqueness is only (workflow_instance_id, command_type, request_id), so another adapter or another target name with the same raw key can reuse or conflict with the wrong command.
    • Derive a fixed-length internal identity from adapter, action, raw key, target field, and target name; keep the raw key only in bridge metadata/responses.
    • Enforce or recheck it under the workflow transition lock before recording an effect.
    • Add controlled concurrent identical signal and update regressions, plus same-key/different-adapter and same-key/different-target regressions. Prove one effect and one receipt across SQLite, PostgreSQL, MySQL, and MariaDB.
  3. The adapter path has a character allow-list but no length bound. It is persisted in command context and used verbatim in the derived workflow ID; adapters longer than 88 bytes exceed the 128-byte workflow-ID contract. Add an explicit bound (or a bounded derivation) and file the shared PHP gap.
  4. Update completion wait_timeout_seconds is caller-unbounded #775 is an activation blocker. This new endpoint must not expose caller-controlled unbounded wait_timeout_seconds; land the documented all-ingress maximum first or include that shared fix here.

Sequential parity fixtures and green CI cannot cover these races/boundary leaks. Publish a successor SHA/tree and complete delta; do not rerun or merge this head. This is source/merge review only, not deployment authority.

@rmcdaniel
rmcdaniel marked this pull request as draft October 26, 2026 06:26
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security follow-up on the internal bridge identity:

  • Close the same-namespace pre-claim path. Reserve a versioned internal prefix such as dw-bridge:v1: at every caller-controlled request_id ingress before admission; only the internal bridge path may set it. Add a regular-route pre-claim regression.
  • Do not hash a NUL-joined tuple: idempotency_key and signal/update names may contain NUL. Use canonical structured encoding or explicit length-prefixed bytes for adapter, action, raw key, target field, and target name.
  • After a unique violation, re-read and return a duplicate only when the exact internal identity and bridge metadata tuple match. Any other collision fails closed.
  • The takeover-era metadata lookup belongs under the same transition lock so a retry of a PHP-written bridge command cannot apply twice.

These are part of the successor acceptance criteria.

…e-adapters

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rmcdaniel and others added 2 commits October 26, 2026 07:07
…tity

Security's review of #776 held four gaps; three are fixed here and the
fourth is #775, merged into this branch.

- A start for an instance id another namespace holds is refused as
  unavailable, checked before the type, so neither that instance's type
  nor its namespace is named. Published PHP names the reserved type; the
  bridge-adapters fixture bounds that answer as a published defect.
- A signal or update is deduplicated inside its own transaction, under
  the transition lock, instead of by a pool read before it. Each bridge
  command is recorded with a fixed-length identity as its request id:
  dw-bridge:v1: and sha256 over the length-prefixed adapter, action, raw
  key, target field and target name. The lookup is the identity first,
  then the metadata of commands PHP recorded before takeover. An identity
  whose command records another tuple fails closed, and the unique
  (instance, command type, request id) index backs the check. The raw key
  stays in the command metadata.
- The regular signal and update routes, PHP and native, refuse caller
  request ids in the dw-bridge: namespace, so no caller can claim a
  bridge identity first.
- The adapter name is at most 64 bytes, so a derived workflow id fits
  128.

The shared PHP defects are Server #777.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rmcdaniel and others added 2 commits October 26, 2026 08:17
…e-adapters

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel
rmcdaniel marked this pull request as ready for review October 26, 2026 10:44
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security hold on exact head 867a2d5, tree eb56b2635504fec182ea46815eb043a85999ab1e.\n\nThe successor correctly adds length-prefixed bridge identities, current-ingress prefix reservation, a 64-byte native adapter bound, under-transition-lock duplicate checks, exact accepted-command metadata matching, takeover fallback, and the 60-second wait ceiling. Two takeover/race paths remain blocking:\n\n1. A rejected update already stored under the derived identity is replayed without checking its bridge tuple. bridge_receipt checks metadata only for status=accepted. admit_update then looks up any existing update by the identity, checks only update_name and run_id, and returns the existing rejected receipt. Before this reservation shipped, a regular caller could choose the predictable internal prefix. A preexisting row with the same identity and target name/run but different adapter/key metadata must fail bridge_identity_conflict, not be reused. Verify the exact adapter/action/raw-key/target tuple for every command found under an internal identity, accepted or rejected. Add a seeded pre-takeover rejected-row counterexample and prove 409, no effect, and no receipt reuse on SQLite, PostgreSQL 17, MySQL 8, and MariaDB 10.11.\n\n2. The foreign-namespace start refusal is still time-of-check/time-of-use. Event::start reads instance_reservation outside the transition transaction. A foreign instance inserted after that read is detected under lock by Store::start_in, but that error is propagated as workflow_id_reserved_in_namespace with an explicit another-namespace message instead of the generic bridge unknown_target/unavailable response. Translate the durable under-lock refusal to the same generic bridge envelope, or make the full decision in one transaction. Add a controlled race/counterfactual on every supported database.\n\nSecurity-conflict exits must retain the documented bridge envelope: schema/version, accepted=false, outcome=rejected, reason=bridge_identity_conflict, caller-derived redacted fields only, and no stored metadata or raw error text. The current unhandled RuntimeError::Refused path emits only the generic reason/message object.\n\nKeep the head immutable and unmerged. Send a minimal successor SHA/tree/delta plus exact-head counterfactuals and hosted results. No credential values were used in this review.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security addendum for exact held head 867a2d538c1d70ef57bc71618715ddfa6612e6c2:

The start-race fix must cover the whole under-lock decision, not only a foreign namespace.

  • Same namespace, different type: Event::start can observe no reservation, then another serialized start can create the requested ID for a different workflow type. Store::start_in then calls start_existing; with the bridge's default use-existing policy it returns returned_existing_active, and the bridge currently reports a successful duplicate for the wrong durable type. This must remain a 409 bridge-envelope rejection (unknown_target under the current adapter contract), never a duplicate acceptance.
  • Queue begins draining: bridge_start_block can pass, then the queue can begin draining before Store::start acquires its transaction. The under-lock task_queue_draining RuntimeError::Protocol currently bypasses Event::start's bridge envelope. Normalize it to the same bridge response as the precheck.

Please add controlled race/counterfactual coverage for foreign namespace, same-namespace different type, and queue-drain state changing between precheck and the serialized start, on SQLite/PostgreSQL/MySQL/MariaDB. The rejected-identity exact-tuple/envelope hold remains unchanged.

Separate audit result: declared-validator concurrency adds no hold. validation_task() uses the globally serialized Store::begin(), so one task is committed before a peer can find/create it, and recursive admission is serialized again.

…e under the lock

Security held 867a2d5 on two paths (#776).

A command already stored under a bridge identity answered the bridge
without its tuple being checked when it was not accepted: a rejected
update was replayed, and a rejected signal reached the unique index and
came back as a bare error. A caller could have chosen the predictable
identity before the prefix reservation. Now every command under an
identity, accepted or rejected, must record the same adapter, action, raw
key and target, in the identity lookup and in the update's receipt
lookup. Anything else answers 409 bridge_identity_conflict in the bridge
envelope, with the caller's fields only, from both the in-transaction
check and the lost-race exit.

A start read the draining gate and the instance reservation outside the
transition lock, so a change before the start could leak a foreign
namespace, return an instance of another type as a duplicate, or bypass
the envelope for a queue that began draining. The store now decides all
three under the lock before anything is written (Store::bridge_start),
in the bridge's order, and the bridge answers each with the envelope the
precheck used to give.

Tests seed each preclaimed identity and each start state; a race test
runs competing starts through a second pool.

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

Copy link
Copy Markdown
Member Author

Security review of the published successor commit 34134f5 / tree 829350deaaff4dafde8212fb39e166f6e0862628 (PR head remains held 867a2d5): the exact-tuple checks for accepted and rejected identities, bridge-envelope conflict mapping, and the under-transition-lock start decision close the two held source defects semantically.

One evidence gap remains before the PR pointer may move. The added racing_starts_never_leak_a_namespace_or_reuse_another_type loop has no deterministic barrier, accepts either winner, and does not require the competitor to win; the draining case is only pre-seeded, not changed between the old precheck and serialized start. Those tests can pass the held implementation without exercising its TOCTOU. Provide a controlled counterfactual for all three transitions—foreign namespace, same-namespace different type, and queue starts draining—showing held 867a fails and the successor passes on SQLite, PostgreSQL 17, MySQL 8, and MariaDB 10.11. Keep the current PR head immutable until that evidence or a deterministic test successor is ready. Source semantics are otherwise clear; hosted exact-head qualification is still required.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Implementation note for the remaining deterministic counterfactual:

Reuse the real cross-backend transition-lock pressure pattern already present in rust/tests/execution/worker_pressure.rs (SQLite BEGIN IMMEDIATE, PostgreSQL advisory transaction lock 4924501473315676163, MySQL/MariaDB dw_server_schema ... FOR UPDATE).

For each foreign-namespace, wrong-type, and drain transition:

  1. Hold the real transition lock.
  2. Spawn the bridge start and prove it remains pending.
  3. Spawn the competing ordinary start or drain transition and prove it remains pending behind the bridge's first lock request.
  4. Release the held lock.

On held head 867a2d53, bridge_start_block acquires and releases the first queued lock around only the stale precheck, allowing the already-queued competitor to win before runtime.start; that exposes the incorrect namespace/type/draining result. On successor 34134f5e, the bridge holds one transaction through decision and write, so it starts first and the competitor observes that committed state.

Assert the exact bridge result, durable state, and competitor result—do not accept either winner. This provides a real-DB, no-mock old-fails/new-passes proof suitable for SQLite, PostgreSQL 17, MySQL 8, and MariaDB 10.11.

Security asked for a controlled interleaving (#776). The test holds the
real transition lock; a bridge start queues on it, then a second
test-held lock, then a competitor: a start of the same id in another
namespace, a start of it for another type, or a drain of the bridge's
queue. Released, the bridge is granted first, and the second hold keeps
the competitor out until any read the bridge made outside the lock has
finished.

PostgreSQL's advisory lock and InnoDB's row lock grant waiters in order,
so there the schedule is forced: a start decided outside the lock meets
the competitor's state at its write, and a start decided in one
serialized transaction has started its own instance first. SQLite's busy
retry has no queue, so there the test asserts only that the bridge is
accepted with its own type or answers that state in its envelope.

The worker pressure lock holder becomes an owned future both tests use.
The opportunistic racing-starts test is replaced.

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

Copy link
Copy Markdown
Member Author

Security review of exact proof successor bfd0ac6d07b98011b1fa13fe14a2a410423d51c8, tree c5c53afa1e4b28847fca851c21dd5f6b749f3c3e, parent 34134f5e10968984777aefd09517b3485e012fd7: the test-only delta is scoped, git diff --check is clean, and the ordered PostgreSQL/MySQL/MariaDB L0/L1 schedule asserts the bridge result, durable default-namespace echo state, and exact competitor outcome.

One assertion gap remains before clearance. In change_queued_behind_a_bridge_start's SQLite competitor-wins branch, competitor_status / competitor_answer and the durable winner state are only logged, not asserted. Because SQLite cannot force waiter order, either winner may be legitimate, but whichever winner occurs must still be fully proven:

  • foreign namespace: competitor creation succeeds, the durable row belongs to the foreign namespace with type echo, and the default namespace cannot expose it;
  • same namespace / other type: competitor creation succeeds and the durable row is other.type;
  • queue drain: the drain succeeds with its exact control-plane result and no bridge workflow instance or command was created.

Keep the current hold until those assertions are present, exact held/mutant/successor counterfactual outcomes are published, and hosted checks pass on the resulting exact head. The production source semantics remain accepted; this is the final proof completeness gap found in bfd0ac6d.

… win

Where SQLite's busy retry grants the competitor first (#776), the test now
asserts the competitor's exact success and the durable state: the foreign
start created echo in "other" and the default namespace cannot see it;
the other type's start created it; the drain answered draining and the
bridge's id was never started. The instance holds only the competitor's
command, so the bridge recorded nothing.

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

Copy link
Copy Markdown

@rmcdaniel

rmcdaniel commented Oct 26, 2026 •

Copy link
Copy Markdown
Member Author

Security final clearance — exact head e29ca324706954e0f9f312e3a47ad2caf2b6fd0e, tree 7ce33fe53856f16b1609152c70bf9a5fbd6b1108, parent bfd0ac6d07b98011b1fa13fe14a2a410423d51c8.

  • The production fix remains exact commit 34134f5e10968984777aefd09517b3485e012fd7. The two proof successors change only rust/tests/execution/bridge_adapters.rs and rust/tests/execution/worker_pressure.rs; production Rust source and dependency manifests are unchanged.
  • The real transition-lock counterfactual now proves the queue-drain, foreign-namespace, and wrong-workflow-type decisions cannot escape the global transition transaction. The competitor-wins branch asserts the exact response plus durable workflow and command state.
  • The held pre-fix source fails the intended ordered-database counterfactuals, while the successor proof passed repeated SQLite schedules and deterministic PostgreSQL/MySQL/MariaDB schedules.
  • Exact-head Shared Server fixtures run https://github.com/durable-workflow/server/actions/runs/40321678938 completed successfully on attempt 2. Attempt 1 had 40 substantive passes and one final Rust compile killed by signal 9 with no compiler diagnostic; its aggregate failure only reflected that job. The one-time failed-job rerun passed the bounded optimized native build, artifact upload and cleanup, all deadline-health provenance/database-family checks, evidence publication, and the final aggregate.
  • PR state now reports 59 terminal checks with no pending, failed, cancelled, timed-out, stale, action-required, or neutral result; merge state is clean.

Security clears this exact SHA/tree for owner merge. Any head change requires re-review.

@rmcdaniel
rmcdaniel merged commit 47f4c54 into main Oct 26, 2026
100 of 102 checks passed
@rmcdaniel
rmcdaniel deleted the rust-325-bridge-adapters branch October 26, 2026 13:07
@rmcdaniel

Copy link
Copy Markdown
Member Author

Merged as 47f4c5447a33dfede4e0f908bfaec7d68bd1a40c, SHA-pinned to the cleared head e29ca324706954e0f9f312e3a47ad2caf2b6fd0e.

  • Merge tree: 7ce33fe53856f16b1609152c70bf9a5fbd6b1108, the same as the head's tree.
  • Parents: 68345fa5d4537ac7e333ae6ff1093c4bbc8c050f (main) and e29ca324706954e0f9f312e3a47ad2caf2b6fd0e.
  • Security clearance: Rust #325: serve bridge adapter webhooks #776 (comment), for this exact head and tree.
  • Hosted gates at the head: 58 success and 1 skipped. Shared Server fixtures run 40321678938 succeeded on attempt 2. Attempt 1's only failure was codec-native-build, which was killed by signal 9 with no compiler diagnostic, and the single rerun of that job passed.

#777 stays open for the published PHP bridge defects.

🤖 Generated with Claude Code

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