@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
@@ -569,3 +569,49 @@ list succeeds over sftp: `meta.via=sftp`, `meta.target.source.kind=provider`,
569
569
  handed over) carried as a provider-sourced `sshKey`. Otherwise the envelope is
570
570
  `ok: false` with `error.code=quickchr/unsupported-via` and exit code 1 — a
571
571
  typed gate, **never** a password prompt and never a silent fallback to REST.
572
+
573
+ ## CDB record `via=` (#591)
574
+
575
+ A record's `via=` names a transport, shared with `execute`; transfer maps it
576
+ to a method (`ssh`→`sftp`, `rest-api`→`rest`, `native-api`→`native`). `$CDB`
577
+ holds one record for `127.0.0.1` whose comment is
578
+ `via=ssh port=$SSH_PORT ssh-key=$KEY insecure=true`; the key is imported as
579
+ in the sftp section. C1–C2 are covered by
580
+ `test/integration/transfer-cdb-via.test.ts`; C3–C4 are offline error
581
+ contracts covered by `test/unit/transfer.test.ts`
582
+ (`resolveTransferRequest takes the CDB record's via=`).
583
+
584
+ ### C1. A `via=ssh` record runs execute over ssh and transfer over sftp
585
+
586
+ ```bash
587
+ centrs execute 127.0.0.1 --cdb-file $CDB /system/identity/print --json
588
+ centrs transfer 127.0.0.1 --cdb-file $CDB upload $SRC centrs-591.txt --json
589
+ ```
590
+
591
+ Both `ok: true` with `meta.via="ssh"`, and no `--via` on either. The
592
+ transfer's `meta.settings.via` is `{kind: "comment-kv", key: "record:0:via"}`.
593
+
594
+ ### C2. Download from the same record, with and without `--via sftp`
595
+
596
+ ```bash
597
+ centrs transfer 127.0.0.1 --cdb-file $CDB download centrs-591.txt $TMP/c2-auto.txt --json
598
+ centrs transfer 127.0.0.1 --cdb-file $CDB --via sftp download centrs-591.txt $TMP/c2-sftp.txt --json
599
+ ```
600
+
601
+ Both `ok: true`, `meta.via="ssh"`; each file is byte-identical to `$SRC`.
602
+ `--via ssh` is accepted as the same method as `--via sftp`.
603
+
604
+ ### C3. A record transport that cannot carry files
605
+
606
+ A record with `via=mac-telnet` (or `romon`, `snmp`, `winbox-terminal`,
607
+ `mndp`, `btest`) and no `--via`: `ok: false`, `error.code=settings/invalid-via`,
608
+ `error.context.source="record:N:via"`, and the remediation suggests
609
+ `--via sftp`, `--via rest` or `--via native`. An explicit `--via` wins over
610
+ the record and the transfer proceeds.
611
+
612
+ ### C4. A `via=rest-api` record does not auto-hop a large upload
613
+
614
+ A `via=rest-api` record uploading more than 60 KB with no `--via`:
615
+ `ok: false`, `error.code=transport/unsupported-operation`, nothing sent. The
616
+ remediation names `record:N:via` and suggests `--via sftp`, matching a
617
+ pinned `--via rest`.
package/docs/CLI.md CHANGED
@@ -15,7 +15,7 @@ implemented flags live in each command's `commands/<name>/README.md`.
15
15
  | [`api`](#api) | Structured RouterOS API passthrough (gh-api style) over REST or native API. |
16
16
  | [`transfer`](#transfer) | Copy files to/from a RouterOS device and manage device files (rest/native/sftp). |
17
17
  | [`terminal`](#terminal) | Open an interactive RouterOS console (ssh or mac-telnet). |
18
- | [`explain`](#explain) | Analyze a RouterOS command offline: canonical form, structure, and syntax diagnostics. |
18
+ | [`explain`](#explain) | Analyze RouterOS input offline or add read-only device evidence from one router. |
19
19
  | [`devices`](#devices) | Inspect and mutate the CDB-backed device registry. `devices` is the only command that writes the CDB. |
20
20
  | [`discover`](#discover) | Discover RouterOS neighbors over MNDP and optionally save them into the CDB. |
21
21
  | [`btest`](#btest) | Run MikroTik's bandwidth test as a client or server (peer measurement). |
@@ -64,6 +64,7 @@ Usage: centrs retrieve <target> <routeros-path> [flags] | centrs retrieve <targe
64
64
  | `--max-results` | `<bytes>` | Fail instead of printing output larger than the given byte budget. |
65
65
  | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
66
66
  | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
67
+ | `--match` | &lt;user=name\|target=addr\|record-type&gt; | Pick one of several CDB records saved for `<router>` (as `devices show --match`); single target only. |
67
68
  | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
68
69
  | `--validate / --no-validate` | | Enable or disable inspect-backed preflight validation. |
69
70
  | `--verbose` | | Show resolved setting sources in text output, on stderr. |
@@ -97,6 +98,7 @@ Usage: centrs execute <target> <command> [flags] | centrs execute <target...> --
97
98
  | `--insecure` | | Disable SSH host-key verification (`--via ssh`: accepts changed/impersonated keys, not just new) or accept a self-signed `api-ssl` TLS cert. Default verifies. |
98
99
  | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
99
100
  | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
101
+ | `--match` | &lt;user=name\|target=addr\|record-type&gt; | Pick one of several CDB records saved for `<router>` (as `devices show --match`); single target only. |
100
102
  | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
101
103
  | `--timeout` | `<duration>` | Per-request timeout (for REST, max 60s). |
102
104
  | `--validate / --no-validate` | | Run RouterOS :parse and /console/inspect validation before execution (default true; `--validate=false` also accepted). |
@@ -145,6 +147,7 @@ Usage: centrs api <router> <endpoint> [flags] | centrs api --host <host|url> <en
145
147
  | `--insecure` | | Accept a self-signed `api-ssl`/REST TLS cert. Default verifies. |
146
148
  | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
147
149
  | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
150
+ | `--match` | &lt;user=name\|target=addr\|record-type&gt; | Pick one of several CDB records saved for `<router>` (as `devices show --match`); single target only. |
148
151
  | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
149
152
  | `--timeout` | `<duration>` | Per-request timeout (for REST, max 60s). |
150
153
  | `--validate / --no-validate` | | Run `/console/inspect` validation before the request (default true; `--validate=false` also accepted). |
@@ -162,7 +165,7 @@ Usage: centrs transfer <router>|--host <host> upload <local> [remote] | download
162
165
 
163
166
  | Flag | Value | Description |
164
167
  | ---- | ----- | ----------- |
165
- | `--via` | `<method>` | Pin the method: rest, native, or sftp (large transfers); scp/fetch/ftp are not built yet. Auto picks the cheapest by size/direction. |
168
+ | `--via` | `<method>` | Pin the method: rest, native, or sftp (large transfers; `ssh` is an alias); scp/fetch/ftp are not built yet. Auto picks the cheapest by size/direction, after a CDB record's `via=` (ssh→sftp). |
166
169
  | `--ssh-key` | `<path>` | sftp only: explicit private-key path. Falls back to CENTRS_SSH_KEY / the ssh-agent. |
167
170
  | `--insecure` | | Accept a self-signed TLS cert (https/api-ssl) or a new SSH host key. Default verifies. |
168
171
  | `--force / --overwrite` | | Skip the existing-destination check on the device for `upload`/`mkdir`/`copy` (`upload` then overwrites). Default refuses an existing target. `download` always replaces the local file. |
@@ -189,6 +192,7 @@ Usage: centrs transfer <router>|--host <host> upload <local> [remote] | download
189
192
  | `--yaml` | | Shortcut for --format yaml. |
190
193
  | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
191
194
  | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
195
+ | `--match` | &lt;user=name\|target=addr\|record-type&gt; | Pick one of several CDB records saved for `<router>` (as `devices show --match`); single target only. |
192
196
  | `--validate / --no-validate` | | Enable or disable the existence-probe preflight. |
193
197
  | `--verbose` | | Show the resolved transport in text output, on stderr. |
194
198
 
@@ -212,6 +216,7 @@ Usage: centrs terminal <router> [flags] | centrs terminal --host <host> [flags]
212
216
  | `--insecure` | | `--via ssh`: disable host-key verification (accepts changed/impersonated keys). Default verifies. |
213
217
  | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
214
218
  | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
219
+ | `--match` | &lt;user=name\|target=addr\|record-type&gt; | Pick one of several CDB records saved for `<router>` (as `devices show --match`); single target only. |
215
220
  | `--resolve` | &lt;none\|arp&gt; | `--via ssh` only: turn a MAC target into an IP. CDB-first; `arp` opts into the host ARP cache (default none). The mac-telnet default ignores it. |
216
221
  | `--format` | &lt;text\|json\|yaml&gt; | Error-envelope format on failure. --json / --yaml shortcuts. |
217
222
  | `--json` | | Shortcut for --format json. |
@@ -220,18 +225,32 @@ Usage: centrs terminal <router> [flags] | centrs terminal --host <host> [flags]
220
225
 
221
226
  ## explain
222
227
 
223
- Analyze a RouterOS command offline: canonical form, structure, and syntax diagnostics.
228
+ Analyze RouterOS input offline or add read-only device evidence from one router.
224
229
 
225
230
  ```text
226
- Usage: centrs explain '<input>' [flags]
231
+ Usage: centrs explain [<router>] '<input>' [flags]
227
232
  ```
228
233
 
229
234
  | Flag | Value | Description |
230
235
  | ---- | ----- | ----------- |
236
+ | `--via` | &lt;rest-api\|native-api&gt; | Pin the transport; no silent downgrade. Default rest-api. |
237
+ | `--host` | &lt;host\|url&gt; | Host or base URL to dial. With no `<router>` it is the target (no CDB lookup); with one it overrides the resolved host. |
238
+ | `--port` | `<port>` | Override the resolved management port. |
239
+ | `--username / --user / -u` | `<name>` | RouterOS username. Falls back to `CENTRS_USERNAME`. |
240
+ | `--password` | `<secret>` | RouterOS password. Falls back to `CENTRS_PASSWORD`. |
241
+ | `--insecure` | | Accept a self-signed `api-ssl`/REST TLS cert. Default verifies. |
242
+ | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
243
+ | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
244
+ | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
245
+ | `--timeout` | `<duration>` | Per-request timeout (for REST, max 60s). |
246
+ | `-p` | `<secret>` | Alias for --password. |
247
+ | `--quickchr` | `<name>` | Resolve one running quickchr VM by name. |
231
248
  | `--file` | `<path>` | Read the input from a file instead of the positional (`-` reads stdin). Piped stdin is used automatically when no input positional is given. |
232
249
  | `--fail-on` | &lt;error\|warning\|never&gt; | Exit 2 when the verdict reaches this severity. Default `error`; an ambiguous or unknown statement is a warning, never an error. |
233
- | `--complete` | | Continuation candidates at the cursor. Live evidence — offline emits a tip and enumerates nothing. |
234
- | `--schema` | | Path enumeration (verbs, args, types, enums). Live evidence — offline emits a tip and enumerates nothing. |
250
+ | `--complete` | | Candidates at the cursor: menus and verbs, argument names, or values. Live evidence — offline emits a tip and enumerates nothing. |
251
+ | `--schema` | | Describe the path at the cursor: a menu's verbs and submenus, a command's arguments, `print`'s proplist. Live evidence — offline emits a tip and enumerates nothing. |
252
+ | `--cursor` | `<byte>` | UTF-8 byte offset for --complete/--schema. Default: the end of the input. |
253
+ | `--full` | | Lift --complete/--schema sizing to its safety ceiling (1024 names, 128 expansion probes). |
235
254
  | `--curl` | | Render a ready-to-edit REST curl for statements covered by a runtime-tested mapping rule. |
236
255
  | `--tokens` | | Emit the total, gapless token partition behind `data.tokens[]` (every byte not claimed by an analyzer is `unclassified`). |
237
256
  | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. |
@@ -244,13 +263,15 @@ Usage: centrs explain '<input>' [flags]
244
263
  Inspect and mutate the CDB-backed device registry. `devices` is the only command that writes the CDB.
245
264
 
246
265
  ```text
247
- Usage: centrs devices <init|list|show|groups|add|edit|set|remove> [args] [flags]
266
+ Usage: centrs devices <init|encrypt|list|show|groups|add|edit|set|remove> [args] [flags]
248
267
  ```
249
268
 
250
269
  | Flag | Value | Description |
251
270
  | ---- | ----- | ----------- |
271
+ | `--encrypt` | | `init` only — require encrypted creation with a non-empty CDB password; refuse an existing open CDB. |
272
+ | `--no-backup` | | `encrypt` only — skip the plaintext pre-conversion snapshot. Without that snapshot, a forgotten password cannot be recovered. |
252
273
  | `--cdb-file` | `<path>` | Override the resolved CDB path (default `~/.config/tikoci/winbox.cdb`). |
253
- | `--cdb-password` | `<secret>` | Decrypt an encrypted CDB. Falls back to `CENTRS_CDB_PASSWORD`. Also used to re-encrypt on writes. |
274
+ | `--cdb-password` | `<secret>` | Decrypt an encrypted CDB. Falls back to `CENTRS_CDB_PASSWORD`. Also used for `init --encrypt`, `encrypt`, and to re-encrypt on writes. |
254
275
  | `--via` | `<protocol>` | Resolve and report the protocol source for provenance examples; no network IO is performed. |
255
276
  | `--group` | `<name>` | `list` filter / `add`,`edit` first-class group field for the target entry. |
256
277
  | `--where` | `<attr=value>` | `list` only — repeatable device-class filter over CDB facts (AND-combined), e.g. `--where lat=37.7749`. |
@@ -326,6 +347,7 @@ Usage: centrs btest <client <router>|server> [flags]
326
347
  | `--port` | `<port>` | Control port. Default 2000. |
327
348
  | `--cdb-file` | `<path>` | (client) Read target credentials from a WinBox CDB file. |
328
349
  | `--cdb-password` | `<password>` | (client) Decrypt an encrypted WinBox CDB file. |
350
+ | `--match` | &lt;user=name\|target=addr\|record-type&gt; | (client) Pick one of several CDB records saved for `<router>` (as `devices show --match`); `btest server` rejects it. |
329
351
  | `--format` | &lt;text\|csv\|json\|yaml&gt; | Output format. Default text. |
330
352
  | `--csv` | | Shortcut for `--format csv`. |
331
353
 
@@ -73,15 +73,20 @@ gate passed keeps `passed`. When something stopped the gate before it gave a
73
73
  verdict (a `transport/*` or `auth/*` failure, a device error that is not a
74
74
  gate answer, or a refusal before the gate ran), `result` and the
75
75
  `syntax`/`semantic` summaries are absent. Each stage the gate did not reach is
76
- then `skipped` with `reason: "not reached: <error.code>"` (GH#435). On an
77
- `execute` rejection, `syntax: true` survives only for a name-level refusal (an
78
- unknown attribute in a structured command). Any other device or offline
79
- refusal is `syntax: false`, and `semantic` is then absent (`not-applicable` for
80
- script input), because the name-level check was never asked. `api`'s
81
- structured requests run no `:parse`, so their `syntax: false` means "not run",
82
- even on success. That overload is part of the review below. This is an
83
- interim shape: the validation meta across commands is reviewed again with live
84
- `explain` (#236).
76
+ then `skipped` with `reason: "not reached: <error.code>"` (GH#435).
77
+
78
+ The flat `syntax` / `semantic` fields summarize `stages[]`, and each is present
79
+ **only when a stage gave that verdict** (#535). Absent means no stage was
80
+ asked, never "passed" and never "failed". So `--validate=false` carries
81
+ neither. A script has no `semantic` field, because it has no names to check,
82
+ and `api`'s structured requests have no `syntax` field, because they run no
83
+ `:parse`. On a rejection, `syntax: true` with `semantic: false` is a
84
+ name-level refusal (an unknown attribute in a structured command). A `:parse`
85
+ or offline refusal is `syntax: false` with no `semantic`. Every gated success
86
+ reports its `stages[]`, retrieve and transfer included. `explain` is not a
87
+ gate, because it never runs the input: its `meta.validation` is
88
+ `{ enabled: false }` with no `result`, offline and live, and its verdict is
89
+ `data.verdict`.
85
90
 
86
91
  `--validate=false` disables **both** stages — one flag, one meaning. A bypass
87
92
  that covered only the device stage would leave a caller stranded behind a
package/docs/MATRIX.md CHANGED
@@ -40,7 +40,7 @@ their command-specific evidence contracts.
40
40
  | devices | — | — | — | — | — | — | — | — |
41
41
  | discover | — | — | — | — | — | `CHR-passed` | — | — |
42
42
  | check | `designed` | `designed` | `designed` | `designed` | `not-started` | `not-started` | `not-started` | `not-started` |
43
- | explain | `designed` | `designed` | — | — | — | — | — | — |
43
+ | explain | `CHR-passed` | `CHR-passed` | — | — | — | — | — | — |
44
44
  | settings | — | — | — | — | — | — | — | — |
45
45
 
46
46
  ## Status pointers
@@ -101,10 +101,14 @@ and implementation sequencing in the linked issues.
101
101
  `winbox-terminal` stay `not-started`. Its per-host fan-out is the machinery the
102
102
  IP-scan discovery (#149) will iterate; the L2-default timing evidence (#136) is
103
103
  recorded in the README before the cells advance past `designed`.
104
- - `explain`'s two `designed` protocol cells are the future live probes
105
- (`/console/inspect` + `:parse`, #236). The live grammar
106
- (`explain <router> '<input>'`) is parsed and refused with
107
- `usage/not-implemented`, never degraded to an offline run. Offline analysis
104
+ - `explain`'s two `CHR-passed` protocol cells are the whole live capability
105
+ (#236). `explain <router> '<input>'` collects read-only `:parse`, highlight
106
+ and child argument-name evidence (#491), and `--schema`/`--complete` describe
107
+ paths and complete at a cursor (#492). Live value types (#225) fill
108
+ `observedType` from visible `:parse` conversions and `schemaType` from
109
+ argument `syntax`. Examples 7–16, 19, 24, 48 and 52–53 pass on REST and
110
+ native API in `test/integration/explain-live*.test.ts`, on stable 7.24.5 and
111
+ long-term 7.23.8. `runtimeAcceptance` stays `not-proven`. Offline analysis
108
112
  has its own capability entry below. The generated structure tables
109
113
  `src/explain/menus.ts` and `src/explain/catalog.ts` keep analysis offline;
110
114
  their ownership and regeneration contracts live in `commands/explain/README.md`.
@@ -182,16 +186,27 @@ Wire, auth, TCP/UDP, and CI caveats live in `commands/btest/README.md` and the
182
186
 
183
187
  ## Current priority
184
188
 
189
+ Live `explain` (#236) is `CHR-passed` on REST and native API: the #491
190
+ foundation, #492's describe and completion facets, and #225's live value types,
191
+ with every applicable example green on stable and long-term CHR. Issue #90
192
+ owns the remaining sequencing, including #507's context surface. Its bounded
193
+ submission matrix is grounded (#574); context selection and binding behavior
194
+ remain follow-up work.
195
+
185
196
  **First: `explain`** (maintainer decision 2026-10-16 on
186
197
  [#90](https://github.com/tikoci/centrs/issues/90)). In order: make the
187
- published offline readings honest (#350, #211), give the result a statement
188
- tree, then the live probe foundation (#491). Then use those probes to
189
- ground #351, #358 and #371, and finish live (#492 → #236 closeout). The
198
+ published offline readings honest (#350, #211; complete), give the result a
199
+ statement tree (#497; complete), then the live probe foundation (#491;
200
+ complete). Those probes grounded #351, #358 and #371 (complete), #492
201
+ added the describe/completion facets, and #225 the live value types. The
202
+ closeout of #236 promoted both live cells (complete). The
190
203
  plan, and what is deliberately out of this run, is the 2026-10-16 plan
191
204
  comment on #90. It is step 3 of the validation track below.
192
205
 
193
- **The CMR track is paused until RouterOS 7.26beta2** (maintainer decision
194
- 2026-10-16). It was first from the 2026-10-12 decision on
206
+ **The CMR track is paused** (maintainer decision 2026-10-16; held for the
207
+ 7.26beta caller-policy fix — #527 now carries direct 7.26beta3 evidence that a
208
+ restricted caller is refused, with the first refusing release still
209
+ unbracketed). It was first from the 2026-10-12 decision on
195
210
  [#441](https://github.com/tikoci/centrs/issues/441). Done: `execute --cmr`
196
211
  (#450), which runs one validated command on CMR clients through their
197
212
  controller. The contract is in
@@ -222,11 +237,11 @@ order agreed on [#236](https://github.com/tikoci/centrs/issues/236):
222
237
  2. **Done** — front the `execute` / `api` device preflight with the offline
223
238
  analyzer (#354). The gate is now two-stage; the contract is in
224
239
  [`docs/CONSTITUTION.md` → Validation is the product](CONSTITUTION.md#validation-is-the-product).
225
- 3. **Next** — **live `explain` (#236)**, as stage two of a gate that now exists.
226
- Start with [the #90 task index](https://github.com/tikoci/centrs/issues/90),
227
- then follow [`commands/explain/README.md`](../commands/explain/README.md) and
228
- the live examples in
229
- [`commands/explain/examples.md`](../commands/explain/examples.md).
240
+ 3. **Done** — **live `explain` (#236)**, as stage two of a gate that now exists.
241
+ The contract is [`commands/explain/README.md`](../commands/explain/README.md)
242
+ and the live examples in
243
+ [`commands/explain/examples.md`](../commands/explain/examples.md);
244
+ [#90](https://github.com/tikoci/centrs/issues/90) indexes what follows.
230
245
 
231
246
  The offline baseline stays `verified`: #264 closed the measured token vocabulary
232
247
  and #322 closed the final bounded-execution gap, including the public depth-axis
@@ -235,17 +250,17 @@ guard.
235
250
  The live pass is the device-backed cross-check on the offline result, not a
236
251
  replacement for it. It adds read-only `/console/inspect` and `:parse` evidence,
237
252
  the describe/completion facets, and the live `observedType`/`schemaType`
238
- producers owned with #225. REST and native-api cells advance only when their
253
+ producers (#225). REST and native-api cells advance only when their
239
254
  applicable examples pass on CHR via `bun run test:integration`.
240
255
 
241
256
  Open offline follow-ups remain explicit but do not reopen the verified baseline.
242
257
  Issue #211 made an unlisted absolute bare path abstain (rule F, R14); rule
243
- P's closed-world reading is still not adopted (#372). #272 owns
244
- non-authoritative `.scratch/` citation cleanup. #350 (the broad optional `=`,
245
- schema-dependent and so belonging to step 3), #351 (an unresolvable menu that
246
- abstains at 2-deep nesting but hard-fails at 3-deep) and #355 (`'` is not a
247
- RouterOS token — a false ACCEPT the corpus join cannot see) are bounded analyzer
248
- defects that step 2 now surfaces through the `execute`/`api` gate. #336, #342 and #498 are complete: the
258
+ P's closed-world reading is still not adopted (#372). The non-authoritative
259
+ scratch citation cleanup is complete (#272, shipped in #568). Issues #350 (the broad
260
+ optional `=`), #351 (a relative submenu brace read as a command operand)
261
+ and #355 (`'` is not a RouterOS token) are complete. A submenu block after a navigation statement
262
+ (`/ip⏎address { … }`) keeps the pre-#351 reading; see
263
+ `commands/explain/README.md`. #336, #342 and #498 are complete: the
249
264
  corpus is pinned to schema 5, with one recorded session per release channel
250
265
  (long-term 7.23.8, stable 7.24.5, testing 7.25rc2, development 7.26beta1) read
251
266
  through `scripts/corpus-db.ts`. The highlight slice is cut from those sessions
@@ -0,0 +1,10 @@
1
+ # `cdb/encrypt-failed`
2
+
3
+ The existing CDB could not be rewritten with encryption.
4
+
5
+ ## Fix
6
+
7
+ Check directory permissions and available disk space, then retry
8
+ `centrs devices encrypt`. Preserve the original file and any timestamped backups
9
+ before recovery. A failed write may already have created a plaintext backup;
10
+ encryption does not protect that backup.
@@ -1,9 +1,10 @@
1
1
  # `cdb/password-required`
2
2
 
3
- The CDB is encrypted; a --cdb-password is required to read it.
3
+ A CDB password is required for decryption or explicit encryption.
4
4
 
5
5
  ## Fix
6
6
 
7
- See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) for the centrs error
8
- contract. This stub will be expanded with the typical trigger and remediation
9
- for `cdb/password-required`.
7
+ Pass `--cdb-password` or set `CENTRS_CDB_PASSWORD` to the CDB password.
8
+ `devices init --encrypt` and `devices encrypt` require a non-empty password,
9
+ even when the file is currently open. A password alone never implicitly
10
+ converts an existing open CDB; use `devices encrypt` explicitly.
@@ -0,0 +1,11 @@
1
+ # `cdb/plaintext-backups`
2
+
3
+ CDB backups may still contain plaintext credentials after encryption.
4
+
5
+ ## Fix
6
+
7
+ Apply your own secure backup and retention policy to the `.bak.*` files beside
8
+ the CDB. `devices encrypt` protects only the active CDB and retains every backup,
9
+ including the plaintext snapshot created during conversion. It does not encrypt
10
+ or delete old copies. This warning also accompanies already-encrypted no-op runs,
11
+ since prior backup protection cannot be inferred from the active file.
@@ -22,9 +22,11 @@ re-proves it (CMR15 in `commands/execute/examples.md`):
22
22
  | `read,test,api,rest-api` | `not enough permissions (9)` | `run-script` wrote to the controller's own row; `/cmr/device/reboot` rebooted a client |
23
23
 
24
24
  MikroTik [says](https://forum.mikrotik.com/t/7-26beta-development-is-released/273582/139) this is fixed for 7.26beta2: a `read` group will not
25
- be allowed to do it. That has not been checked on a beta2 build yet. centrs
26
- still warns on any version, because the warning reads the login's policy, not
27
- the RouterOS version. On a build that refuses the write, the members fail
25
+ be allowed to do it. #527 now carries direct 7.26beta3 evidence that a
26
+ restricted caller is refused, with the first refusing release still
27
+ unbracketed. centrs still warns on any version, because the warning reads
28
+ the login's policy, not the RouterOS version. On a build that refuses the
29
+ write, the members fail
28
30
  instead and the warning only says what the login lacks.
29
31
 
30
32
  `context` names the login (`username`, `group`), the policies it lacks
@@ -1,9 +1,31 @@
1
1
  # `input/invalid-routeros-path`
2
2
 
3
- The RouterOS path is malformed (it must be slash-prefixed).
3
+ The RouterOS path is malformed: not slash-prefixed, a segment that is not a
4
+ menu name, or characters a REST URL would reinterpret.
5
+
6
+ ## Typical triggers
7
+
8
+ - A path without a leading slash: `retrieve <router> ip/address`.
9
+ - A `retrieve` or `api` path segment that is not a RouterOS menu name. Menu
10
+ names are letters, digits and hyphens (case folds) (`/interface/6to4`,
11
+ `/ip/dhcp-server`). The check runs on every transport and even with
12
+ `--validate=false`, because a read must only dispatch a read (#586).
13
+ - A REST request path containing `?`, `#`, `\`, a control character, or a
14
+ `.`/`..` segment, including percent-encoded forms such as `%2e%2e`. The
15
+ URL parser would turn these into a query, a fragment
16
+ or a different path, so the request would not reach the path as written.
17
+ Before this guard, `retrieve '/system/reboot#' --no-validate` posted
18
+ `/rest/system/reboot`.
19
+
20
+ `context.path` names the path; `context.invalidSegments` lists the segments
21
+ that failed the menu grammar.
4
22
 
5
23
  ## Fix
6
24
 
25
+ - Write the path as slash-separated menu names: `/ip/address`,
26
+ `/system/resource`.
27
+ - Select rows with `--query` (for example `--query 'name="a#b"'`), not with
28
+ characters in the path.
29
+
7
30
  See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) for the centrs error
8
- contract. This stub will be expanded with the typical trigger and remediation
9
- for `input/invalid-routeros-path`.
31
+ contract.
@@ -0,0 +1,24 @@
1
+ # `routeros/permission-denied`
2
+
3
+ RouterOS refused the command because the user's group lacks a policy it needs.
4
+ The router answered, and a retry gives the same result. A single refused
5
+ command did not run. A multi-statement script run through `/execute` can be
6
+ partly applied: statements before the refused one already ran.
7
+
8
+ ## Typical RouterOS strings
9
+
10
+ - `not enough permissions (9)`: the command needs a policy the group does not
11
+ have, for example a `set` without `write`.
12
+ - `std failure: not allowed (9)`: on REST before RouterOS 7.26beta3, every
13
+ request fails this way (a plain GET included) when the group has `rest-api`
14
+ but not `api`. 7.26beta3 fixed it.
15
+
16
+ Over REST both arrive as HTTP 500 with the string in `detail`; over the native
17
+ API as a `!trap`. Grounded on CHR 7.23.8, 7.25, 7.26beta1 and 7.26beta3.
18
+
19
+ ## Fix
20
+
21
+ Grant the missing policy to the user's group in `/user/group`: `read` for
22
+ reads, `write` for changes. Before 7.26beta3 a REST user needs both `api` and
23
+ `rest-api`. Avoid the built-in `read` group for a least-privilege user: it also
24
+ carries `reboot`, `sensitive`, `password` and others.
@@ -0,0 +1,22 @@
1
+ # `transport/edge-auth-failed`
2
+
3
+ An HTTP layer in front of RouterOS (auth proxy, tunnel edge, WAF) refused the
4
+ request before RouterOS answered it.
5
+
6
+ ## How centrs tells
7
+
8
+ RouterOS answers every REST auth refusal with a JSON body whose `error` equals
9
+ the HTTP status (`{"error":401,"message":"Unauthorized"}`), adding
10
+ `WWW-Authenticate: Basic` only when no credentials were sent. A 401 or 403 with
11
+ any other body, or a challenge for another scheme (Cloudflare Access sends
12
+ `Bearer`), came from something in front of the router. `context` records the
13
+ challenge scheme, `server`, content type and whether a `cf-ray` header was
14
+ present. A `cf-ray` header alone does not decide it: a tunnel passes RouterOS's
15
+ own 401 through with one.
16
+
17
+ ## Fix
18
+
19
+ centrs sends only RouterOS Basic credentials, so changing the RouterOS username
20
+ or password will not help. Point `--host` at an address that reaches RouterOS
21
+ directly, or at a local proxy that authenticates to the front layer for you.
22
+ The request never reached RouterOS, so a write was not applied.
@@ -9,6 +9,9 @@ whether the write happened.
9
9
 
10
10
  - A connection closed or reset after an `execute` or `api` write was sent.
11
11
  - A REST 5xx or a timeout on a write.
12
+ - A multi-statement `execute` or `api /execute` script refused with
13
+ `routeros/permission-denied`: statements before the refused one may already
14
+ have run (#549). A single refused statement did not run and warns nothing.
12
15
 
13
16
  centrs never retries a write that it has sent, in single-target or fan-out
14
17
  mode, because replaying an `add`, a script run or a counter change would apply
@@ -0,0 +1,20 @@
1
+ # `usage/invalid-cursor`
2
+
3
+ An explain --cursor is not a byte offset inside the input being analyzed.
4
+
5
+ ## Typical trigger
6
+
7
+ `centrs explain <router> '<input>' --complete --cursor <byte>` with a value that
8
+ is not a whole number, or that is larger than the input's UTF-8 byte length.
9
+ Library callers get the same code for an original-document position
10
+ (`{ line, character }`) that addresses no character in the input.
11
+
12
+ The check runs before any connection is made, so nothing is sent to the router.
13
+
14
+ ## Fix
15
+
16
+ - Count the cursor in **UTF-8 bytes**, not characters: `é` is two bytes and
17
+ `🛰` is four. The end of the input (the default) is the input's byte length.
18
+ - A cursor inside a multi-byte character is accepted and moves back to that
19
+ character's first byte.
20
+ - Omit `--cursor` to complete or describe at the end of the input.
@@ -19,3 +19,12 @@ Two sources raise it:
19
19
 
20
20
  Check the spelling of the segment in `error.context.segment`. `centrs explain
21
21
  '<command>'` shows how the rest of the command reads offline.
22
+
23
+ One path is not a typo: `/system/routerboard` exists only on MikroTik
24
+ hardware, so `retrieve` answers its absence on CHR and x86 systems with a
25
+ targeted remediation instead of the generic one. For the device's license
26
+ ID, read `/system/license` (`system-id` on CHR, `software-id` on x86):
27
+
28
+ ```bash
29
+ centrs retrieve <router> /system/license
30
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tikoci/centrs",
3
- "version": "0.1.9-next.23",
3
+ "version": "0.1.9-next.27",
4
4
  "main": "src/index.ts",
5
5
  "module": "src/index.ts",
6
6
  "repository": {
@@ -12,18 +12,18 @@
12
12
  "packageManager": "bun@1.4.2",
13
13
  "license": "MIT",
14
14
  "devDependencies": {
15
- "@biomejs/biome": "2.5.15",
15
+ "@biomejs/biome": "2.6.3",
16
16
  "@secretlint/secretlint-rule-preset-recommend": "^13.0.7",
17
17
  "@types/bun": "1.4.2",
18
18
  "cloc": "^2.6.0-cloc",
19
19
  "cspell": "^10.3.6",
20
20
  "has-flag": "4.0.0",
21
- "markdownlint-cli2": "^0.23.3",
21
+ "markdownlint-cli2": "^0.24.0",
22
22
  "secretlint": "^13.0.7",
23
23
  "typedoc": "^0.28.20"
24
24
  },
25
25
  "optionalDependencies": {
26
- "@tikoci/quickchr": "^0.4.8"
26
+ "@tikoci/quickchr": "^0.5.2"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "typescript": "^6.0.3"
@@ -40,7 +40,7 @@
40
40
  "bin": {
41
41
  "centrs": "src/cli.ts"
42
42
  },
43
- "description": "All-in-one TypeScript library/CLI/TUI/MCP for interacting with MikroTik RouterOS devices",
43
+ "description": "All-in-one TypeScript library/CLI/MCP for interacting with MikroTik RouterOS devices (TUI stub)",
44
44
  "engines": {
45
45
  "bun": ">=1.3.11 <2"
46
46
  },
@@ -118,6 +118,8 @@
118
118
  "explain:operator-readme:check": "bun run scripts/explain-operator-slice.ts --readme --check",
119
119
  "explain:operator-slice": "bun run scripts/explain-operator-slice.ts",
120
120
  "explain:probe:brace-slots": "bun run scripts/probes/explain-brace-slot-sweep.ts",
121
+ "explain:probe:context-matrix": "bun run scripts/probes/explain-context-matrix.ts",
122
+ "explain:probe:corrected-anchors": "bun run scripts/probes/explain-corrected-anchors.ts",
121
123
  "explain:probe:escapes": "bun run scripts/probes/explain-escape-sweep.ts",
122
124
  "explain:probe:highlight-recapture": "bun run scripts/probes/explain-highlight-recapture.ts",
123
125
  "explain:probe:operators": "bun run scripts/probes/explain-operator-sweep.ts",
@@ -125,6 +127,8 @@
125
127
  "explain:probe:symbol-arms-score": "bun run scripts/probes/explain-symbol-arms-score.ts",
126
128
  "explain:probe:symbol-bad-sigil-replay": "bun run scripts/probes/explain-symbol-bad-sigil-replay.ts",
127
129
  "explain:probe:symbol-classes-replay": "bun run scripts/probes/explain-symbol-classes-replay.ts",
130
+ "explain:probe:import-blocker-validity": "bun run scripts/probes/explain-import-blocker-validity.ts",
131
+ "explain:probe:import-dry-run": "bun run scripts/probes/explain-import-dry-run.ts",
128
132
  "explain:probe:version-floors": "bun run scripts/probes/explain-version-floors.ts",
129
133
  "explain:menus:check": "bun run scripts/gen-explain-menus.ts --check",
130
134
  "explain:value-census": "bun run scripts/explain-value-census.ts",
package/src/api-fanout.ts CHANGED
@@ -62,6 +62,7 @@ import {
62
62
  import { promptForWriteConfirmation } from "./execute.ts";
63
63
  import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
64
64
  import {
65
+ assertNoMatchInFanout,
65
66
  emptyWriteSelectionError,
66
67
  expandSelection,
67
68
  isDefaultRecordTarget,
@@ -213,6 +214,7 @@ export async function apiFanout(
213
214
  // missing endpoint or an invalid `-X` is an outer error regardless of how many
214
215
  // targets the selection resolves to (so an empty selection cannot mask it).
215
216
  validateApiRequestShape(request);
217
+ assertNoMatchInFanout(request.match);
216
218
  const config = await loadEnvFileDefaults(env);
217
219
  const method = parseApiMethod(request.method);
218
220
  const requestSummary = apiRequestSummaryFromRequest(request, env);