Skip to content

fix(flows): agent input via input_context, not jq-in-prompt#4590

Merged
senamakel merged 5 commits into
tinyhumansai:mainfrom
graycyrus:feat/flows-agent-input-context
Jul 6, 2026
Merged

fix(flows): agent input via input_context, not jq-in-prompt#4590
senamakel merged 5 commits into
tinyhumansai:mainfrom
graycyrus:feat/flows-agent-input-context

Conversation

@graycyrus

@graycyrus graycyrus commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

The bug (root cause, not a symptom)

An agent node's only input channel was config.prompt, so the builder crammed upstream data in via a jq =-expression like "prompt": "=You are given an email: .item. …". That's invalid jq → silently resolves to null → the agent gets an EMPTY prompt and does nothing (proven live: classify + calendar agents both nulled their input). The agent node does not auto-receive the upstream item. Deeper cause: a =-expression binds one field to one value — forcing prose-with-embedded-data through it is a category error.

Fix

  • A — input_context: agents feed data via config.input_context ("=item", "=items", or "=nodes.<id>.item.json") — a clean binding that always resolves — while config.prompt is a plain instruction. OpenHumanLlm injects the resolved context as a system message before the prompt (completion + harness paths; pretty JSON, 50KB char-safe cap). Backward-compatible; no vendored engine change.
  • B — dry-run now fails on a null-resolved agent prompt (execution-breaking), alongside the tool_call-arg + node-error checks.
  • C — static gate rejects an agent prompt that reads as invalid jq, so propose/save can't persist the broken shape.
  • Builder prompt rewritten to the input_context convention + worked example; opus-sonnet-brief.json template converted.

Verification

cargo check/fmt clean · flows+tinyflows 296 pass · typecheck clean · templates.test 16 pass.

Follow-up: #(catalog output schemas) grounds the tool→node side. Together they close the agent data loop end-to-end.

Summary by CodeRabbit

  • New Features
    • Added support for passing upstream data into agent steps through input_context, including safer message construction and structured-output steering.
  • Bug Fixes
    • Prevented malformed agent prompt expressions from silently resolving to empty content.
    • Dry runs now flag invalid agent prompt bindings (with guidance to use input_context) and include these diagnostics in results.
    • Stricter validation now distinguishes plain instruction prompts from invalid expression-based prompts.
  • Tests
    • Added coverage for agent prompt validation and dry-run outcomes.
  • Chores
    • Updated CI to initialize the required vendor submodule before frontend checks.

…e agent-input loop)

Agents' only input channel was config.prompt, so the builder crammed upstream data
in via a jq =-expression like "prompt": "=You are given an email: .item. …". That's
invalid jq → silently resolves to null → the agent gets an EMPTY prompt and does
nothing. The agent node does not auto-receive the upstream item. Root cause: a =-expr
is for binding one field to one value, not prose-with-embedded-data — a category error.

A) input_context: agents feed data via config.input_context ("=item", "=items", or
   "=nodes.<id>.item.json") — a clean binding that always resolves — while config.prompt
   is a PLAIN instruction. OpenHumanLlm injects the resolved context as a system message
   BEFORE the prompt (both the completion and harness paths; pretty JSON, 50KB char-safe
   cap). Fully backward-compatible; no vendored engine change.
B) dry_run_workflow now FAILS on a null-resolved agent PROMPT (execution-breaking),
   alongside the existing tool_call-arg null + node-error checks.
C) validate_binding_resolvability rejects an agent prompt that reads as invalid jq
   (prose juxtaposing barewords) — so propose/save can't persist the broken shape.
Plus: prompt.md agent-node section rewritten to the input_context convention + worked
example; opus-sonnet-brief.json template converted (planner/drafter).

cargo check/fmt clean; flows+tinyflows 296 pass; typecheck clean; templates.test 16 pass.
@graycyrus graycyrus requested a review from a team July 6, 2026 18:06
@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: 43 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: 696afe22-8941-446d-a24e-1c3e6bc412f4

📥 Commits

Reviewing files that changed from the base of the PR and between 63c150c and 6296bad.

📒 Files selected for processing (1)
  • .github/workflows/ci-lite.yml
📝 Walkthrough

Walkthrough

This PR routes agent data through config.input_context, keeps config.prompt as plain instructions, rejects prose-like jq prompts during validation and dry runs, and updates the workflow-builder guidance plus the brief template to use the new pattern.

Changes

Agent input_context and prompt validation

Layer / File(s) Summary
input_context handling in agent message/prompt construction
src/openhuman/tinyflows/caps.rs
Adds input_context_block, build_completion_messages, and build_harness_run_prompt to place prior-step data before structured-output instructions and prompts, with tests for ordering, truncation, and legacy cases.
Static validation rejecting prose-as-jq agent prompts
src/openhuman/flows/ops.rs, src/openhuman/flows/ops_tests.rs
Adds a heuristic in validate_binding_resolvability for agent prompt expressions that read as prose, emits errors suggesting input_context, and expands validation coverage for reject/accept cases.
Dry-run diagnostics for null agent prompts
src/openhuman/flows/builder_tools.rs, src/openhuman/flows/builder_tools_tests.rs
Tracks agent_node_ids, computes agent_prompt_nulls from prompt-null diagnostics, fails the dry run when present, includes the field in output JSON, and adds matching tests.
Workflow-builder docs and template updates
src/openhuman/flows/agents/workflow_builder/prompt.md, app/src/lib/flows/templates/opus-sonnet-brief.json
Updates agent-node guidance, examples, and the brief template to use input_context and plain prompt text instead of interpolated jq expressions.

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

Suggested labels: rust-core, bug, feature, agent

Suggested reviewers: M3gA-Mind

Poem

I hopped through prompts with twitchy nose,
And found the path where input flows.
No more prose inside the jq,
Just tidy briefs that make it through.
🐰✨

🚥 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 flow change: moving agent input to input_context instead of embedding jq in the prompt.
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.

@coderabbitai coderabbitai Bot added bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Jul 6, 2026

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

ℹ️ 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 thread src/openhuman/tinyflows/caps.rs Outdated
// structured-output steering message regardless of which is present.
let mut prelude: Vec<ChatMessage> = Vec::new();
if let Some(block) = input_context_block(request) {
prelude.push(ChatMessage::system(block));

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 Keep untrusted input_context out of system role

When input_context is bound from an external trigger or connector item (for example =item from an email/webhook), this line promotes that untrusted payload to a system message before the workflow prompt and JSON steering. A malicious payload can include instructions or even close the markdown fence produced by input_context_block, causing those instructions to be delivered with system-role authority rather than as data. Please send the serialized context as user/tool data or wrap/escape it with explicit untrusted-source delimiters instead.

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 3feba7e — the input_context prelude message is now pushed with ChatMessage::user(...) instead of ChatMessage::system(...) in build_completion_messages. Untrusted upstream data (email/webhook/prior node output) is now user-role, not system-role, so it can't masquerade as host instructions. The structured-output steering message right after it stays system-role since that instruction is ours, not upstream data. Added/updated unit tests in caps.rs to assert the role.

Comment on lines +436 to +439
let Some(prompt) = node.config.get("prompt").and_then(Value::as_str) else {
continue;
};
if !tinyflows::expr::is_expression(prompt) {

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 Skip prompt validation when messages supply the turn

This new gate scans every agent prompt even when the node also has a non-empty messages array. Both runtime paths can execute from messages when the resolved prompt is absent/null (build_completion_messages prefers messages, and node_request_to_prompt falls back to them), so a graph with real messages plus an unused legacy prompt expression like =You are ... is now rejected by propose/save even though it would not run with an empty prompt. Please either skip this check when messages is present or make validation match the runtime precedence.

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 63c150c — validate_binding_resolvability now checks node.config.get("messages") for a non-empty array before running the invalid-jq prompt check, and skips the check entirely when messages is populated. Both runtime paths (build_completion_messages and node_request_to_prompt in tinyflows/caps.rs) fall through to messages once prompt resolves to null, so a node with real messages never actually runs on a bad prompt — this gate no longer false-rejects that graph. An empty messages array still leaves the gate active (added a regression test for that too).

@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

🧹 Nitpick comments (1)
src/openhuman/tinyflows/caps.rs (1)

225-245: 🔒 Security & Privacy | 🔵 Trivial

Untrusted input_context embedded unescaped in a markdown code fence.

input_context typically carries upstream node data (e.g. email/webhook content) that can be attacker-influenced. It's serialized and wrapped in a ```json ... ``` fence without escaping/neutralizing any backtick sequences the payload itself might contain, which lets crafted upstream data break out of the fence and impersonate additional "system" text to the model. This doesn't grant any new privilege by itself, but it's worth being aware of as this becomes the primary "data channel" into agent prompts.

🤖 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/tinyflows/caps.rs` around lines 225 - 245, The
`input_context_block` helper is embedding attacker-controlled `input_context`
directly inside a markdown code fence, so the payload can break out with
backticks and inject prompt text. Update `input_context_block` to neutralize or
escape any backtick sequences in the serialized JSON before formatting the
fenced block, or switch to a representation that cannot terminate the fence.
Keep the existing truncation logic and the surrounding prompt text, but ensure
the rendered context stays inert when inserted into the model prompt.
🤖 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/ops.rs`:
- Around line 324-383: `agent_prompt_looks_like_invalid_jq` incorrectly toggles
string state on every double quote, so escaped quotes inside jq strings can
break the strip pass and produce false invalid-prompt detections. Update the
quote-stripping logic in `agent_prompt_looks_like_invalid_jq` to recognize
escaped quotes (for example by tracking backslashes or otherwise skipping `\"`)
so quoted content stays excluded from the bareword scan.

---

Nitpick comments:
In `@src/openhuman/tinyflows/caps.rs`:
- Around line 225-245: The `input_context_block` helper is embedding
attacker-controlled `input_context` directly inside a markdown code fence, so
the payload can break out with backticks and inject prompt text. Update
`input_context_block` to neutralize or escape any backtick sequences in the
serialized JSON before formatting the fenced block, or switch to a
representation that cannot terminate the fence. Keep the existing truncation
logic and the surrounding prompt text, but ensure the rendered context stays
inert when inserted into the model prompt.
🪄 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: 061f4d54-a7c6-4a0c-ae54-1dea84897546

📥 Commits

Reviewing files that changed from the base of the PR and between 0b71713 and c739331.

📒 Files selected for processing (7)
  • app/src/lib/flows/templates/opus-sonnet-brief.json
  • 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/tinyflows/caps.rs

Comment thread src/openhuman/flows/ops.rs
graycyrus added 3 commits July 7, 2026 00:41
The RPC-catalog drift-guard test (rpcMethods.test.ts) reads
vendor/tinychannels/src/controllers/schemas.rs at test time, but the
Frontend Checks job's checkout has no submodule init at all, causing an
ENOENT failure. Mirrors the fix already present in test-reusable.yml's
unit-tests job.
Two related hardening fixes to input_context_block/build_completion_messages:

- Prepend the input_context block as a user-role message instead of
  system-role. It carries untrusted upstream data (email/webhook/prior
  node output); giving it system-role authority let a crafted payload
  masquerade as host instructions (addresses @chatgpt-codex-connector
  on src/openhuman/tinyflows/caps.rs:303).
- Size the enclosing markdown code fence to be one backtick longer than
  the longest backtick run actually present in the serialized payload
  (minimum 3), so untrusted data containing a run of backticks can't
  prematurely close the fence and have trailing content read as prompt
  prose (addresses @coderabbitai on
  src/openhuman/tinyflows/caps.rs:225-245).

Adds unit tests for both: the role assertion in
build_completion_messages_injects_input_context_before_structured_steering,
and two new tests covering the fence-widening behavior.
…gate

Two fixes to validate_binding_resolvability's agent-prompt invalid-jq gate:

- agent_prompt_looks_like_invalid_jq's quote-stripping pass toggled
  in_str on every literal `"`, so an escaped quote (`\"`) inside a jq
  string desynced the toggle and could leak quoted prose into the
  bareword scan, false-rejecting a valid concatenation expression. Now
  consumes an escaped char (backslash + next char) without toggling
  in_str (addresses @coderabbitai on src/openhuman/flows/ops.rs:383).
- The gate scanned a node's `prompt` even when it also declared a
  non-empty `messages` array. Both runtime paths
  (build_completion_messages / node_request_to_prompt in
  tinyflows/caps.rs) fall through to messages once prompt resolves to
  null — exactly what this bad `=`-expression prompt does — so a node
  with real messages never actually runs on the null prompt. Skip the
  gate when messages supplies the turn (addresses
  @chatgpt-codex-connector on src/openhuman/flows/ops.rs:439).

Adds regression tests: an escaped-quote concatenation expression is
accepted, a prose prompt alongside populated messages is accepted, and
alongside an empty messages array is still rejected.
@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. feature Net-new user-facing capability or product behavior. labels Jul 6, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 6, 2026
actions/checkout scopes its own safe.directory config to a temporary HOME
for the duration of the checkout step only, so it doesn't carry over to
this container job's real HOME — the subsequent bare `git submodule
update --init` in a later step hit "detected dubious ownership in
repository". Add the workspace as a safe directory ourselves first.
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 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