Repository navigation
fix(modules): load callback-bound modules from the verified release, not only a local file #8535
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
senamakel
merged 24 commits into
tinyhumansai:main
from
senamakel:callback-modules-verified-release
Nov 4, 2026
Merged
Changes from 20 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
12fa49b
chore(vendor): bump tiny* submodules to latest commits
senamakel da5dc43
chore(vendor): update tinyconnectors submodule
senamakel 9020222
chore(vendor): bump tinydocs submodule to 14f8240
senamakel cc52796
chore(vendor): bump tinyflows submodule
senamakel 5edaecf
chore(vendor): bump tinyhosts submodule
senamakel 7c1548d
chore(vendor): bump tinyhumans-sdk and tinyjuice submodules
senamakel 5b628db
test(modules): add release tests for ops callback
senamakel e9511e9
test(modules): wire up callback release tests and share storage probe…
senamakel 9e596d9
chore(vendor): bump tinymcp and tinymemory submodules
senamakel 5b5ad69
chore(vendor): bump tiny* submodules to latest commits
senamakel e9e1801
test(ops): verify retained callback binding on module reload
senamakel 62ee083
test(ops_callback_release): align probe object path with sdk derivation
senamakel 8e65efe
refactor(modules): extract shared callback load path into callback_load
senamakel ac8a980
fix(modules): confirm admitted host callback binding instead of re-pr…
senamakel 1288f97
test(modules): cover callback binding confirmation for release-served…
senamakel f91de13
docs(modules): document callback module load path in ops table
senamakel 33a360c
test(modules): cover callback load surviving a caller timeout
senamakel 01f4cf4
fix(modules): keep callback loads running when the caller stops waiting
senamakel bdc0dde
test(ops): start runtime before simulating an abandoned callback load
senamakel de764d2
docs(modules): document callback module load paths in the README
senamakel 4a4daeb
test(modules): cover detached callback load inheriting caller context
senamakel fa76601
fix(modules): keep callback loads alive when the caller gives up early
senamakel dab1823
refactor(modules): move callback load helpers into callback_load module
senamakel be853a3
chore(core): reorder imports and reformat callback binding
senamakel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| //! Loading a module that takes host callbacks, from whichever source serves it. | ||
| //! | ||
| //! TinyBus binds host callbacks to a module before its `init` runs, and only | ||
| //! on paths that keep the admitted identity: an explicit local file (a `path` | ||
| //! override, a source build, or a staged `OPENHUMAN_MODULE_PATH` release) and | ||
| //! the pinned release chain itself. The release chain tries the installer | ||
| //! bundle, then the release cache, then a download into the cache when | ||
| //! `modules.allow_download` is on, and every candidate passes the registry's | ||
| //! archive SHA-256 and, where the record pins one, its library SHA-256 before | ||
| //! a binding is offered to it. | ||
| //! | ||
| //! `Resolver::ensure_loaded_with_host_callback_factory` drives both, preparing | ||
| //! a fresh binding for each candidate so a refused candidate revokes only its | ||
| //! own. `Resolver::ensure_loaded_with_host_callbacks` takes one binding and | ||
| //! accepts only a local-file source, so a release-served module (every shipped | ||
| //! host without `OPENHUMAN_MODULE_PATH`) could neither be loaded nor | ||
| //! re-checked through it. Every callback-bound load goes through [`load`] and | ||
| //! every later check through [`confirm`] instead, each run through | ||
| //! [`detached`] so a caller that stops waiting cannot strand the binding's | ||
| //! host state. | ||
|
|
||
| use std::collections::HashMap; | ||
| use std::future::Future; | ||
| use std::sync::{Arc, Mutex, PoisonError}; | ||
|
|
||
| use tinybus::module::{HostCallbackBinding, Resolver}; | ||
|
|
||
| /// Run a callback load on the module runtime and wait for it. | ||
| /// | ||
| /// TinyBus finishes a resolution it has started even when every caller stops | ||
| /// waiting (`ensure_loaded_within` bounds only the wait). A callback load also | ||
| /// publishes the admitted binding's host state, a storage endpoint or a | ||
| /// consequence host, into a process-wide cache, and that publication must not | ||
| /// be cancelled with the caller either: the next caller would find the module | ||
| /// serving with a binding nobody holds the state for. Dropping the returned | ||
| /// future drops only the wait; `work` runs to completion. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// `work`'s error, the module bus failing to start, or the task panicking. | ||
| pub(in crate::modules) async fn detached<T, Fut>( | ||
| resolver: &Resolver, | ||
| work: Fut, | ||
| ) -> Result<T, String> | ||
| where | ||
| T: Send + 'static, | ||
| Fut: Future<Output = Result<T, String>> + Send + 'static, | ||
| { | ||
| let runtime = resolver | ||
| .runtime() | ||
| .await | ||
| .map_err(|error| error.to_string())?; | ||
| runtime | ||
| .spawn(work) | ||
|
senamakel marked this conversation as resolved.
Resolved
|
||
| .await | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| .map_err(|error| format!("the module callback load did not finish: {error}"))? | ||
| } | ||
|
|
||
| /// One prepared candidate: its binding, the module configuration naming it, | ||
| /// and the host state that must live as long as the binding is retained. | ||
| pub(in crate::modules) type Prepared<S> = (Arc<HostCallbackBinding>, serde_json::Value, S); | ||
|
|
||
| /// Load `id` with host callbacks, preparing a fresh set with `prepare` for | ||
| /// each candidate artifact the resolver tries. | ||
| /// | ||
| /// Returns the binding the admitted module retains and the state prepared | ||
| /// with it; the state of refused candidates is dropped. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// The resolver's message when no candidate is admitted, `prepare`'s message | ||
| /// when a binding cannot be prepared, or a refusal when the module is already | ||
| /// serving with a binding this call did not prepare. | ||
| pub(in crate::modules) async fn load<S, F, Fut>( | ||
| resolver: &Resolver, | ||
| id: &str, | ||
| prepare: F, | ||
| ) -> Result<(Arc<HostCallbackBinding>, S), String> | ||
| where | ||
| S: Send + 'static, | ||
| F: Fn() -> Fut + Send + Sync + 'static, | ||
| Fut: Future<Output = Result<Prepared<S>, String>> + Send + 'static, | ||
| { | ||
| let candidates: Arc<Mutex<HashMap<String, S>>> = Arc::new(Mutex::new(HashMap::new())); | ||
| let factory_candidates = Arc::clone(&candidates); | ||
| let prepare = Arc::new(prepare); | ||
| let binding = resolver | ||
| .ensure_loaded_with_host_callback_factory(id, move |_attempt| { | ||
| let candidates = Arc::clone(&factory_candidates); | ||
| let prepare = Arc::clone(&prepare); | ||
| async move { | ||
| let (binding, module_config, state) = prepare().await?; | ||
| candidates | ||
| .lock() | ||
| .unwrap_or_else(PoisonError::into_inner) | ||
| .insert(binding.unique_name().to_string(), state); | ||
| Ok((binding, module_config)) | ||
| } | ||
| }) | ||
| .await?; | ||
| let state = candidates | ||
| .lock() | ||
| .unwrap_or_else(PoisonError::into_inner) | ||
| .remove(binding.unique_name().as_str()) | ||
| .ok_or_else(|| { | ||
| format!("module '{id}' is serving with a callback binding this load did not prepare") | ||
| })?; | ||
|
senamakel marked this conversation as resolved.
Resolved
|
||
| Ok((binding, state)) | ||
| } | ||
|
|
||
| /// Confirm `id` is still admitted with `expected`, preparing nothing. | ||
| /// | ||
| /// For a module an earlier [`load`] admitted: the resolver answers from the | ||
| /// retained binding without touching any source, so this works whether the | ||
| /// module came from a local file or from the release chain. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// The resolver's cached failure, or a refusal when the module is not serving | ||
| /// with `expected`. | ||
| pub(in crate::modules) async fn confirm( | ||
| resolver: &Resolver, | ||
| id: &str, | ||
| expected: &HostCallbackBinding, | ||
| ) -> Result<(), String> { | ||
| let module_id = id.to_owned(); | ||
| let retained = resolver | ||
| .ensure_loaded_with_host_callback_factory(id, move |_attempt| { | ||
| let id = module_id.clone(); | ||
| async move { | ||
| Err(format!( | ||
| "module '{id}' is not serving with its admitted callback binding" | ||
| )) | ||
| } | ||
| }) | ||
| .await?; | ||
| if retained.unique_name() != expected.unique_name() { | ||
| return Err(format!( | ||
| "module '{id}' is serving with a different callback binding" | ||
| )); | ||
| } | ||
| Ok(()) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.