Skip to content

fix(agents): run custom registry agents with their real tools (flows + chat + tasks)#5121

Merged
graycyrus merged 8 commits into
tinyhumansai:mainfrom
graycyrus:feat/flows-custom-agents-real-tools
Jul 22, 2026
Merged

fix(agents): run custom registry agents with their real tools (flows + chat + tasks)#5121
graycyrus merged 8 commits into
tinyhumansai:mainfrom
graycyrus:feat/flows-custom-agents-real-tools

Conversation

@graycyrus

@graycyrus graycyrus commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Cross-cutting fix (B38). The agent factory from_config_for_agent only read harness AgentDefinitions, never config.agent_registry.entries — so a user's CUSTOM agent degraded to persona-only in flows (tools silently ignored) and hard-errored in chat + task-dispatcher.

Fix (synthesize-on-lookup): add the reverse bridge definition_from_registry_entry (agent_registry/defaults.rs) + sync find_custom_in_config; on a harness-registry miss, synthesize an AgentDefinition from the custom entry and run it through the same build_session_agent_inner; flows try the harness path before the persona fallback. Fixes all three surfaces at once.

Security: flows through the identical SecurityPolicy/tool-filter/approval-gate path; tool_allowlistToolScope::Named; a custom agent can't grant tools the user doesn't have. (Local core-crate compile is infeasibly slow on this machine's external SSD — the implementing agent reached test-binary compilation, so the lib compiles; CI runs the full suite.)

Summary by CodeRabbit

  • New Features
    • Custom agents from the registry can now run through the full harness execution path, honoring their configured prompts, tool restrictions, subagents, and pinned model.
  • Bug Fixes
    • Improved custom-agent resolution and fallback behavior: enabled entries execute via harness, while disabled/unknown entries use persona-only fallback.
    • Enforces least-privilege tool visibility via ToolScope filtering, including correct wildcard (*) and empty allowlist handling.
  • Tests
    • Added B38 regression coverage for routing decisions and custom-agent tool/allowlist visibility.

…+ chat + tasks)

The agent factory required a harness AgentDefinition and never consulted
config.agent_registry.entries, so a user-created CUSTOM agent degraded to
persona-only in flows (RegistryFallback, tools ignored) and hard-errored in
chat/task-dispatcher ('agent definition not found'). Add the reverse bridge
definition_from_registry_entry + a sync find_custom_in_config helper, and a
fallback in from_config_for_agent(+_with_profile) that synthesizes an
AgentDefinition from a custom entry and runs it through the normal
build_session_agent_inner. Flows try the harness path before the fallback.
Same tool-filter/approval/security path — custom agents can't grant tools the
user lacks.
@graycyrus
graycyrus requested a review from a team July 22, 2026 10:25

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 267f60d6d6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +138 to +139
if allowlist.is_empty() || allowlist == ["*"] {
ToolScope::Wildcard

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep empty custom allowlists tool-less

When a custom agent is created or edited with no tools selected, the UI and schema describe that as an empty tool_allowlist while "*" means all tools; this branch converts that empty list into ToolScope::Wildcard, so the newly-synthesized custom agent is allowed to see every enabled tool instead of none. This affects any custom agent saved without selecting tools (including existing configs that omitted the field), and it bypasses the per-agent least-privilege setting the editor shows.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ed52640allowlist_to_tool_scope now only maps exactly ["*"] to Wildcard; an empty allowlist maps to Named(vec![]) (tool-less), matching how the forward direction (tools_to_allowlist) renders Named(vec![]) back to []. Added a regression test (definition_from_registry_entry_empty_allowlist_stays_tool_less).

Comment thread src/openhuman/agent_registry/ops.rs Outdated
Comment on lines +224 to +225
.find(|entry| entry.id == id && matches!(entry.source, AgentRegistrySource::Custom))
.cloned()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject disabled custom agents in the factory

This lookup returns custom entries regardless of enabled, and resolve_target_definition immediately synthesizes them into runnable definitions for chat/task/MCP factory paths. As a result, a disabled custom agent can still run if an existing profile or direct caller references its id; flows explicitly route disabled entries to the fallback path so they are rejected, but these direct factory paths now ignore the disabled flag.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b6bfeb7find_custom_in_config now filters on entry.enabled, so a disabled custom entry is treated as a miss (None) everywhere this lookup is used (chat/task-dispatcher factory paths and the flows routing), same as an unknown id — matching the enforcement route_custom_entry_lookup/run_via_registry_fallback already had on the flows side. Added find_custom_in_config_ignores_disabled_custom_entries regression test.

Comment thread src/openhuman/tinyflows/caps.rs Outdated
harness so the custom agent runs with its real tool belt instead of \
the persona-only completion fallback"
);
self.run_via_harness(agent_ref, request, conn).await

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve custom flow agent model pins

For an enabled custom agent_ref, this now switches from run_via_registry_fallback to the harness path, but run_via_harness resolves only the node's own model and passes None for the registry entry model, while the session builder does not apply the synthesized definition's model. Therefore a flow custom agent with model: "hint:reasoning" or a raw BYOK model and no per-node override now runs on the default chat model, whereas the old fallback inserted entry.model into the completion request.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 141dc8crun_via_harness now takes an entry_model: Option<&str> parameter threaded from the custom registry entry's own model at the CUSTOM-REGISTRY call site (None for a shipped/TOML harness definition, which has no such entry), and passes it into resolve_node_model as the fallback below the node's own override — restoring the precedence run_via_registry_fallback already gave entry.model.

@graycyrus
graycyrus marked this pull request as draft July 22, 2026 10:31
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Custom registry entries are looked up from config, synthesized into harness definitions with their tool and model settings, resolved by both agent builders, and routed through full harness execution when enabled. Unknown and disabled entries retain fallback behavior.

Changes

Custom registry execution

Layer / File(s) Summary
Registry lookup and definition synthesis
src/openhuman/agent/harness/definition.rs, src/openhuman/agent_registry/..., src/openhuman/agent/library/ops.rs
Adds DefinitionSource::CustomRegistry, config-only custom-entry lookup, reverse mapping into AgentDefinition, public re-exports, metadata handling, and round-trip tests.
Builder definition resolution
src/openhuman/agent/harness/session/builder/factory.rs, src/openhuman/agent/harness/session/builder/builder_tests.rs
Centralizes harness/custom definition resolution, preserves the orchestrator fallback, errors for unresolved ids, enforces custom tool allowlists, and adds builder coverage.
Runtime custom-agent routing
src/openhuman/tinyflows/caps.rs, src/openhuman/tinyflows/tests.rs
Routes enabled custom entries through harness execution, preserves their model pins, and retains fallback routing for disabled or unknown entries.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Runner
  participant RegistryLookup
  participant AgentBuilder
  participant Harness
  Runner->>RegistryLookup: find enabled custom entry by agent_ref
  RegistryLookup-->>Runner: custom entry and model
  Runner->>AgentBuilder: resolve target definition
  AgentBuilder->>RegistryLookup: synthesize AgentDefinition
  RegistryLookup-->>AgentBuilder: CustomRegistry definition
  AgentBuilder->>Harness: build and run with configured tools and model
Loading

Possibly related PRs

Suggested labels: agent, bug

Suggested reviewers: m3ga-mind

Poem

I’m a rabbit with tools in my pack,
Custom agents now take the full track.
With prompts, scopes, and models aligned,
Harness paths leave fallbacks behind.
Unknown hops still thump back! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: custom registry agents now run with their real tools across flows, chat, and tasks.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@graycyrus
graycyrus marked this pull request as ready for review July 22, 2026 11:30
@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. bug labels Jul 22, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 22, 2026
`builder_tests.rs`'s new
`from_config_for_agent_still_errors_for_a_genuinely_unknown_id` used
`.expect_err()` on `Result<Agent, _>`, but `Agent` has no `Debug` impl
(it holds `Box<dyn Tool>` / provider trait objects). This is a hard
compile error under `--lib` test compilation in both the default and
`--no-default-features` builds, and was the actual root cause of both
the "Rust Feature-Gate Smoke (gates off)" and "Rust Core Coverage"
CI failures on tinyhumansai#5121 (neither job got past compiling the test binary).

Switch to `.is_err()` + `.err().unwrap()`, which doesn't require Debug.
definition_from_registry_entry's allowlist_to_tool_scope collapsed an
empty tool_allowlist to ToolScope::Wildcard, granting a custom agent
saved with no tools selected every enabled tool instead of none. The
settings UI/schema mean "no tools" by an empty list and "all tools" by
["*"] specifically — tools_to_allowlist (the forward direction) already
renders Named(vec![]) back to [], never ["*"], so this collapsed the
two shapes asymmetrically and bypassed the least-privilege setting the
editor shows.

Fix: only exactly ["*"] maps to Wildcard; every other list (including
empty) maps to Named(allowlist), matching the forward direction.

Addresses @chatgpt-codex-connector P1 review comment on
src/openhuman/agent_registry/defaults.rs:139.
find_custom_in_config returned a matching Custom-source entry
regardless of its enabled flag, so a disabled custom agent referenced
directly by chat/task-dispatcher (e.g. via an existing profile) would
still be synthesized into a runnable AgentDefinition and executed —
bypassing the disabled flag that the flows path already enforces
explicitly via route_custom_entry_lookup / run_via_registry_fallback.

Fix: filter on entry.enabled in find_custom_in_config, so a disabled
custom entry is treated as a miss (None) everywhere this lookup is
used, same as an unknown id.

Addresses @chatgpt-codex-connector P2 review comment on
src/openhuman/agent_registry/ops.rs:225.
run_via_harness resolved the node model with resolve_node_model(&req,
None) unconditionally, so once a custom flow agent_ref was routed
through the harness (issue B38), its own entry.model (e.g.
"hint:reasoning" or a raw BYOK model id) was dropped in favor of the
default chat model whenever the flow node had no per-node model
override — a regression versus run_via_registry_fallback, which always
honored entry.model.

Fix: thread the custom entry's model through as run_via_harness's new
entry_model parameter (None for a shipped/TOML harness definition,
which has no such entry), so resolve_node_model's existing precedence
(node override > entry model > definition default) applies on both
paths.

Addresses @chatgpt-codex-connector P2 review comment on
src/openhuman/tinyflows/caps.rs:929.
@coderabbitai coderabbitai Bot added the rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. label Jul 22, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 22, 2026
@graycyrus

Copy link
Copy Markdown
Contributor Author

Pushed 4 commits addressing the review cycle:

Root cause of the 2 originally-red gates (Feature-Gate Smoke + Rust Core Coverage): both jobs were failing at the same compile error, not a real feature-gate bug or a coverage shortfall — builder_tests.rs's new from_config_for_agent_still_errors_for_a_genuinely_unknown_id called .expect_err() on Result<Agent, _>, but Agent has no Debug impl. Neither job got past compiling the test binary. Fixed by switching to .is_err() + .err().unwrap(). Rust Feature-Gate Smoke (gates off) is now green.

The 3 Codex review comments — all fixed and replied to inline:

  1. P1 (agent_registry/defaults.rs:139): empty tool_allowlist was collapsing to ToolScope::Wildcard (all tools) instead of Named(vec![]) (no tools) — bypassed least-privilege for a custom agent saved with no tools selected. Fixed + regression test added.
  2. P2 (agent_registry/ops.rs:225): find_custom_in_config didn't check enabled, so a disabled custom agent could still run via direct factory callers (chat/task-dispatcher). Now filters on entry.enabled. Regression test added.
  3. P2 (tinyflows/caps.rs:929): a custom flow agent's own model pin (e.g. hint:reasoning) was silently dropped once routed through the harness path, falling back to the default chat model. run_via_harness now threads the custom entry's model through as a fallback below the node's own override, matching what run_via_registry_fallback already did.

Unrelated blocker found while watching CI: Rust Quality (fmt, clippy) (and therefore Rust Core Coverage, which gates on it) is currently red on this branch, but it's not from this diff — it's the main compile break from crossed PRs #5081/#5114/#5115 (vendor submodule pointers rolled backwards + a finish_flow_run_row signature mismatch), already being repaired in #5128. Confirmed by reproducing locally: my local build (default features) compiles clean; the CI-only failure is in flows/ops.rs/tinyflows/caps.rs, files this PR never touches. Once #5128 merges and this branch picks up main again, Rust Core Coverage should run cleanly against the new tests added here.

All new/changed tests pass locally under both default and --no-default-features --features tokenjuice-treesitter builds; cargo fmt clean.

@coderabbitai coderabbitai Bot removed the rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. label Jul 22, 2026
…initialized

build_session_agent_inner's delegation-tool-and-visibility computation
matched on (target_def, AgentDefinitionRegistry::global()), with a
catch-all (_, None) arm for "registry not initialised" that discarded
target_def entirely, treating it as "no filter" (empty visible == no
tools restricted). That's correct when target_def is also None, but
wrong when target_def is Some(_) — in particular for a CustomRegistry
definition synthesized by resolve_target_definition /
definition_from_registry_entry, which is deliberately built straight
from config.agent_registry.entries WITHOUT ever consulting the harness
registry, precisely so a custom agent can build even before/without
AgentDefinitionRegistry::init_global. Hitting that arm silently dropped
the custom agent's ToolScope::Named allowlist, leaving
visible_tool_names_for_test() empty instead of the named tools —
regressing the least-privilege contract from the P1 fix earlier in
this review cycle.

Splits (_, None) into (Some(def), None) — apply def.tools (Named or
Wildcard) same as the (Some, Some) arm, just without delegation-tool
synthesis (which needs the registry to resolve subagents) — and
(None, None), the genuine no-definition/no-registry case.

Reproduced deterministically in isolation
(from_config_for_agent_synthesizes_custom_registry_entry_with_named_scope
run alone, with no prior test in the same binary having initialized
the global registry) — not global-state pollution making a good test
wrongly fail, but pollution from OTHER tests' registry-init calls
masking a real bug when this test happened to run after them. Verified
the fix does not regress the (Some, Some) or (None, Some) arms via the
full agent::harness::session::builder:: + agent_registry:: +
tinyflows:: suite (307 passed).

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/openhuman/agent/harness/session/builder/factory.rs (2)

164-164: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use the shared resolver for reflection-chunk sessions too.

from_config_for_agent_with_reflection_chunks still performs only a global harness-registry lookup on Lines 138-142. A config-only custom agent will therefore receive None and enter the legacy path, losing its custom prompt/tool scope and potentially exposing unrestricted tools. Route that constructor through resolve_target_definition(config, agent_id)? and add a regression test.

Suggested fix
-        let target_def: Option<crate::openhuman::agent::harness::definition::AgentDefinition> =
-            match AgentDefinitionRegistry::global() {
-                Some(reg) => reg.get(agent_id).cloned(),
-                None => None,
-            };
+        let target_def = resolve_target_definition(config, agent_id)?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/agent/harness/session/builder/factory.rs` at line 164, Update
from_config_for_agent_with_reflection_chunks to obtain the target definition
through the shared resolve_target_definition(config, agent_id)? resolver instead
of only the global harness registry lookup, preserving config-only custom
agents’ prompt and tool scope. Add a regression test covering a config-only
custom agent and verifying the reflection-chunk session uses its resolved
definition.

1186-1260: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add branch/error logs for the silent resolution paths in resolve_target_definition. The harness-registry hit, uninitialized-registry error, and unknown-id error still return without a grep-friendly message; the caller only logs successful builds, so these cases are hard to distinguish in logs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/agent/harness/session/builder/factory.rs` around lines 1186 -
1260, Update resolve_target_definition to add grep-friendly logs before
returning from the harness-registry hit, the uninitialized-registry error, and
the unknown-agent error paths. Include the agent_id and clearly identify whether
the definition was resolved from the harness registry or why resolution failed,
while preserving the existing return behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/agent/harness/session/builder/factory.rs`:
- Around line 851-854: Preserve the distinction between ToolScope::Named with no
names and ToolScope::Wildcard when constructing filter and visible_tool_names:
represent an empty named scope explicitly so the logic around lines 875-880
yields no visible tools, while only ToolScope::Wildcard yields unrestricted
access. Update or retain regression coverage verifying an empty tool_allowlist
does not expose registered tools.

---

Outside diff comments:
In `@src/openhuman/agent/harness/session/builder/factory.rs`:
- Line 164: Update from_config_for_agent_with_reflection_chunks to obtain the
target definition through the shared resolve_target_definition(config,
agent_id)? resolver instead of only the global harness registry lookup,
preserving config-only custom agents’ prompt and tool scope. Add a regression
test covering a config-only custom agent and verifying the reflection-chunk
session uses its resolved definition.
- Around line 1186-1260: Update resolve_target_definition to add grep-friendly
logs before returning from the harness-registry hit, the uninitialized-registry
error, and the unknown-agent error paths. Include the agent_id and clearly
identify whether the definition was resolved from the harness registry or why
resolution failed, while preserving the existing return behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d6c56539-4242-4e4b-8449-b7fea73ad58c

📥 Commits

Reviewing files that changed from the base of the PR and between fcc539e and 6743c61.

📒 Files selected for processing (2)
  • src/openhuman/agent/harness/session/builder/builder_tests.rs
  • src/openhuman/agent/harness/session/builder/factory.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/openhuman/agent/harness/session/builder/builder_tests.rs

Comment on lines +851 to +854
let filter: Option<std::collections::HashSet<String>> = match &def.tools {
ToolScope::Named(names) => Some(names.iter().cloned().collect()),
ToolScope::Wildcard => None,
};

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.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Preserve the distinction between an empty named scope and wildcard access.

ToolScope::Named(empty) becomes an empty HashSet, but Lines 875-880 treat an empty set as “no filter.” Thus an empty tool_allowlist exposes every registered tool, contradicting the stated contract that only ["*"] maps to ToolScope::Wildcard. Carry an explicit “named scope present” state through visible_tool_names (or otherwise represent empty named scopes distinctly), and retain regression coverage for empty allowlists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/agent/harness/session/builder/factory.rs` around lines 851 -
854, Preserve the distinction between ToolScope::Named with no names and
ToolScope::Wildcard when constructing filter and visible_tool_names: represent
an empty named scope explicitly so the logic around lines 875-880 yields no
visible tools, while only ToolScope::Wildcard yields unrestricted access. Update
or retain regression coverage verifying an empty tool_allowlist does not expose
registered tools.

@graycyrus
graycyrus merged commit c480e6d into tinyhumansai:main Jul 22, 2026
24 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in Team Openhuman Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. bug

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

1 participant