@package-system

Building your own first-party package

Develop a Neuralis package in its own repository: administrator-assigned first-party trust, sandboxed alternatives, build requirements and kernel peers.

A package does not have to live inside Neuralis to be a Neuralis package, and it does not have to live inside Neuralis to be a first-party one. This page is about the shape you want when the package is a product in its own right: its own repository, its own git history, its own release cadence, developed against the published contract and built into the deployment's image from its directory, a packed tarball or a registry version.

First decide which lane you are in

The difference between the two ways a package can reach a running Neuralis is not where the code lives. Both lanes can be a folder on the same machine. The difference is trust, and trust is assigned by how the package arrived, never declared by the package.

Sandboxed laneFirst-party lane
How it arrivesa folder in the project's _packages/, or a mounted directory the project picks upthe deploying administrator registers it as a dependency of the host
Who can do itanyone who can write to the projectonly whoever controls the deploy configuration
Trust tieruntrusted (an admin may elevate to trusted; never to first-party)first-party, assigned by the host
RuntimeWASM-confinedin-process
Outbound callsonly through the ctx.callRoute gatewaytyped in-process package APIs
Connectorsoutbound kinds, in that project's own partitionfull connector contract, platform lane
App surfacesiframe onlyiframe from any registration; compiled React from any registration when the package ships a prebuilt UI module (see UI while registered from a directory)
Install stepdrop the folder; rescanned in about half a secondregister the dependency, rebuild the image

There is a third reach path that is not a lane in this sense at all: plain Markdown in a synced source becomes a package with no install step and no code — see source packages. It carries skills, rules, instructions, agents and docs, and nothing executable, so the trust question above does not arise for it. The two columns are the two lanes that can RUN code.

If you can express what you need in the sandboxed lane, do — it needs no administrator and it is reversible by deleting a folder. Reach for the first-party lane when you need native connectors, in-process routes, or the full contract surface. That reach is a decision somebody with deploy rights has to make about you, which is exactly the point of the tier.

The dependency line is the trust act

Nothing else grants first-party trust. Not a field in your manifest — access.trust is read and ignored. Not the folder's location. Not a git remote. The administrator adding your package to the host's dependencies is the decision, and it is reviewable the way any dependency is reviewable.

That line may point at an exact registry version — a public one, or your own private registry — at a packed tarball, or at a directory on the deploying machine, and every form arrives the same way: the image rebuild installs the package as a real directory in the host's node_modules, under any scope or none. A directory is built there, as part of the same build as the platform's own packages. Registering a directory or a tarball checks your package before the line is written — with the same admission check the runtime applies at load — so a package the platform would refuse is refused there, and nothing changes. If a package still fails when the platform starts (it cannot initialise, or it collides with another package over a service, an OAuth prefix or a configuration key), that package alone is left out: the platform runs without it, and the operator's health view names it.

What the image build expects of a directory

A directory you register is built by the image build, so it has to build there on its own:

  • a build script, and — when you declare app.module — a build:ui script running neuralis-build ui;
  • files[] listing every first-level runtime folder (below);
  • the kernel packages as peers (below), never a dependency;
  • no link: or file: specifier that points outside your directory — the build cannot follow it, and registering refuses it with the specifier named.

A tarball is installed as packed and never rebuilt, so it must carry its dist/ already built.

UI while registered from a directory

Everything on the server side of your package is live once the image carries it: tools, routes, skills, agents, workflows, connectors, the MCP surface. Compiled React ships as a prebuilt UI module: the workspace is a Next.js application that imports no package's React source. Put your install entry at app/host.tsx, build it with neuralis-build ui, and declare the output as app.module — typically { "entry": "dist/app/host.js" }. For a registered directory the image build runs that step itself; a tarball or a registry version ships the module prebuilt. The host attaches the module when the workspace loads: it serves your dist/app/ files from a content-hashed URL to signed-in members, reads React and the platform client library from its own instances (the build fails if your bundle would carry a copy), and compiles your utility classes into the one workspace stylesheet. Without a module, your direct surfaces show the host's "This widget cannot be rendered by this host" placeholder while every other contribution works. iframe surfaces render from every lane, because your package serves them itself.

The host refuses a module built for another host API version or React major — or one whose build record names no React major, or whose entry exports no install<Name>HostComponents function — and the placeholder then names the reason. The image build judges every installed module the same way and names the refusals when it finishes, so a prebuilt module that needs rebuilding after a platform upgrade is visible before anyone opens the widget.

Do not read the placeholder as "not loaded" — check the package by id in the agent-core diagnostics, where a loaded first-party package reports loaded and its trust tier.

Kernel packages are peers

Your package's consumer is a Neuralis host, and that host already loads @neuralis/agent-core and @neuralis/package-system in its own process. Declaring either as a dependency installs a second copy, and one contract with two module graphs is a genuinely hard class of bug to diagnose.

Declare them as peers. The image build provides them — your package is built inside the same workspace as the platform's own — so one copy of each is all any package ever sees:

{
  "peerDependencies": {
    "@neuralis/agent-core": ">=0.1.0",
    "@neuralis/package-system": ">=0.1.0"
  }
}

Do not add a link: devDependency that reaches into a Neuralis checkout beside yours: it points outside your directory, so the image build cannot resolve it and registering refuses it. If your package manager installs peers automatically, a standalone install of your repository will try to fetch the kernel; with pnpm, autoInstallPeers: false in your repository's own pnpm-workspace.yaml keeps that install local.

The host's dependency line that names your directory or tarball is a local source: a path on the deploying machine. It builds into that machine's image and nowhere else — it is never committed, never part of a published host package, and a plain pnpm install of the host must not run while it exists (it would write the path into the host's lockfile). Developing and shipping are different planes, and this is where they separate.

Your tsconfig has nothing to extend

Outside a monorepo there is no shared base config to inherit, so inline what you were getting from it. Two settings are load-bearing rather than stylistic:

  • skipLibCheck — the kernel ships several hundred .d.ts files and you are not typechecking them.
  • declaration — required by composite: true.

Resist widening include while you are in there. If your config only covered src/**, keeping it that way and saying so is worth more than a green run that covers less than it looks like it does.

What you must ship

files[] decides what actually leaves your repository — for a publish and for a live swap alike. List every first-level runtime folder: dist, tools, workflows, skills, rules, instructions, agents, docs, team, app, and hooks.json if you have one. Omitting one is a quiet failure: it works locally and ships empty.

The whitelist cuts the other way too, and usefully. Private development notes belong in a root-level Markdown file that is not in files[] — it then never reaches anyone who installs the package. Putting the same text in rules/ would both ship it and spend the always-on prompt budget of every agent that can see the package.

Four separate acts, and only one of them makes it load

These are routinely confused, and the confusion always sounds the same: it is committed and pushed, why is it not loaded?

  1. Push to a git remote — source backup and review. Changes nothing about any deployment.
  2. Register the dependency and rebuild — the administrator's trust act, then the image rebuild that installs it. This is what makes the package discovered and loaded in-process with first-party trust.
  3. Mount the directory for live development — the container binds your directory over the installed copy, so an edit needs a build in your directory and a restart, not an image rebuild. Plumbing; without step 2 the package is still invisible.
  4. Publish to a registry — a separate decision with its own audience and its own review.

Steps 2 and 3 are operator commands and belong to the deployment's own runbook. Steps 1 and 4 are yours, and they are not the same act: a private git remote is not a publication.

Run your gates where you installed your dependencies

Two things surprise people who assume a package that loads correctly must also typecheck and test correctly in the same place:

  • Your directory's own node_modules is not what runs. A mounted directory is bound under the host's node_modules with its own node_modules masked, so the running package resolves the kernel and React from the host's installation — and a typechecker run in that container finds the kernel the same way, through the parent directory. A typechecker run in your standalone checkout has no kernel unless something there installs one.
  • node_modules installed on the host carries host-built native bindings. A container of a different platform or libc cannot load them, so a test runner can fail before it reads a single test file.

Neither is a defect in your package. Run the gates in the environment where you installed the dependencies, and never report a suite as green in an environment you did not run it in.

On this page