Skip to content

fix(flows): gate flows_create so saving a scheduled/side-effecting flow can't silently arm it - #4835

Merged
graycyrus merged 2 commits into
tinyhumansai:mainfrom
graycyrus:fix/flows-save-enable-safety
Jul 13, 2026
Merged

graycyrus merged 2 commits into
tinyhumansai:mainfrom
graycyrus:fix/flows-save-enable-safety

Conversation

@graycyrus

@graycyrus graycyrus commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • flows_create used to hand back a live, unattended automation the moment a user hit Save: store::create_flow hardcoded enabled: true, and require_approval defaulted to false on most creation paths.
  • Rule 1: a graph whose trigger fires without a human in the loop (schedule / app_event / webhook) now persists disabled on create. The user arms it explicitly via the existing flows_set_enabled toggle. A manual trigger (or no trigger_kind at all) still persists enabled — it only ever runs via explicit flows_run, so there's no surprise.
  • Rule 2: a graph containing any outbound side-effect node (tool_call / http_request / code) now forces require_approval: true on create, regardless of what the caller passed. Read-only graphs are unaffected.
  • SaveWorkflowTool (the copilot's save path, which updates an existing flow rather than creating one — Rule 1 doesn't cover it) now emits a loud warning when it saves a graph onto a flow that is already enabled with an automatic trigger, so the agent relays the live-arming risk to the user.
  • store::create_flow now takes an explicit enabled: bool parameter instead of hardcoding true; all call-sites updated.

Problem

A scheduled flow that posts to Slack (or any other outbound action) could go from draft to armed and acting the instant the user clicked Save, with no confirmation step and no forced approval gate — because nothing in flows_create inspected the graph's trigger kind or its outbound nodes before deciding enabled / require_approval. Every creation path (prompt bar, scratch/template modal, "save & enable" proposal card, copilot save_workflow) was exposed identically.

Solution

Two server-side rules are enforced inside ops::flows_create itself — authoritative regardless of which frontend path called it, so no caller can accidentally bypass them:

  1. trigger_is_automatic(&graph) — new helper, schedule / app_event / webhook trigger kinds count as automatic (deliberately broader than the existing trigger_kind_fires, since webhook isn't wired to auto-dispatch yet but will be, and must not be handed out pre-armed).
  2. graph_has_outbound_side_effect(&graph) — new helper, true when the graph contains a tool_call / http_request / code node.

flows_create computes enabled = !trigger_is_automatic(&graph) and forces require_approval true when graph_has_outbound_side_effect is true, logging both decisions loudly in the RPC outcome's logs (and via tracing::info!) so a caller that got overridden can see why.

store::create_flow no longer decides enabled itself — it takes it as a parameter, decided by flows_create's two rules.

flows_set_enabled, flows_update, flows_duplicate, and the copilot's propose_workflow / revise_workflow tools are all unchanged — this only tightens the initial-creation default.

Existing-test impact

Three pre-existing tests directly asserted the old, unsafe behavior — that flows_create bound a schedule flow's cron job immediately because creation defaulted to enabled: true:

  • flows_create_binds_schedule_cron_job_for_an_enabled_flow
  • flows_delete_unbinds_schedule_cron_job
  • flows_update_rebinds_schedule_cron_job_when_trigger_schedule_changes
  • flows_update_does_not_rebind_when_graph_is_not_supplied

Updated all four to call flows_set_enabled(..., true) explicitly after flows_create (the real caller-facing arming path) before exercising cron-binding behavior — same coverage, now against the corrected contract. No other existing test depended on schedule/app_event/webhook-trigger flows or outbound-node graphs coming back enabled/require_approval-unforced from flows_create; verified by auditing every flows_create and store::create_flow call-site in the crate.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy — 9 new ops_tests.rs cases (Rule 1, Rule 2, and the exact ticket scenario combining both) + 7 inline helper tests + 4 existing schedule/cron tests updated to the new contract.
  • Diff coverage ≥ 80% — Rust-only change; cargo test --lib openhuman::flows:: -- --test-threads=1 passes 287/287. pnpm test:coverage not applicable (no frontend lines changed).
  • N/A: docs/TEST-COVERAGE-MATRIX.md has no Workflows/flows section yet (no existing feature-ID rows to update); this is a bug fix to existing internal behavior, not a new/removed/renamed feature leaf.
  • N/A: all affected feature IDs listed under ## Related — no matrix rows exist for this area (see above).
  • No new external network dependencies introduced (mock backend used per Testing Strategy) — pure Rust domain logic change, no new deps.
  • N/A: manual smoke checklist — this hardens an internal safety invariant (create-time enabled/require_approval), no release-cut UI surface changed.
  • N/A: no linked GitHub issue number was provided for B29; this addresses the "save/enable safety" ticket described in the task brief.

Impact

  • Runtime/platform impact: desktop core only (src/openhuman/flows/), no UI changes, no RPC contract changes (same flows_create params; response now carries extra informational logs entries when a rule fires).
  • Security/safety: closes the "silent live-arm on save" gap — a scheduled or outbound-acting flow can no longer become live/unattended without an explicit user action.
  • No migration: existing persisted flows are untouched: this only changes brand-new flows_create calls.

Related

  • Closes: B29 (save/enable safety)
  • Follow-up PR(s)/TODOs: frontend UX polish noted in the design doc — useCreateFlow.ts could pass requireApproval: true consistently for defense-in-depth (low priority, server now enforces it regardless), and a confirmation dialog when enabling an automatic-trigger flow for the first time.

AI Authored PR Metadata (required for Codex/Linear PRs)

Keep this section for AI-authored PRs. For human-only PRs, mark each field N/A.

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/flows-save-enable-safety
  • Commit SHA: c983829

Validation Run

  • pnpm --filter openhuman-app format:check — N/A (no frontend files changed); cargo fmt + pnpm format run (Rust formatting).
  • pnpm typecheck — N/A (no TypeScript files changed).
  • Focused tests: cargo test --manifest-path Cargo.toml --lib openhuman::flows:: -- --test-threads=1 → 287 passed, 0 failed.
  • Rust fmt/check (if changed): cargo fmt --manifest-path Cargo.toml clean; GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml passes (warnings only, pre-existing/unrelated).
  • Tauri fmt/check (if changed): GGML_NATIVE=OFF cargo check --manifest-path app/src-tauri/Cargo.toml passes (no flows-domain references in the Tauri shell; ran anyway for completeness).

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: flows_create now creates automatic-trigger flows disabled and forces require_approval on graphs with outbound side-effect nodes, instead of defaulting both to the unsafe "live and unattended" state.
  • User-visible effect: a newly created scheduled/app-event/webhook-trigger flow shows up disabled in the Workflows list and must be explicitly enabled via the existing toggle before it fires; a flow with any tool_call/http_request/code node always requires approval on its runs, even if the creating UI forgot to ask for it.

Summary by CodeRabbit

  • New Features

    • Added safety controls for newly created workflows with automatic triggers.
    • Automatic-trigger workflows are saved disabled until explicitly enabled.
    • Workflows with outbound actions now require approval automatically.
    • Added warnings when saving changes to enabled workflows with automatic triggers.
  • Bug Fixes

    • Prevented automatic triggers from being activated before explicit user approval.
    • Prevented outbound-effect workflows from bypassing approval requirements.

…ow can't silently arm it

Issue B29 (save/enable safety): saving a flow used to arm it live and
unattended by default. `store::create_flow` hardcoded `enabled: true`, and
`require_approval` defaulted to `false` on most creation paths — so a
scheduled flow that posts to Slack could go from draft to armed-and-acting
the moment the user clicked Save, with no confirmation.

`flows_create` now enforces two server-side rules, authoritative regardless
of what the caller passed, so no creation path (prompt bar, scratch/template
modal, proposal "save & enable", copilot save_workflow) can bypass them:

- Rule 1: a graph whose trigger fires without a human in the loop
  (schedule/app_event/webhook) persists disabled. The user arms it
  explicitly via the existing flows_set_enabled toggle. A manual trigger (or
  no trigger_kind discriminator) still persists enabled — it only ever runs
  via explicit flows_run, so there's no surprise.
- Rule 2: a graph containing any outbound side-effect node
  (tool_call/http_request/code) forces require_approval: true, overriding
  whatever the caller passed. Read-only graphs are unaffected.

store::create_flow now takes an explicit `enabled` parameter instead of
hardcoding true. SaveWorkflowTool (the copilot's save path, which updates an
existing flow rather than creating one, so Rule 1 doesn't apply there) now
emits a loud warning when it saves onto a flow that is already enabled with
an automatic trigger, so the agent relays the live-arming risk to the user.

Three pre-existing tests asserted the old (unsafe) behavior directly —
that flows_create bound a schedule flow's cron job immediately because it
defaulted to enabled. Updated them to enable explicitly via
flows_set_enabled first, matching the new contract.

Added 9 ops-level tests covering both rules (including the exact ticket
scenario: a scheduled Slack-posting flow saved with require_approval:false
must come back both disabled and require_approval:true) plus 7 inline tests
for the two new helpers.
@graycyrus
graycyrus requested a review from a team July 13, 2026 18:28
@coderabbitai

coderabbitai Bot commented Jul 13, 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: 15 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: f94ef43b-95c4-4bcf-8291-9543e99fd6b9

📥 Commits

Reviewing files that changed from the base of the PR and between c983829 and da760be.

📒 Files selected for processing (18)
  • app/src/components/chat/WorkflowProposalCard.test.tsx
  • app/src/components/chat/WorkflowProposalCard.tsx
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/services/api/flowsApi.ts
  • src/openhuman/flows/ops.rs
📝 Walkthrough

Walkthrough

Flow creation now disables automatically triggered flows, forces approval for outbound side effects, and binds dispatch triggers only when enabled. Workflow saving reports automatic-trigger warnings, with tests covering gating, binding, approval, and updated storage calls.

Changes

Flow creation and persistence

Layer / File(s) Summary
Creation-time gating and persistence
src/openhuman/flows/ops.rs, src/openhuman/flows/store.rs, src/openhuman/flows/store_tests.rs
Automatic triggers create disabled flows, outbound side effects force require_approval, and trigger binding occurs only for enabled flows. Storage callers now provide the explicit enablement argument.
Save warning and trigger description
src/openhuman/flows/builder_tools.rs, src/openhuman/flows/tools.rs
Saving an enabled flow with an automatic trigger appends a descriptive warning, reusing a crate-visible trigger-description helper.
Enablement and approval behavior tests
src/openhuman/flows/ops_tests.rs
Tests cover automatic-trigger gating, schedule binding after explicit enablement, approval forcing, and trigger classification predicates.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant flows_create
  participant FlowGraph
  participant FlowStore
  participant DispatchTrigger
  Client->>flows_create: create flow with graph and options
  flows_create->>FlowGraph: classify trigger and node side effects
  FlowGraph-->>flows_create: enabled and approval decisions
  flows_create->>FlowStore: persist flow settings
  flows_create->>DispatchTrigger: bind trigger when flow is enabled
  DispatchTrigger-->>Client: creation result and logs
Loading

Possibly related PRs

Suggested labels: bug, rust-core

Suggested reviewers: senamakel, codeghost21

Poem

I’m a rabbit guarding each flow,
From triggers that wake before you know.
Side effects earn approval’s light,
Cron waits till enabled right.
Warnings hop into the save reply—
Safe little workflows, carrots held high! 🐇

🚥 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 matches the main change: gating flows_create for automatic triggers and side-effecting graphs.
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.

@coderabbitai coderabbitai Bot added bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Jul 13, 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: c983829ce0

ℹ️ 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".


// Rule 1: automatic triggers create DISABLED — the user must arm them
// explicitly.
let enabled = !trigger_is_automatic(&graph);

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 Surface disabled-on-create to UI callers

When this branch is taken for scheduled/app_event/webhook graphs, flows_create now returns a disabled flow and only explains the change through RpcOutcome.logs. Existing UI callers discard those logs in createFlow (app/src/services/api/flowsApi.ts:270), and the proposal card's “Save & enable” path clears the card immediately after createFlow (app/src/components/chat/WorkflowProposalCard.tsx:84-85), so a user saving an automatic proposal gets no enable prompt and the flow never fires despite the CTA. Please make the create path surface this state or update the caller to explicitly notify/enable.

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 da760be (fix(flows): make \"Save & enable\" actually enable a B29-disabled auto-trigger flow).

Good catch — kept B29 Rule 1's server-side guarantee intact (flows_create still always persists an automatic-trigger graph disabled, so a copilot save_workflow autosave can never silently arm one) but fixed the client so the explicit-arming path actually arms:

WorkflowProposalCard.save now checks the returned Flow.enabled after createFlow, and when Rule 1 left it disabled, follows up with an explicit setFlowEnabled(id, true) call — the same flows_set_enabled toggle the Workflows list page uses. This is legitimate because the user just clicked "Save & enable" themselves; it is NOT the silent copilot auto-arm Rule 1 exists to prevent (that path — the agent's save_workflow tool — has no such follow-up and stays disabled, as intended).

If the follow-up enable call itself fails, the flow stays saved (disabled) and the card keeps the flow id around so a retry only retries the enable step rather than re-creating (which would have duplicated the flow). Surfaced via a new chat.flowProposal.enableError string (added to all locales) shown inline on the card.

Added vitest coverage for both the disabled-return-then-enable path and the enable-fails-then-retry path (app/src/components/chat/WorkflowProposalCard.test.tsx), and confirmed the Rust B29 Rule 1 tests are untouched and still green (cargo test --lib openhuman::flows::, 287 passed).

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 13, 2026
…trigger flow

B29 Rule 1 makes flows_create always persist an automatic-trigger graph
(schedule/app_event/webhook) disabled, regardless of caller intent, so a
copilot save_workflow autosave can't silently arm an unattended automation.
But WorkflowProposalCard's "Save & enable" button is the user's own explicit
arming click, not a silent autosave — and it discarded flows_create's logs
and cleared the card immediately, so the CTA's own label lied: the flow was
saved but never armed.

WorkflowProposalCard.save now checks the returned Flow.enabled and, when
Rule 1 left it disabled, follows up with an explicit setFlowEnabled(id, true)
call — the same flows_set_enabled toggle used everywhere else. If that
follow-up call itself fails, the flow stays saved (disabled) and the card
keeps the id around so a retry only retries the enable step, never
re-creates (which would duplicate the flow).

Addresses @chatgpt-codex-connector on src/openhuman/flows/ops.rs:1547 (PR tinyhumansai#4835).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

1 participant