diff --git a/app/src/components/flows/WorkflowPromptBar.test.tsx b/app/src/components/flows/WorkflowPromptBar.test.tsx index 67b367eb60..50b5a29fb3 100644 --- a/app/src/components/flows/WorkflowPromptBar.test.tsx +++ b/app/src/components/flows/WorkflowPromptBar.test.tsx @@ -35,15 +35,15 @@ describe('WorkflowPromptBar', () => { // The created flow is the standard blank graph (single manual trigger). expect(graph.nodes).toHaveLength(1); expect(graph.nodes[0].kind).toBe('trigger'); - // Prompt-authored flows default to NOT requiring approval, so a - // Run-button/scheduled run doesn't deadlock on an unsurfaceable approval. - expect(requireApproval).toBe(false); + // Parity with the proposal-card path: prompt-authored flows must default + // to requiring approval, matching WorkflowProposalCard's default. + expect(requireApproval).toBe(true); expect(navigateMock).toHaveBeenCalledWith('/flows/flow-1', { state: { copilotBuild: { description: 'digest my Slack every morning' } }, }); }); - it('defaults requireApproval to false so runs do not deadlock on an unsurfaceable approval', async () => { + it('defaults requireApproval to true so outbound side-effects need explicit approval', async () => { render(); fireEvent.change(screen.getByTestId('workflow-prompt-input'), { target: { value: 'auto-reply to every gmail thread' }, @@ -51,9 +51,9 @@ describe('WorkflowPromptBar', () => { fireEvent.click(screen.getByTestId('workflow-prompt-submit')); await waitFor(() => expect(createFlowMock).toHaveBeenCalledTimes(1)); - // Third positional arg must be `false` — prompt-authored flows default to - // not requiring approval so a Run-button/scheduled run can execute. - expect(createFlowMock.mock.calls[0][2]).toBe(false); + // Third positional arg must be `true` — flows_create's server default is + // `false`, so omitting it would silently disarm the approval gate. + expect(createFlowMock.mock.calls[0][2]).toBe(true); }); it('submits on Enter (Shift+Enter reserved for newlines)', async () => { diff --git a/app/src/components/flows/WorkflowPromptBar.tsx b/app/src/components/flows/WorkflowPromptBar.tsx index b9ae14b728..69d5ae849e 100644 --- a/app/src/components/flows/WorkflowPromptBar.tsx +++ b/app/src/components/flows/WorkflowPromptBar.tsx @@ -44,14 +44,13 @@ export default function WorkflowPromptBar({ variant = 'compact', autoFocus = fal const name = deriveWorkflowName(trimmed, t('flows.page.newWorkflow')); log('submit: creating flow name=%s', name); try { - // Prompt-authored flows default to NOT requiring approval — a - // Run-button/scheduled run has no chat context, so a parked approval - // card can't surface and the run would deadlock. The user can still - // turn approval on per-flow afterwards. + // Safe default: prompt-authored flows require approval so outbound + // Slack/Gmail/HTTP/code nodes cannot fire unattended. Omitting this + // arg would fall back to the server default of `false`. const flow = await createFlow( name, createBlankWorkflowGraph(name, t('flows.nodeKind.trigger')), - false + true ); log('submit: created id=%s — opening canvas with build seed', flow.id); navigate(`/flows/${flow.id}`, { state: { copilotBuild: { description: trimmed } } }); diff --git a/app/src/providers/ChatRuntimeProvider.tsx b/app/src/providers/ChatRuntimeProvider.tsx index 7b92591717..e93b985077 100644 --- a/app/src/providers/ChatRuntimeProvider.tsx +++ b/app/src/providers/ChatRuntimeProvider.tsx @@ -324,10 +324,10 @@ function parseWorkflowProposal(output: string): WorkflowProposal | null { return { name: obj.name, graph: obj.graph, - // require_approval defaults to `false` — a proposal only requires approval - // if the builder explicitly set it — so treat anything other than an - // explicit `true` as `false`, in lockstep with the server default. - requireApproval: obj.require_approval === true, + // The Rust tool defaults `require_approval` to `true` when the caller + // omits it, so treat anything other than an explicit `false` as `true` + // here too — keeps the client's fallback in lockstep with the server's. + requireApproval: obj.require_approval !== false, summary: { trigger: typeof summary.trigger === 'string' ? summary.trigger : '', steps }, }; } diff --git a/src/openhuman/flows/builder_tools.rs b/src/openhuman/flows/builder_tools.rs index 51e864e30a..108c8dd097 100644 --- a/src/openhuman/flows/builder_tools.rs +++ b/src/openhuman/flows/builder_tools.rs @@ -114,7 +114,7 @@ impl Tool for ReviseWorkflowTool { }, "require_approval": { "type": "boolean", - "description": "Force a human-approval gate on every outbound action once saved. Defaults to false; set true only when the user explicitly asks for an approval step." + "description": "Force a human-approval gate on every outbound action once saved. Defaults to true for agent-proposed flows." } }, "required": ["name", "graph"] @@ -146,7 +146,7 @@ impl Tool for ReviseWorkflowTool { let require_approval = args .get("require_approval") .and_then(Value::as_bool) - .unwrap_or(false); + .unwrap_or(true); tracing::debug!( target: "flows", diff --git a/src/openhuman/flows/builder_tools_tests.rs b/src/openhuman/flows/builder_tools_tests.rs index d3c1a5ac29..1be8b6d9ba 100644 --- a/src/openhuman/flows/builder_tools_tests.rs +++ b/src/openhuman/flows/builder_tools_tests.rs @@ -58,7 +58,7 @@ async fn revise_workflow_validates_and_returns_revision_proposal() { } #[tokio::test] -async fn revise_workflow_omitted_require_approval_defaults_false() { +async fn revise_workflow_omitted_require_approval_defaults_true() { let tmp = TempDir::new().unwrap(); let tool = ReviseWorkflowTool::new(test_config(&tmp)); @@ -69,7 +69,7 @@ async fn revise_workflow_omitted_require_approval_defaults_false() { assert!(!result.is_error, "{}", result.output()); let parsed: Value = serde_json::from_str(&result.output()).unwrap(); - assert_eq!(parsed["require_approval"], false); + assert_eq!(parsed["require_approval"], true); } #[tokio::test] diff --git a/src/openhuman/flows/tools.rs b/src/openhuman/flows/tools.rs index 147a310ef2..2272dc4336 100644 --- a/src/openhuman/flows/tools.rs +++ b/src/openhuman/flows/tools.rs @@ -120,7 +120,7 @@ impl Tool for ProposeWorkflowTool { }, "require_approval": { "type": "boolean", - "description": "Force a human-approval gate on every outbound tool/HTTP action this flow takes once saved. Defaults to false; set true only when the user explicitly asks for an approval step." + "description": "Force a human-approval gate on every outbound tool/HTTP action this flow takes once saved. Defaults to true for agent-proposed flows." } }, "required": ["name", "graph"] @@ -152,7 +152,7 @@ impl Tool for ProposeWorkflowTool { let require_approval = args .get("require_approval") .and_then(Value::as_bool) - .unwrap_or(false); + .unwrap_or(true); tracing::debug!( target: "flows", diff --git a/src/openhuman/flows/tools_tests.rs b/src/openhuman/flows/tools_tests.rs index f928708d0f..3bd893121a 100644 --- a/src/openhuman/flows/tools_tests.rs +++ b/src/openhuman/flows/tools_tests.rs @@ -112,7 +112,7 @@ async fn missing_graph_is_an_error() { } #[tokio::test] -async fn omitted_require_approval_defaults_false_in_result() { +async fn omitted_require_approval_defaults_true_in_result() { let tmp = TempDir::new().unwrap(); let tool = ProposeWorkflowTool::new(test_config(&tmp)); @@ -122,7 +122,7 @@ async fn omitted_require_approval_defaults_false_in_result() { .unwrap(); let parsed: Value = serde_json::from_str(&result.output()).unwrap(); - assert_eq!(parsed["require_approval"], false); + assert_eq!(parsed["require_approval"], true); } #[tokio::test]