asterism 0.7.1 → 0.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d729d6c233d712c647e7fb1168f2587fa0e81eb1b176a11f12ac7c349f61a592
4
- data.tar.gz: fce6e1102b0e4e2c404adf28a299c77efd548f6a07e92dc81a7b54a9efe1951e
3
+ metadata.gz: 8c3c929e3018c538fab3983c9fffce4fd997bfa8b37cad43fbaa6c0eb5890d65
4
+ data.tar.gz: b4fc43d2d5d286139eb700c5711786827aab41aa8a7628686b45270a321f1807
5
5
  SHA512:
6
- metadata.gz: c2050d9890925ad4b0910f3e8063a7d2b6e9ef1e8f68ea3346bea26c02674cce7a6b0c2ee798d2940f2171d7c763c49dd8160a44b3d3204b13ccea813936430c
7
- data.tar.gz: 862531b730d5bc6f00c673271fb4d1d8db6ecaaab11969a4be95234c09be4ba6eceed245b24102f490fda6608f946b5d30e8ce3b960255b197b95bcd5a4aa550
6
+ metadata.gz: f971a84780f4a1ce1e24554530201f6bccaba7f8de6ec03c5b2dc6742c6c370e2567af5c82e4609fd82f582d32cbe314f82bc5c60d5815b5c33e805980b14a58
7
+ data.tar.gz: c355695ad9b52f7513f791191eeae8ded62011cdc011c50d1e921f6fa1e58190f491cc3adc0b367fa0087def3cfd6cd03729a43143b19f608b3f3a5603879f3c
data/CHANGELOG.md CHANGED
@@ -6,6 +6,106 @@ the earlier ones are only in this repository. From 0.4.0 the two Zenoh
6
6
  bindings (asterism-zenoh, picoruby-asterism-zenoh) carry the same minor
7
7
  version (a patch release of one may come without the other).
8
8
 
9
+ ## 0.9.0
10
+
11
+ The command line only; the library and the wire are as in 0.8.0 (0.8.1
12
+ was not published: its changes are here).
13
+
14
+ - The command line's commands are a noun and a verb, as ros2's are:
15
+ `node list` / `node info NODE`, `app list` / `app info NODE/APP`,
16
+ `object list [PATTERN]` / `object info PATH` / `object find METHOD`
17
+ (the objects that have a method, `*` as a wildcard), `status get` /
18
+ `status echo` / `status hz NODE/APP`. The lists print the names only, a
19
+ name a line, sorted (for grep and xargs). `asterism NOUN` lists its
20
+ verbs; `asterism --help` is grouped by noun. `call`, `ping`, `watch`,
21
+ `tree` and `mcp` are as they were, and the older `nodes`, `objects` and
22
+ `echo` stay with the same output. README "The command line".
23
+ - `status echo NODE/APP`: the status, then each change as it comes (the
24
+ text tells what changed, as `watch` does; `--once`: the status now,
25
+ once). `status hz NODE/APP`: how often the status changes, as
26
+ `ros2 topic hz` (the average rate and the intervals' min, max and
27
+ standard deviation, every `--period`; `--window`).
28
+ - `-o json` / `-o yaml` on every command that reads (YAML with the
29
+ standard library, the same content as the JSON). `--json` is `-o json`;
30
+ its output is as it was, byte for byte.
31
+ - Tab completion: `asterism completion bash` / `zsh` prints a script
32
+ (`source <(asterism completion bash)`). The commands, verbs and options
33
+ complete without the network; the nodes, `node/app`, `node/app/object`
34
+ (a part at a time) and an object's methods come from the network named
35
+ on the line or in the environment, asked for 1.5 s at most and
36
+ remembered for 20 s in `$XDG_CACHE_HOME/asterism/` (names only; never a
37
+ key or a certificate). README "Tab completion".
38
+ - The command line's text is easier to read (`--json` is unchanged): a
39
+ status is shown as indented `key=value` lines instead of one line of
40
+ JSON (`tree`, `nodes`, the first status of `watch`); `tree` shows how
41
+ many methods each object has and `tree --methods` lists them, wrapped to
42
+ the width; `objects` wraps the objects' and methods' descriptions to the
43
+ width in one column, the signatures in bold; `watch` shows only what
44
+ changed in a status (`~ status games/games mahjong1.members 0→1`, keys
45
+ that came with `+`, keys that went with `-`). On a terminal the text has
46
+ colours and the terminal's width; to a pipe or a file it is plain and 80
47
+ columns wide (or `COLUMNS`); `NO_COLOR` turns the colours off. Standard
48
+ library only. README "The command line".
49
+ - The command line takes `ASTERISM_CREDENTIALS` and `ASTERISM_ROUTER`
50
+ from the environment when none of `--router`, `--credentials`, `--peer`
51
+ is given (all or nothing). README "The command line".
52
+
53
+ ## 0.8.0
54
+
55
+ Needs asterism-zenoh 0.4.1 or later, as 0.7.1. Wire protocol 1 as before:
56
+ the statuses and the methods' texts in the meta are additions that older
57
+ nodes ignore. The command line comes with the gem (`exe/asterism`); its MCP
58
+ server needs the mcp gem, which is not a dependency.
59
+
60
+ - Application statuses (protocol 1, an addition: docs/protocol.md 8).
61
+ `Asterism.status(tables: 3)` / `Asterism.status({ ... })` sets this
62
+ application's status, a small Hash that changes while it runs (String
63
+ keys, nested 4 levels at most, 1024 bytes packed at most); it is put on
64
+ `asterism/<node>/<app>/@status` when it changes (the same status again
65
+ puts nothing), at connect and after a lost connection comes back, and a
66
+ get there answers it. `Asterism.status_of(node, app)` reads another
67
+ application's (nil when it has none). `Asterism::Status.check` /
68
+ `.parse` clean what comes in. CRuby: `net.status`, `net.status_of`,
69
+ `net.on_status(pattern) { |node, app, status| }`. Both on mruby too
70
+ (`rake test:mruby`). `@status` is a verbatim chunk like `@profile`:
71
+ `asterism/**` does not reach it. The conformance samples are version 3
72
+ (status queries, `status_of`, `status_puts`).
73
+ - `Asterism.apps(pattern = "*/*")`: the applications alive now
74
+ (`"<node>/<app>"`, from their liveliness tokens).
75
+ - The command line `asterism` (exe/asterism, CRuby): `nodes`, `objects`,
76
+ `tree`, `watch` (nodes, applications and objects coming and going, and
77
+ statuses), `ping` (alias `echo`: round trips of the meta, a profile or a
78
+ call), `call` (arguments as JSON or Ruby literals, read with Ripper), with
79
+ `--router` or `--credentials` (a console's `.tar`) and `--json`.
80
+ README "The command line". `rake test:cli`.
81
+ - `asterism mcp`: an MCP server on stdio (the mcp gem, not a dependency).
82
+ Read-only tools (`asterism_nodes`, `asterism_objects`, `asterism_status`)
83
+ always; a tool per method that `--allow` names, its inputs made from the
84
+ meta's arity; the list follows the network (tools/list_changed).
85
+ - The meta tells the arguments' names and what a method does (protocol 1,
86
+ an addition: docs/protocol.md 5). `expose` takes
87
+ `methods: { "move" => { args: %w[token n square], doc: "..." } }` next to
88
+ the numbers (`{ add: 2 }`), and `doc:` for the object itself; the names
89
+ give the number of arguments checked (`arity:` says otherwise, -1 for
90
+ any). Readers ignore the fields (old nodes and new ones mix). `asterism
91
+ mcp` makes the tools' inputs from the names and their descriptions from
92
+ the docs; `objects` and `tree` show them. The conformance samples are
93
+ version 4 (the object `other` of the node under test has them).
94
+ - `on_app_join { |node, app| }` / `on_app_leave { |node, app| }`: an
95
+ application of another node came or went (its token
96
+ `asterism/<node>/<app>`), while the node may stay with others; on mruby
97
+ too. CRuby: `net.on_app_join` / `net.on_app_leave`, and `off_*`.
98
+
99
+ - `credentials:` on the boards too: `Asterism::Credentials` moved to
100
+ `mrblib/credentials.rb` and reads the console's `.tar` in plain Ruby
101
+ (ustar taken apart with `byteslice` / `getbyte`, base64 without `pack`,
102
+ `asterism.json` with or without a JSON library). `Asterism.connect` takes
103
+ `credentials:` on mruby (with picoruby-asterism-zenoh's TLS, ESP-IDF
104
+ builds); `locator` and `node:` are optional when it is given. CRuby keeps
105
+ its reading (the standard library's tar reader, the file mode warning,
106
+ the key pair check) in a module prepended to the class. `rake test:mruby`
107
+ checks the board's reading against CRuby's.
108
+
9
109
  ## 0.7.1
10
110
 
11
111
  Needs asterism-zenoh 0.4.1 or later (`Session#reconnecting?`).
data/README.md CHANGED
@@ -13,7 +13,8 @@ repository holds the pure Ruby layers, written once for every Ruby:
13
13
  | `data/icon.png` | CRuby's default profile icon (32x32 PNG; [Profiles](#profiles)) |
14
14
  | `data/msgs/` | ROS 2 message types generated from ROS 2 Jazzy's definitions (Apache-2.0; tf2_msgs BSD-3-Clause; see NOTICE) |
15
15
  | `tools/` | The type generator `asterism_msggen.rb`, the Jazzy definitions it reads, `ros2_types.rb` (refreshes them from a ROS 2 image) |
16
- | `test/` | `test/msgs` (types, CDR; CRuby only), `test_asterism.rb` (objects and ROS between CRuby sessions), `test_profile.rb` (node profiles), `test_session.rb` (`Asterism.session`, `on_join` / `on_leave`), `test_api_*.rb` (the CRuby API), `bench_poll.rb` (`rake bench:poll`) |
16
+ | `exe/asterism`, `lib/asterism/cli*` | The command line: look at a network, call an object, an MCP server ([The command line](#the-command-line); CRuby only) |
17
+ | `test/` | `test/msgs` (types, CDR; CRuby only), `test_asterism.rb` (objects and ROS between CRuby sessions), `test_profile.rb` (node profiles), `test_status.rb` (application statuses), `test_meta_apps.rb` (the meta's args / doc, on_app_join / on_app_leave), `test_cli.rb` (the command line), `test_session.rb` (`Asterism.session`, `on_join` / `on_leave`), `test_api_*.rb` (the CRuby API), `bench_poll.rb` (`rake bench:poll`) |
17
18
  | `test/protocol/` | the conformance samples of the wire protocol (`samples.json`), the driver that checks an implementation against them, and the Ruby node it checks (`rake test:protocol`, `rake test:protocol_mruby`; [docs/protocol.md](docs/protocol.md)) |
18
19
  | `host_test/` | `rake test:mruby`: the shared layer on mruby (profiles between two mruby processes; `Asterism.session`, `on_join` / `on_leave` and the time of a poll against `peer.rb`) |
19
20
  | `examples/` | CRuby: `node.rb` (objects with a board), `node_polled.rb` (the same with the portable API of the boards), `ros2_talker.rb` (topics, a timer, services with ROS 2), `ros2_rover.rb` (drive a ROS 2 robot: `/cmd_vel`, `/odom`, `/imu`), `zenoh.rb` (plain Zenoh), `mujoco/` (MuJoCo run from CRuby through Fiddle, its robot exposed as Asterism objects; see its README) |
@@ -100,7 +101,7 @@ build but not yet tested. To install from the repositories instead:
100
101
 
101
102
  ```
102
103
  (cd ../asterism-zenoh && rake gem) && rake gem
103
- gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.4.1.gem pkg/asterism-0.7.1.gem
104
+ gem install --local ../asterism-zenoh/pkg/asterism-zenoh-0.4.1.gem pkg/asterism-0.9.0.gem
104
105
  ```
105
106
 
106
107
  From the working trees, without installing:
@@ -129,6 +130,7 @@ rake # every suite
129
130
  rake test:msgs # generator, type hashes, CDR, bundled types (CRuby only; no Zenoh, no docker)
130
131
  rake test:objects # objects and Asterism::ROS between CRuby sessions over a local peer link
131
132
  rake test:api # the CRuby API (blocks, receiving threads, Enumerators)
133
+ rake test:cli # the command line (exe/asterism), its Tab completion and its MCP bridge (the mcp gem's client; skipped without it)
132
134
  ASTERISM_TEST_ROUTER=tcp/127.0.0.1:7447 rake test:objects test:api # the same through a zenohd router
133
135
  rake test:mruby # the shared layer on mruby (builds mruby; not part of `rake`)
134
136
  rake test:protocol # the wire protocol's conformance samples and driver against CRuby (docs/protocol.md)
@@ -163,7 +165,7 @@ ruby examples/node_polled.rb --router tcp/192.0.2.2:7447 # the same, polled
163
165
  ruby examples/ros2_talker.rb --router tcp/192.0.2.2:7447 [--service NAME]
164
166
 
165
167
  # ROS 2: drive a robot (made for family-mruby's MuJoCo rover,
166
- # docker-compose.mujoco.yml): 1 m forward and 90 degrees left by /odom,
168
+ # docker/asterism/docker-compose.mujoco.yml): 1 m forward and 90 degrees left by /odom,
167
169
  # compared with /ground_truth/odom; or the keyboard
168
170
  ruby examples/ros2_rover.rb --router tcp/192.0.2.2:7447 [--keys]
169
171
 
@@ -315,6 +317,13 @@ and gives zenoh the CA, the certificate and the key as
315
317
  (a Hash) is laid over these keys. A tar that the group or others may read
316
318
  is warned about, as ssh does with a key: `chmod 600 alice.tar`.
317
319
 
320
+ The boards (mruby / PicoRuby over picoruby-asterism-zenoh) take the same
321
+ file with `Asterism.connect(credentials: "/home/asterism/alice.tar", app: "game")`
322
+ when their Zenoh binding has TLS (`Asterism::Zenoh::TLS`; ESP-IDF builds).
323
+ There `Asterism::Credentials` reads the tar in plain Ruby (no Regexp, no
324
+ `pack`), the boards have no file modes to warn about, and the key pair is
325
+ left for the router to check; `config:` takes the TLS keys only.
326
+
318
327
  The router checks the certificate against its CA, and its ACL can tell the
319
328
  nodes apart by the certificate's common name. The name in the locator must
320
329
  be one the router's certificate is valid for (zenoh checks it; it is
@@ -401,6 +410,217 @@ What may be shared between threads:
401
410
  | The boards | one thread (the application's update loop) |
402
411
  | Ractors | not supported |
403
412
 
413
+ ## The command line
414
+
415
+ The gem has a command, `asterism`, that looks at a network from a terminal
416
+ (CRuby). It is one more node: it connects, reads the liveliness tokens,
417
+ the meta, the profiles and the statuses, and calls a method only when told
418
+ to with `call`. Its commands are a noun and a verb, as ros2's are
419
+ (`asterism node` alone lists the node's verbs, `asterism --help` all of
420
+ them):
421
+
422
+ ```
423
+ asterism node list --router tcp/192.0.2.2:7447 # the nodes, a name a line
424
+ asterism node info games # one node: profile, apps, statuses, objects
425
+ asterism app list # node/app, a line each
426
+ asterism app info games/games # its status and its objects
427
+ asterism object list 'games/games/*' # node/app/object, a line each
428
+ asterism object info games/games/othello1 # its description and its methods
429
+ asterism object find join # the objects that have join (* as a wildcard)
430
+ asterism status get games/games # the status now
431
+ asterism status echo games/games # the status, then each change (--once: now, once)
432
+ asterism status hz games/games # how often it changes
433
+ asterism call games/games/othello1 join '"Alice"'
434
+ asterism call linux/demo/screen say hello --kwargs '{color: 3}'
435
+ asterism ping games/games/othello1 # round trips (its meta; no method runs)
436
+ asterism ping games/games/othello1 --call view '"<token>"' # round trips of a call
437
+ asterism watch # nodes, apps and objects coming and going, statuses
438
+ asterism tree --credentials admin.tar # nodes > apps > objects (--methods: the methods)
439
+ asterism completion bash # Tab completion (below)
440
+ ```
441
+
442
+ The older commands stay, with the same output: `nodes` (every node with
443
+ its applications and statuses, in one listing), `objects [PATTERN]` (each
444
+ object with its description and methods) and `echo` (`ping`).
445
+
446
+ ```
447
+ $ export ASTERISM_ROUTER=tcp/192.0.2.2:7447
448
+ $ asterism node list
449
+ games
450
+ linux
451
+ $ asterism object list
452
+ games/games/mahjong1
453
+ games/games/othello1
454
+ linux/demo/screen
455
+ $ asterism app info games/games
456
+ games/games
457
+ status:
458
+ mahjong1: st=wait members=0 humans=0 cpus=0 ais=0 away=0
459
+ othello1: st=wait members=0 humans=0 cpus=0 away=0
460
+ objects:
461
+ mahjong1 18 methods
462
+ A riichi mahjong table (four seats). From a program: call describe first,
463
+ then join, sit, and look / choose in a loop.
464
+ othello1 11 methods
465
+ An othello table (two seats). join, sit, then view / move in a loop.
466
+ $ asterism object info games/games/othello1
467
+ games/games/othello1
468
+ An othello table (two seats). join, sit, then view / move in a loop.
469
+ join(name) Joins the table as name (or gets this node's
470
+ membership back). Returns {"token", ...}: every later
471
+ call brings the token. {"error" => why} when the table
472
+ is full.
473
+ mine() This node's membership ({"token", ...}), or nil when
474
+ it has not joined.
475
+ ...
476
+ $ asterism object find join
477
+ games/games/mahjong1
478
+ games/games/othello1
479
+ $ asterism status get games/games -o yaml
480
+ ---
481
+ mahjong1:
482
+ st: wait
483
+ members: 0
484
+ ...
485
+ $ asterism status echo games/games
486
+ 12:00:01.204 games/games (now)
487
+ mahjong1: st=wait members=0 humans=0 cpus=0 ais=0 away=0
488
+ othello1: st=wait members=0 humans=0 cpus=0 away=0
489
+ 12:00:05.120 mahjong1.members 0→1
490
+ 12:00:05.133 mahjong1.humans 0→1
491
+ $ asterism status hz games/games
492
+ average rate: 0.409 Hz
493
+ min: 0.013 s max: 4.876 s std dev: 2.432 s window: 2
494
+ $ asterism tree
495
+ games (cruby) "asterism-games"
496
+ games
497
+ status:
498
+ mahjong1: st=wait members=0 humans=0 cpus=0 ais=0 away=0
499
+ othello1: st=wait members=0 humans=0 cpus=0 away=0
500
+ mahjong1 18 methods
501
+ othello1 11 methods
502
+ linux (family_mruby/sim)
503
+ demo
504
+ screen 3 methods
505
+ $ asterism watch
506
+ 12:00:01.204 ~ status games/games (now)
507
+ mahjong1: st=wait members=0 humans=0 cpus=0 ais=0 away=0
508
+ othello1: st=wait members=0 humans=0 cpus=0 away=0
509
+ 12:00:04.881 + node wasm-1a2b3c
510
+ 12:00:04.881 + app wasm-1a2b3c/game
511
+ 12:00:05.120 ~ status games/games mahjong1.members 0→1
512
+ 12:00:05.133 ~ status games/games mahjong1.humans 0→1
513
+ ```
514
+
515
+ - **Where the network is.** `--router tcp/HOST:7447` (a router without TLS),
516
+ or `--credentials FILE.tar` (a console's `.tar`: mutual TLS, its router
517
+ and its name; `--router` overrides the router). `--peer LOCATOR` links
518
+ straight to a node listening there (no router). The node ID is the
519
+ certificate's name, otherwise `cli-` and 6 hex digits (`--node`); the
520
+ application `cli-` and 4 (`--app`). With none of `--router`,
521
+ `--credentials`, `--peer` on the line, `ASTERISM_CREDENTIALS` and
522
+ `ASTERISM_ROUTER` from the environment are used instead (all or nothing:
523
+ a router on the line never meets a `.tar` from the environment), so
524
+ `export ASTERISM_CREDENTIALS=~/me.tar` makes `asterism tree` enough, and
525
+ an MCP client can pass it in the server's environment.
526
+ - **What it sees** is what the router lets it see. With a console's `.tar`
527
+ that is the row's roles: a role for looking only is in the console's
528
+ docs/g1_5.md ("Looking only: a viewer role"): it reads every node, object,
529
+ profile and status, and the router refuses its calls, so `asterism call`
530
+ says "nobody answers" and nothing runs. Something the router refuses is
531
+ left out of a listing; it is not reported as gone.
532
+ - **Arguments** of `call` (and `ping --call`): each is JSON, else a Ruby
533
+ literal (nil, true, false, numbers, strings, symbols, arrays, hashes; read
534
+ with Ripper, nothing is evaluated), else the text as written. `--kwargs`
535
+ is a JSON object or a Ruby Hash.
536
+ - **Lists** (`node list`, `app list`, `object list`, `object find`) print
537
+ the names only, a name a line, sorted, so they go to `grep` and `xargs`
538
+ as they are (on a terminal the parts are coloured; to a pipe they are
539
+ plain).
540
+ - **Output**: text, or `-o json` / `-o yaml` on every command that reads
541
+ (the YAML holds what the JSON does; `--json` is `-o json`, its output as
542
+ it always was). The streams (`watch`, `status echo`, `status hz`) give a
543
+ JSON object a line, or a YAML document each (`---`), with the time. The
544
+ text is for people: on a terminal it has colours (nodes, applications,
545
+ objects, the `+` `-` `~` of watch; `NO_COLOR=1` turns them off) and is
546
+ wrapped to the terminal's width; written to a pipe or a file it is plain
547
+ and 80 columns wide (or `COLUMNS`). A status is shown as `key=value`, a
548
+ line for each level that holds Hashes (a long text or array is cut with
549
+ `…`; `-o json` has it whole). `tree` tells how many methods an object
550
+ has, `tree --methods` lists them; `object info` (and `objects`) lists
551
+ each method with its description. `watch` and `status echo` show a
552
+ status whole the first time (and when its application comes back), then
553
+ only what changed: `a.b 0→1`, `+a.c=…` (a key came), `-a.d` (a key
554
+ went). `--timeout` (2.0 s) is each get's and call's limit, `--settle`
555
+ (1.5 s) how long it waits for the tokens after connecting.
556
+ - **status hz** counts the changes of the status (a put of the same status
557
+ again is not one) and prints, every `--period` (1 s) in which a change
558
+ came, the average rate and the intervals' min, max and standard
559
+ deviation over the last `--window` (1000) intervals, as `ros2 topic hz`
560
+ does. An application puts its status when it changes, so this is how
561
+ often the application has something new to tell.
562
+ - **watch** shows the applications and objects themselves (their tokens),
563
+ not only the nodes: a node with two applications stays while one closes,
564
+ and `watch` shows that one leave.
565
+
566
+ ### Tab completion
567
+
568
+ ```
569
+ source <(asterism completion bash) # or put this line in ~/.bashrc
570
+ source <(asterism completion zsh) # zsh, after compinit (~/.zshrc)
571
+ ```
572
+
573
+ The commands, the verbs and the options complete without the network. The
574
+ names on the network complete too: the nodes, `node/app`, `node/app/object`
575
+ a part at a time (`ga<Tab>` gives `games/`, `games/games/<Tab>` the
576
+ objects), and after `call PATH` (or `ping PATH --call`) the object's
577
+ methods. On each Tab the script runs `asterism __complete`, which asks the
578
+ network named on the line (`--router`, `--credentials`, `--peer`) or in
579
+ `ASTERISM_CREDENTIALS` / `ASTERISM_ROUTER`:
580
+
581
+ - briefly: what does not answer within 1.5 s gives no candidates, and the
582
+ shell is not kept waiting;
583
+ - remembered for 20 s, a file per network in `$XDG_CACHE_HOME/asterism/`
584
+ (`~/.cache/asterism/`; the directory 0700, the files 0600), named by a
585
+ digest of where the network is. It holds the names only, never a key or
586
+ a certificate. What did not answer is not remembered;
587
+ - as one more node for a moment (a `cli-` node, or the certificate's name),
588
+ so `asterism watch` shows it come and go.
589
+
590
+ ### MCP: Asterism as tools of an AI assistant
591
+
592
+ `asterism mcp` is an MCP server on stdio (it needs the `mcp` gem, which
593
+ the asterism gem does not depend on: `gem install mcp`). It reads by
594
+ default, and calls only the methods `--allow` names:
595
+
596
+ ```
597
+ asterism mcp --router tcp/192.0.2.2:7447 --allow 'games/othello1.*'
598
+ claude mcp add asterism -- asterism mcp --credentials admin.tar --allow 'games/othello1.*'
599
+ ```
600
+
601
+ | Tool | What |
602
+ |---|---|
603
+ | `asterism_nodes` | the nodes, their profiles, applications (with their statuses) and objects |
604
+ | `asterism_objects` | the objects and their methods (meta); `callable_as` names the tool for an allowed method |
605
+ | `asterism_status` | the applications' statuses (`node` / `app` narrow it) |
606
+ | `<node>.<app>.<object>.<method>` | one per method an `--allow` pattern names, made from the meta: the arguments' names it tells are the inputs and its doc the description (with the object's doc after it); without names, arity n gives the inputs `arg1` .. `argN` (any JSON value), arity -1 an array `args`; `kwargs` is an object of keyword arguments. The answer is `{"value": ...}`, or an error with its kind |
607
+
608
+ - `--allow PATTERN` (repeatable): `[node/][app/]object.method`, `*` within
609
+ a part as a wildcard. A pattern with fewer parts matches the last parts
610
+ of the path (`games/othello1.*` is `*/games/othello1.*`); without
611
+ `.method` every method of the object.
612
+ - The call tools follow the network: when an allowed object appears or
613
+ goes, the list is made again and the client is told
614
+ (`notifications/tools/list_changed`).
615
+ - The calls are made as this node (`--node`, or the certificate's name), so
616
+ an object exposed with `caller: :from` sees who called; a game table
617
+ gives the MCP client its own seat. `--app` names this application (a
618
+ game host that follows its players by the application `game` sees an
619
+ MCP client started with `--app game` come and go).
620
+ - An object that describes its methods at `expose` (`args:` and `doc:`,
621
+ "Describing the methods" below) gives the AI assistant the names and the
622
+ texts; without them it sees only the number of arguments.
623
+
404
624
  ## License
405
625
 
406
626
  MIT (LICENSE) for Asterism's code. The ROS 2 definitions in
@@ -452,11 +672,12 @@ Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
452
672
 
453
673
  | Call | Returns | Raises / notes |
454
674
  |---|---|---|
455
- | `Asterism.connect(locator, node:, app:, mode: nil, listen: nil, config: nil, connect_timeout: nil, check_timeout: 1.5)` (CRuby also `credentials:`, see [Over TLS](#over-tls-a-router-on-the-internet)) | `Asterism` | `locator`, `mode:`, `listen:` (and `config:` / `connect_timeout:` when given, CRuby only: TLS and the rest of zenoh's configuration; the boards raise `ArgumentError`) go to `Asterism::Zenoh::Session.open` (client of a router by default; `mode: :peer` with or without `listen:` without one). `node:` is this machine's ID, `app:` the application's name (one key chunk each: no `/ * $ ? #`, not starting with `@`; else `ArgumentError`). `Disconnected` when it cannot connect; `Error` when objects of the same `<node>/<app>` are already alive (a second copy of the application), or when already connected. Waits up to `check_timeout:` seconds for that check (a router answers at once; give more over a slow relay). Returns the module itself: on a board the module is the connection (one per application). The default is plain text without authentication; see [Over TLS](#over-tls-a-router-on-the-internet). |
456
- | `Asterism.expose(name, obj, methods:, caller: nil)` | `"<node>/<app>/<name>"` | `methods:` is an Array of names, or a Hash `name => number of arguments` (checked before the call; `-1` or the Array form: any). `ArgumentError` when `obj` has no such public method. Exposing a name again replaces it. `caller: :from` passes the calling node's ID to every method as the keyword `from:` and refuses a call that brings `from:` itself ([Who called](#who-called)). |
675
+ | `Asterism.connect(locator = nil, node: nil, app:, mode: nil, listen: nil, config: nil, connect_timeout: nil, check_timeout: 1.5, credentials: nil)` (`credentials:` see [Over TLS](#over-tls-a-router-on-the-internet)) | `Asterism` | `locator`, `mode:`, `listen:` (and `config:` / `connect_timeout:` when given: TLS and the rest of zenoh's configuration on CRuby; the boards take the TLS keys of `config:` only and raise `ArgumentError` for the rest) go to `Asterism::Zenoh::Session.open` (client of a router by default; `mode: :peer` with or without `listen:` without one). `node:` is this machine's ID, `app:` the application's name (one key chunk each: no `/ * $ ? #`, not starting with `@`; else `ArgumentError`). `Disconnected` when it cannot connect; `Error` when objects of the same `<node>/<app>` are already alive (a second copy of the application), or when already connected. Waits up to `check_timeout:` seconds for that check (a router answers at once; give more over a slow relay). Returns the module itself: on a board the module is the connection (one per application). The default is plain text without authentication; see [Over TLS](#over-tls-a-router-on-the-internet). |
676
+ | `Asterism.expose(name, obj, methods:, caller: nil, doc: nil)` | `"<node>/<app>/<name>"` | `methods:` is an Array of names, or a Hash `name => number of arguments` (checked before the call; `-1` or the Array form: any) or `name => { args: [names], doc: "text" }` ([Describing the methods](#describing-the-methods)); `doc:` the object's own text. `ArgumentError` when `obj` has no such public method. Exposing a name again replaces it. `caller: :from` passes the calling node's ID to every method as the keyword `from:` and refuses a call that brings `from:` itself ([Who called](#who-called)). |
457
677
  | `Asterism.current_call` | `Asterism::CurrentCall` or nil | While an exposed method runs for a call: `from` (the calling node's ID), `verified` (`:claimed`, or `:local` for a call from this application), `object`, `method`. nil outside a call ([Who called](#who-called)). |
458
678
  | `Asterism.unexpose(name)` | true / false | |
459
679
  | `Asterism.poll` | true / false | Call from the update loop: polls Zenoh, answers the calls that came in, follows who is alive and calls the `on_join` / `on_leave` blocks. `false` once the connection is closed or lost. Cheap when nothing came (0.6.0): each queue is looked at by its count, and the list of nodes is made again only after a liveliness change. |
680
+ | `Asterism.on_app_join { \|node, app\| }` / `Asterism.on_app_leave { \|node, app\| }` | `Asterism` | An application of another node appeared (its token `asterism/<node>/<app>`) / is gone. A node may run several: each comes and goes on its own while the node stays. Told like `on_join` (from `Asterism.poll`; at connect for those there; all leave when the connection is lost); `off_app_join(proc)` / `off_app_leave(proc)`; CRuby `net.on_app_join` / `net.on_app_leave` |
460
681
  | `Asterism.on_join { \|node\| }` / `Asterism.on_leave { \|node\| }` | `Asterism` | Another node appeared (one of its applications or objects) / is gone (no application and no object left). The nodes there already join on the first polls after connecting; when the connection is lost, every node known until then leaves (on the next `Asterism.poll`). The blocks run from `Asterism.poll` only, never from the polling inside a waiting call; what they raise comes out of `Asterism.poll`. Each call adds a block; `Asterism.off_join(proc)` / `off_leave(proc)` remove one; `Asterism.close` forgets them (a lost connection does not). |
461
682
  | `Asterism[path, timeout: 2.0]` | `Proxy` | `path` is `<node>/<app>/<object>` without wildcards (`ArgumentError`). `timeout:` (seconds) or `timeout_ms:` is the time limit of its calls. Nothing is sent until a method is called. |
462
683
  | `proxy.<method>(*args, **kw)` | the remote return value | Waits for the answer (the proxy's time limit). `RemoteError` when it raised there or could not be called (`remote_kind` says which: `raised`, or not exposed `not_exposed` (`NoMethodError`), wrong number of arguments `arguments` (`ArgumentError`), no such object `no_object` (`NameError`); [Errors](#errors)), `TimeoutError` when no answer came in time (also at once when nobody answers that key), `Disconnected`, `EncodeError` (before anything is sent) for a value MessagePack cannot carry. A block cannot be sent (`ArgumentError`). |
@@ -468,12 +689,39 @@ Asterism.nodes # => ["linux", "fmruby-bbbbbb"]
468
689
  | `proxy.asterism_meta` / `proxy.asterism_path` | Hash, String | `{"methods" => [{"name" => name, "arity" => n}, ...]}` |
469
690
  | `Asterism.each(pattern = "**") { \|proxy\| }` | count (Array without a block) | The exposed objects alive now (this application's own included) whose `<node>/<app>/<object>` matches; `*` is one chunk, `**` any number. 1.0 returns the proxies from the block form instead of their number (Ruby's `each` returns its receiver); write `Asterism.each(pattern).size` for the count. |
470
691
  | `Asterism.nodes` | `[String]` | Node IDs alive now, this one first. |
692
+ | `Asterism.apps(pattern = "*/*")` | `[String]` | The applications alive now, `"<node>/<app>"` (from their liveliness tokens; this one first). |
471
693
  | `Asterism.connected?` / `node_id` / `app` / `exposed` / `lost_reason` | | |
472
694
  | `Asterism.reconnecting?` | true / false | True while the router is away and the binding opens the connection again ([When the connection is lost](#when-the-connection-is-lost)). |
473
695
  | `Asterism.session` | `Asterism::Zenoh::Session` | The connection's own session, for put / subscribe / a ROS 2 node on the same connection ([One connection](#one-connection-asterismsession)). The same object until the connection closes. `Disconnected` when not connected. Its `close` does nothing (warns once); `Asterism.close` closes it. |
474
696
  | `Asterism.close` | nil | Withdraws every exposed object. Idempotent. |
475
697
  | `Asterism.profile(kind:, name:, host:, icon:)` / `Asterism.profile` | Hash | This node's profile: sets the fields given and returns it; without arguments, returns it. See [Profiles](#profiles). |
476
698
  | `Asterism.profile_of(node, icon: true, timeout: 2.0)` | Hash or nil | Another node's profile, nil when it does not answer (an older version, a ROS 2 node). |
699
+ | `Asterism.status(hash = nil, **fields)` / `Asterism.status` | Hash or nil | This application's status: a Hash is the whole status, keywords change those fields (nil removes one); put on the network when it changes. Without arguments, returns it. See [Status](#status). |
700
+ | `Asterism.status_of(node, app, timeout: 2.0)` | Hash or nil | Another application's status, nil when it has none or does not answer. |
701
+
702
+ ## Describing the methods
703
+
704
+ The meta (what `proxy.respond_to?` reads) can carry the arguments' names
705
+ and what each method does, for the people and the tools that show them
706
+ (`asterism mcp` turns them into an AI assistant's tool inputs and
707
+ descriptions; `asterism objects` prints them). Give them at `expose`, next
708
+ to the numbers:
709
+
710
+ ```ruby
711
+ Asterism.expose("othello1", table, caller: :from, doc: "An othello table. Call view for the board.",
712
+ methods: { join: 1, # a number, as before
713
+ "move" => { args: %w[token n square],
714
+ doc: "Plays square (\"d3\") on the board version n of view." },
715
+ view: { args: [:token], doc: "The board as this member sees it." } })
716
+ ```
717
+
718
+ - The names give the number of arguments checked (`args: %w[a b]` is 2);
719
+ `arity: -1` keeps it unchecked (the names are then the leading ones).
720
+ - The texts are at most 2048 bytes, the names at most 16 of 64 bytes; the
721
+ first line of a doc is its summary. They travel in every meta reply, so
722
+ keep them short on a board.
723
+ - Old readers ignore them and old nodes do not send them
724
+ (docs/protocol.md 5); a proxy works the same either way.
477
725
 
478
726
  ## Profiles
479
727
 
@@ -528,6 +776,41 @@ Asterism.profile_of("an-older-node") # => nil
528
776
  an acceptable PNG (nil otherwise), for a tool that wants to check an icon
529
777
  first.
530
778
 
779
+ ## Status
780
+
781
+ An application tells what changes while it runs: how many tables a game
782
+ host has open and who sits at them, a battery level, a mode. The profile
783
+ says once what a node is; the status is the application's and changes.
784
+
785
+ ```ruby
786
+ Asterism.status(tables: 3, humans: 2) # sets those fields, keeps the others
787
+ Asterism.status(humans: nil) # nil removes a field
788
+ Asterism.status({ "mahjong1" => { "st" => "play", "humans" => 1 } }) # a Hash: the whole status
789
+ Asterism.status # => {"mahjong1" => {...}} (nil when none was set)
790
+ Asterism.status_of("games", "games") # another application's, or nil
791
+ ```
792
+
793
+ - **On the network.** `asterism/<node>/<app>/@status`
794
+ ([docs/protocol.md](docs/protocol.md) 8): the application answers a get
795
+ there with its status, and puts it there when it changes (setting the
796
+ same status again puts nothing), at connect and after a lost connection
797
+ comes back. An application that never set one answers nothing.
798
+ - **The value.** A Hash with String (or Symbol) keys; the values are
799
+ anything a call can carry, nested at most 4 levels below the Hash, and
800
+ the whole packs into 1024 bytes at most. `ArgumentError` otherwise.
801
+ Whoever may read `asterism/*/*/@status` reads it: counts and states, not
802
+ secrets.
803
+ - **Following.** Subscribe to `asterism/*/*/@status` and read each put with
804
+ `Asterism::Status.parse(key, payload)` (`[node, app, status]` or nil); on
805
+ CRuby, `net.on_status("*/*") { |node, app, status| }`. Only changes are
806
+ put: ask `status_of` once for an application that appeared.
807
+ - **Behind access control.** `@status` is a verbatim chunk: `asterism/**`
808
+ does not match it. A router's rules need `asterism/*/*/@status` for a
809
+ reader (query, subscription) and `asterism/<node>/*/@status` for the
810
+ application (put, queryable, reply).
811
+ - Kept across `connect` and `close`, like the profile (it belongs to the
812
+ process, or the VM on a board).
813
+
531
814
  ## One connection: `Asterism.session`
532
815
 
533
816
  An application that also puts, subscribes or runs a ROS 2 node uses the
data/exe/asterism ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # The command line of the asterism gem: look at an Asterism network, call
5
+ # an object, or serve it to an MCP client (lib/asterism/cli.rb; `asterism
6
+ # help`).
7
+ require "asterism/cli"
8
+ exit Asterism::CLI.start(ARGV)
@@ -0,0 +1,53 @@
1
+ # The --allow patterns of `asterism mcp` (lib/asterism/cli/mcp.rb): which
2
+ # methods an MCP client may call.
3
+ module Asterism
4
+ class CLI
5
+ # The --allow patterns: [node/][app/]object.method, each part a glob
6
+ # (* any text within the part). A pattern with fewer parts matches the
7
+ # last parts of the path ("games/othello1.*" is "*/games/othello1.*";
8
+ # "othello1" alone is "*/*/othello1.*": a pattern without .method
9
+ # allows every method).
10
+ class Allow
11
+ def initialize(patterns)
12
+ @list = patterns.map { parse(_1) }
13
+ end
14
+
15
+ def empty? = @list.empty?
16
+
17
+ # Whether some pattern names this object (any of its methods).
18
+ def object?(path)
19
+ parts = path.split("/")
20
+ @list.any? { |chunks, _| chunks_match?(chunks, parts) }
21
+ end
22
+
23
+ def method?(path, method)
24
+ parts = path.split("/")
25
+ @list.any? { |chunks, m| chunks_match?(chunks, parts) && glob?(m, method.to_s) }
26
+ end
27
+
28
+ private
29
+
30
+ def parse(pat)
31
+ slash = pat.rindex("/") || -1
32
+ dot = pat.index(".", slash + 1)
33
+ path = dot ? pat[0...dot] : pat
34
+ meth = dot ? pat[(dot + 1)..] : "*"
35
+ chunks = path.split("/", -1)
36
+ if chunks.empty? || chunks.size > 3 || chunks.any?(&:empty?) || meth.empty?
37
+ raise ArgumentError, "--allow #{pat.inspect}: expected [node/][app/]object.method (globs allowed)"
38
+ end
39
+ [Array.new(3 - chunks.size, "*") + chunks, meth]
40
+ end
41
+
42
+ def chunks_match?(chunks, parts)
43
+ parts.size == 3 && chunks.each_with_index.all? { |c, i| glob?(c, parts[i]) }
44
+ end
45
+
46
+ # Only * is a wildcard (any text); ? and the rest are themselves (a
47
+ # method may be called ok?).
48
+ def glob?(pat, text)
49
+ Regexp.new("\\A#{pat.split('*', -1).map { Regexp.escape(_1) }.join('.*')}\\z").match?(text)
50
+ end
51
+ end
52
+ end
53
+ end