# Workflow Console Capability Playground Design Date: 2026-08-11 Status: Approved design for Workflow Console Slice 4. Related: - [Workflow Console draft authoring workbench](2026-08-05-workflow-console-draft-authoring-workbench-design.md) - [Workflow Console selected-step dataflow](2026-08-09-workflow-console-selected-step-dataflow-design.md) - [Current roadmap](../../current_roadmap.md) ## 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 `$ref` that 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-field` owns 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_recorded` action 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: ```text uv run wf cap call --input '' [--deployment ] ``` 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_resource` schema 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.concat` with a literal array and separator, then confirms the outcome and output in the console; and - a real-schema regression that renders `wf.source.read_resource` without 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.