Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
314 changes: 174 additions & 140 deletions architecture.md

Large diffs are not rendered by default.

15 changes: 7 additions & 8 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ import {
scanPlanArgs,
} from "./plan-args.ts";
import type { PlanScan } from "./plan-args.ts";
import { ordinaryEvaluationProfile, statesEvaluation } from "./evaluation-profile.ts";
import { useOrdinaryGeneratedSelection } from "./evaluation-profile.ts";
import { runPlan } from "./plan.ts";
import { runUpgrade } from "./upgrade.ts";
import type { UpgradeAssembly } from "./upgrade.ts";
Expand Down Expand Up @@ -1406,6 +1406,12 @@ function* runDocument(
planDeclaration,
});

// What a generated fragment may write here, offered in the scope this
// execution runs in. It delegates first, so a host that attached its own
// selection — a workflow attachment states its own — keeps it, and this
// command's ordinary selection answers only where nothing else did.
yield* useOrdinaryGeneratedSelection();

// One authoritative execution, and only one. What a host attaches travels as
// values canonical core captures before anything else exists — never as a
// second call, and never as middleware that could be reordered around this
Expand Down Expand Up @@ -1440,13 +1446,6 @@ function* runDocument(
// and does not gain `<Plan>` at its root — but the production run child
// it can launch is the run profile, and gets it below.
...(mode.testing ? {} : { declarations: [plan] }),
// The ceiling a generated fragment runs under, stated only where the
// host that attached this execution stated none: a workflow attachment
// states its own Workspace-bound profile, and one execution offers one
// maximum permission.
...(statesEvaluation(mode.installations)
? {}
: { evaluation: ordinaryEvaluationProfile() }),
},
// The declarations a nested execution may configure a child with, named
// by the exact definitions this command installed. Recognizing one is
Expand Down
145 changes: 83 additions & 62 deletions packages/cli/src/evaluation-profile.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,108 @@
/**
* What `xmd run` lets a generated fragment do.
* What `xmd run` offers a generated fragment, as ordinary Component middleware.
*
* `<Evaluate>` is public and canonical core owns what it means, so this file
* does not decide the component — it decides the *ceiling*, which is the only
* does not decide the component — it decides the *selection*, which is the only
* part a host owns. It is stated here, at the assembly this command already
* does, before any document exists to ask for it.
* does, in the scope that will execute the document.
*
* ## The two tables
* ## What is selected
*
* `read` is core's self-closing `<File />`, its self-closing `<Glob />` and
* canonical `<Syntax />`. `write` is core's paired `<File>…</File>`, its
* self-closing `<File.Delete />` and paired canonical `<Elicit>`: a fragment
* that may change something may also ask the person first, which is what makes
* "preview this, then confirm it" a program an Agent can write. A `read`
* selection admits no question — it promises nobody will be interrupted.
* Names and authored forms, and nothing else:
*
* A fragment run by `xmd run` therefore reaches the caller's own filesystem
* through the Files provider this command installed, and the person through the
* Elicitation provider the run already has. It reaches nothing else at all: no
* network read, no process, no repository, no Git, no credential and no agent.
* ```ts
* {
* File: ["self-closing", "paired"],
* "File.Delete": ["self-closing"],
* Glob: ["self-closing"],
* Syntax: ["self-closing"],
* Elicit: ["paired"],
* }
* ```
*
* ## It states no filesystem of its own
* Core resolves each of those names once, before public execution policy, and
* reads the identity, the effect and the contract of every selected spelling
* off the declaration that owns the implementation. So reading and writing stay
* separate spellings of one name — `allow={["read"]}` admits `<File />` and not
* `<File>…</File>` — without this file saying anything about what either does.
*
* `<Elicit>` is selected paired: a fragment that may change something may also
* ask the person first, which is what makes "preview this, then confirm it" a
* program an Agent can write. Its owner offers the paired form as a write, so a
* `read` selection admits no question and promises nobody will be interrupted.
*
* An admitted name runs canonical core's own component, which performs its
* operation through contextual `API.Files` — the provider this command's
* entrypoint installed, and the same one every authored `<File>` in the
* document crosses. So a fragment's read is observed, transformed, delegated
* and refused by exactly the middleware an author's read is, and the effective
* `Permissions` of the scope it runs in decide it. This file hands the profile
* no operation at all, because there is no second filesystem for it to hand.
* `Dir` and `Fetch` are absent rather than present-and-bounded. An ordinary run
* creates no directory and reads no network on a document's behalf, and
* selecting a name a fragment could never usefully call would tell an agent it
* had something it does not.
*
* Reading and writing stay separate spellings of one name. A selection of
* `read` admits `<File />` and not `<File>…</File>`, so a fragment that asks to
* search and read cannot also write, and `<Syntax />` describing a component
* says only what the vocabulary holds — a name it renders is not a name the
* selection admits.
* ## It states no filesystem of its own
*
* `<Fetch>` is absent rather than present-and-bounded. An unbounded network
* read is a decision this command does not make on a document's behalf, and
* admitting the name with an empty ceiling would be a different thing from not
* admitting it — the fragment would name a component it can never successfully
* call.
* A selected name runs the implementation canonical resolution answered with,
* which performs its operation through contextual `API.Files` — the provider
* this command's entrypoint installed, and the same one every authored `<File>`
* in the document crosses. So a fragment's read is observed, transformed,
* delegated and refused by exactly the middleware an author's read is, and the
* effective `Permissions` of the scope it runs in decide it.
*
* ## No Workspace
* ## Delegate first, default only on absence
*
* An ordinary run evaluates against no Workspace. Its admitted effects address
* the Files provider its own execution installed, and there is no immutable
* root history for a continuation to be held to — which is a different
* statement from evaluating against an empty one, and the profile says so by
* omitting the access rather than answering it with nothing.
* This is ordinary property middleware: it asks what the scope around it
* already offers and supplies the selection above only when that answer is
* `undefined`. A host that stated its own — a workflow attachment, a test
* harness, a plugin — keeps it, and a defined answer that is empty or malformed
* is preserved for core to refuse rather than quietly replaced with this one.
* Two handlers compose normally; there is no contributor count, no automatic
* union and no installation-array inspection.
*
* It accepts `text` and refuses `source`: `xmd run` never shipped the earlier
* spelling, so there is no document written against it to keep working.
* `useGeneratedFallback()` is that rule, and every ordinary host that supplies a
* default goes through it — this command, each REPL entry, an isolated run
* child. A host that answered instead would be the one place a plugin's or a
* captured installation callback's selection could not be read, which is the
* same as not composing at all.
*/

import {
elicitWriteEntry,
fileDeleteEntry,
fileReadEntry,
fileWriteEntry,
globReadEntry,
syntaxReadEntry,
} from "@executablemd/core/host";
import type { ExecutionInstallation, FragmentEvaluationInput } from "@executablemd/core/host";
import { Component } from "@executablemd/core/api";
import type { GeneratedSelection } from "@executablemd/core/api";
import type { Operation } from "effection";

/** The ceiling `xmd run` and its run children state. */
export function ordinaryEvaluationProfile(): FragmentEvaluationInput {
/** The names and forms `xmd run` and its run children offer. */
export function ordinaryGeneratedSelection(): GeneratedSelection {
return {
read: [fileReadEntry(), globReadEntry(), syntaxReadEntry()],
write: [fileWriteEntry(), fileDeleteEntry(), elicitWriteEntry()],
File: ["self-closing", "paired"],
"File.Delete": ["self-closing"],
Glob: ["self-closing"],
Syntax: ["self-closing"],
Elicit: ["paired"],
};
}

/**
* Whether the host that attached this execution already stated a ceiling.
* Offer this selection for this scope, unless something nearer already offers
* one.
*
* One execution offers one maximum permission, so a workflow attachment — which
* states its own Workspace-bound profile — is not also given the run profile's.
* Asked about the installations rather than resolved by order, because
* "whichever came last wins" is exactly how permission stops being auditable.
* Installed where the document will execute. Installing it in a scope that ends
* before the execution would leave the middleware chain core reads composed of
* handlers that are no longer there.
*
* The composition rule lives here rather than at each caller, so a host that
* supplies a default of its own supplies it the same way.
*/
export function statesEvaluation(
installations: readonly ExecutionInstallation[] | undefined,
): boolean {
return (installations ?? []).some((installation) => installation.evaluation !== undefined);
export function useGeneratedFallback(selection: GeneratedSelection): Operation<void> {
return Component.around({
// A value member, so the chain is synchronous: `next()` answers with what
// the scope around this already offers, and only absence defaults. An empty
// or malformed selection is a host statement core validates and refuses;
// replacing it here would answer a host's mistake with this command's
// permission.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// permission.

generated: (_args, next) => {
const stated = next();
return stated === undefined ? selection : stated;
},
});
}

/** Offer the ordinary selection, unless something nearer already offers one. */
export function useOrdinaryGeneratedSelection(): Operation<void> {
return useGeneratedFallback(ordinaryGeneratedSelection());
}
20 changes: 12 additions & 8 deletions packages/cli/src/plan-writer-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { FormOpener } from "@executablemd/web";

import { hostAcpDependencies } from "./agent-stack.ts";
import type { PlanWriterStack } from "./agent-stack.ts";
import { ordinaryEvaluationProfile } from "./evaluation-profile.ts";
import { useOrdinaryGeneratedSelection } from "./evaluation-profile.ts";
import { PLAN_COMMAND_DOCUMENT, readPackagedDocument } from "./packaged-document.ts";

/**
Expand Down Expand Up @@ -450,6 +450,17 @@ export function* runPlanCommandDocument(profile: PlanWriterProfile): Operation<R
yield* useTerminalOutput();
}

// The ordinary run's generated selection, so an information request this
// authorship evaluates reads through exactly what `xmd run` offers —
// File, Glob and canonical Syntax, with the write spellings offered and
// never selected. Offered in the bounded writer scope this execution
// runs in, by the host rather than by `<Plan>`: a component that
// offered its own would be choosing its ceiling instead of being given
// one. The target-run description `installSyntax()` bound above stays
// separate — describing a component neither imports it nor grants its
// effects.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// effects.

yield* useOrdinaryGeneratedSelection();

// The command root is a thin adapter: it projects the request into
// `<Plan>` and returns what comes back. Everything that used to be
// installed around it — the directory, the provider, the Elicitation, the
Expand Down Expand Up @@ -478,13 +489,6 @@ export function* runPlanCommandDocument(profile: PlanWriterProfile): Operation<R
{
components: agentIdentityComponents(),
declarations: [profile.declaration],
// The ordinary run's evaluation ceiling, so an information request
// this authorship evaluates reads through exactly what `xmd run`
// states — File, Glob and canonical Syntax, with the write table
// present and never selected. The host installs it, not `<Plan>`:
// a component that installed its own would be choosing its
// ceiling instead of being given one.
evaluation: ordinaryEvaluationProfile(),
},
],
);
Expand Down
19 changes: 15 additions & 4 deletions packages/cli/src/repl-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
import type { AcpxProviderDependencies } from "@executablemd/acp";
import { agentIdentityComponents } from "@executablemd/core";
import type { PermissionMode } from "@executablemd/core";
import type { GeneratedSelection } from "@executablemd/core/api";
import type { ExecutionInstallation } from "@executablemd/core/host";
import { useScope } from "effection";
import type { Operation } from "effection";

import { installAgentProviderStack } from "./agent-stack.ts";
import type { AgentStack } from "./agent-stack.ts";
import { ordinaryEvaluationProfile } from "./evaluation-profile.ts";
import { ordinaryGeneratedSelection } from "./evaluation-profile.ts";
import { planComponentDeclaration } from "./plan-component.ts";
import { planAgentContext } from "./plan-writer-profile.ts";
import type { CommandPlugins } from "./plugin-host.ts";
Expand Down Expand Up @@ -60,10 +61,20 @@ export interface ReplExecutionProfile {
* What this command declares to every execution it opens.
*
* In installation order: the selected Plugins as they were assembled, then
* this command's own vocabulary — the Agent identity components, the packaged
* `<Plan>` Component, and the ceiling a generated fragment runs under.
* this command's own vocabulary — the Agent identity components and the
* packaged `<Plan>` Component.
*/
readonly installations: readonly ExecutionInstallation[];
/**
* What a generated fragment may write in every entry this command opens.
*
* Data, like everything else here: the names and authored forms this command
* offers, which the session installs as ordinary Component middleware in each
* entry's own execution scope. A profile-construction scope ends before the
* first entry runs, so a selection installed there would be a chain nothing
* reads.
*/
readonly generated: GeneratedSelection;
/** How this REPL answers an Agent permission request. */
readonly permissionMode: PermissionMode;
}
Expand Down Expand Up @@ -115,9 +126,9 @@ export function* assembleReplProfile(
Object.freeze({
components: agentIdentityComponents(),
declarations: Object.freeze([plan]),
evaluation: ordinaryEvaluationProfile(),
}),
]),
generated: ordinaryGeneratedSelection(),
permissionMode: stack.permissionMode,
});
}
Expand Down
24 changes: 18 additions & 6 deletions packages/cli/src/repl/expansion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,14 @@ export interface BoundaryPartition {
/**
* `Component`, partitioned.
*
* `env`, `evalScope`, `persistent` and `registry` are values rather than steps —
* there is no continuation at them to hold. `hasCapture`, `hasContent` and
* `hasBinding` answer a question about an invocation the walk has already
* reached; holding one would stop the same walk the gate before it already
* stops, one question later.
* `env`, `evalScope`, `persistent`, `registry` and `generated` are values rather
* than steps — there is no continuation at them to hold. `generated` is also
* read before any expansion exists: canonical execution asks for the host's
* generated selection once, before public execution policy, so there is no walk
* for a hold to be standing in. `hasCapture`, `hasContent` and `hasBinding`
* answer a question about an invocation the walk has already reached; holding
* one would stop the same walk the gate before it already stops, one question
* later.
*
* `expand` surrounds an element's whole expansion, which makes it a step in
* the walk that reached it and not a walk of its own. Treating it as one would
Expand All @@ -102,7 +105,16 @@ export const COMPONENT_BOUNDARIES: BoundaryPartition = {
"raise",
"handleFailure",
],
unheld: ["env", "evalScope", "persistent", "registry", "hasCapture", "hasContent", "hasBinding"],
unheld: [
"env",
"evalScope",
"generated",
"persistent",
"registry",
"hasCapture",
"hasContent",
"hasBinding",
],
};

/**
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/repl/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export function* runReplProgram(options: ReplProgramOptions): Operation<Result<R
execution,
includes: options.profile.includes,
installations: options.profile.installations,
generated: options.profile.generated,
permissionMode: options.profile.permissionMode,
...(state.route.at === undefined ? {} : { selection: state.route.at }),
});
Expand Down
Loading
Loading