Flow: Boot and launchers: the manifest, the isolation triple, the print mirror, the vendor links
Two launchers start the runtime with only this harness loaded: the terminal launcher (scripts/enso.ts) spawns it as a child; the server (startEnsoServer → createAgentHost) runs it in-process (#69). Both read one manifest, packages/harness/package.json → pi, and enforce the same preconditions before any of the runtime is imported: vendor links present (missingVendorPaths), the isolation triple applied, secrets out of the environment (#89). What differs is flags on a spawn versus loader options, and the print variant (#20). In-process, the host sets PI_CODING_AGENT_DIR and PI_MULTI_ACCOUNT_INDEPENDENT_ROOTS=1, seeds the multi-account config (childProxy off, the auth pattern of #341, Cursor off unless set), quarantines secrets, and then builds each thread’s services with { noExtensions, noSkills, additionalExtensionPaths: [harness] }.
The sequence
Section titled “The sequence”The terminal launcher
Section titled “The terminal launcher”Recorded by enso-launch.test.ts: a print launch reads pi –version from PATH, then spawns that pi with the isolation triple and the print mirror. Arrows are what the test drove and what its doubles received; notes are what the code logged at debug and above, in the order it logged them.
sequenceDiagram participant U as user shell (the test) participant Z as terminal launcher (scripts/enso.ts) participant M as manifest (packages/harness/package.json → pi) participant X as print mirror (print-harness.ts) participant P as pi on PATH (a shim) U->>Z: bun scripts/enso.ts -p hello · stdin not a TTY Z->>M: read pi.* · 14 declared resource paths, every node_modules/ path present Z->>U: enso: permission policy: ENSO_HOME/permissions.json was absent, so it now holds Enso's default policy · Claude Code's settings are not read (#35;446, #35;496) Z->>P: pi --version P->>Z: 0.87.1 · exit 0 Z->>X: materialize: pi.extensions minus the permission engine (9 of 10), every package-root entry linked Z->>U: enso: vanilla pi 0.87.1 | agent-dir=ENSO_HOME/pi-agent | package=./node_modules/.cache/enso/print-harness (print variant, no permission engine — #35;20) | 14 declared resource path(s) Z->>P: spawn pi --no-extensions --no-skills -e ./node_modules/.cache/enso/print-harness -p hello · env PI_CODING_AGENT_DIR=ENSO_HOME/pi-agent X->>P: pi.extensions in manifest order: logging → guards → web-fetch → web-search → permission-modes → pi-multi-account → ask-user → system-prompt → run-context P->>Z: exit 0 Z->>U: exit 0
The invariants it shows
Section titled “The invariants it shows”Load order is the manifest, enumerated by hand, never a glob.
Section titled “Load order is the manifest, enumerated by hand, never a glob.”pi.extensions is declaration order, never sorted: the four repository extensions (logging, guards, web-fetch, web-search), then the two vendor ones, then the rest of the repository’s. The runtime resolves tools first-wins, so a registered tool name shadows every later registration of that name, which is why the guards are declared ahead of both vendor extensions, and why a directory glob (ordering by filename) is not acceptable. It is not what makes the guard hold: the guard registers no tool and participates through a tool_call handler instead (additive, first block final whatever the order), and that hook’s two handlers are guards, then the mode extension. A third is #4’s trigger. What the order does decide is the configurator (boot-and-launchers/logging-first). This function reads the order for the three readers that check the manifest (the launcher, the linker, the server), and never sorts it.
Stated at declaredResources in pi-manifest.ts.
Pinned by:
tool-call-participants.test.ts: ⚠ exactly the two known tool_call participants — a third means #4’s arbiter is due
agent-host.test.ts: exactly the manifest’s extensions load, in order, then the host’s own model watcher, with no load errors
fallow-config.test.ts: ⚠ every repository extension in the harness manifest is a fallow entry, and only those (#163)
A missing vendor path is a refusal at both launch points, naming the path and the fix.
Section titled “A missing vendor path is a refusal at both launch points, naming the path and the fix.”The runtime skips a missing manifest resource path without a word (the symptom, once, was an HTTP 400 several layers away). Only node_modules/ paths are checked; ./extensions/… is our own source. Upstream, link-vendor-packages.ts runs from the root postinstall as verify-or-fix over every dependencies entry plus the node_modules/ paths (@enso/core is only an import, and was unlinked): a present package is verified, an absent one is linked from the hoisted copy, or the install exits 1 naming it. The terminal launcher refuses with exit 1, naming each path and bun install; the server’s assertVendorLinks throws the same message before the host is created.
One precondition, and since #221 one IMPLEMENTATION: this module owns both pi assumptions (a pi.* value is a path or an array of them; a node_modules/ path is literal, not a specifier), and all three readers call it. It was written three times before, with the prefix spelled as a constant twice and a bare literal once, which is why the duplicate-shape gate never named them.
Stated at missingVendorPaths in pi-manifest.ts.
Pinned by:
pi-manifest.test.ts: the vendor paths a manifest declares that are not under the package root are named, and only those
vendor-links.test.ts: refuses to start when a vendor manifest path is missing
link-vendor-packages.test.ts: verify-or-fix: a present package is verified, an absent one is linked from the hoisted copy, and the link resolves
Logging is first in the manifest, and configures only when nothing has.
Section titled “Logging is first in the manifest, and configures only when nothing has.”Two ways this code runs, one behaviour each. As a child (enso.ts → pi) nothing has configured, so this configures: the same day’s file as everyone else, process: "pi", NO console sink (stderr is the TUI’s). In-process (the web host) the server’s launcher configured logging before pi existed, and isLoggingConfigured() says so: this configures nothing, and the extensions’ records land in the server’s file through the server’s configure. Never configure when something already has: LogTape’s configure throws on a second call by design (one configurator per process, or it is a bug: log-process.test.ts), and a reset: true here would silently replace the server’s sinks with ours.
WHY FIRST IN THE MANIFEST (packages/harness/package.json → pi.extensions): the sink is configured here, at load, awaited. Today no extension writes a record before session_start (guards, web-fetch and web-search log only inside handlers, which fire after every extension has loaded), so the position decides nothing yet (checked 2026-09-16, #167). It is first so that stays true: an extension that one day logs while loading finds the file already open, instead of a record that lands nowhere.
Stated at createLoggingExtension in packages/harness/extensions/logging/index.ts.
Pinned by:
logging.test.ts: ⚠ pi as a child: nothing has configured, so the extension does — its own record lands as process pi, and session_shutdown flushes
The terminal launcher refuses a pi it was not pinned against (#217).
Section titled “The terminal launcher refuses a pi it was not pinned against (#217).”The two surfaces resolve pi differently and only one was bounded: in-process imports the WORKSPACE copy, while the terminal launcher spawns the first pi on PATH. The guard REPLICATES pi’s path resolver (packages/harness/core/paths.ts; resolveToCwd is not on pi’s export map) and the corpus that keeps the copy honest (packages/harness/core/__tests__/paths.test.ts) reads the workspace copy, so on the terminal surface the guard could resolve a path differently from the tool it gates, and compute a deny against the wrong target, with the whole suite green (measured 2026-09-18 with a shim printing 0.85.0 earlier on PATH). So enso.ts reads pi --version before the spawn and refuses outside this range, naming both bounds, with no override flag: the range means “the corpus has been run against this”, and packages/harness/package.json → peerDependencies states the same bound. Widening it means running the corpus against the new pi FIRST.
The probe fails closed three ways beside the range: it is bounded at 5s (a wedged pi --version used to park the launcher silently, worse than the refusal it exists to print, and arming a kill is not enough, since a shell shim’s own child holds the pipe open, so the deadline races the READ), a non-zero exit is refused rather than parsed, and a banner carrying no pi version is no version claim. What it does NOT cover: the versions of the libraries inside the pi binary’s own tree.
Stated at SUPPORTED_PI_VERSIONS in scripts/enso-launch.ts.
Pinned by:
enso-launch.test.ts: the tested pi range includes its minimum and excludes the version it stops below
enso-launch.test.ts: a pi whose version cannot be read, or is a prerelease of the excluded version, is refused
enso-launch.test.ts: the version read is pi’s, whatever else the banner names first (#217)
pi-version-parity.test.ts: ⚠ the pi the guard corpus pins against is a pi the terminal launcher will run (#217)
Isolation is three mechanisms, each closing a different hole, and the same three at both launch points.
Section titled “Isolation is three mechanisms, each closing a different hole, and the same three at both launch points.”PI_CODING_AGENT_DIR points the runtime at ENSO_HOME/pi-agent (~/.enso, #421): none of the user’s packages, settings or credentials are visible. --no-extensions turns discovery off, including this repo’s own .pi/settings.json, which installs the package and would load it twice. --no-skills is required and covered by neither: ~/.agents/skills/ is outside the agent dir. They go BEFORE the user’s arguments, so a flag the user adds cannot displace them. The host applies the same triple as loader options and sets the env at creation, before the first services (it is read at module import).
The permission policy is prepared the same way at both points (#446, #496): preparePermissionPolicy seeds ENSO_HOME/permissions.json with Enso’s policy when it holds none, links the engine’s config path under the agent dir to it, and writes the mode agents; an unreadable file is refused, and the launcher says which on stderr, the host in its log.
Stated at piArguments in scripts/enso-launch.ts.
Pinned by:
agent-host.test.ts: ⚠ multi-account resolved its config inside the scratch agent dir, not ~/.pi/agent — the env was set first
enso-launch.test.ts: pi is launched with –no-extensions, –no-skills and -e, ahead of the user’s arguments
enso-launch.test.ts: a print launch reads pi –version from PATH, then spawns that pi with the isolation triple and the print mirror
-e receives the package directory, never extension files.
Section titled “-e receives the package directory, never extension files.”Loaded by package rules, the manifest drives every resource kind: extensions, skills, prompts and themes. Files passed individually loaded the extensions and silently dropped everything else. The ONE package is the harness, or its print variant.
Stated at piArguments in scripts/enso-launch.ts.
Pinned by:
enso-launch.test.ts: pi is launched with –no-extensions, –no-skills and -e, ahead of the user’s arguments
enso-launch.test.ts: a print launch reads pi –version from PATH, then spawns that pi with the isolation triple and the print mirror
A print run loads a generated mirror, not a flag.
Section titled “A print run loads a generated mirror, not a flag.”The trigger is “this run cannot prompt” (-p, --print, or stdin that is not a TTY). The mode extension fails closed when it cannot prompt, the runtime has no per-extension exclusion, and boot-and-launchers/package-dir forbids passing files, so the variant is a package under node_modules/.cache/enso/print-harness (the checkout’s own build cache, since its links point into this checkout): the filtered manifest (buildPrintManifest) plus a symlink for EVERY package-root entry, by readdir, since extensions import siblings by relative path. Enso’s config is not in the package at all: it is the user’s, ENSO_HOME/config.json (#421), found from the environment. A print run has our guards only: bash unconfined (#9).
Stated at materializePrintHarness in scripts/print-harness.ts.
Pinned by:
enso-launch.test.ts: a print run is -p, –print, or stdin that is not a TTY; an interactive run is none of those
enso-launch.test.ts: a print launch reads pi –version from PATH, then spawns that pi with the isolation triple and the print mirror
print-harness.test.ts: the real harness manifest matches the shape the print filter expects
print-harness.test.ts: materializePrintHarness writes the filtered manifest and symlinks into the real package
The contract
Section titled “The contract”/** * The harness manifest with the permission engine removed from `pi.extensions`. * Everything else — skills, prompts, themes, dependencies — is carried verbatim. */export function buildPrintManifest(manifest: HarnessManifest): PrintManifest { const extensions = manifest?.pi?.extensions if (!Array.isArray(extensions)) { throw new PrintHarnessShapeError('pi.extensions is not an array') } const keptExtensions = extensions.filter( (extensionPath) => typeof extensionPath !== 'string' || !extensionPath.includes(PERMISSION_ENGINE_FRAGMENT), ) if (keptExtensions.length === extensions.length) { throw new PrintHarnessShapeError( `no extension entry contains "${PERMISSION_ENGINE_FRAGMENT}" — the filter is stale`, ) } if (keptExtensions.length === 0) { throw new PrintHarnessShapeError('the filter removed every extension') } return { ...manifest, name: `${manifest.name}-print`, pi: { ...manifest.pi, extensions: keptExtensions }, }}Where it is stated
Section titled “Where it is stated”missingVendorPathsinpi-manifest.ts: the flow, vendor-refusaldeclaredResourcesinpi-manifest.ts: manifest-ordercreateLoggingExtensioninpackages/harness/extensions/logging/index.ts: logging-firstSUPPORTED_PI_VERSIONSinscripts/enso-launch.ts: pi-version-gatepiArgumentsinscripts/enso-launch.ts: isolation-triple, package-dirmaterializePrintHarnessinscripts/print-harness.ts: print-mirror- the terminal launcher (header)
- the print mirror (header, materializePrintHarness)
- the linker (requiredPackageNames)
- pi’s manifest-resource layout, for all three readers (header)
- in-process: the host (header ⚠⚠ items, createSession)
- in-process: the server (assertVendorLinks)
- why the guards precede every vendor extension, and why a denial does not depend on that (header)
- the guards seam
- the configurators
- why logging is first (header, Why first in the manifest)
- the manifest as a table
