@tikoci/centrs 0.1.9-next.23 → 0.1.9-next.27

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 (91) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/GLOSSARY.txt +23 -1
  3. package/README.md +30 -18
  4. package/commands/api/AGENTS.md +21 -3
  5. package/commands/api/README.md +18 -7
  6. package/commands/api/examples.md +1 -1
  7. package/commands/btest/README.md +9 -0
  8. package/commands/btest/examples.md +7 -0
  9. package/commands/devices/README.md +72 -3
  10. package/commands/devices/examples.md +56 -1
  11. package/commands/execute/README.md +37 -6
  12. package/commands/execute/examples.md +24 -5
  13. package/commands/explain/README.md +675 -64
  14. package/commands/explain/examples.md +199 -42
  15. package/commands/retrieve/README.md +35 -3
  16. package/commands/retrieve/examples.md +114 -1
  17. package/commands/transfer/README.md +33 -4
  18. package/commands/transfer/examples.md +46 -0
  19. package/docs/CLI.md +30 -8
  20. package/docs/CONSTITUTION.md +14 -9
  21. package/docs/MATRIX.md +37 -22
  22. package/docs/errors/cdb/encrypt-failed.md +10 -0
  23. package/docs/errors/cdb/password-required.md +5 -4
  24. package/docs/errors/cdb/plaintext-backups.md +11 -0
  25. package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
  26. package/docs/errors/input/invalid-routeros-path.md +25 -3
  27. package/docs/errors/routeros/permission-denied.md +24 -0
  28. package/docs/errors/transport/edge-auth-failed.md +22 -0
  29. package/docs/errors/transport/write-outcome-unknown.md +3 -0
  30. package/docs/errors/usage/invalid-cursor.md +20 -0
  31. package/docs/errors/validation/unknown-path.md +9 -0
  32. package/package.json +9 -5
  33. package/src/api-fanout.ts +2 -0
  34. package/src/api.ts +46 -39
  35. package/src/btest.ts +7 -0
  36. package/src/cli/api.ts +18 -1
  37. package/src/cli/btest.ts +9 -0
  38. package/src/cli/devices.ts +76 -8
  39. package/src/cli/execute.ts +9 -0
  40. package/src/cli/explain.ts +206 -47
  41. package/src/cli/retrieve.ts +9 -0
  42. package/src/cli/terminal.ts +9 -0
  43. package/src/cli/transfer.ts +10 -1
  44. package/src/core/envelope.ts +10 -3
  45. package/src/core/error-catalog.ts +28 -2
  46. package/src/core/execute-command.ts +53 -0
  47. package/src/core/fanout.ts +12 -2
  48. package/src/core/inspect.ts +4 -5
  49. package/src/core/menu-path.ts +89 -0
  50. package/src/core/routeros-errors.ts +25 -0
  51. package/src/devices.ts +154 -9
  52. package/src/execute-cmr.ts +2 -8
  53. package/src/execute-fanout.ts +2 -0
  54. package/src/execute.ts +55 -20
  55. package/src/explain/args.ts +115 -10
  56. package/src/explain/blocks.ts +2 -2
  57. package/src/explain/catalog.ts +22 -12
  58. package/src/explain/group-faults.ts +162 -29
  59. package/src/explain/operators.ts +2 -1
  60. package/src/explain/pasted-prompt.ts +198 -0
  61. package/src/explain/pathresolve.ts +2 -2
  62. package/src/explain/scope-brace.ts +226 -11
  63. package/src/explain/segment.ts +138 -3
  64. package/src/explain/symbols.ts +148 -8
  65. package/src/explain/values.ts +55 -4
  66. package/src/explain/verbsplit.ts +2 -2
  67. package/src/explain/write.ts +2 -2
  68. package/src/explain-live-facets.ts +739 -0
  69. package/src/explain-live-types.ts +490 -0
  70. package/src/explain-live.ts +593 -0
  71. package/src/explain.ts +397 -43
  72. package/src/index.ts +10 -0
  73. package/src/protocols/adapter.ts +90 -10
  74. package/src/protocols/btest-session.ts +10 -30
  75. package/src/protocols/ec-srp5.ts +19 -10
  76. package/src/protocols/mac-telnet-console.ts +2 -2
  77. package/src/protocols/mac-telnet.ts +38 -10
  78. package/src/protocols/native-api.ts +13 -12
  79. package/src/resolver/cdb.ts +58 -2
  80. package/src/resolver/index.ts +1 -0
  81. package/src/resolver/quickchr-provider.ts +5 -0
  82. package/src/resolver/selection.ts +11 -0
  83. package/src/retrieve-fanout.ts +2 -0
  84. package/src/retrieve-follow.ts +2 -1
  85. package/src/retrieve-sample.ts +2 -1
  86. package/src/retrieve-wait.ts +10 -6
  87. package/src/retrieve.ts +88 -27
  88. package/src/settings.ts +1 -0
  89. package/src/terminal.ts +7 -0
  90. package/src/transfer-fanout.ts +2 -0
  91. package/src/transfer.ts +117 -27
package/CHANGELOG.md CHANGED
@@ -17,6 +17,41 @@ part of this section.
17
17
 
18
18
  ### Added
19
19
 
20
+ - `explain` offers a non-applied repair tip for pasted console prompts
21
+ (#370, #567, #578), including `[user@host] /menu>` and HotLock `>>` forms.
22
+ The candidate preserves command separators and names the menu context;
23
+ multiline repairs render as fenced code. The original analysis stays intact.
24
+ - `--match user=<name>|target=<addr>|<record-type>` on `retrieve`, `execute`,
25
+ `api`, `transfer`, `terminal` and `btest client` picks one of several CDB
26
+ records saved for the same `<router>` (#583), as `devices show --match`
27
+ does. One address saved under two users was `identity/ambiguous` on every
28
+ command, and the error named a `--match` they rejected. The flag is
29
+ single-target: with a fan-out selection or `--quickchr` it is
30
+ `usage/conflicting-flags`, and with no CDB record in play it is
31
+ `identity/no-match` rather than ignored.
32
+ - **Live `explain` over rest-api + native-api is `CHR-passed`** (#236).
33
+ Examples 7–16, 19, 24, 48 and 52–53 pass on both transports on stable
34
+ 7.24.5 and long-term 7.23.8. `runtimeAcceptance` stays `not-proven`.
35
+ - `explain <router> '<input>'` and `explainLiveEnvelope()` add read-only parse,
36
+ highlight and command-argument evidence over REST/native API. Probe outcomes,
37
+ byte coverage and provenance remain explicit; analyzed input never executes (#491).
38
+ - Live `explain` fills the two live value facts (#225). `observedType` names a
39
+ type only where the device visibly converts a bare scripting literal
40
+ (`2.2` → `2.0.0.2` is `ip`, `0x10` → `16` is `num`), read from an escaped
41
+ `:parse` of the literal alone; an unchanged rendering abstains, and no
42
+ `:typeof` is sent. `schemaType` normalizes the argument's live `syntax`
43
+ declaration into leaf types (`gateway` → `ip`, `ip6`, `enum`), with the
44
+ device's own term in `declared`. Menu arguments are never observed, and
45
+ `op` has no producer.
46
+ - Live `explain --schema` describes the path at the cursor: a menu's verbs and
47
+ submenus with counts, a command's arguments with their declared syntax, and
48
+ `print`'s proplist. `--complete [--cursor <byte>]` returns menu, verb,
49
+ argument-name, `where` variable or value candidates. They are the device's
50
+ own input completion at the cursor, cited by `ev`; declared names and
51
+ syntax stay in `--schema`. `--full` lifts the sizing limits up to a safety
52
+ ceiling.
53
+ Only probe shapes that answer on RouterOS 7.12.2 and 7.20.8 are sent (#492).
54
+
20
55
  - `centrs --version` / `-V` prints the package version alone; `--json` (or
21
56
  `--format json|yaml`) returns an envelope with `data.version` (#463).
22
57
  - `execute --cmr` warns `cmr/caller-policy-not-enforced` when a write runs
@@ -69,9 +104,47 @@ part of this section.
69
104
  client, its controller.
70
105
  - `execute --cmr` accepts a command that opens with a relative menu change
71
106
  (`/system; identity/print`); its `:parse` return was read as incomplete.
107
+ - CDB encryption lifecycle (#558). `devices init --encrypt` creates an
108
+ encrypted empty CDB; `devices encrypt` converts an existing open CDB in place
109
+ with a fresh salt, keeping its records, unknown fields, and file mode. Both
110
+ need a non-empty `--cdb-password` / `CENTRS_CDB_PASSWORD`; a password alone
111
+ still never converts. Conversion keeps every backup and writes a plaintext
112
+ snapshot unless `--no-backup` is passed. It warns with `cdb/plaintext-backups`.
113
+ An already-encrypted file is verified and left unchanged. Library:
114
+ `initCdb({ encrypt })`, `encryptCdb({ skipBackup })`. `init` results now
115
+ include `encrypted`. `init` and `encrypt` refuse flags they would otherwise
116
+ ignore (`--user`, `--group`, `--via`, …) with `input/invalid-command`.
72
117
 
73
118
  ### Changed
74
119
 
120
+ - **Breaking:** permission and edge-auth failures get their own error codes
121
+ (#541). Three codes change for `--json` and library readers:
122
+ - A REST policy refusal (HTTP 500 `not enough permissions (9)` or `std
123
+ failure: not allowed (9)`): `transport/connection-closed` →
124
+ `routeros/permission-denied`. It is a deterministic router answer, so it
125
+ is never retried and no longer carries
126
+ `transport/write-outcome-unknown`.
127
+ - A native-API `not enough permissions (9)` trap: `routeros/api-trap` →
128
+ `routeros/permission-denied`. Retrieve's path probe and live explain's
129
+ child probe surface it instead of reading "path doesn't exist".
130
+ - A 401/403 from a layer in front of RouterOS (auth proxy, tunnel edge,
131
+ WAF): `transport/auth-failed` → `transport/edge-auth-failed`, with the
132
+ challenge scheme, server, content type and `cf-ray` presence as context
133
+ (no secrets). RouterOS's own 401 — JSON `{"error":401,...}`, measured on
134
+ CHR 7.12.1, 7.16.2, 7.23.8, 7.25, 7.26beta1 and 7.26beta3 — stays
135
+ `transport/auth-failed`.
136
+ - **`meta.validation` success shapes (#535).** This is a breaking change for
137
+ `--json` readers of the flat fields. `syntax` and `semantic` are now present
138
+ only when a gate stage gave that verdict; absent means no stage was asked.
139
+ - `api` structured requests (no `:parse`) drop `syntax: false`.
140
+ - Script input drops `semantic: "not-applicable"`; the value is gone from
141
+ the type.
142
+ - `--validate=false` drops both fields on `execute` and `api`.
143
+ - `retrieve`, `transfer` and `api` structured successes now report
144
+ `stages[]`. `transfer list` runs no existence probe, so it reports
145
+ `{ enabled: false }` instead of claiming one.
146
+ - `explain` reports `meta.validation: { enabled: false }` with no `result`
147
+ in all three cases (offline, live, error); it is not a gate.
75
148
  - Text stdout is data (#470). In text mode, single-target `execute`, `api`,
76
149
  `retrieve` and `transfer` write only the result to stdout. Everything else
77
150
  goes to **stderr** on success and on failure: the `--verbose` header, one
@@ -150,6 +223,42 @@ part of this section.
150
223
 
151
224
  ### Fixed
152
225
 
226
+ - **`retrieve` and `api` can no longer send a non-read** (#586). A `#` or
227
+ `?` in the path cut the appended `/print` off the REST URL, so
228
+ `retrieve '/system/reboot#' --no-validate --attribute uptime` posted
229
+ `/rest/system/reboot` and rebooted CHR 7.24.5 without `--yes`; `api` with a
230
+ projection did the same. Both now require every path segment to be a
231
+ RouterOS menu name (letters, digits, hyphens; case folds) on every
232
+ transport, even with `--validate=false`, failing
233
+ `input/invalid-routeros-path` before resolution. The REST adapter also
234
+ refuses any request path containing `?`, `#`, `\`, control characters or a
235
+ dot segment (including `%2e%2e`), for every command. Reported by
236
+ routeros-muse.
237
+ - `devices set <target> key=` removes the comment token instead of storing an
238
+ empty value, as documented (#590). Removing an absent token is a no-op;
239
+ `--password ""` still clears the password, and bare `password=`/`user=`
240
+ stay `cdb/reserved-key`.
241
+ - `transfer` honors a CDB record's `via=` preference (#591). A record with
242
+ `via=ssh` drove `execute` over SSH but transfer ignored it and needed
243
+ `--via sftp`. Transfer now maps the record's transport to a method
244
+ (`ssh`→`sftp`, `rest-api`→`rest`, `native-api`→`native`) after `--via` and
245
+ `CENTRS_TRANSFER_VIA`, and accepts `--via ssh` as an alias of `sftp`. A
246
+ record transport that cannot carry files fails `settings/invalid-via`
247
+ naming the record; a record-pinned REST upload over 60 KB names the record
248
+ in its `transport/unsupported-operation` remediation.
249
+ - The `explain` catalog generator skips the CLI Reference's new
250
+ `compare-versions` widget page (2026-10-30). The page carries no command
251
+ entry, and it aborted the catalog drift report. The refreshed catalog adds
252
+ 10 published paths (`/interface/wifi/easymesh/*`,
253
+ `/app/settings/remove-certificate`, three `/system/resource/hardware`
254
+ commands), and the `/cmr/client` gates drop `!powerpc`.
255
+ - `retrieve --sample`, `--follow` and `--wait` no longer report a validation
256
+ verdict the gate never gave (#537). A cancellation before the gate ran now
257
+ reports the device stage `not reached` (`interrupted` for `--sample` and
258
+ `--follow`, `wait/interrupted` for `--wait`) instead of
259
+ `{enabled: true, result: "skipped"}`. A `--wait` cancelled after retried,
260
+ unanswered attempts no longer reports `result: "failed"`. With validation
261
+ off, all three report `{enabled: false, source: "disabled"}`.
153
262
  - Abbreviated output-file arguments (`fi=`, `file-n=`), and the spaced
154
263
  `file-name value` form under print/export/serialize, require write
155
264
  confirmation in `execute`, including inside scripts, and are refused for
@@ -181,6 +290,56 @@ part of this section.
181
290
  a native API login. A write that failed after it was sent, in single-target
182
291
  or fan-out mode, carries the new warning `transport/write-outcome-unknown`,
183
292
  because it may have run. Reads keep the retry.
293
+ - A bare known-menu path (`/system/identity`) reads as a read in `execute`,
294
+ not `usage/confirmation-required` (#543). It still answers nothing, so the
295
+ envelope steers to `/print` / `retrieve` with `tip/execute-bare-menu-path`.
296
+ Unknown paths, known-mutator verbs, and any attribute or query stay gated.
297
+ - `retrieve /system/routerboard` on a router without the menu (CHR) names the
298
+ workaround: the `validation/unknown-path` remediation points at
299
+ `/system/license --attributes system-id` instead of the generic text (#545).
300
+ - `execute` warns when a multi-statement write is refused for permission after
301
+ earlier statements may have run (#549). Such a refusal carries
302
+ `transport/write-outcome-unknown` alongside `routeros/permission-denied`,
303
+ counting statements inside executed `:if`/`:for`/`:foreach`/`:while`/`:do`/`:retry`/`:onerror` scopes and
304
+ command substitutions (`[…]`) as well as top-level ones; a `/menu { … }`
305
+ prefix is navigation, not a statement. Single refused statements, including
306
+ `remove [find …]` and one wrapped in a menu block, stay silent. The refusal itself
307
+ now surfaces as the error instead of `ok:true`: `/execute` answers HTTP 200
308
+ with `not enough permissions (9) (…; line N)` as the last `ret` line after
309
+ earlier statements' output (CHR 7.25, both transports).
310
+ - The offline gate no longer hard-rejects first-segment script-body
311
+ abbreviations it cannot prove (#525). `down={ :beep }`, accepted on device
312
+ as `down-script=`, warns `explain/canonicalizer/possible-script-body-abbreviation` and
313
+ reaches device validation instead of failing with `invalid-command-brace`. Bodies
314
+ opening with one bare command word or after a comment line warn too, and
315
+ the brace reads as a body for every reader, and whitespace after the `=`
316
+ (`down= { :beep }`) warns like the glued spelling. A bare word followed by
317
+ `;`-separated members keeps its error, as do the other array-content
318
+ braces (`comment= {1;2}` included), the space-for-`=` spelling
319
+ (`comment { :beep }`), positional braces, root directive slots
320
+ (`:put x={…}`), and `where`; `comment={ :beep }` deliberately warns though
321
+ the device rejects it.
322
+ - Native-API traps classify authentication by sentence, never trap text
323
+ (#540). Only a trap on `/login` is `transport/auth-failed`; a later command
324
+ rejected with login-like text (`/user/add name=-2` answering `invalid user
325
+ name`) is `routeros/command-failed`, agreeing with REST.
326
+ - `explain` symbols declare the value of named `name=`/`counter=` spellings in
327
+ any argument order (#533). `:local name=no` declares `no`, and
328
+ `:for from=1 counter=i to=2` binds `i`, so later `$` references link;
329
+ previously the argument name was declared and a reordered `counter=` left
330
+ `$i` unbound.
331
+ - EC-SRP5 hashes shared `z` and the REDP1 loop seed minimal-length, as
332
+ RouterOS does (#565). centrs padded both to 32 bytes, following `mtwei.c`.
333
+ As a result, about 1 in 256 MAC-Telnet logins failed on a short `z`. On
334
+ both MAC-Telnet and btest, every login failed for an account whose salt hit
335
+ the REDP1 edge, about 1 in 317 password changes. btest's `z`-only workaround
336
+ (#516) is replaced by the shared fix.
337
+ - mac-telnet with an explicit directed-broadcast `--host` (`192.168.74.255`)
338
+ sends the owning interface's real MAC (#546, #547). On Linux, `connect()` to
339
+ a broadcast reports `0.0.0.0`, so the session fell back to a synthetic
340
+ source MAC, which hardware ignores: a 30 s timeout on an RB1100Dx4 running
341
+ 7.26beta3. On that unit a real MAC gets a reply in ~0.35 s and a synthetic
342
+ one none. CHR answers both, so the CHR suite never saw it.
184
343
 
185
344
  ### Maintenance
186
345
 
@@ -200,6 +359,10 @@ part of this section.
200
359
  (Security tab), and locally with `bun run lint:actions` (#266). Its first
201
360
  run found two `${{ … }}` expansions inside shell scripts, in `lab.yaml` and
202
361
  `qa.yaml`; both now go through `env:`.
362
+ - The unit tier no longer reads the developer's `~/.config/tikoci` state
363
+ (settings, CDB) (#550, #557). A `bunfig.toml` preload sandboxes `HOME` and
364
+ `XDG_CONFIG_HOME`, so a real and a fake home give the same result. Ambient
365
+ `CENTRS_*` variables are still read.
203
366
 
204
367
  ## 0.1.8 — 2026-10-12
205
368
 
package/GLOSSARY.txt CHANGED
@@ -94,6 +94,7 @@ cwd
94
94
  datagram
95
95
  davidanson
96
96
  DDNS
97
+ dsv # `:serialize to=dsv` delimiter-separated-values format (RouterOS 7.x); the #374 comma-value control pins offline acceptance of a `delimiter=,` value
97
98
  dialable # an endpoint a client can actually connect to (advertised AND reachable)
98
99
  defconf
99
100
  demux
@@ -116,6 +117,7 @@ fat-chr
116
117
  figcaption
117
118
  FlaUI
118
119
  frontends
120
+ frontmatter # YAML metadata block at the top of a Markdown file (e.g. instruction applyTo headers)
119
121
  haakonnessjoen
120
122
  Hammerspoon
121
123
  HMAC
@@ -373,6 +375,8 @@ underflows
373
375
  Werror
374
376
 
375
377
  # MTWEI / EC-SRP vocabulary (src/protocols/mtwei.ts, mac-telnet auth)
378
+ # libucrypto: RouterOS's shared cryptography library; EC-SRP5 encoding source.
379
+ libucrypto
376
380
  # docrypto: mtwei.c's client EC-SRP proof function (mtwei_docrypto), ported to TS.
377
381
  docrypto
378
382
  # redp: REDP1 (IEEE P1363.2) random-point derivation used in the MTWEI tangle.
@@ -721,8 +725,11 @@ remoteap # caps-man definition module for `/caps-man/remote-cap`
721
725
  seccfg # caps-man definition module for `/caps-man/security`
722
726
  ifaces # CLI-Reference definition module for `/tool/graphing/interface`
723
727
  datapath # `/caps-man/datapath` — the real CLI menu behind `dpathcfg`
724
- easymesh # `/interface/wifi/easymesh` — the doc spelling of `/interface/wifi/wps-push-button`
728
+ easymesh # `/interface/wifi/easymesh` — once a doc spelling of `/interface/wifi/wps-push-button`; published as its own ungated menu since 2026-10-30, in no CHR tree through 7.26beta3
725
729
  wps # Wi-Fi Protected Setup — `/interface/wifi/wps-push-button`
730
+ bss # Wi-Fi basic service set — `/interface/wifi/easymesh/bss`
731
+ dpp # Wi-Fi Device Provisioning Protocol — `/interface/wifi/easymesh/dpp-trigger`
732
+ overcurrent # `/system/resource/hardware/reset-overcurrent`, gated by syscap `usb-hub`
726
733
  qos # switch-chip quality of service — `/interface/ethernet/switch/qos`, a real menu segment R1 must never drop
727
734
  qca # Qualcomm Atheros — the `wireless-qca` package gate
728
735
  multiswitch # syscap gate: more than one switch chip
@@ -807,6 +814,7 @@ zzznotamenu # a menu name no RouterOS build has — `/ip { zzznotamenu { print }
807
814
  # #361 absent-menu controls. Deliberate NON-words: each names a path no RouterOS
808
815
  # build has, or quotes a published gate verbatim, so the spelling is load-bearing.
809
816
  nosuch # a top-level menu no RouterOS build has — `/nosuch` is `bad command name nosuch` on CHR 7.23.7, the `validation/unknown-path` control (#361)
817
+ nosuchcmd # a root command no RouterOS build has — `:local varID [nosuchcmd where name=x]` is `bad command name nosuchcmd (line 1 column 15)` on CHR 7.25 and 7.23.8, the #374 unknown-command control
810
818
  nosuchmenu # a submenu no RouterOS build has — `/ip/nosuchmenu/print` is `syntax error (line 1 column 15)` on CHR 7.23.7, pointing at the `/` after it (#361)
811
819
  nosuchverb # a command no RouterOS build has under a real menu — `/ip/address/nosuchverb` is `bad command name nosuchverb` on CHR 7.23.7 (#361)
812
820
  qqq # the second segment of the parse-oracle row `/zzz qqq print`, captured on CHR 7.23.3 (GH#230)
@@ -821,3 +829,17 @@ apptraffic # two paths new in 7.26beta1 with different published gates: the `/to
821
829
  rle # the corpus's `highlight-rle-v1` response encoding: each highlight stream stored as run-length `[class, count]` pairs, decoded by `decodeHighlightRuns` in scripts/corpus-db.ts (#498)
822
830
  removenumbers # the IL head `/ip/firewall/address-list/removenumbers=…` that `remove [find …]` renders as, quoted verbatim in the bracket-`=` corroboration test (#498)
823
831
  vscod # the last segment of a corpus path the forum importer truncated at its slug limit (`…-editors-like-vscod/post-0014-snippet-02.rsc`); cited as the path, so the spelling must match the corpus row (#498)
832
+
833
+ # #358/#371 unquoted-byte controls. Deliberate NON-words: each puts a non-ASCII
834
+ # byte where RouterOS reads only ASCII, so the spelling is the probe.
835
+ systemé # `/systemé/identity print` — a non-ASCII byte in a path, `expected command name` at the `é` on CHR 7.24.5/7.23.8 (#371)
836
+ printé # `/system/identity printé` — a non-ASCII byte in a verb, rejected at the `é` (#371)
837
+ namé # `/system/identity/set namé=x` — a non-ASCII byte in a menu command's argument name, rejected at the name's first byte (#371)
838
+ addré # `/ip/address/add addré=1` — as `namé`: the device reports the argument name's first byte, not the `é` (#371)
839
+ caf # the ASCII prefix the device reads of `café` before the `é`: `:put "a$café"` is `$caf` then `é` (#371)
840
+
841
+ # #225 live value types: CLI Reference `Type` cell spellings the schema vocabulary maps.
842
+ iface # the CLI Reference type `iface_enum`: an argument that names an interface, which `schemaType` calls `name` (#225)
843
+ ubit # the CLI Reference type `ubit (…)`: a set of listed bits, e.g. `ubit (pap, chap, mschap1, mschap2)`, a list of `enum` (#225)
844
+ forcounter # the IL head `/forcounter=$i;…` that `:for i …` renders as; quoted verbatim in the #225 directive-argument probe test (#225)
845
+ foreachcounter # the IL head `/foreachcounter=$i;…` that `:foreach i …` renders as, in any `counter=` order; quoted verbatim in the #533 reorder test (#533)
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # centrs
2
2
 
3
3
  `centrs` is the tikoci RouterOS interaction hub: a Bun/TypeScript library and
4
- CLI (with MCP, TUI, and HTTP proxy frontends planned) for talking to MikroTik
5
- RouterOS devices through a regularized, validated interface.
4
+ CLI (with an MCP server; TUI and HTTP proxy frontends planned) for talking to
5
+ MikroTik RouterOS devices through a regularized, validated interface.
6
6
 
7
7
  Preview builds are published on npm. To try the current offline analyzer without
8
8
  installing centrs or connecting to a router:
@@ -40,7 +40,8 @@ product; without them this would just be a worse `curl`.
40
40
  > Check current dist-tags with
41
41
  > `npm view @tikoci/centrs dist-tags --json`. The library, device registry, and
42
42
  > first MCP server phases are CHR-verified; the CLI is wired (`coded`);
43
- > encrypted-CDB writes are still blocked, and the TUI/proxy frontends are planned.
43
+ > encrypted-CDB reads and writes are supported, and the TUI/proxy frontends
44
+ > are planned.
44
45
  > `docs/MATRIX.md` is the single source of truth for what works today — treat
45
46
  > anything not green there as not-yet-shipped.
46
47
 
@@ -49,9 +50,10 @@ product; without them this would just be a worse `curl`.
49
50
  - **Frontends:** TypeScript API, CLI, MCP server (Phase 1 + first Phase 2 CDB
50
51
  mutation `CHR-passed` — see `commands/mcp/`), TUI and HTTP proxy/daemon
51
52
  (future).
52
- - **Protocols:** REST API (preferred), native API, and SNMP for reads;
53
- SSH / MAC-Telnet / RoMON / WinBox Terminal for interactive and L2 execute;
54
- MNDP for discovery. centrs picks the protocol for you (and lets you pin one
53
+ - **Protocols:** REST API and native API for reads and writes; SSH / MAC-Telnet
54
+ for execution and interactive terminals; SFTP for file transfer; MNDP for
55
+ discovery; bandwidth test for peer measurement. SNMP, RoMON and WinBox
56
+ Terminal are planned. centrs picks the protocol for you (and lets you pin one
55
57
  with `via=` or `--via`).
56
58
  - **Commands:** every router-facing command resolves a *target* — a router you
57
59
  name on the CLI — to a real address, credentials, and a protocol, then runs.
@@ -60,12 +62,14 @@ product; without them this would just be a worse `curl`.
60
62
 
61
63
  | Command | Purpose |
62
64
  | ---------- | ------- |
63
- | `retrieve` | Read RouterOS state over REST/native API and SNMP OID/MIB values. `--follow` keeps a menu's rows current over native API (snapshot, then a line per change; NDJSON); `--sample <interval>` re-reads it on a timer for counters; `--query` filters on the router with the `print where` grammar; `--wait` retries until a read succeeds (or an optional `--until` condition holds), up to a deadline. |
65
+ | `retrieve` | Read RouterOS state over REST/native API. `--follow` keeps a menu's rows current over native API (snapshot, then a line per change; NDJSON); `--sample <interval>` re-reads it on a timer for counters; `--query` filters on the router with the `print where` grammar; `--wait` retries until a read succeeds (or an optional `--until` condition holds), up to a deadline. SNMP OID/MIB reads are planned. |
64
66
  | `execute` | Run RouterOS CLI-shaped read/write commands (add/set/remove) over native API/REST/L2 surfaces. |
65
67
  | `api` | Structured RouterOS API passthrough (gh-api style): one command per operation, structured in/out, can write, over REST or native API. Follow changes with `api … <menu>/listen`; `--stream` streams any command's replies (native API; NDJSON). |
66
68
  | `terminal` | Open an interactive console, primarily SSH or MAC-Telnet. |
69
+ | `transfer` | Upload, download and manage device files over REST/native API or SFTP; `upload` and `download` are shortcuts. |
70
+ | `btest` | Run a MikroTik bandwidth-test client or server over TCP/UDP. |
67
71
  | `check` | Probe reachability and management protocol availability. **Designed, not implemented** — see `docs/MATRIX.md`. |
68
- | `explain` | Analyze a RouterOS command before running it: canonical form, syntax diagnostics, and what is valid at a path. Offline analysis runs today; the live-device probes are designed. |
72
+ | `explain` | Analyze RouterOS source offline, or add read-only device evidence over REST/native API, including path schemas, completion candidates and value types. Runtime acceptance remains unproven. |
69
73
  | `devices` | View and maintain the CDB-backed device registry (the only writer). |
70
74
  | `discover` | Discover MNDP neighbors and optionally save them into the CDB. |
71
75
  | `settings` | Manage centrs's own settings (`centrs.env`, `__default__`) and run first-time setup. |
@@ -168,18 +172,24 @@ so you can always see *why* a given port or transport was chosen.
168
172
 
169
173
  `devices` is the only command that writes the CDB, and it does so safely
170
174
  (timestamped backup, atomic rename, unknown WinBox fields preserved verbatim).
171
- Encrypted-CDB **reads** work with `--cdb-password` / `CENTRS_CDB_PASSWORD`;
172
- encrypted **writes** are currently blocked pending a verified WinBox round-trip.
175
+ Encrypted-CDB reads and writes work with `--cdb-password` /
176
+ `CENTRS_CDB_PASSWORD`; writes decrypt, mutate and re-encrypt with a fresh salt.
177
+ `devices init --encrypt` creates an encrypted registry, and `devices encrypt`
178
+ explicitly converts an existing open CDB. Conversion retains existing backups
179
+ and warns that plaintext copies remain. The manual WinBox round-trip is still
180
+ pending; see the per-command contract for the current evidence and limits.
173
181
  The full contract — identity resolution, ambiguity handling, write strategy,
174
182
  and the comment grammar — lives in `commands/devices/README.md` and
175
183
  `docs/CONSTITUTION.md` (Identity and CDB).
176
184
 
177
185
  ## The result envelope, validation, and errors
178
186
 
179
- - **Validation is on by default** (`validate=true`). Reads are checked through
180
- `/console/inspect`; CLI-shaped `execute` commands through `[:parse]`. Set
181
- `validate=false` (per device or per call) only as a deliberate escape hatch.
182
- - **Every call returns one envelope:** `{ ok, data?, warnings?, error?, meta }`,
187
+ - **Validation is on by default** (`validate=true`). CLI-shaped `execute` and
188
+ `api /execute` scripts pass the offline analyzer before device validation.
189
+ Reads are checked through `/console/inspect`; CLI-shaped commands through
190
+ `[:parse]`. Set `validate=false` (per device or per call) only as a deliberate
191
+ escape hatch.
192
+ - **Every call returns one envelope:** `{ ok, data?, warnings, tips, error?, meta }`,
183
193
  with provenance and source-of-truth reporting in `meta`.
184
194
  - **Errors are typed values**, not thrown strings, with slash-namespaced codes
185
195
  (`routeros/…`, `transport/…`, `validation/…`, `cdb/…`) and a `details_url`
@@ -250,7 +260,9 @@ bun run explain:value-census # value-surface census (#225)
250
260
 
251
261
  ## Done definition (short version)
252
262
 
253
- A feature is done when every line in `commands/<name>/examples.md` is green on
254
- real CHR via `bun run test:integration`. Code existing is not done. Unit tests
255
- passing is not done. Full rule:
256
- `.github/instructions/done-definition.instructions.md`.
263
+ Live capabilities require every applicable example in `commands/<name>/examples.md`
264
+ green on real CHR via `bun run test:integration`. Offline analysis requires its
265
+ command README's full baseline acceptance criteria, including committed
266
+ CHR-grounded semantic evidence. The fixture-backed `devices` and `settings`
267
+ integration suites retain their command-specific contracts. Full rule:
268
+ [`docs/CONSTITUTION.md` → Done definition](docs/CONSTITUTION.md#done-definition).
@@ -87,8 +87,9 @@ stack-language access without centrs modelling the whole query grammar.
87
87
 
88
88
  ## CONFIRMED ON CHR (Phase 0 grounding spikes — CHR 7.23.1, 2026-06-29)
89
89
 
90
- Probes: scratchpad `api-grounding-spike.ts` (listen/CRUD/execute) and
91
- `api-grounding-spike2.ts` (duration/monitor + query forms + inspect path). Raw
90
+ Probes (phase-0 lab history, not runnable methods): `api-grounding-spike.ts`
91
+ (listen/CRUD/execute) and `api-grounding-spike2.ts` (duration/monitor + query
92
+ forms + inspect path). Raw
92
93
  native codec for the listen probe; `fetch` for REST; `connectNativeApi().talk()`
93
94
  for native.
94
95
 
@@ -184,7 +185,7 @@ green on CHR 7.23.1.
184
185
  validity (`request=child` ⇒ `validation/unknown-attribute`) fire
185
186
  identically over rest-api and native-api, **before** any write — no `:put
186
187
  [:parse]` involved (api input is a path, not a CLI string). A `/execute` script
187
- is a CLI string ⇒ `meta.validation.semantic = "not-applicable"`.
188
+ is a CLI string ⇒ no `meta.validation.semantic` field (#535).
188
189
 
189
190
  ## CONFIRMED ON CHR (RouterOS 7.12.2 compatibility — GH#343, 2026-09-14)
190
191
 
@@ -197,6 +198,23 @@ green on CHR 7.23.1.
197
198
  validation therefore uses command-level `request=child` only. This does not
198
199
  prohibit argument-level completion probes such as `print,proplist`, which are
199
200
  a separate shape and returned immediately on the same 7.12.2 controls.
201
+ - **Command-level `request=syntax` is unsafe too, on 7.12.2 and 7.20.8** (#492,
202
+ 2026-10-19, reproduced on a fresh boot of each).
203
+ - REST: `syntax` with `path=` set to a command (`ip,address,set`,
204
+ `system,identity,set`, `ip,route,add`, `interface,print`) got no answer
205
+ within 12 s, 5 of 5 times. The `child` controls queued behind it timed out
206
+ until the handler recovered about 30 s later.
207
+ - Native API closed the connection.
208
+ - The same syntax requests at a menu (`ip,address`) or an argument
209
+ (`ip,address,set,address`, `put,message`, `do,command`) answered in about
210
+ 25 ms. 7.23.8 and 7.24.5 answer every shape.
211
+ - restraml's crawlers send `syntax` to `arg` nodes only, so `inspect.json`
212
+ never exercised the hanging shape.
213
+ - Input-based `completion` (`input="/ip/address add comm"`) answered at every
214
+ position tried on both builds, including scripting keywords and `do={`
215
+ bodies on 7.20.8.
216
+ - Explain's `--schema`/`--complete` therefore send `syntax` for argument
217
+ paths and `completion` with `input=` only.
200
218
 
201
219
  ## CONFIRMED ON CHR (Phase 4 `--stream` follow — CHR 7.23.1, 2026-06-30)
202
220
 
@@ -51,7 +51,14 @@ centrs api <router> <endpoint> [flags]
51
51
  segment (`ip/address/*1`) addresses one row for GET/PATCH/DELETE; PUT and
52
52
  POST reject it with `input/invalid-path`. A trailing `/listen` segment is
53
53
  the menu's `listen` command, which implies `--stream` + `--via native-api`
54
- and requires GET (`ip/address/*1/listen` follows one row).
54
+ and requires GET (`ip/address/*1/listen` follows one row). After that
55
+ normalization every menu segment must be a RouterOS menu name (letters,
56
+ digits, hyphens; case folds), or the request fails with
57
+ `input/invalid-routeros-path` before any transport is chosen, whatever
58
+ `--validate` says. A `#` or `?` once cut `/print` off a projected GET's
59
+ REST URL and posted the bare command (`/system/reboot#` rebooted CHR 7.24.5,
60
+ #586); the grammar and retrieve's REST guard are described in
61
+ [retrieve → Validation](../retrieve/README.md#validation).
55
62
 
56
63
  The HTTP method (`-X`, default `GET`) is honored **literally** against RouterOS's
57
64
  REST mapping — `GET`→print/get, **`PUT`→add**, `PATCH`→set, `DELETE`→remove,
@@ -114,7 +121,9 @@ vs `--query` distinction.
114
121
  before they were sent, keep the `transport/network` /
115
122
  `transport/connection-closed` retry. A mutating request that failed after it
116
123
  was sent carries the warning `transport/write-outcome-unknown`, in single-target
117
- and fan-out mode alike.
124
+ and fan-out mode alike. A multi-statement `/execute` script refused with
125
+ `routeros/permission-denied` carries the same warning (#549); a single
126
+ refused statement, and a refused structured request, stay silent.
118
127
  - **`--stream` (or a `/listen` endpoint) is single-session** → `usage/fanout-not-supported` in
119
128
  fan-out mode. **`--raw` strips the envelope** → `usage/conflicting-flags` in
120
129
  fan-out mode (per-target envelopes can't be bare).
@@ -134,7 +143,8 @@ gate is `/console/inspect` — **not** `:put [:parse]`:
134
143
  does) answers normally on the same build. See the 7.12.2 finding in
135
144
  `AGENTS.md` (GH#343).
136
145
  - **Carve-out:** a script-shaped `POST /rest/execute` is a CLI string, not a path,
137
- so structured inspect is `not-applicable` (`meta.validation.semantic`). It
146
+ so structured inspect does not apply and `meta.validation.semantic` is
147
+ absent. It
138
148
  follows the CLI-string pipeline instead: offline analysis, then live
139
149
  `:put [:parse ...]`, then run. An offline rejection is still a byte span with
140
150
  no connection; an offline pass or abstention continues to the device stage.
@@ -148,10 +158,11 @@ gate is `/console/inspect` — **not** `:put [:parse]`:
148
158
  unaffected — the result matchers are anchored, not substring.
149
159
  - Structured path requests are **not** offline-gated. The input is a path plus a
150
160
  body, not a CLI string, and an offline *path* gate collides with GH#211's
151
- unlisted-path decision. On the success path `meta.validation.stages[]` is
152
- absent there; on a `validation/*` rejection it is present and reports the
153
- offline stage `skipped` with that reason, so the breakdown never implies an
154
- analysis that did not run.
161
+ unlisted-path decision. `meta.validation.stages[]` reports the offline stage
162
+ `skipped` with that reason, on success and on a `validation/*` rejection
163
+ alike, so the breakdown never implies an analysis that did not run. On
164
+ success the device stage is `passed`, `semantic` is `true`, and there is no
165
+ `syntax` field, because no `:parse` ran (#535).
155
166
 
156
167
  `--validate=false` skips **both** stages of the preflight — one flag, one meaning
157
168
  (constitution: validation); RouterOS still re-validates writes server-side.
@@ -156,7 +156,7 @@ centrs api $R execute -X POST -f script=':put [/system/identity/get name]' --use
156
156
  ```
157
157
 
158
158
  Envelope: `ok: true`, `data` is string-shaped and contains the CHR identity;
159
- `meta.validation.semantic=not-applicable` (script, not a path) and
159
+ `meta.validation.syntax=true` with no `semantic` field (script, not a path) and
160
160
  `meta.validation.stages` is `offline: passed` then live `device: passed` from
161
161
  `:put [:parse ...]`.
162
162
 
@@ -139,6 +139,15 @@ is net-new — `mtwei.ts` only implements the client. A bad proof (or a client t
139
139
  will not offer EC-SRP5 against an `authenticate=yes` server) maps to
140
140
  `transport/auth-failed`. Legacy pre-6.43 double-MD5 is out of scope for v1.
141
141
 
142
+ The shared core hashes minimal-length big-endian integers in the REDP1 loop
143
+ and for shared-point `z`; public keys and the REDP1 input keep their fixed
144
+ 32-byte X encoding (#565). The distinction is grounded in RouterOS
145
+ `libucrypto.so` 6.49.23/7.25 and `tikoci/m2ir`'s live WinBox discriminators.
146
+ `test/fixtures/ec-srp5.json` pins calculated regression transcripts;
147
+ `test/integration/ec-srp5.test.ts` forces the encoding edges against a real
148
+ CHR btest client, including a short `z` and a leading-zero REDP1 input
149
+ (confirmed on CHR 7.23.8 and 7.25).
150
+
142
151
  ## Output
143
152
 
144
153
  btest runs over time, so it emits **live records** during the run plus a final
@@ -99,6 +99,13 @@ and non-zero throughput. This is the example that exercises the net-new
99
99
  server-side EC-SRP5 verifier against real RouterOS (TCP keeps the gated path off
100
100
  the SLIRP UDP-reverse-NAT question).
101
101
 
102
+ `test/integration/ec-srp5.test.ts` also exercises this exchange with chosen
103
+ test salts and ephemeral scalars, one edge per pass: a short REDP1 loop hash
104
+ input, a REDP1 input whose leading zero must stay, and that input again with a
105
+ forced short shared `z`. RouterOS's client proof must match the minimal-length
106
+ derivation. This discriminator stops at the
107
+ proof; the normal btest suites cover completed sessions and throughput (#565).
108
+
102
109
  ### 4. Wrong credentials are rejected
103
110
 
104
111
  > *Designed, not yet gated* — the auth-reject path is asserted by the loopback
@@ -89,6 +89,17 @@ or a duplicated `identity=`):
89
89
  lists all matching entries (by `cdbRecordIndex`, `target`, `user`, and
90
90
  `recordType`). The caller pins the choice with `--match` by `user`
91
91
  (`--match user=admin`), by record-type (`--match ipUser`), or by exact target.
92
+ `--match` takes the same selector on `devices show`/`set`/`remove` and on
93
+ every command that resolves a `<router>` from the CDB: `retrieve`,
94
+ `execute`, `api`, `transfer`, `terminal` and `btest client` (#583). It is a
95
+ single-target flag: with a fan-out selection or `--quickchr` it is
96
+ `usage/conflicting-flags`, and when no CDB record is in play (no CDB, an
97
+ unregistered literal host, a `__default__`-only fallback, or `--host`
98
+ without `<router>`) it fails `identity/no-match` rather than being ignored.
99
+ With no target at all (no `<router>`, no `--host`) the CLI still reports
100
+ the missing-target usage error first.
101
+ An ambiguous positional inside a fan-out names the single-target
102
+ `--match` re-run, since the fan-out itself refuses the flag.
92
103
 
93
104
  `--prefer-family=ipv4|ipv6` is a non-interactive way to break v4/v6 ties
94
105
  deterministically; with it set, no prompt is raised. It emits a warning
@@ -141,6 +152,15 @@ Rules:
141
152
  - An **empty value removes** the token: `devices set <t> via=` deletes the `via`
142
153
  override (matching the internal upsert/remove model in `comment-kv.ts`). There
143
154
  is no current need to store a literally-empty value; revisit if one arises.
155
+ Removing a token that is already absent is a no-op success — the comment is
156
+ left byte-for-byte identical.
157
+ - First-class fields have their own empty-value rules, distinct from token
158
+ removal: `--password ""` clears the stored password (and its saved-password
159
+ flag); `--user ""` writes an empty user. Bare `user=` / `password=` /
160
+ `group=` / `profile=` / `session=` positionals never reach those fields —
161
+ they are refused with `cdb/reserved-key` — so comment-token removal (`key=`
162
+ deletes the token) and credential clearing (`--password ""` empties the
163
+ password) are deliberately different operations.
144
164
  - `commentMirror` is kept in sync with `comment` on every write.
145
165
 
146
166
  Precedence (lowest → highest): built-in defaults → user config file
@@ -340,7 +360,8 @@ runs produce stable diffs.
340
360
  ## Subcommands
341
361
 
342
362
  ```text
343
- centrs devices init # create an empty CDB at the resolved path
363
+ centrs devices init [--encrypt] # create an empty CDB at the resolved path
364
+ centrs devices encrypt [--no-backup] # explicitly encrypt an existing open CDB
344
365
  centrs devices list [--group G] [--where attr=value]
345
366
  [--near lat,lon,radius] [--bbox south,west,north,east]
346
367
  [--format json|yaml|text]
@@ -370,9 +391,11 @@ clack-style prompt used by `@tikoci/quickchr`); it is **not** a separate
370
391
  field-editing verb, and invoking it today returns `usage/not-implemented`. There
371
392
  is no `update`.
372
393
 
373
- - `init` creates an empty open CDB at the resolved path (`--cdb-file`,
394
+ - `init` creates an empty CDB (open by default, encrypted with `--encrypt`)
395
+ at the resolved path (`--cdb-file`,
374
396
  `CENTRS_CDB_FILE`, or the default), with parent directories and mode `0600`.
375
- It never overwrites: on an existing, readable CDB it succeeds with a
397
+ It never overwrites: on an existing, readable CDB matching the encryption
398
+ requirement it succeeds with a
376
399
  `cdb/file-exists` warning and writes nothing. An explicit path is created only
377
400
  by `init`; every other subcommand reports `cdb/not-found` for it (typo
378
401
  protection), and that error names `init`.
@@ -437,6 +460,52 @@ can edit it too. Precedence is per-field: args → env → device record →
437
460
  **MCP** it never widens the allowlist — an unregistered target is still
438
461
  rejected; `__default__` only fills missing creds for a registered device.
439
462
 
463
+ ## CDB encryption lifecycle (#558)
464
+
465
+ Start encrypted with `devices init --encrypt`, or start open for a lab and run
466
+ `devices encrypt` when moving to production. Both require a non-empty
467
+ `--cdb-password` / `CENTRS_CDB_PASSWORD`. A password alone does not change the
468
+ mode of initialization or ordinary writes. The library exports
469
+ `initCdb({ encrypt: true, cdbPassword, cdbFile })` and
470
+ `encryptCdb({ cdbPassword, cdbFile })`; both use the shared settings precedence.
471
+
472
+ Encrypted initialization keeps atomic no-clobber creation and mode `0600`.
473
+ Initialization returns `data.encrypted` for both newly created and existing
474
+ files. Competing encrypted initializers with different passwords fail closed:
475
+ the loser gets `cdb/decrypt-failed` when it cannot unlock the winner's file.
476
+ An existing open CDB refuses `init --encrypt`; conversion must be explicit.
477
+ An existing encrypted CDB is verified with the supplied password and left alone.
478
+ Conversion requires an existing readable CDB, preserves its records and unknown
479
+ fields, uses a fresh salt and atomic replacement, and keeps its file mode.
480
+ `data` reports `cdbFile`, `encrypted: true`, `changed`, `recordCount`, and the
481
+ new `backupPath` when conversion occurred. Default text output reports
482
+ `encrypted` or `verified encrypted ... (unchanged)`, the record count, and
483
+ the backup path when created, followed by the plaintext-backup warning.
484
+ Already-encrypted conversion verifies
485
+ the password and returns `changed: false` without rewriting or rotating it.
486
+
487
+ **Backups remain sensitive.** Conversion retains every existing backup and
488
+ creates a plaintext snapshot of the original; it does not run backup retention.
489
+ The `cdb/plaintext-backups` warning accompanies conversion, including no-op runs:
490
+ only the active file is protected. Handle backups according to your own retention
491
+ policy; centrs neither encrypts nor deletes them during conversion. Subsequent
492
+ ordinary writes still use the normal five-backup retention below.
493
+ `--no-backup` (library: `skipBackup: true`) skips the new plaintext snapshot, so
494
+ no fresh plaintext copy is written; earlier backups are still retained and the
495
+ warning still applies to them. Without the snapshot, a forgotten password cannot
496
+ be recovered.
497
+
498
+ `init` and `encrypt` act on the CDB file, not on records: they accept only
499
+ `--cdb-file`, `--cdb-password`, `--format`/`--json`, plus `--encrypt` (`init`)
500
+ or `--no-backup` (`encrypt`). Any other flag is `input/invalid-command`
501
+ instead of being silently ignored.
502
+
503
+ Close WinBox and other CDB writers before conversion. If they share this path,
504
+ they must reopen it using the CDB password; a stale writer can overwrite the
505
+ conversion. Atomic replacement does not provide concurrent-writer transactions
506
+ (#581). Password rotation and decryption are outside this slice. WinBox manual
507
+ interoperability verification remains open (see Residual risks).
508
+
440
509
  ## CDB write strategy
441
510
 
442
511
  All writes go through one helper (`writeWinBoxCdb`) that: