Architecture
Boundaries
Each part of Vyre is independently modular. A part talks to another part only through the
registry (ctx.call, tools) and events. It never imports another part's files. A part can be
switched off without breaking the rest.
- The kernel is
core/config,core/store,core/modules,core/presenceandcore/daemon. Any part may import it. - A part is
core/<name>(a folder, or a single file such ascore/quiet.js),local/<name>ormodules/<name>. - A lib is
lib/<name>: shared pure code with no feature state (ADR 0033). Any part may import a lib. A lib imports only the kernel and other libs, never a feature, and no lib edge can be frozen.
test/boundaries.test.js scans every runtime .js, .mjs and .cjs file under core/,
local/, modules/ and lib/ for relative imports (static, dynamic and require). Tests,
testing/ folders and fixtures are out of scope, since a test may reach into what it tests. The
test fails on:
- an import into another part that is not in its allowlist, or a new file behind a frozen edge;
- an allowlist entry that nothing imports any more (the list only shrinks);
- an allowlist edge missing from this page.
A new exception needs the lead's OK.
The frozen exceptions
Frozen from main on 27 September 2026: 26 edges, 24 now (connectors moved its shared credential,
mail-message and on_behalf helpers into lib/connectors on 2026-09-28, which removed
core/google -> core/connectors and core/mcp -> core/connectors entirely rather than freezing
their wider imports). "Becomes" says where each remaining one should go:
- ctx.call: call a tool through the registry instead.
- lib: the imported file is a pure helper; move it to
lib/<name>. - surface: the CLI is a surface, and keeps its command-side helpers.
| Edge | Files | Why | Becomes |
|---|---|---|---|
core/cli -> core/names | backup.js, system.js | vyre backup and vyre up write the box backup and the systemd unit in-process | ctx.call |
core/cli -> core/recall | embed.js, progress.js | vyre status and vyre doctor read index progress and the embedder's state directly | ctx.call |
core/comms -> core/mail | imap.js | the sending service reuses the mail module's IMAP adapter; it becomes a ctx.call when the adapter has a tool | ctx.call |
core/files -> core/google | drive-seam.js | linked Google Drive folders use the Drive seam's client directly; it goes through google.drive.* tools when the bridge is split | ctx.call |
core/import -> core/sessions | credfiles.js | import must never move a login file; it shares the one credential-file test, which belongs in lib | lib |
core/runner -> core/sessions | credfiles.js | the runner skips login files when it reads or syncs a home; it shares the one credential-file test, which belongs in lib | lib |
core/cli -> core/resilience | backoff.js, node.js, stream.js | the reference client every surface uses, a pure library | lib |
core/cli -> core/vault | backup.js, cli-io.js, refs.js | vyre vault's terminal side: no-echo prompts, vault:// refs, the sealed backup format | surface |
core/cli -> local/voice | talk.js | vyre voice, push-to-talk from a terminal until the native Lumen has voice | ctx.call |
core/daemon -> core/harness | rules.js | the kernel runs the security floor on every call's input; the floor belongs in the kernel | lib |
core/daemon -> core/names | guests.js | the router asks whether a tailnet caller is a guest before the registry | ctx.call |
core/daemon -> core/runner | homesandbox.js, sandbox.js | the daemon composes the runner's home sandbox for the Switchboard (core/sessions cannot import core/runner) | lib |
core/daemon -> core/sessions | drivers/openrouter.js | the daemon hands the kernel's inference door its providers: the API-key chat drivers' door side (the door scans first, this only makes the call with the key the session passes); one-way, the drivers import nothing from the daemon | lib |
core/daemon -> core/runner | homesandbox.js, sandbox.js, lent-home.js | the daemon composes the runner's home sandbox for the Switchboard (core/sessions cannot import core/runner) | lib |
core/daemon -> core/spawner | client.js, confine.js | a session in the packaged box is confined by its own uid, and the daemon composes the self-test that proves it before every start; it asks the root spawner, which is not a module | lib |
core/daemon -> core/switchboard | sessions.js | the router resolves which Claude Code session a call comes from | ctx.call |
core/daemon -> core/wink | node/peer-wire.js | the daemon composes the home's peer door for a paired device's relay stream (core/daemon/peer-door.js) | lib |
core/files -> core/link | transport.js | the legacy folder-as-a-disk path looks up a program the Mac may have (drive.js); it goes with that path | lib |
core/hooks -> core/names | tailscale.js | runs the tailscale CLI | lib |
core/link -> core/names | tailscale.js | finds the box on the tailnet | lib |
core/names -> core/link | transport.js | names and link import each other; the transport belongs in a lib both use | lib |
core/network -> core/names | guests.js, identity.js, tailscale.js | the listeners identify tailnet peers (ADR 0002) | lib |
core/recall -> core/transcripts | index.js | transcripts is the one reader of Claude Code's files | lib |
core/watchers -> core/spawner | client.js | the box's watcher wall is the root spawner's; loaded only when a spawner socket exists | lib |
core/sessions -> core/spawner | client.js | sessions/switchboard split (ADR 0030), cleanup owed by sessions after 0.1.0 | ctx.call |
core/sessions -> core/switchboard | runner.js | sessions/switchboard split (ADR 0030), cleanup owed by sessions after 0.1.0 | ctx.call |
core/sessions -> core/transcripts | sanitize.js | sessions/switchboard split (ADR 0030), cleanup owed by sessions after 0.1.0 | ctx.call |
core/switchboard -> core/harness | rules.js | sessions/switchboard split (ADR 0030), cleanup owed by sessions after 0.1.0 | ctx.call |
core/switchboard -> core/sessions | config.js, providers.js, sdk.js, spawn.js | sessions/switchboard split (ADR 0030), cleanup owed by sessions after 0.1.0 | ctx.call |
core/switchboard -> core/transcripts | sanitize.js | keeps credentials out of what it builds from transcripts | lib |
core/term -> core/files | safety.js | the path gate every file path passes through | ctx.call |
core/vyre-core -> core/vault | vault.js | vyre-core hosts the vault's store and crypto in its own process and db (ADR 0040 phase 2) | host |
core/vault -> core/link | transport.js | the vault relay between the Mac and the box | lib |
core/vault -> core/names | identity.js, tailscale.js | who is on the other end of a vault relay, and the tailscale CLI | lib |
local/capsule -> core/cli | commands/capsule-native.js | where the native Lumen app is built, shared with vyre capsule | lib |
local/hands-mac -> local/screen-mac | floor.js | the floor for Vyre's hands and eyes on the Mac (SPEC section 11) | lib |
local/sideview -> local/screen-mac | floor.js, runner.js | drives the sight helper and its floor directly | ctx.call |
What the list says
Two files carry most of the rest. core/names/tailscale.js (6 edges) and
core/link/transport.js (3) are shared plumbing, not features: the first move is a small tailnet
lib beside the kernel that both names and link use, which also ends the names and link cycle.
The ctx.call cases (8) are places where a part reaches into another's state and should ask its
tools instead. lib/connectors (auth.js, message.js, behalf.js) is the same move already done for
the connectors' own shared helpers.
First cleanup: a tailnet lib
Scheduled after the native-core milestone. core/names/tailscale.js and
core/link/transport.js move into one small tailnet lib beside the kernel, with no feature state.
That removes about 10 edges from the list above and ends the names and link cycle.