From d20d3b08fafee1dd1f9d57f573517b9c61033b45 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Mon, 20 Jul 2026 10:55:21 +0100 Subject: [PATCH] docs: add Server Workflows design spec --- .../2026-07-20-server-workflows-design.md | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-20-server-workflows-design.md diff --git a/docs/superpowers/specs/2026-07-20-server-workflows-design.md b/docs/superpowers/specs/2026-07-20-server-workflows-design.md new file mode 100644 index 0000000..1db78d9 --- /dev/null +++ b/docs/superpowers/specs/2026-07-20-server-workflows-design.md @@ -0,0 +1,238 @@ +# Server Workflows — Design + +**Date:** 2026-07-20 +**Status:** Approved (design) — ready for implementation planning +**Scope:** Server Workflows only. Fleet Inventory and SaaS/local-auth are separate sub-projects with their own specs. + +Approved UI mockup: three-pane builder (Step Library · Canvas · Inspector), env vars shown riding the wire between nodes. + +--- + +## 1. Summary + +Let operators compose **reusable shell steps** (Bash or PowerShell) into **workflows** and run them across many managed servers in parallel. Steps pass data to later steps through a `$WORKFLOW_ENV` file (GitHub-Actions style). Every run is recorded with full per-step logs. Steps can reference org secrets, injected as environment variables at runtime. + +Builds directly on the existing `CommandStream` gRPC infrastructure (`dispatch.go`, `ServerCommand` oneof, agent command loop). + +--- + +## 2. Locked decisions + +| Topic | Decision | +|-------|----------| +| Data passing | Implicit. Every step's `$WORKFLOW_ENV` outputs merge into the run's env and are exposed to **all** later steps as `$KEY`. No explicit port wiring. | +| Failure model | Per-step policy: `stop` (default), `continue`, `retry` (with max attempt count). | +| Targets | Fan-out. Same step sequence runs on N target servers **in parallel**. Steps within one server run **sequentially**. | +| History/logs | Every run persisted: status, timing, per-server per-step stdout/stderr/exit code, captured output env. | +| Secrets | Steps declare needed secret keys; resolved from existing `secrets` store and injected as env vars at exec time. Never persisted into run logs. | +| Testing | **Skipped** for this iteration per request. No test files written. | + +--- + +## 3. Data model (MongoDB) + +### `workflow_steps` — reusable step library +```json +{ + "_id": "ObjectId", + "step_id": "uuid", + "name": "Restart service", + "description": "Restart-Service by name, wait ready", + "interpreter": "bash | powershell", + "script": "Restart-Service vantage-api\n...", + "declared_outputs": ["STARTED_AT"], // documentation/UI hints; not enforced + "secret_refs": ["DEPLOY_TOKEN"], // secret keys this step needs injected + "org_id": "uuid", // for future multi-tenant; single-org for now + "created_at": "ISODate", + "updated_at": "ISODate" +} +``` + +### `workflows` — ordered composition +```json +{ + "_id": "ObjectId", + "workflow_id": "uuid", + "name": "Deploy & Restart API", + "target_server_ids": ["uuid", "uuid"], + "steps": [ + { + "step_id": "uuid", // reference to library step + "order": 0, + "on_failure": "stop | continue | retry", + "max_retries": 0, // used when on_failure = retry + "overrides": { // optional local fork of the library step + "script": null, + "secret_refs": null + } + } + ], + "created_at": "ISODate", + "updated_at": "ISODate" +} +``` +Editing a library step from the Inspector writes an `overrides` block on that workflow step (a local fork) rather than mutating the shared step. + +### `workflow_runs` — execution records +```json +{ + "_id": "ObjectId", + "run_id": "uuid", + "workflow_id": "uuid", + "workflow_snapshot": { }, // frozen copy of workflow + resolved steps at trigger time + "status": "running | success | failed | cancelled", + "triggered_by": "user-id", + "started_at": "ISODate", + "finished_at": "ISODate | null", + "server_runs": [ + { + "server_id": "uuid", + "status": "queued | running | success | failed | skipped", + "started_at": "ISODate | null", + "finished_at": "ISODate | null", + "run_env": { "VERSION": "a1b9f0" }, // accumulated non-secret output env + "steps": [ + { + "order": 0, + "name": "Git pull & build", + "status": "success | failed | running | queued | skipped", + "attempts": 1, + "exit_code": 0, + "stdout": "…", + "stderr": "…", + "output_env": { "VERSION": "a1b9f0" }, + "started_at": "ISODate", + "finished_at": "ISODate" + } + ] + } + ] +} +``` +Secret values are never written to `stdout`/`stderr`/`run_env` by us; masking of known secret values in captured output is applied before persistence. + +--- + +## 4. gRPC protocol changes (`proto/vantage/v1/vantage.proto`) + +### New command in the `ServerCommand` oneof +```protobuf +message RunStepCmd { + string interpreter = 1; // "bash" | "powershell" + string script = 2; + map env = 3; // inputs = accumulated run env + injected secrets + int32 timeout_seconds = 4; +} +``` +Add `RunStepCmd run_step = 6;` to the `ServerCommand` oneof. + +### Richer result — new `AgentMessage` payload +Current `CommandResult{command_id, success, message}` is too thin. Add a dedicated step result: +```protobuf +message StepResult { + string command_id = 1; + int32 exit_code = 2; + string stdout = 3; + string stderr = 4; + map output_env = 5; // parsed $WORKFLOW_ENV KEY=value lines +} +``` +Add `StepResult step_result = 5;` to the `AgentMessage` oneof (alongside existing `ready` / `result`). + +--- + +## 5. Agent execution (`agent/internal/...`) + +New handler for `RunStepCmd` in the agent command loop: + +1. Create a temp dir; create empty `WORKFLOW_ENV` file inside it. +2. Write `script` to a temp script file. +3. Build the process environment: inherited env + `cmd.env` (run env + secrets) + `WORKFLOW_ENV=`. +4. Execute: + - `bash` → `bash