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.
- package/CONTRIBUTING.md +13 -0
- package/LICENSE +55 -202
- package/NOTICE +3 -8
- package/README.md +14 -156
- package/RELEASING.md +15 -0
- package/SECURITY.md +9 -0
- package/bin/nervur.js +6 -0
- package/package.json +26 -81
- package/AUTHORING.md +0 -935
- package/COMMAND.md +0 -179
- package/FACES.md +0 -280
- package/FACULTIES.md +0 -578
- package/GROUNDS.md +0 -315
- package/KIT-SPEC.md +0 -304
- package/WORLD.md +0 -195
- package/dist/app/app-ground.d.ts +0 -18
- package/dist/app/app-ground.js +0 -23
- package/dist/app/index.d.ts +0 -2
- package/dist/app/index.js +0 -5
- package/dist/app/native.d.ts +0 -11
- package/dist/app/native.js +0 -13
- package/dist/being/being.d.ts +0 -306
- package/dist/being/being.js +0 -20
- package/dist/being/covers.d.ts +0 -9
- package/dist/being/covers.js +0 -21
- package/dist/being/index.d.ts +0 -4
- package/dist/being/index.js +0 -6
- package/dist/being/need.d.ts +0 -49
- package/dist/being/need.js +0 -33
- package/dist/being/schema.d.ts +0 -100
- package/dist/being/schema.js +0 -132
- package/dist/being/table.d.ts +0 -48
- package/dist/being/table.js +0 -344
- package/dist/bench/bench-ground.d.ts +0 -73
- package/dist/bench/bench-ground.js +0 -153
- package/dist/bench/bench.d.ts +0 -101
- package/dist/bench/bench.js +0 -368
- package/dist/bench/fake-clock.d.ts +0 -17
- package/dist/bench/fake-clock.js +0 -34
- package/dist/bench/fake-memory.d.ts +0 -16
- package/dist/bench/fake-memory.js +0 -46
- package/dist/bench/fake-network.d.ts +0 -66
- package/dist/bench/fake-network.js +0 -222
- package/dist/bench/fake-unlock.d.ts +0 -6
- package/dist/bench/fake-unlock.js +0 -10
- package/dist/bench/index.d.ts +0 -3
- package/dist/bench/index.js +0 -5
- package/dist/bench/seeded.d.ts +0 -11
- package/dist/bench/seeded.js +0 -37
- package/dist/bench/settle.d.ts +0 -2
- package/dist/bench/settle.js +0 -49
- package/dist/bench/stewards.d.ts +0 -169
- package/dist/bench/stewards.js +0 -128
- package/dist/bodies/class-list.d.ts +0 -14
- package/dist/bodies/class-list.js +0 -33
- package/dist/bodies/joined-carry.d.ts +0 -29
- package/dist/bodies/joined-carry.js +0 -48
- package/dist/bodies/kept.d.ts +0 -44
- package/dist/bodies/kept.js +0 -91
- package/dist/bodies/noble-crypto.d.ts +0 -22
- package/dist/bodies/noble-crypto.js +0 -88
- package/dist/bodies/seed-keys.d.ts +0 -10
- package/dist/bodies/seed-keys.js +0 -42
- package/dist/bodies/strict-tools.d.ts +0 -10
- package/dist/bodies/strict-tools.js +0 -306
- package/dist/bodies/vouch.d.ts +0 -20
- package/dist/bodies/vouch.js +0 -71
- package/dist/bodies/web-carry.d.ts +0 -72
- package/dist/bodies/web-carry.js +0 -245
- package/dist/bodies/web-clock.d.ts +0 -12
- package/dist/bodies/web-clock.js +0 -33
- package/dist/browser/browser-ground.d.ts +0 -76
- package/dist/browser/browser-ground.js +0 -294
- package/dist/browser/index.d.ts +0 -4
- package/dist/browser/index.js +0 -7
- package/dist/browser/indexeddb-memory.d.ts +0 -17
- package/dist/browser/indexeddb-memory.js +0 -97
- package/dist/browser/locked-unlock.d.ts +0 -24
- package/dist/browser/locked-unlock.js +0 -85
- package/dist/browser/origin-classes.d.ts +0 -7
- package/dist/browser/origin-classes.js +0 -31
- package/dist/edge/durable-clock.d.ts +0 -19
- package/dist/edge/durable-clock.js +0 -61
- package/dist/edge/durable-memory.d.ts +0 -5
- package/dist/edge/durable-memory.js +0 -11
- package/dist/edge/durable.d.ts +0 -18
- package/dist/edge/durable.js +0 -24
- package/dist/edge/edge-ground.d.ts +0 -49
- package/dist/edge/edge-ground.js +0 -223
- package/dist/edge/index.d.ts +0 -7
- package/dist/edge/index.js +0 -9
- package/dist/edge/native-crypto.d.ts +0 -8
- package/dist/edge/native-crypto.js +0 -102
- package/dist/edge/secret-unlock.d.ts +0 -7
- package/dist/edge/secret-unlock.js +0 -13
- package/dist/edge/socket-carry.d.ts +0 -39
- package/dist/edge/socket-carry.js +0 -111
- package/dist/foundation.d.ts +0 -162
- package/dist/foundation.js +0 -4
- package/dist/ground/dock.d.ts +0 -1460
- package/dist/ground/dock.js +0 -1159
- package/dist/ground/ground.d.ts +0 -239
- package/dist/ground/ground.js +0 -787
- package/dist/ground/views.d.ts +0 -76
- package/dist/ground/views.js +0 -208
- package/dist/house/crossing.d.ts +0 -50
- package/dist/house/crossing.js +0 -153
- package/dist/house/house.d.ts +0 -88
- package/dist/house/house.js +0 -2069
- package/dist/house/rows-shape.d.ts +0 -110
- package/dist/house/rows-shape.js +0 -12
- package/dist/house/rows.d.ts +0 -27
- package/dist/house/rows.js +0 -185
- package/dist/index.d.ts +0 -33
- package/dist/index.js +0 -29
- package/dist/node/bridge.d.ts +0 -20
- package/dist/node/bridge.js +0 -284
- package/dist/node/cli.d.ts +0 -2
- package/dist/node/cli.js +0 -225
- package/dist/node/file-memory.d.ts +0 -13
- package/dist/node/file-memory.js +0 -86
- package/dist/node/folder-classes.d.ts +0 -5
- package/dist/node/folder-classes.js +0 -40
- package/dist/node/gone.d.ts +0 -2
- package/dist/node/gone.js +0 -7
- package/dist/node/hand.d.ts +0 -39
- package/dist/node/hand.js +0 -107
- package/dist/node/http.d.ts +0 -11
- package/dist/node/http.js +0 -90
- package/dist/node/index.d.ts +0 -11
- package/dist/node/index.js +0 -13
- package/dist/node/ledger-memory.d.ts +0 -17
- package/dist/node/ledger-memory.js +0 -152
- package/dist/node/lock.d.ts +0 -10
- package/dist/node/lock.js +0 -78
- package/dist/node/node-ground.d.ts +0 -26
- package/dist/node/node-ground.js +0 -228
- package/dist/node/notify.d.ts +0 -1
- package/dist/node/notify.js +0 -18
- package/dist/node/shell.d.ts +0 -11
- package/dist/node/shell.js +0 -35
- package/dist/node/sync-folder.d.ts +0 -6
- package/dist/node/sync-folder.js +0 -18
- package/dist/node/tcp-carry.d.ts +0 -42
- package/dist/node/tcp-carry.js +0 -193
- package/dist/node/unlock.d.ts +0 -17
- package/dist/node/unlock.js +0 -86
- package/dist/node/websocket.d.ts +0 -12
- package/dist/node/websocket.js +0 -134
- package/dist/quo/frame.d.ts +0 -33
- package/dist/quo/frame.js +0 -92
- package/dist/quo/index.d.ts +0 -1
- package/dist/quo/index.js +0 -5
- package/dist/quo/room.d.ts +0 -154
- package/dist/quo/room.js +0 -362
- package/dist/serve/index.d.ts +0 -45
- 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.
|