Self-hosting and data residency
@open-multi-agent/core is a library, not a service. There is no OMA backend,
no account, and no hosted control plane: the framework runs entirely inside
your Node.js process, and everything it persists is written through a store you
supply. This page collects the facts a review needs in one place. Each claim is
about the framework itself; where your own configuration decides the answer,
that is said explicitly.
Runtime footprint
Section titled “Runtime footprint”Core’s runtime dependencies are:
| Package | Purpose |
|---|---|
@anthropic-ai/sdk | Anthropic provider adapter |
openai | OpenAI adapter and every OpenAI-compatible provider, including local servers |
zod | Tool input schemas and structured output validation |
Everything else is a peer dependency and optional: the ACP SDK, the MCP SDK,
the Google GenAI SDK, the AWS Bedrock client, and the Vercel AI SDK. Each is
loaded through a dynamic import() at the moment a feature needs it, so an
install that never uses ACP never resolves the ACP SDK. Provider adapter
modules are dynamically imported too, so the two bundled provider SDKs load
only when an adapter that needs them is constructed.
The supported runtime is Node.js 20 or newer. OpenTelemetry lives in the
separate @open-multi-agent/otel package and is never pulled in by the core
import.
What connects to the network
Section titled “What connects to the network”The framework opens outbound HTTP for one purpose: talking to the LLM endpoint
your configuration names. AgentConfig.baseURL and the provider environment
variables decide where that is. Point them at a local server and the framework
has no remaining reason to leave the machine. The one provider that reaches
past its own model API is GitHub Copilot, whose adapter also calls
api.github.com for a token and, for interactive device login, github.com.
There is no telemetry, no analytics, no license check, no update check, and no phone-home of any kind in the package. “Telemetry” inside this codebase means the local observability subsystem: trace records that go to a sink you construct and a store you own. Nothing is transmitted unless you wire an exporter yourself, and the OpenTelemetry adapter that would do so is a separate optional package with its own lifecycle.
Four things start subprocesses rather than connections, and each is something you configured:
- MCP servers connect over stdio only. The MCP client uses
StdioClientTransport, so a server is a child process on the same machine. There is no HTTP or SSE MCP transport in core. processandacpbackends spawn the command you name. See external agents.- The
bashtool runs commands through a local shell. - Process-tree cleanup shells out to
taskkillon Windows to terminate a child’s descendants.
Each of those children has your process’s permissions and its own network access. The framework starts them and exchanges bytes; it does not contain them.
Bounding LLM egress
Section titled “Bounding LLM egress”egressPolicy narrows where a framework-owned LLM request may go. Two modes:
offlinepermits only loopback origins:localhost,*.localhost,127.0.0.0/8, and[::1].allowlistpermits only the exact HTTP(S) origins you list.
Policies configured at the orchestrator, run, and agent levels are intersected, so a narrower scope can tighten but never widen a broader one. Enforcement means the origin is checked before the provider SDK loads, a guarded fetch re-checks every request, and redirects are rejected. An adapter OMA cannot enforce fails closed rather than proceeding unguarded.
Read the scope statement carefully. The policy covers LLM requests the
framework itself issues. It does not cover tool code, the bash tool, MCP
server internals, process and ACP children, application callbacks, or your own
trace exporters. offline is a statement about OMA’s provider calls, not
evidence that the Node.js process and its descendants are offline. The full
per-surface table is the
egress enforcement matrix.
Where state lives
Section titled “Where state lives”Nothing is persisted anywhere you did not choose. Shared memory, checkpoints,
durable approvals, and — when one is configured — authoritative
run records all go through the MemoryStore interface, and core
ships three implementations:
| Store | Durability | Notes |
|---|---|---|
InMemoryStore | None; dies with the process | Process-local compareAndSet |
FileStore | One JSON file, rewritten atomically per mutation | Single Node process at a time; no cross-process file lock |
RedactingStore | Delegates to whatever it wraps | Scrubs values on write; deliberately exposes no compareAndSet, so it cannot back durable approvals. See redaction |
There is no bundled database store. Redis, Postgres, and SQLite appear in
the source only as examples of what an application might implement behind
MemoryStore. Writing one is the supported path when you outgrow a file.
Cross-process atomicity is not provided. FileStore.compareAndSet is
atomic among callers sharing one FileStore instance, and that is the whole
guarantee. A cross-process reviewer should therefore decide only after the
suspended runner has exited, or the deployment needs a database-backed store
whose compareAndSet is atomic across all writers. This matters most for
durable approvals, where the
decision write is the correctness boundary, and for the
run store, where it decides whether two workers can advance the
same run. MemoryStoreRunStore therefore declares atomicity: 'process' unless
you tell it otherwise; the shipped stores never qualify for 'cross-process'.
Trace records and evaluation records have their own stores and the same shape
of choice: InMemoryTraceStore or FileTraceStore, InMemoryEvalStore or
FileEvalStore. Both file-backed variants are append-only local files.
Filesystem scope
Section titled “Filesystem scope”Built-in filesystem tools resolve every path, symlinks included, inside a
sandbox root. Unset, that root is <process.cwd()>/.agent-workspace, created
on first write. OrchestratorConfig.defaultCwd or AgentConfig.cwd moves it,
process.cwd() widens it to the whole project, and null disables it.
The bash tool is not sandboxed. It takes a cwd argument straight from
the model and runs the command through a shell with your process’s
permissions. The sandbox root does not constrain it, and neither does the
per-call gate unless you configure one. Granting bash is granting shell
access to the host; see tool configuration for the
grant model and the per-call onToolCall gate, and
sandbox and shell execution for the sandbox root, its
resolution rules, and replaceable shell executors.
Redaction
Section titled “Redaction”A shared credential redactor runs at several surfaces before content is persisted or exported:
- trace attributes and the tool input/output an agent run records
- observability status messages and record processors
- task metadata
bashtool outputprocessbackend stderr on a failed exit- evaluation sampling and report payloads
RedactingStorevalues, andJsonlRunJournalevents whenredactis set
Its built-in patterns are credential-shaped: key/secret/token/password-style
field names, Authorization and Bearer headers, PEM private key blocks, and
literal token formats such as sk-, ghp_, AKIA, and xox-prefixed keys.
PII is not covered by default. Emails, national IDs, account numbers, and
anything else domain-specific pass through unchanged unless you supply the
patterns yourself. The two surfaces that accept them are RedactingStore and
JsonlRunJournal, both through the patterns option (the underlying helper
calls the same list extraPatterns); the other surfaces above run the built-in
patterns only and take no caller additions. Redaction is also best-effort by
construction: it is a set of regular expressions over text, not a classifier.
Do not rely on it as the only control over what an agent may emit.
One boundary is easy to miss: telemetry redaction does not reach persisted run
state. Shared-memory writes and checkpoint saves store agent output verbatim
unless the store itself is wrapped in RedactingStore. See
shared memory.
Air-gapped deployment
Section titled “Air-gapped deployment”Two pieces of configuration cover the framework’s own behavior:
- An OpenAI-compatible endpoint. Configure it as
provider: 'openai'with abaseURLpointing at the server, plus a non-empty placeholderapiKeybecause the OpenAI SDK validates that the field is set even when the server ignores it. See local model tool-calling. - An egress policy, so a stale
baseURLor an inherited environment variable fails closed instead of reaching a hosted provider. Use{ mode: 'offline' }when the endpoint is on the same host: it permits loopback only, and deliberately rejects private LAN addresses. A server elsewhere on the network needs{ mode: 'allowlist', allowedOrigins: ['http://<host>:<port>'] }instead, becauseofflinewould block it.
Everything else in the boundary is outside what the framework can enforce: an
MCP server that calls out, a bash-granted agent, an external backend whose
child has network access, or a trace exporter pointed off the machine. The
framework will not open a connection on its own, but it also cannot contain the
code it starts on your behalf. A network namespace, an egress proxy, or a host
firewall is the control that actually does.
What is not provided
Section titled “What is not provided”So that nothing here is read as more than it is, the framework does not include:
- Multi-tenancy. There is no tenant concept. Isolating one customer’s runs, stores, and workspaces from another’s is an application concern.
- RBAC or any authorization model. No roles, no permissions, no policy
engine. The tool grant model and
onToolCallgate are per-agent configuration, not an identity system. - SSO or authentication. OMA never authenticates anyone. A durable approval
records the
reviewer.idstring you pass it and validates only that it is non-empty. - An approval UI or workflow. The suspend/decide/resume API and the durable record exist; the reviewer-facing product does not. See durable approvals.
- Tamper-evident audit. The run journal has no hash chain, no signature, and no WORM storage. See identity and integrity.
- A database-backed store. Only in-memory and single-file implementations ship. Anything with cross-process guarantees is yours to write.
- Encryption at rest. File-backed stores write plain JSON and newline-delimited JSON. Use disk or filesystem encryption appropriate to the data those files will hold.