feat(workflows): M5 — SDK workflow::start + admin run API + pic CLI

The user-facing surface for the durable DAG engine. A workflow run can now be
started three ways, all resolving the workflow by name in the caller's app
(`cx.app_id` / the resolved app — never a passed-in arg, the isolation
boundary), seeding a run the orchestrator advances.

SDK seam (shared):
- `WorkflowService` trait + `NoopWorkflowService` + `WorkflowError` in
  `shared::workflow` (mirrors `InvokeService`); added to `Services` as a
  noop-defaulted `with_workflow` builder (all `Services::new` call sites
  unchanged).
- `WorkflowServiceImpl` (manager-core): resolves + seeds a run; authenticated
  callers gated on `AppInvoke` (anonymous skips, script-as-gate), the same gate
  `invoke()` uses.
- `executor-core::sdk::workflow` — the Rhai bridge `workflow::start(name, input)`
  / `workflow::start(name)`, registered in `register_all`. Returns the run id.

Admin API (manager-core `workflows_api`, mounted in the binary):
- `GET  /apps/{app}/workflows`              — definitions (AppRead)
- `POST /apps/{app}/workflows/{name}/runs`  — start a run (AppInvoke)
- `GET  /apps/{app}/workflows/{name}/runs`  — run history (AppRead)
- `GET  /apps/{app}/workflow-runs/{run_id}` — one run + its steps (AppRead)
  `app_id` scopes every query; a foreign run 404s.

CLI (`pic workflows`): `ls` · `run <name> [--input JSON]` · `runs <name>` ·
`run-status <run_id>` — all `--app`-scoped; run definitions stay declarative
(`[[workflows]]` → `pic apply`).

Also: `list_runs_for_workflow` repo reader.

Tests: `workflow_service_start_seeds_a_run` (DB-gated — the SDK/API-shared
entry) + `workflow_run_executes_end_to_end` CLI journey (apply → `pic workflows
run` → poll `run-status` → succeeded, through the real binary + orchestrator).
fmt + clippy -D warnings clean, 449 manager-core lib tests, 14 orchestrator DB
tests, 18 executor-core lib tests, 3 workflow CLI journeys green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-12 17:48:38 +02:00
parent 74d5874384
commit 5a630e1d9f
16 changed files with 965 additions and 7 deletions

View File

@@ -865,6 +865,26 @@ const RUN_COLS: &str = "id, workflow_id, app_id, status, input, output, error, \
root_execution_id, workflow_depth, parent_run_id, parent_step_id, \
started_at, finished_at, created_at";
/// Most-recent runs of a workflow (newest first), capped at `limit`. Backs the
/// admin/CLI run-history list.
pub async fn list_runs_for_workflow(
pool: &PgPool,
app_id: AppId,
workflow_id: WorkflowId,
limit: i64,
) -> Result<Vec<WorkflowRun>, WorkflowRepoError> {
let rows: Vec<RunRow> = sqlx::query_as(&format!(
"SELECT {RUN_COLS} FROM workflow_runs \
WHERE app_id = $1 AND workflow_id = $2 ORDER BY created_at DESC LIMIT $3"
))
.bind(app_id.into_inner())
.bind(workflow_id.into_inner())
.bind(limit)
.fetch_all(pool)
.await?;
rows.into_iter().map(TryInto::try_into).collect()
}
/// Read a single run by id (app-scoped for isolation).
pub async fn get_run(
pool: &PgPool,