Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 149 additions & 77 deletions Cargo.lock

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,24 @@ tinyhivemind-tools = { path = "crates/tinyhivemind-tools" }
# host writes the same four lines against its own. `tinytools` is pinned at
# the commit OpenHuman's own tree vendors, which is what makes the unification
# exact; `.github/scripts/assert-openhuman-pin.sh` checks all three revs.
openhuman-embed = { git = "https://github.com/tinyhumansai/openhuman", rev = "74d2a79612b9d142042381f7fb026002d7f9f82f", default-features = false }
openhuman = { git = "https://github.com/tinyhumansai/openhuman", rev = "74d2a79612b9d142042381f7fb026002d7f9f82f", default-features = false }
tinytools = { git = "https://github.com/tinyhumansai/tinytools", rev = "3a01641f8ebd8a55656cd212f9a90638d958a447" }
tinytools-agent = { git = "https://github.com/tinyhumansai/tinytools", rev = "3a01641f8ebd8a55656cd212f9a90638d958a447" }
openhuman-embed = { git = "https://github.com/tinyhumansai/openhuman", rev = "5f8b7c235f18b46f90fabbf1e7f1b8d9a957cee6", default-features = false }
# `channels` because OpenHuman's agent bus calls `channels::run_channel_turn`
# unconditionally, so its tree does not build without that feature.
openhuman = { git = "https://github.com/tinyhumansai/openhuman", rev = "5f8b7c235f18b46f90fabbf1e7f1b8d9a957cee6", default-features = false, features = ["channels"] }
tinytools = { git = "https://github.com/tinyhumansai/tinytools", rev = "75e42ba9647eb801891a12ad05b6337f6a1c126b" }
tinytools-agent = { git = "https://github.com/tinyhumansai/tinytools", rev = "75e42ba9647eb801891a12ad05b6337f6a1c126b" }
# The memory engine contract and the per-agent lifecycle OpenHuman runs its
# seats' memory through, for the adapter's `Recall`/`Remember` over the same
# engine and layout. Git sources pinned at the commit OpenHuman's tree vendors
# and patched onto that copy below, for the same one-copy reason as the tool
# crates; `.github/scripts/assert-openhuman-pin.sh` checks the rev.
tinymemory-api = { git = "https://github.com/tinyhumansai/tinymemory", rev = "6018a91a773555efb50bf1959a6621292618e04a" }
tinymemory-tools = { git = "https://github.com/tinyhumansai/tinymemory", rev = "6018a91a773555efb50bf1959a6621292618e04a" }
tinymemory-api = { git = "https://github.com/tinyhumansai/tinymemory", rev = "8b919fd6d0b99e06269cab84de0c715772d0c49f" }
tinymemory-tools = { git = "https://github.com/tinyhumansai/tinymemory", rev = "8b919fd6d0b99e06269cab84de0c715772d0c49f" }
# OpenHuman's cron schedule validator, which moved out of `openhuman::cron`
# into the tinyflows scheduler crate. Git source pinned at the commit
# OpenHuman's tree vendors and patched onto that copy below, so its
# `Schedule` is the same type `openhuman::cron` re-exports.
tinyflows-schedule = { git = "https://github.com/tinyhumansai/tinyflows", rev = "99fd391d43a9af943535cda5579f1e2246bafbdc" }
tinyflows-schedule = { git = "https://github.com/tinyhumansai/tinyflows", rev = "26df306117c70b73a580e65ac99764a568824e69" }
# Derive macros for the crate-wide error type in `crates/tinyhivemind-core/src/error/`.
# Every dependency entry should carry a comment like this one saying why it is
# here.
Expand Down
2 changes: 1 addition & 1 deletion crates/tinyhivemind-openhuman/src/deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ inherit an unrelated active hive's permissions. Tool rules requiring unavailable
metadata refuse the call. `EffectClassifier::subject` and `rule_context` supply
trusted application metadata; authored arguments cannot replace actor, session,
hive, episode or scheduled-job identity. Unknown effects refuse execution.
Declared `delegate_<profile>` tools coordinate internally; child calls retain
Declared subagents, spawned through `spawn_subagent`, coordinate internally; child calls retain
parent and child restrictions.

Native tool denials abort a turn and remain failed/interrupted coordinator
Expand Down
7 changes: 4 additions & 3 deletions crates/tinyhivemind-openhuman/src/deploy/permission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,10 @@ impl Permissions {
{
return deny("episode action requires authenticated continuing session");
}
let effect = if children
.iter()
.any(|id| context.tool_name == format!("delegate_{id}"))
let effect = if context.tool_name == "spawn_subagent"
&& context.arguments["agent_id"]
.as_str()
.is_some_and(|target| children.iter().any(|id| id == target))
{
Effect::ReadOnly
} else {
Expand Down
24 changes: 17 additions & 7 deletions crates/tinyhivemind-openhuman/src/deploy/test/delegation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::sync::{
Arc, PoisonError,
atomic::{AtomicUsize, Ordering},
};
use std::time::Duration;
use wiremock::{Request, Respond, ResponseTemplate};

#[derive(Clone)]
Expand Down Expand Up @@ -39,16 +40,19 @@ impl Respond for DelegateScript {
.any(|call| call["function"]["name"] == "hivemind_complete")
})
});
if episode.is_none() {
// Every request without an episode is the native child's.
self.0.fetch_add(1, Ordering::Relaxed);
}
let call = if completed {
None
} else if last["role"] == "user" {
if episode.is_some() {
Some((
"delegate_helper",
json!({"prompt":"Attempt the restricted write", "blocking":true}),
"spawn_subagent",
json!({"agent_id":"helper","prompt":"Attempt the restricted write"}),
))
} else {
self.0.fetch_add(1, Ordering::Relaxed);
Some(("write", json!({})))
}
} else {
Expand Down Expand Up @@ -99,10 +103,16 @@ fn native_delegate_cannot_execute_a_child_denied_effect() -> anyhow::Result<()>
.send_as_host(message("delegate", "a"))
.await?;
let _report = deployment.host().coordinator().run_until_idle().await?;
assert!(
child_requests.load(Ordering::Relaxed) > 0,
"native child never ran"
);
// `spawn_subagent` returns at once, so the child runs after the parent's
// turn; its second request is sent only once the `write` call was
// either executed or denied.
tokio::time::timeout(Duration::from_secs(60), async {
while child_requests.load(Ordering::Relaxed) < 2 {
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await
.map_err(|_| anyhow::anyhow!("native child never ran to its follow-up request"))?;
assert_eq!(count.load(Ordering::Relaxed), 0);
Ok(())
})
Expand Down
9 changes: 7 additions & 2 deletions crates/tinyhivemind-openhuman/src/deploy/test/permission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,20 @@ async fn declared_delegates_are_internal_but_child_rules_still_narrow_effects()
});
config.profiles[0].subagents.push("helper".into());
let engine = permissions(config, &BuildOptions::default());
let spawn = |agent_id: &str| {
let mut spawn = context("spawn_subagent");
spawn.arguments = serde_json::json!({"agent_id": agent_id, "prompt": "task"});
spawn
};
assert!(
!(engine
.decide("alice", Some(&scope("a")), &context("delegate_helper"))
.decide("alice", Some(&scope("a")), &spawn("helper"))
.await)
.is_deny()
);
assert!(
(engine
.decide("alice", Some(&scope("a")), &context("delegate_unknown"))
.decide("alice", Some(&scope("a")), &spawn("unknown"))
.await)
.is_deny()
);
Expand Down
2 changes: 1 addition & 1 deletion vendor/openhuman
Submodule openhuman updated 1317 files
2 changes: 1 addition & 1 deletion wiki
Submodule wiki updated from bb63ec to 92deec
Loading