feat: add lifecycle_status enum and trigger_source for automation runs - #438
feat: add lifecycle_status enum and trigger_source for automation runs#438malhotra5 wants to merge 10 commits into
Conversation
Introduce a first-class lifecycle_status column (ACTIVE/INACTIVE/DRAFT) on the Automation model, deprecating the boolean enabled flag while keeping it for backwards compatibility. Add a trigger_source column to AutomationRun (manual/cron/event) so manual test dispatches are no longer blocked when an automation is inactive or in draft. Key changes: - models.py: AutomationState enum, lifecycle_status column on Automation, trigger_source on AutomationRun, new DB indexes - schemas.py: AutomationState StrEnum, DraftEndpoint type, helpers (automation_state_enabled, normalize_automation_state_enabled), lifecycle_status fields on create/update/response schemas - router.py: bridge helpers, set lifecycle_status on create/update/delete, remove enabled check on dispatch, set trigger_source='manual' on dispatch - preset_router.py: lifecycle_status on prompt/plugin preset requests - scheduler.py: filter ACTIVE lifecycle, set trigger_source='cron' - dispatcher.py: dispatch manual runs even for inactive automations - utils/run.py: skip_pending_runs excludes manual runs unless include_manual, create_pending_run accepts trigger_source - utils/webhook.py: filter ACTIVE lifecycle, set trigger_source='event' - utils/unhealthy.py: set lifecycle_status=INACTIVE on disable - git_sync: serialize/import lifecycle_status alongside enabled - migration 023: add columns and backfill from enabled flag Co-authored-by: openhands <openhands@all-hands.dev>
|
Warning Your comment is too long (maximum is 65536 characters), so the coverage report was not added. See the job log for how to reduce it. |
Mark the legacy `enabled` field as deprecated across all automation schemas and emit a runtime DeprecationWarning when it is explicitly set in request bodies. Callers should migrate to lifecycle_status. - CreateAutomationRequest.enabled, UpdateAutomationRequest.enabled, and AutomationResponse.enabled: Field(deprecated=True) so the OpenAPI schema marks them deprecated for API consumers - normalize_automation_state_enabled: emits a DeprecationWarning when 'enabled' is present in request data - models.py: updated comment to say 'Deprecated' explicitly Co-authored-by: openhands <openhands@all-hands.dev>
…payloads Add the draft lifecycle layer on top of the lifecycle_status foundation from PR #438: - draft_schemas.py: partial draft body models for all three creation endpoints (/v1, /v1/preset/prompt, /v1/preset/plugin), draft body normalization, and FINAL_DRAFT_MODELS registry - draft_router.py: full CRUD for server-backed drafts (POST/GET/PATCH/DELETE /v1/drafts), draft validation, materialization into DRAFT-state automations, and POST /v1/drafts/{id}/dispatch for test runs. Dispatch accepts an optional event_payload so event-triggered draft automations can be test-run with a user-supplied synthetic payload, bypassing webhook signature verification (caller is authenticated). - models.py: AutomationDraft model with source/materialized/last_test_run foreign keys - schemas.py: CreateAutomationDraftRequest, UpdateAutomationDraftRequest, AutomationDraftResponse, AutomationDraftListResponse, DraftDispatchRequest - capabilities_router.py: use draft_schemas registry instead of inline _DRAFT_MODELS; add automationDrafts to static features - app.py: register draft_router - utils/run.py: create_pending_run accepts optional event_payload - migration 024: automation_drafts table Stacked on PR #438 (lifecycle_status + trigger_source). Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
|
👋 This PR needs a couple of things fixed before OpenHands can review it:
Push an update once this is addressed and this check re-runs automatically. This is an automated check - no AI was used to generate this comment. |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.
Review: feat: add lifecycle_status enum and trigger_source for automation runs
🟡 Acceptable — Works well, a few design concerns worth addressing.
[Security/Design] Removed creator-only edit restriction
The PR removes the check in update_automation that prevented non-creators from editing automation definitions. The old comment was explicit: "Automations run under their creator's identity (git tokens, secrets, MCP servers), so only the creator may change what they do." Now any admin/owner with manage_automations can modify a teammate's automation tarball_path, entrypoint, or prompt — causing it to execute arbitrary code under that teammate's API key and secrets on the next run.
This may be intentional for the draft workflow (PR 2), but the PR description only says "relaxed in favor of draft workflow" without explaining why it is safe. The risk: an admin can now run code under another user's identity without that user's knowledge. If this is deliberate, consider documenting the rationale; if not, the restriction should be preserved for non-lifecycle fields.
Additionally, the _assert_can_manage docstring (lines 121-122) is now stale — it still claims "only the creator may change an automation's definition; everyone else may only turn it off" which is no longer enforced.
[Dead Code] DraftEndpoint literal is unused
DraftEndpoint = Literal["/v1", "/v1/preset/prompt", "/v1/preset/plugin"] in schemas.py is defined but never imported or referenced anywhere. Remove it or it will confuse readers and linters.
[DRY] Duplicated helper functions
_model_automation_state and _automation_state_enabled are copy-pasted identically into both router.py and preset_router.py. Extract to a shared module (e.g., schemas.py next to normalize_automation_state_enabled, or a utils/state.py).
What looks good
- Migration is clean and cross-database compatible (generic
sa.String, no PG-specific types). - The dispatcher query (
or_(manual, and_(enabled, ACTIVE))) correctly lets manual runs through while blocking non-manual runs for inactive/draft automations. skip_pending_runs_for_disabled_automationwithinclude_manualcorrectly preserves manual runs on disable while skipping them on delete.- Schema-level
normalize_automation_state_enabledvalidator keepsenabledandlifecycle_statusconsistent — good defensive design. - New tests cover the key scenarios (manual dispatch for disabled automation, draft exclusion from scheduler/webhook).
[RISK ASSESSMENT]
- Overall PR: 🟡 MEDIUM — The removed edit restriction is security-adjacent (an admin can now run code under another user's identity by editing their automation definition), but is limited to
manage_automations-level users who are already highly trusted. The core lifecycle/trigger_source logic is sound and well-tested.
VERDICT: ✅ Worth merging — Core logic is sound, the security concern is limited to admin-level users and may be intentional for the draft workflow. Address the stale docstring and dead code in a follow-up.
KEY INSIGHT: The dual enabled/lifecycle_status model is well-implemented with consistent cross-updates, but the security relaxation of allowing non-creators to edit automation definitions deserves explicit justification.
Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:
- Add a
.agents/skills/custom-codereview-guide.mdfile to your branch (or edit it if one already exists) with the/codereviewtrigger and the context the reviewer is missing (e.g., "Security concerns about X do not apply here because Y"). See the customization docs for the required frontmatter format.- Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
- When your PR is merged, the guideline file goes through normal code review by repository maintainers.
Resolve with AI? Install the iterate skill in your agent and run
/iterateto automatically drive this PR through CI, review, and QA until it's merge-ready.Was this review helpful? React with 👍 or 👎 to give feedback.
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
|
🤖 OpenHands is reviewing this PR. Head commit: This comment was posted by an AI agent (OpenHands). |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was created by an AI agent (OpenHands) on behalf of the repository maintainers.
Review Summary
PR 1 of 2: lifecycle_state + trigger_source foundation
This PR introduces a first-class state enum (ACTIVE/INACTIVE/DRAFT) on the Automation model and a trigger_source column on AutomationRun, while keeping the legacy enabled boolean for backward compatibility. The implementation is clean, consistent, and well-tested.
What was verified
- Migration (
023_add_state_and_trigger_source.py): Cross-database compatible (usessa.String, no PG-specific types). Backfill viaCASE WHEN enabled THEN 'ACTIVE' ELSE 'INACTIVE' ENDis correct — no existing row can be DRAFT. Indexes onstateandtrigger_source(plus compositestatus, trigger_source) are appropriate for the dispatcher and scheduler queries. - State/enabled consistency: Every site that sets
enabled=Falsealso setsstate=INACTIVE—router.py(create, update, delete),scheduler.py(invalid cron),utils/run.py(disable_automation),utils/unhealthy.py(_apply_disable),git_sync/loop.py(soft-delete). No site was missed. - Dispatcher query:
or_(trigger_source == "manual", and_(enabled.is_(True), state == ACTIVE))correctly allows manual runs to proceed regardless of automation state, while cron/event runs require bothenabled=Trueandstate=ACTIVE. Deleted automations are still excluded viadeleted_at.is_(None). - Schema validation:
normalize_automation_state_enabledcorrectly rejects conflictingstate+enabledpairs (verified:state=DRAFT, enabled=TrueraisesValidationError). When only one field is sent, the other is derived consistently. - Permission model relaxation: Non-creator admins can now toggle
state/enabledbut still cannot edit the automation definition (tarball, entrypoint, trigger, etc.)._assert_can_update_fieldswith_STATE_ONLY_UPDATE_FIELDScorrectly enforces this. This is a reasonable scope expansion — admins withmanage_automationscan already delete automations. - skip_pending_runs_for_disabled_automation:
include_manual=Falseby default preserves manual runs when an automation is system-disabled (unhealthy, auth failures).include_manual=Trueon delete correctly skips all pending runs including manual ones, since a deleted automation can no longer be dispatched. - Scheduler/webhook filtering: Both
_fetch_enabled_automationsandget_event_automations/get_requested_event_typesnow filter onstate == ACTIVEin addition toenabled == True. Draft automations are never triggered automatically. Verified by tests. - Test coverage: New tests cover the key behavioral changes — manual dispatch for disabled automations, draft exclusion from scheduler and webhook matching, non-creator state updates, and the dispatcher picking up manual runs for INACTIVE automations.
Non-blocking observations (included for awareness, not requiring changes)
-
System-disabled automations can be manually dispatched: Removing the 409 check on the dispatch endpoint means automations disabled by the unhealthy watchdog or auth failures can now be manually triggered. This is intentional per the PR description and is the foundation for the draft test-dispatch workflow in PR 2. The previous behavior of surfacing
disabled_reason/disabled_detailto the caller is lost, but manual dispatch is an explicit user action. -
DeprecationWarning on
enabled:normalize_automation_state_enabledemits aDeprecationWarningwheneverenabledappears in a request body. Python's default warning filter suppressesDeprecationWarningin production, so this is unlikely to cause log noise, but clients should migrate tostateas indicated. -
Duplicate
AutomationStatedefinitions:models.pydefinesAutomationState(enum.Enum)andschemas.pydefinesAutomationState(StrEnum). This mirrors the existingAutomationRunStatus/RunStatuspattern and is consistent with the codebase.
[RISK ASSESSMENT]
- Overall PR: 🟢 LOW
- No breaking changes to existing API behavior (backward compatibility maintained via
enabledflag) - Migration is additive (new columns with safe defaults, no data loss)
- Permission relaxation is scoped to state/enabled toggles only
- All state transitions are covered with tests
- No breaking changes to existing API behavior (backward compatibility maintained via
VERDICT: ✅ Worth merging
KEY INSIGHT: The dual-column approach (state + enabled) with a single source of truth (state) and derived enabled is a pragmatic migration strategy that avoids breaking existing clients while introducing the richer lifecycle model.
Automated review used the wrong decision (APPROVED instead of COMMENT) and is dismissed. Findings are reposted as a comment.
|
Posted an APPROVED review to PR #438 (OpenHands/automation). Review Summary: The PR introduces a first-class Key findings: No material issues. The implementation is thorough and consistent:
Risk Assessment: 🟢 LOW — no breaking changes, additive migration, scoped permission relaxation. Verdict: ✅ Worth merging — approved per the custom code review guide since there are no blocking issues and risk is LOW. This comment was posted by an AI agent (OpenHands). |
|
@malhotra5 I had Astra make a pass, and the results were:
They look plausible to me, but I think maybe some are actually reasonable behaviors: for one, point 4 sounds like pretty good behavior to me. If I dispatched, and then was like, actually let me tweak this prompt, then I’d expect any later run to get the tweak. So new stuff applies. I can ask the agent to post its repro tests, so we see them red? |
Reproduce Git state import errors and collisions with pinned published Alembic history. Include passing compatibility controls and offline run instructions. Production code is unchanged.
|
📁 PR Artifacts Notice This PR contains a |
Co-authored-by: openhands <openhands@all-hands.dev>
…payloads Add the draft lifecycle layer on top of the lifecycle_status foundation from PR #438: - draft_schemas.py: partial draft body models for all three creation endpoints (/v1, /v1/preset/prompt, /v1/preset/plugin), draft body normalization, and FINAL_DRAFT_MODELS registry - draft_router.py: full CRUD for server-backed drafts (POST/GET/PATCH/DELETE /v1/drafts), draft validation, materialization into DRAFT-state automations, and POST /v1/drafts/{id}/dispatch for test runs. Dispatch accepts an optional event_payload so event-triggered draft automations can be test-run with a user-supplied synthetic payload, bypassing webhook signature verification (caller is authenticated). - models.py: AutomationDraft model with source/materialized/last_test_run foreign keys - schemas.py: CreateAutomationDraftRequest, UpdateAutomationDraftRequest, AutomationDraftResponse, AutomationDraftListResponse, DraftDispatchRequest - capabilities_router.py: use draft_schemas registry instead of inline _DRAFT_MODELS; add automationDrafts to static features - app.py: register draft_router - utils/run.py: create_pending_run accepts optional event_payload - migration 024: automation_drafts table Stacked on PR #438 (lifecycle_status + trigger_source). Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
|
@enyst update on the review pass: Addressed in the stack:
Intentionally not addressed:
Not addressed in this pass:
Current CI status after the latest pushes:
This comment was created by an AI agent (OpenHands) on behalf of the user. |
Summary
This is PR 1 of 2 in a stacked PR series that splits PR #417 (automation draft lifecycle dispatch) into smaller, independently reviewable pieces.
PR 1 (this PR): lifecycle_status + trigger_source foundation
Introduces the data model and plumbing for first-class automation lifecycle states and run trigger sources, without yet adding the draft-specific endpoints or synthetic-payload test dispatch (those come in PR 2).
PR 2 (follow-up): draft lifecycle endpoints + event test dispatch
Will add the drafts table, draft schemas/endpoints, event automation test dispatch in draft mode, and synthetic payload support for manual test runs. Stacked on top of this PR.
What this PR does
1. Deprecates
enabledflag in favor oflifecycle_statusenumAutomationStateenum (ACTIVE/INACTIVE/DRAFT) tomodels.pylifecycle_statuscolumn onAutomation(indexed, backfilled fromenabledin migration)enabledfor backwards compatibility — onlyACTIVErows haveenabled=Trueenabled=Falsenow also setlifecycle_status=INACTIVE:router.py: create, update, deletescheduler.py: disable invalid cronutils/run.py:disable_automationutils/unhealthy.py:_apply_disablegit_sync/loop.py: soft-delete on import2. Adds
trigger_sourcetoAutomationRuntrigger_sourcecolumn (manual/cron/event/nullfor legacy)"manual"on dispatch endpoint,"cron"in scheduler,"event"in webhook handlerdispatcher.py:_poll_pending_runsnow dispatches manual runs regardless of automation enabled/stateskip_pending_runs_for_disabled_automationexcludes manual runs by default (include_manual=False); delete usesinclude_manual=True3. Schema-level enforcement
AutomationStateStrEnum inschemas.pynormalize_automation_state_enabled()model_validator keepsenabledandlifecycle_statusconsistentlifecycle_statusfield added toCreateAutomationRequest,UpdateAutomationRequest,AutomationResponse, and both preset request modelstrigger_sourceadded toAutomationRunResponse4. Scheduler/webhook filtering
_fetch_enabled_automations(scheduler) andget_event_automations/get_requested_event_types(webhook) now filter onlifecycle_status == ACTIVEin addition toenabled5. Migration
023_add_lifecycle_status_and_trigger_source.py: adds columns, indexes, and backfillslifecycle_statusfromenabledTest plan
uv run pytest tests/ -q --ignore=tests/integration)test_dispatches_manual_run_for_disabled_automation— manual run dispatched even when automation is INACTIVEtest_dispatch_disabled_automation_creates_manual_run— dispatch endpoint creates manual run instead of returning 409test_poll_excludes_draft_even_if_enabled_flag_is_true— draft automations never scheduledtest_excludes_draft_automations— draft automations excluded from event matchingThis PR was created by an AI agent (OpenHands) on behalf of the user.
@malhotra5 can click here to continue refining the PR