Repository navigation
Rust #325: import embedded v2 workflow histories like Server 2.11.1 - #764
Conversation
…ished 2.11.1 POST /api/workflows/import/embedded-v2 from bundles built out of real published exports (a completed run with an activity, timer, signal, memo and search attributes; a terminated run; a running run): the request gates, body shapes, a dry run, an import, idempotent and conflicting repeats, integrity and signature refusals, each eligibility rule, request normalization before the checksum, bare bundles, a failed write, non-terminal imports, and the imported run through describe, runs, history and list. Recorded three times, identical. Two published defects are bounded: the importer finds runs and instances in every namespace, so a bundle imported elsewhere answers already_imported and an import takes over an instance of another namespace.
Native now serves POST /api/workflows/import/embedded-v2 as published Server 2.11.1 does with Workflow 2.10.2 (EmbeddedV2ImportController and EmbeddedV2HistoryImport): - The request is read as PHP reads it: json_decode with PHP's key types, order and int/float rules, Laravel's TrimStrings and ConvertEmptyStringsToNull, and options from the body, then the query. - BundleIntegrityVerifier's report keeps PHP's check order, rules, messages, paths and contexts. The checksum and HMAC-SHA256 signature are computed over PHP's canonical JSON and compared in constant time. - The eligibility rules, the dry run, idempotent and conflicting repeats, and the write of every bundle section run in one transaction. The run's summary is projected from the imported rows, with the embedded import's engine source. - The route is admin-only and writes into the requested namespace. A bundle counts as already imported only in that namespace; a run id held in another namespace is a run_id_conflict, and an instance of another namespace is refused rather than taken over. The fixture bounds the published cross-namespace answers as defects. - JSON objects and Avro maps are parsed in linear time; a repeated key keeps its first position with the last value, as in PHP. Two shared reads now match published 2.11.1 for every run, which the import fixture exposed: - Describe shows the reason each command's payload records (commandReason) when the command was not rejected. Only inline payloads up to 8 KiB are read; every writer caps the reason at 1000 characters. - History reports the run's compatibility, compatibility_status, compatibility_supported_in_fleet and compatibility_fleet_reason, as HistoryController::show does, and the control plane carries compatibility. Three stale unit expectations are corrected: published PHP's canonical JSON (JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION) and json_encode both escape non-ASCII characters and line separators. Qualified locally: the workflow-import fixture recorded from published 2.11.1 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 (265 unit, 454 execution). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # .github/workflows/server-parity.yml
…workflow-import Both sides described omitted command previews. Main reads native stored references (a start's payload and a signal's arguments) through per-backend exact-prefix comparisons. This branch also recognised PHP's own external references, which an imported history keeps. The merged query keeps main's comparisons and adds two columns in the same style: php_payload_reference (exact 23-byte PHP prefix) and reason_payload for commandReason(), which never reads either kind of reference. A preview is omitted when either reference is oversized. The query test now pins both new comparisons and still refuses LIKE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unserved-route list arrived with #745 and still named it, so route_coverage failed after the merge. Native serves 131 of the 175 published route methods on this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto rust-325-workflow-import Native now serves 139 route methods: main's 138 plus embedded v2 import. The runtime keeps both new modules, and 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>
…ment served Both are served on main (worker sessions since #740), but the missing route list still named them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The two published PHP defects this fixture bounds ( |
SonarCloud rust:S3776 on #764: check_payload_manifest (31) and Parser::value (33) exceeded the cognitive complexity limit of 30. check_manifest_entry now holds one payload manifest entry's checks, in the same order. Parser::json_list and Parser::json_object hold the JSON array and object parsing that value() delegated to nested() as closures. Behavior is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
New exact head |
|
PHPUnit Feature on |
|
Security review blocker at exact head Cross-namespace lineage rows remain writable through import. The new run/instance guards correctly refuse the two published defects already recorded in #765, but the link writer still accepts each bundle-supplied opposite endpoint without checking its namespace. In That row is observable from the other namespace: The route being global-admin-only limits who can trigger it today, but #765 explicitly establishes namespace isolation as an invariant and this still lets one requested namespace mutate another namespace's durable view. Current fixture bundles have empty parent/child link lists, so the two cross-namespace controls do not cover this path. Please hold merge and add atomic negative controls for both parent and child directions. The implementation needs to prevent existing foreign endpoints and also define a safe rule for dangling related IDs (otherwise a link admitted before the related run exists can become cross-namespace later). Preserve both namespaces on refusal. I recommend extending #765 with this third published/native path and covering the relevant database families. No live environment was touched. |
|
Follow-up: the blocker is broader than The importer copies bundle-supplied Later, ordinary A-namespace execution follows that reference and updates the command by ID alone:
So an A worker can complete imported A work and mutate B's durable command row. The unscoped command/signal/update join in history export also lets the A row alter B's exported command projection. Integrity/checksum verification does not validate this relational ownership. Please treat this as the same merge blocker but fix/test the whole imported reference graph, not only lineage. At minimum, signal/update command references must resolve to a command imported for this exact run/instance (or the import must refuse atomically). Add negative controls for foreign signal and update command IDs that exercise the later application path and prove the foreign command and both namespaces remain byte-for-byte unchanged. Other copied reference fields should be audited for the same ownership property before acceptance. |
|
Additional acceptance gap at exact head 8efdfe0: the Rust role-token test and admission-role-tokens fixture do not mention POST /api/workflows/import/embedded-v2, while the new import probe only uses the legacy/admin token. The guard source classifies the route as admin-only correctly, but this bulk durable-state write needs an executable role boundary: worker and operator must receive 403 before controller execution, while admin reaches the controller. Please add those negative/positive controls with unchanged state. |
|
Security hardening required before acceptance: request() currently accepts signing_key from the URI query just like non-secret options, and the handler parses the full URI. An HMAC key in a URL can be retained by reverse-proxy access logs, tracing, browser/client history, and telemetry even though native does not echo the value. Please restrict signing_key to a protected JSON body field or the configured runtime secret, keep non-secret options in the query if parity requires them, and add a sentinel test proving the value is absent from responses/diagnostics. If this intentionally diverges from published PHP, record the PHP-side defect separately. I am not claiming the current native logger emitted a key; this is an ingress design exposure. |
Security review of #764 (Server #765): published PHP copies every reference an import bundle carries, and native copied them too. A bundle imported into one namespace could link its run to another namespace's run, point its signals and updates at another workflow's commands (which later application updates by id), and point its tasks at another run's activities (which a lost lease settles by id). The signing key was also read from the URL. Native now refuses such a bundle before writing anything: - Every copied reference must name a row this import writes, of the expected kind: signal and update commands, update failures, event tasks and commands, activity attempts and their tasks, task payload references, the archive command and activity failure sources (`references.unresolved`). Resolved runs must be the imported run (`references.foreign_run`), and a link may not name the imported run itself (`references.self_link`). - In the write transaction, a requested run must be another run of the same workflow in this namespace, and a failed update's failure outside the bundle must belong to no run. - A lineage link is written only when its related run exists in this namespace and its instance. A related run or instance of another namespace fails the import. A related run that exists nowhere yet is not written; its own import writes the link once both runs exist, so no stored link can later capture a run created elsewhere. - signing_key is read only from the body or the configured secret; in the query string, in any spelling, it is refused with 422 signing_key_in_query and never repeated. Defense in depth for rows no import can now write: signal and update application join and update their command only within their own instance (typed 409 signal_command_unavailable / update_command_unavailable instead of a foreign mutation), the described-command and export joins require the same instance, history export omits related runs of another namespace, and lost-lease settlement reads only the task's own run's activity. tests/execution/workflow_import_fences.rs covers both lineage directions, a foreign instance, a deferred link a later foreign run never captures, each foreign row reference, forged signal and update rows at application, worker and operator 403s against the admin positive control, and the URL signing key, each against a byte-for-byte export snapshot of the other namespace. Without the fences and predicates five of the seven fail. Full execution suites pass on SQLite, PostgreSQL and MySQL (499 each). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
New exact head What changed (the commit message has the detail; the field-by-field audit is on #765):
Evidence.
🤖 Generated with Claude Code |
General showed on PostgreSQL that published PHP's import, with deferred links, loses a reciprocal link when parent and child import concurrently: each sees the other uncommitted and skips it. Native serializes durable transactions (PostgreSQL's transaction advisory lock, MySQL's ownership row FOR UPDATE, SQLite's single writer), so the second import sees the first run committed and writes the link. The new test dispatches both imports from two runtimes at once and requires both imported with exactly one link. It passes on SQLite, PostgreSQL and MySQL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
New exact head: General showed on PostgreSQL that a deferred link can be lost when parent and child import concurrently: each sees the other uncommitted. Native doesn't hit this, because every durable transaction is serialized: PostgreSQL's 🤖 Generated with Claude Code |
|
Security source re-review is complete at exact head
I separately traced The concurrency claim is supported in source on all three backends: SQLite uses This is Security acceptance of the exact source head only. I did not run Rust builds on the lab host. The current hosted matrix is still pending and must pass; merge/release/installed-image qualification and the published PHP-side correction remain separate gates. |
|
Security source re-ack for exact head 5eea6e2.\n\nGit-object verification:\n- parents are exactly the previously accepted 9ae2934 and merged CI commit f63f6c4;\n- 9ae2934..5eea6e2 changes only .github/workflows/phpunit-feature.yml;\n- every path outside that workflow is byte-identical to 9ae2934;\n- the workflow blob at 5eea6e2 is byte-identical to f63f6c4;\n- the workflow blob at accepted 9ae2934 was byte-identical to its former base 4e79554.\n\nTherefore the exact product source and four-boundary Security acceptance recorded in #764 (comment) carry forward unchanged. The only delta is the separately reviewed/merged #767 CI memory-and-diagnostics change.\n\nThis is exact-source re-ack only. The current final hosted matrix remains in progress; merge/release/installed-artifact claims still require its successful completion. No Rust build or local runtime was run on the lab host. |
On 5eea6e2 the workflow-import corpus differed at conflict_native_run: the probe renames a bundle's run to an existing native run, and published PHP answers target.run_id_conflict, but native's reference fence ran inside eligibility and answered the bundle's commands' resolved runs first. An ineligible bundle (identity_missing) would likewise have gained reference findings PHP never reports. The fence now runs after eligibility and the existing-run check, and before a dry run or a write, so every answer PHP gives stays PHP's and a crafted bundle is still refused before anything is written, dry runs included. A new test pins that order on each backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
Security re-review of exact head ce065ab (parent 5eea6e2): the source delta is accepted with no new source blocker. The change restores the published ordering: base eligibility failures return before native-only reference findings; an existing run returns the published run_id_conflict before those findings; otherwise the defensive reference fence still runs before either dry-run success or any write. The new test pins each boundary. The patch is limited to that reordering/helper and its regression test, and git diff --check passes. I did not run Rust builds or tests. Merge acceptance remains gated on the exact-head hosted matrix. At this receipt, 14 checks are complete without failure and 41 cross-database/corpus checks are still in progress; do not treat this source acceptance as substituting for those required lanes. |
|
Security final exact-head acceptance for The previously reviewed ordering correction remains unchanged: base eligibility failures return before native-reference findings, the existing-run conflict retains the published Hosted exact-head evidence is now terminal:
Disposition: accepted for this exact head. This supersedes my earlier conditional source-only receipt. Any successor commit requires fresh qualification/review. I did not run Rust builds or tests on the lab host or the Rust VPS and made no runtime, credential, service, or production change. |
|
Merged as
Next in #325: the waterline import ( |
…-record-local-activity # Conflicts: # .github/workflows/server-parity.yml
…st-325-cancellation-scopes # Conflicts: # .github/workflows/server-parity.yml # docs/rust-server-port.md
…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
…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>



Native serves
POST /api/workflows/import/embedded-v2as published Server 2.11.1 does (Workflow 2.10.2'sEmbeddedV2ImportControllerandEmbeddedV2HistoryImport), bringing native to 139 of the 175 published route methods.Fixture (
d31d1b460), recorded from published 2.11.1Bundles are built from real published exports: a completed run with an activity, timer, signal, memo and search attributes; a terminated run; and a running run. The fixture covers:
It was recorded three times with identical results. It also bounds two published defects: the importer finds runs and instances in every namespace, so a bundle imported in another namespace answers
already_imported, and an import takes over another namespace's instance. Native refuses both: a run id held in another namespace is arun_id_conflict, and another namespace's instance is refused rather than taken over.Native (
2c9905189,864cadd28)json_decodewith PHP's key types, ordering and int/float rules, Laravel'sTrimStringsandConvertEmptyStringsToNull, and options from the body, then the query.BundleIntegrityVerifier's report keeps PHP's check order, rules, messages, paths and contexts. The checksum and HMAC-SHA256 signature are computed over PHP's canonical JSON and compared in constant time.compatibility,compatibility_status,compatibility_supported_in_fleetandcompatibility_fleet_reason, asHistoryController::showdoes.rust/tests/fixtures/unserved-php-routes.v2.11.1.txtdrops the route. The doc's missing-route list now names only Waterline v1 import, and no longer lists worker sessions or activity cancellation acknowledgement, both served on main.Qualification
No PHP application or fixture-expectation changes beyond the new fixture.
🤖 Generated with Claude Code