@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/README.md +9 -10
- package/package.json +4 -13
- package/GETTING_STARTED.md +0 -138
- package/GLOSSARY.md +0 -118
- package/WORLDS.md +0 -569
- package/quo-api.md +0 -196
- package/quo-app-bells.md +0 -280
- package/quo-dock.md +0 -1945
- package/quo-harbor.md +0 -874
- package/quo-human.md +0 -1042
- package/quo-mcp.md +0 -343
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.
|