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 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. The tags are `init --tags`, else the home's own, else
35
- `AAMIO_TAGS`. Save the home if you want to keep the identity.
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.0", "--home", "/absolute/path/to/home", "serve"]`, or from a
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`. Add-on commands are planned for a later milestone. The
180
- files are those aamio-python and aamio-php keep, so a home opens in any of the
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.0",
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",
@@ -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
- const service = { host: runtime.host, reachable: health.status === 200 };
272
- if (health.status === 200 && isObject(health.body)) {
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 = `no answer from /health (${health.status}), so nothing below about the service was checked`;
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
- const want = properties[name];
526
- if (!want) return `${tool.name} takes no argument called ${name}`;
527
- 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))
528
- : want.type === "integer" ? Number.isInteger(value) && value >= (want.minimum ?? -Infinity) && value <= (want.maximum ?? Infinity)
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 in args)) return `${tool.name} needs ${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({ error: wrong, error_code: "invalid_arguments", fix: ARGUMENTS_FIX }, true));
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: "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.", given: args.gate });
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 });