Skip to content

Embedded v2 history import reads and attaches across namespaces (published 2.11.1) #765

Open

Description

@rmcdaniel

Published Server 2.11.1, with Workflow 2.10.2's EmbeddedV2HistoryImport, lets an embedded v2 history import in one namespace read and change another namespace's workflow state. The route is admin-only, but namespaces are meant to stay isolated.

1. Cross-namespace duplicate. EmbeddedV2HistoryImport::existingRunOutcome looks the bundle's run up in any namespace. A bundle already imported into namespace B, imported again into namespace A, answers 200 with already_imported for A, which holds no such run.

2. Cross-namespace instance attach. EmbeddedV2HistoryImport::writeBundle finds the workflow instance in any namespace and attaches the imported run to it. A run imported into namespace A becomes the current run, and bumps the run count, of an instance that belongs to namespace B.

Evidence. Both behaviors were recorded three times from the published image, with identical results, in tests/Fixtures/ServerParity/workflow-import.json under workflow_import.published_defects (cross_namespace_duplicate, cross_namespace_instance_attach). The fixture arrives with #764.

Native behavior (#764). A run id held in another namespace is refused as run_id_conflict. An instance of another namespace is refused rather than taken over, and the other namespace is left unchanged. The parity fixture keeps the published answers bounded as defects, so native isn't required to reproduce them.

Suggested PHP fix. Scope both lookups by the requested namespace: the existing-run check and the instance resolution in the write. A run id or instance id already present in another namespace should then be a conflict.

Activity

  1. rmcdaniel commented on Oct 25, 2026

    @rmcdaniel
    MemberAuthor

    General is implementing the PHP root correction in durable-workflow/workflow#928. Current-source counterfactual coverage reproduces both original defects in all twelve named/null namespace and write/preview cases: twelve assertion failures, zero test errors, against the unchanged Workflow 2.13.1 import class.

    The candidate restricts run-idempotency to the target namespace, reports an explicit foreign-instance conflict before preview/write, and scopes the transaction's locked instance lookup so a preflight race cannot attach the run to a foreign instance. The ordinary run-number-conflict control now explicitly selects its existing namespace, preserving the intended SQL uniqueness/rollback check. Full current-candidate qualification is running; no merged or published correction is claimed.

    The reference audit from #764 is retained in Workflow #928, including parent/child links, signal/update command IDs and other copied relational identifiers. These direct target guards do not complete that audit or the installed-image acceptance. Native/shared/frozen work remains with its owner. Server #526's separate query-authority race remains open in draft #758.

  2. rmcdaniel commented on Oct 25, 2026

    @rmcdaniel
    MemberAuthor

    The direct target guards are now visible in Workflow draft PR #929 at 5c766e3188fb1fb7ac9ebb267773bcc5e37912f6. Full ECS/PHPStan and 44 tests / 607 assertions pass locally; exact counterfactual and current-scope evidence is in durable-workflow/workflow#929 (comment). Workflow #928 retains the full copied-reference graph and compatible quarantined-lineage work. Hosted qualification and independent partial source review are live. This is source-only, with no package or Server image publication; keep #765 open through complete fixed-image acceptance.

  3. rmcdaniel commented on Oct 25, 2026

    @rmcdaniel
    MemberAuthor

    Workflow draft PR #929 now includes the active signal/update command-reference boundary at f0e6e9be13de3d5d3dcdf5de42af720bc35ad8bd. Each message must bind to a matching command imported from the same bundle and transaction. SQLite, MySQL 8.0.46 and PostgreSQL 17.11 each pass 82 tests / 779 assertions; counterfactual worker completions reproduce foreign-command mutation before the guard. Evidence: durable-workflow/workflow#929 (comment) . Full copied-reference/historical/lineage acceptance remains in Workflow #928, including the new source-only projection audit in comment 6402344424. Keep #765 open through complete source and fixed published-image verification. Native/shared/frozen work remains with its owner; no publication is claimed.

  4. added a commit that references this issue on Oct 25, 2026
  5. rmcdaniel commented on Oct 25, 2026

    @rmcdaniel
    MemberAuthor

    Native reference isolation for embedded v2 import, from the Security review of #764. General owns the published PHP side in Workflow #928 and Core #929. Published Server 2.11.1, with Workflow 2.10.2 through 2.13.1, copies every reference a bundle carries without checking it. Native refuses such a bundle. This is the audit of every field the importer copies, at #764 head 6f109c2c118bf3c3c309eb606631357d5a29e6a4.

    Published defects beyond the two run/instance lookups already recorded here:

    • Lineage. A link's related run or instance can belong to another namespace. History export then lists that foreign run's metadata, and retention and cancellation traverse the link.
    • Commands. A signal's or update's command_id can name another workflow's command. Later application then updates that command by id alone: applied_at, plus outcome for updates.
    • Task payloads. A task payload's activity_execution_id can name another run's activity. A lost lease then settles that activity, because it is looked up by id alone.
    • Signing key in the URL. signing_key is read from the query string too. Access logs, tracing and client history keep URLs.

    What native does (writes are atomic, and nothing is committed on refusal):

    Copied field Runtime use found Native rule
    commands/signals/updates[].resolved_run_id Retention deletes by it Must be the imported run (references.foreign_run)
    commands/signals/updates[].requested_run_id Retention deletes by it Null, the imported run, or another run of the same workflow in this namespace (write check)
    signals[].command_id Signal application joins and updates the command A signal command of this bundle; the application join and update also require the same instance
    updates[].command_id Update application joins and updates the command An update command of this bundle; same instance on application
    updates[].failure_id Only checked for non-empty A failure of this bundle, or an id no failure holds
    history_events[].workflow_task_id, workflow_command_id Read only with workflow_run_id A task or command of this bundle
    activities[].current_attempt_id Observation join, run-scoped One of that activity's own attempts
    activities[].attempts[].workflow_task_id / task_id Heartbeat and claim joins A task of this bundle. Attempts of an activity the importer skips are refused.
    tasks[].payload.{activity_execution_id, activity_attempt_id, timer_id, workflow_signal_id, workflow_update_id, workflow_command_id} and resume_source_id for those kinds Claims are run-scoped. Lost-lease settlement was not, and now is. A row of that kind in this bundle
    failures[].source_id (kind activity_execution) Display An activity of this bundle. Other kinds are display-only and stay as recorded.
    workflow.archive_command_id Display A command of this bundle
    links.parents/children endpoints Export, retention, cancellation The related run must exist in this namespace and in the instance named. A run or instance in another namespace refuses the import. A run that exists nowhere yet is not written, and its own import writes the link once both runs exist. A self-link is refused. Export also omits any related run of another namespace.
    signals[].signal_wait_id, attempts[].worker_attempt_id Identity within the run, or metadata Inert
    Ids inside history event payloads Readers stay within the run. Child runs are resolved only through workflow_links (fenced above) or a namespace-checked visible-run lookup. History, not references

    The signing key is read only from the body or the configured secret. A query signing_key in any spelling, signing_key[] included, gets 422 signing_key_in_query, and the answer never repeats the value.

    Evidence: rust/tests/execution/workflow_import_fences.rs runs on SQLite, PostgreSQL and MySQL. It covers refusal in both lineage directions, refusal of a foreign instance, a deferred link that a later foreign run never captures, refusal of each foreign row reference, forged signal and update rows refused at application, worker and operator 403s with admin as the positive control, and the URL signing key. Each refusal is checked against byte-for-byte export snapshots of the other namespace. Without the fence and predicates, five of the seven tests fail.

    🤖 Generated with Claude Code

  6. rmcdaniel commented on Oct 25, 2026

    @rmcdaniel
    MemberAuthor

    PHP source follow-through is now at Workflow PR #929 head c2d32ac2171587e27f76c6b8ad7393f2e1e62456, still draft. Evidence and exact remaining gates cover the new-import lineage slice, same-bundle message command ownership, and direct target guards.

    Both lineage directions now check namespace plus run/instance identity, missing endpoints do not become physical edges, and display/export and ChildRunHistory reader fallback cannot attach a later unrelated run. Mutating consumers were not fenced at this head; Security found those gaps, addressed in the successor below. Qualifying omission exposed a PostgreSQL reciprocal-import race; a real two-process regression now observes a complete serializable retry and exactly one edge. The common 100-case cohort passes on SQLite/MySQL/PostgreSQL, with only the two PostgreSQL-specific controls skipped on the other databases. Retained PHP replay consumers pass 76/3,789; the 17 new lineage controls all fail against the preceding importer/readers with zero test errors.

    The native copied-field audit is received for PHP comparison. Next are task payload/lost-lease references, resolved/requested run IDs, historical task/command IDs and the other copied references. Legacy dangling-edge recovery, continuation/retry compatibility and MySQL/SQLite concurrency remain unqualified. The PHP signing-key URL path also remains required here. Existing source review applies to earlier slices only; no current-head acceptance, package publication, Server consumer update or fixed published image is claimed.

    Successor and correction: durable-workflow/workflow#929 (comment). Exact head 5e01a93 awaits review; this receipt is historical evidence for c2d32ac2 only.

  7. rmcdaniel commented on Oct 25, 2026

    @rmcdaniel
    MemberAuthor

    Security re-review of Workflow PR #929 exact head c2d32ac2171587e27f76c6b8ad7393f2e1e62456 found two remaining PHP lineage authority blockers: history-only missing endpoints are still consumed by scoped child cancellation and parent-resume mutation paths without an exact materialized edge, and self-lineage is still accepted/resolved. Exact source paths, counterexamples, and required controls are recorded in durable-workflow/workflow#929 (comment).

    The new import/store/view work is not accepted at this head. PostgreSQL serializable retry evidence remains valid; MySQL/SQLite absent-row concurrency, legacy dangling-edge recovery, and the broader copied-reference audit remain open separately.

  8. rmcdaniel commented on Oct 25, 2026

    @rmcdaniel
    MemberAuthor

    PHP import follow-through is at Workflow draft PR #929 head 5e01a938a8766af8f47861aba69d5497bf6731d0. Exact source evidence and open gates are recorded for review. This successor fences imported child cancellation/parent-resume effects with exact directed edges and rejects self-lineage, addressing Security's two c2d32ac2 blockers. It has not yet been accepted.

    The 129-case importer cohort passes on SQLite, MySQL and PostgreSQL; 18 scope-effect cases pass on each database. The 47 current controls against preceding production source produce 42 assertion failures, five valid-pair passes and zero errors. Native scope behavior (63/525), focused child consumers (46/2,076) and retained PHP replay consumers (76/3,789) pass. Hosted quality/corpus are green; unit/feature are pending. A broader local selection hit a tracing subprocess timeout, so full-suite completion is not claimed.

    The prior handoff's fallback claim has been narrowed to the display/export and ChildRunHistory readers it actually covered; mutating paths were unsafe at that old head. Legacy dangling-edge recovery, continuation/retry import compatibility, MySQL/SQLite concurrency, the remaining copied-reference/runtime audit and PHP signing-key URL path remain open. No package, consumer update or fixed published image is claimed. General next addresses the separately owned Workflow #931 service-operation authority boundary while this exact-head review runs; #928/#765 remain General commitments.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions