@nervur-org/dock 0.4.0 → 0.5.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/quo-mcp.md DELETED
@@ -1,343 +0,0 @@
1
- # The model side
2
-
3
- This is the one way a model meets a Quo world. It is one side of the avatar
4
- described in `papers/quo-dock.md`, which owns the avatar, the front
5
- desk, the credential exchange, the roles and the shared invariants; this
6
- document assumes all of that and adds only what a model needs. It is
7
- model-agnostic on purpose. Claude, GPT, Kimi, a model in LM Studio, a model
8
- behind OpenRouter: capable or not, local or hosted, speaking MCP or plain
9
- function calling, every one meets a world in exactly one shape.
10
-
11
- A remote model, claude.ai or an API behind a vendor, is an occupant and
12
- nothing else. A local agent, Claude Code, `claude -p`, or any CLI agent on
13
- the Mac or the droplet, may also be the owner and the developer, and keeps
14
- the three apart.
15
-
16
- ## What MCP is, in Quo words
17
-
18
- MCP is the description and value layer and nothing more. The mapping is total
19
- and needs no translation code beyond an envelope:
20
-
21
- | MCP | Quo |
22
- | ------------------------ | ------------------------------------------------------------ |
23
- | server | an avatar's side |
24
- | session | a standing the avatar holds, persistent across sessions |
25
- | tools/list | the empty ask on that standing: describe for this asker |
26
- | the `describe` tool | the empty ask itself, first in the list, so the notes of the |
27
- | | describe are readable and not only its asks |
28
- | tool name, inputSchema | `asks[].name`, `asks[].input`, verbatim |
29
- | tools/call | a named ask with args |
30
- | result content | the answer object, as JSON |
31
- | tool error result | an error object she answered, `{ error }`, as JSON |
32
- | protocol error | silence and the ward's words, see the table below |
33
- | auth token | an invitation, used once, then keys |
34
- | tool title, annotations | her `look`: a title and the hints per ask, see below |
35
- | resources, prompts | not mapped. |
36
-
37
- A tool list is a describe. Because a being describes per asker, two models
38
- connected to the same user being see two different tool lists, and neither
39
- can call what it cannot see: the gate is one decision for describe and for
40
- dispatch.
41
-
42
- A being who answers `look`, the trunk's presentation ask, is listed with
43
- what it says: `asks.NAME.title` is the tool's title, and `readOnly`,
44
- `destructive` and `idempotent` are the annotations of the same names with
45
- `Hint` after them. Neither `look` nor `page`, the screen's presentation
46
- ask, is a tool: a model reads no page. A carried standing's
47
- asks, `acme-book` on the user being for a model the human let reach, carry
48
- the far being's hints the same way, read from the notes the carrier writes.
49
- The side asks `look` once per digest of the describe. Nothing else changes:
50
- a model sees a flat list with a dash, exactly as a screen sees a section
51
- per standing, and neither can do a thing the other cannot.
52
-
53
- The three words for "no object" cross the envelope like this:
54
-
55
- | the avatar heard | the client gets |
56
- | ---------------- | ------------------------------------------------------------------- |
57
- | an object | a tool result with that JSON |
58
- | `{ error }` | a tool result marked as error, with that JSON |
59
- | silence | an error result, `{ error: 'silence' }`: the work may have happened |
60
- | unreached | an error result, `{ error: 'unreached' }`: nothing was delivered |
61
- | another word | an error result, `{ error: word }`, with what the side says of it |
62
- | `removed` | at session open: 401, the token forgotten, the exchange again |
63
-
64
- Silence is never turned into a retry by the envelope. The model decides, and
65
- the two words give it what it needs to decide.
66
-
67
- ## Flows
68
-
69
- ### A remote MCP client connects
70
-
71
- The client speaks OAuth and lands the human on the allow page of the web
72
- route, where nothing is typed but an invitation: the one the root minted
73
- for this client on the user being with `nervur invite`, and the identity it
74
- will be. The invitation names its ward, so the world the client is let
75
- into is the invitation's, and the grant remembers it: a bearer is an
76
- identity in a world and a session is that identity's there. A session
77
- opening under a grant finds the avatar as she stands and mints nothing; an
78
- identity the user being removed hears `removed` there, which is 401.
79
- `/mcp` stays one endpoint per harbor: `mcp/route.ts` is the route, the OAuth
80
- dance in `oauth.ts` in front and the endpoint in `http.ts` behind it. It is
81
- `Request` in and `Response` out and names no terrain, so the daemon mounts it
82
- and so does the worker at the edge; the randomness and the hash the dance
83
- needs are the web crypto's, and the endpoint takes the SDK's web-standard
84
- transport, so nothing of node stands anywhere in it. The dance holds
85
- clients, pending requests and codes, each living minutes, and it is kept in
86
- a store the terrain supplies, the way a harbor's own store is:
87
- `cli/oauth.ts` keeps it in `<dir>/oauth.json` for the daemon, and
88
- `harbor/edge/storage.ts` keeps it in the Durable Object's own storage on the
89
- edge, and both pass one suite. The route itself reads no file. Where the
90
- dance ends is the box's grants, `dock/grants.ts`, which every route honours,
91
- so the bearer a model client holds is the bearer a script holds on the api
92
- route, for the same identity in the same world.
93
-
94
- ```
95
- root user being client mcp. route avatar
96
- |-- invite ------->| | | |
97
- |<-- invitation ---| | | |
98
- | ... the link reaches the human by any channel ... | |
99
- | | |-- OAuth -------->| |
100
- | | | the allow page: identity, invitation
101
- | | | |-- boot or find avatar for the identity -->|
102
- | | | |-- knock(invitation) -------------------->|
103
- | |<-- knock -------------------------------------------------------------------|
104
- | |--- answer ----------------------------------------------------------------->|
105
- | | | | take |
106
- | | |<-- initialize ---| |
107
- |-- tools/list -->|-- ask() ------------------------------------------------>|--> user being
108
- |<-- tool list ---|<-- describe for this asker --------------------------------|
109
- |-- tools/call -->|-- ask(name, args, { time }) ---------------------------->|--> user being
110
- |<-- result ------|<-- object | error | silence | unreached ------------------|
111
- ```
112
-
113
- From a hosted client, claude.ai or Claude Desktop, this is Settings,
114
- Connectors, Add custom connector, with the URL `https://mcp.<domain>/mcp`.
115
- The client registers itself, the browser lands on `web.<domain>/allow`, the
116
- page, `mcp/allow.ts`, the one page of the model side and mounted on the
117
- web route, takes the invitation the root minted for it and the identity, and that
118
- identity is the avatar the client speaks through from then on. The page
119
- posts to its own path and names no root, so it holds under a hostname per
120
- route and under one origin with the routes as paths, as a box with a
121
- single hostname runs it;
122
- and it offers the worlds the box hosts, never the dock ward, which is the
123
- box's and nobody's world. Discovery holds under both as well: the route
124
- serves its two documents under its own `.well-known`, which is the root's
125
- under a hostname per route, and the terrain serves the same two where its
126
- own root is, with the issuer's path inserted, which is where a hosted client
127
- looks for an issuer that has a path: the origin's root on a daemon, the
128
- harbor's own prefix at the edge. Removing the
129
- client is either revoking at the route or removing the occupant on the user
130
- being; both end it.
131
-
132
- The page itself is the human side's, and `papers/quo-human.md` holds what it
133
- must say and why it is hand-written rather than a blueprint: at this page a
134
- human is a stranger holding nothing, so there is no describe to paint and
135
- no standing to paint it through. What matters on this side is that it is
136
- the one page of the model side, that it wears the same stylesheet every
137
- screen wears rather than a look of its own, and that nothing but an
138
- invitation is ever typed on it.
139
-
140
- ### A local client connects over stdio
141
-
142
- Identical, with the differences that there is no OAuth, the proof is that the
143
- process runs as the human on the device, and the avatar lives in the laptop
144
- harbor. No droplet is needed for this. A dialer harbor is enough, because the
145
- model is on the same machine as the ward it talks to.
146
-
147
- One thing to know when a client launches `nervur side` or `nervur pilot`: the MCP
148
- SDK spawns the server with a stripped environment, without `TMPDIR`. A
149
- harbor folder whose path is long gets its sockets in the temp dir under a
150
- hashed name, and the child then computes a different temp dir and finds no
151
- daemon. The default `~/.nervur` is short and unaffected; for a long folder,
152
- hand the client the full environment.
153
-
154
- ### Two ways a model is driven
155
-
156
- Every model is driven in one of two ways, and the avatar is the same in
157
- both. What differs is who starts the loop.
158
-
159
- **The human is the loop.** You are in a chat and the model is your hands.
160
- You say "book the visit", it chains availability, calendar and book, each
161
- one an ask through your gate under the id you minted for it, and acme sees
162
- your id. Without the model you would press the buttons yourself, and that
163
- sentence is literally true: the screen is the user being's describe spoken
164
- as forms, the tool list is the same describe spoken as tools. Two sides onto
165
- one gate, and neither can do a thing the other cannot.
166
-
167
- **The world is the loop.** An agent lives alone in a folder with its own
168
- constitution, a `CLAUDE.md` or the equivalent for any other model, and is
169
- woken by events. The flow:
170
-
171
- 1. **The trigger is a reverse ask.** The user being holds a standing to the
172
- agent's avatar and asks it when something happens. The avatar's `answer`
173
- is the side: it starts a run, `claude -p` or any CLI agent, in the
174
- agent's folder with the event as the prompt.
175
- 2. **Ack now, report later.** A run takes minutes; an ask waits thirty
176
- seconds by default and five at the ceiling, and a late reply is not read.
177
- So the avatar answers at once with an acknowledgement, and when the run
178
- ends it asks the user being back on its own standing with the result.
179
- A run never holds an ask open.
180
- 3. **One run at a time.** Asks are concurrent by design, so two events would
181
- start two runs in one folder. The avatar serializes runs per agent: one
182
- constitution, one train of thought. A queue in her cells is enough.
183
- 4. **Roles stay apart in one agent.** The run that does chores goes through
184
- the gate as an occupant. If the same agent also boots beings, it holds the
185
- owner pilot, and that is the estate: a separate run, through the pilot,
186
- never the one doing the work.
187
-
188
- Two layers of permission exist here, and only one is real. The gate on the
189
- user being decides what the agent may ask, in hard terms. The constitution
190
- decides how it behaves, in soft terms. A permission never lives in the soft
191
- layer: a run gone wrong can do exactly what the gate shows the asker
192
- "agent", so that list is the narrowest that still gets the job done.
193
-
194
- The conventions this side pins, in `packages/dock/mcp/agent.ts`:
195
-
196
- | the agent side does | as |
197
- | --------------------- | --------------------------------------------------------------- |
198
- | the event | the object the user being pushed; `notify` answers at once |
199
- | the queue | her inbox, in her cells, so it survives the side and a reboot |
200
- | a run | a function of the event, or a process in the agent's folder |
201
- | the process | the command, then the event as JSON as its last argument |
202
- | its result | `{ output }` from stdout, or `{ error, stderr }` on a bad exit |
203
- | a run that throws | `{ error }` with what it said |
204
- | the callback | `report({ event, result })` on the user being, gated to devices |
205
- | a report unanswered | dropped, never retried, never held |
206
- | two events | one after the other, first in first out |
207
- | on a device | a row the root keeps on the `agents` faculty, in her cells |
208
- | who wakes her | the desk, or a device the human allowed to wake at the exchange |
209
-
210
- An event leaves the queue before its run starts, so a crash mid-run loses that
211
- event and never runs it twice. A run costs the human's quota, so the agent side
212
- never starts one on its own: only a push starts a run, one per event, never
213
- retried, and her row names what runs: a command with its args in a folder,
214
- `claude -p` on a droplet, Haiku and no tools; or a model that speaks
215
- function calling, a base URL and a model name, and then the runner below
216
- is the side that stands for her. The row is the root's to place, once, on
217
- the dock ward's `agents` faculty, the way a schedule is placed on the
218
- clock, and it lives in her cells and in no file:
219
-
220
- ```bash
221
- nervur ask --ward dock agents keep '{"id":"claude","ward":"main","command":"claude","args":["-p","--model","haiku"],"dir":"/home/nervur/claude"}'
222
- nervur ask --ward dock agents keep '{"id":"kimi","ward":"main","url":"https://openrouter.ai/api/v1","model":"moonshotai/kimi-k2","key":"..."}'
223
- ```
224
-
225
- She runs at once and again at every boot; `drop` stops her and forgets the
226
- row, and the avatar in her ward stays until the root removes her. The
227
- faculty is a being: today `keep` is the root's, since a row spends a
228
- process or a paid model, and a world that wants to place one from a
229
- standing at her is one gate away. `report` is the
230
- one ask this step
231
- added to the user being: every device may say what a run of its found, and she
232
- keeps it in her cells for whoever renders her. The proof is
233
- `packages/dock/test/agent.test.ts` with a fake run and a fake process.
234
-
235
- ### A model that does not speak MCP
236
-
237
- OpenAI-style function calling, Kimi, LM Studio, OpenRouter, anything with a
238
- tools array. The side is a **runner**: a loop you own that holds the avatar's
239
- standing on one side and the model's API on the other.
240
-
241
- - The runner calls the empty ask and hands the model `asks[]` as its tool
242
- definitions. The schema is already JSON Schema 2020-12; most APIs take it
243
- verbatim, and the runner narrows it where one does not.
244
- - Each tool call from the model becomes a named ask on the avatar's standing.
245
- - Each answer, error, silence or word goes back as the tool result, in the
246
- same mapping as above.
247
-
248
- The runner is not a second architecture. It is an MCP client written inline,
249
- and it uses the same avatar, the same invitation, the same gate. A model's
250
- capability changes how well it uses the tools and nothing about what it can
251
- reach: a weak model calls the wrong ask and gets an error object.
252
-
253
- What moved in-process is the host. An MCP client drives the loop from
254
- outside; a completions API cannot, so the runner drives it. It holds one
255
- conversation for as long as it serves, and two things start a turn in it:
256
- a line from the human, which is the human being the loop, and a push from
257
- the user being, which is the world being the loop. The conventions the
258
- runner pins, in `packages/dock/mcp/runner.ts`:
259
-
260
- | the runner does | as |
261
- | ------------------------------ | ------------------------------------------------------------ |
262
- | the model | a base URL, a model name, a bearer from the device if wanted |
263
- | her describe | `tools[]` of `type: function`, name, description, parameters |
264
- | an ask with no properties | sent with `properties: {}`; LM Studio refuses it bare |
265
- | a tool call | a named ask; arguments that are not JSON are an error object |
266
- | a tool result | one `tool` message: JSON, or the text of the two words |
267
- | a push | a `user` message holding the object; it starts a turn |
268
- | a turn | until the model answers with no tool call, or ten calls |
269
- | two turns | never at once: the second waits, one train of thought |
270
-
271
- Ten is a ceiling, not a permission: a turn that ends there has no final
272
- text, and the gate decided everything the model reached. The proof is
273
- `packages/dock/test/runner.test.ts` over a scripted endpoint on loopback, and the
274
- same suite runs one live turn when `NERVUR_MODEL_URL` names a real endpoint.
275
-
276
- From a terminal the runner is `nervur run --as NAME --url URL --model NAME`:
277
- the same pipe as `nervur side`, with the model named in the hello line, so
278
- the daemon runs the runner beside the avatar it boots for the name. A line
279
- typed is a turn; the model's final text is a line back; a push from the
280
- user being starts a turn of its own and its text comes back the same way.
281
- A turn that ends at the ceiling, `--turns`, has no final text and comes
282
- back as an empty line; a model that fails comes back as one line holding
283
- an error object. The bearer, `NERVUR_MODEL_KEY`, is read by the command from
284
- the device and crosses only the local socket.
285
-
286
- ### A model as the owner
287
-
288
- An **owner pilot** is the model side above, pointed at a ward instead of an
289
- avatar. The ward is a being to her owner, and `quo/SPEC.md` says her
290
- describe carries her asks with a description and an input each, so the
291
- pilot holds no list of its own: tools/list is the ward's describe, the
292
- `describe` tool is the census, and a tool call is an owner ask, boot,
293
- public, invite, knock, remove, unboot or ask, in the ward's own words. It
294
- reaches those asks either as the root through the daemon's socket, or as an
295
- owner the root invited, through a standing over the sealed door. It keeps
296
- nothing and logs nothing: what a model did with it is in the host's own
297
- transcript, and an invitation it was handed is in no file of the dock's.
298
- The pilot is handed to one local agent by the device's own rules. A remote
299
- pilot is a standing at the ward, and still not a route: a model without a
300
- ward of its own cannot be an owner anywhere. `nervur pilot --via S` is that
301
- pilot: S is a standing the user being here holds at the
302
- far ward, taken when the root here knocked for her with an invitation the
303
- far root minted on its ward's pk, and every tool call is a sealed ask
304
- there, answered as the far ward answers an owner at its door.
305
-
306
- The owner does not do the work. A Claude Code session that boots a being and
307
- wants to try it connects to an avatar like anyone else and asks through the
308
- gate. If it could act as the owner, the arrival would be unnamed and no gate
309
- would have judged it.
310
-
311
- ### The estate agent
312
-
313
- The spec has no estate on purpose. On the droplet it is `claude -p`, or any
314
- CLI agent, under cron, holding the owner pilot and one avatar:
315
-
316
- - through the pilot: read the census, boot again a being whose digest is
317
- null this run, report a class the harbor no longer holds.
318
- - through the avatar: run the chores the human gated to it.
319
-
320
- Nothing about the estate agent is inside a ward. Lose the cron and the world
321
- keeps running; it just stops being looked after.
322
-
323
- ### A model as the developer
324
-
325
- A local agent writes a being class to disk. The harbor holds class bodies, so
326
- a new class is a file the harbor loads and a name the owner boots. The
327
- developer role is a file system and an editor, plus the owner pilot to boot.
328
- Nothing crosses the wire.
329
-
330
- ## Invariants of this side
331
-
332
- On top of the shared ones in the trunk:
333
-
334
- 1. A run never holds an ask open: it acknowledges, and asks back with the
335
- result.
336
- 2. A permission lives in a gate, never in a constitution.
337
- 3. The runner and the MCP server are two envelopes over one avatar; no
338
- behaviour lives in either that the other lacks.
339
-
340
- ## Glossary of this side
341
-
342
- Its words, constitution, runner and owner pilot, are in `papers/GLOSSARY.md`,
343
- beside the spec, with every other word above it.