Skip to content

feat(flows): ground the builder in the LIVE Composio catalog + enforce tool contracts#4605

Merged
senamakel merged 4 commits into
tinyhumansai:mainfrom
graycyrus:feat/flows-live-tool-contracts
Jul 6, 2026
Merged

feat(flows): ground the builder in the LIVE Composio catalog + enforce tool contracts#4605
senamakel merged 4 commits into
tinyhumansai:mainfrom
graycyrus:feat/flows-live-tool-contracts

Conversation

@graycyrus

@graycyrus graycyrus commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

The root cause (one problem behind every symptom)

The builder authored graphs against guessed tool contracts — hallucinated slugs (SLACK_POST_MESSAGE_TO_CHANNEL → runtime 404), guessed output paths (split_out.path="items" vs Gmail's real data.messages), guessed args/fields — because search_tool_catalog only searched a static curated subset and nothing validated what the builder wrote. Proven live: a classify-emails flow nulled its whole chain (split found no items) and 404'd on a fake Slack slug.

The systemic fix: one ToolContract, from the LIVE catalog, grounded + enforced

Groundingsearch_tool_catalog + new get_tool_contract pull each named app's full real actions from Composio (list_tool_schemas_v3/backend list), returning required args + input/output schema + computed primary_array_path (the nested array path, e.g. data.messages). Unified LIVE_CATALOG_CACHE (replaces the two per-field caches). Curated list → ranking hint only, never a boundary.

Enforcementvalidate_tool_contracts at propose/revise/save: hard-reject an unreal slug (kills the 404) or a missing required arg; warn on bad output-field bindings + split paths (suggests json.<primary_array_path>). Runtime gate (flow_tool_allowed) now checks the live catalog for slug existence + preserves scope gating (strictly narrower, never loosened).

So the builder can use every real action of any app you name — and can no longer hallucinate a slug, arg, or output field.

Reconciliation

Subsumes #4591 (its response-fields cache folds into LIVE_CATALOG_CACHE); orthogonal to #4590.

Follow-up

Vendored split_out dotted-path (json.data.messages) + its validation land next (submodule change) — that's the last piece of the nested-path bug; this PR already grounds + suggests the correct path.

Verification

cargo check/fmt clean · flows/tinyflows/composio/tools: 1281 tests pass (incl. new tests for compute_primary_array_path, live catalog fetch/search, get_tool_contract, validate_tool_contracts, and the tightened runtime gate).

Summary by CodeRabbit

  • New Features

    • Added a tool-contract lookup experience so workflow building can retrieve full live details for a tool action after catalog search.
    • Expanded workflow guidance to ground tool usage in live catalog data and connect tool inputs/outputs more reliably.
  • Bug Fixes

    • Improved validation so workflows now catch invalid tool slugs and missing required inputs earlier.
    • Added clearer warnings for mismatched output fields and split paths, helping fix wiring issues before saving.

…e tool contracts

The builder authored graphs against GUESSED tool contracts — hallucinated slugs
(SLACK_POST_MESSAGE_TO_CHANNEL → runtime 404), guessed output paths (split_out
path=items vs Gmail's real data.messages), guessed args/fields — because
search_tool_catalog only searched a STATIC curated subset and nothing validated
what the builder wrote. Proven live: a classify-emails flow nulled its whole chain
(split found no items) and 404'd on a fake Slack slug.

Systemic fix — one ToolContract sourced from the FULL live Composio catalog per
named app, grounded AND enforced:

Grounding (PR1):
- caps.rs: ToolContract {slug, required_args, input/output_schema, output_fields,
  primary_array_path, is_curated} + unified LIVE_CATALOG_CACHE. fetch_live_toolkit_
  catalog() pulls a toolkit's REAL actions via direct_list_tools / backend list_tools
  (bypassing the curated filter), degrading gracefully when output_parameters is
  absent. compute_primary_array_path() derives the nested array path (data.messages).
  composio_required_args/response_fields become thin projections; the two old caches
  are removed.
- builder_tools.rs: search_tool_catalog now searches the LIVE catalog (curated-first
  ranking, never hides uncurated matches) and returns required_args/output_fields/
  primary_array_path/featured; new get_tool_contract tool returns the full contract.
  Registered in the builder tool belt + prompt.md teaches: discover live, wire from
  required_args/output_fields, split via json.<primary_array_path>.

Enforcement (PR2):
- ops.rs validate_tool_contracts: HARD-reject an unreal slug (kills the 404) or a
  missing required arg; advisory warnings for bad output-field bindings + split paths
  (suggests json.<primary_array_path>). Wired into propose/revise/save.
- caps.rs flow_tool_allowed: Path B now requires live-connection AND the slug existing
  in the toolkit's live catalog, then applies classify_unknown scope gating — strictly
  narrower, never loosened.

Subsumes tinyhumansai#4591 (its response-fields cache folds into LIVE_CATALOG_CACHE); orthogonal
to tinyhumansai#4590. cargo check/fmt clean; flows/tinyflows/composio/tools 1281 tests pass.
@graycyrus graycyrus requested a review from a team July 6, 2026 21:13
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 56e84452-ea67-4e0b-8aee-30cf75bac5df

📥 Commits

Reviewing files that changed from the base of the PR and between 8d00b5a and 2671c20.

📒 Files selected for processing (5)
  • src/openhuman/flows/agents/workflow_builder/prompt.md
  • src/openhuman/flows/builder_tools_tests.rs
  • src/openhuman/flows/ops.rs
  • src/openhuman/flows/ops_tests.rs
  • src/openhuman/tinyflows/tests.rs
📝 Walkthrough

Walkthrough

Introduces a live Composio tool-catalog cache (ToolContract, LIVE_CATALOG_CACHE) replacing curated schema caches, tightens uncatalogued-toolkit allowlisting against the live catalog, adds a hard validate_tool_contracts gate enforced in propose/revise/save workflow tools, adds search_live_catalog and GetToolContractTool, and updates prompts, agent config, and tests accordingly.

Changes

Live Tool-Contract Validation Feature

Layer / File(s) Summary
Live catalog cache and ToolContract model
src/openhuman/tinyflows/caps.rs, src/openhuman/composio/client.rs
Adds ToolContract, LIVE_CATALOG_CACHE, fetch_live_toolkit_catalog, compute_primary_array_path, reworks composio_required_args/composio_response_fields as live-catalog projections, and widens direct_list_tools visibility to pub(crate).
Path B allowlist tightening
src/openhuman/tinyflows/caps.rs, src/openhuman/tinyflows/tests.rs
flow_tool_allowed now takes &Config, requires the slug to exist in the live catalog for uncatalogued toolkits (fail-closed), with updated call sites and tests seeding the live catalog cache.
Hard validation gate and wiring warnings
src/openhuman/flows/ops.rs, src/openhuman/flows/ops_tests.rs
Adds validate_tool_contracts rejecting tool_call nodes with unreal slugs or missing required args, plus advisory graph_wiring_warnings passes for output-field and split_out path mismatches, with tests.
Enforcement in workflow tools
src/openhuman/flows/tools.rs, src/openhuman/flows/builder_tools.rs
propose_workflow, revise_workflow, and save_workflow now call validate_tool_contracts and return errors when contract validation fails.
New grounding tools
src/openhuman/flows/builder_tools.rs, src/openhuman/tools/ops.rs
search_tool_catalog switches to search_live_catalog against the full live catalog; a new GetToolContractTool returns the full live contract for a slug; both registered in the tool registry.
Builder tool tests
src/openhuman/flows/builder_tools_tests.rs
Tests updated to seed the live catalog cache, validate search_live_catalog/get_tool_contract behavior, and assert hard-rejection for missing required args in revise_workflow.
Prompt and agent config
src/openhuman/flows/agents/workflow_builder/agent.toml, src/openhuman/flows/agents/workflow_builder/prompt.md
Adds get_tool_contract to allowed tools and rewrites guidance to require live catalog slugs and get_tool_contract-driven wiring with hard-rejection wording.

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

Sequence Diagram(s)

sequenceDiagram
  participant Agent as Workflow Builder Agent
  participant SearchTool as search_tool_catalog
  participant ContractTool as get_tool_contract
  participant LiveCache as LIVE_CATALOG_CACHE
  participant SaveTool as save_workflow

  Agent->>SearchTool: query for candidate slugs
  SearchTool->>LiveCache: fetch_live_toolkit_catalog(toolkit)
  LiveCache-->>SearchTool: ToolContract matches
  SearchTool-->>Agent: slug, required_args, output_fields

  Agent->>ContractTool: get_tool_contract{slug}
  ContractTool->>LiveCache: fetch_live_toolkit_catalog(toolkit)
  LiveCache-->>ContractTool: full ToolContract
  ContractTool-->>Agent: required_args, output_fields, primary_array_path

  Agent->>SaveTool: save_workflow(graph)
  SaveTool->>LiveCache: validate_tool_contracts(config, graph)
  LiveCache-->>SaveTool: contract errors (if any)
  alt errors found
    SaveTool-->>Agent: reject with node/slug/missing-arg details
  else no errors
    SaveTool-->>Agent: workflow persisted
  end
Loading

Possibly related PRs

  • tinyhumansai/openhuman#4433: Both PRs modify src/openhuman/flows/ops.rs, extending the same CRUD/run logic with new live tool-contract validation and wiring warnings.
  • tinyhumansai/openhuman#4472: Both PRs modify the propose_workflow execute path in src/openhuman/flows/tools.rs, adding contract validation on top of existing tool plumbing.
  • tinyhumansai/openhuman#4586: Both PRs add validation gates to the propose/revise/save workflow-builder pipeline, sharing the same validation execution path.

Suggested labels: rust-core, bug, feature

Suggested reviewers: M3gA-Mind, sanil-23

Poem

A rabbit dug through catalogs deep,
No more hallucinated slugs to keep.
Live contracts checked, args all wired right,
Before the workflow saves tonight. 🐇
Hop, hop — validated, tight! ✨

🚥 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 accurately summarizes the main changes: live Composio catalog grounding and stricter tool-contract enforcement.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

@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: 8d00b5a171

ℹ️ 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 +766 to +781
let Some(contract) = catalog.iter().find(|c| c.slug.eq_ignore_ascii_case(slug)) else {
tracing::warn!(
target: "flows",
node = %node.id,
%slug,
%toolkit,
"[flows] tool-contract check: slug is not a real action in the live catalog — rejecting"
);
errors.push(format!(
"Node '{}': `{slug}` is not a real action in the `{toolkit}` toolkit's live \
Composio catalog — use search_tool_catalog {{ query: ..., toolkit: \"{toolkit}\" \
}} to find a real action slug.",
node.id
));
continue;
};

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 Align contract validation with the runtime allowlist

When a live contract is found here, validate_tool_contracts lets the graph pass regardless of contract.is_curated, and search_tool_catalog now returns those uncurated live actions. For toolkits that already have a static catalog (e.g. Gmail/Slack), the runtime gate in flow_tool_allowed still takes Path A and rejects any slug not found by find_curated, so a real but uncurated action can be proposed/saved successfully and then fail every run as tool not permitted. Either keep these actions out of authoring/validation for statically cataloged toolkits or update the runtime gate to allow the same live-contract set.

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 14e289e — confirmed valid by reading flow_tool_allowed's Path A (src/openhuman/tinyflows/caps.rs): for a toolkit with a static curated catalog (gmail/slack/notion/...), the runtime dispatch gate hard-rejects any slug find_curated doesn't recognize, even a real live action. validate_tool_contracts only checked "is this a real live-catalog action", never contract.is_curated, so a real-but-uncurated action on a curated toolkit could pass authoring/save and then fail every run.

Took your first suggested direction (tighten authoring to match the runtime gate, rather than loosen the runtime gate — the runtime gate is the security-relevant one here). validate_tool_contracts now rejects a tool_call whose toolkit has a static curated catalog (via the same get_provider(...).curated_tools().or_else(catalog_for_toolkit(...)) lookup flow_tool_allowed uses) unless the matched contract is curated. search_tool_catalog/get_tool_contract are unchanged — they still surface uncurated real actions for discovery, per ToolContract::is_curated's doc (ranking signal only) — only the hard save-time gate now matches runtime enforcement.

Added a regression test (validate_tool_contracts_rejects_a_real_but_uncurated_action_on_a_statically_catalogued_toolkit in ops_tests.rs) and updated the existing seeded_slack_send_contract() fixture to is_curated: true (it was arbitrarily false before, which no longer reflects a real curated Slack action now that the gate checks it).

@coderabbitai coderabbitai Bot added bug feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Jul 6, 2026

@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: 3

🧹 Nitpick comments (1)
src/openhuman/flows/ops.rs (1)

236-300: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Consider memoizing the per-toolkit catalog across the wiring passes.

fetch_live_toolkit_catalog returns an owned Vec<ToolContract>, so each call clones the whole toolkit catalog out of the cache. Here it's invoked once per matching expression, and the same pattern repeats per-node in validate_tool_contracts and per-edge in graph_split_out_path_warnings. For a graph with several tool_call nodes/bindings in the same toolkit, graph_wiring_warnings now performs many full-catalog clones per authoring call. A small HashMap<toolkit, Vec<ToolContract>> memo (or fetching once per distinct toolkit up front) would collapse this to one clone per toolkit per pass.

Advisory only — graphs are usually small, so this is a nice-to-have rather than a hot path.

🤖 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/flows/ops.rs` around lines 236 - 300, The wiring checks in
graph_output_field_warnings, validate_tool_contracts, and
graph_split_out_path_warnings are repeatedly calling fetch_live_toolkit_catalog
for the same toolkit, causing unnecessary full catalog clones. Add a small
per-pass memoization layer keyed by toolkit (for example a HashMap from toolkit
to Vec<ToolContract>, or prefetch distinct toolkits before iterating) and reuse
the cached catalog within each pass so repeated tool_call bindings/edges only
trigger one clone per toolkit.
🤖 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/flows/agents/workflow_builder/prompt.md`:
- Around line 197-229: Clarify the workflow guidance so `get_tool_contract` is
described as a grounding step for a known slug, not a discovery/search
primitive. In the prompt text around `search_tool_catalog` and
`get_tool_contract`, separate the responsibilities clearly: use
`search_tool_catalog` to find candidate slugs, then use `get_tool_contract` to
resolve that slug’s toolkit and fetch the full contract for wiring required
args, output fields, and primary_array_path.

In `@src/openhuman/flows/builder_tools_tests.rs`:
- Around line 205-216: The live-catalog seed in this test is using the shared
gmail cache key, which can be overwritten by other parallel tests and make the
ranking assertion flaky. Update the setup in
search_live_catalog_ranks_curated_before_uncurated_without_hiding_either to seed
a unique toolkit key via seed_live_catalog_cache and query search_live_catalog
with that same unique key so the test is isolated from other #[tokio::test]
cases.

In `@src/openhuman/tinyflows/tests.rs`:
- Around line 426-433: The shared LIVE_CATALOG_CACHE seed key is reused by
multiple tests, which can cause flaky preflight assertions under parallel
execution. Update preflight_fails_before_dispatch_naming_the_missing_field and
preflight_invoker_gates_the_mock_tool_path to use distinct toolkit/slug values
in their seed_live_catalog_cache setup, or explicitly clear the cache between
tests so each test gets an isolated schema. Use the existing
seed_live_catalog_cache and seeded_required_args_contract calls as the places to
adjust.

---

Nitpick comments:
In `@src/openhuman/flows/ops.rs`:
- Around line 236-300: The wiring checks in graph_output_field_warnings,
validate_tool_contracts, and graph_split_out_path_warnings are repeatedly
calling fetch_live_toolkit_catalog for the same toolkit, causing unnecessary
full catalog clones. Add a small per-pass memoization layer keyed by toolkit
(for example a HashMap from toolkit to Vec<ToolContract>, or prefetch distinct
toolkits before iterating) and reuse the cached catalog within each pass so
repeated tool_call bindings/edges only trigger one clone per toolkit.
🪄 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: 0fee9db1-482c-4a9d-9350-a1a5a9ba4e7b

📥 Commits

Reviewing files that changed from the base of the PR and between 9b84f96 and 8d00b5a.

📒 Files selected for processing (11)
  • src/openhuman/composio/client.rs
  • src/openhuman/flows/agents/workflow_builder/agent.toml
  • src/openhuman/flows/agents/workflow_builder/prompt.md
  • src/openhuman/flows/builder_tools.rs
  • src/openhuman/flows/builder_tools_tests.rs
  • src/openhuman/flows/ops.rs
  • src/openhuman/flows/ops_tests.rs
  • src/openhuman/flows/tools.rs
  • src/openhuman/tinyflows/caps.rs
  • src/openhuman/tinyflows/tests.rs
  • src/openhuman/tools/ops.rs

Comment thread src/openhuman/flows/agents/workflow_builder/prompt.md
Comment thread src/openhuman/flows/builder_tools_tests.rs
Comment thread src/openhuman/tinyflows/tests.rs
graycyrus added 3 commits July 7, 2026 03:36
validate_tool_contracts let a graph pass authoring/save whenever a slug
was ANY real live-catalog action, regardless of contract.is_curated.
But flow_tool_allowed's Path A (the runtime dispatch gate) hard-rejects
any slug find_curated doesn't recognize for a toolkit that ships a
static curated catalog (gmail, slack, notion, ...). A real-but-uncurated
action on one of those toolkits could therefore pass validation and
save successfully, then fail every single run as "tool not permitted".

Mirror the runtime gate at authoring time instead of loosening the
runtime gate: reject a tool_call whose toolkit has a static curated
catalog unless the matched contract is curated. search_tool_catalog
still surfaces uncurated real actions (ranking signal only, per
ToolContract::is_curated's doc) — this only tightens the hard
save-time gate to match what would actually be allowed to run.

Addresses @chatgpt-codex-connector on src/openhuman/flows/ops.rs:781.
The "app not connected yet?" bullet described search_tool_catalog and
get_tool_contract as if both search the live catalog, blurring the
line between discovery (search by keyword) and grounding (resolve a
known slug's full contract). Split the two responsibilities out
explicitly so the builder agent doesn't treat get_tool_contract as a
second search primitive.

Addresses @coderabbitai on
src/openhuman/flows/agents/workflow_builder/prompt.md:229.
LIVE_CATALOG_CACHE is a process-global cache, shared by every
#[tokio::test] in the same binary. Several tests seeded the SAME
toolkit key ("gmail"/"slack") with DIFFERENT contract content
(different is_curated, different required_args, or a different item
count), so a concurrently-running test could overwrite another's
cache entry between its seed and its assertion:

- search_live_catalog_ranks_curated_before_uncurated_without_hiding_either
  needs an exact 2-item, exact-order result but shared the "gmail" key
  with ~11 other tests seeding a single curated item.
- search_tool_catalog_degrades_gracefully_when_output_schema_unknown
  seeded "slack" with is_curated: false, racing the curated-catalog
  gate now enforced in validate_tool_contracts (previous commit) against
  ops_tests.rs's "slack"-keyed tests.
- preflight_fails_before_dispatch_naming_the_missing_field and
  preflight_invoker_gates_the_mock_tool_path both seeded "gmail" with
  DIFFERENT required_args lists under the same slug.

Give each of these its own fictional toolkit/slug (verified none collide
with any other seed_live_catalog_cache key in the repo) instead of
reusing the shared "gmail"/"slack" keys. Verified fixed by stress-running
the affected modules at --test-threads=16 (previously reproduced the
failure within a handful of runs; now stable across 10 consecutive
runs).

Addresses @coderabbitai on src/openhuman/flows/builder_tools_tests.rs:216
and src/openhuman/tinyflows/tests.rs:433.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants