@maiaos/node 0.1.0-canary.2

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/README.md ADDED
@@ -0,0 +1,281 @@
1
+ # Maia Node (`@maiaos/node`)
2
+
3
+ [![npm canary](https://img.shields.io/npm/v/@maiaos/node/canary?label=npm%20canary)](https://www.npmjs.com/package/@maiaos/node)
4
+
5
+ Maia Node is the local authority of Maia OS: one process per machine and
6
+ profile that owns Sessions, turns, worker accounts, durable state, event order
7
+ and receipts for swarms of AI coding agents such as Claude Code and Codex.
8
+
9
+ Every Maia client (the `maia` CLI, the `maia mcp` MCP server, the
10
+ `@maiaos/sdk` TypeScript SDK and the Desktop app) reaches the Node through its
11
+ authenticated loopback API; none of them owns state. The Node validates and
12
+ authorizes each operation, stores the result in SQLite, writes a receipt and
13
+ runs workers as child processes under a native process host.
14
+
15
+ ## Run it
16
+
17
+ In development, run a Node inside an isolated dev worktree, so it never touches
18
+ your real `~/.maia`:
19
+
20
+ ```sh
21
+ pnpm dev:worktree create try-maia
22
+ cd ../maiaos-worktrees/try-maia
23
+ pnpm dev:node # builds the Node and runs it in the foreground
24
+ pnpm maia status # in a second terminal: the Node reports ready
25
+ ```
26
+
27
+ The executable itself:
28
+
29
+ ```sh
30
+ pnpm --filter @maiaos/node build
31
+ node services/node/dist/bin/maia-node.mjs --help
32
+ node services/node/dist/bin/maia-node.mjs --desktop --profile <name>
33
+ ```
34
+
35
+ `--desktop` runs the authenticated local Node in the foreground, and
36
+ `--profile` selects its runtime storage (`default` when omitted).
37
+ For a Desktop-owned child, the native host sets `MAIA_DESKTOP_PARENT_PIPE=1`
38
+ and retains its stdin writer. EOF or a pipe error interrupts the existing
39
+ Node scope, including during startup, to drain and release runtime authority.
40
+ A separate Effect deadline kills this process after 10 seconds if its drain
41
+ stalls. A drain that finishes after SIGINT, SIGTERM or a closed pipe exits 0,
42
+ so systemd logs no failure for a restart; a failed drain or startup keeps its
43
+ non-zero code. Without that explicit flag, stdin closure does not stop the Node;
44
+ attached local services and remote Nodes keep their own lifetimes. The
45
+ self-kill deadline requires a responsive Node event loop.
46
+ `maia-node remote probe|install|rollback|uninstall <ssh-host>` installs a
47
+ pinned Node on a Linux host as a systemd user service, and `maia-node bridge`
48
+ serves that host's Node API over SSH standard input and output, with no
49
+ listener beyond loopback ([remote Node install](../../docs/operations.md#remote-node-install)).
50
+ The local Node also registers remote Nodes (`node.remote.*`, S14): a preview
51
+ probes and plans in a Node-scoped job, a confirmation bound to the plan
52
+ digest records a receipt and runs the same install or uninstall, and the
53
+ install pins `codex` and `claude` with a PATH of their directories, the Maia
54
+ runtime and the system directories ([D-080](../../docs/decisions.md#d-080)).
55
+ A local background service is planned (RL3). `@maiaos/node@canary` is
56
+ published on npm for installation testing only; npm's automatic `latest` tag
57
+ also points at a canary build.
58
+
59
+ ## Key concepts
60
+
61
+ - **One authority.** The Node alone validates, authorizes, persists, executes,
62
+ orders events and writes receipts. Clients are projections: a cached client
63
+ state never overwrites newer Node state.
64
+ - **Node API.** The Node binds an ephemeral port on `127.0.0.1` only and writes
65
+ an owner-private connection descriptor and bearer credential to its profile
66
+ runtime directory. Unary operations use `POST /api/v1/operations`;
67
+ observations use the `/api/v1/observations` WebSocket. A restart replaces the
68
+ credential.
69
+ - **Live observation (S12a).** `observe.events` replays committed journal rows
70
+ from SQLite, which stays the source of cursors and replay. An idle
71
+ subscriber wakes as soon as a journal append commits: a TEMP trigger on
72
+ `domain_events` signals every append on the Node's connection, and a burst
73
+ coalesces into one wake. The five-second heartbeat proves liveness and
74
+ publishes filtered progress.
75
+ - **Live Session items (S12b and S12c, source candidates).** A per-Session
76
+ writer assigns each Codex or Claude Code item record its owner sequence. The live lane coalesces
77
+ deltas for 33 ms and offers them to bounded attachment queues (256 records,
78
+ 2 MiB); a full queue gets `resync`. The durable lane commits finished items
79
+ in one transaction per 400 ms (`session_stream_records`, migration v50), and
80
+ startup closes what a crashed epoch left open. `session.attach` on the
81
+ observation socket replays or snapshots, synchronizes the open items, then
82
+ streams live records.
83
+ - **Profiles.** A profile has its own runtime directory, database and
84
+ observability files. Each dev worktree runs its own profile and `MAIA_HOME`.
85
+ - **Sessions and executors.** A Session runs one vendor executor, Codex through
86
+ `codex app-server` or Claude Code through the Claude Agent SDK. A reading
87
+ Session runs read-only in its Workspace; a coding Session (`--base`) gets a
88
+ worktree of its own and writes only there, with `session diff` and
89
+ `session archive` to review and retire it
90
+ ([coding Sessions](../../docs/maia-swarm/external-executors.md#coding-sessions)).
91
+ A Session is bound to
92
+ one account Connection; when the caller names only the executor kind, the
93
+ Node picks the eligible account with the most usage left. At most 8 Sessions
94
+ run at once on a Node; each account allows 2 by default (1–16, set with
95
+ `maia login <claude|codex> --max-sessions` or `maia connection capacity set`).
96
+ - **Accounts.** A Connection is a named Codex or Claude Code account, either a
97
+ Maia-managed account root or the system default login. The vendor CLI keeps
98
+ custody of the credential; Maia never reads it. Vendor usage-limit signals
99
+ mark an account `limited` until its reset time. A gateway Connection is an
100
+ AI gateway's base URL with its API key in an owner-only `gateway-key` file;
101
+ Claude Code and Codex Sessions on it receive the URL and key through their
102
+ launch environment.
103
+ - **Process hosts.** On macOS a native helper owns each worker's process groups,
104
+ limits and cleanup proof (cooperative, not a sandbox). On Linux a helper puts
105
+ each process tree in its own cgroup v2 directory and confines it with
106
+ Landlock and seccomp; it needs a systemd user unit with `Delegate=yes` and
107
+ Linux 6.12 or later, and it bounds the tree, not the user. Both are source
108
+ candidates. A Windows Job Object helper exists in source but is unqualified:
109
+ Windows is unsupported in v1 and fails with `platform_unsupported`. A host
110
+ without qualified process authority fails closed. A vendor launch permit
111
+ records where its host keeps the tree (the macOS process group, the Linux
112
+ tree cgroups' parent), and after a crash the restarted Node frees the
113
+ vendor only once that record shows the tree ended
114
+ ([D-098](../../docs/decisions.md#d-098)).
115
+ - **Mutations.** Every mutation checks a revision or command ID and returns a
116
+ receipt; replaying a command ID returns the first result instead of acting
117
+ twice. Project and container metadata, agent permissions, integration plans
118
+ and the voice relay also need a preview and an explicit confirmation.
119
+ - **Port leases (S16).** `port.lease.acquire|renew|release|list` grant 1–8
120
+ loopback ports from `MAIA_PORT_RANGE` (default `41000-41999`) to a Session
121
+ or the operator. Leases are advisory: the Node never restricts a bind or
122
+ stops a process. A port is granted only when connects to `127.0.0.1` and
123
+ `[::1]` are refused and a loopback bind succeeds. Leases last 15 minutes
124
+ unless renewed, and end with their Session, at expiry or at Node start.
125
+ Codex and Claude Code Sessions that hold leases receive `MAIA_PORTS`.
126
+ - **Emergency holds.** A person holds exact Projects or accounts with a
127
+ previewed, confirmed `hold.place`. Every new Session, start, resume, turn,
128
+ Run dispatch and voice relay on a held target fails with `held`, checked in
129
+ the transaction that admits it; account selection skips held accounts. A
130
+ `stop` hold also stops the Sessions running on its targets in the same
131
+ transaction, then ends their runtimes. Holds survive restarts, recovery never
132
+ resumes held work, and `hold.release` restores admission. See
133
+ [operations](../../docs/operations.md#emergency-holds-implemented-today-source-candidate).
134
+ - **Worker credentials and fan-out guards (S18a, S18b).** Each Codex or
135
+ Claude Code launch gets its own Session credential (`session-credential/`),
136
+ revoked when its runtime ends; `Authorization: Maia-Session` admits that
137
+ Session's role operations (`application/session-request.ts`). Both roles
138
+ list Sessions only in the credential's Project, before pagination; an
139
+ explicit other Project is refused. Human and external-coordinator lists
140
+ retain whole-Node visibility ([D-092](../../docs/decisions.md#d-092)). The Linux
141
+ helper denies Claude Code trees reads of the runtime directory and auth
142
+ file; Codex's profile denies them to Codex commands
143
+ ([D-110](../../docs/decisions.md#d-110)).
144
+ `session-guard/` keeps each Session's role, parent and depth and the
145
+ per-Project depth, live-children, messages-per-minute and
146
+ leases-per-Session limits (`project.guard.*`), checked in the write that
147
+ creates, sends or leases. See
148
+ [vendor executors](../../docs/maia-swarm/external-executors.md#session-bound-worker-credential).
149
+ - **Control dispatch.** An operator or coordinator request with a feature
150
+ descriptor goes through `application/feature-handlers.ts`: one
151
+ context-preserving adapter per registered Node handler, which forwards the
152
+ command ID and any attached credential, selects the executor's services and
153
+ fails closed when one is not composed (adopted-Session resume preflight,
154
+ legacy versus interactive `terminal.create`, voice relay availability).
155
+ Undescribed methods keep explicit routes in `application/control-request.ts`
156
+ until their descriptors land.
157
+ - **Model providers.** `provider.pool.list` reads the owner-only `auth.json`
158
+ that `maia login` writes. The managed Pi worker leases one credential in
159
+ memory per Session and releases it after a cleanup proof.
160
+
161
+ ## Status
162
+
163
+ Implemented as source candidates, not release-qualified:
164
+
165
+ - the Node API, SQLite storage with checksummed migrations, and receipts with
166
+ command-ID replay;
167
+ - Projects, execution Workspaces, Workspace containers and confirmed Project
168
+ metadata;
169
+ - Sessions with Codex and Claude Code executors: create, start, stop, resume,
170
+ rename, observe, events, with titles (given at creation or taken from the
171
+ first turn); turns: send, wait, show, interrupt;
172
+ - reference resolution (`reference.resolve`): an ID, ID prefix, name or
173
+ Session title, or the registered folder containing a path, to one exact ID
174
+ or its candidates;
175
+ - Connections, sign-in, usage and integration approval of vendor executables;
176
+ - record-only external questions (`interaction.*`), presentation reads, live
177
+ terminals on macOS, and the release-disabled voice orchestrator;
178
+ - emergency holds on exact Projects and accounts (`hold.*`, S20);
179
+ - the Run ledger over executor Sessions (`run.ledger.*`, S19): a task graph
180
+ whose dispatches are Codex or Claude Code Session turns, Task state derived
181
+ from dispatches, three failed dispatches stop a Task, and the
182
+ `session.observe` attention value.
183
+ - Session-bound worker credentials, coding-worker MCP launch configuration,
184
+ roles and per-Project fan-out guards
185
+ (`project.guard.*`, S18a and S18b).
186
+
187
+ Planned: Pi item adapters and the Desktop timeline
188
+ (S12d) on the S12b stream, host and agent observability
189
+ (S15), advisory leases (S16, in progress), Node and capability holds with `capability.revoke`, coding
190
+ workers (S5), the installer and
191
+ background service (RL3) and updates (RL4). The
192
+ [roadmap](../../ROADMAP.md#v1-external-brain-and-vendor-executors) tracks each
193
+ slice.
194
+
195
+ ## Direction: Control Nodes, Execution Nodes and principals
196
+
197
+ The Node is designed to grow into an organization's agent hub. None of this is
198
+ implemented:
199
+
200
+ - one **Control Node** owns durable truth for team Workspaces, starting from an
201
+ explicit first-owner bootstrap
202
+ ([design](../../docs/team-workspaces-and-control-nodes.md));
203
+ - every request resolves a server-created **principal** with explicit Project
204
+ **grants**; callers never supply identity, operator rights or grants
205
+ ([identity, authentication and tenancy](../../docs/maia-node/architecture.md#identity-authentication-and-tenancy));
206
+ - enrolled **Execution Nodes** (your computer, a VPS, other servers) provide
207
+ capacity through one-time pairing and Node-issued certificates;
208
+ - **A2A** peers map each accepted task to one durable Maia task and never
209
+ receive worker leases or mutation authority.
210
+
211
+ ## Development
212
+
213
+ ```sh
214
+ pnpm --filter @maiaos/node build
215
+ pnpm --filter @maiaos/node lint
216
+ pnpm --filter @maiaos/node typecheck
217
+ pnpm --filter @maiaos/node test
218
+ pnpm test:resource-diagnostic # from the repository root
219
+ pnpm --filter @maiaos/node test:observation-benchmark
220
+ ```
221
+
222
+ The observation benchmark commits through a real SQLite journal after seeded
223
+ idle gaps of 0–3 s and times delivery to an SDK subscriber over loopback; it
224
+ fails above a p95 of 50 ms and writes `.artifacts/benchmarks/`. Measured on an
225
+ Apple M4 Pro (12 cores, macOS 27.2, Node 24.19.0) while parallel test runs
226
+ loaded the machine (load average 5–32), 100 samples each:
227
+
228
+ | Measure | Before S12a | After S12a |
229
+ | -------------------------------------- | ------------------------ | -------------------- |
230
+ | Commit to subscriber, p50 / p95 / max | 3,410 / 4,874 / 4,957 ms | 1.9 / 4.7 / 19.8 ms |
231
+ | Idle CPU per 60 s, 8 subscribers | 222 ms (165–279) | 267 ms (207–334) |
232
+ | 10,000 commits, 142 forced resumptions | not measured | none lost or doubled |
233
+
234
+ The idle row is the median and range of six alternating runs each; the spread
235
+ between runs is wider than the difference. The new wait races two Effect
236
+ fibers where the old loop slept, about 60 µs per wait in a microbenchmark,
237
+ roughly 6 ms of CPU per minute for eight idle subscribers.
238
+
239
+ The S12b acceptance test (`src/runtime/node-api/observation/attach.integration.test.ts`)
240
+ drives a synthetic harness into the real hub, the real Node API server, the
241
+ SDK and the in-process `maia session attach` command, and times each delta
242
+ until the CLI writes it. Same machine and load conditions:
243
+
244
+ | Measure (S12b) | Result |
245
+ | ------------------------------------------------- | ---------------------------------- |
246
+ | Harness delta to CLI line, 1 streaming Session | p50 18.8 ms, p95 36.0 ms (150) |
247
+ | Harness delta to CLI line, 100 streaming Sessions | p50 21.1 ms, p95 39.3 ms (900) |
248
+ | Durable writes per streaming Session | at most 2.78 per second |
249
+ | Catch-up of 1,000 records to `synchronized` | p50 15.8 ms, p95 22.9 ms (10 runs) |
250
+ | Viewer beside a stalled subscriber | p95 36.1 ms; stalled one: `resync` |
251
+
252
+ At 100 Sessions six viewers attach (the observation socket admits eight), and
253
+ every Session streams a delta every 20 ms. S12c runs the same test with each
254
+ delta sent as an Agent SDK partial message through the Claude adapter:
255
+
256
+ | Measure (S12c, Claude adapter) | Result |
257
+ | ------------------------------------------------- | ------------------------------ |
258
+ | Harness delta to CLI line, 1 streaming Session | p50 18.8 ms, p95 34.6 ms (150) |
259
+ | Harness delta to CLI line, 100 streaming Sessions | p50 19.2 ms, p95 37.2 ms (900) |
260
+ | Durable writes per streaming Session | at most 2.55 per second |
261
+
262
+ Tests use deterministic clocks, identifiers, transports and disposable SQLite.
263
+ Package tests get a throwaway `MAIA_HOME`; never point a test at your real
264
+ `~/.maia`.
265
+
266
+ ## Documentation
267
+
268
+ - [Node architecture](../../docs/maia-node/architecture.md): Sessions,
269
+ activations, supervision, events, observation streaming and recovery
270
+ - [Operations](../../docs/operations.md): deployment, SQLite, identity, backup
271
+ and observability
272
+ - [External brain and vendor executors](../../docs/maia-swarm/external-executors.md):
273
+ executor, account and containment decisions
274
+ - [Release, update and compatibility](../../docs/release.md): the planned
275
+ installer, service and update flow
276
+ - Scope READMEs: [harness adapters](./src/harness/README.md) and
277
+ [Project and Workspace storage](./src/project-workspace/README.md)
278
+
279
+ ## License
280
+
281
+ No license has been published yet. All rights reserved.