feat(server): allocate allowances from HostAdmin without a forged frame - #467
feat(server): allocate allowances from HostAdmin without a forged frame#467filvecchiato wants to merge 6 commits into
Conversation
RFC-0028. HostAdmin::allocate_allowances reaches ProductAuthority::allocate_resources directly and does not raise the product-facing ResourceAllocation confirmation review, since no product is asking. Hosts previously had to synthesize a ProtocolMessage through a short-lived product endpoint and suppress the resulting prompt with an out-of-band token keyed by (product_id, resource), which could match and silently approve a concurrent real product request. Status and invalidate are specified in the RFC but not implemented here: their semantics differ between the pairing and signing roles and need agreement first.
The check-rfc gate requires every docs/rfcs/ change to carry a matching rust/crates/truapi/ change. This surface is host-side and adds no product-facing protocol method, and the crate invariants keep host-side runtime types out of truapi, so it cannot satisfy that gate.
The HostAdmin change stands on its own; the crate README documents it.
| ); | ||
| // Whatever the stub authority answers, it must not be the decline the | ||
| // product path produces from the same platform. | ||
| if let Err(err) = outcome { |
There was a problem hiding this comment.
The if let Err here is a no-op when the outcome is Ok, so nothing is asserted on success. I replaced the whole body of allocate_resources_for_host after the session check with Ok(Vec::new()) and all three tests still passed, so this one cant tell
"reached the authority without a review" from "did nothing". Can we assert the shape we expect unconditionally?
| } | ||
|
|
||
| /// Correlation id for a host-initiated allowance request. Unique per call | ||
| /// because the SSO channel matches responses on it, so two concurrent |
There was a problem hiding this comment.
I dont believe the SSO channel matches responses on this id? It looks like remote_allocate_resources generates its own message_id from sso_message_id(), and submit_remote_message registers on that one.
The uniqueness still seems worth keeping since this id shows up in the cancel error text. Can we fix the reason in the comment?
| ) | ||
| .await | ||
| .map(|response| response.outcomes) | ||
| .map_err(|err| v01::GenericError { |
There was a problem hiding this comment.
Unavailable, NotSupported and Unknown all render as just their inner reason, so after to_string() a host cant tell them apart. For the Recovery origin thats the retry or dont-retry decision.
I know AuthorityError is pub(crate) so this isnt a small change, and the rest of HostAdmin returns GenericError too. Worth saying in the body what a host is meant to do with a failure?
TarikGul
left a comment
There was a problem hiding this comment.
Overall looks good, few nits.
Imod7
left a comment
There was a problem hiding this comment.
HostAdmin is only reachable through PairingHostRuntime::product_admin and SigningHostRuntime::product_admin, so a Rust embedder can call
allocate_allowances but the wasm and UniFFI surfaces cannot.
PairingHostRuntime already wraps the permission operations by product id over
self.product_admin(product_context(product_id)?) in
rust/crates/truapi-server/src/host_core.rs. Please add the same wrapper on both role handles plus a wasm.rs method, so the browser host can stop synthesizing a product frame.
| )] | ||
| pub(crate) async fn allocate_resources_for_host( | ||
| &self, | ||
| resources: Vec<v01::AllocatableResource>, |
There was a problem hiding this comment.
Please refuse AllocatableResource::AutoSigning here. On a signing host this
path reaches grant_auto_signing with no confirmation, and that grant is what
lets sign_vrf skip its per-call prompt, so a host-initiated call can silently
let a product sign as the user. An early return when resources contains
AutoSigning is enough.
| remote_authority_call( | ||
| &cx, | ||
| self.authority | ||
| .allocate_resources(&cx, &session, self.product_id(), request), |
There was a problem hiding this comment.
Increase is the wrong policy for StartupReadiness and ForegroundRenewal. It
sets reuse_existing = false, the flag that lets the slot scan notice an
allowance already exists, so a foreground re-check claims another slot and
submits another extrinsic every time. The live People chain declares 20 plus 10
slots per person per 24 hours, pooled across all their products, with eviction
off. RFC-0010 says send Ignore unless you are scaling up a cached allocation.
Please add a policy argument to ProductAuthority::allocate_resources and derive
it from origin, keeping Increase for Recovery and for the product path.
| /// The product-facing `ResourceAllocation` confirmation review is not | ||
| /// raised: it exists to put a product's request to the user, and no | ||
| /// product is asking. A host that wants to prompt for its own maintenance | ||
| /// work decides that itself, using `origin` to tell the cases apart. |
There was a problem hiding this comment.
Please scope this claim to the local runtime. On the pairing-host role
resource_allocation_response on the paired wallet raises
UserConfirmationReview::ResourceAllocation with calling_product_id set before
allocating, so the user is prompted for something no product asked for and the
call waits up to 300 seconds. Only the signing-host role is silent. The README
paragraph needs the same qualifier, and
host_allowance_allocation_raises_no_confirmation_review would be more accurate
as ..._raises_no_local_confirmation_review.
HostAdmin::allocate_allowances(resources, origin)allocates product-scopedresources on the host's own initiative — startup warm-up, foreground re-check,
recovery after a rejection. It calls
ProductAuthority::allocate_resourcesdirectly and does not raise the product-facing
ResourceAllocationconfirmationreview, whose contract is "a product asked for this": no product is asking.
HostAllowanceOrigin(StartupReadiness,ForegroundRenewal,Recovery)records which lifecycle moment did, so a host prompting policy can be added later
without touching a signature.
RFC-0010 made allowance "entirely the Host's concern", but only the
product-facing half landed.
HostAdmincarries no allowance operation, so a hostadministering its own allowances has to reach the implementation by impersonating
a product: synthesize a
ProtocolMessage, push it through a short-lived productendpoint, capture the response frame. That forged frame then arrives as an
ordinary
UserConfirmationReview::ResourceAllocation, so the host cannot tell theruntime "this one is mine" and instead arms an out-of-band token keyed by
(product_id, resource)that its own consent delegate silently approves. Agenuine product request for the same product and resource, landing while a host
dispatch is armed, consumes that token and is approved without asking the user.
Only the runtime knows which call it originated, so only the runtime can close
that window.
No wire-protocol change and no new generated method.
Not included
allowance_statusandinvalidate_allowanceare the obvious companions and areleft out on purpose: they mean different things in the two roles.
PairingHostcaches keys received over SSO, where presence and eviction are well defined and
the primitives already exist as
pub(super)(evict_bulletin_allowance_key,clear_*_allowance_keys).SigningHostis the Account Holder and provisions ondemand via
OnExistingAllowancePolicy::Ignore, so cache presence is the wrongquestion and an on-chain slot probe is the meaningful one. That wants agreement
before code.
Native bindings are also out of scope here — a separately CI-gated step with
committed bindings. Renewal already reaches the FFI via #308, so allocation and
status remaining Rust-only is a known asymmetry to close next.
Allocation alone retires both the forged frame and the auto-confirm registry,
which is the half with a security consequence.
Tests
cargo +nightly fmt --check,cargo clippy --workspace --all-targets --all-features -- -D warningsandcargo test --workspaceare clean.Three new tests: no-session rejection, correlation-id uniqueness (the SSO channel
matches responses on it, so concurrent host allocations must not collide), and
that the host path raises no confirmation review. The last asserts the product
path raises exactly one review against the same platform first, so it cannot pass
vacuously.