Skip to content

Own-account subtree consent gate, deadline bound, and worker dispose fix - #469

Merged
johnthecat merged 6 commits into
mainfrom
feat/subtree-consent-and-dispose-fix
Aug 21, 2026
Merged

Own-account subtree consent gate, deadline bound, and worker dispose fix#469
johnthecat merged 6 commits into
mainfrom
feat/subtree-consent-and-dispose-fix

Conversation

@johnthecat

@johnthecat johnthecat commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Three fixes on the own-account / desktop-host path.

Product subtree consent gate

Reason. A product requesting its own account takes no access review, so a cold subtree fetched from the Account Holder over SSO resolved with no seam the host could see or reject.

Intent. Let the host draw a rejectable prompt at that fetch.

Fix. New UserConfirmationReview::ProductSubtree(ProductSubtreeReview { product_id }), appended last so existing SCALE indices are untouched. On the own-account cold path the core calls confirm_user_action first: approve resolves over SSO, reject returns HostAccountGetError::Rejected. A cached subtree resolves locally with no prompt, and cross-product access keeps its existing account_access_authorization gate. Codegen and Swift bindings regenerated.

Bound remote authority calls at their deadline

Reason. remote_authority_call awaited the parked call after cancelling it. A call parked in the statement-store setup ignores the cancel token, so the await outlived the deadline and the request hung instead of failing.

Intent. A deadline that actually bounds the call, while still tearing the call down cleanly.

Fix. On cancel or timeout, race the call's unwind against a short grace, then drop it. In the normal case the call observes the cancellation and unsubscribes its statement streams within the grace; a call parked in the setup region is dropped at the grace rather than hanging.

Worker dispose ordering

Reason. disposeCore called free() in the same turn as dispose(). dispose() aborts the in-flight receiveFrame, but wasm-bindgen releases the borrow a microtask later, so free() threw "attempted to take ownership of Rust value while it was borrowed" and leaked the core on any mid-dispatch dispose.

Intent. Free without the borrow throw.

Fix. Track in-flight receiveFrame promises per core and await them after dispose(), before free().

johnthecat and others added 3 commits August 21, 2026 12:35
…t consent

A product requesting its own account takes no access review, so a cold
subtree that must be fetched from the Account Holder over SSO was resolved
with no host-visible seam. Add a ProductSubtree review so a host can show a
rejectable prompt at that moment.

UserConfirmationReview gains a ProductSubtree(ProductSubtreeReview { product_id })
variant, appended last to preserve the SCALE indices of the existing variants.
On the own-account get path, when subtree_resolution_reaches_account_holder is
true (pairing host, cold cache), the core calls confirm_user_action first:
approve proceeds to the SSO resolve, reject returns HostAccountGetError::Rejected.
A cached slot resolves locally with no prompt, and cross-product access keeps its
existing account_access_authorization gate, so neither prompts here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y65XQWkbMjfxG43gKvVV1b
disposeCore called free() in the same turn as dispose(). dispose() aborts
in-flight receiveFrame dispatch, but wasm-bindgen releases the core's borrow
only after the aborted promise settles a microtask later. Calling free()
immediately threw "attempted to take ownership of Rust value while it was
borrowed" and left the core unfreed whenever a provider was disposed
mid-dispatch, which a host does on transient re-renders.

Track outstanding receiveFrame promises per core, and after dispose() await
them before free(). The whole-runtime dispose path awaits every core
teardown before freeing the pairing runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y65XQWkbMjfxG43gKvVV1b
The timeout and cancel arms of remote_authority_call awaited the parked call
after cancelling it. A call parked in the statement-store setup, which does not
watch the cancel token, ignored the cancel, so that await outlived the deadline
and the request hung instead of failing.

On cancel or timeout, race the call's unwind against a short grace, then drop
it. In the normal case the call observes the cancellation and unsubscribes its
statement streams within the grace; a call parked in the setup region is dropped
at the grace rather than hanging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y65XQWkbMjfxG43gKvVV1b
@johnthecat johnthecat changed the title Own-account subtree consent gate and worker dispose fix Own-account subtree consent gate, deadline bound, and worker dispose fix Aug 21, 2026
@johnthecat johnthecat self-assigned this Aug 21, 2026
@johnthecat
johnthecat requested a review from a team August 21, 2026 11:55
The workspace-wide UserConfirmationReview match in the host CLI's approval
summary was left non-exhaustive by the new ProductSubtree variant. Add its arm,
and apply rustfmt to the earlier commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y65XQWkbMjfxG43gKvVV1b
Comment thread js/packages/truapi-host/src/worker-runtime.ts
Comment thread js/packages/truapi-host/src/worker-runtime.ts Outdated
Comment thread rust/crates/truapi-server/src/runtime/authority.rs Outdated
@TarikGul

Copy link
Copy Markdown
Member

Looking good, just a few comments. Feel free to ping me when its out of a draft.

johnthecat and others added 2 commits August 21, 2026 14:44
…ering

The worker's dispose-must-await-in-flight-frames ordering shipped untested
because the worker entry binds `self` and exports nothing. Move dispatchFrame
and disposeAwaitingFrames into worker-core-registry so the ordering is a unit,
and test the case TarikGul asked for: a frame in flight, a dispose mid-flight,
no borrow throw, the core freed. A second test pins that free() throws while a
frame is borrowed, so the first genuinely exercises the ordering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y65XQWkbMjfxG43gKvVV1b
…gate

Two review points from the PR:

- The whole-runtime dispose nulled `runtime` inside the async block, leaving it
  non-null while cores disposed, so a message arriving mid-disposal could pass
  its `if (!runtime)` check. Capture the handle and null `runtime` synchronously,
  then free the capture after the cores finish.

- `subtree_resolution_reaches_account_holder` defaulted to `false` (no prompt),
  a fail-open default on a consent gate that a new authority could skip by
  omission. Make it a required trait method; the signing host implements it
  explicitly, deriving locally so it never prompts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y65XQWkbMjfxG43gKvVV1b
@johnthecat
johnthecat marked this pull request as ready for review August 21, 2026 13:18
@johnthecat
johnthecat enabled auto-merge August 21, 2026 15:02

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

🚀

@johnthecat
johnthecat added this pull request to the merge queue Aug 21, 2026
Merged via the queue into main with commit 229bb23 Aug 21, 2026
17 checks passed
@johnthecat
johnthecat deleted the feat/subtree-consent-and-dispose-fix branch August 21, 2026 20:27
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.

2 participants