27 KiB
Workflow Console Capability Playground Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a safe, schema-generated capability playground to Discover so an operator can inspect and directly call one capability without first creating a workflow.
Architecture: Extend the existing authored Effect RPC boundary for workflow.capabilities.call, expose it through a configured browser policy, and invoke it through the console's existing write executor. Resolve self-contained local JSON Schema references before field normalization, keep direct-call state in a focused controller, and sanitize/bound protocol evidence before it reaches application state.
Tech Stack: TypeScript 6, React 19, Effect RPC, Hono, Valibot, Vitest, Testing Library, Vite, pnpm.
Global Constraints
- Direct capability calls are immediate operations, not workflow runs: do not create graph, state, route, run, or trace records in the UI.
- Never call on selection, tab change, reconnect, form change, or automatic retry.
- Require explicit side-effect acknowledgement before every selected capability can be called.
- Use generated JSON Schema through
runtimeSchemasFor; do not handwrite transport payload/result schemas. - Support only local JSON Pointer references beginning with
#/; reject external references, cycles, and structural$refsiblings that require schema intersection. - Browser capability calls are enabled automatically only when
WEB_HOSTis loopback. Non-loopback hosts requireWEB_ENABLE_CAPABILITY_CALLS=1. - Bound evidence to 100 records, depth 8, 100 collection entries, 4096 characters per string, and 32 KiB serialized per request or response.
- Redact keys case-insensitively for
authorization,cookie,set-cookie,token,access_token,refresh_token,secret,password,api_key, andapi-key. - Preserve the existing package boundaries: RPC transport in
web/packages/rpc, browser policy inweb/apps/server, and product projection/UI inweb/apps/console. - Do not introduce MCP Inspector, n8n, another state store, or another transport as a dependency.
- Add comments or docstrings at security boundaries and non-obvious truncation/reference-resolution logic.
Reference Patterns, Not Dependencies
- MCP Inspector demonstrates the useful product loop: inspect a tool contract, generate parameter inputs, execute explicitly, and show request/result/error history. Keep our implementation inside the existing workflow API and console architecture.
- MCP Inspector documentation treats the inspector as a development surface rather than a workflow runtime. Preserve that same distinction.
- n8n execution-data guidance warns that retained execution data grows without pruning. Apply that lesson to the console evidence ledger with deterministic bounds and redaction.
Task 1: Resolve Self-Contained Local Schema References
Files:
- Create:
web/apps/console/src/workspace/schema-form/schema-reference.ts - Create:
web/apps/console/src/workspace/schema-form/schema-reference.test.ts - Modify:
web/apps/console/src/workspace/schema-form/schema-field.ts - Modify:
web/apps/console/src/workspace/schema-form/schema-field.test.ts - Modify:
web/apps/console/src/workspace/schema-form/SchemaForm.tsx - Modify:
web/apps/console/src/workspace/schema-form/SchemaForm.test.tsx - Modify:
web/apps/console/src/workspace/schema-form/SchemaFieldControl.tsx
Interfaces:
-
Produces:
resolveLocalSchemaNode(rootSchema: unknown, schemaNode: unknown): SchemaReferenceResolution. -
Produces:
SchemaReferenceResolution = { readonly ok: true; readonly schema: unknown } | { readonly ok: false; readonly reason: string }. -
Extends:
SchemaFormPropswithreadonly showSourceControls?: boolean, defaulting totrue. -
Preserves:
normalizeSchema(schema: unknown): SchemaFieldand all existing authoring callers. -
Step 1: Add failing resolver tests
Test direct and nested #/$defs/... references, #/definitions/..., escaped JSON Pointer tokens (~0, ~1), annotation siblings (title, description, default), unresolved pointers, external references, cycles, and structural siblings such as properties beside $ref.
expect(resolveLocalSchemaNode(root, root.properties?.ref)).toEqual({
ok: true,
schema: {
type: "object",
title: "Resource",
properties: {
logical_source: { type: "string" },
uri: { type: "string" },
},
required: ["logical_source", "uri"],
},
});
expect(resolveLocalSchemaNode(root, { $ref: "https://example.test/schema" })).toEqual({
ok: false,
reason: "External schema references are not supported.",
});
- Step 2: Verify the resolver tests fail
Run: pnpm --dir web --filter @lda/console test -- src/workspace/schema-form/schema-reference.test.ts
Expected: FAIL because schema-reference.ts does not exist.
- Step 3: Implement the local-reference resolver
Implement an internal recursive resolver that carries the root document and an active-reference set. Decode pointer tokens with ~1 -> / and ~0 -> ~; only traverse own object properties and non-negative array indices. Merge only annotation siblings title, description, and default over the resolved schema.
export type SchemaReferenceResolution =
| { readonly ok: true; readonly schema: unknown }
| { readonly ok: false; readonly reason: string };
export const resolveLocalSchemaNode = (
rootSchema: unknown,
schemaNode: unknown,
): SchemaReferenceResolution => resolveNode(rootSchema, schemaNode, new Set());
Return a stable failure reason for malformed pointers, missing targets, cycles, external references, and structural siblings. Do not mutate the input schema.
- Step 4: Integrate resolution into field normalization
Change internal normalization to retain rootSchema across recursive object and array fields. Resolve each node before reading type, properties, or items. On failure, return the existing JSON fallback field with the resolver's reason.
Add a realistic wf.source.read_resource schema test whose ref property points to #/$defs/SourceResourceRef; assert generated child fields for logical_source, uri, and defaulted kind.
- Step 5: Add and test source-control suppression
Thread showSourceControls from SchemaForm into SchemaFieldControl. When false, render literal inputs only and do not render path-source selectors. Keep the default true so draft authoring behavior is unchanged.
<SchemaForm
schema={schema}
showSourceControls={false}
submitLabel="Call capability"
/>
- Step 6: Run focused schema-form verification
Run: pnpm --dir web --filter @lda/console test -- src/workspace/schema-form/schema-reference.test.ts src/workspace/schema-form/schema-field.test.ts src/workspace/schema-form/SchemaForm.test.tsx
Expected: PASS.
- Step 7: Commit Task 1
git add web/apps/console/src/workspace/schema-form
git commit -m "feat: resolve local capability schema references"
Task 2: Author the Capability Call RPC
Files:
- Modify:
web/packages/rpc/src/rpcs.ts - Modify:
web/packages/rpc/src/index.ts - Modify:
web/packages/rpc/src/method-registry.ts - Modify:
web/packages/rpc/src/service.ts - Modify:
web/packages/rpc/src/service.test.ts - Modify:
web/packages/rpc/src/json-schema/rpc-parity.test.ts - Modify: registry tests adjacent to
method-registry.tsif present
Interfaces:
- Produces:
WorkflowCapabilitiesCallPayloadSchema,WorkflowCapabilitiesCallResultSchema, andWorkflowCapabilitiesCall. - Extends:
OperationNamewith the already-generated"workflow.capabilities.call"operation. - Produces interpreted camel-case result:
type CapabilityCallInterpreted = {
readonly qualifiedName: string;
readonly sourceId: string;
readonly kind: "node_spec" | "wrapper_artifact";
readonly deploymentId: string | null;
readonly outcome: string;
readonly output: Record<string, unknown> | null;
readonly diagnostics: ReadonlyArray<{
readonly boundSource: string | null;
readonly code: string;
readonly logicalRef: string;
readonly message: string;
readonly repairHint: string | null;
readonly severity: string;
}>;
};
- Step 1: Add failing RPC parity and service tests
Pin that the authored RPC group contains workflow.capabilities.call, malformed payloads fail decoding before fetch, and a successful exchange dispatches the exact JSON-RPC method with qualified_name, payload, and optional deployment_id.
- Step 2: Verify RPC tests fail
Run: pnpm --dir web --filter @lda/workflow-rpc test -- src/json-schema/rpc-parity.test.ts src/service.test.ts
Expected: FAIL because the generated inventory contains the operation but WorkflowRpcs does not.
- Step 3: Define the RPC from generated runtime schemas
const capabilityCallSchemas = runtimeSchemasFor("workflow.capabilities.call");
export const WorkflowCapabilitiesCallPayloadSchema = capabilityCallSchemas.payload;
export const WorkflowCapabilitiesCallResultSchema = capabilityCallSchemas.success;
export const WorkflowCapabilitiesCall = Rpc.make("workflow.capabilities.call", {
payload: WorkflowCapabilitiesCallPayloadSchema,
success: WorkflowCapabilitiesCallResultSchema,
error: Schema.Never,
});
Add it to WorkflowRpcs and export it through index.ts.
- Step 4: Add registry metadata and interpretation
Register the operation with idempotency: "write". Build the equivalent CLI as uv run wf cap call <qualified-name> --input <json> and append --deployment <id> only when present. Use the existing shell-argument helper for both the JSON payload and identifiers. Decode the result through WorkflowCapabilitiesCallResultSchema; do not cast raw objects.
- Step 5: Dispatch the operation in the service switch
Decode with WorkflowCapabilitiesCallPayloadSchema, then call:
return yield* client.workflow["capabilities.call"](payload);
Keep the exhaustive unreachableOperation guard intact.
- Step 6: Run RPC package verification
Run: pnpm --dir web --filter @lda/workflow-rpc test
Run: pnpm --dir web --filter @lda/workflow-rpc typecheck
Expected: PASS.
- Step 7: Commit Task 2
git add web/packages/rpc/src
git commit -m "feat: expose capability calls through workflow rpc"
Task 3: Configure the Browser Operation Security Boundary
Files:
- Modify:
web/apps/server/src/browser-operation-policy.ts - Modify:
web/apps/server/src/browser-operation-policy.test.ts - Modify:
web/apps/server/src/app.ts - Modify:
web/apps/server/src/app.test.ts - Modify:
web/apps/server/src/index.ts - Modify:
web/apps/console/src/connection/contracts.ts - Modify:
web/apps/console/src/connection/contracts.test.ts
Interfaces:
-
Produces:
BrowserOperationDecision = "allowed" | "disabled" | "unknown". -
Produces:
createBrowserOperationPolicy(options: { readonly enableCapabilityCalls: boolean }): { readonly classify(operation: string): BrowserOperationDecision }. -
Produces:
capabilityCallsEnabledForHost(hostname: string, override: string | undefined): boolean. -
Extends:
createAppdependencies with an injected browser operation policy. -
Adds: browser error code
operation_disabled. -
Step 1: Add failing policy tests
Assert that existing operations remain allowed, unknown operations remain unknown, and workflow.capabilities.call is disabled or allowed according to configuration. Cover 127.0.0.1, localhost, ::1, 0.0.0.0, LAN addresses, and explicit WEB_ENABLE_CAPABILITY_CALLS=1.
- Step 2: Verify policy tests fail
Run: pnpm --dir web --filter @lda/web-server test -- src/browser-operation-policy.test.ts src/app.test.ts
Expected: FAIL because policy classification is currently boolean and static.
- Step 3: Implement policy classification
Keep the existing authored allowlist. Treat capability call as a known conditional operation rather than adding it unconditionally.
const conditionalOperations = new Set<OperationName>([
"workflow.capabilities.call",
]);
classify() must return unknown for generated-but-unexposed operations such as admin methods.
- Step 4: Return a distinct disabled response
In /api/rpc, return HTTP 403 and:
{
"ok": false,
"error": {
"code": "operation_disabled",
"message": "workflow.capabilities.call is disabled for this console server"
},
"exchange": { "request": null, "response": null }
}
Keep unknown operations on their existing 400 response. Add the new code to the Valibot browser contract.
- Step 5: Wire startup configuration
Build the policy once in index.ts. Loopback hosts enable direct calls by default; non-loopback hosts require the exact string "1". Log a startup warning when the override enables calls on a non-loopback host. Do not claim authentication or TLS.
- Step 6: Run server and connection-contract tests
Run: pnpm --dir web --filter @lda/web-server test
Run: pnpm --dir web --filter @lda/console test -- src/connection/contracts.test.ts
Expected: PASS.
- Step 7: Commit Task 3
git add web/apps/server/src web/apps/console/src/connection
git commit -m "feat: guard browser capability calls by host policy"
Task 4: Bound and Redact Console Evidence
Files:
- Create:
web/apps/console/src/workspace/domain/evidence-policy.ts - Create:
web/apps/console/src/workspace/domain/evidence-policy.test.ts - Modify:
web/apps/console/src/app/state.ts - Modify:
web/apps/console/src/app/state.test.ts - Modify:
web/apps/console/src/workspace/domain/executor-protocol.ts - Modify:
web/apps/console/src/workspace/domain/read-executor.test.ts - Modify:
web/apps/console/src/workspace/domain/write-executor.test.ts - Modify:
web/apps/console/src/workspace/ConsoleWorkspace.tsx - Update: tests and fixtures constructing
EvidenceRecord
Interfaces:
-
Extends:
EvidenceRecordwithreadonly target: string. -
Produces:
sanitizeEvidenceValue(value: unknown): unknown. -
Produces:
sanitizeEvidenceRecord(record: EvidenceRecord): EvidenceRecord. -
Produces:
retainEvidence(records: readonly EvidenceRecord[], record: EvidenceRecord): readonly EvidenceRecord[]. -
Step 1: Add failing evidence-policy tests
Cover case-insensitive redaction, depth truncation, string truncation, array/object entry truncation, 32 KiB aggregate request/response budgets, preservation of ordinary scalar data, and retention of only the newest 100 records.
expect(sanitizeEvidenceValue({ Authorization: "Bearer secret", value: "ok" })).toEqual({
Authorization: "[redacted]",
value: "ok",
});
- Step 2: Verify evidence tests fail
Run: pnpm --dir web --filter @lda/console test -- src/workspace/domain/evidence-policy.test.ts src/app/state.test.ts
Expected: FAIL because evidence is currently unbounded and unsanitized.
- Step 3: Implement the pure sanitizer
Use a recursive projector with an explicit byte budget. Redact matching keys before traversing their values. Use stable markers such as [redacted], [truncated: depth limit], and [truncated: evidence limit]. Never mutate source objects.
Document why the aggregate budget is applied independently to request and response.
- Step 4: Apply policy before application state
Replace appendEvidence with retainEvidence, sanitizing the incoming record and slicing to the newest 100. This makes the reducer the single retention boundary for live console evidence.
- Step 5: Record the target on every executor path
Add target: options.target in createConsoleExecutor records, including invocation failure, protocol failure, mismatch, and success. Add the connected target to the health evidence constructed in ConsoleWorkspace.
- Step 6: Update fixtures and run focused tests
Run: pnpm --dir web --filter @lda/console test -- src/workspace/domain/evidence-policy.test.ts src/workspace/domain/read-executor.test.ts src/workspace/domain/write-executor.test.ts src/app/state.test.ts
Expected: PASS.
- Step 7: Commit Task 4
git add web/apps/console/src/app web/apps/console/src/workspace web/apps/console/src/components web/apps/console/src/presentation
git commit -m "feat: bound and redact console operation evidence"
Task 5: Add the Capability Call Domain Boundary
Files:
- Modify:
web/apps/console/src/workspace/domain/capability-models.ts - Modify:
web/apps/console/src/workspace/domain/capability-models.test.ts - Create:
web/apps/console/src/workspace/domain/capability-call-client.ts - Create:
web/apps/console/src/workspace/domain/capability-call-client.test.ts - Create:
web/apps/console/src/workspace/routes/useCapabilityPlayground.ts - Create:
web/apps/console/src/workspace/routes/useCapabilityPlayground.test.tsx
Interfaces:
- Produces:
CapabilityCallResultanddecodeCapabilityCallResult(value: unknown): CapabilityCallResult. - Produces:
callCapability(executor, request): Promise<CapabilityCallResult>. - Produces controller fields
phase,result,message,acknowledged,deploymentId,setAcknowledged,setDeploymentId,call, andreset.
export type CapabilityCallRequest = {
readonly qualifiedName: string;
readonly payload: Record<string, unknown>;
readonly deploymentId?: string;
};
- Step 1: Add failing decoder/client tests
Pin successful output, runtime_error as a decoded completed result, dependency diagnostics, malformed results, snake-case request payload, and omission of blank deployment IDs.
- Step 2: Verify domain tests fail
Run: pnpm --dir web --filter @lda/console test -- src/workspace/domain/capability-models.test.ts src/workspace/domain/capability-call-client.test.ts
Expected: FAIL because the call model/client do not exist.
- Step 3: Implement the Valibot decoder and client
Reuse the existing decode() helper. Model the exact interpreted camel-case shape emitted by Task 2. The client must use ConsoleExecutor.run("workflow.capabilities.call", ...); it must not call fetch or callOperation directly.
- Step 4: Add failing controller tests
Cover disconnected, idle, calling, result, and error phases; acknowledgement and deployment reset when capability or target changes; double-submit suppression; and stale completion suppression after selection changes.
- Step 5: Implement the controller
Use request identity or an incrementing generation ref so an earlier promise cannot overwrite a newer selection. Keep runtime_error in phase: "result"; only transport/protocol/decode failures use phase: "error".
- Step 6: Run focused domain/controller tests
Run: pnpm --dir web --filter @lda/console test -- src/workspace/domain/capability-models.test.ts src/workspace/domain/capability-call-client.test.ts src/workspace/routes/useCapabilityPlayground.test.tsx
Expected: PASS.
- Step 7: Commit Task 5
git add web/apps/console/src/workspace/domain web/apps/console/src/workspace/routes/useCapabilityPlayground.ts web/apps/console/src/workspace/routes/useCapabilityPlayground.test.tsx
git commit -m "feat: add capability playground controller"
Task 6: Build the Discover-Route Playground
Files:
- Create:
web/apps/console/src/workspace/routes/CapabilityPlayground.tsx - Create:
web/apps/console/src/workspace/routes/CapabilityPlayground.test.tsx - Modify:
web/apps/console/src/workspace/routes/DiscoverRoute.tsx - Modify:
web/apps/console/src/workspace/routes/DiscoverRoute.test.tsx - Modify:
web/apps/console/src/styles/workspace.css - Move:
web/apps/console/src/workspace/authoring/format-bounded-json.tstoweb/apps/console/src/workspace/domain/format-bounded-json.ts - Move:
web/apps/console/src/workspace/authoring/format-bounded-json.test.tstoweb/apps/console/src/workspace/domain/format-bounded-json.test.ts - Modify: imports of
formatBoundedJsonin workspace authoring/routes
Interfaces:
-
Consumes: Tasks 1, 4, and 5.
-
Produces:
CapabilityPlayground({ capability, target, executor }: CapabilityPlaygroundProps). -
Preserves: existing capability search, inspect, pagination, and Add to draft behavior.
-
Step 1: Move the bounded JSON helper without behavior changes
Move the generic formatter to workspace/domain, update all imports, and run its existing test before adding UI behavior.
Run: pnpm --dir web --filter @lda/console test -- src/workspace/domain/format-bounded-json.test.ts
Expected: PASS.
- Step 2: Add failing playground component tests
Test Contract/Try capability views, generated literal-only form, immediate-execution warning, acknowledgement gate, wrapper-only deployment input, in-flight state, successful outcome/output/diagnostics, runtime_error, and rejected operation alert.
Assert that changing capability resets acknowledgement and result. Assert no call occurs on render, tab selection, or form change.
- Step 3: Verify UI tests fail
Run: pnpm --dir web --filter @lda/console test -- src/workspace/routes/CapabilityPlayground.test.tsx src/workspace/routes/DiscoverRoute.test.tsx
Expected: FAIL because the component is absent.
- Step 4: Implement the detail tabs and call form
Keep the capability list unchanged. Replace the detail body's always-expanded raw schemas with tabs:
- Contract: current facts, input/output schemas, wrapper hints, Add to draft.
- Try capability:
SchemaFormwithshowSourceControls={false}, acknowledgement checkbox, optional wrapper deployment ID, andCall capabilitybutton.
The submit handler must reject serialization issues and non-object root values locally. It passes only literal result.value to the controller.
- Step 5: Implement the result receipt
Show capability name, outcome, duration/provenance supplied by evidence where available, diagnostics as a readable list, and output through formatBoundedJson. Label runtime_error as a completed capability outcome and explain that no workflow run or trace was created.
Use existing workspace visual language and Lucide icons. Do not clone MCP Inspector or n8n styling.
- Step 6: Integrate with DiscoverRoute
Obtain writeExecutor and connectedTarget from the workspace context, create the controller for the selected capability, and preserve CreateDraftDialog. Ensure the conditional operation-disabled response appears inline instead of disconnecting the console.
- Step 7: Run focused UI tests and React Doctor
Run: pnpm --dir web --filter @lda/console test -- src/workspace/routes/CapabilityPlayground.test.tsx src/workspace/routes/DiscoverRoute.test.tsx
Run: pnpm --dir web --filter @lda/console exec react-doctor --verbose
Expected: tests PASS; no new React Doctor errors.
- Step 8: Commit Task 6
git add web/apps/console/src/workspace web/apps/console/src/styles/workspace.css
git commit -m "feat: add capability playground to discover"
Task 7: Document, Smoke-Test, and Close the Slice
Files:
- Modify:
web/README.md - Modify:
docs/current_roadmap.md - Modify:
docs/project_map.mdif it enumerates console workspace modules - Archived at:
docs/historical/superpowers/plans/2026-08-11-workflow-console-capability-playground.md
Interfaces:
-
Documents: loopback defaults, non-loopback opt-in, side-effect warning, direct-call semantics, evidence limits, and known platform-context limitation.
-
Verifies: the complete vertical slice against a running workflow RPC server.
-
Step 1: Add user-facing runbook documentation
Document:
uv run wf-rpc-server --config wf.config.json --host 127.0.0.1 --port 8765
pnpm --dir web dev
Explain that wf.std.concat is a suitable direct-call smoke test. Explain that wf.source.read_resource should render a proper form after local $ref support, but a node-spec direct call can still return runtime_error when it requires workflow platform context. State that this is truthful behavior, not a failed form implementation.
Document WEB_ENABLE_CAPABILITY_CALLS=1 as an explicit non-loopback risk acceptance, not authentication.
- Step 2: Run scoped package verification
Run: pnpm --dir web --filter @lda/workflow-rpc test
Run: pnpm --dir web --filter @lda/web-server test
Run: pnpm --dir web --filter @lda/console test
Expected: PASS.
- Step 3: Run full web verification
Run: pnpm --dir web test
Run: pnpm --dir web typecheck
Run: pnpm --dir web build
Run: git diff --check
Expected: all tests and typechecks pass; build succeeds except any already-documented chunk-size warning; no whitespace errors.
- Step 4: Perform browser smoke verification
At /console/discover:
- Connect to
http://127.0.0.1:8765/rpc. - Select
wf.std.concat; confirm Contract shows schemas and Try capability shows generated fields. - Confirm no request occurs before acknowledgement and submit.
- Submit
items: ["hello", "world"],separator: " "; verify output and operation evidence. - Select
wf.source.read_resource; verifylogical_source,uri, and defaultedkindfields render instead of unresolved$refJSON. - Call it without platform context and verify a completed
runtime_errorreceipt rather than a transport alert. - Verify the evidence ledger shows the target and redacts a synthetic sensitive key in a test fixture; do not send real credentials.
- Run the web server with a non-loopback host and no override; verify the Try surface receives
operation_disabledwithout exposing the call.
- Step 5: Record review status
The controller owns the formal per-task and final reviews. Per the Task 7 brief, no review agent was spawned; self-review found no Critical or Important findings and the narrowly necessary evidence-target display correction was covered by a focused test.
- Step 6: Update roadmap and archive the completed plan
Mark Workflow Console Slice 4 completed in docs/current_roadmap.md and link this historical plan after verification succeeds.
- Step 7: Commit Task 7
git add web/README.md docs/current_roadmap.md docs/project_map.md docs/superpowers docs/historical
git commit -m "docs: complete capability playground slice"
Plan Self-Review
- Spec coverage: Tasks 1–7 cover local
$ref, authored RPC parity, browser policy, evidence safety, decoding/controller state, Discover UI, documentation, live smoke, and completion gates. - Placeholder scan: No
TBD,TODO, unspecified error handling, or unnamed test steps remain. - Type consistency:
CapabilityCallRequest,CapabilityCallResult,SchemaReferenceResolution,BrowserOperationDecision, and evidence-policy function names are defined before downstream use. - Scope control: Composite mixed array/object source bindings remain Workflow Console Slice 5; this plan does not fake
items.0support or alter Python workflow semantics.