@tikoci/centrs 0.1.7 → 0.1.8
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/CHANGELOG.md +256 -0
- package/GLOSSARY.txt +32 -0
- package/README.md +2 -2
- package/SECURITY.md +1 -1
- package/commands/AGENTS.md +2 -1
- package/commands/api/AGENTS.md +55 -30
- package/commands/api/README.md +123 -41
- package/commands/api/examples.md +205 -17
- package/commands/devices/README.md +11 -3
- package/commands/execute/README.md +31 -3
- package/commands/execute/examples.md +136 -1
- package/commands/explain/README.md +30 -4
- package/commands/mcp/README.md +5 -1
- package/commands/mcp/examples.md +18 -1
- package/commands/retrieve/README.md +348 -4
- package/commands/retrieve/examples.md +422 -5
- package/commands/terminal/README.md +14 -4
- package/commands/terminal/examples.md +22 -3
- package/commands/transfer/README.md +55 -9
- package/commands/transfer/examples.md +106 -7
- package/docs/CLI.md +28 -20
- package/docs/CONSTITUTION.md +30 -10
- package/docs/MATRIX.md +38 -1
- package/docs/errors/README.md +3 -1
- package/docs/errors/input/invalid-query.md +36 -0
- package/docs/errors/input/local-destination.md +20 -0
- package/docs/errors/input/unsupported-query.md +19 -0
- package/docs/errors/internal/byte-string.md +13 -0
- package/docs/errors/settings/invalid-timeout.md +6 -0
- package/docs/errors/transport/stream-overflow.md +21 -0
- package/docs/errors/usage/removed-flag.md +20 -0
- package/docs/errors/validation/menu-unavailable.md +22 -0
- package/docs/errors/validation/not-followable.md +23 -0
- package/docs/errors/validation/package-missing.md +29 -0
- package/docs/errors/validation/unknown-path.md +16 -4
- package/docs/errors/wait/deadline-exceeded.md +14 -0
- package/docs/errors/wait/interrupted.md +10 -0
- package/package.json +14 -11
- package/src/absent-menu.ts +280 -0
- package/src/api-fanout.ts +22 -12
- package/src/api.ts +511 -186
- package/src/btest.ts +19 -17
- package/src/cli/api.ts +126 -34
- package/src/cli/execute.ts +18 -5
- package/src/cli/retrieve.ts +223 -22
- package/src/cli/terminal.ts +10 -5
- package/src/cli/transfer.ts +23 -8
- package/src/core/envelope.ts +14 -0
- package/src/core/error-catalog.ts +56 -1
- package/src/core/inspect.ts +43 -5
- package/src/core/query.ts +508 -0
- package/src/core/routeros-errors.ts +1 -1
- package/src/devices.ts +6 -9
- package/src/discover.ts +7 -8
- package/src/errors.ts +116 -7
- package/src/execute-fanout.ts +13 -8
- package/src/execute.ts +106 -65
- package/src/explain/catalog.ts +58 -10
- package/src/explain/menus.ts +23 -2
- package/src/explain.ts +6 -6
- package/src/index.ts +29 -1
- package/src/mcp/safety.ts +43 -18
- package/src/mcp/tools.ts +14 -17
- package/src/protocols/adapter.ts +224 -118
- package/src/protocols/btest-session.ts +3 -2
- package/src/protocols/endpoint.ts +10 -0
- package/src/protocols/mac-telnet-console.ts +6 -3
- package/src/protocols/native-api.ts +223 -70
- package/src/protocols/sftp.ts +44 -14
- package/src/protocols/ssh.ts +42 -14
- package/src/resolver/cdb.ts +28 -7
- package/src/resolver/index.ts +1 -0
- package/src/resolver/target.ts +53 -7
- package/src/retrieve-fanout.ts +51 -12
- package/src/retrieve-follow.ts +862 -0
- package/src/retrieve-sample.ts +244 -0
- package/src/retrieve-stream.ts +115 -0
- package/src/retrieve-wait.ts +286 -0
- package/src/retrieve.ts +732 -108
- package/src/settings.ts +15 -15
- package/src/transfer-fanout.ts +40 -15
- package/src/transfer.ts +234 -32
|
@@ -139,6 +139,9 @@ centrs retrieve $R /system/resource --attribute bogus --username $U --password $
|
|
|
139
139
|
centrs retrieve $R /system/resource --username wrong --password wrong --json
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
+
`meta.validation` has no `result`. Its one stage is `device: skipped` with
|
|
143
|
+
`reason: "not reached: transport/auth-failed"` (GH#435).
|
|
144
|
+
|
|
142
145
|
### 13. Unreachable host
|
|
143
146
|
|
|
144
147
|
`transport/connection-refused` (not the more general `transport/network`).
|
|
@@ -147,6 +150,10 @@ centrs retrieve $R /system/resource --username wrong --password wrong --json
|
|
|
147
150
|
centrs retrieve 127.0.0.1:1 /system/resource --username $U --password $P --json
|
|
148
151
|
```
|
|
149
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
|
+
|
|
150
157
|
### 14. REST timeout ceiling
|
|
151
158
|
|
|
152
159
|
`--timeout 70000` against `--via rest-api` is rejected up front with
|
|
@@ -174,6 +181,41 @@ Call succeeds with a `cdb/password-not-needed` warning in `meta.warnings`.
|
|
|
174
181
|
centrs retrieve $R /system/resource --cdb-file $CDB --cdb-password ignored --json
|
|
175
182
|
```
|
|
176
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
|
+
|
|
177
219
|
## Target selection (fan-out)
|
|
178
220
|
|
|
179
221
|
These exercise the shared target-selection grammar
|
|
@@ -245,11 +287,13 @@ outputs round-trip to the same JS value.
|
|
|
245
287
|
centrs retrieve $R /system/resource --format yaml --username $U --password $P
|
|
246
288
|
```
|
|
247
289
|
|
|
248
|
-
##
|
|
290
|
+
## Row filters
|
|
249
291
|
|
|
250
|
-
### 18. --query
|
|
292
|
+
### 18. --query with a regex
|
|
251
293
|
|
|
252
|
-
|
|
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.
|
|
253
297
|
|
|
254
298
|
```bash
|
|
255
299
|
centrs retrieve $R /ip/address --query 'address~"192"' --username $U --password $P --json
|
|
@@ -257,10 +301,10 @@ centrs retrieve $R /ip/address --query 'address~"192"' --username $U --password
|
|
|
257
301
|
|
|
258
302
|
### 19. --filter
|
|
259
303
|
|
|
260
|
-
|
|
304
|
+
`--filter` is `--query`: the router filters, and every returned row matches.
|
|
261
305
|
|
|
262
306
|
```bash
|
|
263
|
-
centrs retrieve $R /
|
|
307
|
+
centrs retrieve $R /interface --filter 'type=ether' --username $U --password $P --json
|
|
264
308
|
```
|
|
265
309
|
|
|
266
310
|
## native-api (`--via native-api`)
|
|
@@ -371,3 +415,376 @@ centrs retrieve --quickchr $NAME /system/resource --json
|
|
|
371
415
|
Envelope: `ok: true`, `meta.via=rest-api` (normal default; quickchr does not
|
|
372
416
|
change protocol selection), `meta.target.source.kind=provider`,
|
|
373
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
|
|
90
|
-
MAC-on-file-but-SSH-pinned case is settled (see *MAC target
|
|
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
|
|
95
|
-
the CHR-tested set
|
|
96
|
-
|
|
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,
|
|
@@ -118,6 +118,15 @@ past 60 KB) rather than quietly switching to sftp. `scp`, `fetch`, and `ftp` are
|
|
|
118
118
|
explicit-only; `ftp` additionally requires `ALLOW_UNSAFE_PROTOCOLS=ftp` because
|
|
119
119
|
it is cleartext.
|
|
120
120
|
|
|
121
|
+
**File contents are bytes on `rest`/`native`.** RouterOS carries `/file`
|
|
122
|
+
contents as raw bytes on both wires — REST puts bytes 0x80–0xff unescaped in the
|
|
123
|
+
JSON string, so the body is not UTF-8 — and stores the UTF-8 encoding of every
|
|
124
|
+
JSON code point it receives (CHR 7.23.7 + 7.24.5, #404). So the `/file`
|
|
125
|
+
`get`/`read`/`set` calls run in the adapters' `binary` mode: each byte is one
|
|
126
|
+
string code unit (Node `Buffer` latin1 — not WHATWG `TextDecoder("latin1")`,
|
|
127
|
+
which is windows-1252), and a file name in those calls is sent as its UTF-8
|
|
128
|
+
bytes. Every other command stays UTF-8 text.
|
|
129
|
+
|
|
121
130
|
> Future methods documented so callers know the capability exists, behind an
|
|
122
131
|
> explicit `--via`: `/system/smb` (not enabled by default) and `rose-storage`
|
|
123
132
|
> (`rsync`/`nfs`/`nvme-over-tcp`/`iscsi`, needs `rose-storage.npk`).
|
|
@@ -158,16 +167,25 @@ Implemented flags are generated from the CLI metadata into
|
|
|
158
167
|
[`docs/CLI.md` → transfer](../../docs/CLI.md#transfer); this file does not
|
|
159
168
|
duplicate that table. Behavior notes the generated reference cannot carry:
|
|
160
169
|
|
|
161
|
-
- `--force`/`--overwrite`
|
|
162
|
-
`
|
|
163
|
-
|
|
170
|
+
- `--force`/`--overwrite` skips the existing-**device**-destination check of
|
|
171
|
+
`upload`, `mkdir` and `copy`, which by default refuse an existing target
|
|
172
|
+
with `usage/target-exists` (see *Validation*). `upload` then overwrites the
|
|
173
|
+
file; `mkdir` and `copy` still run as RouterOS's own `/file` commands.
|
|
174
|
+
`download` replaces an existing **local** file without `--force`, as
|
|
175
|
+
`curl -o` and `scp` do (#406). The local file is written only after the
|
|
176
|
+
whole remote read completes and, under the default `--verify size`, matches
|
|
177
|
+
the device's size, so a failed read or a size mismatch leaves an existing
|
|
178
|
+
file untouched. The write itself is not atomic: a local write that fails
|
|
179
|
+
part-way can leave the file truncated.
|
|
164
180
|
- `--verify` details and the sftp size caveat are in *Integrity*.
|
|
165
181
|
- `--timeout`: `rest`/`native` are per-request ≤ 60000 ms (a chunked read is
|
|
166
182
|
many short requests, each capped); `sftp` accepts longer for a single large
|
|
167
183
|
transfer.
|
|
168
184
|
- `--ssh-key` is the same `sshKey` setting as `terminal`/`execute`
|
|
169
185
|
(`CENTRS_SSH_KEY`, CDB `ssh-key=`); when unset, the ssh-agent /
|
|
170
|
-
`~/.ssh/config` is used.
|
|
186
|
+
`~/.ssh/config` is used. A selected key adds `IdentitiesOnly=yes` to exclude
|
|
187
|
+
unrelated agent keys; explicit OpenSSH `IdentityFile` entries still apply.
|
|
188
|
+
See `commands/terminal/README.md`.
|
|
171
189
|
- `--insecure` adds a `transport/insecure-trust` warning (constitution:
|
|
172
190
|
transport trust).
|
|
173
191
|
- `--quickchr` resolves host/port/auth from the live `@tikoci/quickchr`
|
|
@@ -201,6 +219,12 @@ single-target. On the top-level `upload`/`download` alias the verb is fixed, so
|
|
|
201
219
|
its positionals are paths — positional fan-out targets are not expressible there;
|
|
202
220
|
fan-out comes from a selector flag.
|
|
203
221
|
|
|
222
|
+
In the verb form, the `--host` **flag** with no positional before the verb is the
|
|
223
|
+
single target (`transfer --host 192.0.2.10 list`), with no CDB lookup.
|
|
224
|
+
`CENTRS_HOST` never stands in for `<router>`. The `upload`/`download` aliases
|
|
225
|
+
still read their first positional as the target, since their optional second
|
|
226
|
+
path makes a count ambiguous.
|
|
227
|
+
|
|
204
228
|
Two transfer-specific rules:
|
|
205
229
|
|
|
206
230
|
- **`download` fan-out requires `--out-dir <dir>`.** N devices cannot share one
|
|
@@ -261,11 +285,15 @@ success, per-target results live in `data.targets`.
|
|
|
261
285
|
## Validation
|
|
262
286
|
|
|
263
287
|
`transfer` validates what it can locally first — conflicting flags
|
|
264
|
-
(`usage/conflicting-flags`), a missing local source on `upload`, a
|
|
288
|
+
(`usage/conflicting-flags`), a missing local source on `upload`, a `download`
|
|
289
|
+
destination whose parent directory is missing or that is itself a directory
|
|
290
|
+
(`input/local-destination`; centrs does not create directories), a `--via rest`
|
|
265
291
|
upload that exceeds 60 KB (`transport/unsupported-operation`). The
|
|
266
|
-
**refuse-overwrite** guard
|
|
267
|
-
unless `--force`, centrs `stat`s the
|
|
268
|
-
for the REST family) and fails with
|
|
292
|
+
**refuse-overwrite** guard protects device files and is a real precondition
|
|
293
|
+
probe, not a local check: unless `--force`, centrs `stat`s the device
|
|
294
|
+
destination (SFTP `stat`, or `/file/print` for the REST family) and fails with
|
|
295
|
+
`usage/target-exists` if it is already there. A `download` overwrites its local
|
|
296
|
+
destination (see *Flags*).
|
|
269
297
|
This is the "validate before write" gate applied to files — see constitution:
|
|
270
298
|
validation is the product.
|
|
271
299
|
|
|
@@ -274,6 +302,20 @@ gate as in `retrieve` / `execute`; RouterOS's own menu errors (missing remote
|
|
|
274
302
|
file, permission denied) map to `routeros/*`, and SFTP/SCP/fetch errors map to
|
|
275
303
|
`transport/*` and `auth/*`.
|
|
276
304
|
|
|
305
|
+
A failure **after** target resolution keeps what resolution found: the error
|
|
306
|
+
envelope carries the resolved `meta.target` (host, port, identity, sources),
|
|
307
|
+
`meta.via`, the setting sources in `meta.settings`, and `meta.operation`
|
|
308
|
+
(including `auth.username` / `auth.passwordProvided` — provenance, never the
|
|
309
|
+
secret). This holds for single-target runs and for each fan-out member. A
|
|
310
|
+
failure before resolution (bad shape, unknown target) keeps only
|
|
311
|
+
`meta.target.input`. An sftp `auth/failed` with a resolved password adds
|
|
312
|
+
`tip/sftp-password-not-used`, plus `tip/transfer-password-transport` unless the
|
|
313
|
+
upload is known to exceed the rest/native write cap — advice only, never a
|
|
314
|
+
fallback across a pinned `--via` (examples S7–S9). Only OpenSSH's pre-login
|
|
315
|
+
refusal is `auth/failed`: a user whose group lacks `write` logs in and gets
|
|
316
|
+
`routeros/command-failed` for upload/mkdir/remove, and one lacking `ftp` gets
|
|
317
|
+
`routeros/session-closed` (SSH disconnect reason 7) — example S9.
|
|
318
|
+
|
|
277
319
|
**Error codes reuse existing namespaces — `transfer` adds no namespace of its
|
|
278
320
|
own.** Everything specific to transfer fits `usage/*` (`usage/target-exists`),
|
|
279
321
|
`transport/*` (`transport/unsupported-operation` for the REST 60 KB write cap,
|
|
@@ -342,7 +384,11 @@ done rule. The first coded pass targets `rest`/`native` (no SSH dependency);
|
|
|
342
384
|
/ [User](https://help.mikrotik.com/docs/spaces/ROS/pages/8978504/User) pages:
|
|
343
385
|
**by default** a user cannot password-auth over SSH once an SSH key is set for it
|
|
344
386
|
(`/ip/ssh password-authentication=yes-if-no-key`, the default — settable to
|
|
345
|
-
`yes` to allow both
|
|
387
|
+
`yes` to allow both; CHR-grounded on 7.23.8 and 7.24.5 by
|
|
388
|
+
`test/integration/ssh-auth-policy.test.ts`). centrs's batch sftp runs
|
|
389
|
+
`BatchMode=yes` and never sends a password at all — it authenticates with the
|
|
390
|
+
selected key or the host OpenSSH configuration/agent — so key auth is the
|
|
391
|
+
only sftp path regardless of that policy; and `strong-crypto=yes`
|
|
346
392
|
disables ssh-rsa/SHA1, so the client must offer ed25519 / rsa-sha2-256 (the host
|
|
347
393
|
OpenSSH negotiates this natively). The SSH file path is the SFTP **subsystem**
|
|
348
394
|
(a real file protocol), not exec-driven scp; on-device `copy`
|