@package-system

Runtime stack and substitution tokens

What packages and skills can know about where they run, and the tokens resolved at activation.

Packages and skills should never guess where the platform lives. The runtime stack snapshot — built once per process at bootstrap — is the single source of truth for host classification, container-versus-host paths, bind mounts, and backing-service URLs. Every consumer reads the same immutable value: the system-prompt <runtime_stack> block agents see, the path resolvers that translate container paths to host paths, and any package that needs to know its environment.

RuntimeStackInfo

discoverRuntimeStack() (from @neuralis/package-system/paths) reads the environment, OS signals, and a few cheap filesystem checks, and never throws. The resulting shape is closed — every field is present, and a value that is not meaningful for the environment is null, never undefined:

GroupFields
Host classificationosKind, osRelease, osType, nodeVersion, isDocker, isWSL, wslDistro
Container path rootsneuralisHome, appRoot (platform-internal zone), projectsRoot, cwd
Host path rootshostHome, hostAppRoot, hostProjectsRoot — populated in Docker when the host home is configured; on native deploys they mirror the container roots
Mount tableosMounts[] — { key, containerPath, hostPath, readOnly } per configured bind mount
ServicesqdrantUrl, ollamaUrl, embeddingModelId, brainInfraMode, maxAgentSteps, apiBaseUrl — URLs and headline knobs only, never secrets

Docker is detected via an env override or the standard container markers; WSL via the distro env var or the kernel release string. Mounts come from NEURALIS_MOUNT_<KEY> env triplets (container path, optional host path, optional read-only flag), which the platform's mount tooling writes.

services.apiBaseUrl is the loopback base URL the host serves /api/packages/* on — this is what the ${NEURALIS_API} token (below) resolves to, so skill scripts call the local service without hard-coding ports.

Path translation

Two pure functions translate between the container view and the host view using the snapshot:

  • resolveHostPath(stack, containerPath) — container → host. Checks the app root, then the projects root, then every mount, and returns the input unchanged when no translation applies (native deploys). Matching is segment-aware, so /neuralis-other never matches a /neuralis root. Use it anywhere a user expects a path they can paste into their own terminal or editor: sync logs, tool results, file trees.
  • resolveContainerPath(stack, hostPath) — the reverse. Models frequently paste back the host path they saw in a tool result; the shell policy gate and working-directory resolution translate it to the container form before spawn and policy mapping.

realProjectRoot(stack, projectId) returns both views ({ containerPath, hostPath }) for a project's filesystem root, and refuses unsafe or sentinel project ids.

What the prompt block carries

The <runtime_stack> block an agent sees is rendered once per stream from this snapshot plus two per-caller tables, in a fixed order: the host flags and path roots; the source table (one entry per source the caller can address — connector, on-disk reflection, effective read/write/exec policy, description); the data_areas rows — one per visible package that declares a manifest dataLayout, naming its data:// directories (data://<zone>/{a,b} and, for per-agent folders, data://<zone>/<agentId>/{…} under the stream's own agent id); the residual scheme reference; the mount table; and the backing-service URLs. A layout's machineWritten list is never part of that row. Everything in it derives from boot-time facts and manifest declarations, so it stays byte-stable for the whole conversation and the prompt cache holds. Volatile per-source state (on/off, index counts) is rendered separately in the trailing <workspace_live> reminder.

The mount table is a display convention

The mount_table lines rendered in the system prompt's runtime-stack block (<key> → <container path> [ro] (host: …)) are labels for discovery — there is no os connector and no tool resolves a mount label as a URI. To make a host path agent-callable, attach it as a local-connector source with a real URI prefix. The real, load-bearing data is RuntimeStackInfo.osMounts.

The file coordinate quadruple

Every package file (skill, rule, instruction, agent, doc) carries up to four coordinates that tie the contribution to the filesystem — the (uri, connector, osUri, containerDir) quadruple on PackageFile:

FieldMeaningWhen filled
uriAddress in the filesystem-tool layer (packages://<dir>/... — the directory name, not the registry id — data://<src>/..., brain://...)Project packages and source-contributed files; undefined for builtins read from the image
connectorConnector kind serving the URI ('local', 'webtop', 'brain', …)Always reflects the disk layout, even when uri is undefined
osUriAbsolute path in the connector's own environment — host-side for host-anchored kinds, container-internal for sandbox kinds — produced by the connector's resolveOsUri. Read it together with connector to know which.When the connector can derive one; UI and vectors consume it
containerDirContainer-internal absolute path (for a skill: its bundle directory)Always for builtin and project-package files; for source-fed files only when a local mount maps into the container

A connector never fabricates a host path. One that cannot derive a real one either throws a structured OsUriUnresolvableError — so URIs surfaced to the UI and the vector store never contain stale fallbacks — or, when its store is a sandbox whose internal absolute paths are themselves the true coordinate, returns that internal path instead. Container-backed desktop sources take the second route, which is why the host/container translators re-map only host-anchored kinds. See Connectors for the contract.

Activation substitution tokens

When a skill is activated through the execute tool, the host renders the skill body — and resolves the skill's environment — with these tokens:

TokenResolves fromTypical use
${SKILL_DIR}The skill bundle's containerDirbash ${SKILL_DIR}/scripts/run.sh; also expands inside the shell cwd parameter
${SKILL_URI}The skill's urifs_read access to bundle files when no container path exists
${SKILL_CONNECTOR}The skill's connector kindBranching a body on how the bundle is reachable
${SKILL_NAME}The skill's idSelf-reference in logs and output
${SESSION_ID}The active stream sessionCorrelating script output with the conversation
${NEURALIS_API}services.apiBaseUrlcurl-based scripts calling host routes (authenticated by the session ticket)
${SKILL_CREDENTIALS_RESOLVED}The declared credentials: ids that resolved, comma-joinedLetting a body state which keys its scripts will find in the environment
${SKILL_CREDENTIALS_MISSING}The declared ids that did not resolveTelling the model to degrade instead of failing on a missing key

A ninth form, ${MISSING:<id>}, is per-credential rather than a fixed token: it renders as (missing: <id>) or (present: <id>) for the id named inside it. Values are never substituted — the tokens carry names and coordinates, and the secrets themselves reach only the script's environment.

Substitution is a plain string pass, so an unknown ${...} placeholder is left intact rather than blanked — it stays visible to the model and easy to spot. When a skill's connector provides no container directory, ${SKILL_DIR} resolves to an explanatory sentinel string and the platform shell cannot run its scripts. When the skill's source can execute commands itself, the activation envelope prints the skill's directory URI to pass as the execute working directory; otherwise the skill body can still be read over ${SKILL_URI}. The full activation flow is described in Skills.

How packages receive the snapshot

The host discovers the stack once at bootstrap and threads it to every package through the bootstrap-complete lifecycle context, alongside the aggregated URI policies (see Lifecycle). Agents receive the same data as the <runtime_stack> system-prompt block, generated from the snapshot — never hand-edited per agent. Configuration values outside the documented services block do not belong in the snapshot: anything secret goes through the credential store (see Credentials), never through environment variables read ad hoc.

On this page