fix(flows): agent input via input_context, not jq-in-prompt#4590
Conversation
…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.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 43 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR routes agent data through ChangesAgent input_context and prompt validation
Estimated code review effort: 4 (Complex) | ~60 minutes Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
💡 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".
| // 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)); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| let Some(prompt) = node.config.get("prompt").and_then(Value::as_str) else { | ||
| continue; | ||
| }; | ||
| if !tinyflows::expr::is_expression(prompt) { |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/openhuman/tinyflows/caps.rs (1)
225-245: 🔒 Security & Privacy | 🔵 TrivialUntrusted
input_contextembedded unescaped in a markdown code fence.
input_contexttypically 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
📒 Files selected for processing (7)
app/src/lib/flows/templates/opus-sonnet-brief.jsonsrc/openhuman/flows/agents/workflow_builder/prompt.mdsrc/openhuman/flows/builder_tools.rssrc/openhuman/flows/builder_tools_tests.rssrc/openhuman/flows/ops.rssrc/openhuman/flows/ops_tests.rssrc/openhuman/tinyflows/caps.rs
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.
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.
The bug (root cause, not a symptom)
An
agentnode's only input channel wasconfig.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
input_context: agents feed data viaconfig.input_context("=item","=items", or"=nodes.<id>.item.json") — a clean binding that always resolves — whileconfig.promptis a plain instruction.OpenHumanLlminjects 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.input_contextconvention + worked example;opus-sonnet-brief.jsontemplate converted.Verification
cargo check/fmtclean · flows+tinyflows 296 pass ·typecheckclean ·templates.test16 pass.Follow-up: #(catalog output schemas) grounds the tool→node side. Together they close the agent data loop end-to-end.
Summary by CodeRabbit
input_context, including safer message construction and structured-output steering.promptexpressions from silently resolving to empty content.promptbindings (with guidance to useinput_context) and include these diagnostics in results.