16 KiB
Workflow Console Capability Playground Design
Date: 2026-08-11
Status: Approved design for Workflow Console Slice 4.
Related:
Goal
Let an operator inspect and directly call one discovered workflow capability
from the console without first creating a draft, artifact, deployment, or run.
The same slice makes the shared schema form understand self-contained local JSON
Schema references so capabilities such as wf.source.read_resource have a real
generated form instead of an unresolved-$ref fallback.
A direct capability call is an authoring and runtime smoke test. It is not a workflow execution: it has no workflow graph, state, routes, persisted run record, or trace frames.
Product Contract
The existing Discover route remains the capability catalog. Selecting a capability opens a detail surface with two views:
- Contract shows identity, source, outcomes, schemas, and wrapper hints; and
- Try capability shows a generated input form, wrapper deployment context when applicable, an explicit call action, and the latest result or failure receipt.
The call action must be labelled as immediate execution. A capability may have external side effects even though the console calls it outside a workflow. The console never invokes a capability on selection, tab change, reconnect, or form change, and it never retries a failed call automatically.
Before calling, the operator acknowledges that the selected capability executes immediately and may have side effects. The acknowledgement resets when the selected capability or connected target changes. This is an accidental-action guard, not an authorization mechanism.
The result surface remains visible until the operator calls again, selects a different capability, or disconnects. It distinguishes:
- input validation errors before any operation is sent;
- a call currently in flight;
- a completed capability outcome and output, including
runtime_error; - returned dependency diagnostics; and
- a rejected transport, protocol, or decoding operation.
Every attempted call also records the existing operation evidence. The inline receipt is the primary feedback for the action; the global evidence ledger is the detailed protocol history, not the only place an operator can discover a failure.
Direct Call Semantics
The console uses the existing workflow.capabilities.call operation with:
qualified_name: the selected capability name;payload: the generated form's JSON object; and- optional
deployment_id: explicit deployment context for saved wrappers.
The optional deployment id is an advanced text field for wrapper artifacts in this slice. Node-spec calls neither display nor send it because the current API does not apply deployments to direct node-spec calls. Deployment discovery and selection are not duplicated inside Discover. Empty deployment text is omitted rather than sent as an empty string.
Direct node-spec calls receive the API's bounded smoke-test runtime context, not the complete context of a prepared deployment run. A capability that depends on runtime-only context may therefore return a diagnostic even when its input is valid. The playground presents this result truthfully and directs operators to a deployment run when full workflow context is required.
For example, this slice fixes the generated form for
wf.source.read_resource, but a direct call still returns runtime_error
because the current node-spec call path does not provide platform context.
Reading the resource successfully remains a prepared workflow/deployment-run
operation unless the backend call contract is deliberately expanded later.
The interpreted result preserves:
- qualified capability name;
- source id;
- capability kind;
- deployment id, if any;
- declared outcome;
- structured output or
null; and - dependency diagnostics.
Output is presented as a bounded, scrollable structured JSON preview with the outcome and provenance as readable facts. The existing evidence ledger retains the request/response exchange and equivalent CLI. This slice does not invent a second output-schema renderer or truncate the transport contract itself.
A handler exception is currently represented by a successful RPC response with
outcome: "runtime_error", output: null, and an error diagnostic. The
playground treats that as a completed call with a failed capability outcome,
not as a transport rejection. Outcomes are otherwise capability-defined; the
UI does not assume that every successful outcome is literally named ok.
The RPC method registry marks capability calls as writes because arbitrary capabilities can mutate external systems. This prevents accidental reuse of read-only assumptions and keeps the browser allowlist decision explicit.
Local JSON Schema References
The shared schema normalization module gains a resolver for references within
the same schema document. Callers continue to pass one schema to
normalizeSchema; reference lookup, recursion protection, and fallback reasons
stay behind that interface.
This resolver applies only to dynamic capability schemas projected into console
forms. It does not change the generated RPC contract translator: capability
input_schema and output_schema remain opaque JSON objects at the transport
boundary, while the generated call payload and result use their existing
checked transport schemas.
Supported references are local JSON Pointers beginning with #/, including
the common #/$defs/Name and legacy #/definitions/Name forms. Pointer tokens
decode JSON Pointer escapes (~0 and ~1). References may be nested and may
target objects, arrays, scalar fields, or enums.
Resolution is fail-closed:
- unresolved pointers use the existing raw-JSON fallback with an exact reason;
- external URIs and non-pointer anchors remain unsupported;
- recursive reference cycles stop with a bounded cycle error rather than recursing indefinitely; and
- structural siblings beside
$refthat would require JSON Schema intersection semantics remain unsupported.
Annotation siblings used by the form, such as title, description, and
default, may decorate the resolved field without changing its structural
contract. The raw-schema disclosure continues to show the original schema,
including $defs; resolution is only the form projection.
The acceptance fixture is the actual shape returned by
wf.source.read_resource: its ref property points to
#/$defs/SourceResourceRef, and the form must expose logical_source, uri,
the defaulted kind discriminator, and the optional resource metadata fields.
Module Design
The modules remain separated by responsibility:
schema-form/schema-fieldowns local-reference resolution and normalized form fields. No route or capability-specific code resolves schemas.- The existing read-oriented capability client continues to own list and inspect operations.
- A focused capability-call client owns
workflow.capabilities.call, its exact payload, and result decoding through the console write executor. - A capability-playground controller owns selected-capability provenance, pending/result/error state, stale-response suppression, and explicit reset on target or selection change.
- Discover composes the contract and playground views. It does not call the transport directly or duplicate evidence recording.
- A pure evidence-policy module owns redaction and structural bounds. The
connection reducer applies it to every
evidence_recordedaction before the record enters application state, so executor and connection evidence follow one retention rule.
The call client interface stays small: one call method accepting qualified
name, payload, and optional deployment id. Transport naming, snake-case wire
fields, decoding, and evidence behavior remain inside its implementation.
TypeScript And Browser Boundary
The generated workflow contract already inventories
workflow.capabilities.call; this slice authors it into the Effect RPC group,
method registry, service dispatch, console operation union, and Hono browser
allowlist. Payload and success schemas come from the checked generated contract
through the existing runtime JSON Schema translator. The implementation must
not hand-write a parallel transport schema.
The method registry provides the equivalent CLI:
uv run wf cap call <qualified-name> --input '<json>' [--deployment <id>]
Input JSON and identifiers use the existing shell-argument escaping helper. The interpreter maps snake-case wire fields into the console result model without discarding diagnostics or nullable output.
The browser allowlist adds only workflow.capabilities.call. Source admin,
auth, registry mutation, generic draft replacement, and unrelated operations
remain blocked. Positive allowlist coverage must be paired with representative
negative tests so adding RPC support cannot silently broaden browser access.
browser-operation-policy becomes a configured policy module rather than a
single static set. Its interface classifies an operation as allowed, known but
disabled, or unknown. createApp receives that policy. The server entrypoint
constructs it from the bound hostname and the explicit environment opt-in; app
tests inject either policy without mutating process-global environment state.
Unknown operations retain the existing unknown_operation response, while a
known disabled capability call returns operation_disabled with HTTP 403.
The console remains a trusted local-operator tool, not a multi-user authorized
control plane. Direct capability calls are enabled by default only when the web
server binds to a loopback hostname. A non-loopback WEB_HOST requires an
explicit WEB_ENABLE_CAPABILITY_CALLS=1 opt-in before the browser policy admits
this operation. Startup logs and the web runbook must state that this opt-in can
let LAN clients execute side-effecting capabilities against the server's
loopback workflow target. It does not add authentication, TLS, tenant isolation,
or internet-safe deployment guarantees.
When policy blocks a direct call, /api/rpc returns an explicit
operation_disabled error rather than pretending the generated operation is
unknown. The playground shows that response inline. This slice does not add a
separate policy-discovery endpoint merely to pre-disable the button.
Errors And Evidence
Form validation prevents calls when required or typed inputs are invalid. Unsupported schemas retain the existing raw JSON editor rather than guessing a control. Server validation and capability execution remain authoritative.
When a call fails, the playground keeps the submitted payload visible and shows
the exact mapped console error inline with role="alert". It does not swallow
the rejected promise. A later successful call replaces the failure receipt;
editing alone does not erase it.
Changing the selected capability or connected target invalidates in-flight UI responses. A late result must not replace the current playground state. If the call was actually sent, its evidence remains in the global ledger as truthful history even when the operator changes selection; connection-generation guards continue to suppress evidence only when the underlying executor is stale.
The current evidence ledger is unbounded, so this slice adds one shared evidence
sanitization policy rather than storing arbitrary capability payloads forever.
The ledger keeps at most 100 recent records. Request and response values are
converted to bounded JSON-safe projections before entering application state,
with maximum depth 8, 100 entries per collection, 4,096 characters per string,
and 32 KiB of serialized content for each request and response projection.
Overflow is represented by explicit truncation markers. Keys matching
authorization, cookie, set-cookie, token, access_token,
refresh_token, secret, password, api_key, or api-key are redacted
case-insensitively. The policy applies to existing operations as well as
capability calls and is covered independently from display formatting.
After sanitization, each record includes operation, label, bounded request and
response, duration, target, and equivalent CLI. EvidenceRecord gains the
executor's connected target so retained calls remain attributable after a
reconnect. Executors populate it from their configured target, and the initial
health record uses the normalized connected target. The full transport response
is still decoded for the immediate caller; bounding affects retained UI
evidence, not operation semantics.
Responsive And Accessible Behavior
Desktop keeps catalog results and selected capability detail visible together. Contract and Try controls use a keyboard-accessible tab or equivalent segmented interface with stable accessible names.
On narrow screens, the selected detail remains in normal document flow below the catalog. The generated form and result own internal overflow only for raw JSON blocks; primary controls do not require horizontal scrolling.
The call button communicates pending state and is disabled while disconnected,
invalid, unacknowledged, or already calling. Status changes use role="status";
failures use role="alert". The result JSON region is labelled and
keyboard-scrollable.
Out Of Scope
Slice 4 does not include:
- mixed literal/path array or object composition;
- per-array-item source selectors;
- workflow state, routes, run records, traces, interrupts, or resume;
- capability-call presets, saved history, or batch calls;
- automatic retries or polling;
- a deployment catalog embedded in the playground;
- external JSON Schema reference fetching;
- arbitrary JSON Schema composition such as
allOf; or - direct graph mutations.
Composite value construction remains Slice 5 and will use this playground to smoke-test capability behavior without pretending the direct call itself is a workflow.
Verification
The implementation must include:
- schema normalization tests for nested local references, escaped pointer tokens, annotation siblings, unresolved pointers, external references, and cycles;
- a form regression using the real
wf.source.read_resourceschema shape; - generated-schema parity, RPC group, service dispatch, method registry, and
equivalent-CLI tests for
workflow.capabilities.call; - browser operation allowlist positive coverage plus representative negative coverage, loopback-default enablement, and non-loopback opt-in behavior;
- evidence retention, structural bounding, credential redaction, and unchanged immediate-result decoding tests;
- capability-call model decoder and client tests for exact payloads, optional
wrapper deployment omission/inclusion, nullable output,
runtime_error, and diagnostics; - controller tests for success, validation blocking, operation failure, acknowledgement, selection changes, target changes, and stale responses;
- Discover route tests for Contract/Try switching, no automatic invocation, explicit call, pending state, result facts, raw output, and inline failure;
- responsive and keyboard-accessibility regressions; and
- a real-server smoke that calls
wf.std.concatwith a literal array and separator, then confirms the outcome and output in the console; and - a real-schema regression that renders
wf.source.read_resourcewithout an unresolved-ref fallback while documenting that its direct call lacks platform context.
Focused tests, full web tests, TypeScript typecheck, production build, React
Doctor changed-scope triage, and git diff --check are completion gates.