nervur 0.23.0 → 0.24.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.
Files changed (157) hide show
  1. package/CONTRIBUTING.md +13 -0
  2. package/LICENSE +55 -202
  3. package/NOTICE +3 -8
  4. package/README.md +14 -156
  5. package/RELEASING.md +15 -0
  6. package/SECURITY.md +9 -0
  7. package/bin/nervur.js +6 -0
  8. package/package.json +26 -81
  9. package/AUTHORING.md +0 -935
  10. package/COMMAND.md +0 -179
  11. package/FACES.md +0 -280
  12. package/FACULTIES.md +0 -578
  13. package/GROUNDS.md +0 -315
  14. package/KIT-SPEC.md +0 -304
  15. package/WORLD.md +0 -195
  16. package/dist/app/app-ground.d.ts +0 -18
  17. package/dist/app/app-ground.js +0 -23
  18. package/dist/app/index.d.ts +0 -2
  19. package/dist/app/index.js +0 -5
  20. package/dist/app/native.d.ts +0 -11
  21. package/dist/app/native.js +0 -13
  22. package/dist/being/being.d.ts +0 -306
  23. package/dist/being/being.js +0 -20
  24. package/dist/being/covers.d.ts +0 -9
  25. package/dist/being/covers.js +0 -21
  26. package/dist/being/index.d.ts +0 -4
  27. package/dist/being/index.js +0 -6
  28. package/dist/being/need.d.ts +0 -49
  29. package/dist/being/need.js +0 -33
  30. package/dist/being/schema.d.ts +0 -100
  31. package/dist/being/schema.js +0 -132
  32. package/dist/being/table.d.ts +0 -48
  33. package/dist/being/table.js +0 -344
  34. package/dist/bench/bench-ground.d.ts +0 -73
  35. package/dist/bench/bench-ground.js +0 -153
  36. package/dist/bench/bench.d.ts +0 -101
  37. package/dist/bench/bench.js +0 -368
  38. package/dist/bench/fake-clock.d.ts +0 -17
  39. package/dist/bench/fake-clock.js +0 -34
  40. package/dist/bench/fake-memory.d.ts +0 -16
  41. package/dist/bench/fake-memory.js +0 -46
  42. package/dist/bench/fake-network.d.ts +0 -66
  43. package/dist/bench/fake-network.js +0 -222
  44. package/dist/bench/fake-unlock.d.ts +0 -6
  45. package/dist/bench/fake-unlock.js +0 -10
  46. package/dist/bench/index.d.ts +0 -3
  47. package/dist/bench/index.js +0 -5
  48. package/dist/bench/seeded.d.ts +0 -11
  49. package/dist/bench/seeded.js +0 -37
  50. package/dist/bench/settle.d.ts +0 -2
  51. package/dist/bench/settle.js +0 -49
  52. package/dist/bench/stewards.d.ts +0 -169
  53. package/dist/bench/stewards.js +0 -128
  54. package/dist/bodies/class-list.d.ts +0 -14
  55. package/dist/bodies/class-list.js +0 -33
  56. package/dist/bodies/joined-carry.d.ts +0 -29
  57. package/dist/bodies/joined-carry.js +0 -48
  58. package/dist/bodies/kept.d.ts +0 -44
  59. package/dist/bodies/kept.js +0 -91
  60. package/dist/bodies/noble-crypto.d.ts +0 -22
  61. package/dist/bodies/noble-crypto.js +0 -88
  62. package/dist/bodies/seed-keys.d.ts +0 -10
  63. package/dist/bodies/seed-keys.js +0 -42
  64. package/dist/bodies/strict-tools.d.ts +0 -10
  65. package/dist/bodies/strict-tools.js +0 -306
  66. package/dist/bodies/vouch.d.ts +0 -20
  67. package/dist/bodies/vouch.js +0 -71
  68. package/dist/bodies/web-carry.d.ts +0 -72
  69. package/dist/bodies/web-carry.js +0 -245
  70. package/dist/bodies/web-clock.d.ts +0 -12
  71. package/dist/bodies/web-clock.js +0 -33
  72. package/dist/browser/browser-ground.d.ts +0 -76
  73. package/dist/browser/browser-ground.js +0 -294
  74. package/dist/browser/index.d.ts +0 -4
  75. package/dist/browser/index.js +0 -7
  76. package/dist/browser/indexeddb-memory.d.ts +0 -17
  77. package/dist/browser/indexeddb-memory.js +0 -97
  78. package/dist/browser/locked-unlock.d.ts +0 -24
  79. package/dist/browser/locked-unlock.js +0 -85
  80. package/dist/browser/origin-classes.d.ts +0 -7
  81. package/dist/browser/origin-classes.js +0 -31
  82. package/dist/edge/durable-clock.d.ts +0 -19
  83. package/dist/edge/durable-clock.js +0 -61
  84. package/dist/edge/durable-memory.d.ts +0 -5
  85. package/dist/edge/durable-memory.js +0 -11
  86. package/dist/edge/durable.d.ts +0 -18
  87. package/dist/edge/durable.js +0 -24
  88. package/dist/edge/edge-ground.d.ts +0 -49
  89. package/dist/edge/edge-ground.js +0 -223
  90. package/dist/edge/index.d.ts +0 -7
  91. package/dist/edge/index.js +0 -9
  92. package/dist/edge/native-crypto.d.ts +0 -8
  93. package/dist/edge/native-crypto.js +0 -102
  94. package/dist/edge/secret-unlock.d.ts +0 -7
  95. package/dist/edge/secret-unlock.js +0 -13
  96. package/dist/edge/socket-carry.d.ts +0 -39
  97. package/dist/edge/socket-carry.js +0 -111
  98. package/dist/foundation.d.ts +0 -162
  99. package/dist/foundation.js +0 -4
  100. package/dist/ground/dock.d.ts +0 -1460
  101. package/dist/ground/dock.js +0 -1159
  102. package/dist/ground/ground.d.ts +0 -239
  103. package/dist/ground/ground.js +0 -787
  104. package/dist/ground/views.d.ts +0 -76
  105. package/dist/ground/views.js +0 -208
  106. package/dist/house/crossing.d.ts +0 -50
  107. package/dist/house/crossing.js +0 -153
  108. package/dist/house/house.d.ts +0 -88
  109. package/dist/house/house.js +0 -2069
  110. package/dist/house/rows-shape.d.ts +0 -110
  111. package/dist/house/rows-shape.js +0 -12
  112. package/dist/house/rows.d.ts +0 -27
  113. package/dist/house/rows.js +0 -185
  114. package/dist/index.d.ts +0 -33
  115. package/dist/index.js +0 -29
  116. package/dist/node/bridge.d.ts +0 -20
  117. package/dist/node/bridge.js +0 -284
  118. package/dist/node/cli.d.ts +0 -2
  119. package/dist/node/cli.js +0 -225
  120. package/dist/node/file-memory.d.ts +0 -13
  121. package/dist/node/file-memory.js +0 -86
  122. package/dist/node/folder-classes.d.ts +0 -5
  123. package/dist/node/folder-classes.js +0 -40
  124. package/dist/node/gone.d.ts +0 -2
  125. package/dist/node/gone.js +0 -7
  126. package/dist/node/hand.d.ts +0 -39
  127. package/dist/node/hand.js +0 -107
  128. package/dist/node/http.d.ts +0 -11
  129. package/dist/node/http.js +0 -90
  130. package/dist/node/index.d.ts +0 -11
  131. package/dist/node/index.js +0 -13
  132. package/dist/node/ledger-memory.d.ts +0 -17
  133. package/dist/node/ledger-memory.js +0 -152
  134. package/dist/node/lock.d.ts +0 -10
  135. package/dist/node/lock.js +0 -78
  136. package/dist/node/node-ground.d.ts +0 -26
  137. package/dist/node/node-ground.js +0 -228
  138. package/dist/node/notify.d.ts +0 -1
  139. package/dist/node/notify.js +0 -18
  140. package/dist/node/shell.d.ts +0 -11
  141. package/dist/node/shell.js +0 -35
  142. package/dist/node/sync-folder.d.ts +0 -6
  143. package/dist/node/sync-folder.js +0 -18
  144. package/dist/node/tcp-carry.d.ts +0 -42
  145. package/dist/node/tcp-carry.js +0 -193
  146. package/dist/node/unlock.d.ts +0 -17
  147. package/dist/node/unlock.js +0 -86
  148. package/dist/node/websocket.d.ts +0 -12
  149. package/dist/node/websocket.js +0 -134
  150. package/dist/quo/frame.d.ts +0 -33
  151. package/dist/quo/frame.js +0 -92
  152. package/dist/quo/index.d.ts +0 -1
  153. package/dist/quo/index.js +0 -5
  154. package/dist/quo/room.d.ts +0 -154
  155. package/dist/quo/room.js +0 -362
  156. package/dist/serve/index.d.ts +0 -45
  157. package/dist/serve/index.js +0 -71
package/COMMAND.md DELETED
@@ -1,179 +0,0 @@
1
- # The command
2
-
3
- `nervur` is the command the package installs. It holds no business of
4
- its own. Two words start things on your machine, and every other word is
5
- read from what the running ground says it holds. This guide assumes you
6
- have read [the package's start](README.md), which runs a first ground.
7
-
8
- ## Starting a ground
9
-
10
- `nervur up` runs a NodeGround on a folder until it is stopped. The
11
- folder holds your code: a folder for each house, and each module or
12
- program your entries name. `state/` holds the ground's key, its ledger
13
- and its hand. The key is drawn on the first start, into a file your user
14
- alone reads.
15
-
16
- ```bash
17
- npx nervur up .
18
- ```
19
-
20
- The ground stops cleanly on an interrupt and on `SIGTERM`. Its ledger
21
- holds a lock on its state while it stands, so a second `nervur up` on
22
- the same folder refuses to start. Keep `state/` as you keep an ssh key:
23
- whoever holds its key and its ledger holds the ground, and without the
24
- key the ledger opens nothing.
25
-
26
- `nervur service` prints what keeps the ground running across reboots: a
27
- systemd unit on Linux, a launchd job on macOS. Nothing is installed; you
28
- place what it prints.
29
-
30
- ```bash
31
- npx nervur service /srv/shop
32
- ```
33
-
34
- ## Settings
35
-
36
- The environment names only what opens the ground's memory, its key and
37
- its hand.
38
-
39
- | Variable | What it names |
40
- | --- | --- |
41
- | `NERVUR_STATE` | the state folder, `state/` in the ground's folder where unset |
42
- | `NERVUR_UNLOCK` | on macOS, `keychain:<account>` keeps the key in the keychain in place of `state/key` |
43
- | `NERVUR_HAND` | where the hand's socket is, `state/hand` where unset; a relative path is read from where the command runs |
44
-
45
- Every other setting is an entry the ground's dock keeps, set through the
46
- hand and kept across a restart. The `tcp` entry listens on the loopback
47
- at 9110 until you name another bind, and the `web` entry listens on no
48
- port until one is named. The web serves the ground's one listener,
49
- which its entry names in `faculties`.
50
-
51
- | Arg | Of | What it sets |
52
- | --- | --- | --- |
53
- | `port` | `tcp`, `web` | the port it listens on; with none, it only dials |
54
- | `bind` | `tcp`, `web` | the address it listens on, every interface where unset |
55
- | `addresses` | `tcp`, `web` | the public addresses written into invitations |
56
- | `origins` | `web` | the page origins the web listener answers |
57
- | `allowPrivate` | `tcp`, `web` | `true` to let the ground dial a private or loopback address |
58
-
59
- ```bash
60
- npx nervur faculties update name=web make=web faculties='["listener"]' args='{"port":8080,"addresses":["https://shop.example/quo"]}'
61
- npx nervur wait set wait=30000
62
- ```
63
-
64
- `faculties catalog` prints every faculty the ground can raise and what
65
- each takes, so an entry it refuses says which arg failed. `wait set`
66
- keeps the longest any ask may run, in milliseconds, and `wait show`
67
- prints it.
68
-
69
- A ground that people reach names its public addresses. Without them, it
70
- writes only the addresses it listens on, which a stranger cannot reach.
71
-
72
- A socket's path fits 103 bytes on macOS and 107 on Linux, and a longer
73
- one refuses to start by name. A ground in a deep folder names a shorter
74
- path in `NERVUR_HAND`, such as `state/hand` run from the folder, and every
75
- command that reaches it names the same.
76
-
77
- ## Asking the ground
78
-
79
- Every other word goes to the ground's hand, a socket in `state/` that
80
- your user alone may open. Run the command in the ground's folder, or
81
- name the socket with `--at <socket>` or `NERVUR_HAND`.
82
-
83
- `help` prints what the ground holds: the asks of its dock, each faculty
84
- with its methods or why it is down, and each house.
85
-
86
- ```bash
87
- npx nervur help
88
- ```
89
-
90
- The dock is the ground's own house, and its steward changes the ground.
91
- Two words ask her, so `houses add` asks `housesAdd`. One word alone
92
- prints the asks it begins. `secrets set` keeps a secret in a being of
93
- the dock, whose cells no one reads. `faculties add` raises a faculty by
94
- the `up` its entry names, and `houses add` opens a house on its entry.
95
- Each answers why the body is down or the house closed, where it is.
96
- Each `update` lands a new entry in one write, and `faculties restart`
97
- takes a body down and up again. A faculty named with a method is called
98
- through the dock, and named alone it prints its methods.
99
-
100
- An entry names only faculties and secrets the ground holds already. A
101
- module and a program stand on the folder's registry, so their entries
102
- say `from=folder`.
103
-
104
- ```bash
105
- npx nervur faculties add name=recipe from=folder make=module args='{"at":"recipe.ts"}'
106
- npx nervur faculties add name=payments from=recipe make=payments secrets='["stripe-key"]'
107
- npx nervur houses add name=main classes='{"faculty":"folder","at":"house"}' faculties='["payments"]'
108
- ```
109
-
110
- `ask` asks a being of a house, as the house's owner. `--id <being>` names
111
- her, and with none it asks the steward. With no method, it prints what
112
- she shows.
113
-
114
- ```bash
115
- npx nervur ask main
116
- ```
117
-
118
- `--cells` reads a being's cells as they last landed, and asks nothing.
119
- Nothing but the hand reads cells, which makes it the tool for a test, a
120
- repair, or a look at what her asks do not show.
121
-
122
- ```bash
123
- npx nervur ask main --cells
124
- ```
125
-
126
- `ask dock` asks the dock's own beings, since no house takes that name.
127
- `faculty.<name>`, `house.<name>` and the steward hold every entry, seed
128
- and setting, and a secret's cells are shown to no one.
129
-
130
- ```bash
131
- npx nervur ask dock --id faculty.web --cells
132
- ```
133
-
134
- `shell run` runs a command on the ground's machine, in its code folder,
135
- and prints its exit code and what it printed. The words after `--` are
136
- the command line. Its environment holds nothing of the ground's.
137
-
138
- ```bash
139
- npx nervur shell run -- git status
140
- ```
141
-
142
- The command asks no watch. A watch is held by a being on her standing,
143
- or by a page through a face, where something waits on its answer.
144
-
145
- ## Arguments and answers
146
-
147
- Arguments are one JSON object, or words `key=value`. A value is read as
148
- JSON where it reads as JSON, and as text where it does not, so
149
- `count=3` is a number and `name=Ada` is text.
150
-
151
- ```bash
152
- npx nervur ask main hello '{"name":"Ada"}'
153
- ```
154
-
155
- A lone `-` reads the one JSON object from standard input instead. So a
156
- secret never stands on a command line or in a shell's history.
157
-
158
- ```bash
159
- npx nervur secrets set - < stripe-key.json
160
- ```
161
-
162
- `--call <id>`, before the words, names the ask's call id. The same ask
163
- sent again with the same id answers what the first answered, and runs
164
- nothing twice. So a script that lost an answer sends its ask again.
165
-
166
- ```bash
167
- npx nervur --call deploy-42 houses update name=main classes='{"faculty":"folder","at":"house"}'
168
- ```
169
-
170
- Each word prints one JSON value, the answer, and its exit code says what
171
- came back.
172
-
173
- | Exit | What it means |
174
- | --- | --- |
175
- | 0 | a result, or what she shows |
176
- | 1 | an error the ground or the being answered |
177
- | 2 | nothing was asked: a word the command does not know, or a hand that does not answer |
178
-
179
- So a script tells a refusal from a ground that is down.
package/FACES.md DELETED
@@ -1,280 +0,0 @@
1
- # Faces
2
-
3
- A face is how a person with no house of their own reaches a being: a
4
- site, an API, or a server of tools for an AI agent. It adds nothing new
5
- to the library. A face is a faculty with a handler on the ground's
6
- listener, and the person behind it is one occupant of one being. This
7
- guide writes one face that serves an API and tools together.
8
- [Writing for nervur](AUTHORING.md) teaches beings, and [Writing a
9
- faculty](FACULTIES.md) teaches faculties.
10
-
11
- ## Doors and tokens
12
-
13
- **A person reaches a house in one of two ways.** Their own house holds a
14
- standing and asks over Quo, or a face asks for them with a token. Nothing
15
- else reaches a being.
16
-
17
- **A token is a door.** A being mints an occupant for the person. The face
18
- receives it as a token, since a face is a faculty, and a faculty calls a
19
- handle by its token. The face asks as that occupant, so the being's roles
20
- decide what the person may do, and the face decides nothing.
21
-
22
- **A token reads the describe, and asks by method.** Asked with no method,
23
- a token answers what its occupant may ask now, with each ask's schema. So
24
- an API face shows those asks as endpoints and a tool server as tools, and
25
- the owner writes the business once.
26
-
27
- **One ask gives every other door.** The person's being mints another
28
- occupant for a device, an agent or an API client, each under notes of its
29
- own. It lists its doors and lets any one go.
30
-
31
- ## The desk
32
-
33
- The steward arms the face with a handle to her `signup`. Each person who
34
- signs up is a member being, and the face holds one door on her. A member
35
- declares the face as a need, so the ground's grant decides which classes
36
- a face reaches.
37
-
38
- ```ts
39
- // desk.ts
40
- import { Being, need, s, type Args } from 'nervur/being';
41
-
42
- /** The face, as the steward arms it with a handle to her signup. */
43
- const Face = need('face', {
44
- arm: { args: s.object({ signup: s.handle() }), hints: { idempotent: true } },
45
- });
46
-
47
- /** A member declares the face, so its doors reach her. */
48
- const Reached = need('face', {});
49
-
50
- export class Desk extends Being.of({
51
- kind: 'com.example.desk',
52
- needs: { face: Face },
53
- asks: {
54
- arm: { for: 'root', hints: { idempotent: true } },
55
- signup: { for: 'handle', args: s.object({ name: s.string() }), result: s.object({ door: s.handle() }) },
56
- },
57
- }) {
58
- async arm() {
59
- await this.face.arm({ signup: this.handle('signup') });
60
- }
61
-
62
- // Each person is one member, and the face holds one door on her.
63
- signup({ name }: Args<Desk, 'signup'>) {
64
- const id = `member-${name}`;
65
- this.powers!.bear({ kind: 'com.example.member', id });
66
- return { door: this.powers!.invite({ id, occupant: 'web', notes: { person: true } }) };
67
- }
68
- }
69
-
70
- export class Member extends Being.of({
71
- kind: 'com.example.member',
72
- needs: { face: Reached },
73
- cells: { visits: 0 },
74
- roles: { person: (asker) => asker.steward.person === true },
75
- asks: {
76
- hello: { for: 'person', hints: { readOnly: true }, description: 'Greets the person.', result: s.string() },
77
- visit: { for: 'person', description: 'Counts one visit.', result: s.integer() },
78
- leave: { for: 'person', description: 'Lets this door go.' },
79
- },
80
- }) {
81
- hello() {
82
- return `hello, ${this.id}`;
83
- }
84
-
85
- visit() {
86
- this.cells.visits += 1;
87
- return this.cells.visits;
88
- }
89
-
90
- leave() {
91
- this.occupants.dismiss(this.asker.id);
92
- }
93
- }
94
-
95
- export const steward = Desk;
96
- export const beings = [Member];
97
- ```
98
-
99
- ## The face
100
-
101
- The face keeps the context its `arm` was called with, and calls every
102
- token through it. Its handler answers the requests that are its own and
103
- passes on every other, so it shares the listener with Quo and any other
104
- face. Each call carries an id of the face's own, so a call the house
105
- tries again acts once.
106
-
107
- ```ts
108
- // api.ts
109
- import type { Body, FacultyContext, Handler } from 'nervur';
110
- import { need, s, type Json } from 'nervur/being';
111
-
112
- type Answer = Awaited<ReturnType<FacultyContext['call']>>;
113
- type Entry = { method: string; description?: string; args?: Json; hints?: Json };
114
- type Calls = Pick<FacultyContext, 'call' | 'describe'>;
115
-
116
- /** What the steward arms the face with. */
117
- export const FaceBlueprint = need('face', {
118
- arm: { args: s.object({ signup: s.handle() }), hints: { idempotent: true } },
119
- });
120
-
121
- /**
122
- * One face, two ways in. `POST /signup` makes a person's door and answers
123
- * its token. With the token as a bearer, `GET /api` lists what the door
124
- * may ask, `POST /api/<ask>` asks it, and `POST /mcp` speaks the same
125
- * asks to an agent as tools.
126
- */
127
- export class Api {
128
- #context: Calls | undefined;
129
- #signup = '';
130
-
131
- arm({ signup }: { signup: string }, context: FacultyContext): Promise<Answer> {
132
- this.#context = context;
133
- this.#signup = signup;
134
- return Promise.resolve({ result: null });
135
- }
136
-
137
- // Told its house opened: every door's token answers from the first request, after any restart.
138
- opened(context: Calls): void {
139
- this.#context = context;
140
- }
141
-
142
- #held(): Calls {
143
- if (this.#context === undefined) throw new Error('the face is not armed');
144
- return this.#context;
145
- }
146
-
147
- // Each call an id drawn afresh, so no life of the face repeats one.
148
- #ask(token: string, method: string, args: Json): Promise<Answer> {
149
- return this.#held().call({ token, method, args, id: `api:${crypto.randomUUID()}` });
150
- }
151
-
152
- async #tools(token: string): Promise<Json> {
153
- const described = await this.#held().describe({ token });
154
- if (!('describe' in described)) return { error: described.error };
155
- const asks = (described.describe as { asks: Entry[] }).asks;
156
- return { tools: asks.map(({ method, description, args, hints }) => ({ name: method, description: description ?? '', inputSchema: args ?? { type: 'object' }, annotations: hints ?? {} })) };
157
- }
158
-
159
- readonly handler: Handler = {
160
- fetch: async (request) => {
161
- const path = new URL(request.url).pathname;
162
- if (request.method === 'POST' && path === '/signup') {
163
- const { name } = (await request.json()) as { name: string };
164
- const answer = await this.#held().call({ token: this.#signup, args: { name }, id: `signup:${name}` });
165
- return 'result' in answer ? Response.json({ token: (answer.result as { door: string }).door }) : Response.json(answer, { status: 400 });
166
- }
167
- const token = /^Bearer (\w+)$/.exec(request.headers.get('authorization') ?? '')?.[1];
168
- if (path !== '/api' && !path.startsWith('/api/') && path !== '/mcp') return null;
169
- if (token === undefined) return new Response(null, { status: 401 });
170
- if (request.method === 'GET' && path === '/api') return Response.json(await this.#held().describe({ token }));
171
- if (request.method === 'POST' && path.startsWith('/api/')) return Response.json(await this.#ask(token, path.slice('/api/'.length), (await request.json()) as Json));
172
- if (request.method === 'POST' && path === '/mcp') {
173
- const { id, method, params } = (await request.json()) as { id: number; method: string; params?: { name: string; arguments?: Json } };
174
- if (method === 'tools/list') return Response.json({ jsonrpc: '2.0', id, result: await this.#tools(token) });
175
- if (method === 'tools/call') {
176
- const answer = await this.#ask(token, params?.name ?? '', params?.arguments ?? {});
177
- const text = JSON.stringify('result' in answer ? (answer.result ?? null) : answer.error.message);
178
- return Response.json({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }], isError: 'error' in answer } });
179
- }
180
- return Response.json({ jsonrpc: '2.0', id, error: { code: -32601, message: 'no such method' } });
181
- }
182
- return null;
183
- },
184
- };
185
- }
186
-
187
- /** The face as a body. */
188
- export const apiOffer = (api: Api): Body => ({ blueprint: FaceBlueprint, object: api, handler: api.handler, opened: (context) => api.opened(context) });
189
- ```
190
-
191
- **The describe maps onto tools by renaming.** An ask is a tool, its
192
- method the tool's name, its args schema the tool's input schema, and its
193
- hints the tool's annotations. A moved describe is a changed list of
194
- tools.
195
-
196
- ## The registry
197
-
198
- The ground raises the face from a registry by its `up`, as it raises any
199
- faculty. A NodeGround stands the module below with the faculty
200
- `module`, and the face from it by an entry that names `from`. The
201
- house's entry names `face` among its faculties, and the steward's `arm`
202
- hands the face its signup.
203
-
204
- ```ts
205
- // recipe.ts
206
- import { Api, apiOffer } from './api.ts';
207
-
208
- // A registry: the ground raises the face by its `up` when an entry names it.
209
- export const faculties = {
210
- face: { up: () => apiOffer(new Api()) },
211
- };
212
- ```
213
-
214
- ```bash
215
- npx nervur faculties add name=recipe from=folder make=module args='{"at":"recipe.ts"}'
216
- npx nervur faculties add name=face from=recipe make=face
217
- ```
218
-
219
- ## Testing it
220
-
221
- A BenchGround answers a request with its faculties' handlers in turn, as
222
- a ground's one listener does, so the face is tested with no socket. The
223
- test hands the bench the registry, and stands the face through the
224
- ground's hand, as an owner does.
225
-
226
- ```ts
227
- // api.test.ts
228
- import assert from 'node:assert/strict';
229
- import { test } from 'node:test';
230
- import { BenchGround, FakeNetwork } from 'nervur/bench';
231
- import * as desk from './desk.ts';
232
- import { faculties } from './recipe.ts';
233
-
234
- test('A person signs up at the face, and reaches their member as an API and as tools', async (t) => {
235
- const ground = await BenchGround.open({ network: new FakeNetwork(), host: 'desk', modules: { desk }, registry: { faculties } });
236
- t.after(() => ground.down());
237
- await ground.hand({ method: 'facultiesAdd', args: { name: 'face', make: 'face' } });
238
- await ground.add('desk', 'desk', { faculties: ['face'] });
239
- await ground.ask({ house: 'desk', method: 'arm' });
240
- const web = async (path: string, body?: unknown, token?: string) =>
241
- (await ground.fetch(
242
- new Request(`https://desk.example${path}`, {
243
- method: body === undefined ? 'GET' : 'POST',
244
- headers: token === undefined ? {} : { authorization: `Bearer ${token}` },
245
- ...(body === undefined ? {} : { body: JSON.stringify(body) }),
246
- }),
247
- )).json() as Promise<Record<string, unknown>>;
248
-
249
- const { token } = (await web('/signup', { name: 'ada' })) as { token: string };
250
- const { describe } = (await web('/api', undefined, token)) as { describe: { asks: { method: string }[] } };
251
- assert.deepEqual(describe.asks.map(({ method }) => method).sort(), ['hello', 'leave', 'visit']);
252
- assert.deepEqual(await web('/api/hello', {}, token), { result: 'hello, member-ada' });
253
- assert.deepEqual(await web('/api/visit', {}, token), { result: 1 });
254
-
255
- const { result } = (await web('/mcp', { jsonrpc: '2.0', id: 1, method: 'tools/list' }, token)) as { result: { tools: { name: string; annotations: { readOnly?: boolean } }[] } };
256
- assert.equal(result.tools.find(({ name }) => name === 'hello')?.annotations.readOnly, true, 'a hint is an annotation');
257
- const called = await web('/mcp', { jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'visit' } }, token);
258
- assert.deepEqual(called.result, { content: [{ type: 'text', text: '2' }], isError: false });
259
-
260
- assert.deepEqual(await web('/api/leave', {}, token), { result: null });
261
- assert.deepEqual(await web('/api/hello', {}, token), { error: { message: 'no such token' } }, 'a door let go is no door');
262
- });
263
- ```
264
-
265
- ## Further than one door
266
-
267
- **A watch reaches a page as events.** A token's call takes `after`, the
268
- answer the face holds, and makes a `readOnly` ask a watch. It answers the
269
- moment the answer changes, so a face streams it as server-sent events.
270
-
271
- **A door carried home leaves as an invitation.** A token's call takes
272
- `home: true`. The handles in its answer then reach the face as invitation
273
- bytes, never as tokens. The person carries them to a house of their own,
274
- which takes them. Their being, its cells and its history stay, and the old
275
- token is let go.
276
-
277
- **A session gives every other door.** A browser session is one door.
278
- Through it the person's being mints another for an agent or an API
279
- client, so an OAuth server is a face that turns that ask into its
280
- client's access token.