Route typed data into an authorized Workspace
Turn a model-shaped routing proposal into bounded local work without allowing the proposal to select an arbitrary directory:
- an
<Agent />returns a schema-validated route record; - ordinary application code validates category, resolves the approved root and candidate, and enforces canonical containment;
- only after those checks does the application construct
localWorkspace()and run a handoff<Agent />inside<Workspace />.
The example uses deterministic Agent providers, so it needs no credentials or network access. It does require an existing playbook directory with an account-access subdirectory. The route record is deliberately a proposal, not an authorization decision.
Prerequisites and status
Section titled “Prerequisites and status”- Node.js
>=26, ESM TypeScript/TSX execution,@aml-jsx/sdk, andzod; AML_SUPPORT_PLAYBOOKSset to an existing application-owned directory;- an existing
account-accesssubdirectory beneath that root; - no model credentials are required for this deterministic walkthrough.
For a live provider, replace the router and handoff fixtures with codexAgent({ workingDirectory }) or another supported Agent provider, then configure its executable and credentials. That variation is credentialed and does not change the host-owned authorization check.
Complete source
Section titled “Complete source”import { realpath } from "node:fs/promises"import { isAbsolute, relative, resolve, sep } from "node:path"
import { Agent, AmlRuntime, localWorkspace, Workspace } from "@aml-jsx/sdk"import { DeterministicAgentProvider } from "@aml-jsx/sdk/testing"import { z } from "zod"
const root = process.env.AML_SUPPORT_PLAYBOOKSif (root === undefined) { throw new Error("Set AML_SUPPORT_PLAYBOOKS to an existing playbook directory")}
const Route = z.object({ category: z.enum(["account-access", "billing", "delivery"]), directory: z.string(), normalizedRequest: z.string(), uncertainties: z.array(z.string()),})
const router = new DeterministicAgentProvider({ name: "support-router", respond(request) { if (request.output?.type === "json") { return { structured: { category: "account-access", directory: "account-access", normalizedRequest: "The user cannot sign in after changing identity providers.", uncertainties: ["The identity-provider change may need an administrator to confirm it."], }, text: "", } }
return { text: request.prompt } },})
const handoff = new DeterministicAgentProvider({ name: "support-handoff", respond(request) { return { text: `Internal handoff: ${request.prompt}` } },})
async function RouteSupportRequest(request: string) { const route = await new AmlRuntime({ agentProvider: router }).evaluate( <Agent> Choose an existing playbook subdirectory for this request and preserve uncertainty. {request} </Agent>, Route )
const canonicalRoot = await realpath(root) const candidateDirectory = await realpath(resolve(canonicalRoot, route.directory)) const relativeDirectory = relative(canonicalRoot, candidateDirectory)
// The model proposes a path; host code decides whether it is in the approved root. if ( relativeDirectory === "" || relativeDirectory === ".." || relativeDirectory.startsWith(`..${sep}`) || isAbsolute(relativeDirectory) ) { throw new TypeError("Selected playbook must be a subdirectory of AML_SUPPORT_PLAYBOOKS") }
const Playbook = localWorkspace({ directory: candidateDirectory }) return await new AmlRuntime({ agentProvider: handoff }).evaluate( <Workspace id={`support-playbook-${route.category}`} provider={Playbook}> <Agent> Draft an internal handoff from the selected playbook and this validated route record: {JSON.stringify(route)} Do not take external action; preserve any uncertainty. </Agent> </Workspace> )}
console.log(await RouteSupportRequest("A user cannot sign in after changing their identity provider."))Run it
Section titled “Run it”Save the complete source as recipe.tsx, create an approved fixture outside the code’s control path, then run that exact workflow:
export AML_SUPPORT_PLAYBOOKS=/srv/aml-support-playbooksmkdir -p "$AML_SUPPORT_PLAYBOOKS/account-access"npx vite-node recipe.tsxThe handoff output is deterministic. It includes the route record and the selected Workspace’s scoped content flow; the exact prompt text depends on the runtime’s resolved Workspace content and your fixture files.
Observable result
Section titled “Observable result”A successful run prints an internal handoff containing a record shaped like:
{"category":"account-access","directory":"account-access","normalizedRequest":"The user cannot sign in after changing identity providers.","uncertainties":[...]}If the route points outside the canonical root, resolves to the root itself, or uses a symlink that escapes the root, the application throws before constructing localWorkspace().
How the boundaries work
Section titled “How the boundaries work”evaluate(tree, Route)requests structured output from exactly one<Agent />and validates the result with Zod’s Standard Schema contract. It validates shape, not truth or permission.realpath()canonicalizes the application-owned root and the selected existing directory.relative()then proves the candidate is a proper descendant; this is ordinary application authorization logic, not an AML primitive.localWorkspace({ directory: candidateDirectory })is created only after the containment check.<Workspace />owns its materialization and lock lifecycle for the handoff Agent.- The later
<Agent />receives the route as data and operates within the selected<Workspace />. It does not inherit authorization from the model’s suggestion; the application chose the directory.
Failure, security, and cancellation notes
Section titled “Failure, security, and cancellation notes”realpath()fails when the configured root or selected directory does not exist. The example intentionally does not create model-selected directories.- Reject empty strings and any category-to-directory mapping your application does not explicitly approve. A production router should usually map the category to a host-owned directory rather than trusting a free-form model path.
localWorkspace()directly materializes an existing host directory. It is not a Sandbox and does not isolate processes, network access, credentials, or host filesystem access.- A local Workspace uses a renewable lock by default. Another healthy writer can cause
WorkspaceConflictError; do not treat it as permission to bypass coordination. - Pass an
AbortSignalto the runtime in request-driven code. Cancellation stops future work but cannot roll back direct filesystem writes or external effects already completed. - Treat playbook files and route text as untrusted data. Do not execute scripts from the selected
<Workspace />unless an explicit, appropriately configured<Sandbox />owns that execution boundary.
Variations
Section titled “Variations”- Replace the deterministic router with
codexAgent({ workingDirectory: canonicalRoot })after configuringcodex-acpand credentials. Keep therealpath/containment check unchanged. - Prefer a host-owned map such as
const directories = { "account-access": ... }when categories are known. The model can choose a category, but never needs to choose a filesystem path. - Use
filesystemWorkspace()ors3Workspace()when the handoff needs revision history or remote persistence; read their lock, retention, and conditional-publication contracts first. - Add
<Sandbox />around the later<Agent />when playbook work involves commands. The Workspace boundary persists files; it does not make execution safe.