Skip to content

feat(scheduled_action): support remote agents as a schedulable ActionKind - #5612

Closed
SergiorCode wants to merge 4 commits into
macro-inc:mainfrom
SergiorCode:feat/scheduled-action-remote-agent
Closed

feat(scheduled_action): support remote agents as a schedulable ActionKind#5612
SergiorCode wants to merge 4 commits into
macro-inc:mainfrom
SergiorCode:feat/scheduled-action-remote-agent

Conversation

@SergiorCode

@SergiorCode SergiorCode commented Aug 12, 2026

Copy link
Copy Markdown

Closes #5606.

Adds ActionKind::RemoteAgent so an agent running outside Macro can be scheduled like a native one. kind is plain TEXT and task is untyped JSONB, so there is no migration and no .sqlx change.

Execution mirrors the existing kind — run chat created up front, prompt stored as the user message, the endpoint's reply stored as the assistant message, same execution record and completion notification — which is what lets the Agents surface work for remote agents without a client change. RemoteAgentClient is a port; the reqwest adapter in outbound/remote_agent_http.rs owns the transport concerns (HTTPS only, no redirects, timeout, capped response body, and rejection of hosts resolving to private/loopback/link-local/metadata addresses). Those address rules are mirrored from crates/webhook/src/outbound/http_validator.rs, whose validator is pub(super) and so unreachable from this service; the copy also folds IPv4-mapped IPv6 back onto the IPv4 rules, so ::ffff:169.254.169.254 is blocked — the original doesn't do that today.

Request signing is not in this PR. That is question 1 on the issue and the only part that needs your decision about where the secret lives. The port is shaped so signing slots into the adapter without touching the executor. Retry is unchanged (fire-and-forget, same as the existing kind, question 2). The SSRF policy is reusable via RemoteAgentEndpointPolicy::AllowLocal but nothing configures it yet, pending your answer to question 3.

What I could not run

I don't have a Rust toolchain or a database on the machine I wrote this on, so nothing here has been compiled, linted or tested — no cargo fmt, no just clippy, no cargo test -p scheduled_action, and I have not seen it work in a local dev environment. Flagging that up front rather than implying otherwise. If you want the direction but not an unverified diff, say so and I'll set up the Nix shell and come back with it built and exercised, or close this and keep the discussion on the issue.

Two known gaps that follow from the same constraint:

  • Generated clients are not regenerated. apps/web/src/lib/service-clients/service-scheduled-action/openapi.json, the orval output, and packages/sdk/generated/scheduled-action/* still say ActionKind = 'Agent'. Those come from bun gen-api against a running service, which I can't do here.
  • No UI for the new kind. The web change is only the soundness fix the second kind forces: getAgentTask is now guarded by the discriminant instead of casting unconditionally, and the agent editor refuses to update an action of another kind rather than overwriting its task payload (which would drop a RemoteAgent's endpoint_url). Creating or editing a RemoteAgent action from the UI is deliberately out of scope.

One residual risk worth naming: the endpoint host is resolved for validation and then resolved again by the request, so a record that changes in between can still steer the request at a blocked address. crates/webhook's validator has the identical window, so I left both consistent rather than diverging — happy to close it in both if you'd rather.

Tests included are the ones that don't need a database: kind round-trips through the TEXT column, RemoteAgentTask deserialisation from stored JSON, and the endpoint/address rules including the IPv4-mapped case.

…Kind

Adds ActionKind::RemoteAgent so an agent running outside Macro can be
scheduled like a native one. Macro stays the scheduler, the system of record
and the UI; the remote endpoint is just another executor.

The kind column is plain TEXT and task is untyped JSONB, so no migration and
no .sqlx changes are needed. Execution mirrors the existing kind: the run chat
is created up front, the prompt is stored as the user message, the endpoint's
reply is stored as the assistant message, and the completion notification and
execution record are unchanged — which is what lets the Agents UI work for
remote agents without a client change.

RemoteAgentClient is a port in domain/ports.rs; the reqwest adapter lives in
outbound/remote_agent_http.rs and owns the transport concerns. The endpoint
URL is user-supplied, so it gets HTTPS-only, no redirects, a request timeout,
a capped response body, and rejection of hosts that resolve to private,
loopback, link-local or metadata addresses — mirrored from
crates/webhook/src/outbound/http_validator.rs, whose validator is pub(super)
and so not reachable from this service. That copy also fixes a gap in the
original: IPv4-mapped IPv6 addresses are folded back onto the IPv4 rules, so
::ffff:169.254.169.254 is blocked.

Request signing is deliberately not included — that is question 1 on the
issue, and it is the only part that needs a maintainer decision about where
the secret lives. The port is shaped so signing slots into the adapter without
touching the executor.

On the web side the getAgentTask cast is now guarded by the kind discriminant
instead of being unconditional, and the agent editor refuses to update an
action of another kind rather than overwriting its task payload.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 28fcbe1c-a261-47e6-a9fa-0b4e0540728e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added scheduled actions that run tasks through external HTTPS agents.
    • Remote-agent conversations preserve prompts, responses, labels, and completion notifications.
    • Added secure public HTTPS endpoint access, with optional local access for development.
    • Added support for JSON and plain-text agent responses.
  • Bug Fixes

    • Improved handling of unsupported scheduled action types and incomplete draft tasks.
    • Added safeguards against unsafe endpoints and oversized responses.
    • Empty agent responses are now ignored to prevent blank messages.

Walkthrough

The scheduled action service now supports RemoteAgent actions. It defines remote-agent task and run models, maps the action kind to storage and OpenAPI schemas, and provides an asynchronous client port. A reqwest adapter validates endpoints, limits responses, and parses JSON or plain text. The executor creates chats, stores messages, invokes the remote client, and sends completion notifications. Web automation utilities preserve non-Agent action kinds and tasks during schedule updates.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title uses conventional commits format and describes the change, but its stated length is 73 characters, exceeding the 72-character limit. Shorten the title by at least one character while keeping the conventional commits format and meaning.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the RemoteAgent feature, execution flow, security controls, exclusions, testing limits, and known gaps.
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.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…he DNS gap

Two follow-ups on the same change:

- The agent editor preserved the stored kind but still overwrote `task` with
  an Agent-shaped payload. Since an update is a full overwrite, editing a
  RemoteAgent action from that editor would have written `{model, prompt,
  user_prompt}` under `kind: "RemoteAgent"`, dropping `endpoint_url` and
  failing every later run with "invalid remote agent task definition" — with
  no way to restore it from the UI. Name, schedule and enabled still apply to
  every kind; only the task payload is now kind-gated.
- Document the DNS-rebinding window between validating the resolved address
  and the request resolving the host again. The webhook validator this mirrors
  has the identical gap, so both are left consistent rather than diverging.
@SergiorCode

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🧹 Nitpick comments (1)
services/scheduled_action/src/outbound/remote_agent_http.rs (1)

27-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Use rootcause for the new remote-agent error path.

The RemoteAgentClient trait and executor call chain currently return anyhow::Result. Update those signatures, add rootcause to services/scheduled_action/Cargo.toml, and replace the new anyhow conversions and context calls.

🤖 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 `@services/scheduled_action/src/outbound/remote_agent_http.rs` around lines 27
- 28, Replace anyhow with rootcause throughout the new remote-agent error path:
update the RemoteAgentClient trait and executor call-chain signatures in
services/scheduled_action/src/outbound/remote_agent_http.rs (lines 27-28) and
services/scheduled_action/src/outbound/inprocess_executor/remote_agent_task.rs
(line 11), replace newly added anyhow conversions and context calls, and add
rootcause to services/scheduled_action/Cargo.toml.

Source: Coding guidelines

🤖 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 `@services/scheduled_action/src/outbound/inprocess_executor/mod.rs`:
- Around line 84-89: Update the setup flow around create_run_chat in the
ActionKind match so any error from chat creation explicitly calls release_action
before returning the error. Preserve the existing successful chat_id path and
ensure both Agent and RemoteAgent claims are released when run-chat setup fails.

In `@services/scheduled_action/src/outbound/remote_agent_http.rs`:
- Around line 244-267: Update is_blocked_ip to reject the RFC6598 shared IPv4
range 100.64.0.0/10, while preserving the existing mapped-IPv6 handling and
other public-endpoint exclusions. Add a regression assertion in
services/scheduled_action/src/outbound/remote_agent_http/test.rs at lines 30-34
verifying that 100.64.0.1 is blocked.

---

Nitpick comments:
In `@services/scheduled_action/src/outbound/remote_agent_http.rs`:
- Around line 27-28: Replace anyhow with rootcause throughout the new
remote-agent error path: update the RemoteAgentClient trait and executor
call-chain signatures in
services/scheduled_action/src/outbound/remote_agent_http.rs (lines 27-28) and
services/scheduled_action/src/outbound/inprocess_executor/remote_agent_task.rs
(line 11), replace newly added anyhow conversions and context calls, and add
rootcause to services/scheduled_action/Cargo.toml.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 07322bcb-68ee-4321-a21c-35c7aaa6b1fd

📥 Commits

Reviewing files that changed from the base of the PR and between f15a6aa and 05b807b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (13)
  • apps/web/src/features/block-automation/component/automationUtils.ts
  • services/scheduled_action/Cargo.toml
  • services/scheduled_action/src/bins/service.rs
  • services/scheduled_action/src/domain/models.rs
  • services/scheduled_action/src/domain/ports.rs
  • services/scheduled_action/src/outbound.rs
  • services/scheduled_action/src/outbound/inprocess_executor/mod.rs
  • services/scheduled_action/src/outbound/inprocess_executor/remote_agent_task.rs
  • services/scheduled_action/src/outbound/pg_scheduled_action_repo.rs
  • services/scheduled_action/src/outbound/pg_scheduled_action_repo/test.rs
  • services/scheduled_action/src/outbound/remote_agent_http.rs
  • services/scheduled_action/src/outbound/remote_agent_http/test.rs
  • services/scheduled_action/src/swagger.rs

Comment thread services/scheduled_action/src/outbound/inprocess_executor/mod.rs
Comment thread services/scheduled_action/src/outbound/remote_agent_http.rs
…RFC 6598

Both from review:

- execute_action claims the action before creating the run chat, and only the
  spawned run releases it. If chat creation failed, the claim survived until
  MAX_ACTION_TIME (20 minutes) and the action could not be run again in the
  meantime. Release it on that path. Pre-existing, but this change touches the
  same lines and both kinds go through it.
- Block RFC 6598 shared address space (100.64.0.0/10) in the endpoint address
  rules; it is routable inside carrier-grade NAT and some cloud networks.
  Ipv4Addr::is_shared is still unstable, so the prefix is checked directly.

Not taken: converting the new error path to rootcause. This service is on
anyhow end to end, and the inbound error type downcasts anyhow::Error to map
AlreadyRunningError onto 409, so a single rootcause path here would either
break that mapping or leave two error idioms in one service. Worth doing as a
service-wide change rather than in a feature PR.
@SergiorCode

Copy link
Copy Markdown
Author

On the rootcause nitpick — not taken, and the reason is in 3a32610's message so it doesn't get lost in a thread:

services/scheduled_action is on anyhow end to end, and ScheduledActionApiError downcasts anyhow::Error to map AlreadyRunningError onto 409. Converting only the new remote-agent path would either break that mapping or leave two error idioms inside one service. It looks like a service-wide change to me rather than something to smuggle into a feature PR — happy to do it as a follow-up if you want it, or to include it here if you'd rather it land together.

The two actionable comments are fixed in the same commit; replies are on their threads.

@SergiorCode

Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@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

🤖 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
`@services/scheduled_action/src/outbound/inprocess_executor/remote_agent_task.rs`:
- Around line 48-53: Update the successful response handling in the remote task
executor so empty or whitespace-only response.output values are not returned
early; store every successful response with store_assistant_message and invoke
notify_completion, preserving the existing success flow.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a9bf752f-7f92-4428-b0ef-39e1b2281633

📥 Commits

Reviewing files that changed from the base of the PR and between f15a6aa and 3a32610.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (13)
  • apps/web/src/features/block-automation/component/automationUtils.ts
  • services/scheduled_action/Cargo.toml
  • services/scheduled_action/src/bins/service.rs
  • services/scheduled_action/src/domain/models.rs
  • services/scheduled_action/src/domain/ports.rs
  • services/scheduled_action/src/outbound.rs
  • services/scheduled_action/src/outbound/inprocess_executor/mod.rs
  • services/scheduled_action/src/outbound/inprocess_executor/remote_agent_task.rs
  • services/scheduled_action/src/outbound/pg_scheduled_action_repo.rs
  • services/scheduled_action/src/outbound/pg_scheduled_action_repo/test.rs
  • services/scheduled_action/src/outbound/remote_agent_http.rs
  • services/scheduled_action/src/outbound/remote_agent_http/test.rs
  • services/scheduled_action/src/swagger.rs

From review: an empty or whitespace-only `output` returned before storing the
assistant message and notifying, while the executor still recorded the run as
successful — a run marked green with an empty chat and no explanation.

Treated as a failure rather than stored, which is the alternative the review
offered. A 200 with nothing in it is a misconfigured endpoint, and failing puts
the reason on the execution record where the UI already surfaces it. Storing an
empty assistant message and firing a notification with empty summary text would
also diverge from the Agent kind, which skips both when the model produced no
text (agent_task.rs: store_conversation returns early on empty parts, and
notify_completion is gated on non-empty text).
@synoet

synoet commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

@SergiorCode We are already working on a big set of feature to support this. And while I really appreciate you putting up a pr, it would just conflict with the implementation we are already working on. I'm going to close this.

@synoet synoet closed this Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(scheduled_action): support remote/self-hosted agents as a schedulable ActionKind

2 participants