feat(workflow): decentrally-hosted external party#245
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new “ExternalParty” workflow so an external party can be onboarded as a multi-host, M-of-N-confirmation identity (hosted across multiple participants), plus end-to-end validation and UI surface to create/list these external parties.
Changes:
- Introduces the
ExternalPartyworkflow (coordinator + peer execution) with new Noise message types and workflow plumbing. - Adds API endpoints/types and frontend UI to start onboarding and list external parties.
- Updates integration/e2e tooling and tests (new phase + optional single-test runner behavior).
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| integration-tests/run.sh | Ensures script runs from repo root; adds selectable e2e test + interactive hold mode. |
| integration-tests/env.sh | Hardens cleanup on macOS/bash3 set -u empty-array behavior. |
| crates/decman/tests/governance_workflows.rs | Runs new external-party phase in the main e2e; adds standalone ignored test. |
| crates/decman/tests/common/phases/mod.rs | Registers new external_party phase module. |
| crates/decman/tests/common/phases/external_party.rs | New e2e phase driving /external-party and asserting results. |
| crates/decman/src/workflow/storage.rs | Adds artifact/identity kinds for external-party onboarding/key persistence. |
| crates/decman/src/workflow/mod.rs | Wires WorkflowType::ExternalParty coordinator + peer dispatch/step mapping. |
| crates/decman/src/workflow/external_party/config.rs | Defines ExternalPartyConfig for workflow runs. |
| crates/decman/src/workflow/external_party/mod.rs | Declares workflow step machine + command mapping. |
| crates/decman/src/workflow/external_party/keys.rs | Implements client-side Ed25519 key generation, fingerprinting, signing. |
| crates/decman/src/workflow/external_party/steps.rs | Wraps Canton Ledger-API calls to prepare topology and allocate external party. |
| crates/decman/src/workflow/external_party/coordinator.rs | Coordinator driver: key mgmt, topology prep, allocate, fan-out, persistence. |
| crates/decman/src/workflow/external_party/peer.rs | Peer handler to authorize hosting via AllocateExternalParty. |
| crates/decman/src/server/types.rs | Exposes new API types for external parties. |
| crates/decman/src/server/mod.rs | Adds invitation meta handling and registers external-party routes. |
| crates/decman/src/server/handlers/workflows.rs | Implements /external-party start/status/cancel + /external-parties listing. |
| crates/decman/src/server/handlers/mod.rs | Re-exports new workflow handlers. |
| crates/decman/src/server/handlers/invitations.rs | Adds step totals for ExternalParty invitation cards. |
| crates/decman/src/noise/mod.rs | Adds AllocateExternalParty + InviteExternalParty message types and tests. |
| crates/decman/src/noise/server.rs | Registers ExternalParty as an active workflow variant and peer threshold behavior. |
| crates/decman/src/db/schema.rs | Adds schema traits for querying runs by kind + persisting dec_party_id mid-run. |
| crates/decman/src/db/sqlite.rs | Implements new DB read/write helpers (get_workflow_runs_by_kind, set_workflow_run_dec_party_id). |
| crates/decman/src/bin/gen_types.rs | Includes new external-party types in type generation. |
| crates/decman/frontend/src/components/NotificationsView.tsx | Avoids dead link for ExternalParty dec_party_id in workflow cards. |
| crates/decman/frontend/src/components/ExternalPartyList.tsx | New table UI for listing external parties. |
| crates/decman/frontend/src/components/ExternalPartyDialog.tsx | New dialog UI to start external-party onboarding with validation + mesh errors. |
| crates/decman/frontend/src/App.tsx | Adds “External Parties” view/tab and create button behavior. |
| crates/decman-cli/src/api.rs | Adds cancel path for ExternalParty. |
| crates/common/src/types.rs | Adds ExternalParty to WorkflowKind/InvitationType. |
| crates/common/src/api.rs | Adds request/response/invite payload types for external parties. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Copilot review: 6 findings, all addressed in ad7881a and threads resolved. 5 were stale comments, now fixed: dropped the single-node framing on ExternalPartyRequest, removed two dead links to an uncommitted doc, and corrected the /external-parties (reads dec_party_id, not artifacts) and EXTERNAL_PARTY_SEED (AES-GCM encrypted, not plaintext) comments. Declined 1 (keys.rs fill_bytes): compiles as-is via the Rng trait in rand 0.10; the suggested RngCore import doesn't build. Verified with clippy --all-targets --all-features -D warnings. |
|
Copilot round 2 (on 6619c02): 3 findings, all fixed in c1ef838 and resolved.
|
| .await | ||
| { | ||
| Ok(response) => interpret_invite_reply(peer_id, "external-party", &response)?, | ||
| Err(e) => tracing::error!("Failed to send invite to {peer_id}: {e}"), |
| observing_participant_uids: Vec::new(), | ||
| }; | ||
|
|
||
| let mut client = utils::create_party_client(config, external_party_token()).await?; |
| identity_provider_id: String::new(), | ||
| }; | ||
|
|
||
| let mut client = utils::create_party_client(config, external_party_token()).await?; |
| let resp: Value = f.get_json(f.p1.http, "/external-parties").await.ok()?; | ||
| let parties = resp.get("parties").and_then(Value::as_array)?; |
Onboard a sovereign external party hosted across N participants at an M-of-N confirmation threshold. The coordinator generates the party's client-side Ed25519 key, builds the multi-host onboarding topology, authorizes hosting on its own participant, and fans the party-signed bundle out to each hosting peer over Noise to authorize on theirs. The client key is sealed into the durable identity store so the party stays recoverable. Includes the Noise message types, invitation flow, peer-coordinated workflow, HTTP handler, frontend dialog, and the 2-of-3 e2e phase.
cargo-machete (#244) dropped rand from decman as unused; the external-party keys module now uses it. Also apply cargo fmt to the import blocks touched by this feature.
Drop the single-node framing and dead docs/ links, and fix the /external-parties and EXTERNAL_PARTY_SEED comments to match actual behavior (dec_party_id column; AES-GCM-encrypted artifacts).
…y FK workflow_artifacts has a FK to workflow_runs, so the test must create the run row before writing artifacts (as the real flow does). Verified: full decman lib suite (283 tests) passes.
The phase posted /external-party but never accepted the invitations on P2/P3, so on non-interactive CI the peers never joined and the coordinator hung at WaitingForPeers until the 180s timeout. Mirror create_dec_party: probe + accept the ExternalParty invitations before waiting for completion.
Hosting an external party is a safe operation (the party's namespace key is client-held; a host only authorizes hosting on its own participant), so it shouldn't need a manual approval. A peer receiving an InviteExternalParty now spawns the peer run directly and records no pending card. Refactored insert_peer_run to take &SqlitePool so record_invitation can reuse it; the e2e phase no longer accepts manually.
Review round 2: (1) the auto-accept path returned unconditionally, dropping the invite when insert_peer_run or the job send failed and leaving the coordinator waiting for a host that never starts; now falls through to a pending card for manual retry. (2) run the e2e with --exact so DECPM_E2E_TEST resolves to exactly one test. (3) fix the ExternalPartyDialog threshold comment (server's unset default is all hosts, not majority).
Adds /v0/tenant/* endpoints so a wallet (holding the party's Ed25519 key client-side) drives onboarding and transacting; DPM relays. Authenticated by a provider-issued API key (DECPM_TENANT_API_KEYS), separate from the Keycloak UI token (bypasses the JWT middleware; each handler enforces require_tenant_api_key; relaxed in insecure/test mode).
Onboarding: POST /v0/tenant/prepare (DPM builds the multi-host topology from the wallet's public key, returns the multi-hash to sign) -> POST /v0/tenant/onboard (wallet submits its signed bundle; DPM allocates + fans out via the existing multi-host workflow, skipping key-gen/sign) -> GET /v0/tenant/{party}/status. Transacting: POST /v0/tenant/{party}/prepare-submission + execute-submission (single-signer interactive submission) and GET /v0/tenant/{party}/acs.
Reshapes prepare_topology to take a public key; ExternalPartyConfig gains prepared_bundle; the coordinator skips key generation + identity persistence in the wallet flow. start_external_party's core is factored into spawn_external_party_onboarding, shared by both flows.
New phase: a client-generated key drives onboarding via /v0/tenant/prepare -> sign -> /v0/tenant/onboard (DPM never sees the private key), then asserts completion across P1+P2+P3 and that the party's ACS is readable via /v0/tenant/{party}/acs. Reuses ExternalKeyPair as the wallet. Full transacting (prepare/execute-submission of a real contract) is exercised on DevNet since it needs a concrete template.
The tenant ACS/prepare-submission calls use the participant's ledger user token, but that user has no rights on a freshly-hosted wallet party, so Canton returned 'no permission'. After a successful AllocateExternalParty, grant the ledger user CanReadAs the party on that host (read-only — never CanActAs, which would let a host impersonate the sovereign party). Best-effort + insecure/test-scoped, mirroring external_party_token; production external-party ledger auth remains the doc's open item.
Makes the external party genuinely decentrally-hosted: hosted across N participants at an M-of-N confirmation threshold, instead of the single-host v0.
Flow:
Validated on a 3-node localnet: onboards a real 2-of-3 party, all three hosts authorize, and it lists on every node.
Tests: unit (threshold bounds, key persistence, fingerprint derivation) plus a 2-of-3 e2e phase.
Not in scope: interactive submission (the party transacting) and /unhost.