Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6bf2963
fix(hooks): forward the resolved working directory to configured hooks
senamakel Oct 10, 2026
2b1b232
fix(embed): preserve control reasons across cancellation races
senamakel Oct 10, 2026
0d2d4f1
fix(embed): retain every agent permission callback
senamakel Oct 10, 2026
0b3c1df
docs(embed): refresh Linux measurements and generated API references
senamakel Oct 10, 2026
f1ac54c
Merge remote-tracking branch 'upstream/main' into medulla-12-scoped-h…
senamakel Oct 10, 2026
a03bc51
docs(test): map native-worker regression coverage
senamakel Oct 10, 2026
d734436
fix(embed): preserve completed dispatches and close removal admission
senamakel Oct 10, 2026
0145729
fix(approval): close instance registration before removal snapshot
senamakel Oct 10, 2026
eb6d3d2
docs(embed): make fleet commands and memory comparisons reproducible
senamakel Oct 10, 2026
7b3c275
fix(embed): serialize approval decisions with removal claims
senamakel Oct 10, 2026
86b5b0b
Merge remote-tracking branch 'upstream/main' into medulla-12-scoped-h…
senamakel Oct 10, 2026
f5ec62f
fix(process): await stopped descendants before cancellation acknowled…
senamakel Oct 10, 2026
3c9d383
fix(approval): guard shared gate decisions during instance removal
senamakel Oct 10, 2026
d4bd6c5
docs(embed): refresh compiled capabilities and removal coverage
senamakel Oct 10, 2026
dfddbad
fix: close agent admission before approval barriers and surface flows…
senamakel Oct 10, 2026
299d672
fix(ci): honor fleet example build configuration
senamakel Oct 10, 2026
3392da4
docs(embed): fix strict rustdoc links and clarify approval surfaces
senamakel Oct 10, 2026
49fc256
test(embed): exercise callback approvals through the scoped gate
senamakel Oct 10, 2026
22e2ab2
test(embed): cover flow approval surfaces and removed instances
senamakel Oct 10, 2026
c82fa02
test(embed): map callback and flow approval regressions
senamakel Oct 10, 2026
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
9 changes: 9 additions & 0 deletions crates/openhuman-core/src/agent/host_overrides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub use tinyagents_harness::cancel::CancellationToken;
pub struct HostOverrides {
/// Parent runtime adapters; local hook registration never edits this parent.
pub parent: Option<Arc<HostOverrides>>,
/// Approval registration barrier owned by this agent instance.
pub approval_scope: Option<Arc<crate::security::approval::ApprovalScope>>,
post_turn_hooks: RwLock<BTreeMap<String, Arc<dyn PostTurnHook>>>,
tool_hooks: RwLock<BTreeMap<String, Arc<dyn ToolHook>>>,
/// Native inference used by this agent instead of config-routed inference.
Expand All @@ -38,6 +40,13 @@ impl std::fmt::Debug for HostOverrides {
}

impl HostOverrides {
/// Resolve the instance barrier inherited by derived turn contexts.
pub fn approval_scope(&self) -> Option<Arc<crate::security::approval::ApprovalScope>> {
self.approval_scope
.clone()
.or_else(|| self.parent.as_ref().and_then(|p| p.approval_scope()))
}

/// Add or replace an agent-local post-turn hook by name; `None` removes it.
pub fn post_turn_hook(&self, name: &str, hook: Option<Arc<dyn PostTurnHook>>) {
let mut hooks = self
Expand Down
5 changes: 4 additions & 1 deletion crates/openhuman-core/src/hooks/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ fn identity_from_tool(context: &ToolHookContext) -> TurnIdentity {
TurnIdentity {
session_id: context.session_id.clone(),
agent_id: context.agent_id.clone(),
cwd: string_field(&context.arguments, "cwd"),
cwd: context
.cwd
.as_ref()
.map(|cwd| cwd.to_string_lossy().into_owned()),
..TurnIdentity::default()
}
}
Expand Down
15 changes: 15 additions & 0 deletions crates/openhuman-core/src/hooks/bridge_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,18 @@ fn timeout_text_classifies_as_a_timeout_failure() {
);
assert_eq!(classify_failure("something else"), "error");
}

#[test]
fn configured_hook_identity_uses_the_resolved_working_directory() {
let mut ctx = context("shell", serde_json::json!({"command": "pwd"}));
ctx.cwd = Some(std::path::PathBuf::from("/resolved/workspace"));
assert_eq!(
identity_from_tool(&ctx).cwd.as_deref(),
Some("/resolved/workspace")
);
ctx.arguments["cwd"] = serde_json::json!("relative/argument");
assert_eq!(
identity_from_tool(&ctx).cwd.as_deref(),
Some("/resolved/workspace")
);
}
22 changes: 22 additions & 0 deletions crates/openhuman-core/src/security/approval/flow_surface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Orders request registration and its approval surfaces after workspace resolution.

use super::ApprovalScope;

/// Resolve the workspace before registering and publishing any approval surface.
/// Removal during resolution refuses registration; accepted registration and
/// publication finish together before removal. No barrier is held across the await.
pub(super) async fn register_after_workspace<W, T>(
scope: Option<&ApprovalScope>,
workspace: impl std::future::Future<Output = W>,
register: impl FnOnce(W) -> T,
) -> Result<T, String> {
let workspace = workspace.await;
match scope {
Some(scope) => scope.with_open(|| register(workspace)),
None => Ok(register(workspace)),
}
}

#[cfg(test)]
#[path = "flow_surface_tests.rs"]
mod tests;
83 changes: 83 additions & 0 deletions crates/openhuman-core/src/security/approval/flow_surface_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use super::*;
use std::cell::Cell;
use std::future::Future;
use std::sync::Arc;
use std::task::{Context, Poll, Wake, Waker};

struct Noop;
impl Wake for Noop {
fn wake(self: Arc<Self>) {}
}

#[test]
fn a_closed_instance_registers_nothing_and_publishes_neither_flow_surface() {
let scope = ApprovalScope::default();
scope.close("agent_removed");
let publications = Cell::new(0);
let mut publication = std::pin::pin!(register_after_workspace(
Some(&scope),
std::future::ready(17),
|_| publications.set(publications.get() + 1),
));
let waker = Waker::from(Arc::new(Noop));
assert_eq!(
publication.as_mut().poll(&mut Context::from_waker(&waker)),
Poll::Ready(Err("agent_removed".to_owned()))
);
assert_eq!(publications.get(), 0);
}

#[test]
fn removal_during_workspace_lookup_suppresses_registration_and_all_surfaces() {
let scope = ApprovalScope::default();
let ready = Cell::new(false);
let workspace = std::future::poll_fn(|_| {
if ready.get() {
Poll::Ready(17)
} else {
Poll::Pending
}
});
let publications = Cell::new(0);
let mut publication = std::pin::pin!(register_after_workspace(Some(&scope), workspace, |_| {
publications.set(publications.get() + 1)
},));
let waker = Waker::from(Arc::new(Noop));
let mut cx = Context::from_waker(&waker);
assert_eq!(publication.as_mut().poll(&mut cx), Poll::Pending);
assert_eq!(
publications.get(),
0,
"registration preceded workspace resolution"
);
scope.close("agent_removed");
ready.set(true);
assert_eq!(
publication.as_mut().poll(&mut cx),
Poll::Ready(Err("agent_removed".to_owned()))
);
assert_eq!(publications.get(), 0);
}

#[test]
fn a_live_or_unscoped_flow_registers_once_with_its_resolved_workspace() {
let scope = ApprovalScope::default();
for scope in [Some(&scope), None] {
let publications = Cell::new(0);
let mut publication = std::pin::pin!(register_after_workspace(
scope,
std::future::ready(Some(("ws_test", 17))),
|workspace| {
assert_eq!(workspace, Some(("ws_test", 17)));
publications.set(publications.get() + 1);
42
},
));
let waker = Waker::from(Arc::new(Noop));
assert_eq!(
publication.as_mut().poll(&mut Context::from_waker(&waker)),
Poll::Ready(Ok(42))
);
assert_eq!(publications.get(), 1);
}
}
2 changes: 2 additions & 0 deletions crates/openhuman-core/src/security/approval/gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ pub(crate) struct RequestRoute {
pub(crate) tool_call_id: Option<String>,
pub(crate) forced: bool,
pub(crate) agent_id: Option<String>,
/// Instance barrier captured at registration, independent of decision context.
pub(crate) approval_scope: Option<Arc<super::ApprovalScope>>,
/// The [`thread_route_key`] the request was parked under, so a decision
/// made outside the parking task's scope clears the right route.
pub(crate) thread_key: Option<String>,
Expand Down
Loading
Loading