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
110 changes: 109 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,103 @@ jobs:
--component-dir smoke-test/agent/components \
--raw

# The host document-filesystem contract, on every target a release ships.
#
# `test-deno`, `test-node`, and `test-bun` run the whole corpus on Linux x64
# and prove the contract holds there. What they cannot prove is that it holds
# on the other four triples: the host adapter's containment is path arithmetic
# plus `realpath`, and both are the platform's β€” Windows has drive letters,
# UNC paths, junctions, and reparse points that POSIX does not, and the two
# macOS rows resolve `/var` through a symlink that a naive comparison reads as
# an escape.
#
# So this row is focused rather than exhaustive: one suite, plus a compiled
# probe, on each of the five. The compiled probe is the second half of the
# claim β€” the shipped artifact is a binary, and the adapter reaches
# `node:path`, `node:fs`, and `node:os` through whatever `deno compile` put in
# its graph.
filesystem-contract:
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15
target: aarch64-apple-darwin
- runner: macos-15-intel
target: x86_64-apple-darwin
- runner: ubuntu-24.04
target: x86_64-unknown-linux-gnu
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
- runner: windows-2025
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.runner }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
with:
deno-version: v2.9.5

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"

- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4

- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14

# `deno install` rather than `deno task deps`: the task also caches the
# graphs a browser build and a release compile walk, and it reaches them
# by spawning a child β€” which does not survive the Windows runner's path
# handling. Nothing here builds the bundle or compiles the CLI, so the
# plain frozen install is the whole preparation this job needs.
- name: Install dependencies
run: deno install --frozen

# The compile below runs under `--node-modules-dir=none`, which resolves
# npm packages from the Deno cache rather than from `node_modules`. This
# caches the probe's graph in that mode without touching the layout the
# step above just created.
- name: Cache the probe's graph for a compile
run: >
deno install --entrypoint --node-modules-dir=none --frozen
scripts/files-contract-probe.ts

# Deno first, and the compile with it: `pnpm install` and `bun install`
# each rewrite `node_modules` into their own layout, so a Deno step after
# one of them resolves through links the other pruned (#279).
- name: Host contract under Deno
run: deno test --allow-all --frozen packages/runtime/tests/host-files.test.ts

- name: Host contract as a compiled binary
run: |
set -eu
deno compile --node-modules-dir=none --cached-only --frozen --allow-all \
--output dist/files-contract-probe scripts/files-contract-probe.ts
if [ -f dist/files-contract-probe.exe ]; then
./dist/files-contract-probe.exe
else
./dist/files-contract-probe
fi

- name: Install the Node layout
run: pnpm install

- name: Host contract under Node
run: pnpm exec tsx --tsconfig tsconfig.node.json --test packages/runtime/tests/host-files.test.ts

- name: Install the Bun layout
run: bun install

- name: Host contract under Bun
run: bun test --timeout=300000 packages/runtime/tests/host-files.test.ts

# The same chain `deno task verify:clean` runs locally. It is the regression
# for #279's ownership claim: a build that installs anything moves the
# prepared-state fingerprint, prunes pnpm's links, and fails the resolution
Expand Down Expand Up @@ -384,7 +481,18 @@ jobs:
run: bun run packages/cli/src/bun.ts test smoke-test/test-agent/README.md --raw

green:
needs: [lint, test-deno, jsr, smoke, composability, site, test-node, test-bun]
needs:
[
lint,
test-deno,
jsr,
smoke,
filesystem-contract,
composability,
site,
test-node,
test-bun,
]
if: always()
runs-on: ubuntu-latest
steps:
Expand Down
72 changes: 71 additions & 1 deletion architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ Existing documents and code get aligned to this section retroactively.
| blocker | execution needs input (auth, a human answer); not a failure |
| suspension | a durable wait: a crash restarts into the same wait |
| Workspace | the provider-neutral, run-owned environment that supplies retained filesystem, repository, process and working-directory capabilities to a workflow |
| document filesystem | the files a document names in its own text, reached only through `API.Files`; distinct from the host paths the engine's own control plane reads |
| Files provider | the installed implementation of `API.Files` for a document execution: the host provider under `xmd run`, a transaction-bound provider under a workflow run. There is no default, and no provider falls back to another |
| stable host namespace | the condition the host Files provider's containment is stated under: no other process replaces a directory, symlink, junction or reparse point between the moment a path is observed and the moment it is used |
| Files infrastructure failure | a Files provider that is absent, that refuses an operation, or that broke its own contract; fatal like a durability failure, and never a printed error |
| ephemeral | a replay classification for an operation, context or attachment that runs again to reconstruct live execution; its result is not substituted from the journal and it owns no durable workflow state |
| live binding | an execution-owned value reconstructed ephemerally for the current document execution; it is visible only to constructs that explicitly consume the live binding overlay and never enters interpolation or the journal |
| attached service | a scoped host process that publishes its authenticated loopback endpoint through the XMD service handshake protocol and remains supervised for the lifetime of its service attachment |
Expand Down Expand Up @@ -688,7 +692,7 @@ the process may terminate, and a later document execution arrives back at the
same wait. An error that reveals a blocker (an expired login) reaches suspension
through middleware; waiting itself is never raised.

### 8. Durability failures are outside the model
### 8. Durability and Files infrastructure failures are outside the model

A durability failure (Β§6.11) says the journal no longer describes the document
execution. No middleware sees it; it is never the document's own outcome. A
Expand All @@ -705,6 +709,69 @@ throws. A pre-persistence policy rejection remains the policy's ordinary
document failure; the guarded stream marks that boundary before the backing
append and does not activate the fail-stop state.

A Files infrastructure failure is outside the model on the same terms. A
missing provider, a refused operation, and a provider that broke its own
contract are none of them things the document did or can act on, and printing
one would let every step after the file work run as though the file work had
happened. No middleware converts one, and no printing boundary prints one.

Both are discovered through one cycle-safe traversal of the whole cause graph,
and precedence is decided by kind rather than by position: a durability failure
first, then a Files infrastructure failure, then a documentation failure. The
selected failure comes back by identity, because a fail-stop that records "the
first error" has to record the one that happened.

The two kinds are *recognized* by different mechanisms, and the difference is
deliberate. A durability failure is recognized exactly as it always has been β€”
by class, against the failure types durable-streams exports β€” and that is
unchanged. A **Files** infrastructure failure is recognized structurally: a
stable tag on frozen data, the fixed diagnostic for its kind, and no cause,
never `instanceof`. It has to be, because the Files boundary is the one a
separately loaded copy of a package reaches across, where `instanceof` answers
false. Recognizing one is also a decision to let that exact object travel
onward, so a candidate carrying anything else β€” a raw message, a cause chain,
an extra field β€” fails the contract and is replaced by a fresh invariant rather
than preserved.

The traversal reads values it did not create, so every read in it is total: a
thrown Proxy or a failing accessor narrows what discovery finds instead of
replacing the failure being classified.

## The document filesystem boundary

Contextual routing is not authority. `API.Files` decides *which* provider
answers a document's file operations; what a provider is allowed to do is
decided by the provider, from identities the contextual layer cannot supply.

The Api's operations are whole semantic acts β€” read this path, replace this
path, list what these patterns select β€” rather than steps a caller sequences.
The one preliminary operation, `checkFilePath`, is deliberately weak: pure path
arithmetic, no filesystem access, and it returns nothing usable. `<File>` calls
it to decide whether a write's children may expand, and the later write repeats
the same admission from the same authored path. Nothing is handed between them,
so a check that was skipped or answered elsewhere authorizes nothing.

The two providers make different containment claims, and both are stated rather
than implied:

- **`xmd run`** resolves document paths in the caller's own filesystem. It
refuses empty, absolute, and lexically escaping paths without touching the
filesystem, and refuses an observed outward symlink once resolution can see
one. That holds while the host namespace is stable; it is not a sandbox, and
closing the replacement window would require a native dependency this project
does not take.
- **A workflow run** resolves document paths in the run-owned Workspace's
logical filesystem, inside the caller-owned transaction. A document path
never becomes a host path, so there is no host namespace to replace.

Neither claim covers a native command a document runs.

Failure data crosses the boundary as a plain frozen object under a stable tag,
carrying a reason from a fixed vocabulary and the phase it came from. No
message, errno code, resolved path, temporary name, or symlink target crosses.
Consumers parse that data before reading a field; a write whose data does not
validate is a provider-contract failure rather than a commit state to invent.

## Attempts

- The journal records every attempt in full; replay restores the outcome
Expand Down Expand Up @@ -831,6 +898,9 @@ Status is measured against main.
| Workspace coordination API | fails closed by default and lets a Workspace operation explicitly select provider coordination | built on the #365 stack; the atomic Deno Workspace handler is unbuilt |
| explicit WorkflowRun journal route | binds one already-filtered publication to one exact active transaction and otherwise uses ordinary serialized journal storage | built on the #365 stack |
| `API.Service` / `startService()` | creates an authenticated, supervised loopback service attachment through a provider-neutral operation | built on main |
| `API.Files` | routes every document filesystem operation to the installed provider, with no host default and structural failure data | built on the #227 stack |
| host Files provider / `useHostFiles()` | resolves document paths in the caller's filesystem, containing them while the host namespace is stable; installed by all four CLI entrypoints | built on the #227 stack |
| transaction-bound Files provider | resolves document paths in the run-owned logical Workspace inside the caller-owned transaction | unbuilt; the adapter is #227's second layer, and workflow effect coordination and CLI reachability remain unbuilt |
| `service=<binding>` | publishes the attachment's endpoint into the live binding overlay for its invocation | built on main |
| `ephemeral eval` | reconstructs live middleware and bindings without a journal entry | built on main |
| `useWorkflowServiceDenial()` | provides and tests a non-delegating workflow service denial provider; #366 will install it in future start and resume scopes | built on main; no workflow CLI execution branch exists yet |
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { main } from "effection";
import { fileURLToPath } from "node:url";
import process from "node:process";
import { API } from "@executablemd/runtime";
import { API, useHostFiles } from "@executablemd/runtime";
import { compileDataUri } from "@executablemd/core";
import { runXmd } from "./cli.ts";
import { useBunService } from "./bun-service.ts";
Expand All @@ -30,5 +30,10 @@ await main(function* (args) {
},
{ at: "min" },
);
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

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
// host by accident.

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
// host by accident.

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
// host by accident.

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
// host by accident.

yield* useHostFiles();
yield* runXmd(args, useBunService);
});
7 changes: 6 additions & 1 deletion packages/cli/src/compiled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { main } from "effection";
import process from "node:process";
import { API } from "@executablemd/runtime";
import { API, useHostFiles } from "@executablemd/runtime";
import { compileDataUri } from "@executablemd/core";
import { runXmd } from "./cli.ts";
import { useCompiledService } from "./compiled-service.ts";
Expand All @@ -27,5 +27,10 @@ await main(function* (args) {
},
{ at: "min" },
);
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

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
// host by accident.

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
// host by accident.

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
// host by accident.

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
// host by accident.

yield* useHostFiles();
yield* runXmd(args, useCompiledService);
});
7 changes: 6 additions & 1 deletion packages/cli/src/deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { main } from "effection";
import { fileURLToPath } from "node:url";
import process from "node:process";
import { API } from "@executablemd/runtime";
import { API, useHostFiles } from "@executablemd/runtime";
import { compileDataUri } from "@executablemd/core";
import { runXmd } from "./cli.ts";
import { useDenoService } from "./deno-service.ts";
Expand All @@ -32,5 +32,10 @@ await main(function* (args) {
},
{ at: "min" },
);
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

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
// host by accident.

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
// host by accident.

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
// host by accident.

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
// host by accident.

yield* useHostFiles();
yield* runXmd(args, useDenoService);
});
7 changes: 6 additions & 1 deletion packages/cli/src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import { main } from "effection";
import { fileURLToPath } from "node:url";
import process from "node:process";
import { API } from "@executablemd/runtime";
import { API, useHostFiles } from "@executablemd/runtime";
import { compileTempFile } from "@executablemd/core";
import { runXmd } from "./cli.ts";
import { useNodeService } from "./node-service.ts";
Expand All @@ -37,5 +37,10 @@ await main(function* (args) {
},
{ at: "min" },
);
// Document filesystem access resolves in the caller's own filesystem here.
// It is installed explicitly, and at the same depth, because `API.Files` has
// no host default: a run with no provider must fail rather than reach the
// host by accident.

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
// host by accident.

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
// host by accident.

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
// host by accident.

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
// host by accident.

yield* useHostFiles();
yield* runXmd(args, useNodeService);
});
Loading
Loading