Skip to content

Productize Samsarix Routine Engine and add its Samsarix roadmap - #1

Merged
Deathcharge merged 2 commits into
mainfrom
codex/productize-samsarix-routine-engine
Jul 29, 2026
Merged

Productize Samsarix Routine Engine and add its Samsarix roadmap#1
Deathcharge merged 2 commits into
mainfrom
codex/productize-samsarix-routine-engine

Conversation

@Deathcharge

Copy link
Copy Markdown
Owner

Merge intent

This PR integrates the reviewed productization branch and its repository-specific roadmap. Merge is distinct from release, publication, deployment, or adoption as a canonical Samsarix Unified subsystem.

Portfolio contract

  • Preserve the pre-productization default at �rchive/pre-samsarix-productization-2026-07-29.
  • Use public package, API, event, schema, artifact, or deployment contracts instead of private helix-unified imports.
  • Track release and flagship-adoption gates in ROADMAP.md.

Verification

The portfolio merge-readiness pass compared this branch with its default, inspected its flagship relationship, and found no merge-blocking regression. Repository CI and any additional local verification remain recorded in the portfolio audit ledger.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • New Features
    • Added the Routine Engine workflow library for validated, dependency-aware workflows.
    • Added synchronous and asynchronous execution, retries, concurrency limits, and clear run statuses.
    • Added a command-line interface for validating, running, and demonstrating workflows.
    • Added built-in actions and local JSON persistence with bounded history.
  • Documentation
    • Added updated getting-started, API, security, product, roadmap, and runnable example documentation.
  • Quality
    • Added automated cross-platform testing, coverage checks, packaging validation, and type/lint checks.

Walkthrough

The repository is repositioned around a packaged RoutineEngine library with validated workflow models, bounded execution, local JSON persistence, a CLI, built-in actions, updated examples, legacy-surface restrictions, expanded documentation, tests, and CI quality gates.

Changes

Routine Engine productization

Layer / File(s) Summary
Product contract and packaging
pyproject.toml, src/routine_engine/*, README.md, docs/*, LICENSE, SECURITY.md, ROADMAP.md
Defines the src-based package, public exports, API contract, licensing, security policy, product boundary, and contribution guidance.
Workflow models and execution runtime
src/routine_engine/models.py, src/routine_engine/engine.py, src/routine_engine/storage.py, src/routine_engine/errors.py
Adds immutable validated workflow/result models, action registration, dependency-aware sync/async execution, retries, concurrency limits, cancellation handling, and atomic bounded JSON persistence.
CLI, built-ins, and usage flows
src/routine_engine/cli.py, src/routine_engine/builtins.py, examples/*
Adds workflow validation/run/demo commands, plugin loading, state-file support, built-in actions, and synchronous, parallel, and failure-handling examples.
Legacy surface restrictions
routine_engine/helix_flow/*, routine_engine/workflow_engine/*, routine_engine/README.md
Marks extracted legacy code as archived, disables network and in-process code actions, fails closed when authorization is unavailable, and strengthens filesystem containment checks.
Validation, tests, and CI
tests/*, .github/workflows/ci.yml, .gitignore
Replaces legacy tests with coverage for validation, execution, persistence, CLI, built-ins, plugins, and cancellation, while adding lint, typing, packaging, wheel smoke tests, and a multi-platform Python matrix.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant RoutineEngine
  participant RegisteredAction
  participant JsonStore

  CLI->>RoutineEngine: Load and validate workflow
  RoutineEngine->>RegisteredAction: Execute resolved action context
  RegisteredAction-->>RoutineEngine: Return output or failure
  RoutineEngine->>JsonStore: Save workflow and run result
  RoutineEngine-->>CLI: Return serialized result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: productizing Samsarix Routine Engine and adding the roadmap.
Description check ✅ Passed The description is directly related to the productization and roadmap changes in the pull request.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/productize-samsarix-routine-engine

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 10

🤖 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 @.github/workflows/ci.yml:
- Line 15: Update both checkout steps in the test and build jobs to set
persist-credentials to false, ensuring the checkout action does not retain the
job token in the repository configuration.

In `@LICENSE`:
- Line 48: Update the LICENSE terms around the pricing-schedule reference so the
complete commercial pricing schedule is included with the distributed license or
replaced with a stable, versioned reference. Ensure downstream users can
determine applicable compliance requirements and fees without relying on an
unavailable document.

In `@ROADMAP.md`:
- Around line 30-38: Update the “Current hardening backlog” section in
ROADMAP.md by removing the stale claims that the candidate is uncommitted or
absent remotely, the license metadata is outdated, and CI or static
implementation claims remain unverified. Replace them with only the currently
outstanding, evidence-backed release gates, consistent with the documented BSL
update and the release status described earlier in the roadmap.

In `@routine_engine/helix_flow/api_routes.py`:
- Around line 41-43: Update _builder_guard handling in _builder_guard to cache a
distinct unavailable sentinel when importing require_builder raises ImportError,
then raise the existing 503 response for that cached state without retrying the
import or logging repeatedly.

In `@src/routine_engine/engine.py`:
- Around line 53-60: Update Engine.validate so direct Workflow inputs are
normalized through the same Workflow.from_dict validation path as mapping
inputs, using the existing to_dict conversion rather than trusting the instance.
Preserve the subsequent unregistered-action check and return the validated
Workflow so empty, invalid concurrency, and dependency violations raise
WorkflowValidationError before scheduling.

In `@src/routine_engine/models.py`:
- Around line 105-110: Update the workflow construction around the classmethod
returning cls to recursively freeze detached_params, converting nested
dictionaries and lists into immutable equivalents while preserving scalar
values. Update Workflow.to_dict() to recursively thaw those immutable containers
back into JSON-serializable dictionaries and lists before returning parameters.
- Around line 39-43: Update _require_json to recursively validate all mapping
keys before json.dumps, rejecting any key that is not a str with
WorkflowValidationError. Traverse nested dictionaries and containers so
non-string keys are rejected at every level, while preserving the existing
finite and JSON-compatibility validation behavior.

In `@src/routine_engine/storage.py`:
- Around line 65-70: Move the parent-directory creation in _write inside the
existing exception-handling scope, and catch the resulting OSError alongside
serialization failures so directory-creation errors are re-raised as
StorageError. Preserve the existing error message and exception chaining for
persistence failures.

In `@tests/conftest.py`:
- Around line 32-33: Remove the unused output helper function from
tests/conftest.py, including its definition and any associated unused import if
applicable. Do not alter other test configuration or fixtures.

In `@tests/test_execution.py`:
- Around line 98-99: Add asyncio_mode = "auto" to the [tool.pytest.ini_options]
pytest configuration, ensuring the `@pytest.mark.asyncio`
test_async_actions_run_in_parallel test is consistently awaited and plugin
absence is detected.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 294780af-2ca5-4fe0-9621-760e44a77ac9

📥 Commits

Reviewing files that changed from the base of the PR and between 50e6371 and e542bcd.

📒 Files selected for processing (43)
  • .github/workflows/ci.yml
  • .gitignore
  • CHANGELOG.md
  • CODE_OF_CONDUCT.md
  • CONTRIBUTING.md
  • LICENSE
  • README.md
  • ROADMAP.md
  • SECURITY.md
  • docs/API_REFERENCE.md
  • docs/GETTING_STARTED.md
  • docs/PRODUCTIZATION.md
  • examples/01_basic_workflow.py
  • examples/02_parallel_workflow.py
  • examples/02_scheduled_workflow.py
  • examples/03_error_handling.py
  • examples/input.json
  • examples/workflow.json
  • pyproject.toml
  • pytest.ini
  • requirements-test.txt
  • routine_engine/README.md
  • routine_engine/helix_flow/api_routes.py
  • routine_engine/helix_flow/pyproject.toml
  • routine_engine/helix_flow/tools.py
  • routine_engine/workflow_engine/README.md
  • routine_engine/workflow_engine/core.py
  • routine_engine/workflow_engine/integrations.py
  • setup.py
  • src/routine_engine/__init__.py
  • src/routine_engine/__main__.py
  • src/routine_engine/builtins.py
  • src/routine_engine/cli.py
  • src/routine_engine/engine.py
  • src/routine_engine/errors.py
  • src/routine_engine/models.py
  • src/routine_engine/py.typed
  • src/routine_engine/storage.py
  • tests/conftest.py
  • tests/test_execution.py
  • tests/test_scheduling.py
  • tests/test_tasks.py
  • tests/test_workflow.py
💤 Files with no reviewable changes (5)
  • examples/02_scheduled_workflow.py
  • routine_engine/helix_flow/pyproject.toml
  • pytest.ini
  • setup.py
  • requirements-test.txt

Comment thread .github/workflows/ci.yml
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Set persist-credentials: false on both checkout steps.

actions/checkout writes the job token into .git/config by default, so any later step (or uploaded artifact containing .git) can leak it. Neither job pushes, so credentials are not needed.

🔒️ Proposed hardening
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v4
+        with:
+          persist-credentials: false

Apply the same change to the test job checkout at Line 44.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.28.0)

[warning] 15-15: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 15-15: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 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 @.github/workflows/ci.yml at line 15, Update both checkout steps in the test
and build jobs to set persist-credentials to false, ensuring the checkout action
does not retain the job token in the repository configuration.

Source: Linters/SAST tools

Comment thread LICENSE
the monthly volume of such production use exceeds 1,000 API calls or equivalent
usage metric, then you must secure a commercial license key from Licensor or pay
the fees specified in the pricing schedule at https://helixcollective.dev/pricing.
the fees specified in the pricing schedule provided by Licensor.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== LICENSE excerpt and structure =="
if [ -f LICENSE ]; then
  wc -l LICENSE
  sed -n '1,120p' LICENSE | cat -n
else
  fd -a '^LICENSE$' .
fi

echo
echo "== Repository references to pricing schedule / Additional Use Grant =="
rg -n --hidden --glob '!/.git/**' --glob '!/node_modules/**' \
  'Additional Use Grant|pricing schedule|production use|BSL|License' . || true

echo
echo "== File diff stat/name-only if available =="
git diff --stat -- LICENSE 2>/dev/null || true
git show --stat -- LICENSE 2>/dev/null | sed -n '1,80p' || true

Repository: Deathcharge/routine-engine

Length of output: 9795


🌐 Web query:

MariaDB Business Source License 1.1 Additional Use Grant production use license text

💡 Result:

The Business Source License (BSL) 1.1 does not contain a single, universal "Additional Use Grant" text [1][2]. Instead, the Additional Use Grant is a placeholder section that the licensor must define for each specific product [3][4][5]. The core BSL 1.1 license text describes this section as follows: "The Licensor may make an Additional Use Grant, above, permitting limited production use." [1][2][6] In the license header or associated license file, the licensor is required to: "To either: (a) specify an additional grant of rights to use that does not impose any additional restriction on the right granted in this License, as the Additional Use Grant; or (b) insert the text 'None'." [1][2][6] Because this section is product-specific, its content varies depending on how the software vendor chooses to define their usage terms [4][5]. For example, in MariaDB Corporation’s use of the BSL for MaxScale, the Additional Use Grant has been defined as: "Additional Use Grant: You may use the Licensed Work when your application uses the Licensed Work with a total of less than three server instances in production." [7][8] In summary, if you are evaluating a BSL 1.1 licensed product, you must look at the specific license file provided with that software to find the actual text of the Additional Use Grant, as it determines the specific parameters under which you are permitted to use the software in production without a separate commercial license [4][5].

Citations:


🌐 Web query:

SPDX BUSL-1.1 BSL template SourceUseLimitation Additional Use Grant

💡 Result:

The Business Source License 1.1 (identified by the SPDX short identifier BUSL-1.1) is a source-available license that balances commercial protection for licensors with a guarantee of eventual open-source status [1][2][3]. It is not an open-source license, as it restricts production use [2][4]. The license operates as a template requiring the licensor to complete specific fields [5][3]. Central to its structure are three key components: 1. Additional Use Grant: This optional field allows the licensor to grant specific permissions that override the default prohibition on production use [2][3]. Licensors can define specific permitted use cases here (e.g., non-commercial use, internal use, or specific integrations) [6][7][8]. If no grant is provided, the licensor may simply insert "None" [5][9][10]. 2. Change Date: The licensor must specify a date or time period, capped at four years, after which the software automatically converts to the Change License [1][2][4]. 3. Change License: The licensor must specify an open-source license (which must be compatible with GPL v2.0 or later) that the software will adopt once the Change Date is reached [5][2][4]. The license explicitly grants users the right to copy, modify, create derivative works, and redistribute the software, provided they comply with the restrictions on production use as defined by the Additional Use Grant [1][5][8]. If a user's intended use falls outside the scope of the Additional Use Grant, they must purchase a separate commercial license from the copyright holder [1][2][3].

Citations:


Publish or embed the pricing schedule referenced by BSL Section 2.

Additional Use Grant: None means the only production-use terms are the Section 2 modifications, but line 48 refers to a pricing schedule that is not attached or versioned here. Make the full commercial-license terms immediately available with the distributed license or reference a stable, versioned schedule; otherwise downstream users cannot determine compliance or cost.

🤖 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 `@LICENSE` at line 48, Update the LICENSE terms around the pricing-schedule
reference so the complete commercial pricing schedule is included with the
distributed license or replaced with a stable, versioned reference. Ensure
downstream users can determine applicable compliance requirements and fees
without relying on an unavailable document.

Comment thread ROADMAP.md
Comment on lines +30 to +38
Current hardening backlog:

- The entire candidate is uncommitted and absent remotely.
- No total workflow-file, nesting, persisted-record, output, or error-message size cap.
- Sync actions can block the event loop; actions have no timeout, isolation, idempotency key, compensation, or durable queue.
- Local JSON persistence is single-process and may retain secrets in plaintext.
- The market already has many DAG/workflow libraries; differentiation is weak without a portfolio consumer.
- License metadata is stale and internally risky: the BSL names Helix Collective and “Helix Licensing System,” has no Additional Use Grant, and does not clearly identify this package.
- Untracked CI and all static implementation claims remain unverified.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove the stale, contradictory release-state backlog.

Lines 32 and 38 contradict Line 16; Line 37 also conflicts with the documented BSL update. Replace these historical assertions with the actual outstanding, evidence-backed gates so release decisions are not based on mutually exclusive states.

🤖 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 `@ROADMAP.md` around lines 30 - 38, Update the “Current hardening backlog”
section in ROADMAP.md by removing the stale claims that the candidate is
uncommitted or absent remotely, the license metadata is outdated, and CI or
static implementation claims remain unverified. Replace them with only the
currently outstanding, evidence-backed release gates, consistent with the
documented BSL update and the release status described earlier in the roadmap.

Comment on lines 41 to +43
except ImportError:
logger.warning("require_builder unavailable — flows fall back to auth-only")
_builder_guard = None
if _builder_guard is None:
return await get_current_user(request=request, credentials=credentials)
logger.error("require_builder unavailable — protected flow execution is disabled")
raise HTTPException(status_code=503, detail="Flow authorization is unavailable") from None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Cache the unavailable authorization state.

_builder_guard remains _UNSET after this failure, so every protected request re-imports and logs an error before returning 503. Cache a distinct unavailable sentinel, then raise the same 503 without repeated imports/log spam.

🤖 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 `@routine_engine/helix_flow/api_routes.py` around lines 41 - 43, Update
_builder_guard handling in _builder_guard to cache a distinct unavailable
sentinel when importing require_builder raises ImportError, then raise the
existing 503 response for that cached state without retrying the import or
logging repeatedly.

Comment on lines +53 to +60
def validate(self, workflow: Workflow | Mapping[str, Any]) -> Workflow:
"""Validate structure, dependency graph, and action registrations."""

definition = workflow if isinstance(workflow, Workflow) else Workflow.from_dict(workflow)
missing = sorted({step.action for step in definition.steps} - self._actions.keys())
if missing:
raise WorkflowValidationError(f"unregistered action(s): {', '.join(missing)}")
return definition

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Revalidate direct Workflow inputs.

Line 56 trusts every Workflow instance, but structural/resource checks occur in Workflow.from_dict(). A directly constructed empty workflow can return success without steps, while invalid concurrency/dependencies can reach scheduler errors instead of WorkflowValidationError. Enforce invariants at model construction or normalize Workflow instances through to_dict()/from_dict() here.

Proposed boundary normalization
-        definition = workflow if isinstance(workflow, Workflow) else Workflow.from_dict(workflow)
+        definition = (
+            Workflow.from_dict(workflow.to_dict())
+            if isinstance(workflow, Workflow)
+            else Workflow.from_dict(workflow)
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def validate(self, workflow: Workflow | Mapping[str, Any]) -> Workflow:
"""Validate structure, dependency graph, and action registrations."""
definition = workflow if isinstance(workflow, Workflow) else Workflow.from_dict(workflow)
missing = sorted({step.action for step in definition.steps} - self._actions.keys())
if missing:
raise WorkflowValidationError(f"unregistered action(s): {', '.join(missing)}")
return definition
def validate(self, workflow: Workflow | Mapping[str, Any]) -> Workflow:
"""Validate structure, dependency graph, and action registrations."""
definition = (
Workflow.from_dict(workflow.to_dict())
if isinstance(workflow, Workflow)
else Workflow.from_dict(workflow)
)
missing = sorted({step.action for step in definition.steps} - self._actions.keys())
if missing:
raise WorkflowValidationError(f"unregistered action(s): {', '.join(missing)}")
return definition
🤖 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/routine_engine/engine.py` around lines 53 - 60, Update Engine.validate so
direct Workflow inputs are normalized through the same Workflow.from_dict
validation path as mapping inputs, using the existing to_dict conversion rather
than trusting the instance. Preserve the subsequent unregistered-action check
and return the validated Workflow so empty, invalid concurrency, and dependency
violations raise WorkflowValidationError before scheduling.

Comment on lines +39 to +43
def _require_json(value: Any, field_name: str) -> None:
try:
json.dumps(value, allow_nan=False)
except (TypeError, ValueError, OverflowError) as exc:
raise WorkflowValidationError(f"{field_name} must be finite, JSON-compatible data") from exc

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reject non-string JSON object keys.

Line 41 accepts {1: "a", "1": "b"}; the round-trip at Line 105 serializes both keys as "1" and silently retains only one value. Recursively require str keys before serialization.

Proposed fix
 def _require_json(value: Any, field_name: str) -> None:
+    def require_string_keys(item: Any) -> None:
+        if isinstance(item, Mapping):
+            if any(not isinstance(key, str) for key in item):
+                raise WorkflowValidationError(f"{field_name} object keys must be strings")
+            for nested in item.values():
+                require_string_keys(nested)
+        elif isinstance(item, list):
+            for nested in item:
+                require_string_keys(nested)
+
+    require_string_keys(value)
     try:
         json.dumps(value, allow_nan=False)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _require_json(value: Any, field_name: str) -> None:
try:
json.dumps(value, allow_nan=False)
except (TypeError, ValueError, OverflowError) as exc:
raise WorkflowValidationError(f"{field_name} must be finite, JSON-compatible data") from exc
def _require_json(value: Any, field_name: str) -> None:
def require_string_keys(item: Any) -> None:
if isinstance(item, Mapping):
if any(not isinstance(key, str) for key in item):
raise WorkflowValidationError(f"{field_name} object keys must be strings")
for nested in item.values():
require_string_keys(nested)
elif isinstance(item, list):
for nested in item:
require_string_keys(nested)
require_string_keys(value)
try:
json.dumps(value, allow_nan=False)
except (TypeError, ValueError, OverflowError) as exc:
raise WorkflowValidationError(f"{field_name} must be finite, JSON-compatible data") from exc
🧰 Tools
🪛 ast-grep (0.45.0)

[info] 40-40: use jsonify instead of json.dumps for JSON output
Context: json.dumps(value, allow_nan=False)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 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/routine_engine/models.py` around lines 39 - 43, Update _require_json to
recursively validate all mapping keys before json.dumps, rejecting any key that
is not a str with WorkflowValidationError. Traverse nested dictionaries and
containers so non-string keys are rejected at every level, while preserving the
existing finite and JSON-compatibility validation behavior.

Comment on lines +105 to +110
detached_params = json.loads(json.dumps(params, allow_nan=False))
return cls(
id=step_id,
action=action,
needs=tuple(raw_needs),
params=MappingProxyType(detached_params),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Deep-freeze validated workflow parameters.

MappingProxyType protects only params itself. Callers can still mutate nested dictionaries or lists in a supposedly immutable Workflow, changing the validated definition after construction. Recursively freeze JSON containers, and thaw them when serializing in to_dict().

🤖 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/routine_engine/models.py` around lines 105 - 110, Update the workflow
construction around the classmethod returning cls to recursively freeze
detached_params, converting nested dictionaries and lists into immutable
equivalents while preserving scalar values. Update Workflow.to_dict() to
recursively thaw those immutable containers back into JSON-serializable
dictionaries and lists before returning parameters.

Comment on lines +65 to +70
def _write(self, state: dict[str, Any]) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
try:
payload = json.dumps(state, indent=2, sort_keys=True, allow_nan=False) + "\n"
except (TypeError, ValueError, OverflowError) as exc:
raise StorageError("workflow outputs must be finite, JSON-compatible data to persist them") from exc

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Wrap parent-directory failures in StorageError.

Line 66 is outside the try block. For example, when a path component is an existing file, mkdir() raises raw OSError rather than the documented StorageError for unwritable state.

Proposed fix
 def _write(self, state: dict[str, Any]) -> None:
-    self.path.parent.mkdir(parents=True, exist_ok=True)
     try:
+        self.path.parent.mkdir(parents=True, exist_ok=True)
         payload = json.dumps(state, indent=2, sort_keys=True, allow_nan=False) + "\n"
-    except (TypeError, ValueError, OverflowError) as exc:
+    except (OSError, TypeError, ValueError, OverflowError) as exc:
         raise StorageError("workflow outputs must be finite, JSON-compatible data to persist them") from exc
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _write(self, state: dict[str, Any]) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
try:
payload = json.dumps(state, indent=2, sort_keys=True, allow_nan=False) + "\n"
except (TypeError, ValueError, OverflowError) as exc:
raise StorageError("workflow outputs must be finite, JSON-compatible data to persist them") from exc
def _write(self, state: dict[str, Any]) -> None:
try:
self.path.parent.mkdir(parents=True, exist_ok=True)
payload = json.dumps(state, indent=2, sort_keys=True, allow_nan=False) + "\n"
except (OSError, TypeError, ValueError, OverflowError) as exc:
raise StorageError("workflow outputs must be finite, JSON-compatible data to persist them") from exc
🧰 Tools
🪛 ast-grep (0.45.0)

[info] 67-67: use jsonify instead of json.dumps for JSON output
Context: json.dumps(state, indent=2, sort_keys=True, allow_nan=False)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 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/routine_engine/storage.py` around lines 65 - 70, Move the
parent-directory creation in _write inside the existing exception-handling
scope, and catch the resulting OSError alongside serialization failures so
directory-creation errors are re-raised as StorageError. Preserve the existing
error message and exception chaining for persistence failures.

Comment thread tests/conftest.py
Comment on lines +32 to +33
def output(context: ActionContext) -> Any:
return context.params.get("value")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Any importer or direct caller of the conftest `output` helper?
rg -nP --type=py '\bfrom\s+(tests\.)?conftest\s+import\b|\bimport\s+conftest\b'
rg -nP --type=py -g 'tests/**' '(?<![\w.])output\s*\('

Repository: Deathcharge/routine-engine

Length of output: 164


🏁 Script executed:

#!/bin/bash
set -u

echo "== Python files mentioning conftest output =="
rg -nP --type=py 'conftest|tests\.conftest|from .*tests.*conftest|import conftest|output\s*\('

echo
echo "== Candidate pytest config/conftest files =="
git ls-files | rg '(^|/)pytest.*\.ini$|(^|/)pyproject\.toml$|(^|/)setup\.cfg$|(^|/)tox\.ini$|^tests/conftest\.py$' || true

echo
echo "== tests/conftest.py =="
if [ -f tests/conftest.py ]; then
  sed -n '1,120p' tests/conftest.py | cat -n
fi

echo
echo "== All Python files under tests =="
git ls-files 'tests/**/*.py' 'tests/*.py' | sort

Repository: Deathcharge/routine-engine

Length of output: 505


🏁 Script executed:

#!/bin/bash
set -u

echo "== Python files mentioning output helper patterns =="
rg -nP --type=py 'conftest|tests\.conftest|from .*tests.*conftest|import conftest|^\s*output\s*\(' tests || true

echo
echo "== Candidate pytest config/conftest files =="
git ls-files | rg '(^|/)pytest.*\.ini$|(^|/)pyproject\.toml$|(^|/)setup\.cfg$|(^|/)tox\.ini$|^tests/conftest\.py$|^conftest\.py$' || true

echo
echo "== tests/conftest.py =="
if [ -f tests/conftest.py ]; then
  cat -n tests/conftest.py
fi

echo
echo "== All Python files under tests =="
git ls-files 'tests/**/*.py' 'tests/*.py' | sort

echo
echo "== Direct AST/import probe for unqualified output helper usages =="
python3 - <<'PY'
import ast, pathlib
for p in pathlib.Path('tests').rglob('*.py'):
    try:
        tree = ast.parse(p.read_text())
    except SyntaxError:
        continue
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            fn = node.func
            if isinstance(fn, ast.Name) and fn.id == 'output' and ast.dump(fn.annotation) != ast.dump(
                ast.parse('ActionContext').body[0].value if isinstance(fn.annotation, ast.Attribute) and fn.annotation.value.id == 'ActionContext' else None
            ):
                print(f"{p}:{node.lineno}: unqualified function call output(...)")
            elif isinstance(fn, ast.Attribute) and fn.attr == 'output':
                try:
                    print(f"{p}:{node.lineno}: attribute access ..{fn.attr}")
                except Exception:
                    pass
PY

Repository: Deathcharge/routine-engine

Length of output: 1619


Remove the unused output helper from tests/conftest.py.

It is not a pytest fixture and no test module calls or imports it.

🤖 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 `@tests/conftest.py` around lines 32 - 33, Remove the unused output helper
function from tests/conftest.py, including its definition and any associated
unused import if applicable. Do not alter other test configuration or fixtures.

Comment thread tests/test_execution.py
Comment on lines +98 to +99
@pytest.mark.asyncio
async def test_async_actions_run_in_parallel() -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -H -t f 'pyproject.toml|setup.cfg|pytest.ini|tox.ini' -x rg -n 'pytest-asyncio|asyncio_mode|filterwarnings|\[tool\.pytest' {}

Repository: Deathcharge/routine-engine

Length of output: 227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== config files =="
fd -H -t f '(^pyproject\.toml$|setup\.cfg|pytest\.ini|tox\.ini)' -x sh -c 'echo "--- $1"; sed -n "1,120p" "$1"' sh {}

echo "== async tests in tests/test_execution.py =="
fd -H -t f 'test_execution.py' -x sh -c 'echo "--- $1"; grep -n "`@pytest.mark.asyncio`\|async def " "$1" || true' sh {}

echo "== pytest async config references =="
rg -n "pytest-asyncio|asyncio_mode|tool\.pytest|filterwarnings" pyproject.toml setup.cfg pytest.ini tox.ini 2>/dev/null || true

echo "== repo dependencies sections =="
sed -n '1,90p' pyproject.toml

Repository: Deathcharge/routine-engine

Length of output: 4918


Set asyncio_mode in pytest config.

pytest-asyncio is in the dev dependency set, but [tool.pytest.ini_options] does not declare asyncio_mode. Add asyncio_mode = "auto" so @pytest.mark.asyncio tests are awaited consistently and fail if the plugin is absent.

🤖 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 `@tests/test_execution.py` around lines 98 - 99, Add asyncio_mode = "auto" to
the [tool.pytest.ini_options] pytest configuration, ensuring the
`@pytest.mark.asyncio` test_async_actions_run_in_parallel test is consistently
awaited and plugin absence is detected.

@Deathcharge
Deathcharge merged commit 40ddb2f into main Jul 29, 2026
19 checks passed
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.

1 participant