OpenClaw Orchestration Runtime Design
Status: implemented M9 pilot design and rollout reference, extended through M10 (concrete executor, agent-node pilot, capability policy, and flip-readiness runbook).
Issue: #62
Decision
Bloom remains the product and coordination control plane. OpenClaw may become an execution runtime for selected workflow work, but it does not become the source of truth for Bloom's product state, GitHub coordination semantics, accounts, or business invariants.
This keeps the migration incremental: Bloom can route one workflow path through OpenClaw, observe it, and switch back to the built-in runtime without moving the whole SDLC process at once.
Responsibilities That Stay In Bloom
Bloom owns the product model and all user-visible SDLC semantics:
- Product state: requirements, PRD revisions, milestones, tickets, reviews, specialist findings, lifecycle state, and project progress.
- GitHub coordination semantics: issue and PR labels, assignees, reviewer requests, human and agent ownership signals, milestone completion, review gates, merge policy, and dependency-aware delegation.
- Account and session state: dashboard sessions, OAuth identities, Telegram account links, active project routing, and project visibility.
- Project read model: dashboard overviews, project details, SSE project events, status answers, and digest data.
- Business invariants: approval gates, destructive action confirmations, owner opt-ins, review strictness, escalation rules, and rollback authority.
- Inbound durability: webhook verification, fast acknowledgment, durable job enqueue, retry policy, dead-letter behavior, and event-key dedupe.
In short: Bloom decides what should happen and what state changed.
Responsibilities That May Move To OpenClaw
OpenClaw may own bounded execution responsibilities behind an adapter:
- Agent turn dispatch.
- Tool execution for selected workflow nodes.
- OpenClaw session routing.
- Runtime-level retries inside one Bloom job attempt.
- Async task tracking for delegated node runs.
- Provider and tool routing for node execution.
In short: OpenClaw may execute work that Bloom requested, then report the result back to Bloom.
Adapter Boundary
The new seam should sit above the current built-in workflow engine, not inside the webhook routes or GitHub handlers.
Implemented seam:
class OrchestrationRuntime(Protocol):
@property
def name(self) -> str: ...
async def start(self, thread_id: str, seed: dict[str, Any]) -> RunState: ...
async def advance(self, run: RunState) -> RunState: ...
async def resume(self, run: RunState, resume_input: Any = None) -> RunState: ...
def observe(self, run: RunState) -> RuntimeObservation: ...
Initial implementations:
BuiltInOrchestrationRuntime: wraps today'sEngine(spec, WorkflowContext)behavior and remains the default.OpenClawPilotRuntime: disabled-by-default adapter that keeps Bloom's built-in engine responsible for graph traversal and reducers, but routes allowlisted low-risk nodes through an OpenClaw execution adapter while preserving Bloom'sRunStateas the persisted state contract.
The existing Orchestrator continues to own entry points such as user messages,
GitHub PR events, issue events, and project ticks. It calls the configured
runtime to start, advance, or resume workflow state.
Idempotency
Bloom keeps event_key as the source of truth for inbound event dedupe. The
current fence in RunState.processed_keys stays Bloom-owned and is saved with
the run state.
Before invoking OpenClaw, Bloom should persist a correlation record keyed by:
thread_id + event_key + current_node + attempt
The correlation record should include:
- Bloom thread id.
- Bloom job id or source event key.
- Workflow node id.
- Runtime provider.
- OpenClaw session key or execution id.
- Status: pending, running, succeeded, failed, abandoned.
- Safe error metadata when failed.
Duplicate delivery must find the existing correlation and observe or resume it, not create another OpenClaw execution.
The implementation stores these records through RuntimeCorrelationStore, with
InMemoryRuntimeCorrelationStore for development/tests and SupabaseRuntimeCorrelationStore
for the production Postgres backend.
M9 Pilot Runtime
Issue #66 introduces the first OpenClaw-backed workflow execution path:
- Runtime flag:
BLOOM_ORCHESTRATION_RUNTIME=openclaw_pilot. - Allowlist:
BLOOM_OPENCLAW_ORCHESTRATION_PILOT_NODES=track_progress. - Failure mode:
BLOOM_OPENCLAW_ORCHESTRATION_FAILURE_MODE=fallbackorfail_closed.
The chosen node is track_progress, a read-only GitHub status refresh that writes only Bloom's
progress field. All other node types and node ids continue through the built-in
WorkflowContext bindings.
The pilot adapter creates a stable OpenClaw session key per Bloom thread and node, reserves a
runtime correlation record before execution, records success or failure, and reuses cached
successful results for duplicate execution inputs. If OpenClaw execution fails and failure mode is
fallback, Bloom runs the built-in node handler and logs a secret-safe diagnostic. If failure mode
is fail_closed, the runtime raises an operator-visible error instead of silently continuing.
Production defaults remain BLOOM_ORCHESTRATION_RUNTIME=builtin, so existing deployments do not
change behavior until the pilot flag is enabled.
M10-1 Concrete Executor Contract
Issue #82 defines the concrete OpenClaw
runtime execution contract that a real OpenClawNodeExecutor must implement, and adds the minimal
config surface for it. Issue #83 implements
that contract as OpenClawGatewayNodeExecutor/OpenClawExecutorGatewayClient
(bloom/workflow/openclaw_executor.py). Issue #84
wires it into create_app via build_openclaw_node_executor: the concrete executor is used when
BLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_BASE_URL is set, and UnavailableOpenClawNodeExecutor
otherwise (see Settings validation and health below).
Client mechanism
The executor is an HTTP client of OpenClaw Gateway's POST /tools/invoke endpoint - the Gateway's
existing tool-invocation surface (same port as the Gateway, WS+HTTP multiplex, always enabled,
gated by Gateway auth and tool policy). This is deliberately not the OpenAI-compatible
/v1/chat/completions endpoint the OpenClaw LLM provider uses (OpenClawGatewayClient in
bloom/integrations/llm/openclaw_provider.py): node execution is a tool call with a structured
result, not a model completion, and reuses none of the chat/message plumbing.
Request fields
Bloom sends one bounded node-execution request per pilot node run:
{
"tool": "<bloom-to-openclaw tool name for this node>",
"args": { "...": "node input resolved from Bloom state, per node.reads" },
"sessionKey": "bloom:<thread_id>:node:<node_id>"
}
tool: the OpenClaw tool name for the node. The node-to-tool mapping is an explicit, reviewed allowlist owned by the executor implementation (#83) - never inferred from the workflow spec at runtime.args: only the node's declaredreads(see Workflow Spec To OpenClaw Execution Mapping), never full project/account state and never raw credentials.sessionKey: Bloom's existing stable session key (bloom:<thread_id>:node:<node_id>, already computed inopenclaw_runtime.py), reused for OpenClaw session continuity.dryRunis not used by Bloom (the Gateway currently ignores it).
Response fields
200 {"ok": true, "result": {...}}-resultbecomes the dict of Bloom-compatible writes returned fromOpenClawNodeExecutor.run_tool; Bloom validates and applies it exactly as it would the built-in tool handler's return value.200 {"ok": false, "error": {"type": "...", "message": "..."}}, or a non-2xx HTTP status - both raiseOpenClawNodeExecutionError. Bloom logs only the safe error type/class, never the raw message body, since it could echo request args.
Idempotency behavior
OpenClaw's /tools/invoke endpoint has no built-in request-level idempotency key (unlike the
Gateway's side-effecting RPC methods such as send/agent). Idempotency is enforced entirely on
Bloom's side, as already implemented in _OpenClawPilotContext._run_openclaw_tool:
- Bloom reserves a
RuntimeCorrelationrow keyed by a hash of the node's resolved input (_idempotency_key) before calling the executor. - A cached
succeededcorrelation for the same key short-circuits the call entirely - the executor is never invoked twice for the same input. - The executor itself must be safe to call at most once per reservation; it does not need its own
idempotency key parameter because Bloom never re-issues a call for a
succeededcorrelation.
Timeout behavior
The executor's HTTP client uses BLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_TIMEOUT_SECONDS (default
30s) as the request timeout. A timeout is treated the same as any other executor failure: it
raises an exception that _OpenClawPilotContext._run_openclaw_tool catches, records the
correlation as failed, and then either falls back to the built-in handler (fallback mode) or
raises OpenClawNodeExecutionError (fail_closed mode). A timeout is never silently retried by
the executor itself - Bloom's own job-retry policy (BLOOM_JOB_MAX_ATTEMPTS) is the only retry
boundary above it.
Auth posture
BLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_API_KEY, when set, is sent as Authorization: Bearer <key>
on every request, matching OpenClaw's Gateway shared-secret auth. Per OpenClaw's own
/tools/invoke documentation, a valid Gateway bearer credential is a full operator-level
credential for that gateway instance, not a narrow per-tool scope - so:
- the executor config is separate from the LLM-provider's
BLOOM_OPENCLAW_API_KEY/BLOOM_OPENCLAW_BASE_URL(see "Config separation" below); a leaked or overscoped LLM-provider key must not also grant orchestration-executor access, and vice versa. - the key is never logged, never returned in
/health, and never included in issue/PR text. - the preferred deployment boundary stays private (loopback or Tailscale/tailnet), matching the existing "Authorization" section above.
Unsupported-node behavior
The executor is only ever called for nodes in BLOOM_OPENCLAW_ORCHESTRATION_PILOT_NODES; the
runtime seam (_OpenClawPilotContext.run_tool) routes every other node id through the built-in
WorkflowContext unconditionally, and human/event_wait/subworkflow node types and
GitHub/Telegram-mutating tools stay built-in per
Unsupported Or Built-In-Only Behavior.
The executor implementation must reject (raise, never silently no-op) a request for a node it does
not recognize; Bloom's fallback/fail-closed handling is the only path that decides what happens
next.
Config separation
| Concern | LLM-provider config (BLOOM_LLM_PROVIDER=openclaw, M8) | Orchestration-runtime executor config (M10-1) |
|---|---|---|
| Selects | which backend structured() calls route through | which backend allowlisted workflow nodes execute through |
| Base URL | BLOOM_OPENCLAW_BASE_URL | BLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_BASE_URL |
| API key | BLOOM_OPENCLAW_API_KEY | BLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_API_KEY |
| Timeout | fixed in OpenClawGatewayClient (60s) | BLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_TIMEOUT_SECONDS (default 30s) |
| Required when | BLOOM_LLM_PROVIDER=openclaw | BLOOM_ORCHESTRATION_RUNTIME=openclaw_pilot and BLOOM_OPENCLAW_ORCHESTRATION_FAILURE_MODE=fail_closed |
| Consumed by | bloom.integrations.llm.factory / OpenClawLLMProvider | the concrete OpenClawNodeExecutor (issue #83) |
These two configs may point at the same physical Gateway, but Bloom never reads one to satisfy the other - each is validated and consumed independently, so a change to one can never silently change the other's behavior.
M10-4 Execution Harness
Issue #85 adds an integration harness that
runs the track_progress workflow path through OpenClawPilotRuntime with the concrete executor
(#83/#84) rather than a fake executor that bypasses it. The reusable rig lives in
bloom/workflow/openclaw_track_progress_harness.py; the tests are in
tests/integration/test_openclaw_track_progress_harness.py.
The harness wires OpenClawGatewayNodeExecutor/OpenClawExecutorGatewayClient to a caller-supplied
httpx.AsyncClient, so the exact same code path can be driven two ways:
- CI (default): the client's transport is
httpx.MockTransport, so the harness proves request shape, response validation, idempotency-key reuse, cached-success reuse, and Bloom's own reducer application with no real network and no credentials. - Optional live check: pointing the client at a real local OpenClaw gateway's base URL, gated
behind
BLOOM_OPENCLAW_EXECUTOR_LIVE_TEST=1(see apps/api/README.md). Never runs in CI; both the enabling env var and the gateway base URL must be set explicitly.
What the harness proves:
- Request shape: the
POST /tools/invokebody'stool,sessionKey, andargs(threadId,idempotencyKey, boundedinput) match the Request fields contract above. - Idempotency key reuse and cached-success reuse: a second run with identical resolved input
reuses the
RuntimeCorrelationreserved by the first run and never calls the gateway again (see Idempotency above). - Validated writes: only the executor's validated
result(per_validate_node_response) reaches Bloom state. - Bloom-owned reducer application: the write lands in run state through the field's declared
reducer (
progressusesmerge, matching the real workflow spec), viaEngine.advance'sapply_writes- not a passthrough assignment. - Correlation status lifecycle:
openclaw_runtime.node_started(running) is followed by eitheropenclaw_runtime.node_succeeded(succeeded) oropenclaw_runtime.node_failed(failed); a duplicate run for the same input instead logsopenclaw_runtime.cached_resultwithout transitioning the correlation again. - Fallback regression: a concrete-executor tool failure or a malformed gateway response both
still reach the built-in
track_progresshandler infallbackmode, and both still raiseOpenClawNodeExecutionErrorinfail_closedmode - unchanged from the M9 pilot's fallback tests intests/unit/test_runtime.py.
Settings validation and health
Settingsfails fast (at construction/startup, not at first node execution) whenBLOOM_ORCHESTRATION_RUNTIME=openclaw_pilotandBLOOM_OPENCLAW_ORCHESTRATION_FAILURE_MODE=fail_closedare set withoutBLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_BASE_URL. The error names the missing variable so an operator can fix it without reading source.fallbackmode never requires this config - it remains deployable with no OpenClaw executor present at all, matching the existing pilot rollout guidance above./health'sorchestration.concrete_executorobject reportsconfigured(whetherBLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_BASE_URLis set) andavailable(whether the runtime's wired executor actually reports itself available). Since #84,create_appbuildsOpenClawGatewayNodeExecutorwheneverconfiguredis true, soavailabletracksconfiguredin everyopenclaw_pilotdeployment;configured=true, available=falsecannot happen (fail_closedwithout config failsSettingsconstruction instead, andfallbackwithout config falls straight toUnavailableOpenClawNodeExecutor, which reportsconfigured=false, available=false). Neither theconfigurednoravailablefield, nor any other/healthoutput, ever exposes the executor base URL or API key.
M10-5 State-Only Agent Node Pilot
Issue #86 extends the OpenClaw pilot from
tool nodes only to one state-only agent node, so OpenClaw is not merely a read-only tool shim.
Bloom's built-in engine still owns graph traversal, business gates, and reducer application; the
pilot node's OpenClaw execution still returns nothing but a validated dict of Bloom writes.
Selected node and why it is safe
prd_authoring is the pilot agent node. See
Current Bloom SDLC Node Classification
for the full rationale; in short:
- It writes exactly one state field (
prd), neverconversationorinbound_message. - It never sends anything to the user - the downstream
request_prd_approvalhuman node (always built-in) is what turnsprd.summary_for_userinto an outbound Telegram message, gated by Bloom's own approval semantics. - It has no external side effects (no GitHub/Telegram calls).
Broader capability policy/safety gates beyond this single-node pilot are out of scope for this issue (tracked separately, issue #87).
Routing: explicit by node id, never by node type
OpenClawPilotRuntime now takes a second allowlist, pilot_agent_nodes, alongside the existing
pilot_nodes (tool allowlist). _OpenClawPilotContext.run_agent checks node.id against
pilot_agent_nodes before ever calling the executor - exactly mirroring run_tool's existing
pilot_nodes check, never inferring routeability from node.type == "agent" alone. An agent node
not named in the allowlist keeps using the built-in WorkflowContext.run_agent binding
unconditionally, the same as any other unsupported node.
Both run_tool and run_agent now share one _run_openclaw_node helper in
bloom/workflow/openclaw_runtime.py for correlation reservation, idempotency-key
computation/reuse, structured logging, and fallback/fail-closed dispatch - the only thing that
differs between the two call sites is which executor method and which built-in delegate method is
invoked.
Config surface:
BLOOM_OPENCLAW_ORCHESTRATION_PILOT_AGENT_NODES(default empty - unlike the tool allowlist, no agent node is piloted out of the box; an operator must opt one in explicitly).- The same
BLOOM_OPENCLAW_ORCHESTRATION_FAILURE_MODE(fallback/fail_closed) governs agent-node failures identically to tool-node failures.
Executor: same /tools/invoke transport, a second allowlist
OpenClawGatewayNodeExecutor gains a run_agent method that posts to the same
POST /tools/invoke endpoint as run_tool - node execution is "a tool call with a structured
result" regardless of the Bloom node's type (see the M10-1 contract above). It is checked against
a separate, explicit allowlist, DEFAULT_NODE_AGENT_MAP ({"prd_authoring": "prd_authoring"} by
default), so a node in the tool allowlist can never be reached via run_agent or vice versa. An
unrecognized node id is rejected with OpenClawNodeExecutionError before any request is made,
exactly like the existing tool-node rejection.
The request-building and response-validation code is shared with run_tool (_invoke,
_build_args, _validate_node_response):
- Bounded input: only
node.readsresolved from state (resolve_node_reads), never full project/account state - identical to the tool path. - Expected structured output:
args.outputSchemais now resolved from either a top-levelnode.config.outputSchemaor, since the real workflow spec puts an agent node's schema under nestedconfig.output.schema(seeprd_authoring's node config inworkflows/bloom-sdlc.workflow.json), that nested shape - so the request carries the same schema Bloom's built-in agent binding expects the LLM to satisfy. - Secret filtering: the same key-name-based secret filter (
_without_secret_like_dict_keys) applies to agent-node args as it does to tool-node args. - Output validation: a non-dict
result, or anyresultkey whose top-level field is not innode.writes, is rejected withOpenClawNodeExecutionErrorbefore Bloom ever applies a reducer - identical to the tool path's write-key allowlist enforcement.
Fallback/fail-closed behavior
Unchanged from the tool-node pilot: on an agent-node OpenClaw failure (network error, invalid
response, unexpected write key, etc.), fallback mode calls the built-in WorkflowContext.run_agent
binding (e.g. the real ProductOwnerAgent.author_prd path in Orchestrator) and logs the failure;
fail_closed raises OpenClawNodeExecutionError instead. Side-effecting and unsupported node ids -
human, event_wait, GitHub/Telegram-mutating tool nodes, and every other agent node not
explicitly named in BLOOM_OPENCLAW_ORCHESTRATION_PILOT_AGENT_NODES - are never routed through
OpenClaw by this change.
Tests
tests/unit/test_runtime.py: agent-node routing, correlation recording, allowlist-miss regression (agent node present but not allowlisted stays built-in), fallback, and fail-closed.tests/unit/test_openclaw_executor.py: agent-node success and request shape (bounded input, nested output schema, secret-safe args), unknown-agent-node rejection, tool-allowlist-does-not- satisfy-agent-routing regression, non-dict result rejection, unexpected write key rejection.tests/integration/test_openclaw_agent_pilot.py: an offline (httpx.MockTransport) integration test provingprd_authoringruns end to end throughOpenClawPilotRuntime->OpenClawGatewayNodeExecutor->OpenClawExecutorGatewayClient, that Bloom applies the validated write through theprdfield'smergereducer, and that invalid structured output/failure both still fall back (or fail closed) exactly like the tool-node pilot.
M10-6 Capability Policy And Safety Gates
Issue #87 adds a centralized, code-owned
capability policy (bloom/workflow/openclaw_policy.py, OpenClawCapabilityPolicy) so that
enabling OpenClaw orchestration - or misconfiguring the operator allowlist - cannot silently route
an unsupported or side-effecting workflow node through OpenClaw. This is a third, independent
gate, above OpenClawPilotRuntime's existing pilot_nodes/pilot_agent_nodes allowlist (config)
and below OpenClawGatewayNodeExecutor's own DEFAULT_NODE_TOOL_MAP/DEFAULT_NODE_AGENT_MAP
(executor). All three must independently allow a node before it ever reaches OpenClaw:
configured allowlist (Settings) -> capability policy (this section) -> executor allowlist
What the policy checks
For every node opted into pilot_nodes/pilot_agent_nodes, _OpenClawPilotContext calls
OpenClawCapabilityPolicy.evaluate_tool/evaluate_agent before any executor call. The policy
checks, in order:
- Node type - only
toolandagentnodes are ever routable;human,event_wait,map,router,passthrough,terminal, andsubworkfloware always rejected, matching Unsupported Or Built-In-Only Behavior. - Side-effect posture - a
toolnode whose declaredconfig.integration/config.actionnames a known side-effecting integration (telegram,deploy,ci) or GitHub-mutating action (issue/milestone create-or-close, PR review publication, merge) is rejected outright, regardless of node id. - The policy's own hard-coded node set (
POLICY_ALLOWED_TOOL_NODES,POLICY_ALLOWED_AGENT_NODES) - exactlytrack_progressandprd_authoring, the full M10 node set (see Exact M10 Routable Node Set below). Changing this set means editingopenclaw_policy.pyand its tests, never a runtime config value. - The operator-configured allowlist - a node must be in both the hard-coded set and
Settings.openclaw_orchestration_pilot_node_names/..._pilot_agent_node_namesto route. - Output validation - the node's declared
writesmust be a subset of the fields the policy allows that node id to write (track_progress->progress,prd_authoring->prd), andtrack_progress's declaredconfig.integration/config.actionmust still match the read-only pair the policy was written against (github/list_project_status).
Fail-closed vs. fallback on policy rejection
A node not in the configured allowlist at all was never a routing candidate, so it silently uses
the built-in binding - unchanged M9/M10-5 behavior, no new logging. A node that is configured but
fails the policy is a genuine, actionable event: _OpenClawPilotContext logs
openclaw_policy.rejected (node id, node type, decision class, and a static, secret-safe reason -
never state or input) and then applies the same BLOOM_OPENCLAW_ORCHESTRATION_FAILURE_MODE
semantics as an executor failure - fallback runs the built-in handler, fail_closed raises
OpenClawNodeExecutionError before any executor call. Silent fallback is never allowed for a
policy rejection.
Observability
OpenClawPilotRuntime.policy_summary (and /health's orchestration.policy field) expose the
policy's hard-coded and configured node-id allowlists, the last decision (node id, node type,
allowed, decision class), and a count per decision class - node ids and decision classes only,
never node state, input, or secrets.
Exact M10 Routable Node Set
At the end of M10, exactly these two nodes may route through OpenClaw (and only when also present
in the operator's BLOOM_OPENCLAW_ORCHESTRATION_PILOT_NODES/..._PILOT_AGENT_NODES):
track_progress(tool): read-only GitHub status refresh, writes onlyprogress.prd_authoring(agent): state-only PRD draft, writes onlyprd.
Every other node in workflows/bloom-sdlc.workflow.json remains built-in-only for M10, enforced by
the capability policy regardless of any future config change:
human:ask_clarification,request_prd_approval.event_wait:await_message,await_project_event.agent(not policy-allowlisted):requirement_discovery,milestone_planning,ticket_generation,review_one_pull_request,gather_feedback.passthrough:freeze_prd.map:sync_tickets,review_pull_requests.tool(side-effecting):sync_milestones(GitHub milestone create),create_issue_for_ticket(GitHub issue create),notify_planning_ready,notify_review(Telegram notify),deploy(deployment trigger),run_tests(CI trigger).
Implementing any newly allowed side-effecting action, or expanding the M10 routable set, is out of
scope for this issue and requires a deliberate change to openclaw_policy.py plus new tests.
M10-7 Flip-Readiness Runbook
Issue #88 adds a machine-checkable readiness
gate and runbook, run before an operator switches a real environment's
BLOOM_ORCHESTRATION_RUNTIME to openclaw_pilot - especially before
BLOOM_OPENCLAW_ORCHESTRATION_FAILURE_MODE=fail_closed, where the built-in fallback no longer
absorbs a misconfiguration. This is the last of the M10 issues (#82-#88): the concrete executor
(M10-1..M10-4), the second pilot node (M10-5), and the capability policy (M10-6) are all
prerequisites this check verifies are actually wired together correctly for the environment about
to be flipped, not just individually correct in isolation.
The command is bloom.workflow.openclaw_readiness (bloom/workflow/openclaw_readiness.py), run
via uv run python -m bloom.workflow.openclaw_readiness. It builds the exact Settings/
create_app wiring the target process would boot with - in-process, via TestClient, no separate
server - and checks, in order:
Settingsconstructs without error (exercising the existing fail_closed-without-executor-url guard inbloom/config.py).- The configured
pilot_nodes/pilot_agent_nodesallowlists are each a subset ofPOLICY_ALLOWED_TOOL_NODES/POLICY_ALLOWED_AGENT_NODES(openclaw_policy.py) - the same check the capability policy itself performs at runtime, run here ahead of time. - For
fail_closed: the executor base URL is configured (redundant with #1's guard, but reported as its own named check for a clear pass/fail line). GET /health(in-process) reportsstatus: ok.orchestration.active_runtime == "openclaw_pilot"andopenclaw_enabled == true.orchestration.concrete_executor.configuredand.availableare bothtrue.orchestration.correlation_store.healthyistrue.orchestration.policy'sconfigured_tool_nodes/configured_agent_nodesmatch the settings used to build the app - catching stale runtime wiring, not just stale config.- The offline
track_progressexecution harness (openclaw_track_progress_harness.py, M10-4) runs end to end through the realOpenClawPilotRuntime->OpenClawGatewayNodeExecutor->OpenClawExecutorGatewayClientcode path, viahttpx.MockTransport- no live gateway or network access. - The same harness against the real configured executor base URL.
- Required by default for
openclaw_pilot+fail_closed./health'sconcrete_executor.availablereflects wiring/config only, not real reachability, so it is not sufficient on its own - a fake or unreachable executor URL must (and now does) reportNOT READY.--skip-live-gateway(alias--offline-only) is the explicit escape hatch for CI/docs dry runs: it is always visibly marked in the report (offline_only_simulation: true) and in the final banner, and it never approves a fail_closed production flip. - Everywhere else (
fallbackmode, orbuiltinruntime), it stays optional and explicitly marked as such in the output: opt in withBLOOM_OPENCLAW_EXECUTOR_LIVE_TEST=1together with an executor base URL - mirrors the M10-4 harness test file's own opt-in live check.
- Required by default for
--runtime/--failure-mode/--pilot-nodes/--pilot-agent-nodes/--executor-base-url flags
override the corresponding Settings field for that invocation only (never written to .env or
the environment), so the same command can preview a flip against a target environment's real
config without changing it, or validate whatever is already configured when run with no flags.
The command exits 0 only when every required check passes and 1 otherwise, prints one
[OK]/[FAIL]/[WARN] line per check, and supports --json for a machine-readable report. No
check output ever includes an API key, gateway URL, prompt, or model response - see
tests/integration/test_openclaw_readiness.py's test_no_secret_ever_appears_in_the_report for
the regression test. See
docs/deployment.md for the full operator-facing
runbook (staged rollout, production enablement, and log events to watch).
Webhook Acknowledgment Timing
Telegram and GitHub webhook routes should continue to:
- Verify the inbound request.
- Map it to a project thread when needed.
- Enqueue a durable Bloom job.
- Return quickly.
OpenClaw execution starts only after the Bloom job runner claims the durable job. This preserves the current fast-ack behavior and avoids coupling third-party webhook delivery to OpenClaw availability.
Authorization
The OpenClaw gateway token is an operator-level credential. Bloom may hold it only in the backend process and must never expose it to the dashboard, browser, issue bodies, logs, or PR comments.
The preferred deployment boundary is private:
- loopback when Bloom and OpenClaw run on the same host,
- Tailscale or equivalent private network when they are separate,
- no public unauthenticated OpenClaw endpoint.
Future per-user OpenClaw authorization can be added later, but M9 should treat OpenClaw as a trusted backend runtime called by Bloom.
Observability
Runtime execution should emit structured, secret-safe metadata:
- runtime provider,
- Bloom thread id,
- Bloom job id,
- workflow node id,
- event key or stable hash,
- OpenClaw session key or execution id,
- status,
- duration,
- retry count,
- safe error class and message.
Do not log prompts, model responses, tokens, API keys, gateway tokens, or raw third-party payloads that may contain sensitive user data.
Dashboard and health surfaces should make the active runtime visible and show whether OpenClaw correlation storage is healthy.
The M9 implementation exposes this through /health:
- configured runtime,
- active runtime,
- whether OpenClaw orchestration is enabled,
- pilot node allowlist,
- failure mode,
- correlation-store backend and health.
The pilot runtime logs node lifecycle events with runtime, thread id, node id, OpenClaw session key, idempotency key, status, fallback decision, and safe error class. It does not log OpenClaw gateway credentials, prompts, responses, or state payloads.
Rollback
The default runtime remains builtin.
Runtime selection should be controlled by configuration and should allow Bloom to switch back to the built-in runtime without a schema migration. If a rollback happens while OpenClaw work is in flight, Bloom should mark those correlations as abandoned or ignored, then resume from persisted Bloom state through the built-in runtime.
Rollback must never duplicate GitHub side effects blindly.
Implemented rollback control:
BLOOM_ORCHESTRATION_RUNTIME=builtin
Switching back to builtin routes every workflow node through Bloom's built-in
runtime and leaves runtime-correlation rows as audit records. No schema migration
is required.
Incremental Migration Plan
Each step is stoppable and should land as its own issue-sized PR.
- Document this boundary design.
- Map Bloom workflow node types to OpenClaw runtime capabilities and identify unsupported node behavior that must remain built-in. See Workflow Spec To OpenClaw Execution Mapping.
- Add the
OrchestrationRuntimeseam and wireBuiltInRuntimeas the default, with no behavior change. - Add OpenClaw correlation persistence and idempotency records.
- Add an
OpenClawRuntimeadapter behind a disabled-by-default feature flag. Start with tests and fake OpenClaw responses before any production path. - Pilot one low-risk, non-mutating workflow path through OpenClaw, then add health checks, metrics, and rollback controls.
- Reconcile PRD, architecture docs, workflow docs, and operations runbooks.
- Define the concrete OpenClaw executor contract and its config surface (M10-1, this document's Concrete Executor Contract section). Building the real gateway-backed executor against that contract is a separate, later issue (#83).
- Build the gateway-backed
OpenClawNodeExecutorimplementation against the M10-1 contract (M10-2, issue #83):OpenClawGatewayNodeExecutorandOpenClawExecutorGatewayClientinbloom/workflow/openclaw_executor.py. - Wire the concrete executor into
create_appbehind a config-driven factory, keepingUnavailableOpenClawNodeExecutorfor the no-configfallbackcase, and expose its real availability through/health(M10-3, issue #84). - Add an integration harness that proves
track_progressend-to-end through the concrete executor - request shape, idempotency/cached-success reuse, validated writes, Bloom's reducer application, and unchanged fallback behavior - plus an opt-in live-gateway check (M10-4, issue #85, this document's Execution Harness section). - Extend the pilot to one state-only
agentnode (M10-5, issue #86):prd_authoring, routed explicitly by node id through a second, opt-in allowlist, with the same bounded-input, validated-output, fallback/fail-closed semantics as the tool-node pilot (this document's State-Only Agent Node Pilot section). - Add a centralized capability policy and safety gates (M10-6, issue #87):
OpenClawCapabilityPolicygates node type, side-effect posture, the policy's own hard-coded node set, the configured allowlist, and declared output/writes before any executor call - see this document's Capability Policy And Safety Gates section. Enabling any newly allowed side-effecting action is out of scope and left for a later issue. - Add a flip-readiness command and operator runbook (M10-7, issue #88): a machine-checkable
gate that verifies config,
/health, executor availability, correlation-store health, and an OpenClaw execution harness before an operator switches an environment toopenclaw_pilot- see this document's M10-7 Flip-Readiness Runbook section and docs/deployment.md. M10 is complete as of this issue; the recommended next expansion candidate is noted in Pilot Path Recommendation below and in docs/milestones/m10-openclaw-concrete-executor-and-safety-gates.md.
Pilot Path Recommendation
The first production-adjacent pilot should be read-only or advisory. Avoid initially routing GitHub-mutating nodes such as issue creation, label updates, PR review publication, milestone closure, or merge decisions through OpenClaw.
The implemented first pilots are:
track_progress: a read-only GitHub status refreshtoolnode that writes Bloom'sprogressfield.prd_authoring: a state-onlyagentnode (M10-5) that writes only Bloom'sprdfield and never messages the user directly.
Good later candidates:
- a status or monitoring answer path that only reads Bloom and GitHub state,
- an advisory specialist-review draft before it is published,
- a fake-runtime integration test path that exercises agent dispatch without external side effects.
The pilot should prove routing, correlation, retries, and observability before OpenClaw is allowed to execute side-effecting workflow nodes.
Non-Goals For M9
- Moving webhook routing into OpenClaw.
- Moving account/session storage into OpenClaw.
- Making OpenClaw the product state store.
- Removing the built-in runtime.
- Routing all workflow nodes through OpenClaw at once.
- Exposing the OpenClaw gateway credential to clients.