@neuralis/agent-core
v0.3.0
Published
Agent runtime kernel for Neuralis — the nervous system of the AI workforce, and the package protocol's first provider and universal consumer at once: the one runtime that loads every package and executes every element of the contract on its way to the mod
Readme
@neuralis/agent-core
Agent runtime kernel for Neuralis — the nervous system of the AI workforce, and the package protocol's first provider and universal consumer at once: the one runtime that loads every package and executes every element of the contract on its way to the model. Owns the agentic LLM loop, model providers, MCP client management, package loading, the contribution host, conversation persistence, hooks, policies, connectors, the skill protocol, the workflow engine, and the chat surface.
Responsibility
- Bootstrap the package system: build the kernel registries, load every builtin and installed package, run
init/start/onBootstrapComplete, and expose a public API viagetPackageApi('@neuralis/agent-core'). - Run the agent stream: provider selection, conversation assembly, tool execution, policy + hook evaluation, persistence, and SSE delivery.
- Aggregate contributions from every loaded package (tools, prompts, resources, files, hooks, policies, connectors) into a single MCP-compatible surface.
- Serve REST routes under
/api/packages/@neuralis/agent-core/*for agents, conversations, streams, interactions, models, tools, files, packages, connectors, MCP servers, usage, logs, context-window usage, and read-only agent setup inspection. - Expose a generic OAuth 2.1 MCP HTTP server backed by the same contribution host for external clients.
- Own scheduled/background execution: the
workflow/engine (calendar workflows, scheduler, run queue, creator-identity runs — see the Workflow engine section). The legacy@neuralis/orchestratorpackage is deleted (modular-meerkat Inc 5); its reserved id stays squat-protected in the host. - Own the interactive terminal: PTY sessions, the WebSocket companion mount, the source-tab resolver, and the xterm.js widget (see the Terminal section). The terminal used to be a separate package; it lives here because the host-access plane needs the SAME PTY code, input guard, and uri-policy source index in-container and host-side.
- Own the host-access plane: the host-resident broker client, the host terminal proxy, and the plane-partitioned shell gate (see the Host access section).
- Does NOT own filesystem indexing (see @neuralis/brain-core), admin surfaces (see @neuralis/admin), or machine sandboxes (see @neuralis/machine-core).
Public Surface
Exported from index.ts:
- Bootstrap:
bootstrapAgentCore,createAgentCoreState,buildAgentCoreApi,applyAgentCorePostBootstrap,AgentCoreState,AgentCoreApi,InstalledPackageMeta. - Runtime:
AgentRuntime,loadAgentSystemPrompt,DEFAULT_SYSTEM_PROMPT,RuntimeParams, everyRuntime*Event,LLMToolSchema,mapMcpToolToLLM,mapMcpToolsToLLM,buildToolResultBlocks,compactHistory,SeqCounter,buildConversationMap,renderConversationMapSection. - Stream:
SseStream. Own-stream Stop keeps the client attached through canonical user-row acceptance and server termination; it preserves the submitted bubble and any next draft. Unconfirmed originals are manually copyable in the existing status banner, without automatic draft restoration or resend.presenceBusfor project-scoped user-presence pub/sub. Published from the SHAREDrunAgentStreampath (stream/StreamOrchestrator.ts:streamingas the first statement inside the active-streamtry,idlein the outer cleanupfinallythat frees_activeStreams) — NOT the stream route — so BOTH interactive chat turns AND headless workflow runs raise the same watch-mode indicators (Golden Rule #1; there is no workflow-specific presence path). APresenceEvent.origin({kind:'workflow', workflowId, workflowTitle}, set byWorkflowRunnerviaStreamRequest.presenceOrigin) tells consumers the "user" is a headless workflow: the watch-mode banner reads "Workflow '' is running…" and the composer locks even for the workflow's OWN creator (a self-created scheduled run is not them typing — the host hook bypasses theuserId !== currentUserIdgate when anoriginis present). Theidlepublish also carries aPresenceEvent.outcome('complete' | 'error' | 'stopped', lifted from the finalizereason; absent onstreaming) so a member watching a conversation they did NOT start — another user's stream or a headless workflow — can tell a clean finish from a failure (presence otherwise collapses both intoidle). Only the enum travels, never the error text; own conversations use the client store's own error state. The conversation picker + chat tabs surface it via theuseConversationStatushook (app/chat/hooks/) →ConversationStatusDot. Presence is tracked per(agentId, conversationId), NOT per agent (presenceKeyinPresenceBus.ts; the client mirrors it withpresenceByConversationId): one agent commonly streams MANY conversations at once (every scheduled workflow runs as the agent into its own conversation), so a per-agent last-event-wins slot let a second same-agent run false-unlock every other still-running conversation's lock — the composer lock resolves the WATCHED conversation's own presence (useConversationPresence), while the dock avatar stays per-agent (useAgentPresence). Gotcha (deploy-load-bearing):presenceBusMUST stay aglobalThis+Symbol.for('@neuralis/agent-core:presenceBus')singleton (seesrc/presence/PresenceBus.ts). Next.js evaluates this module several times per process (observed 3× — the/api/eventspresence channel, the catch-all stream route, and the package loader each get a copy) even though@neuralis/agent-coreis inserverExternalPackages. A plain module-levelnewgives the orchestrator (publisher) a different instance than the/api/eventshub (subscriber), so cross-user presence — dock teammate avatars, the watch-mode banner, the live-timeline refresh (#17) — silently never fires. Do NOT "simplify" it back to a barenew. - Providers:
ProviderType,ModelInfo,StreamProvider,ChatMessage,ToolSchema,ALL_MODELS,findModelById,getModelsByProvider,resolveDefaultModelId(sync, config-aware — replaced the module-loadDEFAULT_MODEL_IDconst in Inc 2),getProvider,getDefaultProvider,getAvailableModels,getDefaultModelId,clearProviderCache,providerFactory,syncLocalProviders,invalidateLocalProvider,invalidateCredential. Per-provider clients (getClaudeClient,getMinimaxClient) and message converters (toAnthropicMessages,toOpenAIMessages,toGeminiMessages). Codex OAuth helpers:setCodexPersistCallback,setCodexPurgeCallback,setCodexRefreshMutexCallback,revokeCodexRefreshToken,CodexCredentialPurgedError. - Tools:
ToolCaller,RuntimeTool,RuntimeToolResult,ToolExecutionLikeResult,CompactAction,CompactHistoryInstructions. The runtime tool dispatch goes throughStreamToolDispatcher(stream/); the legacyLocalToolManager/CompositeToolManagerclasses were removed in the move to the file-first kernelToolDispatcherpath. - Skill protocol:
parseSkillContent,injectVariables,SkillFrontmatter,ParsedSkill(fromskill/). The oldcommands/invocation system (CommandRegistry/CommandExecutor + theexecutetool'scommandaction) was removed — skills replaced model-side command invocation. Packagecommands/*.jsonsurvive only as definition + hosted-MCP-prompt surfaces (prompts/list/prompts/getfor external clients). - MCP:
McpClientManager,McpProtocolClient,createMcpClient,McpSdkClientAdapter,McpError/McpSessionLostError/McpAuthRequiredError,importMcpJson,createHostMcpManager,composeToolCatalog,validateMcpServerUrl,validateMcpServerUrlResolved, MCP entity types, and themcp/serverandmcp/oauthsubpath exports. (The legacyMcpJsonRpcClient/McpHttpSessionclient and its context-header helpers were removed — external HTTP servers receive no identity headers; seemcp/README.md.) - Hooks:
HookRegistry,HookDispatcher,HookExecutionError,HookTimeoutError,resolvePackageHooks,setFirstPartyHookResolver,buildSkillHookCallback,resolveSkillHooks, everyHook*Payload,HookSource,HookRegistration,HOOK_SOURCE_PRIORITY. - Policies:
PolicyRegistry,PolicyEngine,InputValidator,PolicyDeniedError,resolvePackagePolicies,DEFAULT_TRUST_CEILINGS,buildTrustRules,GUARD_PROFILES,DEFAULT_GUARD_PROFILE,evaluateGuard,isGuardProfile, all policy types. - Stores (types only; instances created in lifecycle):
AgentRecord,AgentConfig,SystemPromptConfig,ToolsConfig,PackagesConfig,PackageOverride,FileOverride,RuntimeSettings,McpConfig,AgentAppearance,ConversationMeta,StoredMessage,UsageEventRecord,UsageSummary,ListUsageFilter.PackagesConfigis the nested per-agent package/file enablement (S3 gate,store/README.md); the flatFilesConfigis removed. - Files:
resolveFilesCatalog,resolveDisabledPackageIds(the S3 package gate primitive — agent ∪ conversation deny-list, consumed by injection/skills/tools/overview),buildFileInjectionGroups,buildFileInjectionSections(deprecated bridge),ResolvedFile,ResolvedFilesCatalog,FileInjectionGroups,InjectionContext. On top of S3, the S2 per-contribution feature gate runs at the same boundaries:resolveFilesCatalog/buildPackagesOverview/buildSkillFilesAccessortake the caller'sgrantedFeatures(request.grantedFeatureson the stream,req.session.grantedFeatureson/files/catalog) and drop every file whosePackageFile.requires.featuresthe caller does not satisfy (meetsRequiresfrom@neuralis/package-system/access). Feature-gated files are hidden, not[OFF]-marked — a member cannot infer they exist, and activating one yields the plainSKILL_NOT_FOUND(no distinct forbidden error). Hosted MCP prompts apply the same predicate toPackageCommand.requiresinPackageContributionHost.listPromptsForContext. Authored via the flatrequiredFeatures: [a, b]SKILL.md / contribution frontmatter key (PackageFileDiscoverymaps it to the first-classrequiresfield; a nestedrequires:form is unsupported by the minimal frontmatter parser and warns). Package-level non-inference (S2 tail): when feature-gating reduces a package to zero caller-visible surface (no visible tools/commands/files/connectors/hooks/policies) while it does declare gated content, the WHOLE package is dropped —buildPackagesOverviewomits its block (and returns''if every package is gated away) and thepackages/overviewroute filters it out of the payload (so it never appears in the agent Packages editor or the conversation-scopePackageSelector). A genuinely empty or failed-to-load package (no gated content) is kept so it stays visible/fixable; gated tools are already absent from the dispatch catalog (S2b). - Packages:
PackageDataManager,PackageLoader,discoverPackageContributions,applyDiscovery,resolvePackageDefinitionFromRoot,buildRuntimeDefinition. - Domain: provider-neutral conversation types and
DomainConversationAssembler. - Protocol: MCP-native message and tool-result types under the
protocolnamespace. - Auxiliary (
auxiliary/, formerlysummarizer/— all non-chat-stream LLM callers):generateConversationTitle,runSummarizer,summarizeConversation,computeCoverageCutoff,buildHistoryForMode,buildSummaryMarkdown,parseSummarySentinel,recordAuxiliaryUsage. Title, prompt-polish, and summary share ONE dispatcher (AuxiliaryCompletion.ts, on the provider ADAPTERS — pin / inherited / auto selection, thinking-aware request sizing) + ONE usage path. - Context:
injectAttachmentContextIntoHistory,extractAttachmentTokens,buildAttachmentContextHeader. Attachments use bounded text previews or original image/audio/video blocks; the per-step delivery boundary checks image encoding/signatures and active-model MIME limits, preserving the journal and source file when it emits a recovery marker. - Subpath exports:
@neuralis/agent-core/runtime(lifecycle),@neuralis/agent-core/mcp/server,@neuralis/agent-core/mcp/oauth, and the two UI subpaths a consumer package compiles against,@neuralis/agent-core/app/chatand@neuralis/agent-core/app/chat/store(the types of the two tier-2 ids below; no otherapp/path is exported). The workspace UI ships as ONE runtime module:app/host.tsx(theinstall…HostComponentsentries for chat, calendar and terminal) is built bypnpm build:ui(neuralis-build ui) intodist/app/(host.js+ chunks +host.css, xterm's stylesheet), declared inneuralis.app.module, and attached by the host at runtime; the module publishes@neuralis/agent-core/app/chatand/app/chat/storeas tier-2 shared modules (app.module.provides), so a consumer bundle reads this module's instances. The host renders no chat module of its own: agent-core fills the host slots (app/chat/host/workspaceSlots.tsx— the workspace provider with presence and the card-runtime reconcile, the agent-studio empty state, the dock tile's activity).@neuralis/agent-core/app/chatalso re-exports the card runtime registry — the single runtime source of truth for every chat card, implemented ONCE in the kernel (@neuralis/package-system/client) and seeded here with the builtin cards.registerCardComponent(type, component, { owner, renderer, mountPolicy, estimateHeight? })is deny-by-default (owner-checked; a package cannot overwrite a builtin or another package's card; a snapshot iframe cannot shadow a first-party direct card) and returns an explicitCardRegistrationResultinstead of throwing; a contributed card may bring its own row-height model (estimateHeight, a module-level stable reference) so the timeline stops guessing its size from a host-side constant; the registry is an observable store (useCardRegistration/useCardRegistryVersion) so a card entry re-resolves on a hot register/replace without a timeline event, and the timeline readscardRequiresKeepAliveto raise overscan only for genuine keep-alive (iframe/mcp-app) cards. Full contract inapp/chat/cards/README.md.
The package definition is declared in package.json under neuralis. Feature taxonomy (S2c C1): core.agents (agent CRUD), core.execute (the act of running an agent — stream/interactions/conversations/context-window; it also gates the execute TOOL itself, and a packaged skill's shell scripts need exec.container beside it), core.observe (read-only introspection — logs view/usage/tools-catalog/files-catalog/packages-overview/mcp-servers/connectors/models), core.web (the two web tools), plus the workflow trio workflow.read / workflow.write / workflow.dispatch (the Calendar/engine surface), channels.connect (member-default since Inc 4: a user's OWN channel connections + pairing approvals — USER scope only), mcp.connect (member-default, MCP1 Inc2: connect your OWN external MCP servers — OAuth flow initiation + self-scope mcp.<serverId>.* token storage via the hardened isMcpSelfScopeCredentialId predicate, and the secret outbound-header values of the same class (mcp.<serverId>.header.<KEY>, MCP5B — see mcp/README.md); project/global MCP secrets stay on the admin Credentials surface), git.connect (member-default, G4 keyring-kestrel Slice 1: connect your OWN git remotes — seed a push token/PAT into your OWN users/<id> scope as git.<host>.pat via the hardened isGitSelfScopeCredentialId predicate + the unified SelfScopeCredentialWriter port's 'git' class; project/agent git tokens stay on the admin Credentials surface. Remote OPS additionally require drive.write + the source's uri-policy exec), credentials.self (member-default, Phase M BYOK: set/remove/list YOUR OWN credentials — catalog-declared + arbitrary custom ids — in your OWN users/<id> scope from the chat Connections section's Credentials group; reserved git/channel/MCP ids stay on their own connect surfaces, project/global secrets stay on the admin Credentials surface behind project.credentials.write — and global, plus any scope outside your own, additionally behind platform.scope, which carries no default grant to any role), core.prompt-polish (the composer prompt-polish rewrite call — /prompt-polish route + PromptPolisher), terminal.read (see and open the terminal widget; the route feature for every terminal/* route — manager/member default, deliberately NOT viewer), plus exec.container (the container plane's shell ENTRY — granted wherever core.execute is, and NOT escalation-only: it is a door, the uri-policy is the regulator), and the escalation-only features that carry no grant below the admin tier — the seeded admin role no longer carries the '*' wildcard, so admin holds each of them through its own ENUMERATED defaultRoleGrants.admin entry in this manifest and owner through '*'; manager/member/viewer get none of them, and any custom role can be granted one: mcp.sidecar (MCP1-S: activate command-based stdio MCP servers to run in a managed sidecar container — arbitrary code in platform infra, strictly stronger than mcp.connect; enforced FRESH at the spawn floor), mcp.apps (MCP1-C: render connected MCP servers' ui:// HTML templates as interactive chat cards — untrusted external HTML in the isolated-origin sandbox; gates all three mcp-apps/* routes), core.logs (platform-log read, escalated inside the logs route on top of its core.observe baseline), core.connectors (create, configure, connect/disconnect, enable/disable and delete a project's outbound tool connectors — escalated inside connectors/handleRoute.ts on top of the connectors/* route's core.observe baseline; READING the connector list, health and tools stays on core.observe), exec.unconfined (bypass the execute-shell URI-policy gate and the OS sandbox — inside the container; it also opens the container plane by itself), terminal.container (the terminal analog: open the Container Root tab, a session rooted at / that bypasses source scoping), terminal.supervise (project-wide managed-terminal listing/control; host sessions additionally require terminal.native), and the two host-plane grants exec.host / terminal.native (let execute and exact host-source terminal tabs reach the operator's machine through the host broker — NOT a bypass: no ROLE can bypass the host's OS confinement — the one relaxation is the operator's ceiling file (confinement: unconfined, single-operator) — and host work is additionally clamped by the operator ceiling; see the Host access section), channels.manage (project-scope channel connections, every connection in the project, cross-user peer links), and core.conversation.fork (branch a continuable fork — manual fork + compacted forks; no grant below the admin tier — enumerated for admin, '*' for owner; grantable per role — enforced in-handler in the shared conversations/* route family). Default grants: admin = every id in this taxonomy ENUMERATED (the whole read/execute tier plus each escalation-only id above — the seeded admin role no longer carries '*'); manager = agents+execute+exec.container+observe+web+prompt-polish+workflow r/w/dispatch+channels.connect+mcp.connect+git.connect+credentials.self+terminal.read; member = the same minus workflow.dispatch (members stream — D1); viewer = agents+observe+workflow.read (read-only, no execute/web/polish/terminal). Role-grant repairs ride ROLE_GRANT_VERSION (host ProjectStore.ts, now 19 — v19 moves every shell entry into the exec.* namespace and derives exec.container for every role that holds core.execute). v10 was the first RENAME step — a third migration primitive beside the additive repairs and the subtractive revokes: it rewrote core.shell.host → core.shell.container and terminal.host → terminal.container on every role of an existing project, builtin and custom, preserving position and de-duplicating (v19 then carried the first of those on to exec.unconfined). Repairs/revokes are keyed by role NAME, so neither could express a rename for a role the platform does not enumerate — a custom devops role holding the old id would have silently lost the grant. '*'-wildcard roles are skipped (the wildcard already covers both ids). providesFeatures entries use the {id,title,description} object form (C9) so the admin Roles UI shows what each feature controls. Trust is first-party. Three widget surfaces are declared: agent-core-chat (auto-opens, frameless, sortKey -1), agent-core-calendar (workflow.read-gated, with a dock companion), and terminal (terminal.read-gated, frameless, multi-instance, with a dock companion). The agent creation studio is deliberately NOT among them — the host renders it directly for the agent_create workspace view, so a widget surface would give one capability two entry points. runtime.hosted exposes a selected tool set over the generic MCP server — web_search, web_fetch, connect, and the connector branch of execute — so external MCP clients (Claude Desktop, Cursor) can exercise them directly. Local shell execution and skill activation through execute still require stream context; prompts and resources stay unexposed.
Bootstrap
The host discovers the manifest's provides: ['runtime', …] provider and loads its file-first src/runtimeProvider.ts export through the kernel. runtimeProvider.boot(RuntimeBootConfig) delegates to bootstrapAgentCore, which owns the loader, registries, private contribution host, hooks, policies and connector lifecycle. It loads the provider first and other builtins sequentially. A builtin that fails validation, init or start — or that the host left out (RuntimeBootConfig.refusedBuiltins: the image build refused it, or it declares a config key the host owns) — is REFUSED alone: its routes, tools and surfaces stay out, the reason is logged once, and health().refused names it under a closed code (load_failed · start_failed · conflict) while the platform runs. whenReady() rejects only when the runtime provider itself fails, when a REQUIRED contract's provider is refused, or when an onBootstrapComplete hook throws; successful initialization alone does not imply readiness.
The host resolves installed package roots through Node module resolution, including real production node_modules roots. It passes only generic kernel config, identity, roots, session/credential ports and HostPorts; product work stays in packages. During init, ctx.config is ready and ctx.hostPorts is available only to first-party packages. ctx.platform.runtimeStack() and .packageUriPolicies() fail before their single publication and return the published snapshots afterward. Bootstrap attaches agent-core's private handles directly; lifecycle context never carries those handles to other packages.
Agent-core implements state construction, API/service adapters and start wiring in the existing src/lifecycle.ts owner:
createAgentCoreState(ctx, loader, runtime)installs the config bridge before consumers read settings and starts the existing meta-first conversation reconciliation. The samestate.bootReadygates conversation routes, workflow/channel startup, agent deletion and project deprovision. A failed walk retains data and reports failed health; the package loader bounds deprovision waits.buildAgentCoreApi(state)exposes agent stores/services, verified access, connector/MCP snapshots, OAuth and channel connection APIs.getGitRemoteBinding(session, host)returns only the caller's own token-free view: project/user ids derive from the verified session, and the write-capable store is never exposed. Package rescan/cache administration belongs to admin through generic host runtime ports.buildAgentCoreServices(state)implements the session-ticket verifier, agent directory, channel gateway and three OAuth callback adapters. The separate nativeRuntimeProviderimplements the runtime contract. The gateway includes channel descriptors and OAuth start; callback adapters verify the real session and use the package-owned token services.attachAgentCoreSubstratewires private runtime handles;applyAgentCorePostBootstrapreads published facts and starts existing owners. Structured health reports conversation boot as starting, ok or failed without disclosing exception details.
<runtime_stack> system-prompt block
When streamDeps.runtimeStack is set, the orchestrator calls renderRuntimeStackSection(stack, projectId, sources, areas) (in runtime/stack/) once per stream and threads the result onto RuntimeParams.runtimeStackSection. AgentRuntime.buildBaseSystemPrompt injects it directly after the <workspace> block — between "who you are" and the package overview. The block is byte-stable across agentic steps, so it lives in the cached static prompt and adds zero per-step cost. Contents, in order:
host:—os,docker,wsl(distro),nodeflags- path roots:
neuralis_home,app_root,projects_root,project_root,cwd— container view first, host-side path in parens whenNEURALIS_HOST_HOMEis set sources (…)— the per-source connector table, STRUCTURE only: one block per source the caller can address viafs_*(<uri> connector=<k> os_uri=<path|∅ (reason)> [container=<path>]+policy=<r/w/x> [— description] [pkg: id]). The volatile on/off state and index counts render into<workspace_live>instead (renderRuntimeStackSourceLive) — never here, or a sync between turns would bust the cached prefixdata_areas (…)— one row per VISIBLE package that declares a manifestdataLayout:data://<zone>/{a,b,c}for package-level directories anddata://<zone>/<agentId>/{…}for per-agent ones, under the stream's own agent id (a delegate child renders under its PARENT's id — it runs inside that zone). The rows are collected bycontributions/dataAreas.ts(collectDataAreas) from manifest data alone, so they are as cache-stable as the source table; the package set is the overview's OWN visibility decision (visibleOverviewPackageIds— scope/feature-hidden and fully-gated packages omitted, S3-off packages omitted); the zone root issanitizePackageId(@neuralis/*→ bare slug, an instance URI → its mirrorruntime/<scheme>/<leaf>, a plain npm id →runtime/_npm/<id>); every name is folded (foldPromptTag) and each row is capped by theruntimeStackAreasMaxCharsplatform key (default 320).{a,b}is notation for the model, not a glob. The same collector feeds theagent_setupinspector, so the inspector shows exactly the rows the model gets (runtime/stack/__tests__/runtimeStackSites.test.tspins the three call sites — parent stream, the ONE delegate-child builder for both spawn planes, the inspector)scheme_map (reference…)— fixed builtin schemes with no live source row (app://,neuralis://); discovery-only, not callable until attachedmount_table— everyNEURALIS_MOUNT_<key>mount as a<key> → <container path> [ro] (host: …)label; a siblingNEURALIS_MOUNT_HOST_<KEY>env carries the host-side path. Labels, not URIs: no connector serves anos://scheme; to make a host path agent-callable, attach it as alocal-connector source with a real URI prefix (pnpm neuralis:mount add … --slug <name>)backing:— Qdrant URL, Ollama URL + embedding model, brain infra mode, the loopback API base — URLs only, no API keys. Amax_steps=entry is deliberately NOT rendered: the kernel field behind it reads only the boot-frozenMAX_AGENT_STEPSenv var (never the platform-config file override), and this block is cached per-project while the real budget is per-agent and per-run. The enforced step ceiling reaches the model as the volatile<session>Steps: <step>/<max>line instead (runtime/todos.tsgetSessionForPrompt, fed by the agentic loop's own counters every step)
The package declares its complete per-agent usage/** directory as machine-written metrics, including events, counters and their bookkeeping. New data sources exclude it through the kernel metrics seed; existing explicit source filters remain owner-controlled.
uriPolicies aggregation
bootstrapAgentCore walks every loaded package, resolves the <self> placeholder against each package's slug (agent-core, brain-core, …) and publishes the resolved snapshot through ctx.platform.packageUriPolicies(). Brain-core reads that shared snapshot lazily; BridgeService.createSource then unions every package's matching baselines into the persisted source-config JSON at first attach. Brain-core no longer hardcodes foreign-package URI patterns — every per-package carve-out (e.g. data://agent-core/$self/** self-write override, packages://<pkg>/src/** read-only) lives in the contributing package's manifest.
Agent-core's own manifest declares the per-agent isolation rules under data://<self>/** and packages://<self>/**. The validator (validateManifestUriPolicies in @neuralis/package-system/policies) rejects foreign-namespace patterns at manifest load time.
Conversation visibility/continue (#17, multi-user) rides the SAME data source policy: ConversationPolicyGate (conversation/ConversationPolicyGate.ts) evaluates data://agent-core/<agentId>/conversations/<convId>/ for read (history-list + GET-read routes) and write (ensure/continue + edit/undo/rewind/retry), with $self = the conversation's agent and ConversationMeta.userId as the policy's resourceOwnerUserId ($owner; $creator is never the conversation creator — it is the SOURCE's createdBy). The shipped baseline makes a conversation its CREATOR's: */conversations/** is read- and write-denied, re-granted by $self/conversations/** users:['$owner'] and by the owner/admin role rows, so another member neither lists, reads nor continues it (a former participant included); the composer's conversations/_staging/** stays read+write for every member (uploads before a conversation exists). An owner widens it in the source's policy editor (a users or roles grant row) with no code change. The fs and shell planes ask the same question through the kernel resource-owner port: conversation/conversationOwnerResolver.ts answers it from meta.json (fork-aware — a fork is its forker's). The gate is fail-closed (deny when the data source is absent; an unknown owner never matches). The owner (meta.userId) stays fixed; a continuing owner/admin is appended to ConversationMeta.participantUserIds (server-populated, drives the dock/picker avatar stacks).
Chat-stream finalization (live guard · forks · non-destructive summarize)
The chat stream's three coupled behaviours, all preserving messages.jsonl as
the full, append-only truth (the UI shows it whole, the agent can fs_read it):
- Live guard profile. The active guard is no longer frozen at stream start.
StreamOrchestrator._activeStreamsis aMap<streamKey,{guard}>; aguardProfileProviderclosure is threaded into the policy path soPolicyEngine.evaluateGuardProfilereadsholder.guardat EVERY tool decision.POST /conversations/:id/guard(setStreamGuardProfile) flips it mid-stream — effective at the next tool decision, no approve/deny round-trip.evaluateGuardonly ever decides allow-vs-ASK — its 5 floors are PROMPT floors (UX), not security floors.autoZERO-PROMPTS: it short-circuits toallowat the top ofevaluateGuard(so a mid-stream flip toautoremoves every prompt), while the REAL deny stays inPolicyEngine.checkToolCallarms 1-5 (blocklist / trust-ceiling / capability / allowlist / feature-gate), which run before the guard arm and clampautoto the caller's actual permissions.safe/balancedapply the floors. All three profiles (incl.auto, framed for long/full-control autonomous work) carry aguardOverlayForsystem-prompt block. - Forks. The manual Fork action /
manage-conversationsskill branches a first-class fork (createFork,kind:'manual') instead of rewriting history; the parent is untouched. Forks nest under the root (conversations/<root>/forks/<composite>/), are continuable, get their own SUMMARY.md, andcreateForknever leaves an empty stub dir (seed computed before mkdir; cleanup on failure). Gated bycore.conversation.fork. (NOTE:compact_historyno longer forks — see below.) Seestore/README.mdfor the addressing. - Non-destructive compaction.
compact_historydoes NOT fork or rewritemessages.jsonl. The model'scompact_historytool_use IS the record; the per-row trim (remove / summarize / truncate) is re-applied at prompt build (applyCompaction) + in-memory within the turn, addressing tool outputs by their⟦tool_use_id⟧handle (or whole rows by#seq) from the## Conversation mapsection (a Markdown heading in the per-step reminder, not an XML block). The tool_result is just "success"; the conversation keeps the originals. Reversible via themeta.compactionModetoggle ("Show full" / "Apply") on the timeline marker bar. - Non-destructive summarize (v2 revision store, one owner).
ConversationSummaryService(conversation/) is the SINGLE application owner — the auto stream-trigger (source:'auto', occupancy ≥ effective threshold,autoPaused/divide-by-zero guarded), the manual "Summarize now" route, and Learning (same route,activate:false) all call it. A committed summary is an immutable v2 revision underconversations/<id>/_summary-revisions/<uuid>.md;meta.summary(schemaVersion:2,activeRevisionId+contentSha256+coveredHistorySha256+coveredUntilSeq) is the durable pointer andSUMMARY.mdis only a regenerable projection (seestore/README.md). The raw conversation files (messages.jsonl,meta.json,SUMMARY.md, legacysummary.md,_summary-revisions/**) are write-protected by an immutable uri-policyfloorin the manifest — even the owning agent and owner/admin can'tfs_*-write them; only the internal store can (plan.mdstays writable as the positive control). Routes:POST /conversations/:id/summarize(closed bodyactivate?/focusInstruction?; refused409 stream_activemid-stream),POST …/summarize/cancel(UI "Stop"),GET …/summarize/status(read-gated), andPOST /conversations/:id/history-mode= the singlesetReplayMode"Show full" / "Resume summary" toggle. List/get carry a response-onlyactiveflag. The formerwriteSummary/setSummaryMeta/setHistoryModewriter APIs are removed. Seeauxiliary/README.md+store/README.md.
Other kernel ports used:
PackageInitContext.data— agent-core's process-global state (thesystem.jsonllog, the codex usage snapshots, the pending MCP/git OAuth flows) lives in its PLATFORM homedata.globalDir(~/.neuralis/app/data/agent-core/, first-party packages only, read throughrequireGlobalDir); connector configs stay in the projectdata/agent-core/; per-project agent/conversation/usage data is rooted atdata.projectsRoot.PackageInitContext.credentials(CredentialResolver) — wired into the loader for every package; packages callresolveCredentialinstead of readingprocess.env. (resolveForKindis a dead stub the host answers with{}; there is deliberately noresolveEnv.)PackageInitContext.credentialUse(CredentialUsePort, credential-use limits v1) — the host's per-credential calls/period counter/gate, threaded the same way (RuntimeBootConfig.credentialUsePort→PackageLoader.setCredentialUsePort). The host already counts EVERY resolve at its own choke point (the wrapped resolver, both ConfigProvidergetCredentialclosures,mcpTokenStore.readScoped) — a use-limit deny makesresolveCredentialreturnundefined(auditedcredential.use_denied), so a denied credential surfaces as the consumer's ordinary "not configured" error. A package calls the port only where resolve-counting is dishonest for its call pattern (brain-core's construction-captured embedding key is the canonical case). Honest units this floor implies: web-search counts RESOLVES (≈2× executed keyed searches — the broker'sisConfiguredpre-flights resolve too); channel secrets count at send/verify time and a POLLED channel (Telegram) consumes its window continuously; skill shell-env secrets count once per declared id per activation; MCP-sidecar secrets count atreadScopedgrant time. LLM provider keys are EXCLUDED in v1 (derived from the first-partycategory:'llm'declarations — the USD spend limits govern those).- Declared credentials. agent-core's manifest declares its host-known platform keys in
neuralis.credentials[](PackageCredential[]={ id, label, category?, oauth? }): the 8llm.*provider keys,llm.openai-codex.oauth(oauth: true), the two web-search keys (websearch.tavily/websearch.brave), the four git-host OAuth App pairs (<host>.oauth.clientId/clientSecretforgithub,gitlab,bitbucket,codeberg) and the WhatsApp Meta-App pair (oauth.whatsapp.clientId/clientSecret) — 21 in total. Two things are deliberately NOT declared: the six bare channel ids (telegram.botToken,telegram.webhookSecret, the fourwhatsapp.*), becauseChannelEngineonly ever resolves the per-connection derived form<baseId>.<connectionId>— a bare row would do nothing when filled, so the admin catalog gets the real derived ids fromAgentCoreApi.listProjectChannelCredentialIds(projectId)instead; and any env-backed infrastructure key, which belongs in.env. AgentCoreApi.collectDeclaredPackageCredentials()(mirrorscollectDeclaredSkillCredentials()) walks every loaded package'scredentials[]and de-dupes by id, feeding the adminbuildCatalogpackage tier. It has no visibility trust filter — a third-party package must be able to surface its own id or an owner could never set it — but it does gate ATTRIBUTION: a first-party declaration wins thelabel/category/oauthseed whatever the load order, a non-first-party declarer of an already-first-party id only joinsdeclaredBy, anddeclaredByTrustcarries the tier to the UI. The complementary floor is in the kernel:validateManifestrejects a reserved self-scope id (git / channel / MCP class) from any non-first-party manifest.- The RESOLVE-side id maps (
CREDENTIAL_MAP,WEBSEARCH_CREDENTIAL_IDS,NATIVE_SEARCH_CREDENTIAL_IDS, brain-core'sEMBEDDING_MODELS[].credentialId, plus an explicit host-read set) are pinned to the manifests by thecredentialDeclarationDrifttest — bidirectionally, both sides derived. A resolve-side id with no declaration fails loud (an owner could never set it); a declaration with no consumer fails just as loud (an owner fills it in and nothing happens — that is how the unreadqdrant.apiKeyrow survived for so long). - Declared config settings. agent-core's manifest also declares its platform-config keys in
neuralis.configSettings[](PackageConfigSetting[]): runtime (maxAgentSteps, themaxAgentsPerProjectseat ceiling, defaultTemperature/defaultModel/attachment + composer caps), workflow, summarizer, websearch/web, provideradvancedtimeouts + base URLs,localLLMs(json), the five terminal tunables (terminalMaxSessions,terminalScrollbackLines,terminalExecTimeoutMs,terminalMaxOutputBytes,terminalIdleTimeoutMinutes— declared here since the PTY moved in), and the ten host-plane keys (hostBrokerEnabled,hostBrokerSocketPath,hostBrokerUrl,hostBrokerProbeTimeoutMs,hostBrokerProbeTtlMs,hostBrokerProbeFailureTtlMs,hostBrokerFsTimeoutMs, and the three background-host-shell keyshostShellBackgroundLifetimeMs,hostShellPollMs,hostShellStatusWaitMs).AgentCoreApi.collectDeclaredPackageConfigSettings()collects every loaded FIRST-PARTY package'sconfigSettings[](trust-gated, unlike the display-only credentials collector —envFallbackprojects env VALUES into admin-visible config, so non-first-party declarations are skipped with a warn) and the host registers them intoPlatformConfigStoreat bootstrap, before the ConfigProvider injection. TheconfigDeclarationDrifttest pins the exportedAGENT_CORE_CONFIG_KEYSconst (config.ts — EVERY config read must be listed there) ⟺ the manifest, bidirectionally. Inc 2 re-routed the six HALF-WIRED keys through live reads (getConfigNumber/String/Booleanexports):defaultModel(resolveDefaultModelId(), registry default-model thunk),summarizerTimeoutMs,websearchProviderNative(boolean semantics — "true" now enables, deliberate fix),websearchSearxngUrl,summarizerPreserveTurns(the auto-summarize tail — was an orphan, wired), andwebToolsAllowedHostsvia the host-injected kernel hooksetOutboundAllowedHostsSource(package-system web-common) — admin edits now apply live. Inc 5 added 69 tier-3 rows (delegate/retry/media/shell/summarizer-token/websearch-research/workflow tunables — the full sweep) INCLUDING the kernel-consumed keys agent-core declares as their owner (20 today): 11 ride the host-wiredsetKernelTunablesSourcetyped hook (the fourwebFetch*+webToolsUserAgent+ the fivepackageLog*— the level, its per-packagepackageLogLevelOverridesmap and the three rotation knobs — +mcpStdioRpcTimeoutMs— declared here, read by the kernel, pinned by the drift test's KERNEL_TUNABLES_WIRED set, deliberately NOT in AGENT_CORE_CONFIG_KEYS;webToolsUserAgentis the first NON-numeric field onKernelTunables, so the bootstrap wiring carries atunableStrsibling that reads an empty value back as unset) and 9 ride option seams agent-core passes (toolResult caps ×3 sites, rankSearchResults freshness, dispatcher trusted-tier options + factory cacheSize at PackageLoader, workflowDefaultRuntimeMinutes). PackageInitContext.sessions.createProjectFactory— exposed by the loader; agent-core does not currently consume it.- agent-core's
app.surfaces[]declares threedirect-renderer widgets (agent-core-chat,agent-core-calendar,terminal) plus two dock companions; the host renderer resolves each widget through its CLIENT-sidePackageHostRegistry, which agent-core'sinstall…HostComponentsentry points fill (the server-side loader keeps no component registry). - Cross-package: agent-core resolves
@neuralis/brain-corewithloader.getPackageApi(...)for filesystem reads (skill discovery, source contributions, file revert).
Security & scope
- Every route under
src/routes/declares afeature; the 34 route modules are gated by ten distinct ids —core.observe,core.execute,core.agents,workflow.read,terminal.read,git.connect,mcp.connect,mcp.apps,core.prompt-polish,credentials.self; the kernelRouteDispatcherenforces grants before invoking handlers. No route is feature-less (C2 — the former ungatedmodelsroute now declarescore.observe; the repo-widefeatureAudittest HARD-fails any route that exports apatternwithout afeature). Nine of those modules carry an in-handler ESCALATION on top of their baseline, each declared viaexport const escalations—logs→core.logs,connectors→core.connectors(every mutating/connectors/*arm; the reads stay on the module'score.observe), pluschannels,conversations,interactions,mcp-servers,terminal-sessions,terminal-sourcesandworkflow.__tests__/routes/escalationDrift.test.tsis the drift guard: it fails on a gate the module does not declare AND on a declaration nothing enforces, followsconnectors.tsinto its delegatedconnectors/handleRoute.ts, and proves both arms can redden against synthetic defect sources. It deliberately cannot see per-arm coverage — that stays each module's own behavioural suite. - Codex subscription usage signal. The
codex-usageroute (core.observe, read-only) returns the last-known Codex (ChatGPT OAuth) usage-limit snapshot for the caller's resolved credential scope. It keys bycodexScopeKey({userId, projectId})from the verified session only — the usage lane deliberately carries no agent axis (the snapshot is a workspace signal, not a per-agent one), so?agentIdis IGNORED and there is no forged-?agentIdsurface. The snapshot is written best-effort by the StreamOrchestrator when the codex provider emitscodex_rate_limits*meta (parsed byproviders/openai-codex/rateLimitMeta.ts, persisted toCodexUsageStore— project snapshots under the project data dir, user/global under the platform package data dir). It surfaces in the chat config "Usage Signal" bar (always-on, both windows, labelled "not accurate") and, mid-stream, as a threshold-gatedStreamStatusBannernotice (flashes at 80/90/95/99%, auto-dismiss). Each window's5h/wklabel sits inside a reset-countdown ring that fills as the window nears its refresh (codexWindowReset(window, now)inrateLimitMeta.ts, pure + tested; hover for the exact remaining time). The provider README owns the meta-contract detail. - Codex subscription savings. Because codex models are subscription-billed their catalog
pricingis{0,0}, so every codex usage row settles at$0. The figure counts codex tokens only. It used to be inflated by any delegate child that ran a paid model under a codex parent: the child's steps landed on the parent's codex row, socomputeCodexEquivalentUsdpriced Opus tokens as GPT-5.6 and reported the result as money saved. Per-model partitioning ends that — the child settles its own paid row,findCodexPaidEquivalentreturns nothing for it, and the savings number shrinks to what it always meant. Forward-only, deliberately: there is no backfill, so rows written before the partition keep the attribution they were written with and a long-lived install's all-time savings still carries some delegated work.providers/usageCost.tsprices the equivalent OpenAI API cost of the same tokens —findCodexPaidEquivalentmatches the paidprovider:'openai'entry sharing the codexapiModelId,computeCodexEquivalentUsdprices the token counts against it (reusingcomputeModelCostUsd, the extracted body ofUsageMeter.estimateUsdCost— ONE cost-math source for the live meter AND read-side aggregation). Both row writers (UsageMeterand the auxiliary recorder) STAMP that figure on a subscription row asequivalentCostUsdat write time, and every reader goes throughresolveCodexEquivalentUsd— the stored value when the row carries one, else the read-time computation (rows written before the stamp) — because the catalog deletes rows: a removed codex row or paid twin computes to0and would erase the savings already booked against it.UsageStore.{aggregate,summarize,summarizeProject}carry it asequivalentCostUsd/totalEquivalentCostUsd;GET /usageannotates each codex event withequivalentCostUsdand carries thepricingBasisdisclosure on BOTH arms (the agent-less project-summary branch too, since that is the branch theinspect-agent-coreusage script reads); the chat-config per-model rows render it in an accent colour instead of$0. Every TOKEN-PRICED figure is a catalog list-price estimate —UsageStoreImpl.pricingBasiscarries the one server-authored sentence (providers/usageCost.tsUSAGE_PRICING_BASIS) that says so. A plan FEE is not one of them:openai-codex/planPricing.tsestimates it from the reported plan tier, and the savings card renders it beside the note. FOUR surfaces render the sentence verbatim — the chat config Usage Signal card, the admin usage and Codex-savings cards, and the Limits paragraph; the conversation picker's per-conversation spend and the delegate Done-row cost render money with no disclosure, and the delegate row's hover title carries a short local restatement (list price, model steps only) rather than the sentence. A usage event is one PRICING PARTITION of a turn (seestream/README.md), so a turn spanning two models is two events sharing onerequestId— alimitis a row budget, not a turn budget. The chat-config panel asks for 2000, which is ample today; no production reader joins or dedupes onrequestId, and nothing outside tests reads a row'ssteps.AgentCoreApi.listCodexSubscriptions()enumerates the connected subscriptions (one per snapshot scope) withplanType+ estimatedmonthlyUsd(openai-codex/planPricing.ts, owned byneuralis-provider-maintenance) + the rate-limit windows — consumed by the admin dashboard's per-subscription savings + break-even surface. - Agent CRUD is further constrained by
agents/agentAccessPolicy.ts('*' | 'own' | 'view'× projectagentOwnership). Theownbranch accepts two kinds of ownership evidence — anagentOwnership[id]record, and, when there is none, the agent's own persisted creator (userId) — because no create path writes an ownership record, so for a new agent the creator field is the only evidence there is. The record is consulted FIRST, so an explicit assignment always wins over the creator fallback. Four of the five sibling predicates already accepted the creator;canAccessAgentcould not, because it took a bareagentId— seeagents/README.mdfor the twelve call sites and which of them deliberately keep denying. DELETE /agents/:idemits anagent.deleteaudit event. The route is reachable by the packagedmanage-agentsskill'sDELETE agents/<id>call line as well as by the chat panel's Agent Config card, so an agent deleting an agent leaves a record of who did it. The admin widget deletes through its OWNagents-admin/:idroute and audits there, so the two paths are disjoint and one deletion never produces two rows. For a caller the gate REFUSES, the handler answers ONE generic403whether or not the agent exists — deliberately, so a refusal cannot become an existence oracle for agents in other members' scopes. (A caller the gate ADMITS is answered on its merits: becauseAgentStore.deleteisrm -rf --force, a delete aimed at an id that is already gone returns200, which is what lets an operator clear a leftover ownership record.)- A per-project seat ceiling (
maxAgentsPerProject, default 100) bounds every create path per item, with the batch route refusing a straddling batch whole rather than partially. Refusal is409naming the limit. An in-process caller with no access context (system provisioning) is exempt, so a future provisioner could seed a team of any size — but state it precisely: every agent create today reaches the service throughsrc/routes/agents.tsunder a real session, so every create that exists today is bounded. The carve-out is a forward-looking allowance, not a live path. It is a tenant-facing tunable, not a security floor. The per-item check NARROWS the concurrent-create window rather than closing it (the count and the write are not serialized); the bounded overshoot is accepted. - Agent appearance. The
config.appearancearm ofPATCH /agents/:idis validated bysrc/usecases/agentAppearance.ts: an icon from the platform icon library (stored by its library name), a#rrggbbcolour, a picture as a base64data:URL whose BYTES decide its type (PNG/JPEG/WebP — SVG and every document class refused) under the kernelAPPEARANCE_IMAGE_MAX_BYTES; an unknown key is a400. Only that arm is validated — the rest of aconfigpatch keeps the agent service's own contract. The picture lives in the agent's ownappearance/<sha256>.<ext>, the record keeps{hash, mimeType, updatedAt}, the replaced file is deleted, and list/get answers carry a server-derivedimageUrl(with?projectId=, so the host never builds an agent-core path).GET /agents/:id/appearance-image/:hashserves it behindcore.agents+ the agent read gate, re-hashing and re-sniffing the bytes on every read,nosniff, an immutable cache (a member whose access is later revoked keeps what their browser already cached), and one404for every failure. Agents list in one deterministic order: oldest first, then by name. - Role-priority gate on provisioning. Independent of the CRUD capability check, an agent's
teamRef.roleis a role assignment and is gated bycanAssignAgentRole(agents/agentAccessPolicy.ts, backed by@neuralis/package-system/accessrolePriority). A caller may only stamp an agent with a role whose priority is>=their own (amembercannot mint anadmin/owneragent) — enforced at the singleAgentService.createFromTeamMemberchoke point (coversPOST /agentsfrom-team andPOST /agents/batch), plus the plain-createconfig.teamRef.rolesmuggle guard and thePATCH /agents/:idteamRef.rolerewrite (→ 403role_priority_forbidden). The caller'spriorityrides onSessionContext.priority; a system session (no priority) bypasses. - Stream and tool execution honour
PolicyEngine(deny / approval-required / allow), package trust ceilings (first-party | trusted | untrusted), input validation against the tool's JSON schema, andPreToolUsehook decisions in priority orderadmin/managed > project/host > user/skill > package. - Tool feature-gate (S2b). A tool's
requires.features(x-neuralis.requires) is the ONE declarative source of its feature gate —resolvePackagePoliciescompiles each into afeature-gaterule (the legacy manifestpolicies[] kind:'access'compile path is removed).PolicyEngine.checkFeatureGatesenforces it on EVERY dispatch (ToolExecutor) and re-evaluates it on approval replay (it sits before the guard-profile block, so a feature-deny is alwaysdeny, neverapproval-required). The check uses the sharedhasFeature— no inlineincludescopy. For model visibility,StreamOrchestratorrunsPolicyEngine.filterVisibleToolsover the per-stream catalog and folds thedenycomplement into the same unavailable-name set the S3 package gate uses, so a feature-gated tool is absent from the catalog (hidden ≠[OFF]), and a hallucinated dispatch is rejected with a generic, non-leakingTool not available: <name>. The host client snapshot'sbuildToolsappliesmeetsRequirestoo (3rd surface). machine-core keeps an in-handler escalation on top of the declarative baseline (its README) — that two-tier shape (declarative baseline + input-dependent in-handlerhasFeature, declared asrequires.escalationsfor catalog/lint) is the sanctioned pattern. PendingInteractionrecords replay only the persistedvalidatedInput(verified againstvalidatedInputHash); approve/deny callers cannot inject replacement tool args.- Hosted MCP requires either a per-agent API key (resolves to that agent's
userId/projectId/agentId) or an OAuth JWT bound to the same scope; the__system__sentinel is rejected on every auth arm (assertNotSentinel;SENTINEL_SETholds exactly the three__system__values).__skill__is an audit principal deliberately outside that set and is refused on its own path instead —executeSkillAction's explicit=== SKILL_USER_IDchain check. - The MCP HTTP server validates URLs and rejects header-based scope override (
x-user-id/x-project-id/x-agent-id). - Principal revocation closes live state; it never decides access (
src/usecases/revokePrincipal.ts, registered as agent-core'sonPrincipalRevoked). A disabled, deleted or signed-out user — or one removed from a project, or whose project was archived, then for that project only — is already refused everywhere because the host produces no session for them; this closes what they opened before. Every reason: the principal's external MCP sessions on:3101close and its MCP OAuth refresh tokens are revoked (mcp/server/companion.tsrevokePrincipal), running turns stop through the holder's HUMANstop(stopStreamsOwnedBy, never the drain'sabort, which would finalize an auto-resumableshutdown), background delegates stop, detached shells are killed (killShellsOwnedBy), PTY sessions are destroyed with every attached view toldexit, which closes its WebSocket (PtySessionManager.closeAllFor), the user's HOST-plane terminals are destroyed through the broker's ownDELETE /pty/sessions/:id(hostTerminalDestroy), whose close the:3101proxy follows, and every terminal view the user holds as a VIEWER closes on both planes (closeTerminalViewsFor— a supervisor's view of another user's PTY included; that PTY and its other viewers stay) — an unreachable broker is logged and its PTYs wait for the broker's idle reap, never re-attachable because every attach re-authenticates. Every reason butsessions_resetalso stops the user's own Telegram pollers (in the one project for a membership removal or archive); the resync asks the ONE hostisPrincipalActive(userId, projectId)port, so they stay down while the user is inactive or no longer a member there and restart after a re-enable or a re-add.deletedalone sweeps: the user's own channel connections (removeChannelConnection, the DELETE route's own usecase), the MCP API-key hashes of the agents they created, their git bindings, and every ACTIVE workflow they created is PAUSED (creator_deleted, never deleted) with the list in theprincipal.live_state_closedaudit row. The MCP session and refresh-token closes are host-called (MountResult.closeSessionsFor,FileOAuthRepository.revokeAllForUser). - Tool result bodies are size-capped through the kernel
boundToolResulthelper (20 KB oncontent + structuredContent;_metais bound separately and never serialized to the LLM).
Identity files (self-improvement substrate)
Each provisioned agent owns three identity files under data://agent-core/<agentId>/identity/:
| File | Volatility | Role |
|---|---|---|
| SOUL.md | stable, living | Personality and role posture plus a few direct links to memories that currently shape durable craft. |
| USER.md | slow, living | A curated, person-scoped set of direct preference/context links; add, replace, or prune after meaningful learning. |
| HEARTBEAT.md | hot, living | A small resume index of current plans, commits, reports, evidence, obligations, and next checks; replace stale links rather than append history. |
The files stay thin but self-evolving. They may link directly to several focused memories or artifacts; no mandatory master-index file sits between identity and memory. Routine facts remain in their owning memory/plan/commit/report/source/README, while identity only curates the links that currently affect persona, collaboration, or resumption. A meaningful task or workflow review may update those links; trivial work need not rewrite identity.
The seed templates ship under team/<slug>/identity/<FILE>.md and are copied into data/agent-core/<agentId>/identity/ at agent provision time. Provision-time substitutions: {{agentId}}, {{agentName}}, {{role}}, {{createdAt}}, {{packageId}}, {{memberSlug}}, {{projectId}}, {{userId}}. At stream injection time InstructionInjector additionally substitutes the runtime {{conversationId}}, {{agentId}}, {{projectId}}, {{userId}} placeholders inside any package-file body — use the flat names only; dotted forms like {{user.id}} are NOT supported. The agent rewrites identity files in place through fs_write against data://agent-core/<agentId>/identity/<FILE>.md. Priority order in the system prompt is SOUL → USER → HEARTBEAT, then any extra *.md files alphabetically. Priority matching is case-insensitive so legacy lowercase identity files keep resolving to the same slot.
Prompt-injected instruction & rule files (the always-on floor)
agent-core authors its own always-on system-prompt contributions as package files (discovered from the instructions/ and rules/ folders — category is derived from the source folder, never an authored field). The set was consolidated from nine narrow files into two dense, always-on files; the detailed how-to now lives in matching skills (context-management, plan, execute-skill, delegate-skill) so the prompt floor stays slim while the method stays discoverable.
| File | Category | Carries |
|---|---|---|
| instructions/neuralis-kernel.md | instruction | Platform model, initiative and craft floor, live-capability discovery, session/memory ownership, source pointers and language. Selective-compaction method lives in context-management; filesystem and tool mechanics live in their owning rules. |
| rules/tool-use.md | rule | Schema-valid calls, recovery, approval boundaries, auditability and the execute security floor. Filesystem mechanics and path/scope rules live in brain-core rules/brain-rules.md. |
These are injected every turn (activation: prompt-context, pinned, invocation: always), so a behavioural FLOOR must live in rules//instructions/, never only in a skill body — a skill that is never activated (especially a headless workflow run) would otherwise lose the floor entirely.
The seven base subagents (agents/*.md)
agent-core ships the platform's universal delegation roles. delegate resolves an omitted
agent to general.
| Agent | The question it answers |
|---|---|
| explore | Where does something live, in how many places, or does it exist at all |
| research | What is true outside this workspace — an API, a version, a price, a convention |
| planner | What is the step sequence, and is it the right one |
| execute | Carry out agreed work end to end |
| validate | Is this plan, change or artifact sound |
| operate | Does the running system actually behave as claimed |
| general | Anything that fits no sharper role — the default |
They chain on roles do not overlap: whoever plans does not execute, whoever executes does not certify their own work, whoever validates does not fix.
Two properties make them base, and both fail silently if broken — agents/__tests__/baseSubagents.test.ts derives them from the shipped files:
- No
allowed-tools. Each inherits the caller's full (already S2/S3/scope-filtered) tool set, so it reaches package tools, MCP tools and anything the user adds later. A fixed allowlist would blind it to all of them with no error anywhere. Narrowing belongs to the CALL —delegate'stools/blocked_tools/disabled_packages. ABSENT is the load-bearing word: a persona that declaresallowed-tools: []is making the opposite statement and receives no tools at all, which is why the parser keeps the two apart. The honest consequence: a "read-only" base agent is read-only by instruction, not by contract; passblocked_toolswhere that must be enforced. Custom agents that exist to be restricted may of course declare their own allowlist — and that list is a CEILING: a call's owntoolsis INTERSECTED with it, never added to it. A call can narrow a persona, never widen one. Thetoolslist is answered in three states, and the ceiling is only one of the reasons a name falls out of it: every requested name is measured against the child's own catalog first, whatever survives runs, whatever does not is NAMED BACK in the delegate result (not in the child's catalog · barred from every subagent · outside the persona's list · removed by the call's ownblocked_tools·delegateat the depth limit), and a list of which NOTHING survives is refused before any write rather than starting a tool-less child. A name outside that catalog is reported in the same words whether it is hidden from the caller or invented — telling those apart would disclose the persona's list. The candidate set is ALSO narrowed by the packages this call disables for the child, which is why the sentence names the SUBAGENT's catalog and not the caller's: "not available to you" would be false exactly there. Bothtoolsandblocked_toolsare matched after the same wire-prefix normalization — a denylist that silently blocks nothing is the one direction in which a missed name hands the child MORE. - No
requiredFeatures. A gated base agent is not a base agent. The four retired brain-core agents (planner-agent/reviewer-agent/scaffolder-agent/brain-search-agent) were package-authoring specialists, not these seven — and three of the four sat behindpackages.author, which member and viewer do not hold, so a member saw two of the five shipped agents and nothing anywhere said which two were missing.
Their description is the whole routing surface, and <packages> truncates a non-skill
file's description at 80 chars (MAX_FILE_DESC_CHARS) — a longer one is a CUT one, losing
exactly the discriminator at the end. The role-picking table lives in the delegate-skill
body, where there is room for it.
delegate applies S2 + S3 to agent resolution, not just to the child's tools. An agent
body BECOMES the child's system prompt, so which agents exist for this caller is the same
deny-by-default question as which files get injected. resolveAgent / resolveDefaultAgent
tak
