@brain-core

API usage

The brain-core public surface agents and packages consume: the four fs_* tools, the source and URI-policy HTTP routes with their feature gates, and the typed package API other packages call.

brain-core exposes three public surfaces, each with its own caller and its own gate:

  1. The four fs_* tools — what the model calls to read, list, search and write files across the URI-native filesystem.
  2. The HTTP routes under /api/packages/brain-core/* — what the workspace UI and skill scripts (via the per-stream session ticket) call for the filesystem, source management, and URI-policy surfaces.
  3. The typed package API (getPackageApi('@neuralis/brain-core')) — what other first-party packages call directly, never through MCP.

All three are scoped to the caller's verified SessionContext (userId, projectId, optional agentId) and gated deny-by-default by the same drive.* features. See Features and access for how feature grants and role defaults work.

The four fs_* tools

Each tool declares its own feature gate on its schema (x-neuralis.requires.features); the matching HTTP route carries the same gate independently, so the agent path and the direct-HTTP path are both checked. The Tools page is the full per-tool reference — the three search modes and every write key. This section is the quick task-shaped index.

fs_read — read file contents

Gate: drive.read. Batch up to 10 files by URI or UUID in one call. An entry can carry its OWN line window, so a single call can take a slice of one file and all of another; a top-level line_start/line_end applies to the entries that do not bring their own, and read_version: 'baseline' reads the pre-edit snapshot. Connector-first — returns live data even when the index is cold.

{
  "files": [
    { "uri": "data://notes/todo.md", "line_start": 1, "line_end": 200 },
    "brain://insights/q3.md"
  ],
  "include_ids": true
}

Text comes back line-numbered under a lines X-Y / total header, so a follow-up edit can address the lines it just read. A whole-file read that outgrows the snippet cap is trimmed on a line boundary and says how large the file is and which line to continue from; a window you asked for is never trimmed.

Result: the text (or vision content blocks for images / video / PDFs) of each requested file, with per-file UUIDs when include_ids is set.

A batch answers 200 even when some files failed — one bad URI must not sink the other nine — so failures ride the response as { uri, error, code? }. Branch on code, never on the error text:

codeMeaning
not_foundThe file is genuinely gone. Something confirmed it: storage answered "no such file". Safe to drop it from a cached view.
forbiddenYou may not read it — a path-policy deny or a disabled source. The file still exists; never treat this as absence.
(absent)Something else went wrong and nothing can be concluded. Retryable.

not_found is deliberately withheld when the underlying storage could not be reached at all, so a temporary outage can never be mistaken for a deletion. Error strings never contain server paths.

fs_list — list files as a tree

Gate: drive.read. Renders a compact tree across indexed and live sources. Omit uri (or pass /) for a full root view (all://) with a per-source registry line; recursive: false returns direct children only (UI folder expansion). Presence badges (● synced, ○ unindexed, ✗ stale) tag only entries that differ from their parent; on a git-backed source a second marker rides alongside them ([M] modified, A added, D deleted, R renamed, ? untracked, staged and unstaged).

{ "uri": "data://agent-core", "recursive": false }

Result: a tree (or flat list) of folders and files, with optional inline content previews (summary_chars) and verbose per-file metadata (include_metadata). sources_only: true answers with the source registry alone — which sources exist and what state they are in, with nothing walked — and limit accepts any size down to a single entry.

fs_search — find files across sources

Gate: drive.search. Three modes: semantic (embedding-based, fused with the indexed content-text ranking, needs query), filter (metadata plus keyword — category, tags, dates, no embedding cost; query is a tokenized, case-insensitive keyword over indexed content, ranked by term frequency, and glob matches filenames), and grep (literal ripgrep / git grep over live connector content, needs folder_uri + query; the match is a FIXED STRING, never a regular expression; glob narrows the scanned files, path_glob takes a real path glob, case_sensitive honours the query's case and context returns up to five lines around each match). The HTTP search route speaks the same search_mode vocabulary as the tool. In every mode fs_search enforces the per-URI read policy on each result — a read:false path is silently absent and a fully-denied folder_uri returns 403 — so grep lets a caller with drive.search + read policy grep a path without any execute / shell access.

{ "search_mode": "semantic", "query": "deployment checklist", "limit": 10 }

Result: ranked file matches (URI, snippet, score for semantic) up to limit, plus truncated and total_seen. Every mode sets them: a set that was cut at its limit says so, so a partial answer is never read as proof that the rest does not exist.

fs_write — the write verb

Gate: drive.write. One tool, one key per operation. The content keys — create, overwrite, edit, insert, lines, fields — compose in a single call and land as one write with one undo point; the structural keys — move, copy, mkdir, revert, delete — each stand alone.

{
  "uri": "data://notes/todo.md",
  "edit": [{ "old_string": "draft", "new_string": "final" }],
  "fields": { "tags": { "add": ["reviewed"] } }
}

Result: the node, the keys that were applied, and the URI the write actually landed at. Nothing is written unless every key can be applied, so a failed edit leaves the file untouched. create never overwrites — an occupied name lands beside it as name-2.ext and the answer reports the URI used. A refusal comes back as ok:false on a 200, so a client must read ok before it shows the change as done. delete asks for confirmation under the balanced permission mode and is a revertible tombstone wherever a revert baseline can be taken; every write snapshots one, so revert can undo it once. A write to a path the source excludes from indexing is saved and revertible, and the answer says it is not searchable.

The HTTP route surface

Every route is served at /api/packages/brain-core/<pattern> and discovered by the package-system route dispatcher, which enforces the declared feature against the caller's grantedFeatures before the handler runs. The handler then re-derives projectId / agentId from the verified session — a projectId in the body or query that disagrees with the session is a 403. See Sessions for the session ticket the workspace UI and skill scripts authenticate with.

Filesystem routes

These back the fs_* tools and the Files widget. The route gate matches the tool gate, so the same deny-by-default rule applies whether the caller is the model or the UI. The correspondence is close but not one-to-one: the tools run in-process against the filesystem service, so a route may accept a narrower payload than its tool. Every fs_* capability has a route twin — including writing, where the JSON write route mirrors fs_write (the Files widget's "New File" still goes through upload with an empty body).

PatternFeatureMethodPurpose
readdrive.readPOSTRead file contents. Takes files[] — each entry a URI string or {uri, line_start?, line_end?} — plus the same line_start / line_end, read_version and include_ids options the fs_read tool has. Every entry's URI is path-policy checked before anything is read, and an entry that is neither a string nor a {uri, …} object is a 400.
listdrive.readGET, POSTList a scope as a tree. Both methods run the same handler — the GET arm takes its options from the query string so a browser can link a folder view.
searchdrive.searchPOSTSemantic / filter / grep search.
writedrive.writePOSTThe write route, and the twin of fs_write over the same entry point: the body keys are the same operations (create, overwrite, edit, insert, lines, fields, move, copy, mkdir, revert, delete), plus uri and — on this surface only — id, which the Files widget's pending-changes panel uses to address a node. The handler reads an explicit list of fields and nothing else: the governance flags that decide whether a write lands committed or held for review are never taken from a body, only from whether the request came from a signed-in browser session. A refusal answers 200 with ok:false, so a client must read ok before showing the change as done; a disabled source is a 409 and a path-policy denial a 403.
approvedrive.writePOSTConfirm a pending review using its fresh preview fingerprint. A terminal retry is owner-gated and idempotent; uri is omitted without current READ and WRITE access.
pendingdrive.readGET, POSTThe pending-change review queue. GET lists it — pass include_orphans and the same pass also returns orphanIds, so a caller that renders both does not run the listing twice; count_only returns just { count } from the same per-record filtered listing, for a badge; the POST verbs are scan-orphans, revert, discard, dismiss and discard-orphans. The module feature only guarantees drive.read — every mutating verb re-checks drive.write in the handler before any record lookup.
uploaddrive.writePOST (multipart)Upload bytes into a source, and the create path behind the Files widget's "New File" (an empty body from a browser session). It runs on the same write engine as write, so a disabled source is refused the way a create into it always was, an agent's upload is recorded in the review queue and can be reverted, and the empty-file allowance comes from the browser session rather than a form field. An occupied uri is never overwritten — the upload lands beside it as name (2).ext, and the response uri says where. Multipart cannot carry identity in a body or query, so the form's agentId / projectId are passed as explicit overrides into the same session projection every other route uses — a project override is still a 403. The response carries indexed: false means the bytes reached the source but indexing was skipped by the source selection or failed (a provider outage or quota). An eligible sync retries failed indexing; excluded uploads remain unindexed until the selection changes. A READ-floored upload is refused before any bytes are written. Uploads to brain:// have no such middle state — there the vector is the content, so a failed embed is a real error. The part's declared Content-Type is not stored as given: the filename extension decides when it maps to a known type, the declared value is kept only for an extension the platform does not recognise, and a claim of a common image format that the file's own leading bytes contradict falls back to application/octet-stream.
rawdrive.readGETStream raw file bytes (binary-safe). The response tells the browser what it may do with them, and that decision does not come from the file: images (except SVG), video, audio, PDF, text and JSON are served inline, so previews work as before; the executable document class — SVG, HTML, XHTML, XML and every +xml subtype — is served with its true type and Content-Disposition: attachment, so it downloads normally and never becomes a live document on the app origin; anything else downloads too, keeping its real type. Filenames travel as a sanitized ASCII form plus the RFC 5987 UTF-8 form, so non-ASCII names survive the download.
git/*drive.readGET, POSTThe governed git surface behind the Git Control panel and the git skill — nine GET sub-paths (status, branch, diff, log, log-stats, commit, outgoing, show, repos) and ten POST sub-paths (stage, unstage, discard, commit, branch, checkout, restore, fetch, pull, push). The module feature only guarantees drive.read; every POST re-checks drive.write in the handler, and fetch / pull / push additionally require the uri-policy exec capability on the source root.
eventsdrive.readGET (SSE)Per-project filesystem event stream for the UI. It also carries a content-free pending:changed signal (only the project id) whenever the project's review queue moves, so a client re-asks its own count instead of polling.
health/*drive.readGET, POSTVector-index health: the bare path returns cheap count-only stats and the scan sub-path runs the on-demand orphan/duplicate integrity scan — both reads, and both methods answer identically. The gc sub-path is not a read: it takes an action of gc (retention sweep), repair-orphans or repair-pending (the caller's own pending changes by repair class, run as the caller — orphaned ones are discarded, others are only counted), and dryRun defaults to false, so an un-parameterised call really removes points — deduplicate is read-only but rides the same gate, since its rows enumerate other agents' data:// paths. The module feature only guarantees drive.read; the gc sub-path re-checks drive.write in the handler, and it is POST-only — GET /health/gc answers 405, so a link or a prefetch can never start a sweep; a gc, repair-orphans or repair-pending pass already running for the same project answers 409 action_in_progress. GET health/hot-uris lists the files the index rewrote most in the last hour; it also requires drive.write and is filtered per file to what the caller may read. health/collection is the collection-level maintenance surface — GET reports, POST {action} runs one of migrate-indexes, apply-tuning, apply-quantization, enable-text-ranking, backfill-text-ranking (with cursor / batches), or the embedding-model switch: rebuild-estimate (200, a sample-based estimate, changes nothing), rebuild-generation (202, starts or resumes the rebuild into a new collection) and rebuild-cancel; GET also carries the active model, the target, a running rebuild's progress and the last estimate — null once the target model changed. One collection serves every project, so it requires platform.vector.maintain.

The read, write, upload, raw, search, and git/* routes run the URI-policy guard inside the handler, before they reach the filesystem service — a denied path surfaces as a structured 403 { code: 'uri_policy_denied', capability, uri } instead of a generic error, and a disabled source returns 409 { code: 'source_disabled', source, scope, nextStep: 'enable_source' }. A source slug the project has no config for is a third answer: 404 { code: 'source_not_configured', source, retryable: false, nextStep: 'check_configured_sources', availableSources } — the last field being the caller's own visible sources, so a wrong guess is fixable in one round trip. Which of the two refusals you see depends on where the URI is caught: the in-handler guard runs first and denies an unconfigured source as uri_policy_denied, so the 404 is what list returns, and what write and upload return for a URI with no scheme. read is different again — it reports per file inside a 200. list reaches the same verdict one layer lower, in the service, and reports it in the same uri_policy_denied shape. Connectors never enforce policy themselves: every gate sits above them, which is why a route is the safe way in and a raw connector call is not. See URI policies.

Source-management routes

sources/* is the CRUD and lifecycle surface for scoped sources. Reading a source config needs drive.read. Every source-config mutation — create, update, delete, and the enabled toggle — needs drive.mount, not drive.write: deciding which roots a project mounts is a mount decision, not a file write, so a member who may edit files still cannot re-point a source. The two routes that ENUMERATE THE HOST rather than the config — sources/browse and sources/discoverable — are on the drive.mount side too: they are the step that precedes attaching, so you may browse exactly what you would be allowed to attach. Sync control needs drive.sync.

Sub-routeFeatureMethodPurpose
sourcesdrive.read (GET) / drive.mount (POST, PATCH, DELETE)GET, POST, PATCH, DELETEList, create, update, and remove sources. PATCH preserves sync when omitted; pass clearSync: true to disable indexing. sync.media {images?, pdf?, video?, audio?} (booleans; an absent key keeps the default — images and PDFs on, video and audio off) chooses which media kinds a multimodal active model embeds, and the last sync result carries mediaSkipped counts. It does not preserve permissions — that field is replaced wholesale, so a body without paths drops every non-floor path rule. Round-trip the current object.
sources/<slug>drive.readGETOne source's public config. Carries description (what is stored) alongside description_effective (what the agent reads — the stored text, else the owning package's declaration, else the connector kind). DELETE takes the slug on the same path.
sources/kinds · sources/kinds/:kind/schemadrive.readGETEnumerate connector kinds and their config schemas. :kind is the registry kind (local, brain, webtop, host), never a connector's manifest id or its UI label — the on-disk connector is id local-disk, kind local. An unregistered value answers 404 Unknown kind "<x>" for every caller, so that 404 is never a permission signal.
sources/discoverabledrive.mountGETWell-known unattached source roots the user can attach. Each suggestion names a real host path, so it answers to the attach feature, not the read one.
sources/browsedrive.mountGETBrowse directories before attaching, one level at a time via ?path=. The filesystem root and the privileged zones additionally need drive.mount.privileged, and a privileged directory is omitted from the listing of a parent you may browse — an absent entry is never proof that nothing is there. Errors carry no path and no errno, so a missing directory and an unreadable one are indistinguishable. In a container deployment it lists the operator-provided mount root and ignores path.
sources/healthdrive.readGETPer-source sync-status rows (source, source_kind, sync_status, last_sync) for the sources the caller can SEE — the same scope filter the list answers with, because a health row states a source's existence — and not index counts (those come from fs_list with include_counts).
sources/sync · sources/reindexdrive.syncPOSTTrigger a sync / a full rescan (202 + queued job snapshot). sources/sync answers 404 when the slug does not resolve for the caller — an unknown source and one outside the caller's scope answer alike — and 400 when it resolves but carries no sync configuration. A plain reindex {source} walks the whole source again, and a file whose content did not change keeps its vectors. The body {source, folder? | uri?, force?, estimate?} scopes a re-embed with the active model: force: true embeds every file in scope again whether it changed or not (202 + the job, with the readable document count; omit folder and uri for the whole source), estimate: true instead answers 200 with the sample-based cost of doing so (each sampled text clamped to the model's per-input cap) and enqueues nothing. A folder/uri without force or estimate, or both at once, is 400; the scope must lie inside the source; only files the caller may read are counted or embedded, and an unreadable uri answers 404 exactly like an absent one.
sources/cancel · sources/resetdrive.syncPOSTCancel a per-source (or all visible) job. Reset forgets the source's sync state — it cancels the active job and clears the markers the last sync left, so the next sync compares every file; it deletes no file and no vector, and enqueues nothing.
sources/sync-statusdrive.syncGETCurrent queue job snapshots.
sources/enableddrive.mountPOSTToggle a source live / disabled ('auto' is machine-only).
sources/preflightdrive.mount plus the create target's conditional mount grantsPOSTValidate and authorize a source-create body without writing; the subsequent create repeats every gate.

Two fields escalate beyond drive.mount on create and update. A privileged root (the platform app zone, the Neuralis source tree, the whole host) also requires drive.mount.privileged, and a host-plane root requires drive.mount.host on top of it. recognizesPackages — the flag that lets a source contribute loadable package code — is owner/admin strength, resolved from the role's priority rather than its name, so a custom role declared at that strength qualifies and one merely named "admin" does not.

Mutations on a cross-user or cross-agent source additionally check canAccessScope — gated by filesystem.observeScoped (read) / filesystem.modifyScoped (write). The grant decides, never the role's name: both are enumerated in the seeded admin role's default grants, and owner reaches them through the '*' wildcard; manager, member and viewer hold neither. So anyone else — whatever their role is called — reaches another user's scoped source only through an explicit grant or the wildcard. See Sources and connectors for the source model and Memory and sync for the sync pipeline.

URI-policy routes

policies/uri/* is the path-protection editing surface. Reads are drive.read; writing a path rule escalates to drive.policy — an owner/admin default that is grantable to a custom role.

Sub-routeFeatureMethodPurpose
policies/uri/manifestsdrive.readGETPer-package uriPolicies baselines resolved at boot.
policies/uri/<source>drive.readGETRead a source's effective merged path permissions.
policies/uridrive.policyPOSTPatch path rules. The source is named in the body (source) together with addPaths / removePatterns; at least one must be non-empty. A floor: true marker in addPaths is refused (only a trusted package manifest may declare a floor) and removing an existing floor pattern answers 409 immutable_policy_floor.

Editing a cross-user / cross-agent source's path rules is a WRITE on that scope, so a POST also runs canAccessScope(scope, …, 'write') — an observeScoped-but-not-modifyScoped drive.policy holder cannot rewrite a scoped source's policy. The full evaluator semantics live on URI policies.

Feature gates at a glance

FeatureGrants
drive.readRead, list, raw bytes, events (SSE), pending-change reads, git reads, source-config and policy reads, health reads — the vector-health stats and scan, not the gc sweep.
drive.searchSearch (semantic / filter / grep); each result is still per-URI read-gated.
drive.writeCreate, modify, mkdir, copy, approve, upload, the mutating pending verbs, git writes, the vector-health gc sweep (POST health/gc), and the hot-file list (GET health/hot-uris).
drive.syncTrigger / cancel / reset sync, reindex, read sync status.
drive.mountEvery source-config mutation — attach a new root, update or delete a source, toggle its enabled state — plus the two host-enumeration reads that precede an attach: sources/browse and sources/discoverable.
drive.mount.privilegedAttach, see, or browse a high-blast-radius root — the platform app zone, the Neuralis source tree, the whole host. No grant below the admin tier.
drive.mount.hostAttach or edit a source resolving outside the container through the host broker; required together with drive.mount.privileged. No grant below the admin tier.
drive.policyEdit per-source URI path-access policies (owner/admin by default).
filesystem.observeScopedSee another user's / agent's scoped sources.
filesystem.modifyScopedMutate another user's / agent's scoped sources.
packages.authorThe package-authoring affordances — the Packages panel scaffold launcher and the install / build / rescan routes.
packages.registryBe shown the package-registry skill.
platform.vector.maintainThe vector collection's maintenance actions (health/collection) — platform-wide, so no role holds it by default.

Role defaults (from the manifest): viewer gets drive.read only; member adds drive.search + packages.registry; manager adds drive.write, drive.sync, plain drive.mount and packages.author. The escalated grants — drive.mount.privileged, drive.mount.host, drive.policy, and the scoped filesystem.observeScoped / filesystem.modifyScoped pair — start at the admin tier, and owner reaches every one of them through the '*' wildcard. Grants are deny-by-default — an ungranted feature hides both the tool and the route.

The typed package API

Other first-party packages call brain-core directly through getPackageApi('@neuralis/brain-core') — never through MCP (direct internal APIs). The returned object is project-aware: most calls take a projectId and resolve per-project services. The surface consumers actually rely on today:

MethodReturnsConsumed by
getProjectServices(projectId)The per-project FilesystemService, UI service, and bridge.Any package needing direct filesystem access.
getFilesystem(projectId)The project's FilesystemService directly.agent-core (resolve a project's filesystem).
listShellSourceRoots(projectId)Every local-kind source with its resolved container root + persisted permissions.agent-core's shell URI-policy gate — maps a shell cwd / path token to the owning source's policy.
listSourceRuntimeSummaries(projectId, opts)Access-filtered per-source summaries (description + last-sync stats + liveness), read from persisted config only — no live walk. The description is RESOLVED, not merely read back: the authored text, else what the owning package declared for that source, else what the connector kind says it is.agent-core's <runtime_stack> source table, loaded once per stream; the admin source-config editor for the same resolved text.
getProjectInfra(projectId)The project's own connector registry (resolveExactSource / resolveBySource) and event bus.The terminal package, to resolve a source's connector for a shell tab. Connector instances are per project — there is no project-less lookup.
listPackageSourceRoots(projectId)Every project-associated source with the fields needed to decide whether it may contribute package code (scope, kind, enabled, the recognizesPackages flag).The host, when activating source packages. Identity-free like the shell-roots call — the host applies the trust filter.
subscribeFilesystemEvents(session, agentIds, types, onEvent)An unsubscribe handle.The host's single /api/events realtime hub. Authorizes each agent id deny-by-default against the caller's SessionContext, subscribes only to the survivors, and filters every event — one authorization path shared with the events route.
upsertSourceConfig(scope, patch){ ok: true } or { ok: false, error }admin, the single validated source-config mutation point.

The package-source-roots service delegates to the existing listPackageSourceRoots(projectId) enumerator with {slug, uriRoot, containerRoot, scope, recognizesPackages, enabled, kind} rows. It is trusted activation metadata; scope and URI-policy checks remain at the consumer boundary. Configuration is read through ctx.config, credential invalidation uses the global-scope lifecycle hook, and structured vector health is queried through the runtime. An enabled: "auto" source asks its own connector's isRuntimeActive() without starting that runtime.

listShellSourceRoots and listSourceRuntimeSummaries are the two most load-bearing calls: agent-core invokes them on every stream to build the shell gate and the runtime-stack source table. Both read persisted source-config JSON (plus one cheap liveness probe for the summary), so they sit safely in the cacheable system-prompt prefix. The shell-roots list is intentionally not access-filtered — the consumer evaluates each source's permissions against the caller's own session via the package-system URI-policy evaluator. The runtime-summary list is scope- and feature-filtered (canAccessScope → filesystem.observeScoped), deny-by-default.

On this page