Workbench composition: the Consumes primitive
Goal: Let one workbench call another workbench's hosted mode. Declared in the workspace; wired by the platform.
What Consumes is
Every hosted workbench can declare a list of other workbenches (in the same workspace) that it depends on. Each declaration is one row:
{
"Workbench": "api-sample", // APISlug of the target workbench
"Mode": "API", // Mode key on the target (must be enabled)
"As": "products" // Optional alias for the injected env var name
}
Multiple rows are allowed. Each targets a specific mode of a specific workbench — the SAME target workbench can appear twice if you consume both its API and MCP modes.
What gets injected
For each declared dependency, OpenX injects two things into every per-mode container of the consumer workbench at deploy:
1. Individual env var per dependency:
WB_CONSUMED_{alias}_URL = "{workspace-origin}/oi-api/workbenches/{Workbench}/{Mode}"
- Alias falls back to
WorkbenchwhenAsis unset (so a bare{Workbench: 'pricing', Mode: 'API'}yieldsWB_CONSUMED_pricing_URL). - URL is the same workspace-proxied path any human consumer would use.
2. Enumeration hook (single env var, JSON-encoded):
FATHYM_OX_CONSUMES_JSON = '{"products":"https://.../api-sample/API","pricing":"https://.../pricing/API"}'
- Parse via
JSON.parse(Deno.env.get('FATHYM_OX_CONSUMES_JSON') ?? '{}')when a consumer needs to enumerate all dependencies without knowing alias names in advance (e.g. a workbench that discovers its own connected surface at runtime).
Both patterns land on the consumer at the same time. Pick whichever fits your consumer's access shape.
How to declare Consumes
Consumes lives on the workspace record (Details.Consumes on the workbench EaC). The typical path is via the inspector:
- In your workspace, open the workbench that will consume another.
- Switch to the Consumes tab in the inspector.
- Click Add dependency. For each row:
- Target dropdown: pick another workbench from the workspace (filtered to those with an APISlug set).
- Mode dropdown: pick an enabled mode on the target.
- Alias (optional): an env-var-friendly name; if omitted, the target's APISlug is used.
- Save. Redeploy the consumer workbench to pick up the injected env vars.
The inspector only lets you pick valid targets + modes, so most user error is prevented at the UX layer. On top of that, the workspace commit endpoint validates every Consumes entry before persisting — bad references return 400 { Errors: [...] }.
How to read the injected env vars
Inside the consumer workbench's mode container, Deno code reads the env vars directly:
// The most common pattern: fetch a specific dependency.
const productsUrl = Deno.env.get('WB_CONSUMED_products_URL');
if (!productsUrl) {
throw new Error('WB_CONSUMED_products_URL not set — declare Consumes on this workbench.');
}
const res = await fetch(`${productsUrl}/hello/greet`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// See "Outbound auth caveat" below.
'Authorization': `Bearer ${Deno.env.get('FATHYM_OX_SERVICE_JWT')}`,
},
body: JSON.stringify({ arg0: 'world' }),
});
Enumeration variant:
// Discover all consumed workbenches without knowing alias names.
const consumes = JSON.parse(
Deno.env.get('FATHYM_OX_CONSUMES_JSON') ?? '{}',
) as Record<string, string>;
for (const [alias, url] of Object.entries(consumes)) {
// Enumerate + call each.
}
Governance
The consumer's outbound request hits the workspace proxy at ${workspaceOrigin}/oi-api/workbenches/${target}/${mode} — the same URL a human consumer would use. That means:
- Same access-right gate. Consumer's JWT must carry
Workspace.Workbench.Host. The consumer container is NOT trusted implicitly. - Same audit trail. Every proxied call is logged with caller (the consumer workbench's identity), target APISlug, method, and timestamp.
- Same isolation. Cross-mode traffic is routed through the workspace's network boundary — the consumer container never gets a direct pipe to the target container.
Composition is a WIRING primitive, not a BYPASS primitive.
A workbench that consumes another workbench is just an HTTP client to the workspace proxy. No implicit trust, no service mesh, no cross-container secrets — the exact same governance model as an external caller, applied to the consumer container.
Outbound auth caveat
Service-JWT injection is a known follow-up. The consumer container needs a JWT with Workspace.Workbench.Host to make its outbound proxy calls, but the SOP doesn't yet auto-inject one. Until it does, use one of these workarounds:
- Manual per-workbench JWT — set
FATHYM_OX_SERVICE_JWTin the workbench'sDetails.Envvia the inspector. Short-lived; rotate manually. - Skip outbound auth in demos — the request 401's from the proxy, error surfaces in the consumer response, but proves the plumbing works.
Full E2E composition without manual auth requires a follow-up ticket on service-JWT injection.
Failure modes
| Situation | Outcome |
|---|---|
| Consumes target APISlug doesn't exist | Commit rejected with 400 { Errors: ["Workbench '{lookup}' declares Consumes on '{target}' which does not exist..."] } |
| Consumes target found but named mode not Enabled | Commit rejected with 400 { Errors: ["...mode '{Mode}' which is not enabled on the target."] } |
| Target uninstalled AFTER consumer deployed | Consumer's env var still points at the stale URL; requests to it 404 at the workspace proxy. Redeploy the consumer to re-run the SOP's D.10.3 defence-in-depth check, which throws with HostingStatus: Failed. |
No Details.Consumes at all | FATHYM_OX_CONSUMES_JSON = "{}"; no individual WB_CONSUMED_*_URL vars injected. |
Sample
The fathym-deno/ui-workbench sample declares { Workbench: 'api-sample', Mode: 'API', As: 'products' } and reads WB_CONSUMED_products_URL in its /products route to render a page from Phase 9's api-workbench. See the sample's README for the end-to-end walkthrough.
Next steps
| If you want to... | Go to... |
|---|---|
| Understand WebMode (the most common consumer) | Web access → |
| Understand the target API mode | API access → |
| Install another workbench | Install a workbench from source → |
| See the mode model + overview | Workbenches overview → |