Route typed data into an authorized Workspace
Validate an Agent’s routing proposal before opening an approved local 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 reads account-access/playbook.md beneath an existing application-owned playbook directory. The route record is a proposal; application code approves the category-to-directory mapping before opening it.
Prerequisites and status
Section titled “Prerequisites and status”- an AML project configured through Getting started, with
zodinstalled; AML_SUPPORT_PLAYBOOKSset to an existing application-owned directory;- an existing
account-access/playbook.mdfile 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 { type AML, Agent, AmlRuntime, Block, evaluate, Include, 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}` } },})
const RouteSupportRequest: AML.Component<{ request: string; root: string }> = async ({ request, root }) => { const route = await evaluate( <Agent provider={router}> Choose an existing playbook subdirectory for this request and preserve uncertainty. {request} </Agent>, Route )
// Each category is allowed to select only its application-owned subdirectory. if (route.directory !== route.category) { throw new TypeError("Selected directory must match the approved category") }
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 ( <Workspace id={`support-playbook-${route.category}`} provider={Playbook}> <Agent provider={handoff}> Draft an internal handoff from the selected playbook and this validated route record: <Block tag="route">{JSON.stringify(route)}</Block> <Block tag="playbook"> <Include path="playbook.md" title={false} /> </Block> Do not take external action; preserve any uncertainty. </Agent> </Workspace> )}
console.log( await new AmlRuntime().evaluate( <RouteSupportRequest root={root} request="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="$(mktemp -d)"mkdir -p "$AML_SUPPORT_PLAYBOOKS/account-access"printf '%s\n' 'Ask an administrator to verify the identity-provider configuration.' > "$AML_SUPPORT_PLAYBOOKS/account-access/playbook.md"npx vite-node recipe.tsx# After inspecting the output, remove this temporary fixture:rm -r "$AML_SUPPORT_PLAYBOOKS"The handoff fixture echoes its resolved prompt, including the validated route and the exact playbook text supplied by <Include />. A Workspace makes files available; it does not automatically add their contents to a prompt. The fixture demonstrates dataflow rather than model reasoning.
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”- Component-local
evaluate(tree, Route)collects one typed Agent result insideRouteSupportRequest. The rootruntime.evaluate(tree, options)owns the run and accepts cancellation options, not a schema. 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.- The category-to-directory check rejects empty strings and paths that do not match the approved category. Canonical containment also rejects a matching directory name that is a symlink outside the root. Keep the directory tree application-owned to prevent another process replacing paths between validation and use.
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.