Skip to content

feat(server): allocate allowances from HostAdmin without a forged frame - #467

Open
filvecchiato wants to merge 6 commits into
mainfrom
rfc/host-allowance-admin
Open

feat(server): allocate allowances from HostAdmin without a forged frame#467
filvecchiato wants to merge 6 commits into
mainfrom
rfc/host-allowance-admin

Conversation

@filvecchiato

@filvecchiato filvecchiato commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

HostAdmin::allocate_allowances(resources, origin) allocates product-scoped
resources on the host's own initiative — startup warm-up, foreground re-check,
recovery after a rejection. It calls ProductAuthority::allocate_resources
directly and does not raise the product-facing ResourceAllocation confirmation
review, 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. HostAdmin carries no allowance operation, so a host
administering its own allowances has to reach the implementation by impersonating
a product: synthesize a ProtocolMessage, push it through a short-lived product
endpoint, capture the response frame. That forged frame then arrives as an
ordinary UserConfirmationReview::ResourceAllocation, so the host cannot tell the
runtime "this one is mine" and instead arms an out-of-band token keyed by
(product_id, resource) that its own consent delegate silently approves. A
genuine 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_status and invalidate_allowance are the obvious companions and are
left out on purpose: they mean different things in the two roles. PairingHost
caches 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). SigningHost is the Account Holder and provisions on
demand via OnExistingAllowancePolicy::Ignore, so cache presence is the wrong
question 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 warnings and cargo test --workspace are 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.

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.
@filvecchiato
filvecchiato requested review from a team August 20, 2026 14:50
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.
@filvecchiato filvecchiato changed the title RFC 0028: Host allowance administration feat(server): allocate allowances from HostAdmin without a forged frame Aug 20, 2026
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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 TarikGul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall looks good, few nits.

@Imod7 Imod7 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.

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>,

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.

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),

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.

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.

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.

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants