Skip to content

feat: add Chat v2 product authority operations - #709

Open
replghost wants to merge 69 commits into
feat/pvm-app-runtimefrom
feat/chat-v2-product-authority
Open

replghost wants to merge 69 commits into
feat/pvm-app-runtimefrom
feat/chat-v2-product-authority

Conversation

@replghost

@replghost replghost commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Chat-specific product authority above the generic PolkaVM host integration in #540. Chat cryptographic authority, device binding, request signing, sealing, and opening remain outside the generic host PR.

Authorization review follow-up

  • local product_device_chat now requires dedicated ChatAuthority consent instead of reusing IdentityDisclosure/username consent
  • local and SSO regression coverage exercises username-only grants, explicit denial, separate approval, cached approval, and revocation; real Bind and Seal/Open operations are used
  • get_user_id keeps its separate identity-disclosure permission; no legacy username grant is migrated into Chat authority
  • move the secret-bearing pairing Success out of its box instead of cloning it
  • inherit the warning-free Wasm target boundary and CI fixture/provenance fixes from refactor(polkavm): isolate optional host composition #540

Matched downstream artifacts

  • native source: ddccbac617a7490dc57ed25040e460bfdc4a1b65
  • generic base: e69940f46692f7fc5c0c0e77f910eb0aa02c7442
  • browser consumer: feat(chat): integrate Chat host authority above PolkaVM runtime dotli-community#236; client, host bindings, and release Wasm were rebuilt together from this Chat source
  • Chat Wasm SHA-256: bd9259c586ae38e62d4d87a8bcc338e721355367af1263f60327f171e38dff32
  • TrUAPIHost and TrUAPIProvider Swift bindings and device/simulator XCFrameworks were rebuilt locally; a signed Epoca iOS build-for-testing passed against that SDK

Verification

  • regression reproduced with the old username gate: Bind incorrectly succeeded; the corrected gate passes the regression
  • targeted Chat tests: 25 passed
  • full Rust workspace with warnings denied: 1,324 passed, 21 ignored
  • workspace all-target/all-feature Clippy, provider Wasm and UniFFI Clippy configurations, Wasm warnings-as-errors check, and formatting: passed
  • real Chromium + rebuilt Wasm + Dotli consent UI: deny persists; separate approval permits Bind and a real Seal/Open roundtrip; revocation rejects Bind, Seal, and Open without another prompt
  • Dotli typecheck, lint, production build, and 47 permission tests: passed

The phone remains unavailable to CoreDevice, so the on-device message scenario is not verified and no message has been sent. This correction has not been deployed or released; earlier staging deployments do not contain it.

For native retests, rebuild ios/truapi-host/scripts/rebuild.sh and ios/truapi-provider/scripts/rebuild.sh from this source, then use TRUAPI_USE_LOCAL_BINARY=1 and TRUAPI_PROVIDER_USE_LOCAL_BINARY=1 in the consuming build. A revision-only SPM pin is not sufficient: the published prebuilt SDK artifacts were not changed.

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 10, 2026

@decrypto21 decrypto21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check ordering looks right everywhere. One substantive issue.

1. productDeviceChat reuses a permission meant for something much weaker

It gates on IdentityDisclosure (runtime.rs:576) — the same slot get_user_id uses for "show this product your username" (capabilities/account.rs:513). The key is product-scoped only (truapi-platform/src/lib.rs:1399), nothing marks which capability is asking, and truapi-platform/ has no diff here. But this call binds the wallet's Chat identity and grants a standing Seal/Open oracle against any peer key the product names.

  • Signing-host (host_core.rs:597, via frame_server.rs:177): only gate, since SigningHost::product_device_chat (signing_host.rs:995) checks only the session. A product with an older get_user_id grant gets Bind/Seal/Open with no prompt — reproduced on this branch (pre-seeded grant → proceeds, prompt count 0; no grant → Rejected).
  • Two-device SSO: sso_responder.rs:934 does prompt the first time, but shows "wants to know it's you" for identity binding plus an encryption oracle. Silent after that.

Worth a dedicated PermissionAuthorizationRequest variant with its own review copy, like the neighbouring AccountAccess. If the reuse is deliberate, the doc comment (truapi-platform/src/lib.rs:1029) and prompt copy should say so.

2. Minor

sso_pairing.rs:399 switches success: *success to (*success).clone() on a struct holding identity_chat_private_key. If that was for the new Drop impl, it isn't needed — box-deref-move compiles fine with Drop. Keeping the move avoids a second live copy.

# Conflicts:
#	rust/crates/truapi-codegen/tests/golden/wire_table.rs
#	rust/crates/truapi-server/src/host_logic/sso/messages.rs
#	rust/crates/truapi-server/src/host_logic/sso/messages/v1.rs
#	rust/crates/truapi-server/src/runtime/authority.rs
#	rust/crates/truapi-server/src/runtime/capabilities/account.rs
#	rust/crates/truapi-server/src/runtime/pairing_host.rs
#	rust/crates/truapi-server/src/runtime/pairing_host/sso_channel.rs
#	rust/crates/truapi-server/src/runtime/signing_host.rs
#	rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs
@github-actions github-actions Bot added the host-work Needs implementation in one or more host repos label Sep 10, 2026
@github-actions github-actions Bot added the github_actions Pull requests that update GitHub Actions code label Sep 23, 2026

@decrypto21 decrypto21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 8de303f6e. ChatAuthority resolved, and reconcile no longer
opens the wallet before checking for payment work.

  1. The app target does not compile. Seven errors, five files, all new here:

    InMemoryCoinageAssetLedger.swift:188       parameter uses a private type
    TrUAPIChainConnectionPool.swift:298,308    parameter / result use a private type
    TrUAPIIdentityCandidatesTests.swift:29,44  cannot find 'SS58AddressFactory'
    RustRuntimeBridgeTests.swift:435           'HostThemeSubscribeItem' has no member 'dark'
    TrUAPINativeCoinageTests.swift:185         cannot force unwrap non-optional 'BigUInt'
    

    The first three need private on the method: a private extension scopes to
    the file, the types to the class body. RustRuntimeBridgeTests missed the
    currentTheme() return-type change, and the two new test files have never
    compiled. iOS CI stops earlier on SwiftLint (TrUAPINativeCoinage.swift:11,
    418 of 350, and complexity at :155), so the compiler never runs; fixing the
    lint uncovers these. Green on feat/pvm-app-runtime at 749cf906b.

  2. Over SSO the peer picks which product it is checked against.
    sso_service.rs:598 takes calling_product_id from the request, and
    SsoRequestContext holds only call and session. It keys
    PermissionsService::new (:608) and the authority request (:638), so a peer
    refused under its own name succeeds under another's, and that string also
    picks the keys. The local path is correct (capabilities/account.rs:398), per
    native-chat-main-purse.md:156. statement_store_product_sign (:549) is no
    model: it compares two fields of one request.

  3. read_balance (chat-v2/lib.rs:2681) accepts encodings read_compact_u128
    (:2621) rejects, and reads SendLegacy.amount and CoinageSend.total_value.
    Two byte strings decode to one message, so a statement can be republished with
    different bytes and the same meaning.

  4. The hardened suite is off. chat-v2 binds product, sender, recipient and
    channel into the authenticated data; actor/receive.rs:346 rejects it. The
    identity route uses the older suite, byte-identical to first-contact, so an
    open_chat_request_v2 aimed at a peer's identity key clears decryption and
    only the decoder stops it.

  5. Both tests pinning the wire codec version are dropped. client.test.ts loses
    uses the transport codec version for generated handshake calls, which sent a
    literal 2; the replacement at :650 compares TRUAPI_CODEC_VERSION to itself,
    so 99 stays green. It also loses logs a protocol violation for a known pair's out-of-range message type. Both are on the base.

  6. CHANGELOG.md:17 says codec 2; the constant is 3 and the handshake is exact.
    Line 26 says productDeviceChat; codegen drops product_, so it is
    deviceChat (generated/client.ts:304).

replghost and others added 2 commits September 23, 2026 15:14
- iOS: fix private-type visibility, stale test APIs and the BigUInt unwrap;
  split TrUAPINativeCoinage so SwiftLint passes and CI reaches the compiler.
- SSO: document that the paired host attests calling_product_id, and pin
  that consent, prompts and device keys follow the attested product.
- chat-v2: decode balances through the canonical compact decoder so
  non-canonical amounts are rejected.
- native Chat: pin that first-contact plaintext is rejected on the identity
  route and record why the context-bound suite stays off for native peers.
- TS: restore the literal codec-3 handshake test and the out-of-range
  message-type test.
- CHANGELOG: codec 3 and account.deviceChat.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-authority

Brings #540 (now carrying latest main) under the Chat v2 authority work.
Notable resolutions:

- permission prompts converge on upstream's three-way PermissionDecision.
  Chat v2 had centralised the prompt in PermissionsService using the older
  bool confirm_user_action, which cannot express AllowOnce; both
  check_or_prompt_identity_disclosure and check_or_prompt_chat_authority
  now use confirm_permission, so AllowOnce authorizes one request without
  persisting a grant. Caught by upstream's own regression test.
- PermissionsService takes a validated ProductContext, so the chat paths
  build one per call: a chat session serves several products, so the
  product stays a per-call argument rather than context state.
- native.rs keeps both sides' uniffi surfaces (chat files/coinage from
  Chat v2, permission decisions and product operations from upstream) and
  adopts upstream's chain-connect registration check, with HopProvider
  ported onto the new register_chain signature.
- de-duplicates three clean-but-wrong auto-merges: a stacked on_core_log
  impl, a doubled truapi_platform import list, and an unclosed
  injected-failure guard in the storage stub.
- gates StatementRenewalTarget re-exports to the cfg its item already had,
  and supplies the new permission_grants adapter field on wasm.

cargo check: workspace and wasm32 clean. cargo test -p truapi-server --lib:
1179 passed.
@replghost
replghost requested a review from a team as a code owner September 23, 2026 21:05
@github-actions github-actions Bot added rust Pull requests that update rust code host-ios Touches the iOS host tree host-android Touches the Android host tree labels Sep 23, 2026
replghost and others added 15 commits September 23, 2026 17:15
A Response that arrives after its request timed out is a normal late
frame, as the transport's comments already describe, not a protocol
violation. Pin both cases in the out-of-range message-type test, and keep
the RFC's route-classification claim to the route the tests cover.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Building the JS packages surfaced merge damage that neither cargo nor the
Rust suite could see:

- the callback success arm had both sides stacked, so operation-hold
  tracking and chat-file bookkeeping now live in one arm, and
  discardChatFileCallback's tail is closed again.
- one activateLocalSession protocol variant (upstream's, with the optional
  liteUsername) instead of two; likewise one init `role` and one
  activateLocalSession method on the runtime.
- role stays caller-controlled: a host that asks for none still sends
  none, which is upstream's additive-wire rule, and its two regression
  tests pass again.
- the test-host allowance shortcut moved to the function that actually
  derives and returns the secret; it had landed in the one that returns
  unit and takes the target from its caller.
- chainConnections keeps this file's RpcConnectionEntry, which is the
  shape the chain handlers read.
- the wasm glue test now asserts the split it documents: the production
  web bundle carries no signing host, the testing bundle does.

bun test: truapi 271 pass, truapi-host 282 pass. wasm32 clean under
--no-default-features and under wasm-signing-host,test-host.
…into feat/chat-v2-product-authority

# Conflicts:
#	rust/crates/truapi-platform/src/lib.rs
#	rust/crates/truapi-server/src/runtime.rs
#	rust/crates/truapi-server/src/runtime/signing_host/sso_responder.rs
…chat-v2-product-authority

# Conflicts:
#	js/packages/truapi-host/src/web/worker-provider.test.ts
… test

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…into feat/chat-v2-product-authority

# Conflicts:
#	hosts/ios/polkadot-app/Modules/Products/TrUAPI/TrUAPINativeCoinage.swift
@replghost

Copy link
Copy Markdown
Contributor Author

Thanks for the careful pass. All six addressed in 5704252, e4424be and d3dec53; verified at f296c55:

  1. iOS compile. The three methods using private types are now private. TrUAPIIdentityCandidatesTests imports NovaCrypto for SS58AddressFactory, the theme test checks the HostThemeSubscribeItem fields, and the BigUInt literal drops its !. TrUAPINativeCoinage is split (a dispatch switch, private operation helpers, and the Wallet seam in TrUAPINativeCoinage+Wallet.swift), so SwiftLint reports no errors and CI reaches the compiler. At f296c55, which also carries the migration to the refreshed host APIs (CoinageKeyIndex, chatFiles), build-for-testing for polkadot-app succeeds, and the TrUAPI and CoinageTests suites pass.

  2. SSO product identity. Agreed, the signing host keys on the calling_product_id the paired host sends. That is the trust model of every SSO method, not only deviceChat: AutoSigning already exports product_root_private_key for the same attested id, and resource allocation does the same. The paired host is the authenticated principal and attests which product is calling; a compromised paired host is outside this boundary. The RFC now states this instead of claiming the id comes from SSO context, and ProductRequest/SsoRequestContext document it. sso_chat_keys_consent_and_device_on_the_attested_product pins that a refusal, prompt and device key each bind to the attested product. Verifying product identity at the signing host would need per-product attestation across all of SSO, which belongs in its own RFC.

  3. read_balance. It now decodes through read_compact_u128, so SendLegacy.amount and CoinageSend.total_value reject non-canonical compact encodings. send_legacy_rejects_non_canonical_amount covers all four modes.

  4. Hardened suite. The identity route's key schedule is the native peers' own (pinned by identity_proof_and_routes_match_native_chat_vectors). Enabling the context-bound suite, or domain-separating that route, would stop mobile peers from reading native messages. So the wire stays as is. identity_route_rejects_first_contact_plaintext pins that first-contact plaintext sealed to an identity key is rejected on that route, with a positive control on the same route. The RFC records the constraint and that the context-bound suite stays off until native peers adopt it.

  5. Codec tests. Both tests are restored. The handshake test asserts a literal codecVersion: 3 on the frame and TRUAPI_CODEC_VERSION === 3, and the inbound-handshake test uses the literal too. Separately, a Response arriving after its request timed out was being logged as a violation; late Responses on a known pair are now ignored like the other late frames, and the test pins both.

  6. CHANGELOG. Now says codec 3 with an exact handshake match, and account.deviceChat.

@replghost

Copy link
Copy Markdown
Contributor Author

@decrypto21 ready for another look when you have time. Since the reply above, c5c39cfd also clears the iOS warning ratchet and moves top-up into TrUAPINativeCoinage+TopUp.swift. CI is green on it, and locally the app builds and the TrUAPI and CoinageTests suites pass.

get_account could never answer. Resolving a product's own account reads a
persisted subtree through the host, then asks the host to confirm. Neither
callback had a deadline, so a host that never answered its own storage parked
the request forever: no response frame and no error frame.

Both are now bounded by the caller's context, falling back to the same default
the SSO call below them already uses, so an unresponsive host surfaces a typed
HostAccountGetError instead of hanging.

The regression test stands a platform up whose core-storage reads never return.
Before this change the call never settles; after it, it fails within the
caller's deadline.

Closes #954
# Conflicts:
#	rust/crates/truapi-server/src/runtime/capabilities/account.rs

This branch has not been deployed

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

Labels

documentation Improvements or additions to documentation github_actions Pull requests that update GitHub Actions code host-android Touches the Android host tree host-ios Touches the iOS host tree host-work Needs implementation in one or more host repos javascript Pull requests that update javascript code rfc rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants