@tikoci/centrs 0.1.6 → 0.1.8-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +342 -0
  2. package/GLOSSARY.txt +32 -0
  3. package/README.md +10 -6
  4. package/SECURITY.md +1 -1
  5. package/commands/AGENTS.md +2 -1
  6. package/commands/api/AGENTS.md +55 -30
  7. package/commands/api/README.md +135 -42
  8. package/commands/api/examples.md +275 -19
  9. package/commands/devices/README.md +21 -4
  10. package/commands/devices/examples.md +33 -2
  11. package/commands/execute/README.md +60 -18
  12. package/commands/execute/examples.md +224 -0
  13. package/commands/explain/README.md +42 -5
  14. package/commands/mcp/README.md +2 -1
  15. package/commands/retrieve/README.md +348 -4
  16. package/commands/retrieve/examples.md +442 -5
  17. package/commands/terminal/README.md +14 -4
  18. package/commands/terminal/examples.md +22 -3
  19. package/commands/transfer/README.md +55 -9
  20. package/commands/transfer/examples.md +106 -7
  21. package/docs/CLI.md +29 -21
  22. package/docs/CONSTITUTION.md +41 -16
  23. package/docs/MATRIX.md +38 -1
  24. package/docs/errors/README.md +3 -1
  25. package/docs/errors/cdb/create-failed.md +20 -0
  26. package/docs/errors/cdb/file-exists.md +23 -0
  27. package/docs/errors/input/invalid-query.md +36 -0
  28. package/docs/errors/input/invalid-target.md +25 -3
  29. package/docs/errors/input/local-destination.md +20 -0
  30. package/docs/errors/input/unsupported-query.md +19 -0
  31. package/docs/errors/internal/byte-string.md +13 -0
  32. package/docs/errors/routeros/invalid-value.md +3 -2
  33. package/docs/errors/settings/invalid-timeout.md +6 -0
  34. package/docs/errors/transport/cancel-unacknowledged.md +24 -0
  35. package/docs/errors/transport/stream-overflow.md +21 -0
  36. package/docs/errors/usage/removed-flag.md +20 -0
  37. package/docs/errors/validation/menu-unavailable.md +22 -0
  38. package/docs/errors/validation/not-followable.md +23 -0
  39. package/docs/errors/validation/package-missing.md +29 -0
  40. package/docs/errors/validation/unknown-path.md +16 -4
  41. package/docs/errors/wait/deadline-exceeded.md +14 -0
  42. package/docs/errors/wait/interrupted.md +10 -0
  43. package/package.json +14 -11
  44. package/src/absent-menu.ts +280 -0
  45. package/src/api-fanout.ts +22 -12
  46. package/src/api.ts +603 -194
  47. package/src/btest.ts +19 -17
  48. package/src/cli/api.ts +126 -34
  49. package/src/cli/devices.ts +16 -3
  50. package/src/cli/execute.ts +18 -5
  51. package/src/cli/retrieve.ts +216 -22
  52. package/src/cli/terminal.ts +10 -5
  53. package/src/cli/transfer.ts +23 -8
  54. package/src/core/envelope.ts +14 -0
  55. package/src/core/error-catalog.ts +70 -1
  56. package/src/core/execute-command.ts +313 -6
  57. package/src/core/inspect.ts +65 -27
  58. package/src/core/query.ts +508 -0
  59. package/src/core/routeros-errors.ts +67 -1
  60. package/src/data/winbox-cdb-write.ts +22 -8
  61. package/src/devices.ts +148 -37
  62. package/src/discover.ts +7 -8
  63. package/src/errors.ts +116 -7
  64. package/src/execute-fanout.ts +14 -9
  65. package/src/execute.ts +128 -116
  66. package/src/explain/catalog.ts +64 -16
  67. package/src/explain/menus.ts +27 -6
  68. package/src/explain.ts +6 -6
  69. package/src/index.ts +31 -1
  70. package/src/mcp/tools.ts +10 -15
  71. package/src/protocols/adapter.ts +235 -118
  72. package/src/protocols/btest-session.ts +3 -2
  73. package/src/protocols/endpoint.ts +10 -0
  74. package/src/protocols/mac-telnet-console.ts +12 -7
  75. package/src/protocols/native-api.ts +252 -73
  76. package/src/protocols/sftp.ts +44 -14
  77. package/src/protocols/ssh.ts +42 -14
  78. package/src/resolver/cdb.ts +28 -7
  79. package/src/resolver/index.ts +1 -0
  80. package/src/resolver/target.ts +84 -18
  81. package/src/retrieve-fanout.ts +51 -12
  82. package/src/retrieve-follow.ts +862 -0
  83. package/src/retrieve-sample.ts +244 -0
  84. package/src/retrieve-stream.ts +115 -0
  85. package/src/retrieve-wait.ts +286 -0
  86. package/src/retrieve.ts +741 -109
  87. package/src/settings.ts +15 -15
  88. package/src/transfer-fanout.ts +40 -15
  89. package/src/transfer.ts +234 -32
@@ -84,6 +84,18 @@ Rejected before any network call, with `usage/conflicting-flags`.
84
84
  centrs retrieve $R /system/resource --attribute uptime --all-attributes --username $U --password $P --json
85
85
  ```
86
86
 
87
+ ### 20. Comma-list --attributes on any singleton
88
+
89
+ Singletons are recognized from `/console/inspect` — their `get` takes no
90
+ `number` argument — so every singleton validates against its `get value-name`
91
+ properties, not against `print`'s flags (#377).
92
+
93
+ ```bash
94
+ centrs retrieve $R /tool/romon --attributes enabled,id --username $U --password $P --json
95
+ ```
96
+
97
+ Envelope: `data` is an object containing only `enabled` and `id`.
98
+
87
99
  ## --list-attributes
88
100
 
89
101
  ### 9. List attributes for a path
@@ -127,6 +139,9 @@ centrs retrieve $R /system/resource --attribute bogus --username $U --password $
127
139
  centrs retrieve $R /system/resource --username wrong --password wrong --json
128
140
  ```
129
141
 
142
+ `meta.validation` has no `result`. Its one stage is `device: skipped` with
143
+ `reason: "not reached: transport/auth-failed"` (GH#435).
144
+
130
145
  ### 13. Unreachable host
131
146
 
132
147
  `transport/connection-refused` (not the more general `transport/network`).
@@ -135,6 +150,10 @@ centrs retrieve $R /system/resource --username wrong --password wrong --json
135
150
  centrs retrieve 127.0.0.1:1 /system/resource --username $U --password $P --json
136
151
  ```
137
152
 
153
+ The envelope still carries the resolved endpoint: `meta.via="rest-api"`,
154
+ `meta.target.host`/`port`/`baseUrl`, and `meta.settings` with the source of
155
+ each setting (#433).
156
+
138
157
  ### 14. REST timeout ceiling
139
158
 
140
159
  `--timeout 70000` against `--via rest-api` is rejected up front with
@@ -162,6 +181,41 @@ Call succeeds with a `cdb/password-not-needed` warning in `meta.warnings`.
162
181
  centrs retrieve $R /system/resource --cdb-file $CDB --cdb-password ignored --json
163
182
  ```
164
183
 
184
+ ### 16b. A refused transport still names what resolution found (#433)
185
+
186
+ `$CLOSED` is a loopback port with nothing listening.
187
+
188
+ ```bash
189
+ centrs retrieve $R /system/resource --cdb-file $CDB --port $CLOSED --json
190
+ ```
191
+
192
+ Exit 1, `transport/connection-refused`. `meta.via="rest-api"`, and
193
+ `meta.target` has the CDB record's host, `port=$CLOSED`, `recordIndex=0` and
194
+ `source.kind="cdb"`. `meta.settings.port={kind:"cli",key:"port"}` and
195
+ `meta.settings.username`/`password` have `kind:"cdb"`.
196
+ `meta.operation.auth.passwordProvided=true`. The password itself appears
197
+ nowhere. `meta.validation` has no `result`; its device stage is `skipped` with
198
+ `reason: "not reached: transport/connection-refused"` (GH#435).
199
+
200
+ ### 16c. The gate passed, then a later step failed (GH#435)
201
+
202
+ ```bash
203
+ centrs retrieve $R /system/resource --max-results 10 --username $U --password $P --json
204
+ ```
205
+
206
+ Exit 1, `input/max-results-exceeded`. The read was validated and ran, and the
207
+ answer was larger than the byte budget. `meta.validation.result="passed"`,
208
+ because the gate did pass.
209
+
210
+ ### H1. `--host` stands in for `<target>` (#428)
211
+
212
+ ```bash
213
+ centrs retrieve --host $R /system/identity --username $U --password $P --json
214
+ ```
215
+
216
+ `ok: true`, with `data.name` the router's identity. The lone positional is the
217
+ path, and there is no CDB lookup.
218
+
165
219
  ## Target selection (fan-out)
166
220
 
167
221
  These exercise the shared target-selection grammar
@@ -233,11 +287,13 @@ outputs round-trip to the same JS value.
233
287
  centrs retrieve $R /system/resource --format yaml --username $U --password $P
234
288
  ```
235
289
 
236
- ## Out-of-scope (must surface as not-implemented)
290
+ ## Row filters
237
291
 
238
- ### 18. --query
292
+ ### 18. --query with a regex
239
293
 
240
- Returns `validation/not-implemented` immediately.
294
+ A regex has no RouterOS API query word, so it fails
295
+ `input/unsupported-query` before anything is read. The full `--query`
296
+ contract is in the **Query** section below.
241
297
 
242
298
  ```bash
243
299
  centrs retrieve $R /ip/address --query 'address~"192"' --username $U --password $P --json
@@ -245,10 +301,10 @@ centrs retrieve $R /ip/address --query 'address~"192"' --username $U --password
245
301
 
246
302
  ### 19. --filter
247
303
 
248
- Same handling as `--query`.
304
+ `--filter` is `--query`: the router filters, and every returned row matches.
249
305
 
250
306
  ```bash
251
- centrs retrieve $R /ip/address --filter 'disabled=no' --username $U --password $P --json
307
+ centrs retrieve $R /interface --filter 'type=ether' --username $U --password $P --json
252
308
  ```
253
309
 
254
310
  ## native-api (`--via native-api`)
@@ -336,6 +392,14 @@ centrs retrieve $A /system/resource --attribute bogus-attr --via native-api --po
336
392
  centrs retrieve $A /system/resource --via native-api --port $API_PORT --username wrong --password wrong
337
393
  ```
338
394
 
395
+ ### N12. Singleton `--attributes` projection (#377)
396
+
397
+ ```bash
398
+ centrs retrieve $A /tool/romon --attributes enabled,id --via native-api --port $API_PORT --username $U --password $P
399
+ ```
400
+
401
+ `data` is an object containing only `enabled` and `id`.
402
+
339
403
  ## quickchr targets (#134)
340
404
 
341
405
  `$NAME` is the machine name of a running quickchr-managed CHR
@@ -351,3 +415,376 @@ centrs retrieve --quickchr $NAME /system/resource --json
351
415
  Envelope: `ok: true`, `meta.via=rest-api` (normal default; quickchr does not
352
416
  change protocol selection), `meta.target.source.kind=provider`,
353
417
  `meta.target.identity=$NAME`, no `recordIndex` (the CDB was never consulted).
418
+
419
+ ## Follow (`--follow`, #396)
420
+
421
+ Contract: [README → Follow](README.md#follow---follow). `$A`/`$API_PORT` as in
422
+ the native-api section; `--via` is left unset because `--follow` infers
423
+ native-api. Covered by `test/integration/retrieve-follow.test.ts`; the engine
424
+ orderings a CHR cannot produce on demand (A1 replay, the sweep's
425
+ changed-since-sent exclusion, overflow) are pinned in
426
+ `test/unit/retrieve-follow.test.ts`.
427
+
428
+ ### FL1. Bootstrap: snapshot frames, one `synced`, a summary
429
+
430
+ ```bash
431
+ centrs retrieve $A /ip/address --follow --duration 3s --port $API_PORT --username $U --password $P --json
432
+ ```
433
+
434
+ Every line parses as an envelope. Frames before `synced` have
435
+ `phase: "snapshot"` and `source: "print"`; exactly one `synced`; the last line
436
+ is the summary with `stopReason: "duration-elapsed"`, `synced: true`,
437
+ `changes: 0`. Every frame carries `meta.operation.stream.id`.
438
+
439
+ ### FL2. A live change, and `--count` that ignores the snapshot
440
+
441
+ ```bash
442
+ centrs retrieve $A /ip/address --follow --count 1 --duration 15s --port $API_PORT --username $U --password $P --json
443
+ ```
444
+
445
+ After `synced`, add an address. The follow ends with `stopReason:
446
+ "count-reached"` after exactly one `live` `upsert` frame from `listen`, however
447
+ many snapshot frames came first.
448
+
449
+ ### FL3. A removal RouterOS sends
450
+
451
+ ```bash
452
+ centrs retrieve $A /ip/address --follow --sweep 0 --count 1 --duration 15s --port $API_PORT --username $U --password $P --json
453
+ ```
454
+
455
+ After `synced`, remove an address that existed before the follow. One `live`
456
+ `removed` frame, `source: "listen"`, `data: null`. `--sweep 0` keeps the sweep
457
+ out of the race, and its `tip/follow-sweep-off` notice is the first line.
458
+
459
+ ### FL4. A removal RouterOS does not send (view menu → sweep)
460
+
461
+ ```bash
462
+ centrs retrieve $A /interface/bridge --follow --sweep 1s --duration 15s --port $API_PORT --username $U --password $P --json
463
+ ```
464
+
465
+ Create a bridge after `synced`, then remove it. Its `upsert` comes from
466
+ `listen`; its removal comes from the sweep (`source: "sweep"`), because
467
+ `/interface/<type>` never sends `.dead`.
468
+
469
+ ### FL5. `--attributes` projects `data`, identity stays in meta
470
+
471
+ ```bash
472
+ centrs retrieve $A /ip/address --follow --attributes address --duration 3s --port $API_PORT --username $U --password $P --json
473
+ ```
474
+
475
+ Every snapshot frame's `data` has only `address`; `meta.operation.stream.id`
476
+ still names the row.
477
+
478
+ ### FL6. A menu RouterOS cannot follow
479
+
480
+ ```bash
481
+ centrs retrieve $A /system/resource --follow --port $API_PORT --username $U --password $P --json
482
+ ```
483
+
484
+ One envelope: `ok: false`, `validation/not-followable`, exit 1. No summary.
485
+
486
+ ### FL7. A pinned REST transport
487
+
488
+ ```bash
489
+ centrs retrieve $R /ip/address --via rest-api --follow --username $U --password $P --json
490
+ ```
491
+
492
+ One envelope: `ok: false`, `transport/capability-unsupported`. No polling
493
+ fallback.
494
+
495
+ ### FL8. NDJSON is readable before the process exits; Ctrl-C ends it cleanly
496
+
497
+ ```bash
498
+ centrs retrieve $A /ip/address --follow --format ndjson --port $API_PORT --username $U --password $P
499
+ ```
500
+
501
+ Run as a subprocess. The first frame line parses while the process is still
502
+ running; after `SIGINT` the last line is a successful summary with
503
+ `stopReason: "interrupted"`, and the exit code is 0.
504
+
505
+ ### FL9. A1 and the sweep under churn (regression harness)
506
+
507
+ ```bash
508
+ centrs retrieve $A /ip/firewall/address-list --follow --sweep 200ms --duration 8s --port $API_PORT --username $U --password $P --json
509
+ ```
510
+
511
+ A second connection updates, adds and removes address-list rows while the
512
+ follow bootstraps and runs. Applying every line in order must equal a fresh
513
+ `print` taken after the churn settles: no stale value, no resurrected row, no
514
+ row removed by a sweep while it still exists. #396 round 3 measured 0 errors
515
+ over about 14.7k writes with this bootstrap; arrival-order merging failed the
516
+ same harness.
517
+
518
+ ## Sample (`--sample`)
519
+
520
+ Contract: [README → Sample](README.md#sample---sample). `$R` and `$A`/`$API_PORT`
521
+ as above. Covered by `test/integration/retrieve-sample.test.ts`; cadence, the
522
+ in-flight read at a stop, and the flag rules are pinned in
523
+ `test/unit/retrieve-sample.test.ts`.
524
+
525
+ ### SA1. Three samples, one interval apart, on either transport
526
+
527
+ ```bash
528
+ centrs retrieve $R /ip/address --sample 1s --count 3 --username $U --password $P --json
529
+ centrs retrieve $A /ip/address --via native-api --sample 1s --count 3 --port $API_PORT --username $U --password $P --json
530
+ ```
531
+
532
+ Each run prints three `sample` lines (`index` 1–3, `data` a row array) and a
533
+ summary with `stopReason: "count-reached"`, `samples: 3`. Consecutive `at`
534
+ values are at least one second apart. `meta.via` is the transport used.
535
+
536
+ ### SA2. Counters `listen` never reports
537
+
538
+ ```bash
539
+ centrs retrieve $A /interface --via native-api --attributes name,rx-byte --sample 1s --count 3 --port $API_PORT --username $U --password $P --json
540
+ ```
541
+
542
+ Every row has only `name` and `rx-byte`. `ether1`'s `rx-byte` in the third
543
+ sample is higher than in the first, since the test's own API traffic arrives
544
+ on `ether1`.
545
+
546
+ ### SA3. A singleton with one `--attribute`
547
+
548
+ ```bash
549
+ centrs retrieve $R /system/resource --attribute uptime --sample 1s --count 2 --username $U --password $P --json
550
+ ```
551
+
552
+ Each sample's `data` is the bare `uptime` string, and the two differ.
553
+
554
+ ### SA4. A removal is absence in the next sample
555
+
556
+ ```bash
557
+ centrs retrieve $A /ip/firewall/address-list --via native-api --sample 500ms --count 2 --port $API_PORT --username $U --password $P --json
558
+ ```
559
+
560
+ Add an address-list row before the run and remove it after the first sample.
561
+ The first sample's rows include its `.id`; the second sample's do not.
562
+
563
+ ### SA5. `--follow` and `--sample` are exclusive
564
+
565
+ ```bash
566
+ centrs retrieve $R /ip/address --sample 1s --follow --username $U --password $P --json
567
+ ```
568
+
569
+ One envelope: `ok: false`, `usage/conflicting-flags`, exit 1. No summary.
570
+
571
+ ### SA6. NDJSON is readable before the process exits; Ctrl-C ends it cleanly
572
+
573
+ ```bash
574
+ centrs retrieve $A /system/resource --via native-api --sample 1s --format ndjson --port $API_PORT --username $U --password $P
575
+ ```
576
+
577
+ Run as a subprocess. The first `sample` line parses while the process is still
578
+ running; after `SIGINT` the last line is a successful summary with
579
+ `stopReason: "interrupted"`, and the exit code is 0.
580
+
581
+ ## Query (`--query`, #397)
582
+
583
+ Contract: [README → Query](README.md#query---query). `$R` and `$A`/`$API_PORT`
584
+ as above. Covered by `test/integration/retrieve-query.test.ts`, which seeds
585
+ address-list, VLAN and firewall-filter rows; the grammar and compiled words are
586
+ pinned in `test/unit/query.test.ts`.
587
+
588
+ ### QY1. The same rows as RouterOS's `find where`
589
+
590
+ ```bash
591
+ centrs retrieve $R /ip/firewall/address-list --query 'comment and !disabled' --username $U --password $P --json
592
+ centrs retrieve $A /interface/vlan --via native-api --query 'vlan-id>10 and vlan-id<100 or disabled' --port $API_PORT --username $U --password $P --json
593
+ ```
594
+
595
+ For each of 58 expressions across three menus (equality, `!=`, numeric
596
+ ordering, ordering on a property some rows lack, quoted values and their escapes, bare booleans, bare "is set" names, `and`/`or`
597
+ precedence, parentheses, `!(…)`), the `.id`s returned over both transports
598
+ equal the ids `:put [<menu> find where <expression>]` prints on the same
599
+ router.
600
+
601
+ ### QY2. Where the query and the CLI differ, as documented
602
+
603
+ ```bash
604
+ centrs retrieve $R /ip/firewall/filter --query 'dst-port=80' --username $U --password $P --json
605
+ centrs retrieve $R /ip/firewall/address-list --query 'list=qa-397 and address>192.0.2.9' --username $U --password $P --json
606
+ ```
607
+
608
+ The first returns the rule whose `dst-port` is `"80"`, although
609
+ `find where dst-port=80` selects nothing on CHR 7.23.7/7.24.5 (the CLI reads
610
+ `80` as a number). On CHR 7.25beta3, 7.25beta5, 7.25rc1 and 7.26beta1 the CLI
611
+ also selects that rule. The test covers the same split for `protocol=tcp`
612
+ and checks API query results over both transports. The second compares
613
+ `address` as text, so `192.0.2.10` is not above `192.0.2.9`.
614
+
615
+ ### QY3. `--filter` and repeated `--query` are AND-ed
616
+
617
+ ```bash
618
+ centrs retrieve $R /ip/firewall/address-list --filter list=qa-397 --query comment --query '!disabled' --attributes address --username $U --password $P --json
619
+ ```
620
+
621
+ `data` is exactly `[{ "address": "192.0.2.9" }]`.
622
+
623
+ ### QY4. A regex fails before anything is sent
624
+
625
+ ```bash
626
+ centrs retrieve $R /interface --query 'name~"^ether"' --username $U --password $P --json
627
+ ```
628
+
629
+ `ok: false`, `input/unsupported-query`, exit 1.
630
+
631
+ ### QY5. A misspelled property fails validation
632
+
633
+ ```bash
634
+ centrs retrieve $A /ip/firewall/address-list --via native-api --query 'lists=qa-397' --port $API_PORT --username $U --password $P --json
635
+ ```
636
+
637
+ `validation/unknown-attribute`. Without the check RouterOS would answer with
638
+ no rows and no error.
639
+
640
+ ### QY6. A singleton has no rows to filter
641
+
642
+ ```bash
643
+ centrs retrieve $R /system/identity --query 'name=MikroTik' --username $U --password $P --json
644
+ ```
645
+
646
+ `usage/conflicting-flags`.
647
+
648
+ ### QY7. `--sample` reads the filtered rows each time
649
+
650
+ ```bash
651
+ centrs retrieve $R /ip/firewall/address-list --query 'list=qa-397 and disabled' --sample 500ms --count 2 --username $U --password $P --json
652
+ ```
653
+
654
+ Both samples hold only the disabled `qa-397` row.
655
+
656
+ ## Filtered follow
657
+
658
+ Covered by `test/integration/retrieve-filtered-follow.test.ts` (FQ1–FQ5).
659
+ The harness seeds address-list rows and changes them while following, with
660
+ validation enabled. `$A`/`$API_PORT` are the native-API target as above.
661
+
662
+ ### FQ1. Projection and both membership directions
663
+
664
+ ```bash
665
+ centrs retrieve $A /ip/firewall/address-list --via native-api --port $API_PORT --username $U --password $P --follow --query 'list=fq1' --query 'comment=a and !disabled' --attributes address --count 2 --duration 10s --json
666
+ ```
667
+
668
+ Initially only the matching row appears. Changing its comment out of the
669
+ predicate emits a `removed` frame with `source: "membership"`; changing an
670
+ excluded row into it emits an `upsert` with the same source. `data` contains
671
+ only `address`, while identity remains in meta.
672
+
673
+ ### FQ2. Minimal deletion frames
674
+
675
+ ```bash
676
+ centrs retrieve $A /ip/firewall/address-list --via native-api --port $API_PORT --username $U --password $P --follow --query 'list=fq1 and comment=a' --sweep 0 --count 1 --duration 10s --json
677
+ ```
678
+
679
+ Deleting an excluded row emits nothing; deleting the held row emits one
680
+ `removed` with `source: "listen"`. Predicate fields are absent from `.dead`.
681
+
682
+ ### FQ3. Silent predicate exit
683
+
684
+ ```bash
685
+ centrs retrieve $A /ip/firewall/address-list --via native-api --port $API_PORT --username $U --password $P --follow --query 'list=fq3 and timeout>8s' --sweep 200ms --count 1 --duration 6s --json
686
+ ```
687
+
688
+ The harness adds a row with a 10-second timeout. A sweep removes it from the
689
+ selected set as its remaining timeout crosses 8 seconds, with
690
+ `source: "membership"`; the row still exists in an unfiltered read.
691
+
692
+ ### FQ4. CLI alias and predicate validation
693
+
694
+ ```bash
695
+ centrs retrieve $A /ip/firewall/address-list --port $API_PORT --username $U --password $P --follow --filter list=fq3 --attributes address --duration 500ms --json
696
+ centrs retrieve $A /ip/firewall/address-list --port $API_PORT --username $U --password $P --follow --query lits=fq3 --json
697
+ ```
698
+
699
+ The first emits projected snapshot rows and `synced`. The typo in the second
700
+ fails `validation/unknown-attribute` before listen starts.
701
+
702
+ ### FQ5. Filtered bootstrap and live churn
703
+
704
+ ```bash
705
+ centrs retrieve $A /ip/firewall/address-list --port $API_PORT --username $U --password $P --follow --query 'list=fq5 and comment=in' --attributes address --sweep 200ms --duration 6s --json
706
+ ```
707
+
708
+ The harness changes membership during bootstrap and live operation, then
709
+ deletes rows. Applying all frames yields exactly the ids and projected rows
710
+ of a fresh filtered print after churn settles. Deterministic delayed-reply,
711
+ delete, cancellation and sustained-write bootstrap orderings are anchored in
712
+ `retrieve-follow.test.ts`; newer versions cannot starve `synced`, sweeps or
713
+ live `--count`.
714
+
715
+ ## Readiness and conditions
716
+
717
+ Covered by `test/integration/retrieve-wait.test.ts` (WT1–WT7) on disposable
718
+ CHR, with validation enabled over REST and native-api. `$R` and
719
+ `$A`/`$API_PORT` are the targets as above.
720
+
721
+ ### WT1. Readiness and already-true any-match
722
+
723
+ ```bash
724
+ centrs retrieve $R /system/resource --wait 5s --username $U --password $P --json
725
+ centrs retrieve $A /interface --via native-api --port $API_PORT --query name=ether1 --until '!disabled and mtu>=1500' --attributes name --wait 5s --username $U --password $P --json
726
+ ```
727
+
728
+ Each succeeds with one attempt and one observation. The second returns only
729
+ `[{ "name": "ether1" }]`, with `stopReason: "condition-met"`.
730
+
731
+ ### WT2. Wait for a row to match
732
+
733
+ ```bash
734
+ centrs retrieve $R /ip/firewall/address-list --query list=wait-364 --until comment=ready --attributes address --wait 10s --sample 100ms --username $U --password $P --json
735
+ ```
736
+
737
+ The harness adds a matching row after the wait starts. The waiter returns its
738
+ projected address, with more than one completed observation. The same test
739
+ runs over native-api.
740
+
741
+ ### WT3. Drop to zero and already empty
742
+
743
+ ```bash
744
+ centrs retrieve $R /ip/firewall/address-list --query list=wait-364 --until-empty --wait 10s --sample 100ms --username $U --password $P --json
745
+ ```
746
+
747
+ The harness removes the selected row after the wait starts; `data: []`,
748
+ `stopReason: "condition-met"`. Repeating the wait on the now-empty selection
749
+ succeeds on its first completed observation.
750
+
751
+ ### WT4. Condition expiry and fail-fast faults
752
+
753
+ ```bash
754
+ centrs retrieve $R /interface --until name=missing --wait 500ms --sample 100ms --username $U --password $P --json
755
+ centrs retrieve $R /interface --until unknown-property=yes --wait 5s --username $U --password $P --json
756
+ ```
757
+
758
+ The first fails `wait/deadline-exceeded`, `stopReason: "deadline-elapsed"`.
759
+ The second fails `validation/unknown-attribute` in one attempt. Incorrect
760
+ credentials also fail in one attempt; singleton `--until-empty` is refused.
761
+
762
+ ### WT5. Real reboot readiness
763
+
764
+ ```bash
765
+ centrs retrieve $R /system/resource --wait 60s --timeout 2s --sample 500ms --username $U --password $P --json
766
+ ```
767
+
768
+ The harness reboots the CHR and starts this call while its services are down.
769
+ The waiter succeeds with multiple attempts and exactly one completed data
770
+ observation, without an external retry loop.
771
+
772
+ ### WT6. OSPF reaches Full
773
+
774
+ ```bash
775
+ centrs retrieve $A /routing/ospf/neighbor --via native-api --port $API_PORT --until state=Full --wait 40s --sample 200ms --username $U --password $P --json
776
+ ```
777
+
778
+ Two disposable CHRs share a point-to-point Ethernet link. After configuring
779
+ OSPF, the waiter returns a neighbor whose state is `Full`. A REST wait on
780
+ the established adjacency succeeds on its initial observation.
781
+
782
+ ### WT7. CLI failure is one final envelope
783
+
784
+ ```bash
785
+ centrs retrieve $R /interface --until name=missing --wait 500ms --username $U --password $P --json
786
+ ```
787
+
788
+ One envelope goes to stdout, with `stopReason: "deadline-elapsed"`; exit 1.
789
+ No intermediate sample frames are printed. Unit anchors cover slow validation,
790
+ unfinished reads, cancellation and narrow retry classification.
@@ -6,10 +6,13 @@ Open an interactive RouterOS console.
6
6
  flag (`--group` / `--where` / `--all` / `--default` / `--concurrency`) is rejected
7
7
  with `usage/fanout-not-supported` — open a console to one router at a time; use
8
8
  `execute` / `api` for multi-target commands. See `docs/CONSTITUTION.md`
9
- (Target selection grammar).
9
+ (Target selection grammar). With no `<router>`, the `--host` **flag** is the
10
+ target (`terminal --host 192.0.2.10` opens an ssh console; no CDB lookup).
11
+ `CENTRS_HOST` never stands in for `<router>`. mac-telnet still needs a MAC
12
+ `<router>`, since its `--host` is only the UDP delivery endpoint.
10
13
 
11
14
  Status: `terminal / mac-telnet` **and** `terminal / ssh` are both **`CHR-passed`**
12
- (see `docs/MATRIX.md` and `examples.md` T1–T3 / TS1–TS2). The two transports take
15
+ (see `docs/MATRIX.md` and `examples.md` T1–T3 / TS1–TS2, TS4). The two transports take
13
16
  different paths: **mac-telnet** is the in-process raw passthrough over
14
17
  `MacTelnetConsole.attachInteractive`; **ssh** execs the host `ssh` with inherited
15
18
  stdio (RouterOS grants no PTY, but `ssh user@host` opens the console and the OS
@@ -73,6 +76,12 @@ CLI/API. When no key is set, terminal delegates identity selection to system
73
76
  `ssh` config and the SSH agent; `--ssh-key` is an explicit override and must not
74
77
  silently merge with a conflicting CDB/env key.
75
78
 
79
+ A selected key from any settings source adds `IdentitiesOnly=yes` alongside
80
+ `-i`, preventing unrelated agent identities from authenticating instead. The
81
+ agent can still sign for the selected key. OpenSSH continues to honor explicit
82
+ `IdentityFile` entries in its config; this does not isolate the user's SSH
83
+ configuration. The same selection applies to execute and SFTP.
84
+
76
85
  Unlike the batch sftp/execute clients, the **interactive** `terminal / ssh` argv
77
86
  omits `BatchMode=yes` (`interactive: true` in `sshCommonOptions`), so the host
78
87
  `ssh` can prompt on the inherited TTY for an encrypted key's passphrase or a
@@ -86,8 +95,9 @@ longer open for the file path: **host-key verification** rides the unified
86
95
  a changed key → `transport/host-key-mismatch`; `--insecure` →
87
96
  `StrictHostKeyChecking=no` — see `docs/CONSTITUTION.md`, Transport trust);
88
97
  **agent vs explicit-key** interplay and **algorithm negotiation** are delegated to
89
- the host OpenSSH (`-i <ssh-key>` when set, else the agent / `~/.ssh/config`). The
90
- MAC-on-file-but-SSH-pinned case is settled (see *MAC target over SSH* above):
98
+ the host OpenSSH (`-i <ssh-key> -o IdentitiesOnly=yes` when set, else the agent /
99
+ `~/.ssh/config`). The MAC-on-file-but-SSH-pinned case is settled (see *MAC target
100
+ over SSH* above):
91
101
  centrs resolves a MAC for `--via ssh` CDB-first, then tips `--via mac-telnet` —
92
102
  it does **not** silently fall back to L2 (constitution: a pinned `--via` is never
93
103
  swapped). The no-pseudo-tty console reader is likewise resolved — a CHR spike
@@ -91,9 +91,10 @@ on the argv.
91
91
 
92
92
  Harness inputs (from `test/integration/terminal-ssh.test.ts`): `$SSH_PORT` is
93
93
  `chr.sshPort` (passed via `--port`), and `$KEY` is the ephemeral private key the
94
- test mints with `ssh-keygen -f <tmp>/id` and passes via `--ssh-key`. **TS1–TS2 are
95
- the CHR-tested set** (green via `bun run test:integration`, CHR 7.23.1) — the
96
- matrix's `CHR-passed` claim rests on them. **TS3 is the interactive hand-verified
94
+ test mints with `ssh-keygen -f <tmp>/id` and passes via `--ssh-key`. **TS1, TS2,
95
+ TS4 and TS5 are the CHR-tested set**. The matrix's `CHR-passed` claim rests on
96
+ TS1, TS2 and TS4. TS1, TS2 and TS5 run in `test/integration/terminal-ssh.test.ts`; TS4 runs in the shared
97
+ `test/integration/ssh-identity.test.ts`. **TS3 is the interactive hand-verified
97
98
  analog of T4** (a piped subprocess stdin cannot be a TTY), not in CI.
98
99
 
99
100
  ### TS1. Run a command over an SSH terminal (batch relay)
@@ -130,6 +131,24 @@ RouterOS console to the user; `/quit` (or `Ctrl-D`) ends the session. RouterOS's
130
131
  no-PTY limitation applies (e.g. multi-line brace blocks are not supported over
131
132
  SSH — see the SSH page); single-line commands work.
132
133
 
134
+ ### TS4. A rejected selected key cannot authenticate through another agent identity
135
+
136
+ The shared [SSH identity example S5](../execute/examples.md#s5-an-explicit-rejected-key-cannot-fall-back-to-an-unrelated-agent-key)
137
+ checks the real terminal relay against CHR with a private agent and prompts
138
+ disabled. The rejected key preserves OpenSSH's nonzero exit and
139
+ `Permission denied`; selecting the trusted key or leaving the key unset
140
+ returns the device identity. Covered by `test/integration/ssh-identity.test.ts`.
141
+
142
+ ### TS5. `--host` stands in for `<router>` (#428)
143
+
144
+ ```bash
145
+ printf '/system/identity/print\n/quit\n' | centrs terminal --host 127.0.0.1 --port $SSH_PORT --username $U --ssh-key $KEY --insecure
146
+ ```
147
+
148
+ There's no `<router>` and no `--via`. The host-only target selects ssh, as a
149
+ host `<router>` does, with no CDB lookup. stdout contains the device identity,
150
+ checked as in TS1.
151
+
133
152
  ## MAC target over `--via ssh` resolution (behavior note — not a numbered CHR example)
134
153
 
135
154
  This path resolves a MAC to an IP and, on the unresolved / no-opt-in branch,