@neuralis/package-system
v0.3.1
Published
The foundational contract package for Neuralis — the text of the package protocol itself. Everything the product vision promises ("every capability is a package", "a universal AI package consumer") is ultimately a type, a validator, or a dispatcher define
Readme
@neuralis/package-system
The foundational contract package for Neuralis — the text of the package
protocol itself. Everything the product vision promises ("every capability is
a package", "a universal AI package consumer") is ultimately a type, a
validator, or a dispatcher defined here. It defines PackageDefinition,
the contribution vocabulary every other package speaks (PackageTool,
PackageFile, PackageConnector, PackagePolicy, PackageUriPolicies,
UiElement, …), the lifecycle hooks the host calls, the connector port a
filesystem-like source must implement, the URI policy evaluator that gates
every read/write/exec, the runtime-stack snapshot every package and the
system prompt reads from, the manifest/contribution validators, the
result-bounding helper, and the in-process registries / dispatchers /
session store used by the host runtime.
This package has no LLM-facing surface. No tools, skills, files, UI, or team members ship from here. Every other package depends on it; it depends on nothing inside Neuralis. Treat its public exports as the stable contract vocabulary of the platform — breaking changes here are a breaking change for every consumer.
Module format. ESM is the convention for a first-party package — this one
included: "type": "module" in package.json, module/moduleResolution:
"NodeNext" and target: "ES2022" in tsconfig.json, and explicit .js
extensions on all relative imports (a directory import resolves to
/index.js). It is a convention, not a requirement: the loader performs the
standard CommonJS default-export interop, so a package compiled to CJS loads —
one first-party package is built that way today. Prefer ESM anyway; under CJS a
route's named exports (pattern, feature) reach the module namespace only
through Node's CommonJS lexer, which does not see dynamically-assigned
exports. Consume the
subpath exports below by their published specifiers; the runtime resolves them
through the package exports map. The Next.js host bundles its own client/server
code with moduleResolution: "bundler" and consumes these ESM packages directly.
Subpath exports
Most consumers should target the narrowest entry point. The root barrel is a
convenience re-export of the server-side kernel (contracts, registries,
binding, runtime, access, policies, source) — it is deliberately NOT a superset:
paths, testing, web-common and client are absent from it entirely, and
of validation and data it carries only part of each subpath's surface. Import
by subpath and the resolution is unambiguous.
| Entry | What lives here |
|---|---|
| @neuralis/package-system | Convenience: re-exports of every subpath below |
| .../contracts | All TypeScript types — PackageDefinition, contributions, lifecycle, sessions, connector port, result bound; ProjectServiceFactory.getExisting observes cached/pending services without building or touching LRU — plus the kernelTunables seam (setKernelTunablesSource/kernelTunable): host-injected LIVE knobs for kernel operational defaults, typed named fields only (the kernel never learns platform-config key names; agent-core declares the backing keys). Absent/throwing source ⇒ undefined ⇒ hardcoded defaults, so standalone consumers (neuralweb) work with no host wired. Also the operator-endpoint contract (endpoints.ts, see Operator endpoints below) and the outbound header NAME/VALUE rules it shares with remote MCP assignments (outboundHeaders.ts) |
| .../paths | resolveNeuralisHome, resolveProjectRoot, resolveProjectDataRoot, resolvePackageDataDir, isPathInside, NeuralisRoots; discoverRuntimeStack, discoverOsMounts, realProjectRoot, resolveHostPath, resolveContainerPath, protectedRootPaths, RuntimeStackInfo, OsMount |
| .../policies | URI policy evaluator (evaluateUriPolicy, evaluateSourceLevelUriPolicy, classifyUriSubtree, evaluateUriPolicyFloor, resolveRoleLayer, assertUriCapability, mergeBaselines, …) + UriPolicyDeniedError. The ONE evaluator — brain-core's permissionResolver is a delegating adapter over these, not a second implementation |
| .../validation | validateManifest, validateContribution, validateToolSchema, validateXNeuralis (+ getUnknownXNeuralisKeys), the shared validateRequiresShape, validateWorkflowTemplate + the reusable AJV fragments from workflowSchemas (workflowScheduleConfigSchema, workflowTriggerSchema, workflowExecutionPolicySchema, workflowInstructionTemplateSchema, sessionSetupSchema), and the canonical surface-asset layout rule (resolveSurfaceAssetEntry, canonicalSurfaceAssetRoot, isSafeSurfaceIdSegment, isAbsoluteAssetUrl, normalizeRelativeAssetUrl, isHtmlEntryPath, SURFACE_ASSET_ROOT_PREFIX, SHARED_ASSET_DIR) |
| .../tool-name | TOOL_NAME_PATTERN / isWireSafeToolName: the ONE zero-import tool-name predicate, safe in both server and browser graphs. The validation barrel reaches Node filesystem code and the client barrel reaches React hooks; mixed consumers use this direct leaf export. |
| .../access | hasFeature, requireFeature, meetsRequires (the ONE shared requires.features visibility predicate — S2), rolePriority / canAssignRole / validateRoleAssignment / isValidRolePriority (the S4 ordinal role-priority gate — lower = stronger, declared range 1..99), isPlatformTierFeature (the platform.* tier LABEL — never a gate), the catalog collectors collectAllFeatures / collectFeatureMetadata / mergeDefaultRoleGrants, the derived buildFeatureConsumersIndex / summarizeConsumers (which contributions a feature id gates — computed, never stored on a manifest), PackageFamilyResolver, PackageAccessCatalog. See src/access/README.md |
| .../paths | Runtime-stack + home resolution, and containment.ts — the ONE path-containment primitive (resolveContained/…Sync/…Lexical, containedOrThrow, openContainedForRead/ForWrite/openContainedHandle, listContained/…Sync, the derivations withSuffix/sibling/child/parentOf/ensureDir/ensureDirSync, the guards assertPathSegment/assertPathSegments/isSafePathSegment/isSafePathSegments, the branded ContainedPath + ContainmentDenial). See Path containment below |
| .../data | FileStore, JsonlAppender, PackageLogger — file-backed primitives — the crash-durable write primitive (durableReplaceFile & co.), the data-format ledger (declareDataFormats/claimDeclared/claimDataFormat/assertRecordFormat) and checkpoints (createCheckpoint/listCheckpoints/restoreCheckpoint/pruneCheckpoints), plus FsWatchHub (D6): the ONE shared, ref-counted chokidar-v5 filesystem-event substrate. Full contract under Data primitives below (one watcher per absolute root, coalescing, ignored push-down, classified overflow, bounded reconnect); the hub knows ONLY absolute OS paths — URI mapping, exclude semantics, trust and scoping are the consumer's job (brain-core subscribeEvents today; packages:// hot-reload later) |
| .../source | Generic source vocabulary — the slug helpers (parseSource, normalizeSource, deriveSourceKind, validateSource, resolveLocalMounts) and the sourceConfig types + validateSourceConfig (SourceConfig, the SourceConnection union, SyncConfig/SyncTrigger/SyncStatus, HierarchicalPermissions) — plus packageLayout — the ONE shared package-layout rules module: the trigger-category table (categorizeContributionPath), the source-package grouping rules (detectPackageRoots), the marker dot-dir set (ALLOWED_DOT_DIRS), identity-file recognition, skill-bundle ordering/caps, the minimal frontmatter parser (a - item list indented or at its key's own indent, [a, b] flow lists; a # comment in a list position is dropped through stripYamlComment, which agent-core's activation parser shares, while a plain scalar keeps its # … text). Consumed by agent-core boot discovery, the brain-core sync indexer, and the query-time source-package grouper — layout recognition must never diverge |
| .../testing | Mock sessions, mock dispatchers, connector compliance helpers, tempDir, and assertApiMirror — the compile-time drift guard for a cross-package structural mirror |
| .../web-common | The platform's single SSRF-guarded outbound HTTP stack. Transport: safeFetchText / safeFetchJson / safeFetchBytes over ONE core — a DNS-pinned per-request undici dispatcher (requests go through undici's OWN fetch), manual redirects re-validated per hop, a streaming byte cap, and a whole-operation timeoutMs (it bounds the entire call including every redirect hop, not each hop separately). HTTP ≥ 400 bypasses the content-type gate so rate limits surface as status, not type errors; the gate itself is prefix/suffix tolerant (text/*, application/*+json) and treats a MISSING content-type as allowed — it is ingestion policy, not an SSRF floor. safeFetchBytes returns the raw Uint8Array and is the only path that survives binary content (PDF, images); safeFetchText decodes on top of it. The byte cap is a scalar with an optional per-type override, maxBytesByContentType — DATA, not a callback (a callback can throw mid-fetch or answer NaN, and bytesReceived > NaN is permanently false, i.e. no cap at all), matched with the same wildcard matcher as allowedContentTypes, resolved on the FINAL hop from the served type and used by BOTH the Content-Length pre-check and the streaming cap; a non-finite/non-positive value and an ABSENT content-type both fall back to the scalar, so a ceiling is never raised for a type the response did not claim. The WASM guest path deliberately does NOT get it: createGuardedFetch holds ONE scalar cap, stated once as MAX_GUEST_HTTP_RESPONSE_BYTES and fed to both the fetch and Extism's own readBodyUpTo. Defaults resolve explicit option ?? kernelTunable('webFetch…' / 'webToolsUserAgent') ?? hardcoded via the contracts kernelTunables seam. Abandoned responses are cancelled and the dispatcher is destroy()-ed (not close()-d — close() waits for the very request the caller just gave up on). Policy: networkPolicy validates private-IP / port / redirect targets. IPv6 is judged on the parsed ADDRESS, never on its spelling — an embedded IPv4 (v4-mapped ::ffff:… in either the dotted or the compressed-hex form the URL parser produces, the deprecated v4-compatible form, and the NAT64 64:ff9b::/96 prefix) takes the IPv4 verdict, and anything unparseable is blocked. A mixed DNS answer FILTERS the private addresses and pins the first public one (selection is the enforcement: the dispatcher pins that IP and every hop re-validates); zero survivors still fails closed. Ports default to [80, 443, 8080, 8443]. The outbound host-allowlist reads the host-injected setOutboundAllowedHostsSource hook — globalThis+Symbol.for anchored — so the webToolsAllowedHosts admin key applies LIVE, env WEB_TOOLS_ALLOWED_HOSTS as provider-less fallback; that knob only ever NARROWS. The ONE widening knob is WEB_TOOLS_ALLOW_SHARED_ADDRESS_SPACE, a boot-time env constant (deliberately not platform config — see below), default OFF, which opens the CGNAT 100.64.0.0/10 and benchmark 198.18.0.0/15 ranges for deployments behind fake-IP VPN resolvers. Vocabulary: ranking (90-day freshness decay with a per-call freshnessHalfLifeDays override, domain-authority prior, GEO/AI-slop heuristics), records (trimText for snippets, clampText for document bodies), web tool contracts + the structured WebToolError vocabulary — where a blocked target reports host_not_allowed / port_not_allowed specifically but a private address stays a generic blocked_by_policy, so the tool cannot be used as an internal-hostname oracle. Classification reads the error's cause chain as well as its message (undici reports a reset, a connect timeout and a certificate failure alike as TypeError: fetch failed), and feeds the branch tests only — never a model-visible string. TLS_ERROR names a certificate failure without naming the certificate or the host, and is reachable only for a target that already passed the address floor. Guest egress: createGuardedFetch({policy, maxBytes, timeoutMs, maxRedirects, networkRetries?, userAgent?, onResult?}) returns a typeof fetch façade over the same core for callers that must be denied SSRF reach but cannot survive an exception — the WASM sandbox wires it as Extism's fetch option. It takes the COMPLETE OutboundPolicy as a parameter (so an operator-designated destination stays expressible and the private-address rule is never restated), validates the verb against an allowlist, coerces and VALIDATES caller headers (field-name token charset, no CR/LF, framing headers dropped — undici throws on the rest), and maps every failure onto a status: policy/DNS refusal 403 with one generic body, 405, 413, 502, 504. Operator-endpoint egress: resolveOperatorEndpointEgress(entry, {apiKey?}) (see Operator endpoints below) is the streaming counterpart for a platform.config-declared endpoint — same resolver and private-address floor, but its own policy and no body buffering. Image bytes: imageSniff holds the ONE raster magic table (RASTER_MAGIC_PREFIXES, MIN_MAGIC_BYTES) behind two deliberately different questions — the permissive matchesMagic(mime, bytes) (a known raster type its bytes contradict; an unknown type PASSES — the stored-mime normalization depends on that) and the strict sniffRasterImage(bytes) (PNG/JPEG/WebP from the bytes alone, null for everything else, SVG and GIF included) with APPEARANCE_IMAGE_MAX_BYTES (256 KiB, a code floor) for appearance pictures. Server-side only — it lives in the kernel so the web tools, the WASM guest and the future web:// source connector share one fetch path. Not re-exported from the root barrel. |
| .../icons | The platform icon library — ZERO imports, read by the validators, the authoring preflight and every picker: ICON_LIBRARY (~270 curated lucide names, each with a category and search keywords), ICON_CATEGORIES, ICON_ALIASES, normalizeIconName (PascalCase, lucide-kebab or an alias → the library name, else null), isLibraryIcon, unknownIconFinding (the ONE warning sentence — an unknown icon is a load WARNING, an ERROR only in check-package.sh). See Icons and appearance below |
| .../client | createPackageApi + the React PackageApiProvider / usePackageApi / usePackageContext and the imperative accessors (getPackageApiImperative, tryGetPackageApiImperative, tryGetPackageApiImperativeAsync), the client-side host registry (createPackageHostRegistry / PackageHostRegistryImpl), the icon library and its components (ICON_LIBRARY/ICON_CATEGORIES/ICON_ALIASES/normalizeIconName/isLibraryIcon, ICON_MAP / Github / resolveLucideIcon / resolveIconWithFallback), the ONE appearance picker IconPicker (+ AppearanceValue, browser-side downscaleImageFile, and the zero-import APPEARANCE_IMAGE_MAX_BYTES), the agent appearance authority (AGENT_COLOR_PRESETS, resolveAgentIcon, getDefaultAgentColor, resolveAgentColor), the generic responsive hook (useContainerWidth, a default 900 px wide threshold with 60 px hysteresis and one observer per root), the Skill launcher family (SkillLauncher, SkillButton, useSkillHandoff, consumeWidgetNav, WorkspaceHostPortProvider, useWorkspaceHostPort — see below), and a pure re-export of the client-safe access primitives (hasFeature, rolePriority, isBuiltinRole, canAssignRole, isValidRolePriority, BUILTIN_ROLE_PRIORITY, MIN_/MAX_/WEAKEST_/DEFAULT_CUSTOM_ROLE_PRIORITY) so package UI applies the SAME rules the server enforces instead of hand-rolling a second copy. Package UI must import them from HERE, not from .../access (that subpath also pulls PackageAccessCatalog/PackageFamilyResolver, which are not vetted for a client bundle). TOOL_NAME_PATTERN / isWireSafeToolName remain UI re-exports of the zero-import validation/toolName.ts leaf. Mixed server/browser consumers, including the per-run tool catalog, use .../tool-name directly: this UI barrel reaches React hooks and is invalid in a React Server Component graph. The .../validation barrel reaches node:fs and is invalid in a browser graph. Both failures escape type gates. Keep every module re-exported in this block zero-import. UI is never the floor — the server re-checks every one. The barrel also carries the chat-card contract (registerCardComponent & co. with setFallbackCard, CardProps/ToolCallData/CardStatus, cardHeights, composerContracts, cardTypeClaimedByTool, the pure tool-input readers, row identity/disclosure state, and CardShell + the live-motion primitives LiveSpinner/LivePulseDot/ToolStatusIcon) — re-exported by agent-core's chat cards so every package shares the kernel chrome and one React context — and externalFrameProfile (+ EXTERNAL_FRAME_SANDBOX / OPAQUE_FRAME_SANDBOX), the ONE predicate that gives a first-party frame on a foreign https: origin its own-origin sandbox (never allow-top-navigation*) and every other frame the opaque one. Because this subpath is a shared-module table entry, every package UI module reads the host's ONE instance of it |
| .../bridge | Cross-widget state bridge (set/get/subscribeCrossWidgetState) |
| .../client/shared-modules | The ONE shared-module table (SHARED_MODULES = tier 1 only: React ×5 + @neuralis/package-system/client, provided by the host; tier 2 is PACKAGE-DECLARED — app.module.provides, subpaths of the declaring package's own name, declareSharedModules), read by BOTH neuralis-build ui (each id becomes a globalThis shim, never a bundled copy) and the host's module loader (publishSharedModule, planUiModuleInstall — providers before consumers, a missing provider skips the consumer). Also the ONE host-API version (UI_MODULE_HOST_API_VERSION) and the build record's format and parser (UiModuleBuildRecord, parseUiModuleBuildRecord; reactMajor is absent only for a bundle that reads no React id, and the host refuses a React-reading record without one as react-major — a module built before the field existed is rebuilt with build:ui after a host/kernel upgrade, by its owner when the package ships it prebuilt), and the ONE verdict both the image build and the host apply to a record, checkUiModuleBuildRecord (+ resolveBundleReactMajor), under ONE vocabulary UiModuleRecordRefusal (not-built · shared-imports-unreadable · host-api-version · react-major · no-install-export) — the last read off the bundle against UI_MODULE_INSTALL_EXPORT, the install<Name>HostComponents name the host loader attaches through. Zero imports; a second, different instance of a shared id THROWS |
| .../theme.css | The workspace @theme design tokens (dark defaults). The union sheet's prelude imports it, so a token change reaches every runtime-attached package UI with no package rebuild; the host's build sheet still carries its own @theme copy, and a test pins the two equal |
| .../runtime/wasm, .../runtime/mcp, .../runtime/wasm-bridge, .../runtime/wasm-build, .../runtime/ui-build, .../runtime/ui-sheet | Lazy-loaded sandbox runtime and UI build tooling — split out so Next.js bundles do not pull esbuild + @extism/extism native binaries by accident. ui-sheet (compileUnionSheet) takes its Tailwind engine by INJECTION, so the kernel itself carries no native CSS dependency |
| .../pdk-guest | The guest PDK a WASM _packages/ build links — node-free (platform: 'neutral') wrappers over the 8 host functions (ctx.data.*, ctx.callRoute cross-package route dispatch with the caller's host-threaded real session, the ctx.fs.* sugar over brain-core's file routes, ctx.context/ctx.log, and the currently-inert ctx.config ({}) / ctx.emitEvent (no-op)), the makeToolCtx/makeEnsureInit factories, and the dispatchTool/dispatchRoute the generated handleTool/handleRoute delegate to. The build aliases this specifier to the installed package-system PDK (the data zone has no node_modules). Those eight are the complete set of NEURALIS capabilities a guest can reach — say it that way, not as "the guest's entire import table". A built guest also links the toolchain's WASI preview-1 imports (measured: 12, via Node's node:wasi), so the stronger claim is false; it is not a hole, because that surface is instantiated with no args, no environ and no preopened descriptors, but an invariant stated too strongly is worse than a bounded one. |
Why the shared-address-space switch is an env constant and not a config key. It is the only
knob here that WIDENS the SSRF floor, and it is read once at module load. Platform config would be
the wrong home twice over: platform.config PATCHes the platform-global platform.json, so any
role an owner granted that feature could loosen the whole deployment from inside the app; and the
PlatformConfigStore cascade resolves file-override → envFallback → default, so a platform.json
PATCH would beat the operator's env value even if it were only declared as a fallback. The blast
radius is deployment-wide, not web-tools-wide: isBlockedIp sits under resolveAndValidateTarget,
which every safeFetch* caller passes through — including agent-core's git OAuth token exchange.
The wasm/mcp entry points are runtime sandboxes; their types live in
contracts. Import the runtime entry points only from server-side
bootstrap code that explicitly needs the sandbox implementation. The host-side
WASM route gate (createWasmRouteDispatcher/evaluateWasmRouteWiring/
parseRouteManifest) is pure and re-exported from the root barrel.
Icons and appearance
ONE icon vocabulary serves every surface: icons/iconLibrary.ts (the
.../icons subpath, zero imports) names ~270 curated lucide icons, and
client/iconResolver.ts's ICON_MAP holds exactly those components — a
satisfies Record<IconLibraryName, …> fails the build on a missing or extra
key, and test/icons/iconLibrary.test.ts checks each name against the
installed lucide-react. A manifest may write the PascalCase name, its
lucide-kebab form or an ICON_ALIASES entry; resolveLucideIcon normalizes and
stays MAP-ONLY (no lucide passthrough — a name outside the library is null).
validateContribution (widget/dock/card), validateManifest (team[].appearance)
and validateWorkflowTemplate warn on an unknown name; the package still loads
with the fallback glyph. Adding a name is one row in each of the two files.
Agents use the same library: resolveAgentIcon resolves any library name and
falls back to Bot only outside it (there is no narrower agent list). On the
colour side AGENT_COLOR_PRESETS is the shared deterministic 12-colour
palette, getDefaultAgentColor(index) indexes into it, and
resolveAgentColor(configured, index) gives a configured colour precedence.
IconPicker is the ONE picker (search over names, category labels and
keywords; a category strip; roving keyboard focus; palette + custom colour; an
optional picture slot that downscales to 256 px and re-encodes in the browser —
convenience only, the server sniffs and bounds the bytes). It never fetches or
persists: the caller owns storage. This module stores no user or agent data;
agent appearance stays in AgentRecord.config.appearance (agent-core) and the
human profile in the host UserStore.
Stability tiers
Annotated on every public type. When you see one of these markers in a JSDoc comment, treat it as a contract guarantee for refactors.
- Frozen —
PackageDefinition.id/name/version,PackageTool.name/inputSchema,PackageFile.{id,path,category,uri,connector,osUri,containerDir}quadruple,RuntimeStackInfoshape,UriPolicyContext,CrossPackageSession,SystemSessionsentinel constants,boundToolResultcap defaults,CONNECTOR_PORT_VERSION = 4,ConnectorPortmethod signatures,PackageRuntime.skillScripts ∈ {'host','wasm-only','none'}(closed enum). - Stable but evolving —
PackageUriPoliciesallowed-prefix list (currentlypackages://<self>/,data://<self>/),ToolRiskCategoryenum,UiElement.kindvariants. - Host → package ports (
contracts/hostPorts.ts) — everyPackageInitContextcarriesconfig(live platform-config read; an unregistered key throws) andplatform(lazy bootstrap facts:runtimeStack,packageUriPolicies,packageSourceDeclarations,machineWrittenPaths,connectorRegistries,hostIdentity— each getter throws before the host published them). Both throwPackageContextDeniedErrorfor a package below host-assigned first-party.hostPorts(members, audit, session resolver, scope-hidden resolver, package maintenance, package diagnostics, agent lifecycle, credential writers, the Codex OAuth credential writer, the trusted app origin, MCP token store, git OAuth, credential store, governance, platform config admin, credential-use admin) is present ONLY on a host-assigned first-party context (hostPortsFor), and a source scan keeps the credential writers inside agent-core, the credential store / credential-use admin / governance / platform-config ports inside admin, and bans aglobalThisfallback beside any port read. The Codex writer is restricted by the host to the one idCODEX_CREDENTIAL_IDand re-derives its four scope tiers on every call; no member takes a credential id or a writer class. OptionalpackageDiagnosticsreads live runtime declarations and aggregate health; the host throws before successful runtime readiness. Consumers keep their feature gates and bounded response projections; declarations grant no authority.BootstrapCompleteContextis the init context itself. - Service contracts (
contracts/services.ts,contracts/runtimeProvider.ts) — a first-party manifest'sprovidesnames what it implements (runtime·session-ticket-verifier·route-token-verifier·agent-directory·channel-gateway·package-source-roots·mcp-server·oauth-callback:<prefix>);runtime/serviceRegistry.tsresolves exactly oneruntimeand onesession-ticket-verifierover the builtin set (0 or 2 is a boot failure, overlapping OAuth prefixes too), and at most one of every other singular contract.route-token-verifier(verifyRouteToken(token, expectedResource)) is what the host catch-all asks for an OAuth bearer on its package-route API — a liveSessionContextor a refusal code; when no provider is present that arm denies; loads the runtime provider (dist/src/runtimeProvider.js, exportruntimeProvider) and serves the others by contract id from the provider'sPackageRuntimeApi.services. TheRuntimeInstanceis the host's only handle: loader port (load/reloadretain the raw manifest id and owning project,unload/start, and load status carries its definition), route dispatch,whenReady(), the discovered realtime channels,health(),notifyCredentialChanged,drainAll(). Channel services expose safe descriptors and authenticated OAuth start; callback services re-resolve the captured project and optionally return channel tokens. The optional singularpackage-source-rootsservice preserves each source's URI/container root, project/user/agent scope, enabled flag, recognition flag and kind for internal runtime activation, andsubscribePackageSourceRoots(listener)tells the host which project to re-list after a committed source change (after the write, never inside it); member listings retain their existing scope/policy gates.
PackageDefinition — the canonical shape
Declared in package.json under the neuralis key. Flat identity fields
- standard capability arrays + optional Neuralis-specific blocks. Field
groups (see
src/contracts/package.tsfor the full type):
- Identity.
id,name,version,description,descriptionForModel,authors,tags,license,icon,homepage,repository. - Capabilities (cross-platform). The complete array set is
tools,commands,resources,hooks, the five first-class file arrays (skills/instructions/rules/agents/docs),workflows,connectors,sources,credentials,configSettings,policies,uriPolicies,team,provides,events— a prose list shorter than this one is stale.commandsis the legacy definition-only form (no model invocation — published to external MCP clients as prompts);workflowsare declarative workflow templates (see below);connectorsregister source KINDS whilesourcesdeclare concrete source INSTANCES of them (see below);credentialsandconfigSettingsare the two DECLARATION arrays — they surface a credential id in the admin catalog and a platform-config key in the admin Config tab respectively, and declaration ≠ grant for both (see below);policiesare tool-call gates anduriPoliciesthe URI access baselines;providesnames the host service contracts a first-party package implements (see Stability tiers above);eventsdeclares the event types a first-party package emits (see Events under Dispatchers). - Neuralis UI.
app.surfaces[]— widgets, dock entries, cards. The legacy flatui[]array is no longer read.app.module({ entry, css?, provides? }, paths package-relative underdist/) is a first-party package's ONE prebuilt browser module;provideslists the tier-2 shared modules it publishes (subpaths of its own id).PackageRuntimeSnapshot.ui(PackageUiAttachments) types what the host's runtime route attaches beside the snapshot — the kernel runtime never sets it.providesandapp.modulecount only on a POST-merge, host-assigned first-party manifest:validateManifestwarns (never errors) on any other tier andrestrictFirstPartyManifestFields(definition, trust)drops them at load. - Requirements.
requires.providesFeatures,requires.defaultRoleGrants,requires.packages,requires.accessFeature.providesFeaturesentries are a bare id string OR the{id,title,description}ProvidedFeatureobject form (C9) —normalizeProvidedFeatures/providedFeatureIdsread both;collectFeatureMetadatamerges the metadata.requires.accessFeature(optional) is a SINGLE feature id that gates the WHOLE package's visibility (base-access): a caller lacking it sees none of the package (validated shape-only — like a routefeature, it may name a feature provided elsewhere). Undeclared ⇒ visible (preserves the import-unchanged floor). A declaredaccessFeatureis auto-registered intocollectAllFeatures/collectFeatureMetadataso it is GRANTABLE in the admin Roles UI (an owner can grant base access to a role) —featureAuditis unaffected (it readsprovidedfrom manifests, not viacollectAllFeatures). The host can ATTACH an additional requirement per-package via a restrict-only override (ProjectRecord.packageAccessFeature); the effective gate (manifest ∪ override, each independently required) is enforced through the sharedhasFeature. A feature's CONSUMERS (which tools/skills/commands/widgets/docks it gates) are NEVER stored on the manifest —buildFeatureConsumersIndex(definitions)derives them at boot/request time (route consumers excluded by design; see its scope note).auditFeatures(@neuralis/package-system/testing) is the C7c lint core: unknown-id + declared-but-unreferenced analysis over collected references. - Operational metadata.
source(where the package came from),runtime(how it executes),access(trust + external policy),meta(hosts, families, distribution),dataLayout(the package's declareddata://layout —directoriespackage-level,agentDirectoriesper-agent — validated fail-closed byvalidateManifest(segment guard + a rendering charset, no duplicates, unknown keys rejected) because agent-core renders it VERBATIM into every agent's cached<runtime_stack>prefix as adata_areasrow; the zone root issanitizePackageId(./paths), never the id).dataLayout.machineWritten[]is the one layout field that is NOT rendered:{ path, class }rows naming the files the package writes on its own (metrics·log·runtime-text), each a glob relative to the zone (validateManifestrefuses a leading/, empty or../.segments and any character outside[A-Za-z0-9._*-]).collectMachineWrittenPathsanchors them to the zone (trusted-or-above packages only) and ridesctx.platform.machineWrittenPaths();seedExcludedMachineWrittenGlobskeeps the classes inSEED_EXCLUDED_MACHINE_WRITTEN_CLASSES(metrics), which a NEW project'sdatasource seeds intosync.excludeonce, at creation — a preference seed, never a floor, never re-applied to an existing config. - Lifecycle.
lifecycle— host callsinit/start/onBootstrapComplete/ drain/stop/provisionProject/deprovisionProject/onPrincipalRevoked/ onCredentialChanged/health/handleToolCall/handleRoute.onCredentialChangedis value-free ({ credentialId, scope, change }), fanned out bounded per package;healthanswers the closedPackageHealthReportvocabulary (ok · starting · degraded · failed+ closed reasons, never free text), validated bynormalizePackageHealthReport;drainruns once at shutdown beforestop. Runtime-only, never serialized.onPrincipalRevoked(PrincipalRevokedEvent)is the ONE revocation signal (disabled/deleted/sessions_resetuser-wide,membership_removed/project_archivedwith aprojectId): a package closes the live state it holds for that user through its existing stops — it never decides access, since a revoked principal gets no session from any producer. The loader fans it out with a per-hook timeout.deprovisionProject({ projectId })isprovisionProject's pair for a PERMANENT project delete: the package removes every resource it keys by the project outside the project's data tree (containers, volumes, vector points), idempotently, and THROWS when anything is left. The loader walks the hooks in sequence, each bounded by a timeout, and the first failure stops the host's purge before any file is removed with aProjectDeprovisionErrornaming the package. Only an in-process first-party lifecycle can implement it.
The id is checked at validation time against a reserved-route set
(widgets, dock, install, query, command, commands, publish,
rescan, runtime, stream, plus templates as a deliberate
forward-reservation for the planned publish surface) so a package id never
shadows a literal segment in the Next.js host route tree — a derive-and-verify
test (reservedPackageIds-style, in validation.test.ts) reads the host route
dir and asserts every live segment is reserved. The id must also be a
well-formed lowercase npm name (name or @scope/name); .. segments, extra
path separators, and the <self> token are rejected.
PackageFile — first-class markdown contributions
Markdown contributions (skills, instructions, rules, agents, docs) are
first-class capability arrays on PackageDefinition — skills,
instructions, rules, agents, docs — exactly like tools. Each element
is a PackageFile carrying a required category ('skill' |
'instruction' | 'rule' | 'agent' | 'docs') set authoritatively by the loader
from the contribution's flat source folder. category is deliberately NOT
called role — role denotes the user rank (owner/admin/member)
everywhere else in the system.
The runtime snapshot mirrors this with five typed arrays
(snapshot.skills/instructions/rules/agents/docs). Category-specific
consumers read the array they need directly (snapshot.agents); only genuinely
cross-category features (the per-agent active-file catalog, the <packages>
overview) fan over all five via the allFiles(snapshot) helper.
Model-facing descriptions are contracts
The package overview always exposes each visible package's model description and each visible skill's frontmatter description; the skill body arrives only after activation. Author a skill description as a routing contract: the outcome it enables, the natural request or failure state that should trigger it, and the decisive distinction from neighboring capabilities. Keep the procedure in the body.
Visible tools are sent separately with their root description and full input schema on every model step. The root description owns tool selection plus the immediate recovery/safety boundary; property descriptions own only input-specific meaning. Do not duplicate a long skill/rule procedure into the schema, but do not treat descriptions as display copy either—the model operates from them.
files — the skill bundle enumeration (skill scope = the whole directory)
A skill's SCOPE is its full directory (dirname(path)), not just the SKILL.md
entry file. PackageFile.files?: string[] is the dir-relative enumeration of
the bundle, discovered at scan time: SKILL.md first, then scripts/**,
references/**, assets/**, then any other skill-protocol file in code-unit
order; capped at 64 entries (the SKILL.md entry has a reserved slot).
Code-unit order is compareCodeUnits (.../source), the kernel's exported
locale-free string comparator for any order that reaches the model's prompt:
localeCompare follows the runtime's ICU data, so an order built on it can
move between images.
Skill-only today — flat categories (rules/instructions/agents/docs) carry no
enumeration. There is no separate dir field: the bundle directory is always
dirname(path) ('.' for a root-SKILL.md package whose root IS the skill).
files is display/advertisement metadata (UI folder rows, the
<packages> overview [files: …] suffix) — never a permission boundary; the
S1 shell gate's active-SKILL_DIR allowance covers the directory itself.
requires — per-contribution feature visibility (S2)
PackageFile.requires?: { features?: string[] } declares which features a
caller must hold to even see the contribution — parity with the same field
on PackageCommand / WidgetUiElement / DockUiElement. The single
authoritative predicate is meetsRequires(session, requires)
(access/meetsRequires.ts, built on hasFeature): no declaration ⇒ visible,
'*' passes everything, otherwise EVERY listed feature must be granted (AND).
A failing file is hidden entirely (prompt injection, <packages> overview,
skills accessor, files catalog, client snapshot) — never rendered as
[OFF]/locked, so non-holders cannot infer it exists. Execution stays
independently gated (route feature, shell uri-policy, credentials).
Declaring a feature id no package provides makes the file wildcard-holder-only
('*') by construction — an intentional deny-by-default property. Authors set
it via the flat requiredFeatures: [a, b] frontmatter key (the discovery
parser does not support a nested requires: form). Surfaces must call
meetsRequires, not inline their own copy of the wildcard/AND logic.
Tools carry the same requires (S2b). PackageTool.requires?: { features?:
string[]; escalations?: string[] } is the ONE declarative source for tool
feature-gating — it replaces the legacy manifest policies[] kind:'access'
gate (that compile path is removed). A caller lacking a required feature never
sees the tool in the model-facing catalog (in-stream + hosted MCP) nor in the
client snapshot, and a dispatch is rejected with a generic, non-leaking
message. escalations is catalog/lint metadata only — additional features
a handler may demand from its INPUT at runtime (e.g. machine_use's
drive-class actions need machine.drive beyond the machine.read baseline);
it does NOT gate. The shape validator is the shared validateRequiresShape
(one helper for PackageFile / PackageCommand / PackageTool).
The (uri, connector, osUri, containerDir) quadruple (W4E)
Every contribution also carries up to four coordinates:
uri— the fs-tool address (e.g.packages://agent-core/skills/ops/SKILL.md,data://repo/notes.md,brain://…). Undefined for builtin packages whose files are read directly from the container image.connector— the connector kind that serves the URI ('local' | 'brain' | 'webtop' | 'host' | …) — there is noneuralisconnector kind; theneuralis://mount surfaces as alocal-kind source. Even whenuriis undefined this still reflects the disk layout ('local').osUri— host-side absolute path produced byconnector.resolveOsUri(uri)when supported. UI + vectors consume this to render an editor-pasteable path. Undefined for remote / Qdrant-backed files.containerDir— container-internal absolute path. Always set for builtin and_packages/files because they are present inside the container by image snapshot or bind-mount. Source-fed files setcontainerDironly when the connector islocal-kind AND the source mount maps into the container.
The skill-execution substrate (execute action="skill") substitutes
${SKILL_DIR} from containerDir and ${SKILL_URI} from uri. Builtin
defaults: uri/osUri undefined, containerDir filled. User _packages/
defaults: all four filled.
PackageWorkflow — declarative workflow templates (contracts/workflow.ts)
WorkflowExecutionPolicy includes optional modelId, reasoningEffort, contextWindowOverride, and fastMode alongside runtime/concurrency limits. Authored templates and persisted workflow entries are null-free: omission means inherit from the target agent at fire time. Runtime update schemas may accept null as a patch instruction to clear an override, but that sentinel is never stored or admitted by template validation. false is a real fast-mode override, not omission. Capability compatibility belongs to the consuming runtime because the kernel owns the shape, not any provider catalog.
workflows/*.json files are a first-class parallel typed array on
PackageDefinition (workflows?: PackageWorkflow[]) — the exact tools
precedent (JSON folder → AJV → typed array), deliberately NOT a sixth
PackageFile category: templates are structured JSON that must never be
prompt-injected, so they are absent from FileArrays/allFiles() and the
runtime snapshot exposes them separately
(snapshot.workflows: WorkflowRuntimeDefinition[], which is where
packageId lives — PackageTool parity).
contracts/workflow.ts is THE one owner of the workflow trigger/schedule
union (WorkflowTrigger/WorkflowScheduleConfig) and
WorkflowExecutionPolicy: the agent-core engine re-exports these for its
persistent WorkflowEntry, and the AJV fragments in
validation/workflowSchemas.ts derive from the same module — no second
union copy anywhere. WorkflowTriggerTemplate is the authorable form
(id optional, derived type), and engine-runtime trigger fields
(nextFireAt, fireCount, completed, …) are rejected by the template
validator.
Template fields: identity/visuals (id from the filename, title,
description in the agentskills.io "what + when" style, icon, color,
group — NOT category, which is reserved contribution vocabulary),
defaultInstructionTemplate (≤10k), inputs (gh-aw
workflow_dispatch.inputs field names: description/required/type
string|choice|boolean/default/options — install-form generation +
install-time {{KEY}} substitution; the engine fire-time placeholders
WORKFLOW_DIR/WORKFLOW_STATE_URI/FIRE_TIME are rejected as input keys),
defaultTriggers/defaultStateMode/defaultExecutionPolicy,
defaultSessionSetup (the SessionSetup goal + loop working-state seeded into
every run's <session> — a formalized goal.objective + checkable
doneConditions, a loop bound until: goal|manual + an enforced maxSteps,
and an evolve knob; loop.schedule is display-only and never authored — the
runner mirrors the trigger. SessionSetup is shared with conversation
settings.session, and the length caps MAX_SESSION_* are a hardening floor),
channelKinds/requiredCredentials, the delivery block (OpenClaw field
names — every mode authorable since the channel gateway landed; same for
message/webhook defaultTriggers, with a
message trigger's unknown bindingId fail-louding at install), and
requires.features (S2 — the ONE meetsRequires predicate, applied by EVERY
listing surface itself), and the optional revision? (positive integer, missing
⇒ 1). validateWorkflowTemplate rejects a zero/negative/fractional/string
revision. Because an install COPIES the instruction template into the persisted
WorkflowEntry rather than live-linking it, revision is how a package signals
that already-installed copies are stale: the agent-core engine records
templateRef.revision at install and pauses an out-of-date active/draft copy
(template_upgrade_required) instead of silently rewriting the creator's
instruction. Clearing that pause is an explicit, authorized act of the consumer's own —
agent-core upgrades the instance IN PLACE, keeping its id and therefore its run
history and state folder; the kernel's part ends at the revision number. The contract also owns WorkflowDeliveryPolicy
(per-entry announce/none, streaming reserved) and the member-grade self-scope
credential-write vocabulary in contracts/credentials.ts. The RESERVED id
classes are three DISJOINT ones (each with its OWN structural predicate, none
interchangeable) reached through TWO ports — and the file carries two further
host-injected ports that must never be folded into them
(MemberCredentialWriter, GitOAuthAppConfigPort, both below), for four in
total:
- the
SelfScopeCredentialWriter— the UNIFIED port (Phase B, 2026-07-13: the 2→1 collapse of the two line-for-line-identical narrow ports, the oldScopedCredentialWriterfor channels +GitCredentialWriterPortfor git). One interface,writeUserScoped(scopeClass, userId, id, value)/deleteUserScoped(scopeClass, userId, id), whereSelfScopeClass = 'channel' | 'git' | 'workflow'. It carries the member-grade classes: the channel whitelist (CHANNEL_SELF_SCOPE_CREDENTIAL_IDS+isChannelSelfScopeCredentialId); the git class (git.<host>.pat|.oauth.accessToken|.oauth.refreshToken, built bygitPatCredentialId/gitOAuthAccessTokenCredentialId/gitOAuthRefreshTokenCredentialId, matched by the structuralisGitSelfScopeCredentialIdoverGIT_HOST_CHARSET^[a-z0-9.-]{1,255}$, disjoint from mcp/channel/data.github); and the workflow class (workflow.<workflowId>.signingSecret, built byworkflowSigningSecretCredentialId, matched by the structuralisWorkflowSigningSecretCredentialIdwhose workflow-id charset is DOT-FREE so the family can never alias another).scopeClassselects the PREDICATE + AUDIT TAG ONLY — the write is ALWAYSusers/<userId>(no scope-kind arg ⇒ SHAPE ≠ AUTHORITY). The per-class predicates stay disjoint and are re-enforced by the host impl, which owns the class registry as CODE — the contract carries only the type + predicates, no manifest field feeds a self-scope class. - the
McpTokenStorePort— kept SEPARATE (M2), NOT folded into the unified writer: the outbound-MCP family carries anMcpCredentialScopeRef(user|project|global) scope arg +readScoped, so folding it would reintroduce a scope-kind arg and break SHAPE ≠ AUTHORITY. The family is THREE structurally distinct classes, each with its own predicate and a deliberately different DESTINATION — never merge them:mcp.<serverId>.<accessToken|refreshToken|clientId|clientSecret|expiresAt>(isMcpSelfScopeCredentialId, OAuth tokens and the persisted access-token expiry),mcp.<serverId>.env.<ENVKEY>(isMcpSidecarEnvCredentialId, a local sidecar container's env), andmcp.<serverId>.header.<KEY>(isMcpHeaderCredentialId, an outbound HTTP header transmitted to a caller-configured external URL). All three parse STRUCTURALLY — serverId up to the FIRST dot, then a literalenv./header.segment — so a serverId literally namedenvorheaderstill parses; never split on.env./.header..isReservedSelfScopeCredentialIdlists all five predicates EXPLICITLY rather than relying on fallthrough.
RED LINE (JSDoc-pinned on each port): users/<id> writes only for the self-scope
writer, predicate re-enforced at the port, id-only audit, ONE call-site per class
— never widen the prefix/suffix set, never merge two classes into a super-predicate,
never add a second call site. The git class also owns the pure HTTPS-Basic username
mapping — GitHostType × GitCredentialType → resolveUsernameConvention →
GitUsernameConvention → resolveUsername (NEVER empty; token is always the
password).
The remaining two ports are deliberately outside the class-dispatched writer.
MemberCredentialWriter (writeUserScoped / deleteUserScoped /
listUserScoped) is the member-BYOK port: the ONE broad writer, and a
DENY-list — it accepts any id that passes isValidCredentialId and is NOT
isReservedSelfScopeCredentialId, i.e. exactly the complement of what the
self-scope writer allows. Mixing the two polarities into one port would need a
mode: 'allow' | 'deny' switch, which is floor-bug fuel, so it stays separate;
the users/<userId>-only SHAPE ≠ AUTHORITY rule binds it identically (no
scope-kind arg, ever). GitOAuthAppConfigPort (readClientId /
readClientSecret) is READ-only and hard-wired to the two fixed GitHub OAuth
App ids — resolved through the standard agent→project→user→global cascade,
NOT global-exact. It is deliberately not a generic caller-supplied resolver, so
it can never become an arbitrary-id read primitive; the git-remote OAuth flow
reads clientId at the session-derived scope and clientSecret at the flow's
CAPTURED scope.
Alongside the member-grade WRITE vocabulary, contracts/credentials.ts owns the
package DECLARATION vocabulary: PackageCredential (an entry of the
optional PackageDefinition.credentials?: PackageCredential[] array — exactly
{ id, label, category?, oauth? }, where oauth is a DISPLAY hint), the
CredentialSource union 'package' | 'skill' | 'manual', and the
CredentialCatalogEntry shape the admin UI + manage-credentials skill read
(never carries the secret value, only hasValue + declaredBy +
declaredByTrust + the oauth display hint + an optional status?:
CredentialStatus — { kind: 'valid' | 'expiring' | 'expired' |
'refresh_failed' | 'unknown'; expiresAt?; lastRefreshAt?; note? }, the
lifecycle of an OAuth-class value that the package OWNING the token computes;
the kernel carries only the type, and note never holds a token fragment, an
identity-provider body or a store path). description / isSecret /
isRequired / scopeHint / envVar were RETIRED (CRED-TAIL INC 7): declared
but read by nothing, so they taught a contract the system did not enforce — a
manifest still carrying one loads with a warning. The kernel exports
isValidCredentialId — the SINGLE
charset gate (^[A-Za-z_][A-Za-z0-9_.-]*$, ≤ 128, no slash/leading-dot, since the
id becomes an <id>.enc.json path segment) reused by BOTH the manifest
credentials[] validator and the member BYOK writer. validateManifest runs a
light hand-written block over credentials[] (non-empty id/label, id passes
isValidCredentialId, no duplicate ids, category/oauth type checks) plus ONE
security gate: a RESERVED self-scope id — the git (git.<host>.pat /
.oauth.*), channel, and MCP (mcp.<server>.*) classes, i.e.
isReservedSelfScopeCredentialId) — may only be declared by a first-party
package. Those classes have host-owned resolution semantics; a _packages/
drop squatting git.github.com.pat under its own label would be
indistinguishable in the admin catalog, and once an owner filled it in at global
scope the token would cascade-resolve for every member without their own. The
gate is deliberately NOT the config-style blanket trust filter: a package's OWN
namespace is declarable at any trust tier (the import-unchanged floor), because
otherwise an owner could never set it and the package could not work.
Declaration ≠ grant (M1 floor): a credentials[] row is catalog/display
metadata ONLY — it surfaces the id for an OWNER to set and carries NO self-scope
write authority; consumption still resolves against the caller's real scope via
CredentialResolver.
Trust also governs catalog attribution: a first-party declaration wins the
label/category/oauth seed regardless of load order, a later non-first-party
declarer of the same id only joins declaredBy, and declaredByTrust carries
the tier per declarer so a UI never renders an untrusted declarer as the
platform. A declared id that nothing resolves is a defect, not a placeholder —
the agent-core credentialDeclarationDrift guard asserts declared ⟷ consumed in
both directions from derived sources.
The READ port is CredentialResolver, and it has exactly ONE working method:
resolveCredential(id, scope), cascade-resolved agent → project → user → global
(most-specific wins). resolveForKind(kind, scope) is a dead stub — the host
returns {}; it survives only as the Phase-C anchor for a future kind→ids
mapping, so a package that builds on it silently gets nothing. There is
deliberately no resolveEnv(key): it used to hand every loaded package —
untrusted _packages/ drops included — an unbounded, un-blocklisted
process.env[key] read (making NEXTAUTH_SECRET and every provider key
readable) through the very port whose purpose is to REPLACE direct env access.
It had zero production callers and was removed; boot-critical infra reaches
packages as typed config (configSettings[] + envFallback), never raw env.
contracts/config.ts owns the parallel platform-CONFIG declaration vocabulary
(tunable-tanager): PackageConfigSetting (an entry of the optional
PackageDefinition.configSettings?: PackageConfigSetting[] array — flat
camelCase key / label / description / type: number|string|boolean|json /
default / optional envFallback / editable / category / number-only
min/max) plus the kernel single-source isValidConfigKey
(^[a-z][a-zA-Z0-9]{0,63}$) and the looksSecretShaped WARN heuristic. The
host PlatformConfigStore is a pure registry+cascade engine (file override →
envFallback → default) with ZERO hardcoded schema: first-party packages'
declarations register at bootstrap, BEFORE the first package init (the
kernel collectDeclaredConfigSettings collector, ./access), the host's own residual
keys via the same mechanism (declarer 'host'). validateManifest runs a
light block over configSettings[] (charset, type enum, default typeof-match,
min<=default<=max, per-manifest dup reject, secret-shaped WARNs on key AND
envFallback name, json-envFallback WARN). Declaration ≠ grant here too —
a row only surfaces the key on the feature-gated admin Config tab — and,
STRICTER than credentials (display-only), config declarations are honored for
first-party packages ONLY: envFallback projects an env var's VALUE into
admin-visible state, so an untrusted _packages/ declaration would be an
env-exfiltration primitive.
Schedule guardrails: maxRuns (cron) and endAt (cron + once) complete a
trigger when hit — schedule-hygiene bounds, NOT a cost-quota system (every
fire already runs under the creator's per-user USD spend limits + rate limits).
Installing instantiates a mutable WorkflowEntry in data:// with
templateRef recorded, creator = the installing caller, status draft.
workflows is in RESERVED_DIRS — nothing under it is ever a markdown
contribution, and source-synced workflows never load.
Source-contributed packages (the third recognition tier — source/packageLayout)
Beside packages:// runtime drops and deps-sourced builtin-class packages, a
synced brain-core filesystem source (repo://, data://, …) can contribute
source packages: groups of indexed markdown contributions recognized from
the vector index at query time. The contract lives in
src/source/packageLayout.ts (pure, I/O-free) and has three parts:
- The trigger-category table (
categorizeContributionPath): which source-relative paths get a contribution category at sync time —SKILL.mdanywhere →skill(plus the legacycommands/*.mdsingle-file form),instructions/**→instruction, the universal identity basenames (AGENTS.md,CLAUDE.md,CLAUDE.local.md,GEMINI.md,.github/copilot-instructions.md) → opt-ininstruction,rules/**+.cursor/rules/*.mdc+.cursorrules/.windsurfrules→rule,agents/**+*.agent.md→agent,docs/**→docs(README/templates excluded), manifests →package(anchor iff content is usable),.mcp.json/hooks.json→connector/hook(indexed only). Patterns match inside the marker dot-dirs (ALLOWED_DOT_DIRS:.claude .agents .cursor .codex .gemini .windsurf .github .opencode .kilo .roo). - The admission rule (
admitsContribution(hit, frontmatter)): the table says WHICH category a path would get; admission says whether the file IS a contribution. A markdown is admitted where the ecosystem protocol makes the filename its identity (identityFromPathon the hit —SKILL.md,commands/*.md, the identity basenames,.cursorrules/.windsurfrules,.cursor/rules/*.mdc) or where its frontmatter declares one (idornameas a non-empty string —hasFrontmatterIdentity). A folder-bound.md(agents/**,rules/**,instructions/**,docs/**) and*.agent.mdwithout either is a plain indexed file: a.claude/agents/references/*.mdreference folder is NOT a dozen agents. This is the first-party loader's rule applied to every package form — the loader (first-party,_packages/drops,recognizesPackagesroots), the brain-core sync indexer (which also re-derives a stale trigger category on the next full sync, payload-only) and the marketplace scanner all call the ONE predicate; an inline copy is a regression. - The grouping rules (
detectPackageRoots): usable-manifest anchor → marker dot-dir (one package per dot-dir —repo://.claudeis ONE package) → category-dirs root → bare-skill dir → the RESTRICTED implicit source-root package (root-level identity files + root-level category dirs only; nested unclaimed nodes stay indexed-but-unmapped). Nearest root wins; grouping is query-time, so layout changes self-heal without index migration.
Two functions answer "is this manifest usable", and they answer differently ON
PURPOSE. isUsableManifestContent (source/packageLayout.ts) is a grouping
heuristic on the vector plane: it requires only that a neuralis key be an
object, deliberately generous so foreign ecosystems (plugin.json,
gemini-extension.json, neuralis.package.json) group correctly. It confers
no trust, no registration, no execution. validateManifest (validation/)
is the kernel gate: fail-closed, run before anything is registered. So a
manifest the kernel REJECTS can still anchor a source-package root (rule
'manifest', precedence 0, above every folder rule) and still supply the
display name that reaches the model's <packages> overview.
Do not "align" them. Narrowing the heuristic to the kernel's rules would
silently stop grouping every non-Neuralis ecosystem tree — a capability cut
disguised as hardening. What the asymmetry requires instead is that the
CONSUMER bound and fold what it renders: the name is read-gated by uri-policy
(a file-denied manifest is refused a content-derived name), and agent-core's
<packages> builder folds the container tag out of it and caps its length.
What sources will NEVER load: code surfaces. No tools, hooks, routes,
lifecycle, team, workflows, or connector registrations come from a synced
source — markdown contribution categories only; package/connector/hook
nodes are indexed and categorized but never executed or registered. Dropped
capabilities are logged, never best-effort-loaded (neuralis-universal
import doctrine).
Tool authoring rules (cross-platform MCP-standard)
Every package tool follows MCP shape. Authoring rules the validator
enforces (validateToolSchema + validateXNeuralis):
inputSchema.$schemaset,inputSchema.titleset,inputSchema.type === "object".- Root
additionalProperties: false. - No legacy
x-tool/x-output/x-validationextensions — only the canonicalx-neuralisblock, whose accepted top-level keys are exactly nine: dottedoperation, explicittransport, and the optionalfamily,annotations,ui,defaults,requires(the S2b feature gate + the non-gatingescalationsmetadata — see above),askOnKeysandconcurrencySafe(below). Any other top-level key is rejected. - No session-context input keys — EVERY
SessionContextfield (the set is derived fromkeyof SessionContext:userId,projectId,agentId,conversationId,requestId,toolUseId,delegateRunId,delegateMode,role,priority,grantedFeatures,spendLimits,llmRateLimitRpm,agentAccess,agentOwnership) plus the bundledsessionname are rejected as top-level input properties. The ban is ROOT-ONLY by design — the same names are legitimate domain fields when nested (atodos[].priorityenum, amembers[].userId), and identity is always read from the verified session, never frominput.foo.userId. The reserved SET is derived fromkeyof SessionContextso it can never drift from the identity type, even though the CHECK is scoped to the root. annotations.category(ToolRiskCategory) is REQUIRED for native tools. Imported tools default to'unknown'+destructiveHint: true.annotations.cancellable(optional boolean) declares that the HANDLER honours the hard abort signal the caller forwards and returns promptly. It is a promise about the handler, not about the dispatch: a declaring tool must be bounded by construction, because the caller then awaits it to settlement with only the trust tier's execution ceiling as a backstop. Undeclared ⇒ the caller abandons its WAIT after the join grace and reports an interrupted result; the call itself keeps running.askOnKeys(optionalstring[]) is the per-key approval posture: the ROOT input keys whose PRESENCE makes a call ask. Fail-closed and trust-blind — a third party declares it exactly like a first-party package, because it can only ever ADD an approval prompt. Rules, all checked at load: a non-empty array of at most 8 distinct strings; every element a ROOT property of the tool's owninputSchema(the caller therefore passesrootInputKeys, and a declaration without them is an ERROR, never a skipped check); never aRESERVED_INPUT_KEYSname;annotations.destructiveHint === true; andannotations.category ∈ {write, execute, network, machine}— elsewhere the consent floor asks about every call, so an inert declaration is refused rather than silently ignored. The guard ENGINE stays input-blind: the CALL SITE computes the effective metadata (agent-core'seffectiveToolPolicyMetadata), reading PRESENCE and never a value.concurrencySafe(optional,trueonly) declares that two calls of the tool may run at the SAME time. Requiresannotations.readOnlyHint === trueand must not sit besideaskOnKeys. Declaration only — what a consumer does with it (agent-core runs maximal runs of adjacent declaring calls together) is the consumer's rule, and it widens no gate.x-neuralis.recoveryis REJECTED — recovery proof is asked of the brain-core mutation-safety policy at runtime, never declared on the tool.
Open-bag normalisation
validateToolSchema walks every nested subschema after the contract
checks pass and injects a default propertyNames constraint on every
open object bag (additionalProperties: true and no existing
propertyNames). The default pattern blocks prototype-pollution and
reserved-namespace keys (__proto__, constructor, prototype,
anything starting with __ or $). Authors who need a different shape
declare their own propertyNames and the normaliser leaves it alone.
The root schema is exempt because the contract requires
additionalProperties: false at the root.
App surfaces
Every UI contribution lives under neuralis.app.surfaces[]. Each surface
declares kind: 'widget' | 'card' | 'dock'.
Renderer × trust matrix:
| Renderer | First-party | Trusted | Untrusted | Notes |
|---------------|-------------|---------|-----------|--------------------------------------------------------|
| direct | yes | no | no | Compiled React; first-party builtins only. |
| iframe | yes
