Skip to content

Route typed data into an authorized Workspace

Validate an Agent’s routing proposal before opening an approved local Workspace.

No credentials neededLocal playbooks required

Turn a model-shaped routing proposal into bounded local work without allowing the proposal to select an arbitrary directory:

  1. an <Agent /> returns a schema-validated route record;
  2. ordinary application code validates category, resolves the approved root and candidate, and enforces canonical containment;
  3. 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.

  • an AML project configured through Getting started, with zod installed;
  • AML_SUPPORT_PLAYBOOKS set to an existing application-owned directory;
  • an existing account-access/playbook.md file 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.

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_PLAYBOOKS
if (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." />
)
)

Save the complete source as recipe.tsx, create an approved fixture outside the code’s control path, then run that exact workflow:

Terminal
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.

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().

  1. Component-local evaluate(tree, Route) collects one typed Agent result inside RouteSupportRequest. The root runtime.evaluate(tree, options) owns the run and accepts cancellation options, not a schema.
  2. 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.
  3. localWorkspace({ directory: candidateDirectory }) is created only after the containment check. <Workspace /> owns its materialization and lock lifecycle for the handoff Agent.
  4. 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.
  • 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 AbortSignal to 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.
  • Replace the deterministic router with codexAgent({ workingDirectory: canonicalRoot }) after configuring codex-acp and credentials. Keep the realpath/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() or s3Workspace() 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.