From c308a1dd5ec8162d27ed4ef0f54312d3cca191b5 Mon Sep 17 00:00:00 2001 From: Taras Mankovski <74687+taras@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:53:59 -0400 Subject: [PATCH 1/9] =?UTF-8?q?=E2=9C=A8=20Let=20a=20configured=20run=20ch?= =?UTF-8?q?ild=20establish=20the=20Plan=20authorship=20ceiling=20(#728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan authorship becomes a trusted host installation capability: the default agent, plus the operation that installs one Plan invocation's provider inside the ceiling. Everything else about that ceiling — the permission mode, the prompt-failure policy, the capability refusals and the session directory — stays where it was, identical for every provider, so a second implementation cannot bring a weaker one with it. The production ACPX adapter is one concrete provider of that capability, built from the Agent stack a run settled, unchanged. A nested `` that declares a canonical `` is the second: it installs the controlled provider it was already given, again, for the Plan invocation that asks, and gets an authorship root of its own that goes when the child settles. The failure this corrects was assembly order. The outer `xmd test` invocation built one Plan declaration closed over the absence of a stack and handed it to every child, so by the time a child's provider existed the declaration could no longer use it. Children now build their own declaration from the ceiling they settled; a child that settled none resolves the same protected bytes and is refused before a directory, a provider, a turn or a review exists. Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7 --- packages/cli/src/authorship-profile.ts | 133 +++++++++++++---- packages/cli/src/cli.ts | 54 ++++--- packages/cli/src/plan-component.ts | 41 ++---- packages/cli/src/plan.ts | 15 +- packages/cli/src/testing-host.ts | 137 ++++++++++++++++-- .../tests/document-suites/plan/Plan.test.md | 54 +++++++ .../plan/agents/approved-plan.md | 27 ++++ .../plan/plan-markdown.test.ts | 31 ++++ .../tests/document-suites/plan/uses-plan.md | 9 ++ packages/cli/tests/support/plan-harness.ts | 26 ++-- .../cli/tests/support/run-markdown-tier.ts | 63 +++++--- .../cli/tests/testing-execution-host.test.ts | 6 +- packages/test-agent/mod.ts | 11 ++ 13 files changed, 486 insertions(+), 121 deletions(-) create mode 100644 packages/cli/tests/document-suites/plan/Plan.test.md create mode 100644 packages/cli/tests/document-suites/plan/agents/approved-plan.md create mode 100644 packages/cli/tests/document-suites/plan/plan-markdown.test.ts create mode 100644 packages/cli/tests/document-suites/plan/uses-plan.md diff --git a/packages/cli/src/authorship-profile.ts b/packages/cli/src/authorship-profile.ts index baf6d38cf..14bf146a4 100644 --- a/packages/cli/src/authorship-profile.ts +++ b/packages/cli/src/authorship-profile.ts @@ -106,10 +106,8 @@ export interface AuthorshipProfile { * test never reads, creates or removes anything under a real one. */ root: string; - /** The one Agent configuration this invocation settled. */ - stack: AgentStack; - /** What the constrained provider is built on, beyond the host's assembly. */ - acp?: AcpxProviderDependencies; + /** Whether this host can put an Agent under the Plan ceiling, and why not. */ + ceiling: PlanAuthorshipCeiling; /** Who answers the review question. */ installElicitation(): Operation; /** @@ -138,6 +136,93 @@ export interface AuthorshipCeilingInputs { readonly acp?: AcpxProviderDependencies; } +/** + * The trusted host's ability to put an Agent under the Plan ceiling. + * + * A closure the host supplies before any Plan invocation exists, and the only + * thing that decides whether a Plan can be written here. It is never a prop, a + * Context value, a registration result or anything a document, component or + * middleware can reach or replace — which is what keeps "who may write a Plan" + * a question about the host rather than about what a document arranged. + * + * What it does not decide is the rest of the ceiling. The permission mode, the + * prompt-failure policy, the capability refusals and the session directory are + * {@link installAuthorshipFrame}'s, identically for every provider, so a second + * implementation cannot quietly bring a weaker ceiling with it. + */ +export interface PlanAuthorship { + /** The agent this Plan conversation defaults to. */ + readonly defaultAgent: string; + /** + * Install this invocation's Agent provider, inside the Plan ceiling. + * + * Called within ``, so what it registers belongs to that one + * invocation and goes when the invocation does. + * + * @param workdir This conversation's directory, established and proven empty. + * @param host The scope captured before the ceiling existed, for host acts. + */ + installProvider(workdir: string, host: Scope): Operation; +} + +/** + * Whether this host can put an Agent under the Plan ceiling, and why not. + * + * One value rather than an absent capability beside a reason, because the two + * must agree: a host that cannot establish the ceiling owes the person a + * sentence saying so, and a host that can owes no sentence at all. + */ +export type PlanAuthorshipCeiling = + | { readonly established: true; readonly authorship: PlanAuthorship } + | { readonly established: false; readonly refusal: string }; + +/** What a host with no coding-agent ceiling at all refuses a Plan with. */ +export const NO_CEILING = + "this host establishes no coding-agent ceiling, so no Plan can be written here — " + + "no Plan was returned"; + +/** + * The production ceiling: ACPX, built from the Agent stack this run settled. + * + * One concrete provider of {@link PlanAuthorship}, and the only one production + * has. Its ACPX construction, embedded adapters, machine-session assembly, + * system instruction, strict permission policy, empty MCP servers, empty + * allowed tools and controlled working directory are exactly what they were + * when this was the only way to establish a ceiling. + */ +export function planAuthorshipCeiling( + stack: AgentStack | undefined, + acp?: AcpxProviderDependencies, +): PlanAuthorshipCeiling { + if (stack === undefined) { + return { established: false, refusal: NO_CEILING }; + } + if (stack.provider !== "acpx") { + return { + established: false, + refusal: + `the ${stack.provider} provider cannot establish the Plan authorship ceiling — ` + + "no Plan was returned", + }; + } + return { + established: true, + authorship: { + defaultAgent: stack.defaultAgent, + *installProvider(workdir: string, host: Scope): Operation { + const acpx = createAcpxProvider( + authorshipCeiling({ stack, ...(acp === undefined ? {} : { acp }) }, workdir, host), + ); + yield* registerAgentProvider("acpx", acpx); + yield* installInvocationAgentProvider("acpx", { + defaultAgent: stack.defaultAgent, + permissionMode: AUTHORSHIP_PERMISSION_MODE, + }); + }, + }, + }; +} + /** What claiming one conversation's directory needs, and nothing more. */ export interface AuthorshipPlacement { /** Where this host keeps its authorship session directories. */ @@ -149,11 +234,13 @@ export interface AuthorshipPlacement { } /** Everything the constrained authorship frame is built from. */ -export interface AuthorshipFrame extends AuthorshipCeilingInputs { +export interface AuthorshipFrame { /** This conversation's directory, already established and proven empty. */ readonly workdir: string; /** The scope the two host acts run in, captured before this frame exists. */ readonly host: Scope; + /** The host's ability to put an Agent under this ceiling. */ + readonly authorship: PlanAuthorship; installElicitation(): Operation; } @@ -175,19 +262,17 @@ export function* installAuthorshipFrame(frame: AuthorshipFrame): Operation yield* openFormsThroughHost(frame.host); yield* frame.installElicitation(); - // Registered here, so the name resolves to *this* ceiling and not to whatever - // the enclosing document registered under it, and installed in this - // invocation rather than in a frame nested inside it — the content this - // ceiling was selected for is projected into the invocation, and a provider - // installed anywhere else would be invisible to it. The default agent and the - // permission mode travel with the installation, so an enclosing document - // cannot widen either by inheritance. - const acpx = createAcpxProvider(authorshipCeiling(frame, frame.workdir, frame.host)); - yield* registerAgentProvider("acpx", acpx); - yield* installInvocationAgentProvider("acpx", { - defaultAgent: frame.stack.defaultAgent, - permissionMode: AUTHORSHIP_PERMISSION_MODE, - }); + // Installed here, so the provider resolves to *this* ceiling and not to + // whatever the enclosing document registered, and in this invocation rather + // than in a frame nested inside it — the content this ceiling was selected for + // is projected into the invocation, and a provider installed anywhere else + // would be invisible to it. The default agent and the permission mode travel + // with the installation, so an enclosing document cannot widen either by + // inheritance. + // + // Which provider it is belongs to the host that supplied the capability. What + // does not is everything below: one ceiling, whoever is underneath it. + yield* frame.authorship.installProvider(frame.workdir, frame.host); // A candidate comes from a turn's complete successful close value or from // nowhere. `` ordinarily renders whatever a failed turn managed to // emit and carries on, which for a workflow that reviews source would mean @@ -212,13 +297,9 @@ export function* runPlanCommandDocument(profile: AuthorshipProfile): Operation> { @@ -227,7 +308,7 @@ export function* runPlanCommandDocument(profile: AuthorshipProfile): Operation` there resolves the // same protected bytes and is refused at the ceiling rather than told the // component does not exist. - const plan = yield* planComponentDeclaration({ - surface: "component", - includes: include, - ...(mode.agent === undefined ? {} : { stack: mode.agent }), - ...(mode.machineSessions === undefined ? {} : { sessions: mode.machineSessions }), - ...(mode.planAuthorshipRoot === undefined ? {} : { authorshipRoot: mode.planAuthorshipRoot }), - // Captured before the document exists, so the two acts that are this host's - // — putting this build's adapter on disk, and opening the review form — run - // outside the ceiling the Component installs around itself. + // + // A factory rather than a value, because a nested `` + // learns what ceiling it can establish only after its own configuration has + // been read — and a declaration built out here would have closed over the + // absence of one before that child existed. Each caller supplies the ceiling + // it settled, the authorship root it owns and the scope its host acts run in; + // everything else about the Component is this entrypoint's and identical for + // all of them. + const planDeclaration = (request: ChildPlanDeclaration): Operation => + planComponentDeclaration({ + surface: "component", + includes: include, + ceiling: request.ceiling, + ...(mode.machineSessions === undefined ? {} : { sessions: mode.machineSessions }), + ...(request.authorshipRoot !== undefined + ? { authorshipRoot: request.authorshipRoot } + : mode.planAuthorshipRoot === undefined + ? {} + : { authorshipRoot: mode.planAuthorshipRoot }), + // Captured before the document exists, so the two acts that are this + // host's — putting this build's adapter on disk, and opening the review + // form — run outside the ceiling the Component installs around itself. + host: request.host, + installElicitation: installWebElicitation, + // Rendered when a `` first asks, not before: an ordinary run that + // writes none never builds a catalog it has no reader for. + *catalog() { + return renderSyntaxMarkdown(yield* syntaxCatalog(include)); + }, + }); + + const plan = yield* planDeclaration({ + ceiling: planAuthorshipCeiling(mode.agent), host: yield* useScope(), - installElicitation: installWebElicitation, - // Rendered when a `` first asks, not before: an ordinary run that - // writes none never builds a catalog it has no reader for. - *catalog() { - return renderSyntaxMarkdown(yield* syntaxCatalog(include)); - }, }); // Wire --verbose observability via Signal. @@ -1062,7 +1082,7 @@ function* runDocument( // invocation identity, its own leases and its own Push evidence. installRepositories: childRepositories, testAgentWorker: yield* readWorkerCommand(), - plan, + planDeclaration, }); // One authoritative execution, and only one. What a host attaches travels as diff --git a/packages/cli/src/plan-component.ts b/packages/cli/src/plan-component.ts index ff65ecfd0..2a2296b27 100644 --- a/packages/cli/src/plan-component.ts +++ b/packages/cli/src/plan-component.ts @@ -55,15 +55,14 @@ import type { IdentityComponent, } from "@executablemd/core/host"; import type { DocumentValidation } from "@executablemd/core"; -import type { AcpxProviderDependencies } from "@executablemd/acp"; import type { ComponentInvocation } from "@executablemd/core"; -import type { AgentStack } from "./agent-stack.ts"; import { DEFAULT_AUTHORSHIP_ROOT, installAuthorshipFrame, useSessionDirectory, } from "./authorship-profile.ts"; +import type { PlanAuthorshipCeiling } from "./authorship-profile.ts"; import type { CandidateAssessment } from "./authorship-profile.ts"; import type { MachineSessionAssembly } from "./session-coordinator.ts"; import { PLAN_DOCUMENT, readPackagedDocument } from "./packaged-document.ts"; @@ -108,19 +107,21 @@ export interface PlanComponentAssembly { /** The component search path a Plan's own components resolve against. */ readonly includes: readonly string[]; /** - * The one Agent configuration this invocation settled, when it settled one. + * Whether this host can put an Agent under the Plan ceiling, and why not. * - * Absent is a host that has no run stack — `xmd test` drives agents through - * the deterministic TestAgent stack — and it is not a reason to withhold the - * Component. A document that writes `` there resolves the same protected - * bytes and is refused at the ceiling, before any placement, rather than told - * the component does not exist. + * A host that establishes none — `xmd test` at its own root, or an + * unconfigured run child — still declares the Component. A document that + * writes `` there resolves the same protected bytes and is refused at + * the ceiling, before any placement, rather than told the component does not + * exist. + * + * The capability is a closure the host supplied before this declaration + * existed. No prop, binding, registration, middleware answer or separately + * loaded copy can supply or replace one. */ - readonly stack?: AgentStack; + readonly ceiling: PlanAuthorshipCeiling; /** What this host states about machine-wide agent sessions, if anything. */ readonly sessions?: MachineSessionAssembly; - /** What the constrained provider is built on, beyond the host's assembly. */ - readonly acp?: AcpxProviderDependencies; /** * Where this host keeps its authorship session directories. * @@ -427,18 +428,9 @@ function planAuthorship(assembly: PlanComponentAssembly): IdentityComponent { // cannot establish this ceiling refuses rather than writing a Plan under // a weaker one, and broader authority in the calling document cannot // widen it. - const stack = assembly.stack; - if (stack === undefined) { - throw new Error( - "this host establishes no coding-agent ceiling, so no Plan can be written here — " + - "no Plan was returned", - ); - } - if (stack.provider !== "acpx") { - throw new Error( - `the ${stack.provider} provider cannot establish the Plan authorship ceiling — ` + - "no Plan was returned", - ); + const ceiling = assembly.ceiling; + if (!ceiling.established) { + throw new Error(ceiling.refusal); } const session = String(props.session); @@ -457,8 +449,7 @@ function planAuthorship(assembly: PlanComponentAssembly): IdentityComponent { yield* installAuthorshipFrame({ workdir: established.value, - stack, - ...(assembly.acp === undefined ? {} : { acp: assembly.acp }), + authorship: ceiling.authorship, host: assembly.host, installElicitation: assembly.installElicitation, }); diff --git a/packages/cli/src/plan.ts b/packages/cli/src/plan.ts index 469a52a93..bd2e14566 100644 --- a/packages/cli/src/plan.ts +++ b/packages/cli/src/plan.ts @@ -67,7 +67,11 @@ import type { AcpxProviderDependencies } from "@executablemd/acp"; import { cwd } from "@executablemd/runtime"; import type { AgentStack } from "./agent-stack.ts"; -import { DEFAULT_AUTHORSHIP_ROOT, runPlanCommandDocument } from "./authorship-profile.ts"; +import { + DEFAULT_AUTHORSHIP_ROOT, + planAuthorshipCeiling, + runPlanCommandDocument, +} from "./authorship-profile.ts"; import type { CandidateAssessment } from "./authorship-profile.ts"; import { PLAN_IDENTITY, planComponentDeclaration } from "./plan-component.ts"; import type { MachineSessionAssembly } from "./session-coordinator.ts"; @@ -226,14 +230,16 @@ export function* runPlan(command: PlanCommand, deps: PlanDependencies): Operatio // disk, and opening the review form — and both run a command, which the // ceiling the Component installs refuses to everything inside it. const host = yield* useScope(); + // The one ceiling this invocation can establish, settled before the + // declaration exists so nothing the document does can reach or replace it. + const ceiling = planAuthorshipCeiling(command.stack, deps.acp); const declaration = yield* planComponentDeclaration({ surface: "command", // The adapter root resolves no repository component, and neither does the // Component it invokes. A Plan's own components are the caller's business, // and the final gate below is where they are resolved. includes: command.include, - stack: command.stack, - ...(deps.acp === undefined ? {} : { acp: deps.acp }), + ceiling, authorshipRoot: root, session, explicitSession, @@ -255,8 +261,7 @@ export function* runPlan(command: PlanCommand, deps: PlanDependencies): Operatio session, explicitSession, root, - stack: command.stack, - ...(deps.acp === undefined ? {} : { acp: deps.acp }), + ceiling, installElicitation: deps.installElicitation, declaration, assess: assessOne, diff --git a/packages/cli/src/testing-host.ts b/packages/cli/src/testing-host.ts index f6d3218ca..84d8384ab 100644 --- a/packages/cli/src/testing-host.ts +++ b/packages/cli/src/testing-host.ts @@ -28,17 +28,29 @@ import type { DurableEvent } from "@executablemd/durable-streams"; import { forEach } from "@effectionx/stream-helpers"; import { API, useHostFiles } from "@executablemd/runtime"; import { installWebElicitation } from "@executablemd/web"; -import type { Operation, Result } from "effection"; +import { ensure, until, useScope } from "effection"; +import type { Operation, Result, Scope } from "effection"; import { agentIdentityComponents, fileSource, inlineSource, installAgentComponents, + registerAgentProvider, } from "@executablemd/core"; -import type { RootDocumentSource } from "@executablemd/core"; -import { executeInstalled, installAnswerProvider } from "@executablemd/core/host"; +import type { AgentComponentsOptions, RootDocumentSource } from "@executablemd/core"; +import { + executeInstalled, + installAnswerProvider, + installInvocationAgentProvider, +} from "@executablemd/core/host"; +import { mkdir, rm } from "node:fs/promises"; +import { randomUUID } from "node:crypto"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { NO_CEILING } from "./authorship-profile.ts"; +import type { PlanAuthorshipCeiling } from "./authorship-profile.ts"; import type { ExecutionInstallation } from "@executablemd/core/host"; -import { installChildTestAgent } from "@executablemd/test-agent"; +import { installChildTestAgent, TEST_AGENT_PROVIDER } from "@executablemd/test-agent"; import type { AnswersChildConfiguration, ChildInvocation, @@ -51,21 +63,35 @@ import { installDocumentComponents } from "./cli.ts"; import type { HostServiceInstaller } from "./cli.ts"; import type { RepositoryInstaller } from "./run-repositories.ts"; +/** What one child asks the entrypoint to build its `` declaration from. */ +export interface ChildPlanDeclaration { + /** Whether this child can put an Agent under the Plan ceiling, and why not. */ + readonly ceiling: PlanAuthorshipCeiling; + /** The authorship root the host made for this child, when it made one. */ + readonly authorshipRoot?: string; + /** The scope this child's own host acts run in. */ + readonly host: Scope; +} + /** What the entrypoint already decided, and a child must not decide again. */ export interface TestingHostSettings { /** The component search path this run resolves names through. */ readonly includes: string[]; /** - * The packaged `` Component this run profile declares. + * How this run profile builds a `` declaration for one execution. * * A child of the run profile writes `` and means what a `` in the - * parent means, so the declaration crosses as the value the parent built - * rather than being rebuilt here from state a child cannot see. It crosses - * even from an entrypoint that settled no Agent stack: the child is the - * production run profile whatever launched it, and a `` there is refused - * at the ceiling rather than reported as a component nothing supplies. + * parent means, so the Component, its origin, its digest and its private + * closure come from the entrypoint rather than from state a child could + * reach. What the child supplies is the part only the child knows: the + * ceiling its own configuration established, the authorship root the host + * made for it, and its own scope. + * + * A declaration built once out there and shared would close over the absence + * of a ceiling before any child configuration had been read, which is exactly + * why a configured child could not write a Plan. */ - readonly plan: DeclaredMarkdownComponent; + readonly planDeclaration: (request: ChildPlanDeclaration) => Operation; /** Whether durable events are scanned for credentials before they persist. */ readonly secretDetection: boolean; /** The native service adapter this entrypoint supplies. */ @@ -151,6 +177,65 @@ function selectConfiguration(request: HostProfileRequest): { return { testAgent, answers }; } +/** + * The Plan ceiling a configured child establishes: the controlled provider it + * was already given, installed again for the Plan invocation that asks. + * + * Installed *inside* `` rather than inherited from what the + * child registered around itself, so the Plan conversation gets the deny-all + * permission mode, the prompt-failure policy and the capability refusals that + * every Plan gets, whichever provider is underneath. The provider is the + * child's own partition, which is what lets a `` address the + * Plan's session by name. + * + * The partition, the scenarios and this closure belong to one child. A sibling + * that declares the same thing provisions all of it again, and neither reaches + * the other. + */ +function controlledCeiling(agents: AgentComponentsOptions): PlanAuthorshipCeiling { + const root = agents.rootProvider; + const defaultAgent = agents.defaultAgent; + if (root === undefined || defaultAgent === undefined) { + // Not reachable from `installChildTestAgent`, which states both. A child + // that somehow reached here has no provider to put under the ceiling, and + // saying so is the honest answer rather than establishing one anyway. + return { established: false, refusal: NO_CEILING }; + } + return { + established: true, + authorship: { + defaultAgent, + *installProvider(): Operation { + yield* registerAgentProvider(TEST_AGENT_PROVIDER, root.factory); + yield* installInvocationAgentProvider(TEST_AGENT_PROVIDER, { + defaultAgent, + permissionMode: PLAN_PERMISSION_MODE, + }); + }, + }, + }; +} + +/** The permission mode every Plan conversation runs under, test or production. */ +const PLAN_PERMISSION_MODE = "deny-all"; + +/** + * A Plan authorship root this child owns and nothing else can reach. + * + * Not the child's working directory, not the outer test's, not the process + * home and not anything a document named: an agent writing a program has no + * business in the tree the program will run in. Registered before the + * directory exists, so a partial creation is still cleaned up, and recursive + * because the Plan sessions underneath it are this child's too — including a + * named one, which production keeps and a test may not. + */ +function* useChildAuthorshipRoot(): Operation { + const root = join(tmpdir(), `xmd-child-plan-${randomUUID()}`); + yield* ensure(() => until(rm(root, { recursive: true, force: true }))); + yield* until(mkdir(root, { recursive: true })); + return root; +} + function* runProfileChild( invocation: ChildInvocation, settings: TestingHostSettings, @@ -186,6 +271,11 @@ function* runProfileChild( yield* installDocumentComponents({ testing: false }, false); const installations: ExecutionInstallation[] = []; + // What this child can establish for a `` written inside it. A child + // nobody configured establishes nothing, which is the refusal `` has + // always given where no coding-agent ceiling exists. + let ceiling: PlanAuthorshipCeiling = { established: false, refusal: NO_CEILING }; + let authorshipRoot: string | undefined; if (testAgent !== undefined) { const worker = settings.testAgentWorker; if (!worker.ok) { @@ -200,15 +290,30 @@ function* runProfileChild( // separately because its implementation names durable work after its own // invocation, so the execution is told about it rather than a registration // being made for it. - yield* installAgentComponents( - yield* installChildTestAgent(testAgent, { workerCommand: worker.value }), - ); + const agents = yield* installChildTestAgent(testAgent, { workerCommand: worker.value }); + yield* installAgentComponents(agents); installations.push({ components: agentIdentityComponents() }); + // Created out here, outside the ceiling that refuses a directory to + // everything inside it, and owned by this child alone: the Plan invocation + // still makes and proves its own empty session directory underneath it, and + // the whole tree goes when this child settles however it settles. + authorshipRoot = yield* useChildAuthorshipRoot(); + ceiling = controlledCeiling(agents); } // The production run profile's own vocabulary, whichever command launched the // child: `` means the run profile, and a child that - // could not resolve `` would be a different one. - installations.push({ declarations: [settings.plan] }); + // could not resolve `` would be a different one. Built here, from what + // this child settled above, rather than taken from a declaration the + // entrypoint built before this child's configuration had been read. + installations.push({ + declarations: [ + yield* settings.planDeclaration({ + ceiling, + ...(authorshipRoot === undefined ? {} : { authorshipRoot }), + host: yield* useScope(), + }), + ], + }); // A child gets what `xmd run` gets, and the browser form is part of that. // Installed here rather than inherited: this scope is isolated from the // command that started it, so the run profile's provider reaches a child only diff --git a/packages/cli/tests/document-suites/plan/Plan.test.md b/packages/cli/tests/document-suites/plan/Plan.test.md new file mode 100644 index 000000000..5f72a0742 --- /dev/null +++ b/packages/cli/tests/document-suites/plan/Plan.test.md @@ -0,0 +1,54 @@ +# Running `` in a Markdown test + +`` asks a coding agent to write a program and asks a person to approve it. +Both of those are reasons a test could not run one: an agent is slow and +different every time, and a review opens a browser form somebody has to click. + +A nested `` answers both. Declaring `` inside it +gives that child a scripted agent, and declaring `` gives it the review +decision, so the child document below runs the ordinary public component with +nothing in it changed for testability. + +Two kinds of path appear below, and they resolve differently on purpose. A +scenario's `src` is a file this document reads, so it is written relative to this +document. A child's `target` is a document the run host resolves, so it is +written relative to the working directory — the repository root, where each +runtime starts its test process. + +## The successful path is not here yet + +The two rows this suite exists for — a document capturing its approved Plan, and +the approved source not having run — are not written here, because a scenario +cannot address the conversation `` opens. + +`` places its conversation under a session it derives from the expansion +that asked for it, so `` talks to +`xmd-plan:<64 hex digits>` rather than to `planner`. A scenario maps one exact +agent and logical session, and an omitted `session` maps the unnamed default +rather than acting as a wildcard (specs/test-agent-spec.md, "Behavior +documents"). There is therefore no name a checked-in test can write that reaches +the Plan's turn, and the child fails with `no maps agent +"test" and session "xmd-plan:…"`. + +The refusal below is the part that does hold today, and it is the part that +proves the ceiling is real. + +## Without a scripted agent there is no ceiling to write under + +`` establishes an agent ceiling before it makes a directory, starts a +conversation or asks anybody anything. A child nobody configured has no agent to +put under one, so it is refused there — not given a live coding agent, and not +told the component does not exist. + + + + + + + + + + diff --git a/packages/cli/tests/document-suites/plan/agents/approved-plan.md b/packages/cli/tests/document-suites/plan/agents/approved-plan.md new file mode 100644 index 000000000..82b4bc44a --- /dev/null +++ b/packages/cli/tests/document-suites/plan/agents/approved-plan.md @@ -0,0 +1,27 @@ +# The Plan this scenario writes + +The coding agent behind a Plan is asked for one thing: a complete program, as +source and nothing else. This scenario answers that request with the same +program every time, so a test reading the approved source is reading a decision +this document made rather than a model's. + + + +The program is bound rather than written out, because writing it out would run +it: a `` element in this document's body is an element this worker +expands. Interpolating the same text emits it as the characters it is, which is +what an agent replying with source actually sends. + +the approved Plan ran', + "", + ].join("\n")} +/> + +{program} diff --git a/packages/cli/tests/document-suites/plan/plan-markdown.test.ts b/packages/cli/tests/document-suites/plan/plan-markdown.test.ts new file mode 100644 index 000000000..a868d04e5 --- /dev/null +++ b/packages/cli/tests/document-suites/plan/plan-markdown.test.ts @@ -0,0 +1,31 @@ +/** + * Tier PMT — the checked-in Markdown suite for `` under ``, + * launched once per runtime corpus. + * + * The row evidence lives in `Plan.test.md`; this wrapper asserts only that the + * Markdown suite produced passing rows. What a document cannot observe about + * itself — which provider the Plan invocation received, under what + * restrictions, in which authorship root, and what an unconfigured child did + * before it refused — is `../../testing-execution-host.test.ts`. + */ + +import { describe, it } from "@executablemd/test-support/bdd"; +import { expect } from "@executablemd/test-support/expect"; +import { runMarkdownTier } from "../../support/run-markdown-tier.ts"; + +describe( + "Tier PMT — checked-in Markdown suite", + { sanitizeOps: false, sanitizeResources: false }, + () => { + it("runs Plan.test.md once under the production run host", function* () { + const run = yield* runMarkdownTier("packages/cli/tests/document-suites/plan/Plan.test.md"); + if (!run.completion.ok) { + throw run.completion.error; + } + expect(run.results.length).toBeGreaterThan(0); + for (const result of run.results) { + expect(result.status).toBe("pass"); + } + }); + }, +); diff --git a/packages/cli/tests/document-suites/plan/uses-plan.md b/packages/cli/tests/document-suites/plan/uses-plan.md new file mode 100644 index 000000000..197167be7 --- /dev/null +++ b/packages/cli/tests/document-suites/plan/uses-plan.md @@ -0,0 +1,9 @@ +# A document that writes a Plan + +This is an ordinary document. It writes `` the way any author would, with +no knowledge that a test is running it: nothing here names a scenario, an +answer, or a provider. + +Write the release program. + +Approved source: {approved} diff --git a/packages/cli/tests/support/plan-harness.ts b/packages/cli/tests/support/plan-harness.ts index c408a4ea7..bf80e74dc 100644 --- a/packages/cli/tests/support/plan-harness.ts +++ b/packages/cli/tests/support/plan-harness.ts @@ -28,6 +28,7 @@ import { syntaxCatalog } from "../../src/syntax.ts"; import { planComponentDeclaration } from "../../src/plan-component.ts"; import type { PlanSurface } from "../../src/plan-component.ts"; import type { CandidateAssessment } from "../../src/authorship-profile.ts"; +import { planAuthorshipCeiling } from "../../src/authorship-profile.ts"; import type { AgentStack } from "../../src/agent-stack.ts"; import type { DeclaredMarkdownComponent } from "@executablemd/core/host"; import type { PlanDependencies, PlanExecution } from "../../src/plan.ts"; @@ -295,21 +296,24 @@ export function* planDeclarationHarness(options: { const declaration = yield* planComponentDeclaration({ surface: options.surface, includes: options.includes ?? [], - ...(options.stack === null - ? {} - : { - stack: options.stack ?? { + // The production ceiling, built from the stack a case states and this + // harness's own ACPX seams. `stack: null` is a host that settled none, which + // is what `xmd test` at its own root and an unconfigured run child are. + ceiling: planAuthorshipCeiling( + options.stack === null + ? undefined + : (options.stack ?? { provider: "acpx", defaultAgent: AGENT, permissionMode: "deny-all", adapters: ADAPTERS, - }, - }), - acp: { - createRuntime: fake.create, - sessionStore: options.store ?? makeStore(), - agentRegistry: makeRegistry({ [AGENT]: `${AGENT}-cmd` }), - }, + }), + { + createRuntime: fake.create, + sessionStore: options.store ?? makeStore(), + agentRegistry: makeRegistry({ [AGENT]: `${AGENT}-cmd` }), + }, + ), authorshipRoot: options.authorshipRoot, ...(options.session === undefined ? {} : { session: options.session }), ...(options.explicitSession === undefined ? {} : { explicitSession: options.explicitSession }), diff --git a/packages/cli/tests/support/run-markdown-tier.ts b/packages/cli/tests/support/run-markdown-tier.ts index f63638bcf..3a3688ad2 100644 --- a/packages/cli/tests/support/run-markdown-tier.ts +++ b/packages/cli/tests/support/run-markdown-tier.ts @@ -18,16 +18,18 @@ * result left unread would hold the completion open. */ -import { Ok, scoped, useScope } from "effection"; +import { Ok, scoped } from "effection"; import type { Operation, Result } from "effection"; import { forEach } from "@effectionx/stream-helpers"; import { InMemoryStream } from "@executablemd/durable-streams"; -import { useHostFiles } from "@executablemd/runtime"; +import { API, useHostFiles } from "@executablemd/runtime"; +import { installAgentComponents } from "@executablemd/core"; import type { Json } from "@executablemd/core"; +import { installTestAgentComponents, testAgentChildDeclaration } from "@executablemd/test-agent"; import { executeInstalled } from "@executablemd/core/host"; import { testHarnessInstallation, useTesting } from "@executablemd/testing"; import type { TestResult } from "@executablemd/testing"; -import { cliBase, cliRuntime } from "@executablemd/test-support/launch"; +import { cliBase, cliCommand, cliRuntime } from "@executablemd/test-support/launch"; import { planComponentDeclaration } from "../../src/plan-component.ts"; import { renderSyntaxMarkdown, syntaxCatalog } from "../../src/syntax.ts"; import { testingExecutionHost } from "../../src/testing-host.ts"; @@ -57,7 +59,23 @@ export interface MarkdownTierRun { export function runMarkdownTier(document: string): Operation { return scoped(function* () { yield* useHostFiles(); + // How this process re-invokes itself, which only a runtime-named entrypoint + // knows and this harness bypasses. A scripted child agent is a subprocess, + // so without it a configured `` child cannot start one. + yield* API.Env.around({ + // deno-lint-ignore require-yield + *command([args]): Operation { + const invocation = cliCommand(args ?? []); + return [invocation.command, ...invocation.arguments]; + }, + }); const tests = yield* useTesting(); + // What `xmd test` installs beside the session, in the order it installs + // them: `` before the Agent words, so its `` interceptor + // is the nearer one. A suite that declares a scripted agent for a nested + // child needs the outer document to be able to write the declaration. + yield* installTestAgentComponents(); + yield* installAgentComponents(); const installService = SERVICES[cliRuntime()]; const testingHost = testingExecutionHost({ includes: ["components", "."], @@ -68,26 +86,35 @@ export function runMarkdownTier(document: string): Operation { // ask. testAgentWorker: Ok([...cliBase(), "test-agent"]), // The run profile's own ``, so a child assembled here has the - // vocabulary a child assembled by `xmd run` has. This harness settles no - // Agent stack, so a document that writes one resolves the packaged Component - // and is refused at the ceiling — which is what a host with no coding - // agent should say, rather than that the component does not exist. - plan: yield* planComponentDeclaration({ - surface: "component", - includes: ["components", "."], - host: yield* useScope(), - // deno-lint-ignore require-yield - *installElicitation(): Operation {}, - *catalog(): Operation { - return renderSyntaxMarkdown(yield* syntaxCatalog(["components", "."])); - }, - }), + // vocabulary a child assembled by `xmd run` has. What ceiling it can + // establish is the child's own answer, settled after that child's + // configuration has been read: a configured `` child gets the + // controlled one, and every other child gets none and is refused at the + // ceiling — which is what a host with no coding agent should say, rather + // than that the component does not exist. + planDeclaration: (request) => + planComponentDeclaration({ + surface: "component", + includes: ["components", "."], + ceiling: request.ceiling, + ...(request.authorshipRoot === undefined + ? {} + : { authorshipRoot: request.authorshipRoot }), + host: request.host, + // deno-lint-ignore require-yield + *installElicitation(): Operation {}, + *catalog(): Operation { + return renderSyntaxMarkdown(yield* syntaxCatalog(["components", "."])); + }, + }), // This harness runs Markdown tiers, not repository work: a child that // asked for a checkout is told there is no provider. installRepositories: unsupportedRepositories, }); const execution = yield* executeInstalled({ path: document, stream: new InMemoryStream() }, [ - testHarnessInstallation(testingHost), + // The child declarations `xmd test` hands the harness, so a suite can + // configure a nested run's Agent the way the command lets one. + testHarnessInstallation(testingHost, [testAgentChildDeclaration()]), ]); yield* forEach(function* () {}, execution.output); const completion = yield* execution; diff --git a/packages/cli/tests/testing-execution-host.test.ts b/packages/cli/tests/testing-execution-host.test.ts index 34571d81b..ba6d596dc 100644 --- a/packages/cli/tests/testing-execution-host.test.ts +++ b/packages/cli/tests/testing-execution-host.test.ts @@ -504,9 +504,9 @@ describe("deterministic dependencies declared for a nested run", () => { installService: function* (): Operation {}, installRepositories: unsupportedRepositories, testAgentWorker: Err(new Error("xmd command not installed")), - // The run profile's own Component travels to every child, and this case is - // about the relaunch it cannot perform rather than about ``. - plan: yield* planComponentDescription(), + // The run profile's own Component is built for every child, and this case + // is about the relaunch it cannot perform rather than about ``. + planDeclaration: () => planComponentDescription(), }); const refusal = yield* scoped(function* () { try { diff --git a/packages/test-agent/mod.ts b/packages/test-agent/mod.ts index 3235c4ebe..0cafc4128 100644 --- a/packages/test-agent/mod.ts +++ b/packages/test-agent/mod.ts @@ -56,3 +56,14 @@ export { runTestAgentWorker } from "./src/worker/run.ts"; * by the first crosses into the second. */ export { installChildTestAgent, testAgentChildDeclaration } from "./src/child-configuration.ts"; + +/** + * The provider name a controlled child registers its Agent under. + * + * Published so a trusted host can install that same provider again where it + * needs one — under the Plan authorship ceiling, which registers its own + * provider for the invocation rather than inheriting whatever surrounds it. + * Holding the name grants nothing: what it resolves to is the partition the + * host provisioned for that child. + */ +export { TEST_AGENT_PROVIDER } from "./src/provider.ts"; From 375100c1c38479ec5189552efdcbcb668883fcdb Mon Sep 17 00:00:00 2001 From: Taras Mankovski <74687+taras@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:15:22 -0400 Subject: [PATCH 2/9] =?UTF-8?q?=E2=9C=85=20Pin=20which=20declarations=20co?= =?UTF-8?q?nfigure=20a=20Plan=20ceiling,=20and=20which=20profiles=20have?= =?UTF-8?q?=20one=20(#728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two host cases for the boundaries the capability draws. A repository `TestAgent.md` ends the scan, so what an `` prefix holds is an ordinary component invocation rather than a declaration this host recognizes. A child whose `` found a ceiling anyway would mean the capability came from the name rather than from the definition ordinary resolution selected, so the child is held to the same refusal an unconfigured one gets. A direct `xmd test` root does not resolve `` at all: the run profile's vocabulary is installed for a run, and the testing profile declares the Component to the production run children it launches and to nothing else. Either refusal would satisfy "a test root cannot write a Plan", so the case pins which one is delivered — a later change that gave the test root the declaration, and therefore a ceiling to be refused at, is one somebody has to make deliberately. Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7 --- .../cli/tests/testing-execution-host.test.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/packages/cli/tests/testing-execution-host.test.ts b/packages/cli/tests/testing-execution-host.test.ts index ba6d596dc..05fa00d00 100644 --- a/packages/cli/tests/testing-execution-host.test.ts +++ b/packages/cli/tests/testing-execution-host.test.ts @@ -469,6 +469,71 @@ describe("deterministic dependencies declared for a nested run", () => { expect(result.code).toBe(0); }); + /** + * PMT6 — only the canonical declaration configures a Plan ceiling. + * + * The repository file ends the scan, so what the `` prefix holds is + * an ordinary component invocation rather than a declaration this host + * recognizes. A child whose `` found a ceiling anyway would mean the + * capability came from the name rather than from the definition ordinary + * resolution selected. + */ + it("configures no Plan authorship ceiling from a repository TestAgent", function* () { + const project = yield* useProject({ + "agents/review.md": BEHAVIOR, + // Chosen ahead of the package's, so this is ordinary assertion content. + "components/TestAgent.md": doc("a repository component"), + "writes-a-plan.md": doc('Write a program.'), + "README.md": doc( + '', + '', + '', + "", + '', + "", + "", + "", + "", + ), + }); + const result = yield* runCli(["test", "README.md"], { cwd: project, ...WORKER }).join(); + expect(result.stdout + result.stderr).not.toContain("❌"); + expect(result.code).toBe(0); + }); + + /** + * PMT7 — the direct test root is the testing profile, not a Plan host. + * + * The run profile's vocabulary is installed for a run, and `xmd test` is a + * different profile: it declares `` to the production run children it + * launches and to nothing else, so its own root does not resolve the name at + * all (`cli.ts`, where the declaration is withheld under `mode.testing`). + * + * Either refusal would satisfy "a test root cannot write a Plan". This case + * pins which one is delivered, so a later change that quietly gave the test + * root the declaration — and therefore a ceiling to be refused at — is a + * change somebody has to make deliberately. + */ + it("gives a direct test root no Plan authorship authority", function* () { + const project = yield* useProject({ + "README.md": doc( + '', + 'Write a program.', + "", + ), + }); + const result = yield* runCli(["test", "README.md"], { cwd: project, ...WORKER }).join(); + expect(result.code).toBe(1); + const reported = result.stdout + result.stderr; + expect(reported).toContain("Cannot resolve component: Plan"); + // And no ceiling was established for it to be refused at, which is the + // difference between "not this profile" and "this profile, no agent". + expect(reported).not.toContain("establishes no coding-agent ceiling"); + }); + it("refuses an unreadable behavior document before the child's root is imported", function* () { const project = yield* useProject({ "two-turns.md": doc("the child imported its root"), From 2c734b17cb7be857bfaf9e6d84b4e7d0e10c6ded Mon Sep 17 00:00:00 2001 From: Taras Mankovski <74687+taras@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:16:37 -0400 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=93=9D=20Describe=20the=20three=20sur?= =?UTF-8?q?faces=20that=20state=20a=20Plan=20ceiling=20(#728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The declared `` section of the executable MDX specification, the authorship-profile entry in the architecture inventory, and the deterministic Agent configuration section of the testing specification each described one way to establish the Plan ceiling, because there was one. They now describe three surfaces and keep them distinct: a production run states the ACPX ceiling from the Agent stack it settled; a nested run child declaring a canonical `` states the deterministic one from the controlled provider that declaration produced; everything else states none and carries the sentence a person reads instead. What the ceiling does once stated is the same for both — the permission mode, the prompt-failure policy, the capability refusals and the empty session directory are installed in one place rather than restated by each provider, so a second implementation cannot bring a weaker ceiling with it. `specs/test-agent-spec.md` is deliberately untouched: what it says about session matching is the open question this story is blocked on, and writing it now would mean rewriting it after. Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7 --- architecture.md | 2 +- specs/executable-mdx-spec.md | 18 ++++++++++++++++++ specs/testing-spec.md | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/architecture.md b/architecture.md index b3d26989f..de8441630 100644 --- a/architecture.md +++ b/architecture.md @@ -44,7 +44,7 @@ Existing documents and code get aligned to this section retroactively. | Plan | the executable program produced from a Prompt: an Executable Markdown document combining readable prose that expresses the Prompt's intent with the components that carry it out, each placed beside the prose describing what it does. It begins with one descriptive level-one heading. A Plan is what `xmd plan` approves and then delivers: printed to stdout by default, written to an `--output` file, and run only under `--run`. It is not a synonym for a workflow, a policy document or any executable Markdown file | | plan command document | the one exact checked-in first-party Markdown value root `xmd plan` executes. It is the command's adapter and nothing else: it projects the request into ``, supplies the session, and returns the approved source. It is not itself a Plan. Internal: no command-line option selects another one, and no repository component search can answer for it | | packaged `` Component | the one exact checked-in first-party Markdown value component that converts a Prompt into a Plan, `packages/cli/src/documents/Plan.md`, declared to every ordinary run as the public ``. It owns and implements the Plan authorship workflow — the Prompt wording, the draft and repair loops, the `` branches, human review, revision, approval, stopping, exhaustion and the final explanation turn — and returns the exact approved Plan source. Every Plan-producing turn in it states the complete Plan requirements for itself, so a replacement may add or correct a title rather than only carry one forward. Both surfaces expand these exact bytes under one origin and one digest; there is no generated TypeScript copy and no second Markdown implementation. Its four phase components are private to it, and it is not itself a Plan | -| authorship profile | the trusted-host assembly the packaged `` Component runs its authored turns under, installed by its own `` inside the invocation that owns it rather than around an execution — which is what makes it the same ceiling whether `xmd plan` or an ordinary document asked: its fixed inputs, a constrained Agent provider, Elicitation, the fixed first-party components and the host-declared ``. It uses no repository component search and exposes no custom root, and the ceiling it establishes is not readable from the command line. Its working directory is one host-owned directory dedicated to the logical session, keyed by the digest of that name, created empty and required to be empty on the way in. An explicitly named session's directory is durable, because continuation derives the same session identity from it; an invocation-unique default session's is scope-owned, claimed before it is created, and exactly one cleanup is attempted after profile teardown and before admission on every ending — the leaf removed non-recursively when it is still the empty directory that was handed over, and left as found with the command failing terminally when it has gained content or vanished. Where those directories live is a host dependency no caller or document selects | +| authorship profile | the trusted-host assembly the packaged `` Component runs its authored turns under, installed by its own `` inside the invocation that owns it rather than around an execution — which is what makes it the same ceiling whether `xmd plan`, an ordinary document, or a configured `` run child asked. Which Agent provider goes under it is a trusted-host capability the declaration carries — the production ACPX one built from the run's Agent stack, or the deterministic one a canonical `` child declaration produced — and a host that supplies none states the sentence a `` written there is refused with. Everything else about the ceiling is installed in one place for both, so a second provider cannot bring a weaker one: its fixed inputs, a constrained Agent provider, Elicitation, the fixed first-party components and the host-declared ``. It uses no repository component search and exposes no custom root, and the ceiling it establishes is not readable from the command line. Its working directory is one host-owned directory dedicated to the logical session, keyed by the digest of that name, created empty and required to be empty on the way in. An explicitly named session's directory is durable, because continuation derives the same session identity from it; an invocation-unique default session's is scope-owned, claimed before it is created, and exactly one cleanup is attempted after profile teardown and before admission on every ending — the leaf removed non-recursively when it is still the empty directory that was handed over, and left as found with the command failing terminally when it has gained content or vanished. Where those directories live is a host dependency no caller or document selects | | upgrade command document | the one exact checked-in first-party Markdown streaming text root `xmd upgrade` executes to select and install a published release. It owns the exact-tag grammar, release selection, semantic-version comparison, consent, the status, already-current and installation branches, and the wording of every refusal and report; its rendered body is the command's output rather than a value it returns. Internal: no command-line option selects another one, and no repository component search can answer for it | | upgrade assembly | what one runtime-named entrypoint states about the `xmd` that is running: its provenance, reported version, invoked executable path, platform, architecture, release target when the release publishes one, and — for an eligible compiled macOS or Linux host alone — the factory for the four phases an installation needs. It describes how this `xmd` is running, never how its files arrived | | release identity | the invocation-local opaque identifier `` mints for each release it admits. Holding one is what authorizes downloading that release, and nothing outside that one invocation's private admission map can read, extend or forge it. A download mints an upgrade candidate identity in the same way, and that candidate advances `downloaded → verified → committed` exactly once | diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index a6b2435df..360c58be0 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -2805,6 +2805,24 @@ capabilities — ``, ``, `` and `` — are the closure those exact bytes carry, and are syntax no document may write. [The plan command](./plan-command-spec.md) is the contract. +**Who can establish its ceiling is the host's to say, not the Component's.** The +declaration carries a trusted-host capability: the agent a Plan conversation +defaults to, and the operation that installs that one invocation's provider +inside the ceiling. It is a closure the host supplied before the declaration +existed — never a prop, a Context value, a registration result or anything a +document, component or middleware can reach or replace. + +Three surfaces state one. A production run states the ACPX ceiling built from +the Agent stack it settled. A nested `` that declares a +canonical `` states the deterministic one built from the controlled +provider that declaration produced (specs/testing-spec.md). Everything else +states none, and carries the sentence a person reads instead: an `xmd test` root +does not resolve the name at all, and an unconfigured child resolves these exact +bytes and is refused at the ceiling before a directory, a provider, a turn or a +review exists. What the ceiling then does — the permission mode, the +prompt-failure policy, the capability refusals, the empty session directory — is +the same whichever provider is underneath. + Two registrations for one name and kind at the same scope are a configuration error naming both origins. Installation order is not a resolution mechanism — reserved and default registrations are held apart, so which one wins is decided diff --git a/specs/testing-spec.md b/specs/testing-spec.md index 016549b23..b098a0825 100644 --- a/specs/testing-spec.md +++ b/specs/testing-spec.md @@ -412,6 +412,39 @@ declarations under the same ``; a named session cannot continue across the boundary. The behavior-document path resolves from the outer test document under the TestAgent package's existing containment rule. +#### The Plan authorship ceiling + +A configured child can also establish the ceiling `` writes under, which +is the one thing about it a document cannot arrange for itself. + +Three surfaces, and they stay distinct. A **production run** establishes the +production ACPX ceiling from the Agent stack the command settled. A **configured +`` child** establishes the deterministic test ceiling from the +controlled provider its own declaration produced. A **direct `xmd test` root** +and an **unconfigured child** establish none: the root does not resolve `` +at all, because the run profile's vocabulary belongs to a run, and an +unconfigured child resolves the same protected bytes every run resolves and is +refused at the ceiling before a directory, a provider, an Agent turn or a review +exists. + +Which provider is underneath is the only thing that differs. The Plan system +instruction, the deny-all permission mode, the empty MCP-server and native-tool +sets, the prompt-failure policy and the document-capability refusals are the +same for both established ceilings, installed in one place rather than restated +by each provider, so a second implementation cannot bring a weaker ceiling with +it. The controlled provider is installed *inside* the Plan invocation rather +than inherited from what the child registered around itself. + +A configured child is given a Plan authorship root of its own, created outside +that ceiling and removed when the child settles — the Plan sessions underneath +it included, a named one among them, which production keeps and a test may not. +The Plan invocation still creates and proves its own empty session directory +under that root. + +Only the canonical declaration this harness recognizes grants any of it. A +repository component named `TestAgent` ends the scan and configures nothing, so +a child that reached one is refused exactly as an unconfigured child is. + Only detached declaration data enters the host-profile request. A test cannot send an Agent provider, Elicitation middleware, context, Api handler, execution installation, controller or scope to the child. The trusted host consumes the From ce84545725749e35018caa2117e60efce1d2bb79 Mon Sep 17 00:00:00 2001 From: Taras Mankovski <74687+taras@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:27:26 -0400 Subject: [PATCH 4/9] =?UTF-8?q?=E2=9C=A8=20Let=20a=20scenario=20answer=20f?= =?UTF-8?q?or=20a=20conversation=20a=20test=20cannot=20name=20(#728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `` derives the session it opens from the expansion that asked, so a checked-in test has no name to write: the conversation is `xmd-plan:` followed by a digest that moves when the document moves. A scenario maps one exact agent and session, and an omitted session maps the unnamed default rather than acting as a wildcard — so nothing could address the Plan's turn, and the story's successful path was unreachable. `anySession` is an explicit opt-in for exactly that: a scenario that answers for any of its agent's sessions no exact mapping claims. An exact mapping always wins, so every guarantee about named sessions still holds wherever one is written; an agent declares at most one; and writing both `session` and `anySession` on one scenario is refused where it is written rather than when a prompt arrives. Chosen because it is additive. It invalidates no existing declaration and changes neither production Plan session identity nor what an omitted `session` means — the two alternatives both edit a settled guarantee. PMT1 and PMT2 now run as checked-in Markdown rows: a document captures its approved Plan through a scripted agent and an authored approval, and the file the approved source names is absent afterwards. That negative control was verified by planting the file, which fails the row. Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7 --- .../tests/document-suites/plan/Plan.test.md | 74 ++++++++++++++----- .../plan/agents/approved-plan.md | 14 +--- .../test-agent/src/child-configuration.ts | 51 ++++++++++--- packages/test-agent/src/components.ts | 22 +++++- packages/testing/src/child-configuration.ts | 12 +++ specs/test-agent-spec.md | 16 ++++ specs/testing-spec.md | 5 ++ 7 files changed, 153 insertions(+), 41 deletions(-) diff --git a/packages/cli/tests/document-suites/plan/Plan.test.md b/packages/cli/tests/document-suites/plan/Plan.test.md index 5f72a0742..d2100606d 100644 --- a/packages/cli/tests/document-suites/plan/Plan.test.md +++ b/packages/cli/tests/document-suites/plan/Plan.test.md @@ -15,23 +15,63 @@ document. A child's `target` is a document the run host resolves, so it is written relative to the working directory — the repository root, where each runtime starts its test process. -## The successful path is not here yet - -The two rows this suite exists for — a document capturing its approved Plan, and -the approved source not having run — are not written here, because a scenario -cannot address the conversation `` opens. - -`` places its conversation under a session it derives from the expansion -that asked for it, so `` talks to -`xmd-plan:<64 hex digits>` rather than to `planner`. A scenario maps one exact -agent and logical session, and an omitted `session` maps the unnamed default -rather than acting as a wildcard (specs/test-agent-spec.md, "Behavior -documents"). There is therefore no name a checked-in test can write that reaches -the Plan's turn, and the child fails with `no maps agent -"test" and session "xmd-plan:…"`. - -The refusal below is the part that does hold today, and it is the part that -proves the ceiling is real. + + +The child is `uses-plan.md`, which writes `` and prints what it bound. The +scenario answers the one request the Plan workflow sends, and the answer +approves the draft it produced. + +`anySession` is how the scenario reaches it. `` derives the conversation +it opens from the expansion that asked, so there is no session name an author +could write here — that is what this opt-in is for, and an exact mapping still +wins wherever one exists. + + + + + + + + + + + + + + + + + + +## An approved Plan is source, not something that ran + +The program this scenario approves names a file. Approving it hands the source +back; it does not run it. The file is therefore the evidence: a child that +executed what it was given would have created it. + + + + + + + + + + + + + + + + + +The source arrived, and the file it names is nowhere: the child ran in this +working directory, so a child that executed what it was given would have written +it here. + + + + ## Without a scripted agent there is no ceiling to write under diff --git a/packages/cli/tests/document-suites/plan/agents/approved-plan.md b/packages/cli/tests/document-suites/plan/agents/approved-plan.md index 82b4bc44a..1492b86fd 100644 --- a/packages/cli/tests/document-suites/plan/agents/approved-plan.md +++ b/packages/cli/tests/document-suites/plan/agents/approved-plan.md @@ -1,16 +1,4 @@ -# The Plan this scenario writes - -The coding agent behind a Plan is asked for one thing: a complete program, as -source and nothing else. This scenario answers that request with the same -program every time, so a test reading the approved source is reading a decision -this document made rather than a model's. - - - -The program is bound rather than written out, because writing it out would run -it: a `` element in this document's body is an element this worker -expands. Interpolating the same text emits it as the characters it is, which is -what an agent replying with source actually sends. + , ): Operation { - const { agent, session: sessionProp, src } = props; + const { agent, session: sessionProp, src, anySession } = props; if (typeof src !== "string" || src.length === 0) { refuse(`<${SCENARIO}> requires a "src" prop.`); return ""; @@ -111,19 +121,35 @@ function open(collect: { return ""; } const agentName = typeof agent === "string" ? agent : defaultAgent; + const serveAny = anySession === true || anySession === "" || anySession === "true"; + if (serveAny && typeof sessionProp === "string") { + refuse( + `<${SCENARIO}> was written with both "session" and "anySession". One names the ` + + "conversation it answers for and the other says it does not name one.", + ); + return ""; + } const session = typeof sessionProp === "string" ? sessionProp : ""; - const key = mappingKey(agentName, session); + // Kept in the same table as the exact mappings, under a key no session name + // can produce, so declaring two of them for one agent is refused where it is + // written rather than discovered when a prompt arrives. + const key = serveAny ? anyKey(agentName) : mappingKey(agentName, session); if (mapped.has(key)) { // Refused where it is written rather than where it would be used: the // wrapper can only find out when a prompt asks for the mapping, and a // child has not been created yet for one to ask in. - refuse(`<${SCENARIO}> maps ${describeMapping(agentName, session)} more than once.`); + refuse( + serveAny + ? `<${SCENARIO}> maps any session of agent "${agentName}" more than once.` + : `<${SCENARIO}> maps ${describeMapping(agentName, session)} more than once.`, + ); return ""; } mapped.add(key); scenarios.push({ agent: agentName, session, + ...(serveAny ? { anySession: true } : {}), rootDir: read.value.rootDir, document: read.value.document, }); @@ -189,13 +215,18 @@ export function* installChildTestAgent( const controller = yield* useTestAgentController(); const declarations = new Map(); for (const scenario of configuration.scenarios) { - declarations.set(mappingKey(scenario.agent, scenario.session), { - agent: scenario.agent, - sessionName: scenario.session, - rootDir: scenario.rootDir, - document: { path: scenario.document.path, source: scenario.document.source }, - duplicate: false, - }); + declarations.set( + scenario.anySession === true + ? anyKey(scenario.agent) + : mappingKey(scenario.agent, scenario.session), + { + agent: scenario.agent, + sessionName: scenario.session, + rootDir: scenario.rootDir, + document: { path: scenario.document.path, source: scenario.document.source }, + duplicate: false, + }, + ); } const partition = yield* provisionPartition({ defaultAgent: configuration.defaultAgent, diff --git a/packages/test-agent/src/components.ts b/packages/test-agent/src/components.ts index 88ce65d3b..990839fb6 100644 --- a/packages/test-agent/src/components.ts +++ b/packages/test-agent/src/components.ts @@ -94,6 +94,11 @@ function configError(source: string, message: string): ErrorSegment { return { type: "error", message: `<${source}> ${message}`, source }; } +/** Where one agent's any-session mapping sits, if it declared one. */ +function anyDeclarationKey(agent: string): string { + return JSON.stringify([agent]); +} + function declarationKey(agent: string, sessionName: string): string { // JSON encoding keeps the key textual and collision-safe for any // agent/session values. @@ -177,7 +182,13 @@ export function* provisionPartition(options: { sessionName: string | undefined, dir: string, ): Operation { - const declared = declarations.get(declarationKey(agentName, sessionName ?? "")); + // The exact mapping first, always. An any-session declaration is a fallback + // an author opted into for conversations they cannot name — `` derives + // its session from the expansion that asked — and it must never take a + // session somebody did name. + const declared = + declarations.get(declarationKey(agentName, sessionName ?? "")) ?? + declarations.get(anyDeclarationKey(agentName)); if (!declared) { throw new Error(`no maps ${describeMapping(agentName, sessionName)}`); } @@ -499,6 +510,15 @@ export const SCENARIO_PROPS: PropsSchema = { src: { type: "string" }, agent: { type: "string" }, session: { type: "string" }, + /** + * Answer for any of this agent's sessions no exact mapping claims. + * + * For a conversation whose name a test cannot write down — `` derives + * its session from the expansion that asked. An exact mapping always wins, + * and it is an explicit opt-out of exact matching rather than a new meaning + * for an omitted `session`. + */ + anySession: { type: "boolean" }, }, required: ["src"], additionalProperties: false, diff --git a/packages/testing/src/child-configuration.ts b/packages/testing/src/child-configuration.ts index 7aae374b8..1899072b0 100644 --- a/packages/testing/src/child-configuration.ts +++ b/packages/testing/src/child-configuration.ts @@ -63,6 +63,17 @@ export interface ChildScenario { readonly agent: string; /** The logical session, or the empty string for the unnamed one. */ readonly session: string; + /** + * Whether this mapping answers for any of its agent's sessions that no exact + * mapping claims. + * + * For a conversation whose name the test cannot write down. `` is the + * case it exists for: it derives its session from the expansion that asked, + * so there is no name an author could put here. An exact mapping always wins, + * and an agent may declare at most one of these — it is an explicit opt-out + * of exact matching, never what omitting `session` means. + */ + readonly anySession?: boolean; /** The containment root the behavior document's dependencies resolve under. */ readonly rootDir: string; readonly document: { readonly path: string; readonly source: string }; @@ -160,6 +171,7 @@ function detachScenario(scenario: ChildScenario): ChildScenario { return frozen({ agent: scenario.agent, session: scenario.session, + ...(scenario.anySession === true ? { anySession: true } : {}), rootDir: scenario.rootDir, document: frozen({ path: scenario.document.path, source: scenario.document.source }), }); diff --git a/specs/test-agent-spec.md b/specs/test-agent-spec.md index c3931fe22..c4a31c61c 100644 --- a/specs/test-agent-spec.md +++ b/specs/test-agent-spec.md @@ -82,6 +82,22 @@ wildcard. A named `` requires an exact named mapping. A missing or duplicate mapping fails the owning test before its agent turn starts. An unused mapping does not fail the test-agent scope. +`anySession` is the one way to answer without naming a conversation, and it is +an explicit opt-in rather than a new meaning for an omitted `session`: + +```md + +``` + +It exists for a conversation whose name a test cannot write down. `` +derives its session from the expansion that asked for it, so no author can name +it and no name stays true when the document moves. Such a mapping answers for +any of its agent's sessions that no exact mapping claims: an exact mapping +always wins, so every guarantee above still holds wherever one is written. An +agent declares at most one, and writing both `session` and `anySession` on one +scenario is refused where it is written. Two of them for one agent is a +duplicate mapping like any other. + The test runtime does not inject agent, session, cwd, or harness metadata into the behavior document. The document sees its own frontmatter and bindings, including values captured by prompt matchers. diff --git a/specs/testing-spec.md b/specs/testing-spec.md index b098a0825..227d4aeb9 100644 --- a/specs/testing-spec.md +++ b/specs/testing-spec.md @@ -445,6 +445,11 @@ Only the canonical declaration this harness recognizes grants any of it. A repository component named `TestAgent` ends the scan and configures nothing, so a child that reached one is refused exactly as an unconfigured child is. +A scenario reaches the Plan's own turn through `anySession` +(specs/test-agent-spec.md): `` derives its conversation from the expansion +that asked, so there is no session name a test could write, and that opt-in is +what answers for it without weakening exact matching anywhere else. + Only detached declaration data enters the host-profile request. A test cannot send an Agent provider, Elicitation middleware, context, Api handler, execution installation, controller or scope to the child. The trusted host consumes the From 50a9cf9e89149d1671e32a2949eda7f59e40185c Mon Sep 17 00:00:00 2001 From: Taras Mankovski <74687+taras@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:12:05 -0400 Subject: [PATCH 5/9] =?UTF-8?q?=E2=9C=A8=20Let=20a=20configured=20child=20?= =?UTF-8?q?answer=20its=20own=20Plan=20review=20(#728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Plan ceiling installs its review provider inside the invocation, which is nearer than anything installed around the child. A configured child therefore got the browser form in front of its own `` matchers, and the review waited for a person no test can supply: the run printed a form URL and hung until the harness abandoned it. Who answers now travels with the child's declaration request. An ordinary run states the browser form, because that is how a person reviews a Plan. A configured child states nothing and lets what it already has answer — the matcher provider when the test declared one, and the form otherwise, which is what an unconfigured child had all along. PMT4 and PMT5 follow: two sibling children, one approving and one stopping, each writing a Plan under a root of its own, with neither root surviving and neither reaching the tree a production `xmd plan` owns. Both endings are in one case because cleanup that only ran on the happy path would satisfy a case that checked one of them. Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7 --- packages/cli/src/cli.ts | 5 +- packages/cli/src/testing-host.ts | 15 ++ .../cli/tests/support/run-markdown-tier.ts | 3 +- .../cli/tests/testing-execution-host.test.ts | 132 ++++++++++++++++++ 4 files changed, 152 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 43cac560a..73abadfd6 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -979,7 +979,7 @@ function* runDocument( // host's — putting this build's adapter on disk, and opening the review // form — run outside the ceiling the Component installs around itself. host: request.host, - installElicitation: installWebElicitation, + installElicitation: request.installElicitation, // Rendered when a `` first asks, not before: an ordinary run that // writes none never builds a catalog it has no reader for. *catalog() { @@ -990,6 +990,9 @@ function* runDocument( const plan = yield* planDeclaration({ ceiling: planAuthorshipCeiling(mode.agent), host: yield* useScope(), + // This command's own root: the browser form is how a person reviews a Plan + // written by an ordinary run. + installElicitation: installWebElicitation, }); // Wire --verbose observability via Signal. diff --git a/packages/cli/src/testing-host.ts b/packages/cli/src/testing-host.ts index 84d8384ab..ca29c4d59 100644 --- a/packages/cli/src/testing-host.ts +++ b/packages/cli/src/testing-host.ts @@ -71,6 +71,16 @@ export interface ChildPlanDeclaration { readonly authorshipRoot?: string; /** The scope this child's own host acts run in. */ readonly host: Scope; + /** + * Who answers this child's Plan review. + * + * The ceiling installs it inside the Plan invocation, which is nearer than + * anything installed around the child — so a host that installed the browser + * form here would put it in front of a configured child's ``, and the + * review would wait for a person no test can supply. A configured child + * therefore installs nothing and lets its own matcher provider answer. + */ + installElicitation(): Operation; } /** What the entrypoint already decided, and a child must not decide again. */ @@ -311,6 +321,11 @@ function* runProfileChild( ceiling, ...(authorshipRoot === undefined ? {} : { authorshipRoot }), host: yield* useScope(), + // Nothing, so the review is answered by whatever this child already + // has: the `` matcher provider installed above when the test + // declared one, and the browser form installed for the child otherwise. + // deno-lint-ignore require-yield + *installElicitation(): Operation {}, }), ], }); diff --git a/packages/cli/tests/support/run-markdown-tier.ts b/packages/cli/tests/support/run-markdown-tier.ts index 3a3688ad2..fe99a94f4 100644 --- a/packages/cli/tests/support/run-markdown-tier.ts +++ b/packages/cli/tests/support/run-markdown-tier.ts @@ -101,8 +101,7 @@ export function runMarkdownTier(document: string): Operation { ? {} : { authorshipRoot: request.authorshipRoot }), host: request.host, - // deno-lint-ignore require-yield - *installElicitation(): Operation {}, + installElicitation: request.installElicitation, *catalog(): Operation { return renderSyntaxMarkdown(yield* syntaxCatalog(["components", "."])); }, diff --git a/packages/cli/tests/testing-execution-host.test.ts b/packages/cli/tests/testing-execution-host.test.ts index 05fa00d00..fc2e61474 100644 --- a/packages/cli/tests/testing-execution-host.test.ts +++ b/packages/cli/tests/testing-execution-host.test.ts @@ -24,6 +24,7 @@ import { until } from "effection"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { runCli } from "@executablemd/test-support/launch"; +import { DEFAULT_AUTHORSHIP_ROOT } from "../src/authorship-profile.ts"; import { testingExecutionHost } from "../src/testing-host.ts"; import { planComponentDescription } from "../src/plan-component.ts"; @@ -70,6 +71,75 @@ const CHILD = doc( const GUIDE = doc("# First", "", "first body", "", "# Second", "", "second body"); +/** The one request the Plan workflow sends, answered with a complete program. */ +const PLAN_BEHAVIOR = doc( + '', + "", + "the approved Plan ran',`, + ' "",', + ' ].join("\\n")}', + "/>", + "", + "{program}", +); + +/** An ordinary document that writes a Plan and prints what it bound. */ +const PLAN_CHILD = doc( + "# A document that writes a Plan", + "", + 'Write the release program.', + "", + "Approved source: {approved}", +); + +/** + * What a child needs to write one: a scripted agent for the turn, and an + * authored approval for the review. + * + * `anySession` because `` derives the conversation it opens from the + * expansion that asked, so there is no session name to write here. + */ +const PLAN_DECLARATION = [ + "", + '', + "", + "", + "", + '', + "", +]; + +/** + * What the two Plan authorship trees hold right now. + * + * `children` is the temporary directory, filtered to the roots a configured + * child makes; `production` is the tree a real `xmd plan` keeps its sessions in, + * read and never written. A tree that does not exist yet holds nothing, which is + * the ordinary state of the production one on a machine that has never run the + * command. + */ +function* planRoots(): Operation<{ children: string[]; production: string[] }> { + return { + children: (yield* listing(tmpdir())).filter((entry) => entry.startsWith("xmd-child-plan-")), + production: yield* listing(DEFAULT_AUTHORSHIP_ROOT), + }; +} + +function* listing(directory: string): Operation { + try { + return (yield* readdir(directory)).sort(); + } catch { + return []; + } +} + /** A declared child relaunches `xmd` as its agent worker, so it needs both. */ const WORKER = { inheritEnv: true, timeout: 180_000 }; @@ -469,6 +539,68 @@ describe("deterministic dependencies declared for a nested run", () => { expect(result.code).toBe(0); }); + /** + * PMT4 and PMT5 — where a Plan conversation lives, and that none of it stays. + * + * Where the root goes is a host dependency no caller selects. Production keeps + * it under the caller's home; a configured child gets one of its own under the + * temporary directory, so a test never reads or removes anything a real + * `xmd plan` owns. Both facts are read the same way — what these directories + * held before the run against what they hold after — because a child's root + * exists only while that child does, and the evidence is the absence. + * + * The two endings are here together because cleanup that only ran on the happy + * path would satisfy a case that checked one of them. The approved sibling and + * the stopped sibling also prove the two children are separate: each answers + * from its own scenario and its own authored decision. + */ + it("gives each configured child its own Plan root, and keeps none of them", function* () { + const before = yield* planRoots(); + const project = yield* useProject({ + "agents/plan.md": PLAN_BEHAVIOR, + "writes-a-plan.md": PLAN_CHILD, + "README.md": doc( + '', + '', + ...PLAN_DECLARATION, + "", + '', + "", + "", + '', + "", + "", + '', + "", + '', + "", + "", + "", + '', + "", + "", + "", + "", + "", + "", + ), + }); + const result = yield* runCli(["test", "README.md"], { cwd: project, ...WORKER }).join(); + expect(result.stdout + result.stderr).not.toContain("❌"); + expect(result.code).toBe(0); + + // Both children wrote a Plan — the rows above say so — and neither kept the + // root it wrote it under. One that outlived its child would be here, named + // for the child that made it. + const after = yield* planRoots(); + expect(after.children.filter((entry) => !before.children.includes(entry))).toEqual([]); + // And neither of them reached the tree a production `xmd plan` owns. + expect(after.production.filter((entry) => !before.production.includes(entry))).toEqual([]); + }); + /** * PMT6 — only the canonical declaration configures a Plan ceiling. * From c158217fd818147cfb04874391b47957de215dd7 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Wed, 2 Sep 2026 12:57:43 -0400 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=A7=AA=20Keep=20Plan=20test=20scenari?= =?UTF-8?q?os=20exact=20(#728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- architecture.md | 2 +- packages/cli/src/authorship-profile.ts | 74 ++++++++-- packages/cli/src/cli.ts | 3 + packages/cli/src/documents/Plan.md | 6 +- packages/cli/src/plan-component.ts | 14 +- packages/cli/src/testing-host.ts | 56 +++++--- .../tests/document-suites/plan/Plan.test.md | 12 +- .../cli/tests/support/run-markdown-tier.ts | 3 + .../cli/tests/testing-execution-host.test.ts | 103 ++++++++++++-- packages/test-agent/mod.ts | 1 + .../test-agent/src/child-configuration.ts | 132 +++++++++++------- packages/test-agent/src/components.ts | 44 +++--- packages/test-agent/src/provider.ts | 25 ++++ packages/testing/src/child-configuration.ts | 12 -- specs/executable-mdx-spec.md | 7 + specs/test-agent-spec.md | 23 ++- specs/testing-spec.md | 10 +- 17 files changed, 373 insertions(+), 154 deletions(-) diff --git a/architecture.md b/architecture.md index de8441630..35e0b7cb3 100644 --- a/architecture.md +++ b/architecture.md @@ -3800,7 +3800,7 @@ Status is measured against main. | foreground command routing and retention | forwards a child's channels live, captures stdout for a `` region, and retains output only when the host asked for a record | built on the #441 stack | | `exec as="name"` | binds one command's settled outcome — exit status, stdout and stderr — as an ordinary mutable binding; the block displays neither channel, renders nothing, and a nonzero status raises nothing. Only the built-in exec terminal and the built-in `timeout` may compose one, authorized by factory identity rather than by registered name, and asked for through a capability-backed request public middleware composes around but cannot issue, claim twice or answer | built on the #447 stack | | testing harness (``) | runs another document as a real root under a production host profile, authorized by canonical `` alone: declarations installed before the root import, child output displayed progressively and collected only when asked, journal retention selected independently of observation, and the outcome published by the invocation's own terminal through a request public middleware composes around but cannot answer | built on the #454 stack for `host="run"`; the workflow profile and `` are unbuilt, and a host that offers no workflow profile refuses them | -| nested run-profile Agent and elicitation declarations | lets one `` declare one child-scoped `` scenario set and one non-delegating `` matcher set; only frozen test data crosses the harness request, the trusted host constructs both providers inside the isolated child, siblings share no session or provider state, ordinary component shadowing remains in force, and the child journal retains only the selected Prompt and Elicit components' ordinary results | built on the #641 stack | +| nested run-profile Agent and elicitation declarations | lets one `` declare one child-scoped `` scenario set and one non-delegating `` matcher set; only frozen test data crosses the harness request, the trusted host constructs both providers inside the isolated child, siblings share no session or provider state, ordinary component shadowing remains in force, and the child journal retains only the selected Prompt and Elicit components' ordinary results. A controlled `` may author an exact scenario label that this host alone maps to Plan's derived conversation identity; declaration selection uses the label while runtime state stays keyed by the opaque identity and child, with no matcher or fallback added to ordinary TestAgent sessions | built on the #641 stack; controlled Plan routing added on the #728 stack | | `Config` run deadline / exec default / Fetch default / verbosity | three independently owned contextual timeouts, absent unless configured, each read by exactly one consumer, and contextual verbosity — a boolean that is false unless configured, seeded by the command line and overridable for a lexical subtree, bounding nothing and owning no authority | built on this stack | | native session launch (`` / `launchAgentSession()`) | prepares one durable coding-agent session from the rendered body of `` and hands the provider's native UI the terminal for that exact session, then continues the document after it exits. The body renders completely first and only what it rendered crosses as the instruction layer; the launch performs no model turn; the run's one foreground-terminal lease is taken before an agent is resolved, so a host with no terminal refuses without probing for an installed CLI. A session is constructed once, by one of two mechanisms, and its create-once construction route says which. Where the provider returns the identity, the ACPX provider creates the session, installs the layer at creation, releases ACP ownership before the spawn, and marks its handle stale so a later `` reattaches. Where the adapter names its own sessions, it allocates the identity inside ownership before any process exists, the native process creates the session under that name from a private mode-0600 instruction file, and ACP creates nothing — the instruction text reaches neither argv nor environment, and the file is removed on success, failure and cancellation alike while ownership is still held. Neither route converts into the other, and which one governs is chosen by the first operation that consumes the placement rather than by the `` that made it: a fresh `` publishes no route and establishes nothing, so a `` nested inside one constructs the session it placed, while a first subscribed `` publishes ACP-first before it ensures and keeps that account even if the turn that follows is never accepted. An established route is validated eagerly by a later ``, and a launch meeting a published ACP-first route refuses before an identity exists. A `` or `` meeting a bound client-allocated route attaches under the route's exact identity; a legacy unbound route or an unavailable attachment capability refuses before a turn and creates no substitute conversation. Phases are retained as `agent_session_launch` records under one expansion identity — `prepared` before ownership is released, then `detached`, then `exited` — so a completed replay launches nothing, a replay holding only `prepared` proves the handoff never began and may still create under the retained identity, and one holding `detached` resumes and never falls back. The public route carries an opaque one-use launch request and answers nothing; authority to run and retain a phase is delivered to the installed provider directly, so neither a returned completion nor a rebuilt request authors a launch. Every operation that can act on an advertised session takes exclusive ownership under one natural key first, through a coordinator the host built and passed in; contention refuses instead of queueing, and an owner that never proved it stopped leaves a recovery tombstone. A host that cannot say who owns a session refuses every advertised operation, and one that cannot say how a session was constructed additionally refuses an agent that names its own — before any provider effect. Every private setup or child-creation failure is normalized to `process-creation-failed` with fixed provider-owned text, carrying no path, argv, environment or host message. No launch path discards persistent provider state. A client-allocated session is bound to one executable build: the build is observed inside ownership before an identity is allocated, the binding is published with the V2 route and retained beside the prepared record, the native child runs the exact observed path in place of the launcher name, and every later create, resume, attachment and incomplete replay reobserves and compares before a process, an ensure or a turn. A `` or `` meeting a bound client-native route attaches to it: it reobserves the build, requires any retained provider arrangement to assert that same conversation, calls ensure with the route identity as `resumeSessionId`, and requires the provider to report that identity before a turn — refusing on missing capability, build drift, missing history or a differing assertion without creating a substitute conversation. ACP runtimes are partitioned by resolved agent command and binding, each handle is closed by the partition that created it, and a bound partition is torn down when its last handle closes. A legacy V1 client-native route keeps exactly the released native-only behavior and never attaches | built on the #517 stack, extended by the #519 and #561 stacks; Deno and the compiled binary assemble the host — coordinator, route store and executable observer — and Node and Bun keep the same advertised names while assembling none of it, so every advertised operation refuses before provider work; `claude` is advertised for native launch after passing the client-allocated gate at Claude Code 2.1.241 on macOS arm64 (#520) and separately for client-native attachment after passing the native-to-ACP marker gate (#561), and Codex remains unadvertised because nothing has run its provider-returned claims against an installed Codex; `Agent.AddDir` is unbuilt | | `` | performs one XMD-mediated HTTP read through contextual `API.Fetch`, admitting the whole request before transport, and retains the normalized request and the detached response as one `fetch` durable observation; capture decides whether a status is data or a failure, and the trusted host's destination ceiling sits below the component | built on the #456 stack; a generated fragment may name the pinned identity only for a request the trusted host stated exactly, on the #369 stack | diff --git a/packages/cli/src/authorship-profile.ts b/packages/cli/src/authorship-profile.ts index 14bf146a4..330346035 100644 --- a/packages/cli/src/authorship-profile.ts +++ b/packages/cli/src/authorship-profile.ts @@ -153,6 +153,8 @@ export interface AuthorshipCeilingInputs { export interface PlanAuthorship { /** The agent this Plan conversation defaults to. */ readonly defaultAgent: string; + /** The trusted adapter that supplies this invocation's provider. */ + readonly origin: "production-acpx" | "controlled-test-agent"; /** * Install this invocation's Agent provider, inside the Plan ceiling. * @@ -162,7 +164,30 @@ export interface PlanAuthorship { * @param workdir This conversation's directory, established and proven empty. * @param host The scope captured before the ceiling existed, for host acts. */ - installProvider(workdir: string, host: Scope): Operation; + installProvider(invocation: PlanAuthorshipInvocation): Operation; +} + +export interface PlanAuthorshipInvocation { + readonly workdir: string; + readonly host: Scope; + readonly session: string; + readonly authoredSession?: string; + readonly policy: PlanAuthorshipPolicy; + observe?(observation: PlanAuthorshipObservation): Operation; +} + +export interface PlanAuthorshipPolicy { + readonly systemInstruction: string; + readonly permissionMode: "deny-all"; + readonly promptFailures: "fail"; + readonly mcpServers: readonly never[]; + readonly allowedTools: readonly never[]; +} + +export interface PlanAuthorshipObservation { + readonly providerOrigin: PlanAuthorship["origin"]; + readonly policy: PlanAuthorshipPolicy; + readonly workdir: string; } /** @@ -209,14 +234,20 @@ export function planAuthorshipCeiling( established: true, authorship: { defaultAgent: stack.defaultAgent, - *installProvider(workdir: string, host: Scope): Operation { + origin: "production-acpx", + *installProvider(invocation: PlanAuthorshipInvocation): Operation { const acpx = createAcpxProvider( - authorshipCeiling({ stack, ...(acp === undefined ? {} : { acp }) }, workdir, host), + authorshipCeiling( + { stack, ...(acp === undefined ? {} : { acp }) }, + invocation.workdir, + invocation.host, + invocation.policy, + ), ); yield* registerAgentProvider("acpx", acpx); yield* installInvocationAgentProvider("acpx", { defaultAgent: stack.defaultAgent, - permissionMode: AUTHORSHIP_PERMISSION_MODE, + permissionMode: invocation.policy.permissionMode, }); }, }, @@ -241,6 +272,11 @@ export interface AuthorshipFrame { readonly host: Scope; /** The host's ability to put an Agent under this ceiling. */ readonly authorship: PlanAuthorship; + /** The opaque conversation identity the provider must preserve. */ + readonly session: string; + /** The exact authored label a trusted child host may address privately. */ + readonly authoredSession?: string; + observe?(observation: PlanAuthorshipObservation): Operation; installElicitation(): Operation; } @@ -272,7 +308,14 @@ export function* installAuthorshipFrame(frame: AuthorshipFrame): Operation // // Which provider it is belongs to the host that supplied the capability. What // does not is everything below: one ceiling, whoever is underneath it. - yield* frame.authorship.installProvider(frame.workdir, frame.host); + yield* frame.authorship.installProvider({ + workdir: frame.workdir, + host: frame.host, + session: frame.session, + ...(frame.authoredSession === undefined ? {} : { authoredSession: frame.authoredSession }), + policy: PLAN_AUTHORSHIP_POLICY, + ...(frame.observe === undefined ? {} : { observe: frame.observe }), + }); // A candidate comes from a turn's complete successful close value or from // nowhere. `` ordinarily renders whatever a failed turn managed to // emit and carries on, which for a workflow that reviews source would mean @@ -280,7 +323,7 @@ export function* installAuthorshipFrame(frame: AuthorshipFrame): Operation // failed, cancelled or protocol-invalid turn ends authorship before anything // is presented. The Component cannot opt out of it. yield* installPromptFailurePolicy(function* () { - return true; + return PLAN_AUTHORSHIP_POLICY.promptFailures === "fail"; }); yield* refuseDocumentCapabilities(); } @@ -309,7 +352,7 @@ export function* runPlanCommandDocument(profile: AuthorshipProfile): Operation` first asks, not before: an ordinary run that // writes none never builds a catalog it has no reader for. diff --git a/packages/cli/src/documents/Plan.md b/packages/cli/src/documents/Plan.md index 7a1c41e9b..6b35dc0fe 100644 --- a/packages/cli/src/documents/Plan.md +++ b/packages/cli/src/documents/Plan.md @@ -64,7 +64,11 @@ of them is raised. - + ## Create the first draft diff --git a/packages/cli/src/plan-component.ts b/packages/cli/src/plan-component.ts index 2a2296b27..381e7493d 100644 --- a/packages/cli/src/plan-component.ts +++ b/packages/cli/src/plan-component.ts @@ -62,7 +62,7 @@ import { installAuthorshipFrame, useSessionDirectory, } from "./authorship-profile.ts"; -import type { PlanAuthorshipCeiling } from "./authorship-profile.ts"; +import type { PlanAuthorshipCeiling, PlanAuthorshipObservation } from "./authorship-profile.ts"; import type { CandidateAssessment } from "./authorship-profile.ts"; import type { MachineSessionAssembly } from "./session-coordinator.ts"; import { PLAN_DOCUMENT, readPackagedDocument } from "./packaged-document.ts"; @@ -144,6 +144,8 @@ export interface PlanComponentAssembly { readonly explicitSession?: boolean; /** The scope the two host acts run in, captured before the ceiling exists. */ readonly host: Scope; + /** A trusted host-only observation after the complete ceiling is installed. */ + observeAuthorship?(observation: PlanAuthorshipObservation): Operation; /** Who answers the review question. */ installElicitation(): Operation; /** The run profile's rendered vocabulary, as the first Agent turn receives it. */ @@ -172,6 +174,7 @@ const INPUTS_RETURNS = { session: { type: "string" }, surface: { type: "string" }, durable: { type: "boolean" }, + authoredSession: { type: "string" }, }, required: ["syntax", "session", "surface", "durable"], additionalProperties: false, @@ -188,6 +191,7 @@ const AUTHORSHIP_PROPS = { properties: { session: { type: "string", minLength: 1 }, durable: { type: "boolean" }, + authoredSession: { type: "string", minLength: 1 }, }, required: ["session", "durable"], additionalProperties: false, @@ -354,6 +358,7 @@ function planInputs(assembly: PlanComponentAssembly): IdentityComponent { session, surface: assembly.surface, durable: durability(assembly, authored), + ...(authored === undefined ? {} : { authoredSession: authored }), }; }, }; @@ -451,6 +456,13 @@ function planAuthorship(assembly: PlanComponentAssembly): IdentityComponent { workdir: established.value, authorship: ceiling.authorship, host: assembly.host, + session, + ...(typeof props.authoredSession === "string" + ? { authoredSession: props.authoredSession } + : {}), + ...(assembly.observeAuthorship === undefined + ? {} + : { observe: assembly.observeAuthorship }), installElicitation: assembly.installElicitation, }); diff --git a/packages/cli/src/testing-host.ts b/packages/cli/src/testing-host.ts index ca29c4d59..6b0ed4b5c 100644 --- a/packages/cli/src/testing-host.ts +++ b/packages/cli/src/testing-host.ts @@ -35,22 +35,19 @@ import { fileSource, inlineSource, installAgentComponents, - registerAgentProvider, } from "@executablemd/core"; -import type { AgentComponentsOptions, RootDocumentSource } from "@executablemd/core"; -import { - executeInstalled, - installAnswerProvider, - installInvocationAgentProvider, -} from "@executablemd/core/host"; +import type { RootDocumentSource } from "@executablemd/core"; +import { executeInstalled, installAnswerProvider } from "@executablemd/core/host"; import { mkdir, rm } from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { NO_CEILING } from "./authorship-profile.ts"; import type { PlanAuthorshipCeiling } from "./authorship-profile.ts"; +import type { PlanAuthorshipObservation } from "./authorship-profile.ts"; import type { ExecutionInstallation } from "@executablemd/core/host"; -import { installChildTestAgent, TEST_AGENT_PROVIDER } from "@executablemd/test-agent"; +import { installChildTestAgent } from "@executablemd/test-agent"; +import type { ChildTestAgentInstallation } from "@executablemd/test-agent"; import type { AnswersChildConfiguration, ChildInvocation, @@ -81,6 +78,7 @@ export interface ChildPlanDeclaration { * therefore installs nothing and lets its own matcher provider answer. */ installElicitation(): Operation; + observeAuthorship?(observation: PlanAuthorshipObservation): Operation; } /** What the entrypoint already decided, and a child must not decide again. */ @@ -130,6 +128,8 @@ export interface TestingHostSettings { * agent has anything to say about it. */ readonly testAgentWorker: Result; + /** Trusted host evidence after a controlled Plan ceiling is fully installed. */ + observePlanAuthorship?(observation: PlanAuthorshipObservation): Operation; } /** @@ -202,9 +202,9 @@ function selectConfiguration(request: HostProfileRequest): { * that declares the same thing provisions all of it again, and neither reaches * the other. */ -function controlledCeiling(agents: AgentComponentsOptions): PlanAuthorshipCeiling { - const root = agents.rootProvider; - const defaultAgent = agents.defaultAgent; +function controlledCeiling(installation: ChildTestAgentInstallation): PlanAuthorshipCeiling { + const root = installation.components.rootProvider; + const defaultAgent = installation.components.defaultAgent; if (root === undefined || defaultAgent === undefined) { // Not reachable from `installChildTestAgent`, which states both. A child // that somehow reached here has no provider to put under the ceiling, and @@ -215,11 +215,28 @@ function controlledCeiling(agents: AgentComponentsOptions): PlanAuthorshipCeilin established: true, authorship: { defaultAgent, - *installProvider(): Operation { - yield* registerAgentProvider(TEST_AGENT_PROVIDER, root.factory); - yield* installInvocationAgentProvider(TEST_AGENT_PROVIDER, { - defaultAgent, - permissionMode: PLAN_PERMISSION_MODE, + origin: "controlled-test-agent", + *installProvider(invocation): Operation { + const observe = invocation.observe; + yield* installation.installPlanProvider({ + agent: defaultAgent, + ...(invocation.authoredSession === undefined + ? {} + : { authoredSession: invocation.authoredSession }), + session: invocation.session, + workdir: invocation.workdir, + policy: invocation.policy, + ...(observe === undefined + ? {} + : { + *observeTurn(): Operation { + yield* observe({ + providerOrigin: "controlled-test-agent", + policy: invocation.policy, + workdir: invocation.workdir, + }); + }, + }), }); }, }, @@ -227,8 +244,6 @@ function controlledCeiling(agents: AgentComponentsOptions): PlanAuthorshipCeilin } /** The permission mode every Plan conversation runs under, test or production. */ -const PLAN_PERMISSION_MODE = "deny-all"; - /** * A Plan authorship root this child owns and nothing else can reach. * @@ -301,7 +316,7 @@ function* runProfileChild( // invocation, so the execution is told about it rather than a registration // being made for it. const agents = yield* installChildTestAgent(testAgent, { workerCommand: worker.value }); - yield* installAgentComponents(agents); + yield* installAgentComponents(agents.components); installations.push({ components: agentIdentityComponents() }); // Created out here, outside the ceiling that refuses a directory to // everything inside it, and owned by this child alone: the Plan invocation @@ -326,6 +341,9 @@ function* runProfileChild( // declared one, and the browser form installed for the child otherwise. // deno-lint-ignore require-yield *installElicitation(): Operation {}, + ...(settings.observePlanAuthorship === undefined + ? {} + : { observeAuthorship: settings.observePlanAuthorship }), }), ], }); diff --git a/packages/cli/tests/document-suites/plan/Plan.test.md b/packages/cli/tests/document-suites/plan/Plan.test.md index d2100606d..c21d6a561 100644 --- a/packages/cli/tests/document-suites/plan/Plan.test.md +++ b/packages/cli/tests/document-suites/plan/Plan.test.md @@ -21,14 +21,14 @@ The child is `uses-plan.md`, which writes `` and prints what it bound. The scenario answers the one request the Plan workflow sends, and the answer approves the draft it produced. -`anySession` is how the scenario reaches it. `` derives the conversation -it opens from the expansion that asked, so there is no session name an author -could write here — that is what this opt-in is for, and an exact mapping still -wins wherever one exists. +The child authors `session="planner"`, and its scenario names that exact label. +The trusted child host privately connects the declaration to the opaque +conversation identity `` derives for this invocation; the provider keeps +all runtime state under that opaque identity. - + @@ -52,7 +52,7 @@ executed what it was given would have created it. - + diff --git a/packages/cli/tests/support/run-markdown-tier.ts b/packages/cli/tests/support/run-markdown-tier.ts index fe99a94f4..6ff5d00dc 100644 --- a/packages/cli/tests/support/run-markdown-tier.ts +++ b/packages/cli/tests/support/run-markdown-tier.ts @@ -101,6 +101,9 @@ export function runMarkdownTier(document: string): Operation { ? {} : { authorshipRoot: request.authorshipRoot }), host: request.host, + ...(request.observeAuthorship === undefined + ? {} + : { observeAuthorship: request.observeAuthorship }), installElicitation: request.installElicitation, *catalog(): Operation { return renderSyntaxMarkdown(yield* syntaxCatalog(["components", "."])); diff --git a/packages/cli/tests/testing-execution-host.test.ts b/packages/cli/tests/testing-execution-host.test.ts index fc2e61474..832b5e0ec 100644 --- a/packages/cli/tests/testing-execution-host.test.ts +++ b/packages/cli/tests/testing-execution-host.test.ts @@ -16,17 +16,18 @@ import { describe, it } from "@executablemd/test-support/bdd"; import { expect } from "@executablemd/test-support/expect"; -import { Err, ensure, resource, scoped } from "effection"; +import { Err, Ok, ensure, resource, scoped, spawn, withResolvers } from "effection"; import type { Operation } from "effection"; import { ensureDir, readdir, rm, writeTextFile } from "@effectionx/fs"; import { mkdtemp } from "node:fs/promises"; import { until } from "effection"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { runCli } from "@executablemd/test-support/launch"; -import { DEFAULT_AUTHORSHIP_ROOT } from "../src/authorship-profile.ts"; +import { cliBase, runCli } from "@executablemd/test-support/launch"; +import { AUTHORSHIP_INSTRUCTIONS, DEFAULT_AUTHORSHIP_ROOT } from "../src/authorship-profile.ts"; +import type { PlanAuthorshipObservation } from "../src/authorship-profile.ts"; import { testingExecutionHost } from "../src/testing-host.ts"; -import { planComponentDescription } from "../src/plan-component.ts"; +import { planComponentDeclaration, planComponentDescription } from "../src/plan-component.ts"; import { unsupportedRepositories } from "../src/run-repositories.ts"; function doc(...lines: string[]): string { @@ -94,7 +95,7 @@ const PLAN_BEHAVIOR = doc( const PLAN_CHILD = doc( "# A document that writes a Plan", "", - 'Write the release program.', + 'Write the release program.', "", "Approved source: {approved}", ); @@ -103,12 +104,12 @@ const PLAN_CHILD = doc( * What a child needs to write one: a scripted agent for the turn, and an * authored approval for the review. * - * `anySession` because `` derives the conversation it opens from the - * expansion that asked, so there is no session name to write here. + * The trusted child host privately connects the authored `planner` label to + * the opaque conversation identity while the provider keeps that identity. */ const PLAN_DECLARATION = [ "", - '', + '', "", "", "", @@ -572,7 +573,7 @@ describe("deterministic dependencies declared for a nested run", () => { "", '', "", - '', + '', "", "", "", @@ -601,6 +602,90 @@ describe("deterministic dependencies declared for a nested run", () => { expect(after.production.filter((entry) => !before.production.includes(entry))).toEqual([]); }); + it("installs the controlled Plan policy and removes its root after cancellation", function* () { + const before = yield* planRoots(); + const observed = withResolvers(); + const hold = withResolvers(); + const host = testingExecutionHost({ + includes: [], + secretDetection: true, + // deno-lint-ignore require-yield + installService: function* (): Operation {}, + installRepositories: unsupportedRepositories, + testAgentWorker: Ok([...cliBase(), "test-agent"]), + planDeclaration: (request) => + planComponentDeclaration({ + surface: "component", + includes: [], + ceiling: request.ceiling, + ...(request.authorshipRoot === undefined + ? {} + : { authorshipRoot: request.authorshipRoot }), + host: request.host, + ...(request.observeAuthorship === undefined + ? {} + : { observeAuthorship: request.observeAuthorship }), + installElicitation: request.installElicitation, + // deno-lint-ignore require-yield + *catalog(): Operation { + return ""; + }, + }), + *observePlanAuthorship(observation): Operation { + observed.resolve(observation); + yield* hold.operation; + }, + }); + const running = yield* spawn(() => + host.runChild({ + request: { + host: "run", + source: 'Write a program.\n', + props: {}, + journal: "transient", + collectJournal: false, + configuration: [ + { + kind: "test-agent", + defaultAgent: "test", + scenarios: [ + { + agent: "test", + session: "planner", + rootDir: tmpdir(), + document: { path: "plan.md", source: PLAN_BEHAVIOR }, + }, + ], + }, + ], + }, + run: undefined, + cwd: tmpdir(), + // deno-lint-ignore require-yield + *chunk(): Operation {}, + }), + ); + + const ceiling = yield* observed.operation; + expect(ceiling.providerOrigin).toBe("controlled-test-agent"); + expect(ceiling.policy.systemInstruction).toBe(AUTHORSHIP_INSTRUCTIONS); + expect(ceiling.policy.permissionMode).toBe("deny-all"); + expect(ceiling.policy.promptFailures).toBe("fail"); + expect(ceiling.policy.mcpServers).toEqual([]); + expect(ceiling.policy.allowedTools).toEqual([]); + expect(ceiling.workdir.startsWith(join(tmpdir(), "xmd-child-plan-"))).toBe(true); + expect(ceiling.workdir.startsWith(DEFAULT_AUTHORSHIP_ROOT)).toBe(false); + + // The observer runs from controlled turn routing after its scenario exists, + // so the child's Prompt is in flight here. halt() waits for the provider, + // declaration, session directory and child root to finish teardown before + // it returns. + yield* running.halt(); + const after = yield* planRoots(); + expect(after.children.filter((entry) => !before.children.includes(entry))).toEqual([]); + expect(after.production.filter((entry) => !before.production.includes(entry))).toEqual([]); + }); + /** * PMT6 — only the canonical declaration configures a Plan ceiling. * diff --git a/packages/test-agent/mod.ts b/packages/test-agent/mod.ts index 0cafc4128..8089253c6 100644 --- a/packages/test-agent/mod.ts +++ b/packages/test-agent/mod.ts @@ -56,6 +56,7 @@ export { runTestAgentWorker } from "./src/worker/run.ts"; * by the first crosses into the second. */ export { installChildTestAgent, testAgentChildDeclaration } from "./src/child-configuration.ts"; +export type { ChildTestAgentInstallation } from "./src/child-configuration.ts"; /** * The provider name a controlled child registers its Agent under. diff --git a/packages/test-agent/src/child-configuration.ts b/packages/test-agent/src/child-configuration.ts index e60e34be2..0afd6061a 100644 --- a/packages/test-agent/src/child-configuration.ts +++ b/packages/test-agent/src/child-configuration.ts @@ -35,6 +35,7 @@ import type { Operation } from "effection"; import { hasContent, registerAgentProvider, tryContent } from "@executablemd/core"; import type { AgentComponentsOptions, Json } from "@executablemd/core"; import { createPartitionedAcpxProvider } from "@executablemd/acp"; +import { installInvocationAgentProvider } from "@executablemd/core/host"; import { installControlledLauncher } from "@executablemd/runtime"; import type { ChildDeclaration, @@ -63,16 +64,6 @@ function mappingKey(agent: string, session: string): string { return JSON.stringify([agent, session]); } -/** - * The table key one agent's any-session mapping occupies. - * - * A shape `mappingKey()` cannot produce for any session name, so the two kinds - * of declaration share one duplicate check without one masking the other. - */ -function anyKey(agent: string): string { - return JSON.stringify([agent]); -} - function describeMapping(agent: string, session: string): string { return `agent "${agent}" and session "${session === "" ? "(default)" : session}"`; } @@ -110,7 +101,7 @@ function open(collect: { const declareScenario: ChildDeclarationChild = function* ( props: Record, ): Operation { - const { agent, session: sessionProp, src, anySession } = props; + const { agent, session: sessionProp, src } = props; if (typeof src !== "string" || src.length === 0) { refuse(`<${SCENARIO}> requires a "src" prop.`); return ""; @@ -121,35 +112,19 @@ function open(collect: { return ""; } const agentName = typeof agent === "string" ? agent : defaultAgent; - const serveAny = anySession === true || anySession === "" || anySession === "true"; - if (serveAny && typeof sessionProp === "string") { - refuse( - `<${SCENARIO}> was written with both "session" and "anySession". One names the ` + - "conversation it answers for and the other says it does not name one.", - ); - return ""; - } const session = typeof sessionProp === "string" ? sessionProp : ""; - // Kept in the same table as the exact mappings, under a key no session name - // can produce, so declaring two of them for one agent is refused where it is - // written rather than discovered when a prompt arrives. - const key = serveAny ? anyKey(agentName) : mappingKey(agentName, session); + const key = mappingKey(agentName, session); if (mapped.has(key)) { // Refused where it is written rather than where it would be used: the // wrapper can only find out when a prompt asks for the mapping, and a // child has not been created yet for one to ask in. - refuse( - serveAny - ? `<${SCENARIO}> maps any session of agent "${agentName}" more than once.` - : `<${SCENARIO}> maps ${describeMapping(agentName, session)} more than once.`, - ); + refuse(`<${SCENARIO}> maps ${describeMapping(agentName, session)} more than once.`); return ""; } mapped.add(key); scenarios.push({ agent: agentName, session, - ...(serveAny ? { anySession: true } : {}), rootDir: read.value.rootDir, document: read.value.document, }); @@ -187,11 +162,11 @@ function open(collect: { * Build this package's Agent behavior inside one nested child. * * Called by the trusted host, in the child's own isolated scope, from the - * frozen data one declaration produced. Exactly one partition: this child is - * the isolation boundary, so a sibling execution repeating the same - * declarations reaches its own controller, its own worker, its own routes and - * its own logical sessions, and a named session continues only within the - * child whose declaration created it. + * frozen data one declaration produced. This child is the isolation boundary, + * so a sibling execution repeating the same declarations reaches its own + * controller, workers, routes and logical sessions. The ordinary provider has + * one partition; each controlled Plan invocation gets a private partition so + * its derived identity and working directory cannot meet a sibling's state. * * What comes back is what the host passes to `installAgentComponents()`. The * wrapper installs its provider from inside a running document, where @@ -205,28 +180,40 @@ function open(collect: { * outcome the `` binds — rather than an enclosing `` the child * cannot see. */ +export interface ChildTestAgentInstallation { + readonly components: AgentComponentsOptions; + installPlanProvider(request: { + readonly agent: string; + readonly authoredSession?: string; + readonly session: string; + readonly workdir: string; + readonly policy: { + readonly systemInstruction: string; + readonly permissionMode: "deny-all"; + readonly mcpServers: readonly never[]; + readonly allowedTools: readonly never[]; + }; + observeTurn?(): Operation; + }): Operation; +} + export function* installChildTestAgent( configuration: TestAgentChildConfiguration, options: { /** How the trusted entrypoint re-invokes itself as the agent worker. */ readonly workerCommand: readonly string[]; }, -): Operation { +): Operation { const controller = yield* useTestAgentController(); const declarations = new Map(); for (const scenario of configuration.scenarios) { - declarations.set( - scenario.anySession === true - ? anyKey(scenario.agent) - : mappingKey(scenario.agent, scenario.session), - { - agent: scenario.agent, - sessionName: scenario.session, - rootDir: scenario.rootDir, - document: { path: scenario.document.path, source: scenario.document.source }, - duplicate: false, - }, - ); + declarations.set(mappingKey(scenario.agent, scenario.session), { + agent: scenario.agent, + sessionName: scenario.session, + rootDir: scenario.rootDir, + document: { path: scenario.document.path, source: scenario.document.source }, + duplicate: false, + }); } const partition = yield* provisionPartition({ defaultAgent: configuration.defaultAgent, @@ -246,11 +233,52 @@ export function* installChildTestAgent( yield* installControlledLauncher({}); const permissionMode = "deny-all"; return { - defaultAgent: configuration.defaultAgent, - permissionMode, - rootProvider: { - factory, - options: { defaultAgent: configuration.defaultAgent, permissionMode }, + components: { + defaultAgent: configuration.defaultAgent, + permissionMode, + rootProvider: { + factory, + options: { defaultAgent: configuration.defaultAgent, permissionMode }, + }, + }, + *installPlanProvider(request): Operation { + const planDeclarations = new Map(declarations); + if (request.authoredSession !== undefined) { + const declared = declarations.get(mappingKey(request.agent, request.authoredSession)); + if (declared === undefined) { + throw new Error( + `no maps ${describeMapping(request.agent, request.authoredSession)}`, + ); + } + const key = mappingKey(request.agent, request.session); + const existing = planDeclarations.get(key); + if (existing !== undefined && existing !== declared) { + throw new Error( + `duplicate mappings for ${describeMapping(request.agent, request.session)}`, + ); + } + planDeclarations.set(key, declared); + } + const plan = yield* provisionPartition({ + defaultAgent: configuration.defaultAgent, + controller, + declarations: planDeclarations, + workerCommand: [...options.workerCommand], + planCeiling: { + workdir: request.workdir, + policy: request.policy, + ...(request.observeTurn === undefined ? {} : { observeTurn: request.observeTurn }), + }, + }); + // deno-lint-ignore require-yield + const planFactory = createPartitionedAcpxProvider(function* () { + return plan.provider; + }); + yield* registerAgentProvider(TEST_AGENT_PROVIDER, planFactory); + yield* installInvocationAgentProvider(TEST_AGENT_PROVIDER, { + defaultAgent: request.agent, + permissionMode: request.policy.permissionMode, + }); }, }; } diff --git a/packages/test-agent/src/components.ts b/packages/test-agent/src/components.ts index 990839fb6..29af4099e 100644 --- a/packages/test-agent/src/components.ts +++ b/packages/test-agent/src/components.ts @@ -94,11 +94,6 @@ function configError(source: string, message: string): ErrorSegment { return { type: "error", message: `<${source}> ${message}`, source }; } -/** Where one agent's any-session mapping sits, if it declared one. */ -function anyDeclarationKey(agent: string): string { - return JSON.stringify([agent]); -} - function declarationKey(agent: string, sessionName: string): string { // JSON encoding keeps the key textual and collision-safe for any // agent/session values. @@ -160,9 +155,9 @@ function resolvePinned( * reaches its own of each. * * Shared by both placements of ``. The wrapper provisions one of - * these per enclosing ``; nested-run configuration provisions exactly one, - * inside the child it configures. What differs is the lifetime the caller - * acquires it in — the partition itself is the same world either way. + * these per enclosing ``; nested-run configuration provisions one for the + * ordinary child and a private one for each controlled Plan invocation. Each is + * acquired inside the child it belongs to and remains its own world. */ export function* provisionPartition(options: { defaultAgent: string; @@ -170,8 +165,18 @@ export function* provisionPartition(options: { declarations: ReadonlyMap; /** How to re-invoke this host as the agent worker. */ workerCommand: string[]; + planCeiling?: { + readonly workdir: string; + readonly policy: { + readonly systemInstruction: string; + readonly permissionMode: "deny-all"; + readonly mcpServers: readonly never[]; + readonly allowedTools: readonly never[]; + }; + observeTurn?(): Operation; + }; }): Operation { - const { defaultAgent, controller, declarations, workerCommand } = options; + const { defaultAgent, controller, declarations, workerCommand, planCeiling } = options; const scenarios = new Map(); const pending = new Map>(); const bySessionKey = new Map(); @@ -182,13 +187,7 @@ export function* provisionPartition(options: { sessionName: string | undefined, dir: string, ): Operation { - // The exact mapping first, always. An any-session declaration is a fallback - // an author opted into for conversations they cannot name — `` derives - // its session from the expansion that asked — and it must never take a - // session somebody did name. - const declared = - declarations.get(declarationKey(agentName, sessionName ?? "")) ?? - declarations.get(anyDeclarationKey(agentName)); + const declared = declarations.get(declarationKey(agentName, sessionName ?? "")); if (!declared) { throw new Error(`no maps ${describeMapping(agentName, sessionName)}`); } @@ -249,6 +248,9 @@ export function* provisionPartition(options: { return { route: pinned.scenario.route, resolved: () => {} }; } const scenario = yield* provision(context.agentName, context.session, context.cwd); + if (planCeiling?.observeTurn !== undefined) { + yield* planCeiling.observeTurn(); + } return { route: scenario.route, resolved(value) { @@ -277,6 +279,7 @@ export function* provisionPartition(options: { // bound to a build the next does not share. executableObserver: createControlledExecutableObserver().observer, routeStore: createMemorySessionRouteStore(), + ...(planCeiling === undefined ? {} : { planCeiling }), }); return { provider, boundaryScope, scenarios, pending, bySessionKey }; } @@ -510,15 +513,6 @@ export const SCENARIO_PROPS: PropsSchema = { src: { type: "string" }, agent: { type: "string" }, session: { type: "string" }, - /** - * Answer for any of this agent's sessions no exact mapping claims. - * - * For a conversation whose name a test cannot write down — `` derives - * its session from the expansion that asked. An exact mapping always wins, - * and it is an explicit opt-out of exact matching rather than a new meaning - * for an omitted `session`. - */ - anySession: { type: "boolean" }, }, required: ["src"], additionalProperties: false, diff --git a/packages/test-agent/src/provider.ts b/packages/test-agent/src/provider.ts index 9594d0d52..8695abbe9 100644 --- a/packages/test-agent/src/provider.ts +++ b/packages/test-agent/src/provider.ts @@ -73,6 +73,16 @@ export interface TestAgentProviderOptions { /** Which build this partition observes. Its own controlled one. */ executableObserver?: ExecutableObserver; dependencies?: AcpxProviderDependencies; + /** The fixed, narrower ceiling used only by a trusted child Plan host. */ + planCeiling?: { + readonly workdir: string; + readonly policy: { + readonly systemInstruction: string; + readonly permissionMode: "deny-all"; + readonly mcpServers: readonly never[]; + readonly allowedTools: readonly never[]; + }; + }; } /** @@ -137,6 +147,7 @@ export const TEST_AGENT_CLIENT_NATIVE_ADAPTER: NativeAdapter = { export function* useTestAgentProvider(options: TestAgentProviderOptions): Operation { let pendingRoute: string | undefined; const routeSlot = yield* useRouteSlot(); + const planCeiling = options.planCeiling; // ACPX tokenizes the command on whitespace with quote support, so // command segments containing spaces (e.g. a binary path) are quoted. @@ -194,6 +205,20 @@ export function* useTestAgentProvider(options: TestAgentProviderOptions): Operat ...(options.dependencies?.createRuntime ? { createRuntime: options.dependencies.createRuntime } : {}), + ...(planCeiling === undefined + ? {} + : { + // deno-lint-ignore require-yield + *agentCwd(): Operation { + return planCeiling.workdir; + }, + mcpServers: [...planCeiling.policy.mcpServers], + permissions: "strict", + newSessionOptions: { + systemPrompt: planCeiling.policy.systemInstruction, + allowedTools: [...planCeiling.policy.allowedTools], + }, + }), }, ); } diff --git a/packages/testing/src/child-configuration.ts b/packages/testing/src/child-configuration.ts index 1899072b0..7aae374b8 100644 --- a/packages/testing/src/child-configuration.ts +++ b/packages/testing/src/child-configuration.ts @@ -63,17 +63,6 @@ export interface ChildScenario { readonly agent: string; /** The logical session, or the empty string for the unnamed one. */ readonly session: string; - /** - * Whether this mapping answers for any of its agent's sessions that no exact - * mapping claims. - * - * For a conversation whose name the test cannot write down. `` is the - * case it exists for: it derives its session from the expansion that asked, - * so there is no name an author could put here. An exact mapping always wins, - * and an agent may declare at most one of these — it is an explicit opt-out - * of exact matching, never what omitting `session` means. - */ - readonly anySession?: boolean; /** The containment root the behavior document's dependencies resolve under. */ readonly rootDir: string; readonly document: { readonly path: string; readonly source: string }; @@ -171,7 +160,6 @@ function detachScenario(scenario: ChildScenario): ChildScenario { return frozen({ agent: scenario.agent, session: scenario.session, - ...(scenario.anySession === true ? { anySession: true } : {}), rootDir: scenario.rootDir, document: frozen({ path: scenario.document.path, source: scenario.document.source }), }); diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index 360c58be0..9442d5387 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -2823,6 +2823,13 @@ review exists. What the ceiling then does — the permission mode, the prompt-failure policy, the capability refusals, the empty session directory — is the same whichever provider is underneath. +For that controlled child alone, an authored `` gives +the trusted host the exact scenario label to select before it privately maps the +declaration to Plan's derived conversation identity. The provider and scenario +state remain keyed by that opaque identity and the child working directory. +This adds no TestAgent matcher or fallback, and ordinary Agent sessions retain +their exact-session semantics. + Two registrations for one name and kind at the same scope are a configuration error naming both origins. Installation order is not a resolution mechanism — reserved and default registrations are held apart, so which one wins is decided diff --git a/specs/test-agent-spec.md b/specs/test-agent-spec.md index c4a31c61c..4b92f7fc8 100644 --- a/specs/test-agent-spec.md +++ b/specs/test-agent-spec.md @@ -82,21 +82,14 @@ wildcard. A named `` requires an exact named mapping. A missing or duplicate mapping fails the owning test before its agent turn starts. An unused mapping does not fail the test-agent scope. -`anySession` is the one way to answer without naming a conversation, and it is -an explicit opt-in rather than a new meaning for an omitted `session`: - -```md - -``` - -It exists for a conversation whose name a test cannot write down. `` -derives its session from the expansion that asked for it, so no author can name -it and no name stays true when the document moves. Such a mapping answers for -any of its agent's sessions that no exact mapping claims: an exact mapping -always wins, so every guarantee above still holds wherever one is written. An -agent declares at most one, and writing both `session` and `anySession` on one -scenario is refused where it is written. Two of them for one agent is a -duplicate mapping like any other. +The configured-child Plan host has one sealed translation because `` +derives an opaque identity after the declaration was captured. A Plan site may +author a label such as `session="planner"`; the trusted host selects the exact +scenario declared under that label and installs it under that invocation's +opaque identity. Runtime and scenario state stay keyed by the opaque identity +and child working directory. This is not a TestAgent matcher: ordinary Agent +use still requires the exact session it opens, a missing authored Plan label is +refused, and siblings cannot reach one another's mapping or state. The test runtime does not inject agent, session, cwd, or harness metadata into the behavior document. The document sees its own frontmatter and bindings, diff --git a/specs/testing-spec.md b/specs/testing-spec.md index 227d4aeb9..f259ac226 100644 --- a/specs/testing-spec.md +++ b/specs/testing-spec.md @@ -445,10 +445,12 @@ Only the canonical declaration this harness recognizes grants any of it. A repository component named `TestAgent` ends the scan and configures nothing, so a child that reached one is refused exactly as an unconfigured child is. -A scenario reaches the Plan's own turn through `anySession` -(specs/test-agent-spec.md): `` derives its conversation from the expansion -that asked, so there is no session name a test could write, and that opt-in is -what answers for it without weakening exact matching anywhere else. +A scenario reaches the Plan's own turn by its exact authored label. The trusted +child host privately connects that label to the opaque conversation identity +`` derives for the invocation. Scenario selection uses the label; provider +and scenario state remain keyed by the opaque identity and working directory. +This mapping belongs only to the sealed Plan-authorship path, so ordinary Agent +use keeps exact-session matching and siblings share no state. Only detached declaration data enters the host-profile request. A test cannot send an Agent provider, Elicitation middleware, context, Api handler, execution From 557a5a3c2367c6b8800710bd9f7260a8698b9cb7 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Wed, 2 Sep 2026 13:33:23 -0400 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=90=9B=20Update=20two=20test-agent=20?= =?UTF-8?q?call=20sites=20for=20the=20child=20installation=20shape=20(#728?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `installChildTestAgent()` now answers with the components *and* the operation that installs a Plan provider, so the whole value is no longer what `installAgentComponents()` takes. Two call sites in the package's own tests still passed it whole, which left `deno task check` red. Only the calls move; both cases assert exactly what they asserted before. Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7 --- packages/test-agent/tests/components.test.ts | 2 +- packages/test-agent/tests/smoke.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/test-agent/tests/components.test.ts b/packages/test-agent/tests/components.test.ts index 9d3f005c3..5c6df2e68 100644 --- a/packages/test-agent/tests/components.test.ts +++ b/packages/test-agent/tests/components.test.ts @@ -673,7 +673,7 @@ describe( }, { workerCommand: options.worker }, ); - yield* installAgentComponents(agent); + yield* installAgentComponents(agent.components); if (options.answers) { yield* installAnswerProvider(options.answers); } diff --git a/packages/test-agent/tests/smoke.test.ts b/packages/test-agent/tests/smoke.test.ts index c98c7f3b3..86780eb7d 100644 --- a/packages/test-agent/tests/smoke.test.ts +++ b/packages/test-agent/tests/smoke.test.ts @@ -58,7 +58,7 @@ function nestedRunHost(worker: string[]): ExecutionHostProvider { switch (configuration.kind) { case "test-agent": yield* installAgentComponents( - yield* installChildTestAgent(configuration, { workerCommand: worker }), + (yield* installChildTestAgent(configuration, { workerCommand: worker })).components, ); installations.push({ components: agentIdentityComponents() }); break; From 548c90f96539764600c32a87cd497efdf5ca169a Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Wed, 2 Sep 2026 15:10:13 -0400 Subject: [PATCH 8/9] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Say=20a=20Plan's=20Age?= =?UTF-8?q?nt=20availability=20as=20a=20Result,=20and=20its=20restrictions?= =?UTF-8?q?=20as=20policy=20(#728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Availability and restrictions were one word doing two jobs. They are now two. Whether a host can give a Plan an Agent is `Result`: `Ok` with the capability, or `Err` with the sentence a person reads. The bespoke `{ established, authorship | refusal }` protocol is gone, and provider installation remains impossible on `Err` because there is no capability to call. What a Plan then runs under stays where it was — `installAuthorshipFrame()` owns the fixed policy, identical whoever supplies the Agent. The observation a trusted host receives is now read from what the adapter assembled and the frame installed, after the last install, rather than from the policy either was handed. A report taken from the input agreed with the policy however the adapter assembled its dependencies, so it could not tell an assembly that honored the policy from one that dropped it: both adapters now build their provider dependencies once and describe that object, the provider is the name that actually routes a turn, and the prompt-failure policy is reported by the call that installs it. Changing the system instruction, the permission mode, the prompt-failure policy, the MCP servers, the native tools or the provider fails PMT4 — verified by changing the assembled tool set, which fails it. The diagnostics are the approved sentences, and the negative assertions read them whole rather than by fragment, at the same moment they were read before. `TEST_AGENT_PROVIDER` is unexported again; Plan routing stays private. Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7 --- architecture.md | 4 +- packages/cli/src/authorship-profile.ts | 257 +++++++++++------- packages/cli/src/cli.ts | 14 +- packages/cli/src/plan-component.ts | 30 +- packages/cli/src/plan.ts | 10 +- packages/cli/src/testing-host.ts | 91 +++---- packages/cli/tests/agent-adapters.test.ts | 12 +- .../tests/document-suites/plan/Plan.test.md | 16 +- packages/cli/tests/plan-component.test.ts | 8 +- packages/cli/tests/support/plan-harness.ts | 6 +- .../cli/tests/support/run-markdown-tier.ts | 14 +- .../cli/tests/testing-execution-host.test.ts | 46 +++- packages/test-agent/mod.ts | 12 +- .../test-agent/src/child-configuration.ts | 96 +++++-- packages/test-agent/src/components.ts | 22 +- packages/test-agent/src/provider.ts | 35 +-- packages/test-agent/tests/components.test.ts | 10 +- specs/testing-spec.md | 51 ++-- 18 files changed, 410 insertions(+), 324 deletions(-) diff --git a/architecture.md b/architecture.md index 35e0b7cb3..64b76421f 100644 --- a/architecture.md +++ b/architecture.md @@ -44,7 +44,7 @@ Existing documents and code get aligned to this section retroactively. | Plan | the executable program produced from a Prompt: an Executable Markdown document combining readable prose that expresses the Prompt's intent with the components that carry it out, each placed beside the prose describing what it does. It begins with one descriptive level-one heading. A Plan is what `xmd plan` approves and then delivers: printed to stdout by default, written to an `--output` file, and run only under `--run`. It is not a synonym for a workflow, a policy document or any executable Markdown file | | plan command document | the one exact checked-in first-party Markdown value root `xmd plan` executes. It is the command's adapter and nothing else: it projects the request into ``, supplies the session, and returns the approved source. It is not itself a Plan. Internal: no command-line option selects another one, and no repository component search can answer for it | | packaged `` Component | the one exact checked-in first-party Markdown value component that converts a Prompt into a Plan, `packages/cli/src/documents/Plan.md`, declared to every ordinary run as the public ``. It owns and implements the Plan authorship workflow — the Prompt wording, the draft and repair loops, the `` branches, human review, revision, approval, stopping, exhaustion and the final explanation turn — and returns the exact approved Plan source. Every Plan-producing turn in it states the complete Plan requirements for itself, so a replacement may add or correct a title rather than only carry one forward. Both surfaces expand these exact bytes under one origin and one digest; there is no generated TypeScript copy and no second Markdown implementation. Its four phase components are private to it, and it is not itself a Plan | -| authorship profile | the trusted-host assembly the packaged `` Component runs its authored turns under, installed by its own `` inside the invocation that owns it rather than around an execution — which is what makes it the same ceiling whether `xmd plan`, an ordinary document, or a configured `` run child asked. Which Agent provider goes under it is a trusted-host capability the declaration carries — the production ACPX one built from the run's Agent stack, or the deterministic one a canonical `` child declaration produced — and a host that supplies none states the sentence a `` written there is refused with. Everything else about the ceiling is installed in one place for both, so a second provider cannot bring a weaker one: its fixed inputs, a constrained Agent provider, Elicitation, the fixed first-party components and the host-declared ``. It uses no repository component search and exposes no custom root, and the ceiling it establishes is not readable from the command line. Its working directory is one host-owned directory dedicated to the logical session, keyed by the digest of that name, created empty and required to be empty on the way in. An explicitly named session's directory is durable, because continuation derives the same session identity from it; an invocation-unique default session's is scope-owned, claimed before it is created, and exactly one cleanup is attempted after profile teardown and before admission on every ending — the leaf removed non-recursively when it is still the empty directory that was handed over, and left as found with the command failing terminally when it has gained content or vanished. Where those directories live is a host dependency no caller or document selects | +| authorship profile | the trusted-host assembly the packaged `` Component runs its authored turns under, installed by its own `` inside the invocation that owns it rather than around an execution — which is what makes it the same frame whether `xmd plan`, an ordinary document, or a configured `` run child asked. Which Agent context goes under it is a trusted-host capability the declaration carries — the production ACPX one built from the run's Agent stack, or the deterministic one a canonical `` child declaration produced — and a host that supplies none states the sentence a `` written there is refused with. The fixed policy is installed in one place for both, so a second provider cannot bring a weaker one: its fixed inputs, a constrained Agent provider, Elicitation, the fixed first-party components and the host-declared ``. It uses no repository component search and exposes no custom root, and the policy it installs is not readable from the command line. Its working directory is one host-owned directory dedicated to the logical session, keyed by the digest of that name, created empty and required to be empty on the way in. An explicitly named session's directory is durable, because continuation derives the same session identity from it; an invocation-unique default session's is scope-owned, claimed before it is created, and exactly one cleanup is attempted after profile teardown and before admission on every ending — the leaf removed non-recursively when it is still the empty directory that was handed over, and left as found with the command failing terminally when it has gained content or vanished. Where those directories live is a host dependency no caller or document selects | | upgrade command document | the one exact checked-in first-party Markdown streaming text root `xmd upgrade` executes to select and install a published release. It owns the exact-tag grammar, release selection, semantic-version comparison, consent, the status, already-current and installation branches, and the wording of every refusal and report; its rendered body is the command's output rather than a value it returns. Internal: no command-line option selects another one, and no repository component search can answer for it | | upgrade assembly | what one runtime-named entrypoint states about the `xmd` that is running: its provenance, reported version, invoked executable path, platform, architecture, release target when the release publishes one, and — for an eligible compiled macOS or Linux host alone — the factory for the four phases an installation needs. It describes how this `xmd` is running, never how its files arrived | | release identity | the invocation-local opaque identifier `` mints for each release it admits. Holding one is what authorizes downloading that release, and nothing outside that one invocation's private admission map can read, extend or forge it. A download mints an upgrade candidate identity in the same way, and that candidate advances `downloaded → verified → committed` exactly once | @@ -3768,7 +3768,7 @@ Status is measured against main. | `xmd syntax` | describes every structural construct and every selected component the production `run` profile would let a document write in the contextual working directory, as deterministic Markdown or as version-1 JSON, from one catalog. Inspection only: it registers the run profile's declarations in a bounded scope and reads the filesystem for which files exist and, for a selected Markdown component, that file's frontmatter. It runs no body, imports no repository TypeScript module, installs no provider, mints no authority and writes no journal. An include it cannot enumerate — a selection-relevant symbolic link to a directory beneath it included — fails the whole request rather than printing a healthy subset | built on the #632 stack | | document validation | validates one supplied root projection and the recursive Markdown source closure normal component selection discovers, returning deterministic version-1 document diagnostics and `valid`, `invalid` or `not-statically-checkable` invocation outcomes without evaluating document code or installing operational host behavior | built on the #654 stack | | `xmd plan` | turns one Prompt into a Plan and delivers it, by executing one root document — and, only under `--run`, a second — with a complete scope boundary between them. First the packaged plan command document, under the internal `` identity — an adapter that projects the request into `` and returns what comes back — and inside that, the packaged `` Component under the authorship profile its own `` installs: one enclosing Session, a host ceiling of one host-owned directory dedicated to that logical session — under `~/.xmd/plan/sessions` by default, keyed by the digest of the name, never the name, created empty and required to be empty before the provider exists or a session is materialized, refused rather than cleaned when it is not, durable when the caller named the session and handed back non-recursively after teardown when it did not — with no additional directories, no MCP servers, no native tools and a private strict denial no permission flag widens, no Files, command, service or network capability for that document, no repository component search, and the Component's own private `` whose closed assessment answers `valid: false` for a defect the draft authored and raises for a defect the command line authored. Its instructions require every Plan to begin with one descriptive level-one title and to keep the Prompt's outcomes as readable steps with each component beside the step it performs, through repairs and revisions alike; that is an authorship and human-review requirement, and `` never enforces it. A tenth draft that still has problems may be stopped or explained: the explanation is one more ordinary turn in the same Session carrying only the final diagnostics, is inert text, reopens no draft limit, and ends the command. The host's instruction layer states only that an answer belongs to the message that asked for it, so which shape a turn wants stays in the document. Authorship sits outside durability: it runs on an invocation-owned in-memory stream that is never journaled, persisted, reused or replayed. Then, only after every provider, Prompt task and Elicitation resource inside the authorship frame has torn down, the Component's own `` structurally admits the exact approved bytes, and after that execution ends the host validates the returned Plan again against the command line, resolves props for exactly those bytes, and then delivers the approved Plan where the caller asked: to stdout byte for byte by default, to an exclusively created `--output` path, and — only under `--run` — through the ordinary supplied-source path under the `` identity, one ordinary document with its own Agent provider, its own journal and ordinary run output, result and failure behavior. A journal exists only when `--run` begins, and the flags that configure only a run are refused in preflight without it. The retired `prompt` spelling is not a command and is not absorbed by the default `run` grammar, which would read it as a document reference and execute a file of that name: an invocation whose exact first token is `prompt` is refused before any scan, selection or path lookup, establishing nothing, while `xmd run ./prompt` still executes a document legitimately called that | built on the #660 stack | -| `` | writes and reviews one Plan, from a Prompt an ordinary document wrote. `` expands its paired body once with the capabilities the calling document already has — the complete untrimmed rendering is the Prompt, and it is never emitted separately — and binds the exact approved Plan source. It is the public name of the packaged `` Component: exact first-party Markdown declared to every ordinary run, so a repository `Plan.md`, a workflow bundle, a registration, `Component.importComponent` middleware and another loaded copy can none of them answer for it. Paired only, one optional non-empty `session` prop, and a required `as`; a body that renders to nothing fails before any catalog, directory, Session, turn, review or check exists. The Agent writes under exactly the `xmd plan` ceiling however broad the calling document's authority is, and a host that cannot establish it refuses before placement — including the `` child an `xmd test` document launches, which is the run profile and therefore resolves the same protected bytes rather than reporting a missing component. An authored `session` keeps the existing durable named-directory lifetime, so the same name at the same site reaches the same conversation next time, while an omitted one is site- and iteration-unique, replay-stable and handed back after teardown; sibling sites stay distinct even when they write one name, and the name never becomes a path. Every turn, answer, check, approval and admission belongs to the enclosing document's journal, so a continuation restores completed authorship instead of repeating it; there is no second journal. Complete authorship teardown precedes structural admission, which precedes the binding — and the admission is structure alone, so a Plan declaring properties a later run will supply is returned rather than refused. It prints no source, creates no file, and executes nothing it returns | built on the #660 stack; no delivery, custom root, policy prop or replacement selector exists (#536 owns constrained caller-authored policy) | +| `` | writes and reviews one Plan, from a Prompt an ordinary document wrote. `` expands its paired body once with the capabilities the calling document already has — the complete untrimmed rendering is the Prompt, and it is never emitted separately — and binds the exact approved Plan source. It is the public name of the packaged `` Component: exact first-party Markdown declared to every ordinary run, so a repository `Plan.md`, a workflow bundle, a registration, `Component.importComponent` middleware and another loaded copy can none of them answer for it. Paired only, one optional non-empty `session` prop, and a required `as`; a body that renders to nothing fails before any catalog, directory, Session, turn, review or check exists. The Agent writes under exactly the `xmd plan` fixed policy however broad the calling document's authority is, and a host that supplies no Agent context refuses before placement — including the `` child an `xmd test` document launches, which is the run profile and therefore resolves the same protected bytes rather than reporting a missing component, and which supplies one when it declares a canonical ``. An authored `session` keeps the existing durable named-directory lifetime, so the same name at the same site reaches the same conversation next time, while an omitted one is site- and iteration-unique, replay-stable and handed back after teardown; sibling sites stay distinct even when they write one name, and the name never becomes a path. Every turn, answer, check, approval and admission belongs to the enclosing document's journal, so a continuation restores completed authorship instead of repeating it; there is no second journal. Complete authorship teardown precedes structural admission, which precedes the binding — and the admission is structure alone, so a Plan declaring properties a later run will supply is returned rather than refused. It prints no source, creates no file, and executes nothing it returns | built on the #660 stack; no delivery, custom root, policy prop or replacement selector exists (#536 owns constrained caller-authored policy) | | `` / `` | chooses one branch by comparing a value with `===`. `` decides its whole case structure from source before evaluating anything, then evaluates the selector once and each non-default matcher at most once in source order, expands the first `===` match — or the final default, or nothing — inline and transparently, and appends no journal event | built on the #692 stack | | `xmd upgrade` | replaces the standalone binary that ran it with a published release, by executing one root document: the packaged upgrade command document, under the internal `` identity, with an empty component search path and no Files, Process, Service, command, Fetch, Agent, Elicitation, workflow or repository capability. That document is an **ordinary streaming text root** — it declares no `returns` and uses neither `` nor `` — so its rendered body is the command's output: each root segment reaches the reader as it completes, and a branch the command did not take contributes no prose, no phase call and no result. Its durable events go to one invocation-local in-memory stream, or to the file `--journal` named and the CLI exclusively created; neither is ever read back, and neither grants any resume or retry authority. Markdown owns the whole of the policy — the exact-tag grammar, which release is selected, semantic-version comparison through the npm `semver` package, which consent an install needs, the status, already-current and installation branches, and the wording of every refusal and every report. A compiled macOS or Linux binary whose platform the release publishes for is the only host that declares the four phases that policy may reach, ``, ``, `` and ``, and it declares them to canonical execution rather than through any contextual Api, middleware, repository lookup, ordinary `xmd run` profile or public syntax catalog; every other entrypoint states its provenance and no authority at all, so an npm, Bun, Deno-source or compiled Windows invocation has no phase to reach and stops at its own refusal before release lookup or any filesystem change. That host alone owns the private half: the exact `process.execPath` spelling it will replace and never a link it resolved, one non-blocking exclusive advisory lock on a stable sidecar beside that file, the bounded anonymous GitHub reads under a scope-bound abort signal, the downloaded bytes, the digest, the staged candidate it runs for its version, and one same-directory rename. Opaque identity is the boundary between the two halves — a release identity per admitted release, then one candidate advancing `downloaded → verified → committed` exactly once, with one installation attempt per invocation — so the document chooses among the releases it was shown and can name no other release, target, asset or destination, skip verification or replay a phase. Before the rename every failure and cancellation leaves the installed file byte-identical; after it the candidate is authoritative and no cleanup restores the old bytes | built on the #659 stack | | `` / `printErrors(fn)` | prints failures | built on main | diff --git a/packages/cli/src/authorship-profile.ts b/packages/cli/src/authorship-profile.ts index 330346035..a14acc0b7 100644 --- a/packages/cli/src/authorship-profile.ts +++ b/packages/cli/src/authorship-profile.ts @@ -10,7 +10,7 @@ * the host-declared draft checker, and it exposes no custom root and no * repository component search: the document it runs is the one the CLI ships. * - * The Agent ceiling is assembled here rather than read from the command line, + * The Agent context is assembled here rather than read from the command line, * because it is not the caller's to choose. Writing a Plan is a conversation * about text; it never lets an agent touch anything. So the provider gets a * host-owned directory dedicated to this logical session — created empty, and @@ -18,7 +18,7 @@ * MCP servers, an empty native-tool allowlist and a private strict denial of * every native * permission request — one that answers inside the provider and consults no - * authored approval scope, so nothing composed around it can widen a ceiling + * authored approval scope, so nothing composed around it can widen a policy * with nothing in it. `--approve-all`, `--approve-reads` and `--deny-all` * configure the approved document, later, and reach none of this. * @@ -106,8 +106,8 @@ export interface AuthorshipProfile { * test never reads, creates or removes anything under a real one. */ root: string; - /** Whether this host can put an Agent under the Plan ceiling, and why not. */ - ceiling: PlanAuthorshipCeiling; + /** The Agent context this host can give a Plan, or why it can give none. */ + context: Result; /** Who answers the review question. */ installElicitation(): Operation; /** @@ -115,7 +115,7 @@ export interface AuthorshipProfile { * * Built by the command, from the packaged Component's bytes, before the adapter * root is imported. It carries the sealed surface, the precomputed catalog and - * the ceiling this invocation settled — none of which is a prop the adapter + * the Agent context this invocation settled — none of which is a prop the adapter * could supply or a document could reach. */ declaration: DeclaredMarkdownComponent; @@ -131,13 +131,13 @@ export interface AuthorshipProfile { } /** What building the constrained provider needs, and nothing more. */ -export interface AuthorshipCeilingInputs { +export interface AuthorshipProviderInputs { readonly stack: AgentStack; readonly acp?: AcpxProviderDependencies; } /** - * The trusted host's ability to put an Agent under the Plan ceiling. + * The trusted host's ability to give one Plan invocation an Agent. * * A closure the host supplies before any Plan invocation exists, and the only * thing that decides whether a Plan can be written here. It is never a prop, a @@ -145,26 +145,23 @@ export interface AuthorshipCeilingInputs { * middleware can reach or replace — which is what keeps "who may write a Plan" * a question about the host rather than about what a document arranged. * - * What it does not decide is the rest of the ceiling. The permission mode, the - * prompt-failure policy, the capability refusals and the session directory are - * {@link installAuthorshipFrame}'s, identically for every provider, so a second - * implementation cannot quietly bring a weaker ceiling with it. + * Availability is all it decides. What the Plan then runs under — the permission + * mode, the prompt-failure policy, the capability refusals and the session + * directory — is {@link installAuthorshipFrame}'s fixed policy, identical for + * every provider, so a second implementation cannot quietly bring a weaker one. */ export interface PlanAuthorship { /** The agent this Plan conversation defaults to. */ readonly defaultAgent: string; - /** The trusted adapter that supplies this invocation's provider. */ - readonly origin: "production-acpx" | "controlled-test-agent"; /** - * Install this invocation's Agent provider, inside the Plan ceiling. + * Install this invocation's Agent provider under the fixed policy. * * Called within ``, so what it registers belongs to that one - * invocation and goes when the invocation does. - * - * @param workdir This conversation's directory, established and proven empty. - * @param host The scope captured before the ceiling existed, for host acts. + * invocation and goes when the invocation does. What comes back is what the + * adapter actually assembled, so the frame can report the configuration that + * is installed rather than the one it asked for. */ - installProvider(invocation: PlanAuthorshipInvocation): Operation; + installProvider(invocation: PlanAuthorshipInvocation): Operation; } export interface PlanAuthorshipInvocation { @@ -173,9 +170,9 @@ export interface PlanAuthorshipInvocation { readonly session: string; readonly authoredSession?: string; readonly policy: PlanAuthorshipPolicy; - observe?(observation: PlanAuthorshipObservation): Operation; } +/** The fixed policy every Plan runs under, whoever supplies the Agent. */ export interface PlanAuthorshipPolicy { readonly systemInstruction: string; readonly permissionMode: "deny-all"; @@ -184,73 +181,109 @@ export interface PlanAuthorshipPolicy { readonly allowedTools: readonly never[]; } -export interface PlanAuthorshipObservation { - readonly providerOrigin: PlanAuthorship["origin"]; - readonly policy: PlanAuthorshipPolicy; - readonly workdir: string; -} - /** - * Whether this host can put an Agent under the Plan ceiling, and why not. + * What one adapter actually assembled, read off the values it handed the + * provider rather than off the policy it was given. * - * One value rather than an absent capability beside a reason, because the two - * must agree: a host that cannot establish the ceiling owes the person a - * sentence saying so, and a host that can owes no sentence at all. + * The difference is the whole point. A report built from the policy input would + * say the same thing however the adapter assembled its dependencies, so it + * could not tell an assembly that honored the policy from one that dropped it. */ -export type PlanAuthorshipCeiling = - | { readonly established: true; readonly authorship: PlanAuthorship } - | { readonly established: false; readonly refusal: string }; +export interface PlanProviderAssembly { + /** The provider name this adapter registered, which is what routes a turn. */ + readonly provider: string; + /** The working directory the assembled dependencies actually carry. */ + readonly agentCwd: string; + /** The system instruction a new session is actually opened with. */ + readonly systemInstruction: string | undefined; + /** The native tools a fresh session is actually allowed. */ + readonly allowedTools: readonly string[] | undefined; + /** The MCP servers actually configured. */ + readonly mcpServers: number | undefined; + /** The native permission answer actually configured. */ + readonly permissions: string | undefined; + /** The permission mode actually installed for the invocation. */ + readonly permissionMode: string; +} -/** What a host with no coding-agent ceiling at all refuses a Plan with. */ -export const NO_CEILING = - "this host establishes no coding-agent ceiling, so no Plan can be written here — " + - "no Plan was returned"; +/** Everything a Plan's configuration turned out to be, once it is installed. */ +export interface PlanAuthorshipObservation extends PlanProviderAssembly { + /** Whether the frame installed the policy that ends authorship on a failed turn. */ + readonly promptFailures: PlanAuthorshipPolicy["promptFailures"]; +} + +/** What a host that supplies no Agent at all refuses a Plan with. */ +export const NO_AGENT_CONTEXT = "No Agent context was found. No Plan was returned."; + +/** What a host whose provider supplies no Agent for `` refuses it with. */ +export function noAgentContextFrom(provider: string): string { + return ( + `The ${provider} provider did not provide an Agent context for . ` + + "No Plan was returned." + ); +} /** - * The production ceiling: ACPX, built from the Agent stack this run settled. + * The production Agent context: ACPX, built from the stack this run settled. * - * One concrete provider of {@link PlanAuthorship}, and the only one production - * has. Its ACPX construction, embedded adapters, machine-session assembly, - * system instruction, strict permission policy, empty MCP servers, empty - * allowed tools and controlled working directory are exactly what they were - * when this was the only way to establish a ceiling. + * One concrete implementation of {@link PlanAuthorship}, and the only one + * production has. Its ACPX construction, embedded adapters, machine-session + * assembly, system instruction, strict permission policy, empty MCP servers, + * empty allowed tools and controlled working directory are exactly what they + * were when this was the only way to supply one. */ -export function planAuthorshipCeiling( +export function planAgentContext( stack: AgentStack | undefined, acp?: AcpxProviderDependencies, -): PlanAuthorshipCeiling { +): Result { if (stack === undefined) { - return { established: false, refusal: NO_CEILING }; + return Err(new Error(NO_AGENT_CONTEXT)); } if (stack.provider !== "acpx") { - return { - established: false, - refusal: - `the ${stack.provider} provider cannot establish the Plan authorship ceiling — ` + - "no Plan was returned", - }; + return Err(new Error(noAgentContextFrom(stack.provider))); } - return { - established: true, - authorship: { - defaultAgent: stack.defaultAgent, - origin: "production-acpx", - *installProvider(invocation: PlanAuthorshipInvocation): Operation { - const acpx = createAcpxProvider( - authorshipCeiling( - { stack, ...(acp === undefined ? {} : { acp }) }, - invocation.workdir, - invocation.host, - invocation.policy, - ), - ); - yield* registerAgentProvider("acpx", acpx); - yield* installInvocationAgentProvider("acpx", { - defaultAgent: stack.defaultAgent, - permissionMode: invocation.policy.permissionMode, - }); - }, + return Ok({ + defaultAgent: stack.defaultAgent, + *installProvider(invocation: PlanAuthorshipInvocation): Operation { + // Assembled once and then read, so what is reported is what the provider + // was built from rather than what this adapter was asked for. + const dependencies = authorshipDependencies( + { stack, ...(acp === undefined ? {} : { acp }) }, + invocation.workdir, + invocation.host, + invocation.policy, + ); + yield* registerAgentProvider("acpx", createAcpxProvider(dependencies)); + yield* installInvocationAgentProvider("acpx", { + defaultAgent: stack.defaultAgent, + permissionMode: invocation.policy.permissionMode, + }); + return yield* describeAssembly("acpx", dependencies, invocation.policy.permissionMode); }, + }); +} + +/** + * One assembled provider, as the values it was actually built from. + * + * `agentCwd` is an operation on the dependencies rather than a field, so it is + * asked the way the provider asks it. + */ +export function* describeAssembly( + provider: string, + dependencies: AcpxProviderDependencies, + permissionMode: string, +): Operation { + const session = dependencies.newSessionOptions; + return { + provider, + agentCwd: dependencies.agentCwd === undefined ? "" : yield* dependencies.agentCwd(), + systemInstruction: typeof session?.systemPrompt === "string" ? session.systemPrompt : undefined, + allowedTools: session?.allowedTools, + mcpServers: dependencies.mcpServers?.length, + permissions: + typeof dependencies.permissions === "string" ? dependencies.permissions : undefined, + permissionMode, }; } @@ -270,7 +303,7 @@ export interface AuthorshipFrame { readonly workdir: string; /** The scope the two host acts run in, captured before this frame exists. */ readonly host: Scope; - /** The host's ability to put an Agent under this ceiling. */ + /** The host's ability to give this invocation an Agent. */ readonly authorship: PlanAuthorship; /** The opaque conversation identity the provider must preserve. */ readonly session: string; @@ -283,14 +316,14 @@ export interface AuthorshipFrame { /** * Install the constrained authorship frame on the current scope. * - * One function for both surfaces, because the ceiling a Plan is written under is - * not a property of who asked for it. What leaving this scope tears down is the + * One function for both surfaces, because what a Plan is written under is not a + * property of who asked for it. What leaving this scope tears down is the * provider, the Elicitation resources, the Prompt tasks and the capability - * refusals — so whoever installs it decides what the ceiling covers by choosing + * refusals — so whoever installs it decides what the frame covers by choosing * the scope, and nothing else has to be remembered. * * The refusals go last, over whatever the entrypoint provided, so the document - * is refused rather than served. They are ambient, and a ceiling cannot tell the + * is refused rather than served. They are ambient, and the frame cannot tell the * host's own act from the document's — which is why the two acts that are the * host's run in the scope captured before this one (src/host-acts.ts). */ @@ -298,34 +331,52 @@ export function* installAuthorshipFrame(frame: AuthorshipFrame): Operation yield* openFormsThroughHost(frame.host); yield* frame.installElicitation(); - // Installed here, so the provider resolves to *this* ceiling and not to + // Installed here, so the provider resolves for *this* invocation and not for // whatever the enclosing document registered, and in this invocation rather - // than in a frame nested inside it — the content this ceiling was selected for + // than in a frame nested inside it — the content this frame was selected for // is projected into the invocation, and a provider installed anywhere else // would be invisible to it. The default agent and the permission mode travel // with the installation, so an enclosing document cannot widen either by // inheritance. // // Which provider it is belongs to the host that supplied the capability. What - // does not is everything below: one ceiling, whoever is underneath it. - yield* frame.authorship.installProvider({ + // does not is everything below: one policy, whoever is underneath it. + const assembly = yield* frame.authorship.installProvider({ workdir: frame.workdir, host: frame.host, session: frame.session, ...(frame.authoredSession === undefined ? {} : { authoredSession: frame.authoredSession }), policy: PLAN_AUTHORSHIP_POLICY, - ...(frame.observe === undefined ? {} : { observe: frame.observe }), }); - // A candidate comes from a turn's complete successful close value or from - // nowhere. `` ordinarily renders whatever a failed turn managed to - // emit and carries on, which for a workflow that reviews source would mean - // showing a person half a program; the host decides otherwise here, so a - // failed, cancelled or protocol-invalid turn ends authorship before anything - // is presented. The Component cannot opt out of it. + const promptFailures = yield* installPlanPromptFailurePolicy(); + yield* refuseDocumentCapabilities(); + // After everything, and from what everything turned out to be. A report built + // before the last install would describe an arrangement that does not exist + // yet, which is the one thing a trusted observer must not be given. + if (frame.observe !== undefined) { + yield* frame.observe({ ...assembly, promptFailures }); + } +} + +/** + * End authorship on a failed turn, and say that it was installed. + * + * A candidate comes from a turn's complete successful close value or from + * nowhere. `` ordinarily renders whatever a failed turn managed to emit + * and carries on, which for a workflow that reviews source would mean showing a + * person half a program; the host decides otherwise here, so a failed, cancelled + * or protocol-invalid turn ends authorship before anything is presented. The + * Component cannot opt out of it. + * + * The answer is returned rather than assumed by the caller, so removing this + * installation removes the fact an observer reports rather than leaving one that + * describes an install that no longer happens. + */ +function* installPlanPromptFailurePolicy(): Operation { yield* installPromptFailurePolicy(function* () { return PLAN_AUTHORSHIP_POLICY.promptFailures === "fail"; }); - yield* refuseDocumentCapabilities(); + return PLAN_AUTHORSHIP_POLICY.promptFailures; } /** @@ -339,19 +390,19 @@ export function* installAuthorshipFrame(frame: AuthorshipFrame): Operation export function* runPlanCommandDocument(profile: AuthorshipProfile): Operation> { // Before a directory exists, before a provider exists, and therefore before // any session could be placed or any turn started. A host that cannot - // establish this ceiling refuses rather than writing a Plan under a weaker one. - const ceiling = profile.ceiling; - if (!ceiling.established) { - return Err(new Error(`${ceiling.refusal} — nothing was written or run`)); + // supplies no Agent context refuses rather than writing a Plan under a weaker one. + const context = profile.context; + if (!context.ok) { + return Err(new Error(`${context.error.message} Nothing was output or run.`)); } return yield* scoped(function* (): Operation> { // The agent words and this execution's prompt bookkeeping, with no root - // provider: what writes the Plan is the ceiling the Component installs around + // provider: what writes the Plan is the frame the Component installs around // its own content, and a root provider here would be one the Component's // regional install had to shadow rather than one it owns. yield* installAgentComponents({ - defaultAgent: ceiling.authorship.defaultAgent, + defaultAgent: context.value.defaultAgent, permissionMode: PLAN_AUTHORSHIP_POLICY.permissionMode, }); const source = yield* readPackagedDocument(PLAN_COMMAND_DOCUMENT); @@ -401,7 +452,7 @@ export function* runPlanCommandDocument(profile: AuthorshipProfile): Operation> { diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 6eace0e1c..b73e5e90b 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -96,7 +96,7 @@ import { timebox } from "@effectionx/timebox"; import { timeout as runTimeout } from "@executablemd/runtime"; import { installRunAgentStack, resolveAgentStack } from "./agent-stack.ts"; import { planComponentDeclaration } from "./plan-component.ts"; -import { planAuthorshipCeiling } from "./authorship-profile.ts"; +import { planAgentContext } from "./authorship-profile.ts"; import { VERBOSE_REGISTRATION } from "./verbose-component.ts"; import type { AgentStack } from "./agent-stack.ts"; import { reportFailure } from "./report.ts"; @@ -954,13 +954,13 @@ function* runDocument( // Built whether or not this command settled an Agent stack. A host with none — // `xmd test` drives agents through the deterministic TestAgent stack — still // declares the Component, so a document that writes `` there resolves the - // same protected bytes and is refused at the ceiling rather than told the + // same protected bytes and is refused for want of an Agent rather than told the // component does not exist. // // A factory rather than a value, because a nested `` - // learns what ceiling it can establish only after its own configuration has + // learns what Agent context it has only after its own configuration has // been read — and a declaration built out here would have closed over the - // absence of one before that child existed. Each caller supplies the ceiling + // absence of one before that child existed. Each caller supplies the context // it settled, the authorship root it owns and the scope its host acts run in; // everything else about the Component is this entrypoint's and identical for // all of them. @@ -968,7 +968,7 @@ function* runDocument( planComponentDeclaration({ surface: "component", includes: include, - ceiling: request.ceiling, + context: request.context, ...(mode.machineSessions === undefined ? {} : { sessions: mode.machineSessions }), ...(request.authorshipRoot !== undefined ? { authorshipRoot: request.authorshipRoot } @@ -977,7 +977,7 @@ function* runDocument( : { authorshipRoot: mode.planAuthorshipRoot }), // Captured before the document exists, so the two acts that are this // host's — putting this build's adapter on disk, and opening the review - // form — run outside the ceiling the Component installs around itself. + // form — run outside the frame the Component installs around itself. host: request.host, ...(request.observeAuthorship === undefined ? {} @@ -991,7 +991,7 @@ function* runDocument( }); const plan = yield* planDeclaration({ - ceiling: planAuthorshipCeiling(mode.agent), + context: planAgentContext(mode.agent), host: yield* useScope(), // This command's own root: the browser form is how a person reviews a Plan // written by an ordinary run. diff --git a/packages/cli/src/plan-component.ts b/packages/cli/src/plan-component.ts index 381e7493d..dece5e628 100644 --- a/packages/cli/src/plan-component.ts +++ b/packages/cli/src/plan-component.ts @@ -39,7 +39,7 @@ import { createHash } from "node:crypto"; import { scoped } from "effection"; -import type { Operation, Scope } from "effection"; +import type { Operation, Result, Scope } from "effection"; import { createDurableOperation } from "@executablemd/durable-streams"; import type { Json } from "@executablemd/durable-streams"; import { @@ -62,7 +62,7 @@ import { installAuthorshipFrame, useSessionDirectory, } from "./authorship-profile.ts"; -import type { PlanAuthorshipCeiling, PlanAuthorshipObservation } from "./authorship-profile.ts"; +import type { PlanAuthorship, PlanAuthorshipObservation } from "./authorship-profile.ts"; import type { CandidateAssessment } from "./authorship-profile.ts"; import type { MachineSessionAssembly } from "./session-coordinator.ts"; import { PLAN_DOCUMENT, readPackagedDocument } from "./packaged-document.ts"; @@ -107,19 +107,19 @@ export interface PlanComponentAssembly { /** The component search path a Plan's own components resolve against. */ readonly includes: readonly string[]; /** - * Whether this host can put an Agent under the Plan ceiling, and why not. + * The Agent context this host can give a Plan, or why it can give none. * * A host that establishes none — `xmd test` at its own root, or an * unconfigured run child — still declares the Component. A document that * writes `` there resolves the same protected bytes and is refused at - * the ceiling, before any placement, rather than told the component does not + * the frame, before any placement, rather than told the component does not * exist. * * The capability is a closure the host supplied before this declaration * existed. No prop, binding, registration, middleware answer or separately * loaded copy can supply or replace one. */ - readonly ceiling: PlanAuthorshipCeiling; + readonly context: Result; /** What this host states about machine-wide agent sessions, if anything. */ readonly sessions?: MachineSessionAssembly; /** @@ -128,7 +128,7 @@ export interface PlanComponentAssembly { * Absent is the ordinary host default. A harness that owns a temporary tree * names that tree here, which is the only way anything but production selects * one — there is no flag, no environment variable and no contextual Api to - * reach, so a document cannot move where the ceiling lives. + * reach, so a document cannot move where authorship directories live. */ readonly authorshipRoot?: string; /** @@ -142,9 +142,9 @@ export interface PlanComponentAssembly { readonly session?: string; /** Whether that fixed name was one a caller asked for and can ask for again. */ readonly explicitSession?: boolean; - /** The scope the two host acts run in, captured before the ceiling exists. */ + /** The scope the two host acts run in, captured before the frame exists. */ readonly host: Scope; - /** A trusted host-only observation after the complete ceiling is installed. */ + /** A trusted host-only observation after the whole frame is installed. */ observeAuthorship?(observation: PlanAuthorshipObservation): Operation; /** Who answers the review question. */ installElicitation(): Operation; @@ -258,7 +258,7 @@ export function* planComponentDeclaration( * * Inspection and validation answer about what a document may write. They mint no * execution, so there is no claimant to build a private capability from and no - * ceiling to establish — and none of that is describable anyway: a private name + * frame to install — and none of that is describable anyway: a private name * is not syntax a document may write, so a catalog listing one would describe an * environment that does not exist. * @@ -430,12 +430,12 @@ function planAuthorship(assembly: PlanComponentAssembly): IdentityComponent { function* PlanAuthorship(props: Record): Operation { // Before a directory exists, before a provider exists, and therefore // before any session could be placed or any turn started. A host that - // cannot establish this ceiling refuses rather than writing a Plan under + // supplies no Agent context refuses rather than writing a Plan under // a weaker one, and broader authority in the calling document cannot // widen it. - const ceiling = assembly.ceiling; - if (!ceiling.established) { - throw new Error(ceiling.refusal); + const context = assembly.context; + if (!context.ok) { + throw context.error; } const session = String(props.session); @@ -454,7 +454,7 @@ function planAuthorship(assembly: PlanComponentAssembly): IdentityComponent { yield* installAuthorshipFrame({ workdir: established.value, - authorship: ceiling.authorship, + authorship: context.value, host: assembly.host, session, ...(typeof props.authoredSession === "string" @@ -467,7 +467,7 @@ function planAuthorship(assembly: PlanComponentAssembly): IdentityComponent { }); // The Component's own phases, projected inside everything installed above. - // The content scope descends from this body's, so the ceiling covers the + // The content scope descends from this body's, so the frame covers the // turns and the review and nothing outside them. yield* content(); return ""; diff --git a/packages/cli/src/plan.ts b/packages/cli/src/plan.ts index bd2e14566..1d1b15c91 100644 --- a/packages/cli/src/plan.ts +++ b/packages/cli/src/plan.ts @@ -69,7 +69,7 @@ import { cwd } from "@executablemd/runtime"; import type { AgentStack } from "./agent-stack.ts"; import { DEFAULT_AUTHORSHIP_ROOT, - planAuthorshipCeiling, + planAgentContext, runPlanCommandDocument, } from "./authorship-profile.ts"; import type { CandidateAssessment } from "./authorship-profile.ts"; @@ -230,16 +230,16 @@ export function* runPlan(command: PlanCommand, deps: PlanDependencies): Operatio // disk, and opening the review form — and both run a command, which the // ceiling the Component installs refuses to everything inside it. const host = yield* useScope(); - // The one ceiling this invocation can establish, settled before the + // The one Agent context this invocation can supply, settled before the // declaration exists so nothing the document does can reach or replace it. - const ceiling = planAuthorshipCeiling(command.stack, deps.acp); + const context = planAgentContext(command.stack, deps.acp); const declaration = yield* planComponentDeclaration({ surface: "command", // The adapter root resolves no repository component, and neither does the // Component it invokes. A Plan's own components are the caller's business, // and the final gate below is where they are resolved. includes: command.include, - ceiling, + context, authorshipRoot: root, session, explicitSession, @@ -261,7 +261,7 @@ export function* runPlan(command: PlanCommand, deps: PlanDependencies): Operatio session, explicitSession, root, - ceiling, + context, installElicitation: deps.installElicitation, declaration, assess: assessOne, diff --git a/packages/cli/src/testing-host.ts b/packages/cli/src/testing-host.ts index 6b0ed4b5c..526daacf5 100644 --- a/packages/cli/src/testing-host.ts +++ b/packages/cli/src/testing-host.ts @@ -28,7 +28,7 @@ import type { DurableEvent } from "@executablemd/durable-streams"; import { forEach } from "@effectionx/stream-helpers"; import { API, useHostFiles } from "@executablemd/runtime"; import { installWebElicitation } from "@executablemd/web"; -import { ensure, until, useScope } from "effection"; +import { ensure, Err, Ok, until, useScope } from "effection"; import type { Operation, Result, Scope } from "effection"; import { agentIdentityComponents, @@ -42,11 +42,12 @@ import { mkdir, rm } from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { NO_CEILING } from "./authorship-profile.ts"; -import type { PlanAuthorshipCeiling } from "./authorship-profile.ts"; +import { NO_AGENT_CONTEXT } from "./authorship-profile.ts"; +import type { PlanAuthorship } from "./authorship-profile.ts"; import type { PlanAuthorshipObservation } from "./authorship-profile.ts"; import type { ExecutionInstallation } from "@executablemd/core/host"; import { installChildTestAgent } from "@executablemd/test-agent"; +import type { PlanProviderAssembly } from "@executablemd/test-agent"; import type { ChildTestAgentInstallation } from "@executablemd/test-agent"; import type { AnswersChildConfiguration, @@ -62,8 +63,8 @@ import type { RepositoryInstaller } from "./run-repositories.ts"; /** What one child asks the entrypoint to build its `` declaration from. */ export interface ChildPlanDeclaration { - /** Whether this child can put an Agent under the Plan ceiling, and why not. */ - readonly ceiling: PlanAuthorshipCeiling; + /** The Agent context this child can give a Plan, or why it can give none. */ + readonly context: Result; /** The authorship root the host made for this child, when it made one. */ readonly authorshipRoot?: string; /** The scope this child's own host acts run in. */ @@ -71,7 +72,7 @@ export interface ChildPlanDeclaration { /** * Who answers this child's Plan review. * - * The ceiling installs it inside the Plan invocation, which is nearer than + * The frame installs it inside the Plan invocation, which is nearer than * anything installed around the child — so a host that installed the browser * form here would put it in front of a configured child's ``, and the * review would wait for a person no test can supply. A configured child @@ -92,11 +93,11 @@ export interface TestingHostSettings { * parent means, so the Component, its origin, its digest and its private * closure come from the entrypoint rather than from state a child could * reach. What the child supplies is the part only the child knows: the - * ceiling its own configuration established, the authorship root the host + * Agent context its own configuration settled, the authorship root the host * made for it, and its own scope. * * A declaration built once out there and shared would close over the absence - * of a ceiling before any child configuration had been read, which is exactly + * of an Agent context before any child configuration had been read, which is * why a configured child could not write a Plan. */ readonly planDeclaration: (request: ChildPlanDeclaration) => Operation; @@ -128,7 +129,7 @@ export interface TestingHostSettings { * agent has anything to say about it. */ readonly testAgentWorker: Result; - /** Trusted host evidence after a controlled Plan ceiling is fully installed. */ + /** Trusted host evidence after the whole authorship frame is installed. */ observePlanAuthorship?(observation: PlanAuthorshipObservation): Operation; } @@ -188,62 +189,44 @@ function selectConfiguration(request: HostProfileRequest): { } /** - * The Plan ceiling a configured child establishes: the controlled provider it + * The Agent context a configured child gives a Plan: the controlled provider it * was already given, installed again for the Plan invocation that asks. * * Installed *inside* `` rather than inherited from what the - * child registered around itself, so the Plan conversation gets the deny-all - * permission mode, the prompt-failure policy and the capability refusals that - * every Plan gets, whichever provider is underneath. The provider is the - * child's own partition, which is what lets a `` address the - * Plan's session by name. + * child registered around itself, so the Plan conversation runs under the same + * fixed policy every Plan runs under, whichever provider is underneath. The + * provider is the child's own partition, which is what lets a + * `` address the Plan's session by name. * * The partition, the scenarios and this closure belong to one child. A sibling * that declares the same thing provisions all of it again, and neither reaches * the other. */ -function controlledCeiling(installation: ChildTestAgentInstallation): PlanAuthorshipCeiling { +function controlledAgentContext(installation: ChildTestAgentInstallation): Result { const root = installation.components.rootProvider; const defaultAgent = installation.components.defaultAgent; if (root === undefined || defaultAgent === undefined) { // Not reachable from `installChildTestAgent`, which states both. A child - // that somehow reached here has no provider to put under the ceiling, and - // saying so is the honest answer rather than establishing one anyway. - return { established: false, refusal: NO_CEILING }; + // that somehow reached here has no provider to give a Plan, and saying so is + // the honest answer rather than supplying one anyway. + return Err(new Error(NO_AGENT_CONTEXT)); } - return { - established: true, - authorship: { - defaultAgent, - origin: "controlled-test-agent", - *installProvider(invocation): Operation { - const observe = invocation.observe; - yield* installation.installPlanProvider({ - agent: defaultAgent, - ...(invocation.authoredSession === undefined - ? {} - : { authoredSession: invocation.authoredSession }), - session: invocation.session, - workdir: invocation.workdir, - policy: invocation.policy, - ...(observe === undefined - ? {} - : { - *observeTurn(): Operation { - yield* observe({ - providerOrigin: "controlled-test-agent", - policy: invocation.policy, - workdir: invocation.workdir, - }); - }, - }), - }); - }, + return Ok({ + defaultAgent, + *installProvider(invocation): Operation { + return yield* installation.installPlanProvider({ + agent: defaultAgent, + ...(invocation.authoredSession === undefined + ? {} + : { authoredSession: invocation.authoredSession }), + session: invocation.session, + workdir: invocation.workdir, + policy: invocation.policy, + }); }, - }; + }); } -/** The permission mode every Plan conversation runs under, test or production. */ /** * A Plan authorship root this child owns and nothing else can reach. * @@ -298,8 +281,8 @@ function* runProfileChild( const installations: ExecutionInstallation[] = []; // What this child can establish for a `` written inside it. A child // nobody configured establishes nothing, which is the refusal `` has - // always given where no coding-agent ceiling exists. - let ceiling: PlanAuthorshipCeiling = { established: false, refusal: NO_CEILING }; + // always given where no Agent context exists. + let context: Result = Err(new Error(NO_AGENT_CONTEXT)); let authorshipRoot: string | undefined; if (testAgent !== undefined) { const worker = settings.testAgentWorker; @@ -318,12 +301,12 @@ function* runProfileChild( const agents = yield* installChildTestAgent(testAgent, { workerCommand: worker.value }); yield* installAgentComponents(agents.components); installations.push({ components: agentIdentityComponents() }); - // Created out here, outside the ceiling that refuses a directory to + // Created out here, outside the frame that refuses a directory to // everything inside it, and owned by this child alone: the Plan invocation // still makes and proves its own empty session directory underneath it, and // the whole tree goes when this child settles however it settles. authorshipRoot = yield* useChildAuthorshipRoot(); - ceiling = controlledCeiling(agents); + context = controlledAgentContext(agents); } // The production run profile's own vocabulary, whichever command launched the // child: `` means the run profile, and a child that @@ -333,7 +316,7 @@ function* runProfileChild( installations.push({ declarations: [ yield* settings.planDeclaration({ - ceiling, + context, ...(authorshipRoot === undefined ? {} : { authorshipRoot }), host: yield* useScope(), // Nothing, so the review is answered by whatever this child already diff --git a/packages/cli/tests/agent-adapters.test.ts b/packages/cli/tests/agent-adapters.test.ts index 592fe8655..46ce7703b 100644 --- a/packages/cli/tests/agent-adapters.test.ts +++ b/packages/cli/tests/agent-adapters.test.ts @@ -32,8 +32,8 @@ import { resolveAgentStack, } from "../src/agent-stack.ts"; import type { AgentStack } from "../src/agent-stack.ts"; -import { authorshipCeiling } from "../src/authorship-profile.ts"; -import type { AuthorshipCeilingInputs } from "../src/authorship-profile.ts"; +import { authorshipDependencies } from "../src/authorship-profile.ts"; +import type { AuthorshipProviderInputs } from "../src/authorship-profile.ts"; import { runPlan } from "../src/plan.ts"; import { scanPlanArgs } from "../src/plan-args.ts"; import { AGENT, createPlanHarness, useWorkingDirectory } from "./support/plan-harness.ts"; @@ -84,7 +84,7 @@ function installingAdapters(prepared: string[]): EmbeddedAdapters { * Component rather than to the provider this case is about, so naming it here would * be describing an arrangement the ceiling never reads. */ -function ceilingFrom(stack: AgentStack): AuthorshipCeilingInputs { +function dependenciesFrom(stack: AgentStack): AuthorshipProviderInputs { return { stack }; } @@ -129,7 +129,11 @@ describe("Tier AE — embedded adapters on the run and plan paths", () => { const root = adapterRoot(); const adapters = createEmbeddedAdapters(root); const stack = stackWith(adapters); - const ceiling = authorshipCeiling(ceilingFrom(stack), join(root, "workdir"), yield* useScope()); + const ceiling = authorshipDependencies( + dependenciesFrom(stack), + join(root, "workdir"), + yield* useScope(), + ); const registry = ceiling.agentRegistry; if (registry === undefined) { throw new Error("the plan path handed its provider no agent registry"); diff --git a/packages/cli/tests/document-suites/plan/Plan.test.md b/packages/cli/tests/document-suites/plan/Plan.test.md index c21d6a561..cb82bfbe4 100644 --- a/packages/cli/tests/document-suites/plan/Plan.test.md +++ b/packages/cli/tests/document-suites/plan/Plan.test.md @@ -73,21 +73,21 @@ it here. -## Without a scripted agent there is no ceiling to write under +## Without a scripted agent there is no Agent to write with -`` establishes an agent ceiling before it makes a directory, starts a -conversation or asks anybody anything. A child nobody configured has no agent to -put under one, so it is refused there — not given a live coding agent, and not -told the component does not exist. +`` asks its host for an Agent context before it makes a directory, starts a +conversation or asks anybody anything. A child nobody configured has none to +give, so it is refused there — not given a live coding agent, and not told the +component does not exist. - + - diff --git a/packages/cli/tests/plan-component.test.ts b/packages/cli/tests/plan-component.test.ts index d32e9a14c..d3bfcd226 100644 --- a/packages/cli/tests/plan-component.test.ts +++ b/packages/cli/tests/plan-component.test.ts @@ -237,7 +237,7 @@ describe("Tier PC — in an ordinary document", () => { }); }); - it("PC5: a host that cannot establish the ceiling refuses before placement", function* () { + it("PC5: a host whose provider gives no Agent context refuses before placement", function* () { yield* useWorkingDirectory(function* () { const root = yield* authorshipRoot(); const run = yield* runDocument({ @@ -252,7 +252,11 @@ describe("Tier PC — in an ordinary document", () => { }, }); - expect(run.failure).toContain("cannot establish the Plan authorship ceiling"); + // Named, so the person reads which provider had nothing to give rather + // than that something unspecified went wrong. + expect(run.failure).toBe( + "The not-acpx provider did not provide an Agent context for . No Plan was returned.", + ); // Before placement: no directory was made, and no turn was taken. expect(run.leftover).toEqual([]); expect(run.harness.fake.prompts).toEqual([]); diff --git a/packages/cli/tests/support/plan-harness.ts b/packages/cli/tests/support/plan-harness.ts index bf80e74dc..5090583e4 100644 --- a/packages/cli/tests/support/plan-harness.ts +++ b/packages/cli/tests/support/plan-harness.ts @@ -28,7 +28,7 @@ import { syntaxCatalog } from "../../src/syntax.ts"; import { planComponentDeclaration } from "../../src/plan-component.ts"; import type { PlanSurface } from "../../src/plan-component.ts"; import type { CandidateAssessment } from "../../src/authorship-profile.ts"; -import { planAuthorshipCeiling } from "../../src/authorship-profile.ts"; +import { planAgentContext } from "../../src/authorship-profile.ts"; import type { AgentStack } from "../../src/agent-stack.ts"; import type { DeclaredMarkdownComponent } from "@executablemd/core/host"; import type { PlanDependencies, PlanExecution } from "../../src/plan.ts"; @@ -296,10 +296,10 @@ export function* planDeclarationHarness(options: { const declaration = yield* planComponentDeclaration({ surface: options.surface, includes: options.includes ?? [], - // The production ceiling, built from the stack a case states and this + // The production Agent context, built from the stack a case states and this // harness's own ACPX seams. `stack: null` is a host that settled none, which // is what `xmd test` at its own root and an unconfigured run child are. - ceiling: planAuthorshipCeiling( + context: planAgentContext( options.stack === null ? undefined : (options.stack ?? { diff --git a/packages/cli/tests/support/run-markdown-tier.ts b/packages/cli/tests/support/run-markdown-tier.ts index 6ff5d00dc..123f847f2 100644 --- a/packages/cli/tests/support/run-markdown-tier.ts +++ b/packages/cli/tests/support/run-markdown-tier.ts @@ -86,17 +86,17 @@ export function runMarkdownTier(document: string): Operation { // ask. testAgentWorker: Ok([...cliBase(), "test-agent"]), // The run profile's own ``, so a child assembled here has the - // vocabulary a child assembled by `xmd run` has. What ceiling it can - // establish is the child's own answer, settled after that child's - // configuration has been read: a configured `` child gets the - // controlled one, and every other child gets none and is refused at the - // ceiling — which is what a host with no coding agent should say, rather - // than that the component does not exist. + // vocabulary a child assembled by `xmd run` has. What Agent context it + // has is the child's own answer, settled after that child's configuration + // has been read: a configured `` child gets the controlled one, + // and every other child gets none and is refused — which is what a host + // with no coding agent should say, rather than that the component does + // not exist. planDeclaration: (request) => planComponentDeclaration({ surface: "component", includes: ["components", "."], - ceiling: request.ceiling, + context: request.context, ...(request.authorshipRoot === undefined ? {} : { authorshipRoot: request.authorshipRoot }), diff --git a/packages/cli/tests/testing-execution-host.test.ts b/packages/cli/tests/testing-execution-host.test.ts index 832b5e0ec..9c9d5ad6d 100644 --- a/packages/cli/tests/testing-execution-host.test.ts +++ b/packages/cli/tests/testing-execution-host.test.ts @@ -602,7 +602,21 @@ describe("deterministic dependencies declared for a nested run", () => { expect(after.production.filter((entry) => !before.production.includes(entry))).toEqual([]); }); - it("installs the controlled Plan policy and removes its root after cancellation", function* () { + /** + * PMT4 — the configuration a Plan invocation actually runs under. + * + * Read from what the adapter assembled and the frame installed, after the last + * install, rather than from the policy either was handed: a report taken from + * the input would agree with the policy however the adapter assembled its + * dependencies, and could not tell an assembly that honored it from one that + * dropped it. The provider is the name that actually routes a turn rather than + * a label authored beside it. + * + * Cancellation is the ending here because it is the one that has no completion + * to hang teardown on: `halt()` returns only once the provider, the + * declaration, the session directory and the child root have all gone. + */ + it("installs the controlled Plan configuration and removes its root after cancellation", function* () { const before = yield* planRoots(); const observed = withResolvers(); const hold = withResolvers(); @@ -617,7 +631,7 @@ describe("deterministic dependencies declared for a nested run", () => { planComponentDeclaration({ surface: "component", includes: [], - ceiling: request.ceiling, + context: request.context, ...(request.authorshipRoot === undefined ? {} : { authorshipRoot: request.authorshipRoot }), @@ -666,15 +680,19 @@ describe("deterministic dependencies declared for a nested run", () => { }), ); - const ceiling = yield* observed.operation; - expect(ceiling.providerOrigin).toBe("controlled-test-agent"); - expect(ceiling.policy.systemInstruction).toBe(AUTHORSHIP_INSTRUCTIONS); - expect(ceiling.policy.permissionMode).toBe("deny-all"); - expect(ceiling.policy.promptFailures).toBe("fail"); - expect(ceiling.policy.mcpServers).toEqual([]); - expect(ceiling.policy.allowedTools).toEqual([]); - expect(ceiling.workdir.startsWith(join(tmpdir(), "xmd-child-plan-"))).toBe(true); - expect(ceiling.workdir.startsWith(DEFAULT_AUTHORSHIP_ROOT)).toBe(false); + const installed = yield* observed.operation; + // The provider that routes this Plan's turns is the controlled one, named by + // the registration rather than by anything authored beside it. + expect(installed.provider).toBe("test-agent"); + // And every term of the fixed policy, as the assembled dependencies carry it. + expect(installed.systemInstruction).toBe(AUTHORSHIP_INSTRUCTIONS); + expect(installed.permissionMode).toBe("deny-all"); + expect(installed.promptFailures).toBe("fail"); + expect(installed.mcpServers).toBe(0); + expect(installed.allowedTools).toEqual([]); + expect(installed.permissions).toBe("strict"); + expect(installed.agentCwd.startsWith(join(tmpdir(), "xmd-child-plan-"))).toBe(true); + expect(installed.agentCwd.startsWith(DEFAULT_AUTHORSHIP_ROOT)).toBe(false); // The observer runs from controlled turn routing after its scenario exists, // so the child's Prompt is in flight here. halt() waits for the provider, @@ -708,9 +726,9 @@ describe("deterministic dependencies declared for a nested run", () => { "", '', "", - "", "", "", @@ -748,7 +766,7 @@ describe("deterministic dependencies declared for a nested run", () => { expect(reported).toContain("Cannot resolve component: Plan"); // And no ceiling was established for it to be refused at, which is the // difference between "not this profile" and "this profile, no agent". - expect(reported).not.toContain("establishes no coding-agent ceiling"); + expect(reported).not.toContain("No Agent context was found."); }); it("refuses an unreadable behavior document before the child's root is imported", function* () { diff --git a/packages/test-agent/mod.ts b/packages/test-agent/mod.ts index 8089253c6..139641adf 100644 --- a/packages/test-agent/mod.ts +++ b/packages/test-agent/mod.ts @@ -56,15 +56,5 @@ export { runTestAgentWorker } from "./src/worker/run.ts"; * by the first crosses into the second. */ export { installChildTestAgent, testAgentChildDeclaration } from "./src/child-configuration.ts"; +export type { PlanProviderAssembly, PlanProviderPolicy } from "./src/child-configuration.ts"; export type { ChildTestAgentInstallation } from "./src/child-configuration.ts"; - -/** - * The provider name a controlled child registers its Agent under. - * - * Published so a trusted host can install that same provider again where it - * needs one — under the Plan authorship ceiling, which registers its own - * provider for the invocation rather than inheriting whatever surrounds it. - * Holding the name grants nothing: what it resolves to is the partition the - * host provisioned for that child. - */ -export { TEST_AGENT_PROVIDER } from "./src/provider.ts"; diff --git a/packages/test-agent/src/child-configuration.ts b/packages/test-agent/src/child-configuration.ts index 0afd6061a..6f1ffb03d 100644 --- a/packages/test-agent/src/child-configuration.ts +++ b/packages/test-agent/src/child-configuration.ts @@ -35,6 +35,7 @@ import type { Operation } from "effection"; import { hasContent, registerAgentProvider, tryContent } from "@executablemd/core"; import type { AgentComponentsOptions, Json } from "@executablemd/core"; import { createPartitionedAcpxProvider } from "@executablemd/acp"; +import type { AcpxProviderDependencies } from "@executablemd/acp"; import { installInvocationAgentProvider } from "@executablemd/core/host"; import { installControlledLauncher } from "@executablemd/runtime"; import type { @@ -180,6 +181,58 @@ function open(collect: { * outcome the `` binds — rather than an enclosing `` the child * cannot see. */ + +/** What a Plan's controlled provider is built from, assembled from the policy. */ +function planProviderDependencies( + workdir: string, + policy: PlanProviderPolicy, +): AcpxProviderDependencies { + return { + // deno-lint-ignore require-yield + *agentCwd(): Operation { + return workdir; + }, + mcpServers: [...policy.mcpServers], + permissions: "strict", + newSessionOptions: { + systemPrompt: policy.systemInstruction, + allowedTools: [...policy.allowedTools], + }, + }; +} + +/** The fixed policy a trusted host states for one Plan invocation. */ +export interface PlanProviderPolicy { + readonly systemInstruction: string; + readonly permissionMode: "deny-all"; + readonly mcpServers: readonly never[]; + readonly allowedTools: readonly never[]; +} + +/** What one adapter actually assembled, read off the values it handed over. */ +export interface PlanProviderAssembly { + readonly provider: string; + readonly agentCwd: string; + readonly systemInstruction: string | undefined; + readonly allowedTools: readonly string[] | undefined; + readonly mcpServers: number | undefined; + readonly permissions: string | undefined; + readonly permissionMode: string; +} + +/** What a Plan whose scenario nobody declared is refused with. */ +export function missingScenario(agent: string, session: string): string { + return `No was found for agent "${agent}" and session "${session}".`; +} + +/** What a Plan whose scenario was declared twice is refused with. */ +export function duplicateScenario(agent: string, session: string): string { + return ( + `More than one was declared for agent "${agent}" and ` + + `session "${session}".` + ); +} + export interface ChildTestAgentInstallation { readonly components: AgentComponentsOptions; installPlanProvider(request: { @@ -187,14 +240,8 @@ export interface ChildTestAgentInstallation { readonly authoredSession?: string; readonly session: string; readonly workdir: string; - readonly policy: { - readonly systemInstruction: string; - readonly permissionMode: "deny-all"; - readonly mcpServers: readonly never[]; - readonly allowedTools: readonly never[]; - }; - observeTurn?(): Operation; - }): Operation; + readonly policy: PlanProviderPolicy; + }): Operation; } export function* installChildTestAgent( @@ -241,34 +288,30 @@ export function* installChildTestAgent( options: { defaultAgent: configuration.defaultAgent, permissionMode }, }, }, - *installPlanProvider(request): Operation { + *installPlanProvider(request): Operation { const planDeclarations = new Map(declarations); if (request.authoredSession !== undefined) { const declared = declarations.get(mappingKey(request.agent, request.authoredSession)); if (declared === undefined) { - throw new Error( - `no maps ${describeMapping(request.agent, request.authoredSession)}`, - ); + throw new Error(missingScenario(request.agent, request.authoredSession)); } const key = mappingKey(request.agent, request.session); const existing = planDeclarations.get(key); if (existing !== undefined && existing !== declared) { - throw new Error( - `duplicate mappings for ${describeMapping(request.agent, request.session)}`, - ); + throw new Error(duplicateScenario(request.agent, request.session)); } planDeclarations.set(key, declared); } + // Assembled once, here, and then both used and described. The provider is + // built by spreading this exact object, so a report taken from it is a + // report of what runs rather than of what was asked for. + const dependencies = planProviderDependencies(request.workdir, request.policy); const plan = yield* provisionPartition({ defaultAgent: configuration.defaultAgent, controller, declarations: planDeclarations, workerCommand: [...options.workerCommand], - planCeiling: { - workdir: request.workdir, - policy: request.policy, - ...(request.observeTurn === undefined ? {} : { observeTurn: request.observeTurn }), - }, + planCeiling: { dependencies }, }); // deno-lint-ignore require-yield const planFactory = createPartitionedAcpxProvider(function* () { @@ -279,6 +322,19 @@ export function* installChildTestAgent( defaultAgent: request.agent, permissionMode: request.policy.permissionMode, }); + return { + provider: TEST_AGENT_PROVIDER, + agentCwd: dependencies.agentCwd === undefined ? "" : yield* dependencies.agentCwd(), + systemInstruction: + typeof dependencies.newSessionOptions?.systemPrompt === "string" + ? dependencies.newSessionOptions.systemPrompt + : undefined, + allowedTools: dependencies.newSessionOptions?.allowedTools, + mcpServers: dependencies.mcpServers?.length, + permissions: + typeof dependencies.permissions === "string" ? dependencies.permissions : undefined, + permissionMode: request.policy.permissionMode, + }; }, }; } diff --git a/packages/test-agent/src/components.ts b/packages/test-agent/src/components.ts index 29af4099e..b4d99054a 100644 --- a/packages/test-agent/src/components.ts +++ b/packages/test-agent/src/components.ts @@ -48,6 +48,8 @@ import { NativeLaunchObserver, useTestAgentController } from "./controller.ts"; import type { ScenarioHandle, TestAgentControllerInternals } from "./controller.ts"; import { createControlledExecutableObserver } from "./executable-observer.ts"; import { createDeterministicSessionCoordinator } from "./session-coordinator.ts"; +import type { AcpxProviderDependencies } from "@executablemd/acp"; +import { duplicateScenario, missingScenario } from "./child-configuration.ts"; import { TEST_AGENT_CLIENT_NATIVE, TEST_AGENT_PROVIDER, useTestAgentProvider } from "./provider.ts"; import type { SessionRouting } from "./provider.ts"; @@ -165,16 +167,7 @@ export function* provisionPartition(options: { declarations: ReadonlyMap; /** How to re-invoke this host as the agent worker. */ workerCommand: string[]; - planCeiling?: { - readonly workdir: string; - readonly policy: { - readonly systemInstruction: string; - readonly permissionMode: "deny-all"; - readonly mcpServers: readonly never[]; - readonly allowedTools: readonly never[]; - }; - observeTurn?(): Operation; - }; + planCeiling?: { readonly dependencies: AcpxProviderDependencies }; }): Operation { const { defaultAgent, controller, declarations, workerCommand, planCeiling } = options; const scenarios = new Map(); @@ -189,12 +182,10 @@ export function* provisionPartition(options: { ): Operation { const declared = declarations.get(declarationKey(agentName, sessionName ?? "")); if (!declared) { - throw new Error(`no maps ${describeMapping(agentName, sessionName)}`); + throw new Error(missingScenario(agentName, sessionName ?? "")); } if (declared.duplicate) { - throw new Error( - `duplicate mappings for ${describeMapping(agentName, sessionName)}`, - ); + throw new Error(duplicateScenario(agentName, sessionName ?? "")); } const key = scenarioKey(agentName, sessionName, dir); const existing = scenarios.get(key); @@ -248,9 +239,6 @@ export function* provisionPartition(options: { return { route: pinned.scenario.route, resolved: () => {} }; } const scenario = yield* provision(context.agentName, context.session, context.cwd); - if (planCeiling?.observeTurn !== undefined) { - yield* planCeiling.observeTurn(); - } return { route: scenario.route, resolved(value) { diff --git a/packages/test-agent/src/provider.ts b/packages/test-agent/src/provider.ts index 8695abbe9..ce4010018 100644 --- a/packages/test-agent/src/provider.ts +++ b/packages/test-agent/src/provider.ts @@ -73,16 +73,13 @@ export interface TestAgentProviderOptions { /** Which build this partition observes. Its own controlled one. */ executableObserver?: ExecutableObserver; dependencies?: AcpxProviderDependencies; - /** The fixed, narrower ceiling used only by a trusted child Plan host. */ - planCeiling?: { - readonly workdir: string; - readonly policy: { - readonly systemInstruction: string; - readonly permissionMode: "deny-all"; - readonly mcpServers: readonly never[]; - readonly allowedTools: readonly never[]; - }; - }; + /** + * The narrower configuration a trusted child Plan host assembled. + * + * Assembled by the caller and spread whole, so the caller holds the exact + * object this provider was built from and can report it. + */ + planCeiling?: { readonly dependencies: AcpxProviderDependencies }; } /** @@ -205,20 +202,10 @@ export function* useTestAgentProvider(options: TestAgentProviderOptions): Operat ...(options.dependencies?.createRuntime ? { createRuntime: options.dependencies.createRuntime } : {}), - ...(planCeiling === undefined - ? {} - : { - // deno-lint-ignore require-yield - *agentCwd(): Operation { - return planCeiling.workdir; - }, - mcpServers: [...planCeiling.policy.mcpServers], - permissions: "strict", - newSessionOptions: { - systemPrompt: planCeiling.policy.systemInstruction, - allowedTools: [...planCeiling.policy.allowedTools], - }, - }), + // Spread whole, so what the provider is built from is the one object the + // caller assembled and can therefore report. Restating the policy here + // would give a report that agrees with the policy however this assembled. + ...(planCeiling === undefined ? {} : planCeiling.dependencies), }, ); } diff --git a/packages/test-agent/tests/components.test.ts b/packages/test-agent/tests/components.test.ts index 5c6df2e68..f4d142255 100644 --- a/packages/test-agent/tests/components.test.ts +++ b/packages/test-agent/tests/components.test.ts @@ -176,8 +176,14 @@ describe("Tier TV — TestAgent components", { sanitizeOps: false, sanitizeResou ].join("\n"), }); expect(run.results.map((entry) => entry.status)).toEqual(["fail", "fail"]); - expect(run.output).toContain("no maps agent"); - expect(run.output).toContain("duplicate mappings"); + // Both refusals name the agent and the session they are about, so a reader + // of a failing suite knows which mapping to write or remove. + expect(run.output).toContain( + 'No was found for agent "test" and session "unmapped".', + ); + expect(run.output).toContain( + 'More than one was declared for agent "test" and session "dup".', + ); }); it("TV3: each gets fresh state; a mismatch fails only its owning test", function* () { diff --git a/specs/testing-spec.md b/specs/testing-spec.md index f259ac226..15b117549 100644 --- a/specs/testing-spec.md +++ b/specs/testing-spec.md @@ -412,34 +412,33 @@ declarations under the same ``; a named session cannot continue across the boundary. The behavior-document path resolves from the outer test document under the TestAgent package's existing containment rule. -#### The Plan authorship ceiling - -A configured child can also establish the ceiling `` writes under, which -is the one thing about it a document cannot arrange for itself. - -Three surfaces, and they stay distinct. A **production run** establishes the -production ACPX ceiling from the Agent stack the command settled. A **configured -`` child** establishes the deterministic test ceiling from the -controlled provider its own declaration produced. A **direct `xmd test` root** -and an **unconfigured child** establish none: the root does not resolve `` -at all, because the run profile's vocabulary belongs to a run, and an -unconfigured child resolves the same protected bytes every run resolves and is -refused at the ceiling before a directory, a provider, an Agent turn or a review -exists. - -Which provider is underneath is the only thing that differs. The Plan system -instruction, the deny-all permission mode, the empty MCP-server and native-tool -sets, the prompt-failure policy and the document-capability refusals are the -same for both established ceilings, installed in one place rather than restated -by each provider, so a second implementation cannot bring a weaker ceiling with -it. The controlled provider is installed *inside* the Plan invocation rather -than inherited from what the child registered around itself. +#### The Agent context a Plan is written with + +A configured child can also give `` its Agent context, which is the one +thing about it a document cannot arrange for itself. + +Three surfaces, and they stay distinct. A **production run** supplies the +production ACPX context from the Agent stack the command settled. A **configured +`` child** supplies the deterministic one from the controlled provider +its own declaration produced. A **direct `xmd test` root** and an **unconfigured +child** supply none: the root does not resolve `` at all, because the run +profile's vocabulary belongs to a run, and an unconfigured child resolves the +same protected bytes every run resolves and is refused before a directory, a +provider, an Agent turn or a review exists. + +Availability is the only thing that differs. What a Plan then runs under is the +authorship frame's fixed policy: the Plan system instruction, the deny-all +permission mode, the empty MCP-server and native-tool sets, the prompt-failure +policy and the document-capability refusals, installed in one place rather than +restated by each provider, so a second implementation cannot bring a weaker one. +The controlled provider is installed *inside* the Plan invocation rather than +inherited from what the child registered around itself. A configured child is given a Plan authorship root of its own, created outside -that ceiling and removed when the child settles — the Plan sessions underneath -it included, a named one among them, which production keeps and a test may not. -The Plan invocation still creates and proves its own empty session directory -under that root. +that frame and removed when the child settles — the Plan sessions underneath it +included, a named one among them, which production keeps and a test may not. The +Plan invocation still creates and proves its own empty session directory under +that root. Only the canonical declaration this harness recognizes grants any of it. A repository component named `TestAgent` ends the scan and configures nothing, so From f72f3db97fca50153a2db6ede68b016a1c21d77f Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Wed, 2 Sep 2026 15:45:47 -0400 Subject: [PATCH 9/9] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Observe=20the=20Plan?= =?UTF-8?q?=20provider=20installation=20itself,=20and=20drop=20the=20last?= =?UTF-8?q?=20ceiling=20words=20(#728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The observation was built beside the installation rather than being it. The controlled adapter registered its provider and invocation settings and then constructed a separate record from the requested policy; the frame installed prompt-failure middleware and then reported the literal "fail". Either could stay green while the effective installation moved. There is now one assembly — the provider identity, the dependencies, and the invocation options — and it is what registers the provider, what installs the invocation, and what a trusted host receives. The dependencies it carries are the partition's own account of what its provider holds, so a provider built from anything but the assembled Plan dependencies reports as what it actually is. Nothing is reconstructed afterward. The prompt-failure rule is proven by a turn instead of a value: a scripted turn emits part of a candidate and then fails, and authorship ends before that partial can be checked or reviewed. A value saying the policy is installed would say so however the middleware behaved. Four mutations confirm the evidence — disconnecting the provider from its assembled dependencies, changing the effective permission mode, changing the registered identity, and bypassing the prompt-failure handler — each failing its case, all restored. The cancellation case keeps its `halt()` and its root-cleanup assertion, and its comment no longer claims a Prompt is in flight: the observer runs once the frame is installed and before the Component's content starts. Plan-specific "ceiling" is gone from the words this story introduced — `planConfiguration` for the assembled dependencies, Agent context for whether a host supplies one, authorship policy or frame for the fixed restrictions. Established uses elsewhere are untouched. Claude-Session: https://claude.ai/code/session_01TNJwcFmnt3kYSn9gGsx9u7 --- packages/cli/src/authorship-profile.ts | 114 ++++++----------- .../cli/tests/testing-execution-host.test.ts | 106 ++++++++++++---- .../test-agent/src/child-configuration.ts | 65 +++++----- packages/test-agent/src/components.ts | 19 ++- packages/test-agent/src/provider.ts | 115 ++++++++++-------- packages/test-agent/tests/provider.test.ts | 4 +- specs/executable-mdx-spec.md | 32 ++--- 7 files changed, 253 insertions(+), 202 deletions(-) diff --git a/packages/cli/src/authorship-profile.ts b/packages/cli/src/authorship-profile.ts index a14acc0b7..c050f5853 100644 --- a/packages/cli/src/authorship-profile.ts +++ b/packages/cli/src/authorship-profile.ts @@ -42,7 +42,7 @@ import { registerAgentProvider, retainedSource, } from "@executablemd/core"; -import type { Json } from "@executablemd/core"; +import type { AgentProviderOptions, Json } from "@executablemd/core"; import type { DeclaredMarkdownComponent } from "@executablemd/core/host"; import { executeInstalled, installInvocationAgentProvider } from "@executablemd/core/host"; import { createAcpxProvider } from "@executablemd/acp"; @@ -182,35 +182,24 @@ export interface PlanAuthorshipPolicy { } /** - * What one adapter actually assembled, read off the values it handed the - * provider rather than off the policy it was given. + * One installed provider, as the objects it was installed with. * - * The difference is the whole point. A report built from the policy input would - * say the same thing however the adapter assembled its dependencies, so it - * could not tell an assembly that honored the policy from one that dropped it. + * Not a description built beside the installation but the installation itself: + * the same value registers the provider, installs the invocation options, and + * is handed to a trusted host as its observation. There is nothing for a report + * to disagree with, because there is no second report. */ export interface PlanProviderAssembly { - /** The provider name this adapter registered, which is what routes a turn. */ + /** The identity registered and selected for this invocation. */ readonly provider: string; - /** The working directory the assembled dependencies actually carry. */ - readonly agentCwd: string; - /** The system instruction a new session is actually opened with. */ - readonly systemInstruction: string | undefined; - /** The native tools a fresh session is actually allowed. */ - readonly allowedTools: readonly string[] | undefined; - /** The MCP servers actually configured. */ - readonly mcpServers: number | undefined; - /** The native permission answer actually configured. */ - readonly permissions: string | undefined; - /** The permission mode actually installed for the invocation. */ - readonly permissionMode: string; + /** The dependencies the provider was built from, as the provider holds them. */ + readonly dependencies: AcpxProviderDependencies; + /** The options the invocation provider was installed with. */ + readonly invocation: AgentProviderOptions; } -/** Everything a Plan's configuration turned out to be, once it is installed. */ -export interface PlanAuthorshipObservation extends PlanProviderAssembly { - /** Whether the frame installed the policy that ends authorship on a failed turn. */ - readonly promptFailures: PlanAuthorshipPolicy["promptFailures"]; -} +/** What a Plan's configuration turned out to be, once it is installed. */ +export type PlanAuthorshipObservation = PlanProviderAssembly; /** What a host that supplies no Agent at all refuses a Plan with. */ export const NO_AGENT_CONTEXT = "No Agent context was found. No Plan was returned."; @@ -245,48 +234,29 @@ export function planAgentContext( return Ok({ defaultAgent: stack.defaultAgent, *installProvider(invocation: PlanAuthorshipInvocation): Operation { - // Assembled once and then read, so what is reported is what the provider - // was built from rather than what this adapter was asked for. - const dependencies = authorshipDependencies( - { stack, ...(acp === undefined ? {} : { acp }) }, - invocation.workdir, - invocation.host, - invocation.policy, - ); - yield* registerAgentProvider("acpx", createAcpxProvider(dependencies)); - yield* installInvocationAgentProvider("acpx", { - defaultAgent: stack.defaultAgent, - permissionMode: invocation.policy.permissionMode, - }); - return yield* describeAssembly("acpx", dependencies, invocation.policy.permissionMode); + // One assembly, used for every installation and handed back as the + // observation. Nothing is reconstructed afterward, so a report cannot + // describe an arrangement other than the one installed. + const installed: PlanProviderAssembly = { + provider: "acpx", + dependencies: authorshipDependencies( + { stack, ...(acp === undefined ? {} : { acp }) }, + invocation.workdir, + invocation.host, + invocation.policy, + ), + invocation: { + defaultAgent: stack.defaultAgent, + permissionMode: invocation.policy.permissionMode, + }, + }; + yield* registerAgentProvider(installed.provider, createAcpxProvider(installed.dependencies)); + yield* installInvocationAgentProvider(installed.provider, installed.invocation); + return installed; }, }); } -/** - * One assembled provider, as the values it was actually built from. - * - * `agentCwd` is an operation on the dependencies rather than a field, so it is - * asked the way the provider asks it. - */ -export function* describeAssembly( - provider: string, - dependencies: AcpxProviderDependencies, - permissionMode: string, -): Operation { - const session = dependencies.newSessionOptions; - return { - provider, - agentCwd: dependencies.agentCwd === undefined ? "" : yield* dependencies.agentCwd(), - systemInstruction: typeof session?.systemPrompt === "string" ? session.systemPrompt : undefined, - allowedTools: session?.allowedTools, - mcpServers: dependencies.mcpServers?.length, - permissions: - typeof dependencies.permissions === "string" ? dependencies.permissions : undefined, - permissionMode, - }; -} - /** What claiming one conversation's directory needs, and nothing more. */ export interface AuthorshipPlacement { /** Where this host keeps its authorship session directories. */ @@ -348,18 +318,19 @@ export function* installAuthorshipFrame(frame: AuthorshipFrame): Operation ...(frame.authoredSession === undefined ? {} : { authoredSession: frame.authoredSession }), policy: PLAN_AUTHORSHIP_POLICY, }); - const promptFailures = yield* installPlanPromptFailurePolicy(); + yield* installPlanPromptFailurePolicy(); yield* refuseDocumentCapabilities(); - // After everything, and from what everything turned out to be. A report built - // before the last install would describe an arrangement that does not exist - // yet, which is the one thing a trusted observer must not be given. + // After everything, and it is the installation rather than an account of one. + // Whether the prompt-failure policy is installed is not reported here at all: + // a value saying so would agree with itself however the middleware behaved, + // so a turn that fails partway proves it instead. if (frame.observe !== undefined) { - yield* frame.observe({ ...assembly, promptFailures }); + yield* frame.observe(assembly); } } /** - * End authorship on a failed turn, and say that it was installed. + * End authorship on a failed turn. * * A candidate comes from a turn's complete successful close value or from * nowhere. `` ordinarily renders whatever a failed turn managed to emit @@ -367,16 +338,11 @@ export function* installAuthorshipFrame(frame: AuthorshipFrame): Operation * person half a program; the host decides otherwise here, so a failed, cancelled * or protocol-invalid turn ends authorship before anything is presented. The * Component cannot opt out of it. - * - * The answer is returned rather than assumed by the caller, so removing this - * installation removes the fact an observer reports rather than leaving one that - * describes an install that no longer happens. */ -function* installPlanPromptFailurePolicy(): Operation { +function* installPlanPromptFailurePolicy(): Operation { yield* installPromptFailurePolicy(function* () { return PLAN_AUTHORSHIP_POLICY.promptFailures === "fail"; }); - return PLAN_AUTHORSHIP_POLICY.promptFailures; } /** diff --git a/packages/cli/tests/testing-execution-host.test.ts b/packages/cli/tests/testing-execution-host.test.ts index 9c9d5ad6d..e56f8e3f5 100644 --- a/packages/cli/tests/testing-execution-host.test.ts +++ b/packages/cli/tests/testing-execution-host.test.ts @@ -91,6 +91,24 @@ const PLAN_BEHAVIOR = doc( "{program}", ); +/** + * A turn that produces part of a candidate and then fails. + * + * The marker is emitted before the failure, so it is exactly what a `` + * that rendered whatever a failed turn managed to emit would hand onward. Under + * the authorship policy no such partial reaches the draft check or the review, + * and the run ends instead. + */ +const PARTIAL_THEN_FAILS = doc( + '', + "", + "# Half a program", + "", + "partialcandidatemarker", + "", + '', +); + /** An ordinary document that writes a Plan and prints what it bound. */ const PLAN_CHILD = doc( "# A document that writes a Plan", @@ -680,24 +698,32 @@ describe("deterministic dependencies declared for a nested run", () => { }), ); + // What comes back is the installation, not an account of it: the same value + // registered the provider and installed the invocation options. const installed = yield* observed.operation; - // The provider that routes this Plan's turns is the controlled one, named by - // the registration rather than by anything authored beside it. + + // The identity that was registered and selected for this invocation. expect(installed.provider).toBe("test-agent"); - // And every term of the fixed policy, as the assembled dependencies carry it. - expect(installed.systemInstruction).toBe(AUTHORSHIP_INSTRUCTIONS); - expect(installed.permissionMode).toBe("deny-all"); - expect(installed.promptFailures).toBe("fail"); - expect(installed.mcpServers).toBe(0); - expect(installed.allowedTools).toEqual([]); - expect(installed.permissions).toBe("strict"); - expect(installed.agentCwd.startsWith(join(tmpdir(), "xmd-child-plan-"))).toBe(true); - expect(installed.agentCwd.startsWith(DEFAULT_AUTHORSHIP_ROOT)).toBe(false); - - // The observer runs from controlled turn routing after its scenario exists, - // so the child's Prompt is in flight here. halt() waits for the provider, - // declaration, session directory and child root to finish teardown before - // it returns. + expect(installed.invocation.permissionMode).toBe("deny-all"); + + // And the dependencies the provider actually holds. Read through the + // provider's own accessors where it has them, so a provider disconnected + // from these dependencies reports what it really has. + const dependencies = installed.dependencies; + expect(dependencies.newSessionOptions?.systemPrompt).toBe(AUTHORSHIP_INSTRUCTIONS); + expect(dependencies.newSessionOptions?.allowedTools).toEqual([]); + expect(dependencies.mcpServers).toEqual([]); + expect(dependencies.permissions).toBe("strict"); + const agentCwd = dependencies.agentCwd === undefined ? "" : yield* dependencies.agentCwd(); + expect(agentCwd.startsWith(join(tmpdir(), "xmd-child-plan-"))).toBe(true); + expect(agentCwd.startsWith(DEFAULT_AUTHORSHIP_ROOT)).toBe(false); + + // The observer runs once the authorship frame is installed and before the + // Component's content starts, so no Prompt has been sent yet — what is in + // flight is the invocation holding the provider, the session directory and + // the child root. halt() waits for all of them to finish teardown before it + // returns, which is what makes this a structured-cancellation proof rather + // than a check that something was deleted eventually. yield* running.halt(); const after = yield* planRoots(); expect(after.children.filter((entry) => !before.children.includes(entry))).toEqual([]); @@ -705,22 +731,58 @@ describe("deterministic dependencies declared for a nested run", () => { }); /** - * PMT6 — only the canonical declaration configures a Plan ceiling. + * PMT4 — the prompt-failure rule, proven by a turn rather than by a value. + * + * `` ordinarily renders whatever a failed turn managed to emit and + * carries on. Authorship installs the opposite, and this is the difference + * being observed: a turn that emits part of a candidate and then fails must + * end authorship before that partial can be checked or reviewed. A report + * saying the policy is installed would say so however the middleware behaved. + */ + it("stops authorship when a turn fails after emitting part of a candidate", function* () { + const project = yield* useProject({ + "agents/plan.md": PARTIAL_THEN_FAILS, + "writes-a-plan.md": PLAN_CHILD, + "README.md": doc( + '', + '', + ...PLAN_DECLARATION, + "", + '', + "", + "", + // The partial never became a draft, so it reached neither the check nor + // the review, and no approved source came back. + '', + '', + "", + "", + ), + }); + const result = yield* runCli(["test", "README.md"], { cwd: project, ...WORKER }).join(); + expect(result.stdout + result.stderr).not.toContain("❌"); + expect(result.code).toBe(0); + // And the partial reached the person running the suite nowhere either. + expect(result.stdout + result.stderr).not.toContain("partialcandidatemarker"); + }); + + /** + * PMT6 — only the canonical declaration supplies a Plan Agent context. * * The repository file ends the scan, so what the `` prefix holds is * an ordinary component invocation rather than a declaration this host - * recognizes. A child whose `` found a ceiling anyway would mean the + * recognizes. A child whose `` found an Agent context anyway would mean the * capability came from the name rather than from the definition ordinary * resolution selected. */ - it("configures no Plan authorship ceiling from a repository TestAgent", function* () { + it("supplies no Plan Agent context from a repository TestAgent", function* () { const project = yield* useProject({ "agents/review.md": BEHAVIOR, // Chosen ahead of the package's, so this is ordinary assertion content. "components/TestAgent.md": doc("a repository component"), "writes-a-plan.md": doc('Write a program.'), "README.md": doc( - '', + '', '', '', "", @@ -749,7 +811,7 @@ describe("deterministic dependencies declared for a nested run", () => { * * Either refusal would satisfy "a test root cannot write a Plan". This case * pins which one is delivered, so a later change that quietly gave the test - * root the declaration — and therefore a ceiling to be refused at — is a + * root the declaration — and therefore an Agent context to be refused for — is a * change somebody has to make deliberately. */ it("gives a direct test root no Plan authorship authority", function* () { @@ -764,7 +826,7 @@ describe("deterministic dependencies declared for a nested run", () => { expect(result.code).toBe(1); const reported = result.stdout + result.stderr; expect(reported).toContain("Cannot resolve component: Plan"); - // And no ceiling was established for it to be refused at, which is the + // And no Agent context was supplied for it to be refused for, which is the // difference between "not this profile" and "this profile, no agent". expect(reported).not.toContain("No Agent context was found."); }); diff --git a/packages/test-agent/src/child-configuration.ts b/packages/test-agent/src/child-configuration.ts index 6f1ffb03d..321e56e5f 100644 --- a/packages/test-agent/src/child-configuration.ts +++ b/packages/test-agent/src/child-configuration.ts @@ -33,7 +33,7 @@ import type { Operation } from "effection"; import { hasContent, registerAgentProvider, tryContent } from "@executablemd/core"; -import type { AgentComponentsOptions, Json } from "@executablemd/core"; +import type { AgentComponentsOptions, AgentProviderOptions, Json } from "@executablemd/core"; import { createPartitionedAcpxProvider } from "@executablemd/acp"; import type { AcpxProviderDependencies } from "@executablemd/acp"; import { installInvocationAgentProvider } from "@executablemd/core/host"; @@ -209,15 +209,21 @@ export interface PlanProviderPolicy { readonly allowedTools: readonly never[]; } -/** What one adapter actually assembled, read off the values it handed over. */ +/** + * One installed provider, as the objects it was installed with. + * + * Not a description built beside the installation but the installation itself: + * the same value registers the provider, installs the invocation options, and + * is handed to a trusted host as the observation. There is nothing for a report + * to disagree with, because there is no second report. + */ export interface PlanProviderAssembly { + /** The identity registered and selected for this invocation. */ readonly provider: string; - readonly agentCwd: string; - readonly systemInstruction: string | undefined; - readonly allowedTools: readonly string[] | undefined; - readonly mcpServers: number | undefined; - readonly permissions: string | undefined; - readonly permissionMode: string; + /** The dependencies the provider was built from, as the provider holds them. */ + readonly dependencies: AcpxProviderDependencies; + /** The options the invocation provider was installed with. */ + readonly invocation: AgentProviderOptions; } /** What a Plan whose scenario nobody declared is refused with. */ @@ -302,39 +308,36 @@ export function* installChildTestAgent( } planDeclarations.set(key, declared); } - // Assembled once, here, and then both used and described. The provider is - // built by spreading this exact object, so a report taken from it is a - // report of what runs rather than of what was asked for. - const dependencies = planProviderDependencies(request.workdir, request.policy); + // One assembly, used for every installation and handed back as the + // observation. Nothing is reconstructed afterward, so a report cannot + // describe an arrangement other than the one installed. const plan = yield* provisionPartition({ defaultAgent: configuration.defaultAgent, controller, declarations: planDeclarations, workerCommand: [...options.workerCommand], - planCeiling: { dependencies }, + planConfiguration: { + dependencies: planProviderDependencies(request.workdir, request.policy), + }, }); + const installed: PlanProviderAssembly = { + provider: TEST_AGENT_PROVIDER, + // The partition's own account of what its provider holds, so a provider + // built from anything but the assembled Plan dependencies reports as + // what it actually is rather than as what was asked for. + dependencies: plan.dependencies, + invocation: { + defaultAgent: request.agent, + permissionMode: request.policy.permissionMode, + }, + }; // deno-lint-ignore require-yield const planFactory = createPartitionedAcpxProvider(function* () { return plan.provider; }); - yield* registerAgentProvider(TEST_AGENT_PROVIDER, planFactory); - yield* installInvocationAgentProvider(TEST_AGENT_PROVIDER, { - defaultAgent: request.agent, - permissionMode: request.policy.permissionMode, - }); - return { - provider: TEST_AGENT_PROVIDER, - agentCwd: dependencies.agentCwd === undefined ? "" : yield* dependencies.agentCwd(), - systemInstruction: - typeof dependencies.newSessionOptions?.systemPrompt === "string" - ? dependencies.newSessionOptions.systemPrompt - : undefined, - allowedTools: dependencies.newSessionOptions?.allowedTools, - mcpServers: dependencies.mcpServers?.length, - permissions: - typeof dependencies.permissions === "string" ? dependencies.permissions : undefined, - permissionMode: request.policy.permissionMode, - }; + yield* registerAgentProvider(installed.provider, planFactory); + yield* installInvocationAgentProvider(installed.provider, installed.invocation); + return installed; }, }; } diff --git a/packages/test-agent/src/components.ts b/packages/test-agent/src/components.ts index b4d99054a..5340d3b11 100644 --- a/packages/test-agent/src/components.ts +++ b/packages/test-agent/src/components.ts @@ -73,6 +73,8 @@ interface PinnedSession { export interface BoundaryState { provider: AcpxProvider; + /** The exact dependencies that provider was built from. */ + dependencies: AcpxProviderDependencies; /** Owns every scenario resource provisioned for this boundary. */ boundaryScope: Scope; scenarios: Map; @@ -167,9 +169,9 @@ export function* provisionPartition(options: { declarations: ReadonlyMap; /** How to re-invoke this host as the agent worker. */ workerCommand: string[]; - planCeiling?: { readonly dependencies: AcpxProviderDependencies }; + planConfiguration?: { readonly dependencies: AcpxProviderDependencies }; }): Operation { - const { defaultAgent, controller, declarations, workerCommand, planCeiling } = options; + const { defaultAgent, controller, declarations, workerCommand, planConfiguration } = options; const scenarios = new Map(); const pending = new Map>(); const bySessionKey = new Map(); @@ -250,7 +252,7 @@ export function* provisionPartition(options: { }; } - const provider = yield* useTestAgentProvider({ + const partition = yield* useTestAgentProvider({ defaultAgent, // Two agents, because the two construction routes are two contracts: // the default one's worker asserts its own identity, and the second @@ -267,9 +269,16 @@ export function* provisionPartition(options: { // bound to a build the next does not share. executableObserver: createControlledExecutableObserver().observer, routeStore: createMemorySessionRouteStore(), - ...(planCeiling === undefined ? {} : { planCeiling }), + ...(planConfiguration === undefined ? {} : { planConfiguration }), }); - return { provider, boundaryScope, scenarios, pending, bySessionKey }; + return { + provider: partition.provider, + dependencies: partition.dependencies, + boundaryScope, + scenarios, + pending, + bySessionKey, + }; } /** diff --git a/packages/test-agent/src/provider.ts b/packages/test-agent/src/provider.ts index ce4010018..ae5cb1eb8 100644 --- a/packages/test-agent/src/provider.ts +++ b/packages/test-agent/src/provider.ts @@ -76,10 +76,11 @@ export interface TestAgentProviderOptions { /** * The narrower configuration a trusted child Plan host assembled. * - * Assembled by the caller and spread whole, so the caller holds the exact - * object this provider was built from and can report it. + * Spread whole into what this provider is built from, and handed back as part + * of the effective dependencies, so what a trusted host reports is what the + * provider actually holds rather than what it asked for. */ - planCeiling?: { readonly dependencies: AcpxProviderDependencies }; + planConfiguration?: { readonly dependencies: AcpxProviderDependencies }; } /** @@ -141,10 +142,12 @@ export const TEST_AGENT_CLIENT_NATIVE_ADAPTER: NativeAdapter = { resume: (nativeSessionId) => ["xmd-test-agent-ui", "--resume", nativeSessionId], }; -export function* useTestAgentProvider(options: TestAgentProviderOptions): Operation { +export function* useTestAgentProvider( + options: TestAgentProviderOptions, +): Operation { let pendingRoute: string | undefined; const routeSlot = yield* useRouteSlot(); - const planCeiling = options.planCeiling; + const planConfiguration = options.planConfiguration; // ACPX tokenizes the command on whitespace with quote support, so // command segments containing spaces (e.g. a binary path) are quoted. @@ -159,53 +162,61 @@ export function* useTestAgentProvider(options: TestAgentProviderOptions): Operat }, }; - return yield* useAcpxProvider( + // Assembled once and then both used and returned. The provider is created + // from this exact object, so a trusted host reporting it reports what runs — + // a provider built from anything else would be reported by nothing. + const dependencies: AcpxProviderDependencies = { + sessionStore: createMemorySessionStore(), + agentRegistry: registry, + advertiseNativeLaunch: options.agents, + // Both gates, stated separately, because they are separate capabilities. + // This partition proves them the same way — deterministically — so it + // advertises the same names for each. + advertiseClientNativeAttachment: options.agents, + // Every agent this partition serves gets the provider-returned adapter, + // except the one name reserved for the client-allocated contract. + nativeAdapters: Object.fromEntries( + options.agents.map((name) => [ + name, + name === TEST_AGENT_CLIENT_NATIVE + ? TEST_AGENT_CLIENT_NATIVE_ADAPTER + : TEST_AGENT_NATIVE_ADAPTER, + ]), + ), + // withSlot bounds the route mutex to the hook's op without a scope + // of its own — op's acquisitions (turn resources) belong to the + // provider's subscriber scope and outlive the critical section. + withSessionRoute: (context, op) => + routeSlot.withSlot(function* () { + const routing = yield* options.routeFor(context); + pendingRoute = routing.route; + try { + const value = yield* op(); + // Reported before the slot advances, so the next operation to pin a + // route already sees what this one established. + routing.resolved(value); + return value; + } finally { + pendingRoute = undefined; + } + }), + ...(options.coordinator ? { coordinator: options.coordinator } : {}), + ...(options.routeStore ? { routeStore: options.routeStore } : {}), + ...(options.executableObserver ? { executableObserver: options.executableObserver } : {}), + ...(options.dependencies?.createRuntime + ? { createRuntime: options.dependencies.createRuntime } + : {}), + ...(planConfiguration === undefined ? {} : planConfiguration.dependencies), + }; + const provider = yield* useAcpxProvider( { defaultAgent: options.defaultAgent, permissionMode: "deny-all" }, - { - sessionStore: createMemorySessionStore(), - agentRegistry: registry, - advertiseNativeLaunch: options.agents, - // Both gates, stated separately, because they are separate capabilities. - // This partition proves them the same way — deterministically — so it - // advertises the same names for each. - advertiseClientNativeAttachment: options.agents, - // Every agent this partition serves gets the provider-returned adapter, - // except the one name reserved for the client-allocated contract. - nativeAdapters: Object.fromEntries( - options.agents.map((name) => [ - name, - name === TEST_AGENT_CLIENT_NATIVE - ? TEST_AGENT_CLIENT_NATIVE_ADAPTER - : TEST_AGENT_NATIVE_ADAPTER, - ]), - ), - // withSlot bounds the route mutex to the hook's op without a scope - // of its own — op's acquisitions (turn resources) belong to the - // provider's subscriber scope and outlive the critical section. - withSessionRoute: (context, op) => - routeSlot.withSlot(function* () { - const routing = yield* options.routeFor(context); - pendingRoute = routing.route; - try { - const value = yield* op(); - // Reported before the slot advances, so the next operation to pin a - // route already sees what this one established. - routing.resolved(value); - return value; - } finally { - pendingRoute = undefined; - } - }), - ...(options.coordinator ? { coordinator: options.coordinator } : {}), - ...(options.routeStore ? { routeStore: options.routeStore } : {}), - ...(options.executableObserver ? { executableObserver: options.executableObserver } : {}), - ...(options.dependencies?.createRuntime - ? { createRuntime: options.dependencies.createRuntime } - : {}), - // Spread whole, so what the provider is built from is the one object the - // caller assembled and can therefore report. Restating the policy here - // would give a report that agrees with the policy however this assembled. - ...(planCeiling === undefined ? {} : planCeiling.dependencies), - }, + dependencies, ); + return { provider, dependencies }; +} + +/** One partition, and the exact dependencies its provider was built from. */ +export interface TestAgentPartition { + readonly provider: AcpxProvider; + readonly dependencies: AcpxProviderDependencies; } diff --git a/packages/test-agent/tests/provider.test.ts b/packages/test-agent/tests/provider.test.ts index a97350895..2cf7979ed 100644 --- a/packages/test-agent/tests/provider.test.ts +++ b/packages/test-agent/tests/provider.test.ts @@ -30,7 +30,7 @@ describe("Tier TS — test-agent ACPX state", () => { it("TS1: withSessionRoute pins the instance route for provider work; probe route otherwise", function* () { const harness = createFakeRuntime(); yield* useFlatWorld("/work"); - const provider = yield* useTestAgentProvider({ + const { provider } = yield* useTestAgentProvider({ defaultAgent: "test", agents: ["test"], workerCommand: ["xmd", "test-agent"], @@ -77,7 +77,7 @@ describe("Tier TS — test-agent ACPX state", () => { harness.script({ manual: true }); yield* useFlatWorld("/work"); const routes: string[] = []; - const provider = yield* useTestAgentProvider({ + const { provider } = yield* useTestAgentProvider({ defaultAgent: "test", agents: ["test"], workerCommand: ["xmd", "test-agent"], diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index 9442d5387..211c4ae4e 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -2805,23 +2805,23 @@ capabilities — ``, ``, `` and `` — are the closure those exact bytes carry, and are syntax no document may write. [The plan command](./plan-command-spec.md) is the contract. -**Who can establish its ceiling is the host's to say, not the Component's.** The -declaration carries a trusted-host capability: the agent a Plan conversation +**Which Agent a Plan is written with is the host's to say, not the Component's.** +The declaration carries a trusted-host capability: the agent a Plan conversation defaults to, and the operation that installs that one invocation's provider -inside the ceiling. It is a closure the host supplied before the declaration -existed — never a prop, a Context value, a registration result or anything a -document, component or middleware can reach or replace. - -Three surfaces state one. A production run states the ACPX ceiling built from -the Agent stack it settled. A nested `` that declares a -canonical `` states the deterministic one built from the controlled -provider that declaration produced (specs/testing-spec.md). Everything else -states none, and carries the sentence a person reads instead: an `xmd test` root -does not resolve the name at all, and an unconfigured child resolves these exact -bytes and is refused at the ceiling before a directory, a provider, a turn or a -review exists. What the ceiling then does — the permission mode, the -prompt-failure policy, the capability refusals, the empty session directory — is -the same whichever provider is underneath. +under the authorship policy. It is a closure the host supplied before the +declaration existed — never a prop, a Context value, a registration result or +anything a document, component or middleware can reach or replace. + +Three surfaces answer it. A production run supplies the ACPX Agent context built +from the Agent stack it settled. A nested `` that declares +a canonical `` supplies the deterministic one built from the +controlled provider that declaration produced (specs/testing-spec.md). +Everything else supplies none, and carries the sentence a person reads instead: +an `xmd test` root does not resolve the name at all, and an unconfigured child +resolves these exact bytes and is refused before a directory, a provider, a turn +or a review exists. What the authorship frame then imposes — the permission +mode, the prompt-failure policy, the capability refusals, the empty session +directory — is the same whichever provider is underneath. For that controlled child alone, an authored `` gives the trusted host the exact scenario label to select before it privately maps the