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
99 changes: 72 additions & 27 deletions architecture.md

Large diffs are not rendered by default.

53 changes: 11 additions & 42 deletions packages/cli/src/evaluation-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@
* 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.
*
* ## It states no filesystem of its own
*
* 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.
*
* 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
Expand Down Expand Up @@ -52,54 +62,13 @@ import {
globReadEntry,
syntaxReadEntry,
} from "@executablemd/core/host";
import type {
ExecutionInstallation,
FragmentEvaluationInput,
FragmentFileAccess,
} from "@executablemd/core/host";
import { cwd, hostFilesHandler } from "@executablemd/runtime";

/**
* The exact filesystem operations an admitted fragment performs.
*
* A handler of this command's own, constructed here rather than read from
* `API.Files` when a fragment runs. The document's provider and this one behave
* identically β€” both are `hostFilesHandler()` β€” and the difference is that
* nothing a document, a repository component or middleware installs is between
* a fragment and this instance.
*
* Only the six operations a fragment can name are copied across. The handler
* also offers temporary directories; an admitted fragment has none, and not
* copying it is what makes that true.
*/
function ordinaryFiles(): FragmentFileAccess {
const handler = hostFilesHandler();
return {
checkFilePath: (input) => handler.checkFilePath(input),
readTextFile: (input) => handler.readTextFile(input),
globFiles: (input) =>
handler.globFiles({
cwd: input.cwd,
include: [...input.include],
exclude: [...input.exclude],
}),
writeTextFile: (input) => handler.writeTextFile(input),
deleteFile: (input) => handler.deleteFile(input),
ensureDirectory: (input) => handler.ensureDirectory(input),
// The caller's working directory, which is what `xmd run` resolves every
// authored path against. Read when a fragment runs rather than frozen at
// assembly, so a fragment and the document that produced it resolve the
// same relative path to the same file.
workingDirectory: () => cwd(),
};
}
import type { ExecutionInstallation, FragmentEvaluationInput } from "@executablemd/core/host";

/** The ceiling `xmd run` and its run children state. */
export function ordinaryEvaluationProfile(): FragmentEvaluationInput {
return {
read: [fileReadEntry(), globReadEntry(), syntaxReadEntry()],
write: [fileWriteEntry(), fileDeleteEntry(), elicitWriteEntry()],
files: ordinaryFiles(),
};
}

Expand Down
26 changes: 22 additions & 4 deletions packages/cli/src/plan-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,8 +635,17 @@ function planWriter(assembly: PlanComponentAssembly): IdentityComponent {
origin: `${PLAN_ORIGIN}#PlanWriter`,
forms: ["paired"],
props: PLAN_WRITER_PROPS,
factory: () =>
function* PlanWriter(props: Record<string, Json>): Operation<string> {
factory: (claim: IdentityClaimant) =>
function* PlanWriter(
props: Record<string, Json>,
invocation: ComponentInvocation,
): Operation<string> {
// The identity canonical execution minted where resolution selected
// this exact implementation, for this exact expansion. What it protects
// is the private invocation and the projection beneath it β€” a name, a
// copied function or a caller's claim that it is one cannot stand here β€”
// and it decides nothing about permission.
yield* claim(invocation);
// Before a directory exists, before a provider exists, and therefore
// before any session could be placed or any turn started. A host that
// supplies no Agent context refuses rather than writing a Plan under
Expand Down Expand Up @@ -830,8 +839,17 @@ function planInformation(): IdentityComponent {
forms: ["paired"],
props: EMPTY_PROPS,
returns: INFORMATION_RETURNS,
factory: () =>
function* PlanInformation(): Operation<Json> {
factory: (claim: IdentityClaimant) =>
function* PlanInformation(
_props: Record<string, Json>,
invocation: ComponentInvocation,
): Operation<Json> {
// Authenticated, for the reason the writer phase is: this phase runs
// the agent's text under the host's read-only generated selection, and
// an invocation canonical execution did not issue is not that phase.
// It authorizes no filesystem access of its own β€” the trusted workflow
// reads through the ordinary chain either way.
yield* claim(invocation);
// The child and its structured teardown have both settled by the time
// this returns, so what is scanned below is complete text and not a
// fragment still able to produce more.
Expand Down
39 changes: 29 additions & 10 deletions packages/cli/src/plan-writer-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,23 @@ function* releaseSessionDirectory(directory: string, claim: DirectoryClaim): Ope
* refused rather than served. It decides what to write; writing a file,
* running a command, starting a service and reaching the network are all the
* approved document's business, under the caller's own configuration.
*
* ## Reading is not one of them
*
* The packaged workflow is trusted source, and reading is how it decides what
* to propose: it inspects the project before drafting, and the agent's own
* information request is public read-only `<Evaluate>` under the host's
* admitted vocabulary and complete preflight. So reads are absent from this
* table rather than refused and re-permitted somewhere β€” `checkFilePath`,
* `readTextFile` and `globFiles` are simply not members of it, and they take
* the ordinary chain: the provider the entrypoint or the enclosing host
* installed, the middleware around it, and the effective `Permissions` of the
* scope asking. An enclosing read restriction therefore still denies them, and
* this frame answers `Permissions` with nothing.
*
* What stays refused is every way of *changing* anything, wherever it is asked
* from. An agent's text is not trusted source; what bounds it is the generated
* admission it runs under rather than a read denial here.
*/
function* refuseDocumentCapabilities(): Operation<void> {
const refuse = (capability: string) => () => {
Expand All @@ -785,14 +802,6 @@ function* refuseDocumentCapabilities(): Operation<void> {
);
};
yield* API.Files.around({
// deno-lint-ignore require-yield
*checkFilePath() {
return refuse("a file")();
},
// deno-lint-ignore require-yield
*readTextFile() {
return refuse("a file")();
},
// deno-lint-ignore require-yield
*writeTextFile() {
return refuse("a file")();
Expand All @@ -801,9 +810,19 @@ function* refuseDocumentCapabilities(): Operation<void> {
*deleteFile() {
return refuse("a file")();
},
/**
* Refused, and newly named.
*
* The table omitted the semantic directory operation while nothing in the
* writer frame could reach one: `<Dir>` was a Plugin's component and the
* frame declared none. Canonical core owns it now, and an admitted
* generated `<Dir>` performs it through this same Api β€” so the accepted
* no-mutation boundary has to say so rather than rely on the name being
* unreachable.
*/
// deno-lint-ignore require-yield
*globFiles() {
return refuse("a file")();
*ensureDirectory() {
return refuse("a directory")();
},
// deno-lint-ignore require-yield
*temporaryDirectory() {
Expand Down
Loading
Loading