core/src/pi-manifest
What a pi package’s manifest declares, and where pi will look for it (#221) — THE one place that knows pi’s manifest-resource layout.
TWO pi ASSUMPTIONS LIVE HERE, AND NOWHERE ELSE:
- a
pi.*value is a resource path or an array of them (docs/packages.md§ Dependencies), so reading them means flattening one level; - a resource path beginning
node_modules/is a LITERAL path relative to the package root — not a specifier pi resolves — so the file must be there on disk. Our harness manifest depends on that at the file level: it namesnode_modules/@gotgenes/pi-permission-system/src/index.tsandnode_modules/pi-multi-account/index.ts, source paths inside vendor packages rather than export-map entries.
⚠ pi does NOT error on a manifest resource path that does not exist — it skips it
SILENTLY (verified 2026-09-03: the vendor extension did not load and nothing was
reported). That is why missingVendorPaths exists at all, and why both launch points
refuse on a non-empty result instead of starting.
⚠ ONE COPY. Before #221 the flatten-and-filter existed three times — scripts/enso.ts’s
launcher decisions, scripts/link-vendor-packages.ts, and the server’s
assertVendorLinks — with the prefix spelled as a constant in two of them and as a bare
literal in the third, which is why the no-duplicate-shapes gate never named them. The
cost of three was that a change in how pi resolves resources would have to land in three
files in two languages, and the one that was missed becomes the silent path — the exact
failure packages/web/src/server/index.ts records having already cost a debugging detour.
⚠ node: — a subpath (@enso/core/pi-manifest), never the barrel (#211).
pi manifest
Section titled “pi manifest”declaredResources()
Section titled “declaredResources()”declaredResources(
manifest):string[]
Defined in: core/src/pi-manifest.ts:99
Every resource path the manifest declares under pi.*, flattened, in declaration order.
A non-string entry is not a resource path and is dropped rather than coerced — a hand-edited
manifest should not turn into a check against "42".
Parameters
Section titled “Parameters”manifest
Section titled “manifest”unknown
Returns
Section titled “Returns”string[]
Invariant
Section titled “Invariant”boot-and-launchers/manifest-order 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.
missingVendorPaths()
Section titled “missingVendorPaths()”missingVendorPaths(
manifest,packageRoot):string[]
Defined in: core/src/pi-manifest.ts:160
The declared node_modules/… paths that are not under packageRoot.
Only the vendor paths are checked: a repo-local resource (./extensions/…) is the
package’s own and is there or the package is broken, whereas the vendor ones are
materialized by the installer and npm workspaces used to hoist them away.
Parameters
Section titled “Parameters”manifest
Section titled “manifest”unknown
packageRoot
Section titled “packageRoot”string
Returns
Section titled “Returns”string[]
boot-and-launchers 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] }.
Contract
Section titled “Contract”scripts/print-harness.ts#buildPrintManifest
- scripts/enso.ts the terminal launcher (header)
- scripts/print-harness.ts the print mirror (header, materializePrintHarness)
- scripts/link-vendor-packages.ts the linker (requiredPackageNames)
- packages/core/src/pi-manifest.ts pi’s manifest-resource layout, for all three readers (header)
- packages/web/src/host/agent-host.ts in-process: the host (header ⚠⚠ items, createSession)
- packages/web/src/server/index.ts in-process: the server (assertVendorLinks)
- packages/harness/extensions/guards/index.ts why the guards precede every vendor extension, and why a denial does not depend on that (header)
- docs/seams/guards.md the guards seam
- docs/seams/logging.md the configurators
- packages/harness/extensions/logging/index.ts why logging is first (header, Why first in the manifest)
- docs/reference/env-and-config.md the manifest as a table
Invariant
Section titled “Invariant”boot-and-launchers/vendor-refusal 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.
vendorPackageNames()
Section titled “vendorPackageNames()”vendorPackageNames(
manifest):string[]
Defined in: core/src/pi-manifest.ts:173
The package names those vendor paths reach into, deduplicated.
⚠ A scoped package carries its scope in the FIRST TWO segments (@scope/name), so the
name is not simply the first one — node_modules/@gotgenes/pi-permission-system/src/index.ts
names @gotgenes/pi-permission-system, not @gotgenes.
Parameters
Section titled “Parameters”manifest
Section titled “manifest”unknown
Returns
Section titled “Returns”string[]
