Skip to content

feat: ship bundled recipe workflows#118

Draft
Keith-CY wants to merge 26 commits intodevelopfrom
feat/recipe-import-library
Draft

feat: ship bundled recipe workflows#118
Keith-CY wants to merge 26 commits intodevelopfrom
feat/recipe-import-library

Conversation

@Keith-CY
Copy link
Collaborator

@Keith-CY Keith-CY commented Mar 12, 2026

Summary

  • ship bundled and importable recipe workflows for dedicated agents and persona packs
  • align the recipe action surface with OpenClaw CLI semantics while keeping higher-level business actions for recipe authors
  • add a canonical recipe action catalog shared by backend validation/materialization and frontend action descriptions
  • keep runner execution OpenClaw-first, with guarded destructive actions and profile/auth sync for remote environments
  • add bundled recipe lifecycle controls: explicit bundled upgrades, digest-based update detection, and per-version approval invalidation
  • tighten Recipe review/execution with strict approval gating and non-technical bundled/source/risk status in Recipes and Cook
  • add dockerized OpenClaw recipe e2e coverage plus expanded authoring and runner documentation

Key additions

  • two-layer action surface: recommended business actions plus CLI primitive actions
  • canonical action catalog module: src-tauri/src/recipe_action_catalog.rs
  • bundled recipe seed state machine for missing / upToDate / updateAvailable / localModified / conflictedUpdate
  • workspace metadata for source kind, bundled version, trust level, risk level, and approval-required state
  • explicit bundled recipe upgrade action from the Recipes workspace cards
  • digest-scoped recipe approval for saved workspace recipes, enforced in both Review UX and backend execution
  • updated docs for authoring, runner boundaries, local docker debugging, and CLI action catalog

Product behavior changes

  • bundled recipes are only auto-seeded when missing; newer bundled versions no longer silently overwrite workspace copies
  • bundled recipe upgrades are explicit, and local modifications block one-click replacement
  • approval is tracked per workspace recipe digest and is invalidated by edits, re-imports, and bundled upgrades
  • Review now explains what must be ready, why execution is blocked, and when approval is required before continuing
  • Recipes workspace cards now show bundled/local/url source, update state, risk/approval badges, and bundled update actions

Testing

  • cargo test recipe_ --manifest-path src-tauri/Cargo.toml
  • cargo test recipe_workspace --manifest-path src-tauri/Cargo.toml
  • cargo test recipe_library_tests --manifest-path src-tauri/Cargo.toml
  • cargo fmt --all --manifest-path src-tauri/Cargo.toml -- --check
  • bun run typecheck
  • bun test src/pages/__tests__/Recipes.test.tsx src/components/__tests__/RecipePlanPreview.test.tsx src/pages/__tests__/cook-execution.test.ts

@github-actions
Copy link
Contributor

github-actions bot commented Mar 12, 2026

📊 Test Coverage Report

Metric Base (develop) PR (feat/recipe-import-library) Delta
Lines 74.44% (6142/8251) 74.38% (6153/8272) 🔴 -0.06%
Functions 68.98% (705/1022) 68.97% (709/1028) 🔴 -0.01%
Regions 75.97% (10171/13388) 75.87% (10185/13425) 🔴 -0.10%

Coverage measured by cargo llvm-cov (clawpal-core + clawpal-cli).

@github-actions
Copy link
Contributor

github-actions bot commented Mar 12, 2026

📦 PR Build Artifacts

Platform Download Size
Windows-x64 📥 clawpal-Windows-x64 17.6 MB
Linux-x64 📥 clawpal-Linux-x64 106.2 MB
macOS-x64 📥 clawpal-macOS-x64 14.0 MB
macOS-ARM64 📥 clawpal-macOS-ARM64 13.3 MB

🔨 Built from 98f7150 · View workflow run
⚠️ Unsigned development builds — for testing only

Copy link
Collaborator

@dev01lay2 dev01lay2 left a comment

Choose a reason for hiding this comment

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

PR #118 Review — Recipe Import Library & Authoring Workbench

CI all green across 4 platforms + coverage + e2e. This is a substantial feature PR (~19k lines, 106 files) that ships:

  • Recipe library model with examples/recipe-library/ structure
  • RecipeBundle + ExecutionSpec core schema with validation
  • Recipe adapter (legacy → structured spec compilation)
  • Recipe executor with systemd materialization
  • Runtime store for instances/runs/artifacts
  • Recipe workspace (fork/save/delete)
  • Recipe Studio with source editor + form mode
  • Draft recipe → Cook execution pipeline
  • Runtime traceability (source digest/origin)
  • Docker recipe E2E test
  • Comprehensive authoring documentation

Architecture is clean: bundle declares capabilities/resources, spec template gets compiled with params, executor materializes to systemd commands, runtime store tracks provenance. The phase boundary discipline (no remote reciped, no workflow engine, no OPA in phase 1) is well-documented.

One non-blocking note below. LGTM ✅

reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.133"
serde_yaml = "0.9"
Copy link
Collaborator

Choose a reason for hiding this comment

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

NBS: serde_yaml 0.9 is officially deprecated (the crate description literally says +deprecated). It still works fine, but if you plan to maintain this long-term, consider migrating to serde_yml or just using serde_json + json5 (which you already have) and dropping YAML support from parse_structured_document. Not urgent — just flagging for future awareness.

@Keith-CY Keith-CY marked this pull request as draft March 12, 2026 19:31
Copy link
Collaborator

@dev01lay2 dev01lay2 left a comment

Choose a reason for hiding this comment

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

Re-review after d9f8276..ba8460e (2 new commits)

CI: 7/9 green, Windows-x64 and Docker Recipe E2E still pending (typical for these runners).

The expanded action surface is solid:

  • New actions (delete_agent, unbind_channel, set_agent_model, set_agent_persona, clear_agent_persona, set_channel_persona, clear_channel_persona, upsert_markdown_document, delete_markdown_document, ensure_model_profile, delete_model_profile, ensure_provider_auth, delete_provider_auth) all follow the established internal-command pattern with proper JSON payload serialization.
  • markdown_document.rs — clean new module with path traversal prevention (validate_relative_path rejects .. components), section-level upsert via heading matching, and proper local/remote symmetry.
  • Safety guards are consistent: bindings_reference_agent prevents orphaned bindings on agent delete, auth_ref_is_in_use_by_bindings blocks auth removal while in use, with force escape hatches.
  • agent_identity.rsPersonaChange enum is a nice refinement over the previous Option<&str> approach, making set/clear/preserve semantics explicit.
  • Recipe adapter correctly registers capability and resource claims for all new action types.
  • Test coverage hits the key paths (materializer, binding removal, markdown section upsert, path validation).

NBS: channel_persona_patch (commands/mod.rs) currently only supports discord and errors on other channel types. If telegram/signal/etc. are planned for a future phase, a brief doc comment noting this is intentional scope limiting would help.

LGTM ✅

@Keith-CY
Copy link
Collaborator Author

Follow-up CI fix: preserve trailing newlines for content-bearing recipe args. The failing Docker Recipe E2E was caused by action_string() trimming persona text before materialization, which stripped the final newline from channel persona presets. Added a regression test for set_channel_persona, kept trimmed parsing for identifier-like fields, and re-ran the exact Docker Recipe E2E locally:

  • cargo test set_channel_persona_materialization_preserves_trailing_newline --manifest-path src-tauri/Cargo.toml
  • cargo test recipe_action_materializer_tests --lib --manifest-path src-tauri/Cargo.toml
  • CLAWPAL_RUN_DOCKER_RECIPE_E2E=1 cargo test -p clawpal --test recipe_docker_e2e -- --nocapture --test-threads=1

Copy link
Collaborator

@dev01lay2 dev01lay2 left a comment

Choose a reason for hiding this comment

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

Re-review after ba8460e..3550eeb (1 new commit)

CI: 10/10 green across all platforms + coverage + e2e.

The new commit fixes a real bug: action_string was normalizing multiline content (trimming/collapsing whitespace), which would break persona text containing paragraph breaks. The new action_content_string helper preserves the original string as-is while still rejecting whitespace-only values. Applied consistently to setup_identity, set_agent_persona, and set_channel_persona — the three actions that accept freeform content.

The test (set_channel_persona_materialization_preserves_trailing_newline) directly validates the round-trip through materialization. Clean fix.

LGTM ✅

Copy link
Collaborator

@dev01lay2 dev01lay2 left a comment

Choose a reason for hiding this comment

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

Re-review after 3550eeb..39c1223 (2 new commits)

CI: 9/10 green — Docker Recipe E2E fails (details below, not a blocker for merge IMO but worth fixing before or right after).

The two commits add a recipe action catalog (recipe_action_catalog.rs) and align the full CLI action surface with it. Clean architectural improvement:

  • Catalog as source of truth — every action has a canonical entry with metadata (group, category, read_only, interactive, runner_supported, capabilities, resource_kinds). Materializer and adapter both gate on runner_supported, so interactive/unsupported actions fail fast.
  • New CLI actionslist_agents, list_agent_bindings, set_agent_identity, bind_agent, unbind_agent, config ops, model ops, channel ops, secrets ops — all follow the established pattern.
  • Frontend parityactions.ts mirrors the Rust catalog. TypeScript types + API binding added.
  • Test coverage — materializer + adapter tests for new actions including compile-time rejection of interactive actions.

LGTM ✅

"label": "Create dedicated agent",
"args": {
"agentId": "{{agent_id}}",
"modelProfileId": "{{model}}",
Copy link
Collaborator

Choose a reason for hiding this comment

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

NBS: The dedicated-agent recipe now uses set_agent_identity + set_agent_persona instead of upsert_markdown_document — correct move. However, Docker Recipe E2E (recipe_docker_e2e.rs:514) still asserts dedicated_identity.contains("- Name: Ops Bot") by reading IDENTITY.md via SFTP, but openclaw agents set-identity writes to openclaw config, not IDENTITY.md. The SFTP read returns 81 bytes (the template), causing the assertion failure.

Fix: update the E2E to verify identity through openclaw agents list --json or config get instead of reading IDENTITY.md.

Copy link
Collaborator

@dev01lay2 dev01lay2 left a comment

Choose a reason for hiding this comment

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

Re-review after 39c1223..3c78eb7 (4 new commits)

CI: 9/10 green — Docker Recipe E2E fails on an identity assertion (dedicated_identity.contains("- Name: Ops Bot") at recipe_docker_e2e.rs:514). Details below.

What's new

Config fallback for remote discovery (discovery.rs) — when openclaw config get subcommands fail (e.g. older OpenClaw versions), discovery now falls back to reading openclaw.json directly via SFTP. The online_set fetch is moved before the error check so the fallback path can use it. Helper functions discord_sections_from_openclaw_config and agent_overviews_from_openclaw_config are clean, tested, and well-scoped.

identityName/identityEmoji field support (overview.rs) — collect_agent_overviews_from_config now prefers identityName/identityEmoji with name/emoji as fallback. Good backward compat. Visibility changed to pub(crate) for the new discovery fallback consumer. Test added.

Recipe library resolution hardening (recipe_library.rs) — resolve_bundled_recipe_library_root now checks an additional examples/recipe-library resource path (for Tauri's packaged layout), validates candidates with looks_like_recipe_library_root (checks for at least one recipe.json inside), deduplicates, and reports all checked paths on failure. This is the right fix for packaged-app distribution where the dev-time CARGO_MANIFEST_DIR relative path doesn't exist.

Recipe URL support in UI (Recipes.tsx, recipe-source-input.ts) — Load now accepts https:// URLs, Import correctly rejects them with a helpful message. Localization for both en/zh. Small but polished.

Docker debug guide (local-docker-openclaw-debug.md) — thorough step-by-step with the important "don't connect ClawPal during install" footgun called out. Good addition.

Docker E2E failure

The E2E asserts dedicated_identity.contains("- Name: Ops Bot") but the IDENTITY.md read-back after the recipe execution was only 81 bytes (down from 636 before the write cycle). Likely the set_agent_identity recipe step is writing back a truncated or differently-formatted IDENTITY.md. This looks like a pre-existing issue with how the dedicated-agent recipe materializes identity — not introduced by these 4 commits. Worth a look before merge but not blocking IMO since the other 9 checks pass and the core changes are solid.

LGTM ✅

.map(|path| path.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join(", ");
format!(
Copy link
Collaborator

Choose a reason for hiding this comment

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

NBS: dedupe_paths uses string comparison via to_string_lossy(). If any candidate contains symlinks or .. components, two paths pointing to the same directory won't be deduped. A canonicalize() call (with fallback to the lossy string for non-existent paths) would make this more robust. Low priority since the current callers produce clean paths.

Copy link
Collaborator

@dev01lay2 dev01lay2 left a comment

Choose a reason for hiding this comment

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

Re-review after 3c78eb7..ba41bd7 (1 new commit)

CI: Docker E2E still failing (same pre-existing identity assertion issue), Linux-x64 and Windows-x64 pending, rest green.

The commit adds two more _up_/ resource path candidates (_up_/recipe-library and _up_/examples/recipe-library) to bundled_recipe_library_candidates. This handles Tauri's packaged resource layout where the binary is nested one level deeper (e.g. inside a .app bundle's MacOS/ directory). The test is updated to match the new _up_ prefix. Minimal, targeted fix.

LGTM ✅

Copy link
Collaborator

@dev01lay2 dev01lay2 left a comment

Choose a reason for hiding this comment

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

Re-review after ba41bd7..70a695f (1 new commit)

CI: 9/10 green — Docker Recipe E2E still failing (pre-existing identity assertion issue, not introduced here).

The new commit extracts compile_recipe_directory_source as a pub(crate) helper and wires it into load_recipes_from_source so that passing a recipe directory path (not just a .json file) works as a load source. This is a natural extension — the compilation logic already existed for the import path, now it's reusable for the load path too.

  • recipe.rs: directory detection via path.is_dir() gates into the new helper cleanly
  • recipe_library.rs: refactor is mechanical — moves the recipe.json read + compile_recipe_source call into the new function, no behavior change for the existing import_recipe_dir caller
  • 129 lines of tests covering: directory load with asset inlining, find_recipe_with_source directory support, and missing recipe.json rejection
  • Localization updated in both en/zh, frontend test assertion updated

LGTM ✅

Copy link
Collaborator

@dev01lay2 dev01lay2 left a comment

Choose a reason for hiding this comment

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

Re-review after 70a695f..5315c70 (1 new commit)

CI: 9/10 green — Docker Recipe E2E still failing (pre-existing identity assertion issue), Windows-x64 pending.

Unified recipe source import flow

Solid UX improvement consolidating the Load + Import split into a single Import action for all source types.

Backend (recipe_library.rs):

  • import_recipe_source unified entry with prepare_recipe_imports dispatcher — auto-detects HTTP URL, recipe dir, library root, or file
  • Conflict detection via overwrite_existing flag with RecipeImportConflict list for UI prompting
  • seed_recipe_library now tracks bundled seeds via BundledSeedIndex (UUID v5 digest), so re-seeding skips user-modified recipes while updating unchanged ones. Clean upgrade-safe design.
  • BundledSeedStatus enum makes intent explicit

Frontend (Recipes.tsx):

  • Load button removed; Import is the single action
  • Native folder picker via tauri-plugin-dialog
  • Drag-and-drop with onDragDropEvent + hit-testing against drop target ref, visual dragActive highlight
  • Conflict resolution dialog with overwrite confirmation
  • loadedSource state eliminated

Workspace (recipe_workspace.rs):

  • save_bundled_recipe_source writes seed index; save_recipe_source clears it; delete cleans up. Correct lifecycle.

Test coverage is thorough. One NBS inline.

LGTM ✅

}: {
onCook: (id: string, source?: string) => void;
onCook: (
Copy link
Collaborator

Choose a reason for hiding this comment

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

NBS: The devicePixelRatio conversion is correct (Tauri reports physical pixels for drag-drop positions) but non-obvious. A brief comment like // Tauri drag-drop positions are in physical pixels; convert to logical for DOM rect comparison would help future readers.

Copy link
Collaborator

@dev01lay2 dev01lay2 left a comment

Choose a reason for hiding this comment

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

Re-review after 5315c70..e2feb58 (1 new commit)

CI: 9/10 green — Docker Recipe E2E still failing (pre-existing identity assertion issue).

The commit converts pick_recipe_source_directory from a blocking blocking_pick_folder() call to an async oneshot-channel pattern with pick_folder(callback). This prevents the native folder picker from blocking the Tauri main thread / IPC handler, which would freeze the UI on platforms where the dialog runs synchronously. Clean use of tokio::sync::oneshot for the async bridge.

LGTM ✅

Copy link
Collaborator

@dev01lay2 dev01lay2 left a comment

Choose a reason for hiding this comment

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

Re-review after e2feb58..123d3f3 (2 new commits)

CI: 10/11 green — Docker Recipe E2E still failing (pre-existing identity assertion issue).

Recipe approval & trust model

Substantial and well-designed addition shipping source-based trust, risk classification, and digest-bound approval gating.

Backend (recipe_workspace.rs):

  • RecipeWorkspaceSourceKind (Bundled/LocalImport/RemoteUrl) → RecipeTrustLevel (Trusted/Caution/Untrusted) mapping is clean
  • RecipeRiskLevel (Low/Medium/High) derived from action catalog read_only flags + explicit high-impact action list
  • approval_required_for(source_kind, risk_level) matrix: bundled only needs approval for high-risk, local/remote needs approval for medium+
  • Approval is digest-bound: approve_recipe stores approval_digest, save_recipe_source clears it, so any edit invalidates prior approval. Correct lifecycle.
  • BundledRecipeState (Missing/UpToDate/UpdateAvailable/LocalModified/ConflictedUpdate) with three-way digest comparison (workspace vs seeded vs current bundled) — solid upgrade state machine
  • describe_entries enriches workspace listing with all metadata for the UI
  • Workspace index renamed from BundledSeedIndex to RecipeWorkspaceIndex with source_kind, bundled_version, approval_digest fields. Backward-compatible via #[serde(default)]

Backend (recipe_library.rs):

  • seed_recipe_library no longer auto-upgrades unchanged bundled recipes — instead leaves them as UpdateAvailable for explicit user action. This is the right call for a trust model.
  • upgrade_bundled_recipe validates state before replacing, rejects LocalModified/ConflictedUpdate
  • load_bundled_recipe_descriptors provides the bundled-side metadata for describe_entries
  • workspace_source_kind_for_import correctly maps import source kinds to workspace source kinds

Backend (commands/mod.rs):

  • execute_recipe_with_services now gates on approval before execution — the trust model is enforced at the execution boundary, not just UI
  • New commands: approve_recipe_workspace_source, upgrade_bundled_recipe_workspace_source

Frontend:

  • Recipes.tsx — source/state/risk/approval badges, upgrade button for updateAvailable, conflict hint text
  • Cook.tsx — approval gate with "Approve and continue" button, execution blocked when approval required
  • RecipePlanPreview.tsx — blocking items section separated from attention items, requirements section added
  • Full en/zh localization for all new strings
  • Test coverage updated for both components

Docs:

  • recipe-authoring.md — upgrade rules, trust/approval semantics, review-phase blocking behavior
  • recipe-runner-boundaries.md — trust as execution boundary (not just UI hint)

One NBS: risk_level_for_action_kinds hardcodes the high-risk action list — as the action catalog grows, consider adding a risk field directly to RecipeActionCatalogEntry so the classification lives with the action definition. Not blocking.

LGTM ✅

let catalog = crate::recipe_action_catalog::list_recipe_actions();
let all_read_only = action_kinds.iter().all(|kind| {
catalog
.iter()
Copy link
Collaborator

Choose a reason for hiding this comment

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

NBS: As the action catalog grows, consider adding a risk_level field to RecipeActionCatalogEntry directly rather than maintaining a separate hardcoded list here. Would keep classification co-located with the action definition.

Copy link
Collaborator

@dev01lay2 dev01lay2 left a comment

Choose a reason for hiding this comment

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

Re-review after 123d3f3..979ecb7 (2 new commits)

CI: 10/10 green — notably Docker Recipe E2E now passes, fixing the pre-existing identity assertion issue.

Commit 1: scope cook auth blockers to active recipe

Solid UX fix. Previously the Cook pre-flight surfaced auth issues for all configured model profiles, even ones unrelated to the current recipe. Now buildCookAuthProfileScope extracts the set of profile IDs actually referenced by the recipe plan (from concreteClaims + ensure_model_profile / set_agent_model / create_agent actions), and filterCookAuthIssues drops issues for unrelated profiles and suppresses AUTH_CREDENTIAL_UNRESOLVED for profiles the recipe will auto-create. Test coverage for both.

Commit 2: stabilize recipe docker approval and identity sync

Fixes the Docker E2E failure that trailed through the last ~8 re-reviews. Root cause: set/clear_local_agent_persona always rewrote the full IDENTITY.md from scratch via identity_content, truncating template files without structured fields. Fix: new upsert_persona_content detects structured identity fields and uses upsert_markdown_section to surgically update only the Persona section when they exist, falling back to full rewrite with explicit config defaults otherwise. resolve_identity_explicit_defaults split out cleanly to distinguish config-provided vs inferred name/emoji. Config resolution now checks nested identity.name/emoji with flat field fallback. Two new unit tests, E2E assertions relaxed for format robustness, and approve gate added to the E2E helper.

LGTM ✅

@Keith-CY Keith-CY deployed to development March 13, 2026 10:41 — with GitHub Actions Active
Copy link
Collaborator

@dev01lay2 dev01lay2 left a comment

Choose a reason for hiding this comment

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

Re-review after 979ecb7..98f7150 (1 new commit)

CI: 10/10 green across all platforms + coverage + e2e.

Defer agent workspaces to OpenClaw defaults

Clean, well-scoped refactor that removes the independent flag from agent creation and delegates workspace resolution entirely to OpenClaw's configured defaults.

Backend (commands/agent.rs):

  • create_agent no longer manually constructs the agent JSON entry — delegates to openclaw agents add --non-interactive with the resolved default workspace. The independent param is accepted but ignored (let _ = independent) for backward compat.
  • resolve_openclaw_default_workspace extracts the pattern (check agents/defaults/workspaceagents/default/workspace → first existing agent's workspace) into a reusable helper.

Backend (commands/mod.rs):

  • Materializer refactored: resolve_workspace_for_route split into resolve_openclaw_default_workspace_for_route (always resolves, returns Result<String> not Option) and expand_workspace_for_route (handles ~ expansion for local vs SSH). The independent branch is gone — workspace is always the OpenClaw default.
  • Good test: resolve_openclaw_default_workspace_prefers_defaults_before_existing_agents.

Backend (agent_identity.rs):

  • Identity dir candidate ordering changed: agentDir → fallback_agent_root → workspace (was workspace → agentDir → fallback). This prioritizes the OpenClaw-managed agent dir over workspace for identity lookups, which is correct since identity should live in the agent dir structure.
  • resolve_local_identity_path and resolve_remote_identity_path — creation path now follows workspace → resolve_workspace → agentDir → fallback ordering (distinct from lookup ordering). Ensures new IDENTITY.md files get created in the workspace when it exists.

Frontend:

  • CreateAgentDialog — removed independent/displayName/emoji fields. Persona field gated behind allowPersona prop (only enabled from Channels page). Remote path resolves workspace via config + agent overview with absolute-path preference. Local path calls ua.createAgent directly (Tauri command).
  • actions.tscreate_agent stepToCommands drops independent handling, workspace resolution simplified.
  • Localization cleaned up (removed 4 unused keys in en/zh).

E2E: Docker config updated with explicit defaults.workspace. Assertions relaxed appropriately.

LGTM ✅

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.

2 participants