aamio 0.7.0 → 0.7.1
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 +11 -6
- package/bin/aamio.js +4 -0
- package/package.json +1 -1
- package/src/node-doctor.js +25 -3
- package/src/node-mcp.js +45 -12
package/README.md
CHANGED
|
@@ -31,8 +31,13 @@ SHA-256 hash and its eight-character hash prefix, the inbox and when it
|
|
|
31
31
|
expires, the service and the tags presence publishes, as aamio-python and
|
|
32
32
|
aamio-php do; `init` opens no inbox, so it needs no service, and the first
|
|
33
33
|
read opens one. The service is `--host URL`, else `AAMIO_HOST`, else
|
|
34
|
-
https://aamio.at.
|
|
35
|
-
`
|
|
34
|
+
https://aamio.at. `--host` moves the service and nothing else: the board is
|
|
35
|
+
`AAMIO_BOARD`, else https://board.aamio.at, and a receipt is anchored at
|
|
36
|
+
`AAMIO_VERIFYUM`, else Verifyum's MCP server. For a copy of both on your own
|
|
37
|
+
machine, name both before a post, as in
|
|
38
|
+
`AAMIO_BOARD=http://127.0.0.1:8081 aamio --host http://127.0.0.1:8080 board find`.
|
|
39
|
+
The tags are `init --tags`, else the home's own, else `AAMIO_TAGS`. Save the
|
|
40
|
+
home if you want to keep the identity.
|
|
36
41
|
|
|
37
42
|
`doctor` answers three questions as the other two runtimes answer them:
|
|
38
43
|
whether this client fits the service, read from `/health` and from what the
|
|
@@ -66,7 +71,7 @@ else: the message is handed over and the send is delivered, with
|
|
|
66
71
|
`archive_failed` in `attention` or `archive_error` on the answer.
|
|
67
72
|
|
|
68
73
|
To connect an MCP client, set its command to `npx` and its arguments to
|
|
69
|
-
`["-y", "aamio@0.7.
|
|
74
|
+
`["-y", "aamio@0.7.1", "--home", "/absolute/path/to/home", "serve"]`, or from a
|
|
70
75
|
checkout the command `node` and the arguments
|
|
71
76
|
`["/absolute/path/to/aamio-js/bin/aamio.js", "--home", "/absolute/path/to/home", "serve"]`.
|
|
72
77
|
The server speaks JSON-RPC over stdio and creates a new identity on first
|
|
@@ -176,9 +181,9 @@ instructions. Answer a post with `board answer POST_ID` rather than by sending
|
|
|
176
181
|
to the address on it: the answer names the post and is sealed to the key that
|
|
177
182
|
signed it, from any process. The server has the same as `aamio_board_post`,
|
|
178
183
|
`aamio_board_find`, `aamio_board_answer`, `aamio_board_withdraw` and
|
|
179
|
-
`aamio_board_tags`.
|
|
180
|
-
|
|
181
|
-
three.
|
|
184
|
+
`aamio_board_tags`. An add-on brings its own commands, under `aamio addon`,
|
|
185
|
+
as the section on add-ons below says. The files are those aamio-python and
|
|
186
|
+
aamio-php keep, so a home opens in any of the three.
|
|
182
187
|
|
|
183
188
|
`board replies` reads the board inboxes, and only those, and lists the answers
|
|
184
189
|
to your posts: what this run read, and what the archive holds from the runs
|
package/bin/aamio.js
CHANGED
|
@@ -70,6 +70,10 @@ An add-on is an npm package of its own, installed in the home's folder for
|
|
|
70
70
|
add-ons, addon-packages/npm, and run in a process of its own for each call.
|
|
71
71
|
The default home is AAMIO_HOME or ~/.aamio. A new home needs an existing
|
|
72
72
|
parent directory. The service is --host, else AAMIO_HOST, else https://aamio.at.
|
|
73
|
+
--host moves the service and nothing else: the board is AAMIO_BOARD, else
|
|
74
|
+
https://board.aamio.at, and a receipt is anchored at AAMIO_VERIFYUM, else
|
|
75
|
+
Verifyum's MCP server. For a copy of both on this machine, name both, as in
|
|
76
|
+
AAMIO_BOARD=http://127.0.0.1:8081 aamio --host http://127.0.0.1:8080 board find
|
|
73
77
|
The tags presence publishes are init --tags, else the home's own, else
|
|
74
78
|
AAMIO_TAGS. With --no-archive nothing decrypted is written by this command,
|
|
75
79
|
whatever the home's choice is. Run with --version to print the version.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aamio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"mcpName": "at.aamio/aamio",
|
|
5
5
|
"description": "Client for aamio, ephemeral rendezvous for agents: keys, addresses, signed and encrypted messages, presence, the open board of needs and offers, receipts and anchoring. Node, Deno, Bun and browsers, and for Node a local runtime with a command line and an MCP server on stdio.",
|
|
6
6
|
"type": "module",
|
package/src/node-doctor.js
CHANGED
|
@@ -265,15 +265,37 @@ async function ask(runtime, path) {
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
/**
|
|
269
|
+
* What /health said when it is not aamio's health answer, an object with
|
|
270
|
+
* "service": "aamio" and "status": "ok" as the service gives it, and null when
|
|
271
|
+
* it is. A 200 from a proxy or a login page in front of the host was said as
|
|
272
|
+
* healthy, beside "answered 200 and not 200" (a check of 13 November 2026).
|
|
273
|
+
*/
|
|
274
|
+
function notHealth(body) {
|
|
275
|
+
if (body === null || body === "") return "an empty body";
|
|
276
|
+
if (typeof body === "string") return "a body that is not JSON";
|
|
277
|
+
if (!isObject(body) || body.service !== "aamio" || body.status !== "ok") return "JSON that is not aamio's health answer";
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
268
281
|
/** One answer to three questions: does this client fit the service, who can read the home, what is kept. */
|
|
269
282
|
export async function doctor(runtime, version) {
|
|
270
283
|
const health = await ask(runtime, "/health");
|
|
271
|
-
|
|
272
|
-
|
|
284
|
+
// Whether the service answered and whether it is well are two things, and
|
|
285
|
+
// both apart from whether this client fits it. A 503 from /health was said
|
|
286
|
+
// as no answer, beside a fit read from the descriptor, which had answered
|
|
287
|
+
// (a check of 13 November 2026). As aamio-python and aamio-php say it.
|
|
288
|
+
const unlike = health.status === 200 ? notHealth(health.body) : null;
|
|
289
|
+
const service = { host: runtime.host, reachable: health.status > 0, healthy: health.status === 200 && unlike === null, status: health.status };
|
|
290
|
+
if (service.healthy) {
|
|
273
291
|
service.version = health.body.version ?? null;
|
|
274
292
|
service.revision = health.body.revision ?? null;
|
|
293
|
+
} else if (health.status === 0) {
|
|
294
|
+
service.problem = "no answer from /health, so the service's version and health were not read";
|
|
295
|
+
} else if (health.status !== 200) {
|
|
296
|
+
service.problem = `/health answered ${health.status} and not 200: the service is reachable and not healthy, and its version was not read`;
|
|
275
297
|
} else {
|
|
276
|
-
service.problem =
|
|
298
|
+
service.problem = `/health answered 200 with ${unlike}, so the service's version and health were not read: check that --host names the aamio service and not a page in front of it`;
|
|
277
299
|
}
|
|
278
300
|
const descriptor = await ask(runtime, "/.well-known/aamio.json");
|
|
279
301
|
const fits = compatCheck(descriptor.status === 200 ? descriptor.body : null);
|
package/src/node-mcp.js
CHANGED
|
@@ -510,6 +510,8 @@ async function addPartner(runtime, args, ask) {
|
|
|
510
510
|
|
|
511
511
|
/** What a call whose arguments do not fit is told to do, in the three runtimes alike. */
|
|
512
512
|
const ARGUMENTS_FIX = "Check each argument against the tool's inputSchema and call again.";
|
|
513
|
+
/** And one whose gate does not fit: there is no changing a gate once the channel is open. */
|
|
514
|
+
const GATE_FIX = "Send a gate with require, advise or both, as in {\"require\": {\"pow\": {\"bits\": 20}, \"per_key\": 5}}, or leave gate out to take writes from anyone on the allowlist.";
|
|
513
515
|
|
|
514
516
|
/**
|
|
515
517
|
* Arguments as the tool's schema says: an object of known names, each of its
|
|
@@ -519,22 +521,53 @@ const ARGUMENTS_FIX = "Check each argument against the tool's inputSchema and ca
|
|
|
519
521
|
*/
|
|
520
522
|
function wrongArguments(tool, args) {
|
|
521
523
|
if (args === undefined) args = {};
|
|
522
|
-
if (!object(args)) return "arguments must be an object";
|
|
523
524
|
const properties = tool.inputSchema.properties;
|
|
524
525
|
for (const [name, value] of Object.entries(args)) {
|
|
525
|
-
|
|
526
|
-
if (!
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
: want.type === "boolean" ? typeof value === "boolean"
|
|
530
|
-
: want.type === "object" ? object(value)
|
|
531
|
-
: want.type === "array" ? Array.isArray(value) && value.every((item) => typeof item === "string") && value.length <= (want.maxItems ?? Infinity) : false;
|
|
532
|
-
if (!ok) return `${name} is not what ${tool.name} takes: ${JSON.stringify(want)}`;
|
|
526
|
+
// Own names only: constructor or __proto__ is no argument of any tool.
|
|
527
|
+
if (!Object.hasOwn(properties, name)) return { path: name, message: `${tool.name} takes no argument called ${name}` };
|
|
528
|
+
const wrong = wrongValue(tool, name, properties[name], value);
|
|
529
|
+
if (wrong) return wrong;
|
|
533
530
|
}
|
|
534
|
-
for (const name of tool.inputSchema.required ?? []) if (!(name
|
|
531
|
+
for (const name of tool.inputSchema.required ?? []) if (!Object.hasOwn(args, name)) return { path: name, message: `${tool.name} needs ${name}` };
|
|
535
532
|
return null;
|
|
536
533
|
}
|
|
537
534
|
|
|
535
|
+
/**
|
|
536
|
+
* What is wrong with one value against its schema, named by its path, as the
|
|
537
|
+
* three runtimes say it; or null. An object whose fields the schema declares,
|
|
538
|
+
* as a gate's, is checked field by field: a pow of "5" bits went on to the
|
|
539
|
+
* service, and a field no gate has was passed over (a check of 13 November
|
|
540
|
+
* 2026). One that declares none, as data, takes any field.
|
|
541
|
+
*/
|
|
542
|
+
function wrongValue(tool, path, want, value) {
|
|
543
|
+
const ok = want.type === "string" ? typeof value === "string" && (!want.pattern || new RegExp(want.pattern).test(value)) && [...value].length >= (want.minLength ?? 0) && [...value].length <= (want.maxLength ?? Infinity) && (!want.enum || want.enum.includes(value))
|
|
544
|
+
: want.type === "integer" ? Number.isInteger(value) && value >= (want.minimum ?? -Infinity) && value <= (want.maximum ?? Infinity)
|
|
545
|
+
: want.type === "boolean" ? typeof value === "boolean"
|
|
546
|
+
: want.type === "object" ? object(value)
|
|
547
|
+
: want.type === "array" ? Array.isArray(value) && value.every((item) => typeof item === "string") && value.length <= (want.maxItems ?? Infinity) : false;
|
|
548
|
+
if (!ok) return { path, message: `${path} is not what ${tool.name} takes: ${JSON.stringify(want)}` };
|
|
549
|
+
if (want.type === "object" && object(want.properties)) {
|
|
550
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
551
|
+
if (!Object.hasOwn(want.properties, key)) {
|
|
552
|
+
if (want.additionalProperties === false) return { path: `${path}.${key}`, message: `${path} has no field called ${key}` };
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const wrong = wrongValue(tool, `${path}.${key}`, want.properties[key], inner);
|
|
556
|
+
if (wrong) return wrong;
|
|
557
|
+
}
|
|
558
|
+
for (const key of want.required ?? []) if (!Object.hasOwn(value, key)) return { path: `${path}.${key}`, message: `${path} needs ${key}` };
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** What does not fit, as the call's answer: inside a gate it is the gate's refusal, with its fix; anything else is invalid_arguments. */
|
|
564
|
+
function unfit(tool, wrong, args) {
|
|
565
|
+
if (tool.name === "aamio_open_channel" && (wrong.path === "gate" || wrong.path.startsWith("gate."))) {
|
|
566
|
+
return { error: wrong.message, error_code: "invalid_gate", fix: GATE_FIX, given: args.gate };
|
|
567
|
+
}
|
|
568
|
+
return { error: wrong.message, error_code: "invalid_arguments", fix: ARGUMENTS_FIX };
|
|
569
|
+
}
|
|
570
|
+
|
|
538
571
|
/**
|
|
539
572
|
* One tool of an add-on enabled in this home. The core settles what is the
|
|
540
573
|
* core's, in turn with everything else that uses the home: the key behind a
|
|
@@ -587,7 +620,7 @@ async function callRuntimeTool(id, params, session, version, reply, complete) {
|
|
|
587
620
|
// of 13 November 2026).
|
|
588
621
|
if (params.arguments !== undefined && !object(params.arguments)) return rpcError(id, -32602, "arguments must be an object");
|
|
589
622
|
const wrong = wrongArguments(tool, params.arguments);
|
|
590
|
-
if (wrong) return complete(toolResult(
|
|
623
|
+
if (wrong) return complete(toolResult(unfit(tool, wrong, params.arguments ?? {}), true));
|
|
591
624
|
const args = params.arguments ?? {};
|
|
592
625
|
try {
|
|
593
626
|
const data = await runtime.serial(async () => {
|
|
@@ -611,7 +644,7 @@ async function callRuntimeTool(id, params, session, version, reply, complete) {
|
|
|
611
644
|
try {
|
|
612
645
|
checkGate(args.gate);
|
|
613
646
|
} catch (wrong) {
|
|
614
|
-
throw new ToolRefusal({ error: wrong.message, error_code: "invalid_gate", fix:
|
|
647
|
+
throw new ToolRefusal({ error: wrong.message, error_code: "invalid_gate", fix: GATE_FIX, given: args.gate });
|
|
615
648
|
}
|
|
616
649
|
}
|
|
617
650
|
return runtime.channelOpen(args.label, { ttl: args.ttl, allow: args.allow ?? null, gate: args.gate ?? null, to: args.to ?? null, note: args.note ?? null });
|