@package-system

Manifest and distribution

The neuralis block in package.json: identity, trust, features, surfaces, and packaging.

The neuralis key in package.json is the single source of truth for package metadata. It is the only required file in a package — tools, workflow templates, markdown contributions, commands, hooks, and team members are all auto-discovered from the directory contract, and file-based entries override anything declared inline. Routes (routes/*.ts) are discovered the same way and have no inline form at all.

Auto-discovery does not mean unchecked: every declaration the manifest carries is validated before the package registers anything, and a single error rejects the whole package rather than loading it partially. That includes the team-member template paths, which must stay inside the package — see the directory contract for the exact rule.

For builtin-class packages the block is also the discovery signal: the host loads a dependency if and only if its package.json carries a neuralis block. The minimal opt-in is { "neuralis": {} }; a reference or documentation-only package opts out with { "neuralis": { "referenceOnly": true } } so it can sit in the dependency tree without ever loading.

Minimal manifest

{
  "name": "my-package",
  "version": "0.1.0",
  "private": true,
  "neuralis": {
    "id": "my-package",
    "name": "My Package",
    "description": "Human-facing description",
    "descriptionForModel": "LLM-facing: describe USE CASES, not implementation."
  }
}

This is enough for a declarative-only package. Everything else below is optional.

Identity fields

FieldTypeRequiredPurpose
idstringyesUnique identifier. Reserved ids that would shadow a host route are rejected: widgets, dock, install, query, command, commands, publish, rescan, runtime, stream, templates.
namestringyesHuman-readable display name.
versionstringnoSemver.
descriptionstringrecommendedShown in the admin panel.
descriptionForModelstringrecommendedWhat the model reads to decide when the package's capabilities are relevant. It is rendered into the always-on packages overview, truncated at 1,536 characters — the same budget a skill description gets, and for the same reason: it is a routing signal, not documentation.
authors, tags, license, icon, homepage, repository—noStandard metadata; icon is a Lucide icon name.

provides — host service contracts

First-party packages may declare provides using the closed service vocabulary: runtime, session-ticket-verifier, agent-directory, channel-gateway, package-source-roots, mcp-server, and oauth-callback:<prefix>. Runtime and ticket verifier are required singletons; the other service families are optional singletons. OAuth prefixes are lowercase, end in _, have 3–32 characters and cannot overlap. Implementations are resolved by contract id from PackageRuntimeApi.services; file-discovered lifecycle modules expose them on the state returned by init(ctx), separately from api(state); runtime instead names the native runtimeProvider module, and mcp-server is reserved without an implementation slot. Host-assigned trust is authoritative: declarations from other tiers are dropped. See Lifecycle.

runtime — how the package executes

"runtime": {
  "type": "wasm",
  "binding": "none",
  "permissions": { "fs": "data-only", "net": "none", "exec": false },
  "hosted": { "tools": "all" },
  "skillScripts": "none"
}
FieldValuesRules
typenode | wasm | mcpnode and mcp are both first-party only — node runs in-process, mcp spawns a subprocess as the host user — leaving wasm as the only code runtime below that tier. A non-first-party manifest declaring either loads its declarative contributions, spawns nothing, and returns a clean runtime-unavailable error from every handler (partial, reason untrusted-node / untrusted-mcp). Omit entirely for declarative-only packages.
bindingnone | embedded | subprocess | remoteembedded is first-party only; remote usually pairs with source.kind: "mcp-server".
entry{ module, command, args, url }WASM packages normally omit it — the loader defaults to dist/package.wasm. MCP packages declare entry.command (and optional args) for the subprocess spawn; the values are used verbatim, which is part of why that runtime is first-party only.
permissions.fsnone | data-only | mounts | fullfull is rejected for untrusted packages.
permissions.netnone | allowlist | fullfull is rejected for untrusted packages.
permissions.execbooleanRejected for untrusted packages.
hostedtools/prompts/resources: none | selected | all (+ selected lists)What the platform MCP server advertises to external clients. Omitting the block is the permissive default: a package carrying at least one tool, command or resource is hosted implicitly, and every tool it declares is advertised. "none" on a surface withholds it — the schema still appears in package catalogs, but no hosted entry is built, and the call is refused by the caller's own visible tool list with Tool not hosted for this agent: <name>. A package drops out of hosting entirely only when tools, prompts and resources are all "none"; selected narrows to the ids listed under selected.
skillScriptshost | wasm-only | noneDeclared script posture for the package's bundled scripts/*.sh, checked at manifest load: an untrusted package may only declare none (omitting the key is also valid), anything else requires trusted or first-party. Beyond that check it is advertisement — it is shown as a tag in the agent's package overview and refuses no spawn on its own; the shell URI-policy gate and the sandbox are what actually stop a script.

Tool names are global

A tool name is one key for the whole deployment — there is no per-package prefix. When two installed packages declare the same name, or a package reuses the name of one of the platform's own tools, nothing is renamed. Each agent answers the name with one claimant, chosen by a fixed rule rather than by load order: the first-party claimant when there is one (the platform's own tools included), otherwise the lexically-smaller package id, and any package before an MCP server offering the same name. A claimant the caller cannot use — its package switched off, out of scope, or gated behind a feature the caller does not hold — drops out before the rule runs, so it never takes the name from anyone. The other claimant stays loaded and keeps every one of its other tools.

The rule holds for every install class, including the packages the platform ships and the ones an administrator adds as a host dependency. Both of those count as first-party, so between them the lexically-smaller package id wins — an added package can take a name from the platform's own tools. It never refuses to start: a name collision is settled by the rule, it does not take the deployment down — an installed package that reuses a shipped name is a diagnosable annoyance, never an outage.

The platform reports the outcome rather than hiding it: the boot log names the owner and the shadowed package ids, and the tool catalog lists every contested name with its claimants, so the agent's Tool Access panel can pick which one answers.

Name your tools for the domain they serve — acme_release_status, not status.

access.trust

TierWhoKey limits
untrustedProject packages (default)Manifest floor: no permissions.exec, no net: "full", no fs: "full" — fs: "data-only" and fs: "mounts" both validate, as does net: "allowlist". skillScripts must be "none". 30 s tool timeout, 10 tool calls/min, rules downgraded to docs
trustedAdmin-promoted project packagesNo manifest permission floor — any fs, net or exec value validates (the WASM host API still exposes no exec primitive), and skillScripts may be declared. 120 s tool timeout, 100 tool calls/min, rules downgraded to docs
first-partyBuiltin-class onlyIn-process node, no dispatch limits

Do not self-declare first-party expecting it to matter: the host assigns trust by source. Dependency-sourced builtins are overwritten to first-party; project packages can never reach it.

requires — features, role grants, dependencies

"requires": {
  "packages": ["@neuralis/brain-core"],
  "providesFeatures": [
    "my-pkg.read",
    { "id": "my-pkg.write", "title": "Write access", "description": "Create and modify items" }
  ],
  "defaultRoleGrants": {
    "owner": ["my-pkg.read", "my-pkg.write"],
    "admin": ["my-pkg.read", "my-pkg.write"],
    "member": ["my-pkg.read"]
  },
  "accessFeature": "my-pkg.access"
}
  • providesFeatures entries are bare id strings or { id, title?, description? } objects — the object form adds human-facing metadata for the admin Roles UI without renaming the capability id. Duplicate ids are rejected.
  • defaultRoleGrants maps role names to the feature ids each role receives by default.
  • packages declares dependencies on other packages by id.
  • accessFeature (optional) is a single feature id that gates the WHOLE package's visibility — a caller lacking it sees none of the package. Omit it to keep the package visible to everyone (the default; imported packages load unchanged). See Features and access.
  • Which tools, skills, routes, and widgets a feature gates is never stored on the manifest — it is derived at boot from the loaded definitions. See Features and access.

app.surfaces — UI contributions

Every UI contribution lives under app.surfaces[] with kind: 'widget' | 'card' | 'dock'. The renderer a surface may use depends on package trust (direct React is first-party only; project packages use iframe). An iframe surface's relative url must point into that surface's own canonical root — app/surfaces/{kind}/{surfaceId}/, where {surfaceId} is the widget's type or the card's id — and an entry anywhere else is a load-time validation error that fails the whole package. That same block — a widget's component, a card's render — also carries the optional assetMode: omitted (or "self-contained") the entry must inline everything it needs, while "bundle" has the platform serve the entry's sibling files. Full reference: App surfaces.

"app": {
  "surfaces": [
    {
      "kind": "widget",
      "id": "my-pkg.widget",
      "type": "my_workspace",
      "title": "My Package",
      "component": {
        "renderer": "iframe",
        "url": "./app/surfaces/widget/my_workspace/index.html"
      },
      "requires": { "features": ["my-pkg.read"] }
    },
    {
      "kind": "dock",
      "id": "my-pkg.open",
      "label": "My Package",
      "action": { "type": "open-widget", "widget": "my_workspace" }
    }
  ]
}

app.module — a first-party package's prebuilt UI module

A host-assigned first-party package may declare ONE prebuilt browser module that carries the React code of all its direct surfaces (they share module state, so they share one bundle):

"app": {
  "module": {
    "entry": "dist/app/host.js",
    "provides": ["@acme/reports/app/charts"]
  }
}

entry (.js/.mjs) and the optional css (.css, present when your UI entry imports a stylesheet) are package-relative paths under dist/, and the host serves only the dist/app/ tree — exactly what neuralis-build ui emits (dist/app/host.js, and dist/app/host.css when there is one); the build fails if the declaration does not match its output. Any other key, an absolute path, a .. segment or a path outside dist/ is a validation error. The host derives the URL it serves the module at — your manifest never names one — and attaches the module to the workspace with no host rebuild. On a package of any other trust the field is a warning and is dropped at load.

provides (optional, at most 16 unique ids) lists the modules this package PUBLISHES to other first-party UI modules — each a subpath of the package's own name, never a module the host already provides (React, the platform client library). The host declares those ids before it imports the module, and another package's UI build reads them from this module instead of bundling a second copy; importing any other module of a package that ships a UI module fails that build. An id outside the package's own name, a duplicate, or more than 16 ids is a validation error.

The build writes a record beside the bundle (dist/app/shared-imports.json): the host API version, the React major the bundle was built against and the shared ids it reads. The host refuses a module whose record does not match it — a different host API version, a different React major, or a bundle that reads React with no recorded major — and its surfaces show the "cannot be rendered" placeholder with the reason. An entry that exports no install<Name>HostComponents function is refused the same way. The image build judges every installed module against the host it builds and names the refusals. After a platform upgrade, rebuild the module with neuralis-build ui (a build:ui script) before the host loads it — the image build does this itself for a package registered from a directory.

events — what a person should hear about

A first-party package declares the moments a person should be told about — a run failed, a sync stopped — and the platform turns each published event into a notification for the people it concerns. The package builds no notification UI or storage of its own.

"events": [{
  "id": "report.failed",
  "title": "Report failed",
  "subject": "report",
  "dataSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": { "reportId": { "type": "string", "maxLength": 128 } }
  },
  "requires": { "features": ["acme.reports.read"] },
  "notify": { "default": "on", "tone": "error" },
  "dock": "acme-reports"
}]
FieldRequiredMeaning
idyesDotted lowercase id, unique in the package. The wire type is derived from the publishing package, so no package can emit another package's events.
titleyesHuman title (at most 80 characters), always rendered as text.
descriptionnoLonger explanation.
subjectyesWhat the event is about; names the src/events/<subject>.ts file that decides who may see it.
dataSchemayesA closed JSON schema for the event's data: "type": "object" with "additionalProperties": false at the root, nested at most four levels.
requires.featuresyesThe feature a reader needs — the feature of the route that serves the same record. A reader without it never receives the event.
notifyno{ "default": "on" | "off", "tone": "info" | "success" | "warning" | "error" }. Omitted means not a notification unless a person follows it.
docknoThe id of one of the package's own kind: "dock" surfaces; the unread count of that dock entry is kept from these events.

Each subject needs a source file, src/events/<subject>.ts (compiled to dist/src/events/). It exports subject, a required visible(event, session, state) predicate — may this person see an event about this subject? — and an optional bridge(publish, state) that subscribes to the package's own internal events, calls publish({ event, subjectId, projectId, data, audience }) and returns its unsubscribe. A subject without a predicate has every event dropped: visibility is deny-by-default.

An event is a signal, never content. data holds ids and fixed codes, validated against dataSchema and capped at 4 KiB — never an error message, a path, a transcript or anything the subject's route would refuse to show. Two optional data keys shape the notification row when the schema declares them: title (a string shown before the declaration title) and nav ({ widgetType, initialState? }, the row's navigation target). A publish that breaks the schema throws, so a producer bug is loud rather than silently dropped.

The delivered envelope uses the CloudEvents 1.0 attribute names (id, type, source, subject, time, data); the events[] declaration itself is a Neuralis shape. The field is honored for first-party packages only; on any other package it is a warning and is dropped at load. At most 64 events per package.

connectors — sources and external services

A connectors[] entry with type: 'source' registers a filesystem-like source kind: a closed configSchema for the admin creation form, declared capabilities the runtime instance must mirror, and a connectorFactory module path the loader imports. A source connector's secrets are declared in neuralis.credentials[] and read through the injected CredentialResolver — never as config-schema properties. MCP/OpenAPI/HTTP connectors declare transports and an auth block whose sensitive setupFields are stored encrypted, never in plain config. Full reference: Connectors.

sources — declared source instances

Where connectors[] registers a source kind, sources[] declares a concrete source instance of an already-registered kind that the platform seeds into every new project (seed: "auto") or surfaces for on-demand add (seed: "discoverable").

"sources": [
  {
    "source": "data",
    "kind": "local",
    "root": "data",
    "scope": "project",
    "description": "The project's shared working directory.",
    "seed": "auto"
  }
]

Each declaration names a source slug, a registered kind, an ownership scope, a required description (rendered into the agent's runtime-stack prompt block), and a seed mode. The declaring package becomes the source's origin attribution. Declarations are trust-gated at load: seed: "auto" needs trusted+, a ${appRoot} platform root needs first-party, and untrusted packages may only declare project-relative discoverable sources. Full reference: Connectors.

credentials — declared credential ids

A package declares the external secrets it needs in credentials[]. Each entry surfaces as a row in the admin Credentials catalog (source package) so an owner can give it a value — the platform's LLM providers, web-tool keys, and infrastructure keys are all declared this way rather than hardcoded in the host.

"credentials": [
  { "id": "llm.anthropic", "label": "Anthropic API Key", "category": "llm" },
  { "id": "embedding.voyage", "label": "Voyage AI API Key (embeddings)", "category": "vector" },
  { "id": "acme.apiKey", "label": "Acme API Key", "category": "search" }
]

The shape is exactly { id, label, category?, oauth? }. The id must pass the credential-id charset (^[A-Za-z_][A-Za-z0-9_.-]*$, ≤ 128 chars, no slashes or leading dot, because it becomes an <id>.enc.json path segment). An oauth: true entry is a display hint — the catalog renders Connect/Disconnect chrome instead of a value field (only the Codex flow is wired today).

Two rules, enforced at different places:

  • Declare only ids something actually resolves. A row an owner can fill in that no code reads is worse than no row: the operator believes the key is configured. If the value is read from the environment at boot (infrastructure topology), it belongs in .env, not here. This one is a convention plus a build-time drift guard over the first-party packages — the manifest validator does not and cannot check it, so a third-party declaration will load either way.
  • The reserved self-scope namespaces are first-party-only. Ids in the git (git.<host>.pat, git.<host>.oauth.*), channel, and MCP (mcp.<server>.*) classes have host-owned resolution semantics; a manifest outside first-party trust that declares one is rejected. Your own namespace is unrestricted at any trust tier.

A declared id surfaces on two management surfaces: the owner/admin Credentials catalog (all scopes), and — for members who can see the declaring package — the self-service BYOK card, where a member may set the id in their own user scope. The admin catalog is the union of every loaded package's credentials[], every installed skill's credentials: frontmatter, and any ids already stored by hand (package ∪ skill ∪ manual); the member view is filtered to the packages and skills that member is actually allowed to see. See Credentials and Bring your own keys.

Declaration is not a grant. A credentials[] row is catalog metadata only: it makes the id visible for an owner (or a self-service member) to set. It carries no write authority — a package never stores its own secret — and at runtime the value still resolves against the caller's real scope (agent → project → user → global, most specific wins). Full model: Credentials.

configSettings — declared platform config keys

A package declares the platform-level tunables it OWNS in configSettings[]. Each entry registers a schema row in the platform config store and surfaces on the admin Config → Platform Settings tab (grouped by category, with the declaring package shown) — the host hardcodes no config schema of its own; every key an admin can adjust is declared by the package that consumes it.

"configSettings": [
  {
    "key": "maxAgentSteps",
    "label": "Max Agent Steps",
    "description": "Maximum steps per agent stream run",
    "type": "number",
    "default": 300,
    "min": 1,
    "max": 10000,
    "envFallback": "MAX_AGENT_STEPS",
    "category": "runtime"
  },
  {
    "key": "searchDebug",
    "label": "Search Debug",
    "type": "boolean",
    "default": false,
    "category": "debug"
  }
]

The shape is { key, label, description?, type, default, envFallback?, editable?, category, min?, max? }, and type is one of exactly four values: number, string, boolean, or json.

Keys are flat camelCase (^[a-z][a-zA-Z0-9]{0,63}$) in one shared namespace — a key has exactly ONE owning declarer; a collision with another package's key is rejected at load. Each key resolves through a three-step cascade: the admin-set file override, else the optional envFallback environment variable, else the declared default. type: "json" entries are edited by dedicated UIs (never the flat settings list) and ignore envFallback. Number keys may carry inclusive min/max bounds, enforced when an admin saves a value. editable: false surfaces the key read-only on the admin Config tab; omitting it leaves the key editable.

Declaration is not a grant, and secrets never belong here. A configSettings[] row only makes the key adjustable on the feature-gated admin Config surface; mutation stays behind the admin config feature. Config values are admin-visible plaintext — anything secret-shaped belongs in credentials instead (the validator warns on secret-looking keys and envFallback names). Config declarations are honored for first-party packages only.

uriPolicies — path-protection baselines

"uriPolicies": {
  "data://<self>/**": {
    "default": { "read": true, "write": true, "exec": false }
  }
}

Patterns must stay inside the package's own namespace via the <self> placeholder — only packages://<self>/ and data://<self>/ prefixes are accepted, and a manifest with a foreign-namespace pattern is rejected at load time. The loader substitutes <self> at boot and seeds matching baselines into source configuration at first attach. Full semantics: URI policies.

A path rule inside paths[] may add "floor": true to declare an immutable, restrict-only floor — a denial that no later override, admin edit, or policy-route call can remove. The validator enforces the authoring shape: a floor rule may carry only false capabilities and is rejected if it is combined with override, roles, agents, or any granting/true capability. Floors come from trusted package manifests only. See Immutable floors.

dataLayout — where the package keeps its files

"dataLayout": {
  "directories": ["cache", "state"],
  "agentDirectories": ["memory"]
}

A package that writes under its own data:// zone declares the first-level directory names it uses: directories for package-level folders (data://<zone>/<name>/) and agentDirectories for folders keyed by agent id (data://<zone>/<agentId>/<name>/). The declaration creates nothing on disk — it is what every agent sees: the agent runtime renders one data_areas row per visible package into the <runtime_stack> prompt block (runtime stack), so an agent — or a freshly spawned delegate that receives no workspace summary — can address the layout without guessing a path or listing a root.

The zone root is the platform's provisioning mapping, not the package id: data://<slug>/ for a first-party package, data://_installed/<slug>/ for every other install class. Names only — no descriptions, no paths. The validator is fail-closed: a traversal (..), a separator, a placeholder such as <self>, a brace, a comma, whitespace, an unknown key or a duplicate rejects the whole manifest at load, because the value lands in every agent's cached prompt prefix. An empty declaration renders no row.

machineWritten — the files the package writes on its own

"dataLayout": {
  "machineWritten": [
    { "path": "*/usage/events.jsonl", "class": "metrics" },
    { "path": "audit/**", "class": "log" },
    { "path": "*/runs/**", "class": "runtime-text" }
  ]
}

Counters, logs and runtime state a package writes without anyone asking are declared here, each as a glob relative to the package's zone (* is one path segment, ** any depth) and a class: metrics (numbers only — a dashboard reads the file, a search finds nothing in it), log, or runtime-text (runtime state a person may search for, such as a run's events). This field is never rendered into the prompt. It tells the platform what is automated: a new project's data source leaves the metrics files out of its index from the start, while logs keep the default exclude they already have and runtime text stays searchable. The seed is written once, when the project's data source is created (for the first project of a fresh install, on the platform's first start); an existing project's source settings are never rewritten, and its owner can edit the list at any time. The globs are anchored to your own zone, so a declaration can never reach another package's files — the validator refuses a leading /, an empty or .. segment and any character other than letters, digits, ., _, - and *. Only trusted and first-party packages' declarations are used.

Distribution

A builtin-class package is a regular npm package under any scope (or none). The host image installs it as a real directory at node_modules/<id>/ — from a registry version, a packed tarball, or a directory the image build builds — applying the package's files whitelist; the host resolves the package root via require.resolve('<id>/package.json'), so the exports map (if any) must keep "./package.json" resolvable.

The npm files whitelist

package.json#files declares the runtime contract of the packed tarball. Every first-level directory that carries runtime content — dist, tools, workflows, skills, instructions, rules, agents, docs, commands, team, app — plus hooks.json must be listed, or packing silently drops it and the installed package loses those contributions. Workspace symlinks hide the mistake: the package works in a development checkout and breaks only when installed from a tarball.

On this page