Repository navigation
Evaluate Agent-generated XMD through a constrained allowlist #369
Description
Story
As a workflow author, I want untrusted XMD returned by an Agent to be validated and executed by the host, so the Agent can request observations and propose changes without receiving direct Workspace or tool authority.
Delivery status
Slice 1: read-only observation admission — delivered
PR #497 supplies the provider-neutral generated-XMD admission boundary. PR #550 supplies the authored workflow seam:
<Evaluate source={turn.source} as="observation" />The trusted workflow host supplies the exact candidate source, immutable pinned observation identities, the retained Workspace roots and selected root, and exact request ceilings such as allowed HTTP requests. The complete fragment is preflighted before its first effect. Admitted observations execute through ordinary durable XMD effects, and the filtered source, decision, observation values, rendered output, identities, roots, and request policy are retained for replay.
<Evaluate> currently accepts only source; omitting the new allow prop below preserves this delivered behavior exactly.
Slice 2: Workspace mutation admission
Extend the same <Evaluate> seam rather than adding a second construct:
<!-- `allow` omitted: read-only, as today -->
<Evaluate source={turn.source} as="observation" />
<!-- generated mutations confined to the run-owned Workspace -->
<Evaluate source={proposal.changes} allow={["write"]} />allow is an optional non-empty set drawn from the closed values read and write. Omission means exactly ["read"]. Duplicate or unknown values are refused before the candidate source is parsed. The normalized set is retained with the admission.
The two values describe effect classes, not component identities:
readselects observation-only identities. It includes core's pinned self-closingFile:readidentity and includes pinnedFetchonly when the host supplied at least one exact request ceiling.Fetchremains the existing non-mutatingGET/HEADcomponent; there is no mutating Fetch form.writeselects mutations confined to the run-owned Workspace. This initial slice admits the exact pinned identities for lexicalDirand pairedFile:write.<File.Delete>and its pinned identity are owned by Add a contextual File.Delete component for contained file removal #567 and join the standard host policy only after both the ordinary component and this admission slice exist. The initial table excludes local Git even though those effects are also Workspace-local; Git-host, Issue, process, eval, exec, native-command, credential, and other external effects are outside the class entirely.
An authored document may request both classes, but same-name forms remain distinct identities. A self-closing <File /> resolves only to File:read; a paired <File>…</File> resolves only to File:write. Admission therefore resolves by the exact pinned definition and admitted form, not by the word File. A same-name repository component, registration, bundle member, or middleware answer satisfies neither identity.
The complete fragment is still preflighted before its first effect. A component, form, request, expression, interpolation, import, code block, or native execution outside the selected classes refuses the whole fragment before any generated effect. Admitted mutations then execute through their ordinary contextual providers, Workspace transactions, durability, and return semantics. Generated source receives no special mutation API.
Binding and output contract
allow and as are independent. allow selects effect classes from host policy; as is the language's ordinary caller-owned binding for the value <Evaluate> returns. A read-, write-, or mixed-policy invocation may be bound:
<Evaluate source={proposal.changes} allow={["write"]} as="evaluation" />This does not grant authority and does not expose the caller's binding environment to generated source. <Evaluate> remains an ordinary function-component boundary: its return crosses to the caller only when the caller writes as, and an unbound non-string return renders nothing.
Slice 2 does not invent a generic mutation results collection. <Evaluate> keeps its delivered return shape:
{
"observations": [{ "name": "File", "value": "..." }],
"output": "..."
}observations contains the ordinary values of admitted read identities, in invocation order. output contains what the generated fragment rendered. Admitted write identities do not acquire synthetic receipts or entries merely because they mutated the Workspace; their durable effect records remain the authoritative account. A write-only fragment therefore normally binds { "observations": [], "output": "" } with this slice's Dir and paired File:write table.
The generated-XMD preflight's delivered refusal of as inside generated source, and of generated binding reads, remains unchanged in this mutation slice. That restriction is separate from ordinary component semantics: authored write operations such as <Git.Commit ... as="commit" /> continue to bind their ordinary returns, and <Evaluate ... as="evaluation" /> remains valid for every allow selection. Exporting named fragment-local bindings would be a separate generated-language contract, including replay and collision semantics; this slice does not need it for #181.
Authored approval
Admission does not prompt and does not approve. Approval is ordinary authored control flow before a write-enabled <Evaluate> is reached.
For PR #181, the existing UserCheckpoint authorizes the converged plan before Implementation begins. That authorization is sufficient for the implementation Agent's generated Workspace changes; the workflow does not require a second approval of the literal fragment. A workflow that wants exact-fragment approval places its own elicitation or suspension gate immediately before <Evaluate allow={["write"]}>.
If the authored approval branch is not taken, the write-enabled <Evaluate> is never invoked and no generated admission or mutation exists.
Host policy and authority
Generated XMD is untrusted input. The Agent output carries data, never authorization. Only the trusted workflow host installs generated-XMD policy, as immutable values captured before any installation, middleware, or document code runs.
The host policy contains separate read and optional write tables. Each table holds exact pinned definitions and their form/request constraints. Slice 2 makes the standard Deno workflow profile supply the Workspace-local write table with lexical Dir and paired File:write. The profile supplies no external-write or execution table. Another host that supplied no write table refuses allow={["write"]} before parsing the generated source. #567 owns the ordinary <File.Delete> component and its later pinned-policy integration; deletion is not part of this initial slice, and #181 cannot consume it until #567 is delivered.
The authored allow prop selects a subset of that already-installed policy. It can narrow or request a class but cannot create a pinned identity, add a root, authorize a destination, attach a credential, or widen a request. Public middleware, document props, eval bindings, repository components, generated names, and another loaded package copy cannot manufacture admission.
Generated source receives none of the policy values. Network destination, method, headers, timeout, retained roots, selected root, exact component definitions, and credentials remain host-owned ceilings beneath the read and write labels.
Replay contract
One ordinary durable generated_xmd admission retains:
- the normalized
allowselection; - the exact admitted source;
- retained roots and selected root;
- every exact pinned identity and admitted form the fragment named; and
- normalized request constraints.
A continuation compares that policy whole and exactly before one generated component is invoked. A changed class, root, selected root, component definition, admitted form, or request ceiling refuses without performing an effect. The retained source, rather than a later caller's candidate, is what expands. Completed replay asks neither the Agent nor a provider to repeat completed work.
Integration with the workflow Agent
#302 supplies the retained Agent session and authored multi-turn loop:
- The Agent returns an observation fragment.
<Evaluate>defaults toread, admits it under the host's read table, and returns its observation value.- The authored
<Evaluate ... as="observation" />invocation binds that value for a later Prompt in the same Agent session. - The Agent eventually returns one final proposal as source data.
- Authored approval control reaches
<Evaluate allow={["write"]}>. - The host intersects that request with its immutable write table and admits the proposal or refuses it whole.
The Agent receives no writable channel through this integration.
Acceptance
Existing read behavior
<Evaluate source={source} />is byte-for-byte and behaviorally compatible with the delivered read-only seam.- Omitted
allownormalizes to["read"]and retains that selection. - Pinned self-closing
File:readand exact boundedFetchcontinue to work; paired File, mutating/unknown HTTP methods, and requests outside the exact Fetch ceiling perform nothing. - Bound read evaluation keeps the delivered
{ observations, output }value; unbound evaluation renders nothing.
Write behavior
allowaccepts only a non-empty, duplicate-free set ofreadandwrite; invalid policy is refused before candidate parsing or retention.allow={["write"]}fails before candidate parsing when the host installed no write table.- The host's immutable write table, never the prop or generated source, selects the exact pinned mutation identities.
- A paired pinned
File:writecan mutate the selected run-owned Workspace through the ordinary Files provider and effect transaction. - Self-closing and paired forms of the same component resolve to different pinned identities; selecting only one class never admits the other form.
- The first consumer admits only the Workspace-local forms its host supplied. Generated Git push, PullRequest, Issue upsert, process, eval, exec, native command, credential, and arbitrary network effects remain refused.
- Mixed admitted and unadmitted syntax produces no generated effect.
- Rejected mutation syntax or authority produces no partial effect.
- Admitted mutations use their ordinary providers, transactions, and durable records; the evaluator creates no generic mutation result or receipt.
ason<Evaluate>is valid forread,write, and mixed selections and binds the same{ observations, output }shape. It neither widens admission nor exports the caller's bindings.- Authored refusal before the write-enabled Evaluate produces no generated admission or mutation.
- Continuation compares the normalized classes, identities, forms, roots, and request ceilings before performing anything; completed replay repeats no mutation.
- Identity substitution, same-name repository components, middleware replacement, another loaded copy, and a changed host policy cannot turn a read identity into a write identity or manufacture either one.
Dependencies
- Commit Workspace mutations and journal results atomically #365 supplies ordinary Workspace transactions for admitted local effects.
- Add the xmd workflow start/resume filesystem vertical slice #366 supplies retained workflow execution and the selected run-owned Workspace.
- 🌐 Fetch retained HTTP observations from Markdown #456 supplies retained declarative HTTP reads; generated Fetch remains bounded to its pinned identity and exact request.
- Run workflow Agents through XMD-mediated observations #302 and PRs 🔐 Give a workflow Agent an empty room and a session it can come back to #549/🔎 Let an agent ask to see something, and see only that #550 supply the live Agent integration and existing
<Evaluate>seam. - The ordinary components admitted by a generated policy must exist before their pinned identities can be added.
Dirand pairedFileexist, so they define this initial slice. Add a contextual File.Delete component for contained file removal #567 owns ordinary<File.Delete>plus its later pinned write-policy integration. It does not block this slice, but it does block PR ✨ Compose and certify the supervised adversarial implementation workflow #181 from consuming generated deletion. - Require ACP adapters to enforce tool-free workflow Agent sessions #496 remains a non-blocking provider-hardening follow-up.
- End goal: ✨ Compose and certify the supervised adversarial implementation workflow #181.
Out of scope
- Direct Agent filesystem, terminal, MCP, or network access.
- ACP
additionalDirectories. - A document prop or public component that grants generated source authority;
allowonly narrows the host's captured policy. - Mutating HTTP through
<Fetch>. - Generated local Git operations, Git-host mutations, Issue upserts, process execution, eval/exec, native commands, credentials, or an unspecified external-write class.
- Workflow-bundled Markdown component admission.
<File.Delete>and its pinned write-policy integration; Add a contextual File.Delete component for contained file removal #567 owns both after this initial slice.- Generated-source
as, binding reads, or exporting a fragment-local binding environment. - Synthetic mutation results or receipts.
- Partial execution before complete-fragment preflight.
- A hidden approval prompt inside
<Evaluate>. - Mandatory exact-fragment approval after a user already authorized the converged plan.
- Unattended approval of mutation proposals.
- Portable ACP adapter-level no-tool enforcement; Require ACP adapters to enforce tool-free workflow Agent sessions #496 owns it.
Activity
📝 Separate the shipped suspension substrate from what this Markdown c…
🔒 Give a workflow Agent nothing but what its prompt renders
📝 Separate the shipped suspension substrate from what this Markdown c…
🔒 Give a workflow Agent nothing but what its prompt renders
📝 Move the remote effects from owed to shipped
📝 Separate the shipped suspension substrate from what this Markdown c…
🔒 Give a workflow Agent nothing but what its prompt renders
📝 Move the remote effects from owed to shipped
📝 Separate the shipped suspension substrate from what this Markdown c…
🔒 Give a workflow Agent nothing but what its prompt renders
📝 Move the remote effects from owed to shipped
🔒 Report the authored form on the invocation the engine issued
📄 Say what the contextual content answer does and does not guarantee
🔒 Report the authored form on the invocation the engine issued (#569)
24 remaining items
📝 Move the remote effects from owed to shipped
📝 Separate the shipped suspension substrate from what this Markdown c…
🔒 Give a workflow Agent nothing but what its prompt renders
📝 Move the remote effects from owed to shipped
📝 Separate the shipped suspension substrate from what this Markdown c…
🔒 Give a workflow Agent nothing but what its prompt renders
📝 Move the remote effects from owed to shipped
📝 Separate the shipped suspension substrate from what this Markdown c…
🔒 Give a workflow Agent nothing but what its prompt renders
📝 Move the remote effects from owed to shipped
🧱 Close the generated-XMD durable protocol and state the evaluation p…
✨ Evaluate generated XMD text with constrained authority (#713) (#756)
📝 Separate the shipped suspension substrate from what this Markdown c…
🔒 Give a workflow Agent nothing but what its prompt renders
📝 Move the remote effects from owed to shipped