n2n-nexus 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/CONTRIBUTING.md +49 -0
- package/README.md +157 -77
- package/SECURITY.md +37 -0
- package/build/cli/client-command.js +58 -0
- package/build/cli/doctor.js +58 -8
- package/build/cli/entry.js +5 -0
- package/build/cli/hook.js +104 -16
- package/build/cli/install.js +37 -13
- package/build/cli/mcp-config.js +16 -12
- package/build/client/discovery.js +115 -0
- package/build/client/nexus-client.js +8 -2
- package/build/contract.js +69 -0
- package/build/index.js +12 -18
- package/build/server/nexus.js +63 -35
- package/contract/errors.json +52 -0
- package/contract/hook.json +156 -0
- package/contract/tools.json +2812 -0
- package/docs/ARCHITECTURE.md +142 -108
- package/docs/ARCHITECTURE_zh.md +144 -110
- package/docs/ASSISTANT_GUIDE.md +5 -4
- package/docs/ASSISTANT_GUIDE_zh.md +134 -0
- package/docs/MEETING_MINUTES_2025-12-29.md +166 -0
- package/docs/README_zh.md +159 -79
- package/docs/TOOLS_REFERENCE.md +404 -0
- package/docs/discussion_2025-12-29.md +727 -0
- package/llms.txt +16 -5
- package/package.json +12 -8
- package/build/cli/autostart.js +0 -64
- package/build/config/cli.js +0 -8
- package/build/config/paths.js +0 -38
- package/build/constants.js +0 -18
- package/build/daemon/hook-contract.js +0 -68
- package/build/daemon/index.js +0 -57
- package/build/daemon/rate-limit.js +0 -62
- package/build/daemon/server.js +0 -862
- package/build/daemon/tool-outputs.js +0 -180
- package/build/daemon/tools.js +0 -326
- package/build/storage/agents.js +0 -227
- package/build/storage/docs.js +0 -81
- package/build/storage/events.js +0 -31
- package/build/storage/fs-utils.js +0 -88
- package/build/storage/global-messages.js +0 -44
- package/build/storage/index.js +0 -141
- package/build/storage/migrations.js +0 -87
- package/build/storage/paths.js +0 -23
- package/build/storage/projects.js +0 -53
- package/build/storage/registry.js +0 -355
- package/build/storage/sqlite-meeting.js +0 -323
- package/build/storage/sqlite.js +0 -275
- package/build/storage/tasks.js +0 -138
- package/build/types.js +0 -1
- package/build/utils/async-rwlock.js +0 -64
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -4,33 +4,40 @@
|
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
┌──────────────────────────────────────────────────┐
|
|
7
|
-
│ n2n-nexus
|
|
8
|
-
│
|
|
9
|
-
│ or in the background by a local hook / adapter │
|
|
7
|
+
│ n2n-nexus-host (Go) │
|
|
8
|
+
│ Tray app, or a boot service (`service install`) │
|
|
10
9
|
│ │
|
|
11
|
-
│
|
|
12
|
-
│ │
|
|
13
|
-
│ │ /
|
|
14
|
-
│ │ /api/tools │ │
|
|
15
|
-
│ │
|
|
16
|
-
│
|
|
17
|
-
│
|
|
10
|
+
│ ┌──────────────────┐ ┌──────────────────────┐ │
|
|
11
|
+
│ │ HTTP API │ │ Storage │ │
|
|
12
|
+
│ │ GET /health │ │ one SQLite file │ │
|
|
13
|
+
│ │ GET /api/tools │ │ (nexus.db, WAL), │ │
|
|
14
|
+
│ │ POST /api/tools │ │ numbered migrations,│ │
|
|
15
|
+
│ │ /call │ │ one transaction │ │
|
|
16
|
+
│ │ POST /api/hook │ │ per write │ │
|
|
17
|
+
│ └──────┬───────────┘ └──────────────────────┘ │
|
|
18
|
+
│ Tools, conversations, LAN discovery, cleanup │
|
|
18
19
|
└─────────┼────────────────────────────────────────┘
|
|
19
20
|
│ HTTP (NEXUS_ENDPOINT — cross-env capable)
|
|
20
21
|
┌──────┼──────┐
|
|
21
22
|
▼ ▼ ▼
|
|
22
23
|
MCP-A MCP-B MCP-C hooks (Claude Code / Codex)
|
|
23
24
|
(Win) (WSL) (VM) POST /api/hook per client event
|
|
24
|
-
|
|
25
|
+
n2n-nexus (npm): stateless adapter, one per MCP connection
|
|
25
26
|
```
|
|
26
27
|
|
|
28
|
+
Two programs share one protocol:
|
|
29
|
+
|
|
30
|
+
- **`n2n-nexus-host`** (Go, `host/`): owns all data, tool definitions and business logic. Installed from GitHub Releases, Homebrew or Scoop.
|
|
31
|
+
- **`n2n-nexus`** (TypeScript, npm, `src/`): the client only — the MCP adapter (`mcp`), the hook command (`hook`), `install` / `uninstall` for Claude Code and Codex, and `doctor`. It contains no storage and no server.
|
|
32
|
+
- **`contract/`** is the single source of the protocol between them: `tools.json` (every tool's name, title, description, annotations, input and output JSON Schema), `hook.json` (hook fields, `_meta` keys, timings, limits, notification texts) and `errors.json` (endpoints, error body, status codes). The host embeds these files, the client reads `hook.json` at run time, and the conformance tests in `tests/conformance/` check a host against them. `GET /health` reports the host's `contractVersion`; `n2n-nexus doctor` compares it with the client's.
|
|
33
|
+
|
|
27
34
|
### Core Principles
|
|
28
35
|
|
|
29
|
-
1. **
|
|
30
|
-
2. **MCP is stateless** — no hardcoded tool names, no local persistent data and no session of its own; conversations and direct messages live in the
|
|
31
|
-
3. **
|
|
32
|
-
4. **No local fallback** — if the
|
|
33
|
-
5. **Plain HTTP between MCP, hooks and
|
|
36
|
+
1. **The host is the single source of truth** — all reads/writes, business logic, and tool definitions live in the host.
|
|
37
|
+
2. **MCP is stateless** — no hardcoded tool names, no local persistent data and no session of its own; conversations and direct messages live in the host.
|
|
38
|
+
3. **The host decides tool capability** — the adapter fetches `GET /api/tools` at startup and reconnect.
|
|
39
|
+
4. **No local fallback** — if the host is unreachable, the MCP reports an error and retries every 3 s. No split-brain degradation. A tool call the host rejects (validation, not found, permission) is returned to the IDE as an error result (`isError: true`) and does not affect the connection; an unknown tool name is a JSON-RPC `-32602` error.
|
|
40
|
+
5. **Plain HTTP between MCP, hooks and host** — the client's hooks report conversation events and carry new-message notifications back to the model; the model reads the text with a tool.
|
|
34
41
|
|
|
35
42
|
### MCP Startup Flow
|
|
36
43
|
|
|
@@ -43,20 +50,22 @@ Read NEXUS_ENDPOINT (default: http://127.0.0.1:5688)
|
|
|
43
50
|
│ │ list_changed delivered on subscriptions/listen
|
|
44
51
|
│ └─ 2025-era clients: initialize handshake, unsolicited list_changed
|
|
45
52
|
│
|
|
46
|
-
├─
|
|
53
|
+
├─ Nothing answers: say how to start n2n-nexus-host on this machine,
|
|
54
|
+
│ or on the remote machine for a remote endpoint; never start one
|
|
55
|
+
├─ A call not answered in time fails alone: tool list kept, no reconnect
|
|
47
56
|
│
|
|
48
|
-
├─ If the
|
|
57
|
+
├─ If the host was down: retry loop (every 3 s)
|
|
49
58
|
│ └─ GET /api/tools succeeds → cache tool list
|
|
50
59
|
│ → notifications/tools/list_changed → IDE re-fetches
|
|
51
60
|
│
|
|
52
61
|
└─ stdin closes → stop retrying, close, exit
|
|
53
62
|
```
|
|
54
63
|
|
|
55
|
-
The adapter's instance ID (read cursors, meeting permissions) uses the first client name seen on the connection, from the request's `io.modelcontextprotocol/clientInfo` (2026-07-28) or from `initialize` (2025 era), unless `NEXUS_INSTANCE_ID` / `--id` pins it.
|
|
64
|
+
The adapter's instance ID (read cursors, meeting permissions) uses the first client name seen on the connection, from the request's `io.modelcontextprotocol/clientInfo` (2026-07-28) or from `initialize` (2025 era), unless `NEXUS_INSTANCE_ID` / `--id` pins it. Every tool call carries it as `instanceId`; `read_messages` refuses a call without one, since unread cursors are kept per instance.
|
|
56
65
|
|
|
57
66
|
### Conversations and hooks
|
|
58
67
|
|
|
59
|
-
Each Claude Code or Codex conversation is one session, `claude:<session id>` or `codex:<session id>`, using the ID the client itself assigns. `n2n-nexus install <claude|codex>` writes hooks that run `n2n-nexus hook <client>` on SessionStart, UserPromptSubmit, PostToolUse, Stop and SessionEnd (and, for Claude Code, PreToolUse on the n2n-nexus tools). The hook command forwards the event's identifying fields to `POST /api/hook` and prints the
|
|
68
|
+
Each Claude Code or Codex conversation is one session, `claude:<session id>` or `codex:<session id>`, using the ID the client itself assigns. `n2n-nexus install <claude|codex>` writes hooks that run `n2n-nexus hook <client>` on SessionStart, UserPromptSubmit, PostToolUse, Stop and SessionEnd (and, for Claude Code, PreToolUse on the n2n-nexus tools). The hook command forwards the event's identifying fields to `POST /api/hook` and prints the host's answer; on any error it exits 0 without output, so a missing host never blocks the client.
|
|
60
69
|
|
|
61
70
|
```
|
|
62
71
|
Client event ──▶ n2n-nexus hook ──▶ POST /api/hook ──▶ session status, notifications
|
|
@@ -67,36 +76,56 @@ Stop ◀── decision: block ◀──┘ once pe
|
|
|
67
76
|
|
|
68
77
|
Tool calls name their conversation outside the model's arguments. The adapter reads it from the request's `_meta`: Claude Code sends `claudecode/toolUseId`, which the PreToolUse hook bound to the conversation just before the call (verified: the call waits for the hook); Codex sends `sessionId`. The adapter forwards it as `caller` in `POST /api/tools/call`. Because the identity is the conversation, not the adapter process, an adapter restart, a resumed conversation and a sub-agent all keep it, and one adapter can serve several Codex conversations.
|
|
69
78
|
|
|
70
|
-
Notifications carry the sender and the first line only; the text reaches the model through `read`, as tool output rather than injected context. A listening Stop hook is held open for up to 590 s and answered as soon as a message arrives; the client killing the hook (the user pressing Esc) or the
|
|
79
|
+
Notifications carry the sender and the first line only; the text reaches the model through `read`, as tool output rather than injected context. A listening Stop hook is held open for up to 590 s and answered as soon as a message arrives; the client killing the hook (the user pressing Esc) or the host shutting down ends the wait. This remains a trusted local host, not an authentication boundary: every process of the same operating-system user is trusted and could act as any conversation, and agents that must be isolated run as different users with their own hosts (see the local trust model in [SECURITY.md](../SECURITY.md)). See [tool reference](TOOLS_REFERENCE.md#direct-messages-between-conversations).
|
|
71
80
|
|
|
72
|
-
|
|
81
|
+
### Sessions, presence and waking idle conversations
|
|
73
82
|
|
|
74
|
-
|
|
83
|
+
A new conversation's SessionStart hook prints an introduction: its own name and ID, the other sessions that are not offline (up to five) and how to use `peers`, `send` and `read`. The adapter also sends MCP server instructions with the same guidance. Clients do not always report the end of a conversation, so an `idle` or `working` session without activity for 30 minutes counts as `offline` until its next event.
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
An idle conversation is not running, so hooks alone can only reach it when its user acts. To let AI sessions answer each other without anyone typing, `install` adds a second Stop hook, `n2n-nexus hook <client> --wait`, that starts a background wait when a turn ends:
|
|
77
86
|
|
|
78
87
|
```
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
│
|
|
82
|
-
|
|
83
|
-
│
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
│ ├── internal_blueprint.md # Internal technical docs
|
|
88
|
-
│ └── assets/ # Binary assets (images, PDFs)
|
|
89
|
-
├── registry.json # Global project index
|
|
90
|
-
└── nexus.db # SQLite database (meetings, global chat, events, tasks, cursors, conversations, direct messages)
|
|
88
|
+
turn ends ──▶ Stop hook (announces unread, or listens)
|
|
89
|
+
└─▶ Stop hook --wait ──▶ POST /api/hook {BackgroundWait} (up to 590 s per request, repeated for up to 1 h)
|
|
90
|
+
│ session shows as "listening"
|
|
91
|
+
├─ new message ─▶ { wake: notification }
|
|
92
|
+
│ Claude Code: asyncRewake hook exits 2, the notification starts a turn
|
|
93
|
+
│ Codex: detached process runs `codex queue --thread <id> --message <notification>`
|
|
94
|
+
├─ any other event of the session ─▶ null (wait ends)
|
|
95
|
+
└─ time runs out ─▶ { timeout: true } (ask again)
|
|
91
96
|
```
|
|
92
97
|
|
|
93
|
-
|
|
98
|
+
The wait never delivers while the session is `working` or while a listening Stop hook is waiting, so a message is announced once. Verified on Windows with the Claude desktop app and the Codex desktop app (2026-10-23): both directions wake an idle conversation within seconds. Known limits:
|
|
99
|
+
|
|
100
|
+
- Codex starts a queued turn only for a conversation loaded in the running Codex app. After Codex restarts, a conversation is loaded once the user opens it.
|
|
101
|
+
- The Codex background wait ends when the Codex app quits, and starts again only when the conversation ends a turn. Messages sent meanwhile stay unread in the host (not in Codex's own queue) until the user next prompts that conversation.
|
|
102
|
+
- A Claude Code conversation is reachable for an hour after its last turn (the hook timeout).
|
|
103
|
+
- Every AI reply wakes the recipient again. To stop two sessions waking each other forever, the host counts automatic turns (wakes, listening Stops, Stops held open for messages) since the user's last prompt and stops after `maxAutoTurns` in `settings.json` (default 8); a prompt within a minute of a wake is the woken turn starting and keeps the count. Messages past the limit wait until the user prompts.
|
|
104
|
+
- The automatic turn count tells the woken turn from the user by time alone. If a wake does not start a turn (a Codex conversation not loaded) and the user prompts within that minute, that prompt keeps the count; the user's next prompt resets it. If a woken turn starts more than a minute late (a Codex conversation the user opens later), its prompt resets the count. Neither loses a message or lets sessions loop without a user.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 💾 Data Persistence
|
|
109
|
+
|
|
110
|
+
All data lives in one SQLite file, `nexus.db`, in the host's data directory: `%LOCALAPPDATA%\n2n-nexus` on Windows, `~/Library/Application Support/n2n-nexus` on macOS, `$XDG_DATA_HOME/n2n-nexus` (default `~/.local/share/n2n-nexus`) on Linux; `--root` or `NEXUS_ROOT` overrides it. The same directory holds `settings.json` and `host.lock`.
|
|
111
|
+
|
|
112
|
+
- **Schema**: numbered migrations embedded in the host (`host/internal/store/migrations/0001_core.sql`, `0002_agents.sql`) run in order at startup, each in one transaction; the version is `PRAGMA user_version`. A database newer than the host is refused ("upgrade n2n-nexus-host").
|
|
113
|
+
- **Tables**: `projects` (scalar manifest fields as columns, structured fields as JSON, internal docs as text), `project_relations` and `project_assets` (asset bytes as BLOBs; both cascade on project rename and delete), `global_strategy`, `global_docs`, `meetings` (with `status` and `is_default`), `meeting_messages`, `decisions`, `global_messages`, `events`, `tasks`, `read_cursors`, and for conversations `agent_sessions`, `agent_messages`, `agent_notices`, `agent_idle_subscriptions`, `tool_use_bindings`.
|
|
114
|
+
- **Transactions**: every write is one transaction, so a multi-row change (a meeting message, starting / ending / reopening a meeting, renaming a project) is applied completely or not at all. The database runs in WAL mode with `foreign_keys` on and a 5 s busy timeout, through a single connection.
|
|
115
|
+
- **Only the host opens the database**. MCP adapters and hooks go through HTTP.
|
|
116
|
+
- **Activity log**: state changes made through tools (project sync/update/rename/delete/asset, doc sync, strategy update, meeting start/end/archive/reopen, maintenance) are recorded as structured events and read with `list_events`; they are not mixed into chat streams.
|
|
117
|
+
- **Old data**: the file-based storage of n2n-nexus 0.x (`~/.n2n-nexus`) is not imported or read, and is left untouched.
|
|
118
|
+
|
|
119
|
+
### Maintenance and retention
|
|
94
120
|
|
|
95
|
-
|
|
121
|
+
At startup and then every hour the host:
|
|
96
122
|
|
|
97
|
-
|
|
123
|
+
- marks pending or running tasks past their TTL as `failed` and deletes tasks 7 days after they finished (`completed`, `failed`, `cancelled`);
|
|
124
|
+
- deletes events older than 30 days;
|
|
125
|
+
- expires unread direct messages past their 24-hour lifetime (telling the senders), and deletes read or expired direct messages and delivered notices after 30 days;
|
|
126
|
+
- deletes tool-use bindings once they have expired (5 minutes).
|
|
98
127
|
|
|
99
|
-
|
|
128
|
+
Logs: the service, and the tray while it runs the host in-process, write `host.log` in the log directory (`%LOCALAPPDATA%\n2n-nexus\logs`, `~/Library/Logs/n2n-nexus`, `$XDG_STATE_HOME/n2n-nexus` with default `~/.local/state/n2n-nexus`), rotated at 10 MB with 3 old files kept; only the holder of `host.lock` writes it. The tray's own lines go to `tray.log` there, rotated the same way. `serve` writes to standard error.
|
|
100
129
|
|
|
101
130
|
---
|
|
102
131
|
|
|
@@ -124,24 +153,26 @@ All project IDs must follow the `[prefix]_[name]` format:
|
|
|
124
153
|
|
|
125
154
|
## 🌐 Deployment Model
|
|
126
155
|
|
|
127
|
-
|
|
156
|
+
Two programs:
|
|
128
157
|
|
|
129
158
|
```
|
|
130
|
-
n2n-nexus
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
├─
|
|
134
|
-
├─
|
|
135
|
-
|
|
136
|
-
|
|
159
|
+
n2n-nexus-host Install once per host machine. Owns all data.
|
|
160
|
+
├─ (no arguments) Tray app; runs the host in-process, or only controls
|
|
161
|
+
│ a host already answering on the configured port
|
|
162
|
+
├─ serve Foreground, no tray (debugging; what the service runs)
|
|
163
|
+
├─ service install|uninstall|status
|
|
164
|
+
│ Boot service running as the installing user
|
|
165
|
+
├─ autostart on|off Start the tray at login
|
|
166
|
+
└─ version
|
|
167
|
+
|
|
168
|
+
n2n-nexus (npm) Install on every machine with an IDE.
|
|
169
|
+
├─ n2n-nexus mcp IDE starts automatically. Stateless proxy.
|
|
170
|
+
├─ n2n-nexus hook Run by Claude Code / Codex on each hook event.
|
|
171
|
+
├─ n2n-nexus install Write the hooks and MCP server into a client's user configuration.
|
|
172
|
+
└─ n2n-nexus doctor Check the installed commands and the host's contract version.
|
|
137
173
|
```
|
|
138
174
|
|
|
139
|
-
**
|
|
140
|
-
```bash
|
|
141
|
-
npx n2n-nexus daemon --port 5688
|
|
142
|
-
# or with explicit root
|
|
143
|
-
npx n2n-nexus daemon --root ~/.n2n-nexus --port 5688
|
|
144
|
-
```
|
|
175
|
+
**Host**: started as a tray app or a boot service; the adapter and the hooks never start one, they report that none answers. Configuration comes only from `<data dir>/settings.json` (`host`, `port`, `discoveryPort`, `allowedHosts`, `rateLimit`, `rateBurst`, `maxAutoTurns`), overridden by command-line flags; the only environment variable the host reads is `NEXUS_ROOT`. A service does not get the environment of the terminal that installed it, so the file is the one source the tray, the service and the command line share.
|
|
145
176
|
|
|
146
177
|
**MCP** (IDE configuration):
|
|
147
178
|
```json
|
|
@@ -156,80 +187,83 @@ npx n2n-nexus daemon --root ~/.n2n-nexus --port 5688
|
|
|
156
187
|
}
|
|
157
188
|
```
|
|
158
189
|
|
|
159
|
-
**
|
|
190
|
+
**Standalone host on a LAN**: run one host with LAN access on (tray **Allow LAN access**, or `n2n-nexus-host service install --host 0.0.0.0`), independent of any IDE, and allow TCP and UDP 5688 from the local subnet (on Windows the tray adds both firewall rules). Each client machine then needs only the client side: `n2n-nexus install <claude|codex> --endpoint discover` writes the MCP server and the hooks, which find the host by UDP broadcast. On Windows, run `npm install -g` from a regular terminal: a terminal inside a packaged (MSIX) app such as Codex installs into that app's private copy of `%APPDATA%`, which only that app sees and which hides a later global install from it.
|
|
191
|
+
|
|
192
|
+
**No required startup order**: start the IDE before the host — tools appear automatically once the host comes up. Host restarts are transparent; MCP reconnects and notifies the IDE.
|
|
193
|
+
|
|
194
|
+
### Tray and service per OS
|
|
195
|
+
|
|
196
|
+
| | Windows | macOS | Linux |
|
|
197
|
+
| --- | --- | --- | --- |
|
|
198
|
+
| Boot service (`service install`, administrator / root) | Scheduled task `n2n-nexus-host`: at startup, S4U logon of the installing user (no password stored), no time limit | `/Library/LaunchDaemons/io.n2ns.n2n-nexus-host.plist` with `UserName`, `RunAtLoad`, `KeepAlive` | `/etc/systemd/system/n2n-nexus-host.service` with `User=`, `Restart=always`, `RestartSec=5` |
|
|
199
|
+
| Tray at login (`autostart`, tray menu) | `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` value `n2n-nexus-host` | `~/Library/LaunchAgents/io.n2ns.n2n-nexus-host.tray.plist` | `~/.config/autostart/n2n-nexus-host.desktop` |
|
|
200
|
+
| LAN access from the tray | Adds inbound TCP / UDP firewall rules through a UAC prompt (`netsh advfirewall`) | Tells the user which ports to allow | Tells the user which ports to allow |
|
|
201
|
+
|
|
202
|
+
Every service runs `n2n-nexus-host serve --supervise --root <data dir> --log-dir <log dir>` with the installing user's directories, so the service's own environment does not move them: the supervisor runs `serve` as a child process and starts it again 5 s after it exits, and logs to `host.log`. `service install` refuses while a tray runs the host for the data directory in-process (quit the tray first; opened again, it controls the service); otherwise it stops the host running for the data directory (found through the PIDs recorded in `host.lock`, never by matching command lines), writes `--host` / `--port` to `settings.json`, registers and starts the service and reports what answers on the port; under `sudo` it uses the invoking user's default directories. `service uninstall` removes the service and stops only the service's own `serve`, never a tray or a foreground `serve`. The tray first probes `GET /health` on the configured port: when a host answers (for example the service), it only controls that host — **Restart** (and a changed setting) writes `restart-request` into the data directory; the service's `serve` polls for it every second and exits, and its supervisor starts it with the new settings (a file, since the tray may not end a process of the scheduled task's S4U logon), then waits up to 20 s for it to answer again, never starting a host of its own — and otherwise it runs the host in-process. Changing a setting from the tray writes `settings.json` and restarts the host. The menu shows the address the host is bound to and the number of online sessions (`sessions.online` of `GET /health`), refreshed every 5 s. An IPv4 address (`0.0.0.0` included) listens on IPv4 only, an IPv6 address on IPv6 only. The tray's texts are Chinese on a Chinese system and English otherwise (on macOS without `LANG`, as under the login item, from `AppleLanguages`). The macOS tray needs cgo (Cocoa), so macOS release builds are built on macOS with `CGO_ENABLED=1`; Windows and Linux are cross-compiled with `CGO_ENABLED=0`, and Windows ships two builds of the same program: `n2n-nexus-host.exe` (console, for commands; run without arguments it starts the tray build next to it and returns) and `n2n-nexus-host-tray.exe` (GUI, `-H=windowsgui`, opens no console window; start at login registers it).
|
|
203
|
+
|
|
204
|
+
### Single instance
|
|
205
|
+
|
|
206
|
+
A host takes an exclusive lock on `<data dir>/host.lock` before it opens the database and records its PID, its supervisor's and its role (`tray`, `serve` or `supervised`) there. A second host for the same data directory exits with "another n2n-nexus-host is running for <dir> (PID n)".
|
|
160
207
|
|
|
161
208
|
---
|
|
162
209
|
|
|
163
|
-
##
|
|
210
|
+
## 🧩 Host internals
|
|
164
211
|
|
|
165
|
-
### Tool capability (MCP-facing)
|
|
166
212
|
```
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
213
|
+
host/
|
|
214
|
+
├── cmd/n2n-nexus-host/ command line: tray (default), serve, service, autostart, version
|
|
215
|
+
└── internal/
|
|
216
|
+
├── config/ data and log directories, settings.json
|
|
217
|
+
├── protocol/ loads the embedded contract (contract/contract.go), compiles the JSON Schemas,
|
|
218
|
+
│ strips undeclared argument keys, validates arguments and results
|
|
219
|
+
├── store/ opens nexus.db, runs the migrations, transactions
|
|
220
|
+
├── core/ project, doc, meeting, chat, event and task tools and their tables
|
|
221
|
+
├── agents/ conversations: hooks, direct-message tools, waits, their tables
|
|
222
|
+
├── api/ HTTP server: the 4 endpoints, Host / Origin guard, body limit, rate limits
|
|
223
|
+
├── discovery/ UDP LAN discovery responder
|
|
224
|
+
├── host/ assembles one host: lock, database, tools, server, discovery, hourly maintenance, logs
|
|
225
|
+
├── tray/ tray menu and its English / Chinese texts
|
|
226
|
+
├── service/ boot service, tray autostart, the `serve --supervise` supervisor, firewall script
|
|
227
|
+
├── clock/ system clock and the settable test clock
|
|
228
|
+
└── testhooks/ test-build only (`-tags testclock`): POST /test/clock, --test-listen-wait-ms
|
|
170
229
|
```
|
|
171
230
|
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
GET /health Health status + version
|
|
175
|
-
```
|
|
231
|
+
The Go module is `github.com/n2ns/n2n-nexus` at the repository root, so the host embeds `contract/` directly. A tool exists only if it is in `contract/tools.json`; the host registers one handler per tool and logs any tool without a handler at startup.
|
|
176
232
|
|
|
177
|
-
|
|
178
|
-
```
|
|
179
|
-
POST /api/projects/sync
|
|
180
|
-
POST /api/projects/update
|
|
181
|
-
POST /api/projects/rename
|
|
182
|
-
POST /api/projects/delete
|
|
183
|
-
GET /api/projects/search
|
|
184
|
-
GET /api/projects/topology
|
|
185
|
-
GET /api/projects/:projectId
|
|
186
|
-
```
|
|
233
|
+
**Conversation state machine**: `agents.Agents` is the in-process coordinator of one host. It holds all in-memory conversation state — held listening Stops, background waits, automatic-turn counts, wake times — behind one mutex; every hook and direct-message tool call runs under it, and whoever stores a message or notice delivers it to a waiting hook before releasing the lock. At startup it resets sessions a previous host left `listening` to `idle`. Shutting down answers every held hook (`{ "output": null }`) before the server closes. Time comes from a `Clock`: the system clock in release builds, a clock the conformance tests set through `POST /test/clock` in the `testclock` build.
|
|
187
234
|
|
|
188
|
-
|
|
189
|
-
```
|
|
190
|
-
POST /api/messages/send
|
|
191
|
-
GET /api/messages/unread
|
|
192
|
-
GET /api/events
|
|
193
|
-
GET /api/global/docs
|
|
194
|
-
GET /api/global/docs/:docId
|
|
195
|
-
POST /api/global/docs/:docId
|
|
196
|
-
GET /api/global/strategy
|
|
197
|
-
POST /api/global/strategy
|
|
198
|
-
```
|
|
235
|
+
**Discovery**: a host whose `host` is not a loopback address answers `n2n-nexus/discover` datagrams on UDP `discoveryPort` with `{"service":"n2n-nexus","version","host","port"}`. Clients with `NEXUS_ENDPOINT=discover` broadcast the query on `NEXUS_DISCOVERY_PORT`, use the first answer and cache it in `~/.n2n-nexus/discovered-endpoint`.
|
|
199
236
|
|
|
200
|
-
|
|
201
|
-
```
|
|
202
|
-
GET /api/meetings
|
|
203
|
-
POST /api/meetings/start
|
|
204
|
-
POST /api/meetings/end
|
|
205
|
-
POST /api/meetings/archive
|
|
206
|
-
POST /api/meetings/reopen
|
|
207
|
-
GET /api/meetings/:meetingId
|
|
208
|
-
```
|
|
237
|
+
---
|
|
209
238
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
GET /api/tasks
|
|
214
|
-
GET /api/tasks/:taskId
|
|
215
|
-
POST /api/tasks/:taskId/update
|
|
216
|
-
POST /api/tasks/:taskId/cancel
|
|
217
|
-
```
|
|
239
|
+
## 📡 HTTP API
|
|
240
|
+
|
|
241
|
+
The host has four endpoints (see `contract/errors.json`):
|
|
218
242
|
|
|
219
|
-
### Maintenance
|
|
220
243
|
```
|
|
221
|
-
|
|
244
|
+
GET /health { ok, service: "n2n-nexus", version, contractVersion, sessions: { online } }
|
|
245
|
+
GET /api/tools Tool definitions from contract/tools.json (JSON Schema)
|
|
246
|
+
POST /api/tools/call { tool, args, instanceId, caller? } → { ok: true, result }
|
|
247
|
+
POST /api/hook Client hook event { client, input } → { output }; the BackgroundWait event is the background wait
|
|
222
248
|
```
|
|
223
249
|
|
|
224
|
-
|
|
250
|
+
Every capability is a tool; there are no other REST routes (an unknown route is 404). A tool call validates `args` against the tool's input schema after dropping keys the schema does not declare (400 `Invalid arguments for <tool>: ...`), and the result against its output schema. A failure the caller caused, including "not found", is 400 with the error text; an unknown tool is 404. Errors have the body `{ ok: false, error }`.
|
|
225
251
|
|
|
226
252
|
### Request guards
|
|
227
253
|
|
|
228
|
-
The
|
|
254
|
+
The host has no authentication, so it keeps web pages out: it rejects browser-originated requests (`Origin` header or cross-site `Sec-Fetch-Site`: 403), `Host` names that are neither IP literals nor `localhost`, `host.docker.internal` or a name in `allowedHosts` (403, prevents DNS rebinding), POST bodies that are not `application/json` (415) and bodies above 16 MB (413).
|
|
255
|
+
|
|
256
|
+
Rate limits are token buckets (`rateLimit` per second, `rateBurst` at once; `rateLimit: 0` disables them), answered with 429 and `Retry-After`:
|
|
257
|
+
|
|
258
|
+
- tool calls per `instanceId`, and, because instance IDs are self-declared, per client address at 5x;
|
|
259
|
+
- hooks per conversation (`<client>:<session_id>`) and per client address at 5x, in buckets of their own, so a busy conversation's hooks do not use up the tool-call budget of its address;
|
|
260
|
+
- `GET /health` and `GET /api/tools` are not limited.
|
|
261
|
+
|
|
262
|
+
The trust model is unchanged: every process of the same operating-system user is trusted. See `SECURITY.md`.
|
|
229
263
|
|
|
230
264
|
## Scope Boundaries
|
|
231
265
|
|
|
232
266
|
- No cloud bridge by default.
|
|
233
|
-
- No cross-machine live sync by default — point `NEXUS_ENDPOINT` at a reachable host for that.
|
|
234
|
-
- Single
|
|
267
|
+
- No cross-machine live sync by default — point `NEXUS_ENDPOINT` at a reachable host for that, or set it to `discover` to find a host with LAN access on through a UDP broadcast.
|
|
268
|
+
- Single host node; no built-in clustering.
|
|
235
269
|
- No authentication layer in the open-source baseline.
|