@tikoci/centrs 0.1.9-next.22 → 0.1.9-next.24
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 +106 -0
- package/GLOSSARY.txt +38 -0
- package/README.md +2 -2
- package/commands/api/AGENTS.md +18 -1
- package/commands/api/README.md +10 -6
- package/commands/api/examples.md +1 -1
- package/commands/execute/README.md +26 -6
- package/commands/execute/examples.md +43 -5
- package/commands/explain/README.md +873 -182
- package/commands/explain/examples.md +447 -38
- package/commands/retrieve/README.md +13 -2
- package/commands/retrieve/examples.md +30 -1
- package/commands/transfer/README.md +8 -4
- package/docs/CLI.md +19 -5
- package/docs/CONSTITUTION.md +18 -9
- package/docs/MATRIX.md +51 -26
- package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
- package/docs/errors/routeros/permission-denied.md +24 -0
- package/docs/errors/transport/edge-auth-failed.md +22 -0
- package/docs/errors/transport/write-outcome-unknown.md +3 -0
- package/docs/errors/usage/invalid-cursor.md +20 -0
- package/docs/errors/validation/unknown-path.md +9 -0
- package/package.json +5 -2
- package/src/api.ts +34 -39
- package/src/cli/explain.ts +206 -47
- package/src/core/envelope.ts +10 -3
- package/src/core/error-catalog.ts +15 -0
- package/src/core/execute-command.ts +53 -0
- package/src/core/fanout.ts +12 -2
- package/src/core/inspect.ts +4 -5
- package/src/core/routeros-errors.ts +25 -0
- package/src/execute-cmr.ts +2 -8
- package/src/execute.ts +48 -20
- package/src/explain/args.ts +181 -15
- package/src/explain/blocks.ts +3 -4
- package/src/explain/brace-slots.ts +5 -0
- package/src/explain/catalog.ts +13 -6
- package/src/explain/defects.ts +1 -0
- package/src/explain/group-faults.ts +1060 -0
- package/src/explain/highlight-projection.ts +10 -8
- package/src/explain/operators.ts +22 -20
- package/src/explain/pathresolve.ts +102 -20
- package/src/explain/quoted-string.ts +41 -6
- package/src/explain/run-tokens.ts +223 -0
- package/src/explain/scope-brace.ts +319 -7
- package/src/explain/segment.ts +177 -4
- package/src/explain/spaced-args.ts +116 -0
- package/src/explain/symbols.ts +163 -7
- package/src/explain/transport.ts +13 -0
- package/src/explain/values.ts +55 -4
- package/src/explain/verbsplit.ts +14 -188
- package/src/explain/write.ts +8 -1
- package/src/explain-live-facets.ts +739 -0
- package/src/explain-live-types.ts +490 -0
- package/src/explain-live.ts +593 -0
- package/src/explain.ts +772 -60
- package/src/index.ts +6 -0
- package/src/offline-gate.ts +30 -9
- package/src/protocols/adapter.ts +88 -10
- package/src/protocols/btest-session.ts +30 -10
- package/src/protocols/mac-telnet-console.ts +16 -0
- package/src/protocols/mac-telnet.ts +38 -10
- package/src/protocols/native-api.ts +13 -12
- package/src/retrieve-follow.ts +2 -1
- package/src/retrieve-sample.ts +2 -1
- package/src/retrieve-wait.ts +10 -6
- package/src/retrieve.ts +77 -27
- package/src/transfer.ts +38 -13
|
@@ -96,7 +96,7 @@ size-aware. Grounding: [Files](https://help.mikrotik.com/docs/spaces/ROS/pages/2
|
|
|
96
96
|
| `rest` / `native` | rest-api / native-api | small via `/file/get … contents` (**≤ 60 KB**); **large via chunked `/file/read offset chunk-size≤32768`** | **`/file/set contents=` ≤ 60 KB only** — no chunked write |
|
|
97
97
|
| `sftp` (default secure) | ssh | any size | any size |
|
|
98
98
|
| `scp` | ssh | any size | any size |
|
|
99
|
-
| `fetch` *(deferred)* | rest-api/native-api + inbound HTTP | router
|
|
99
|
+
| `fetch` *(deferred, upload-only)* | rest-api/native-api + inbound HTTP | chunked `/file/read` (router PUT fails: `only [s]ftp modes support upload`, #548) | router GETs from centrs (`url=http://centrs/… dst-path=…`) |
|
|
100
100
|
| `ftp` *(gated)* | ftp | any size | any size |
|
|
101
101
|
|
|
102
102
|
Auto-selection (no `--via`):
|
|
@@ -302,6 +302,10 @@ gate as in `retrieve` / `execute`; RouterOS's own menu errors (missing remote
|
|
|
302
302
|
file, permission denied) map to `routeros/*`, and SFTP/SCP/fetch errors map to
|
|
303
303
|
`transport/*` and `auth/*`.
|
|
304
304
|
|
|
305
|
+
`meta.validation` reports that existence probe as one `device` stage. `list`
|
|
306
|
+
runs no probe, so it is not a gate: its `meta.validation` is
|
|
307
|
+
`{ enabled: false }`, on success and on failure (#535).
|
|
308
|
+
|
|
305
309
|
A failure **after** target resolution keeps what resolution found: the error
|
|
306
310
|
envelope carries the resolved `meta.target` (host, port, identity, sources),
|
|
307
311
|
`meta.via`, the setting sources in `meta.settings`, and `meta.operation`
|
|
@@ -371,9 +375,9 @@ done rule. The first coded pass targets `rest`/`native` (no SSH dependency);
|
|
|
371
375
|
(random URL token, bound to the interface facing the router) and drives the
|
|
372
376
|
router with `/tool/fetch` over `rest`/`native`. **upload**: serve the local
|
|
373
377
|
file, `fetch url=http://<advertise>/<token> dst-path=<remote>` → router GETs.
|
|
374
|
-
**download
|
|
375
|
-
|
|
376
|
-
|
|
378
|
+
**download** stays on chunked `/file/read`: router PUTs fail with `only
|
|
379
|
+
[s]ftp modes support upload` (#548), and HTTP `http-data` caps at 60000
|
|
380
|
+
bytes, so the PUT half cannot carry files. Open questions: advertise-host
|
|
377
381
|
auto-detection behind NAT, HTTP-vs-HTTPS + `check-certificate`, and whether to
|
|
378
382
|
add fetch's `user`/`password` on top of the URL token.
|
|
379
383
|
- **ssh** — `sftp` is **built** (`src/protocols/sftp.ts`, host OpenSSH `sftp`
|
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
|
|
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). |
|
|
@@ -220,18 +220,32 @@ Usage: centrs terminal <router> [flags] | centrs terminal --host <host> [flags]
|
|
|
220
220
|
|
|
221
221
|
## explain
|
|
222
222
|
|
|
223
|
-
Analyze
|
|
223
|
+
Analyze RouterOS input offline or add read-only device evidence from one router.
|
|
224
224
|
|
|
225
225
|
```text
|
|
226
|
-
Usage: centrs explain '<input>' [flags]
|
|
226
|
+
Usage: centrs explain [<router>] '<input>' [flags]
|
|
227
227
|
```
|
|
228
228
|
|
|
229
229
|
| Flag | Value | Description |
|
|
230
230
|
| ---- | ----- | ----------- |
|
|
231
|
+
| `--via` | <rest-api\|native-api> | Pin the transport; no silent downgrade. Default rest-api. |
|
|
232
|
+
| `--host` | <host\|url> | Host or base URL to dial. With no `<router>` it is the target (no CDB lookup); with one it overrides the resolved host. |
|
|
233
|
+
| `--port` | `<port>` | Override the resolved management port. |
|
|
234
|
+
| `--username / --user / -u` | `<name>` | RouterOS username. Falls back to `CENTRS_USERNAME`. |
|
|
235
|
+
| `--password` | `<secret>` | RouterOS password. Falls back to `CENTRS_PASSWORD`. |
|
|
236
|
+
| `--insecure` | | Accept a self-signed `api-ssl`/REST TLS cert. Default verifies. |
|
|
237
|
+
| `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
|
|
238
|
+
| `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
|
|
239
|
+
| `--resolve` | <none\|arp> | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
|
|
240
|
+
| `--timeout` | `<duration>` | Per-request timeout (for REST, max 60s). |
|
|
241
|
+
| `-p` | `<secret>` | Alias for --password. |
|
|
242
|
+
| `--quickchr` | `<name>` | Resolve one running quickchr VM by name. |
|
|
231
243
|
| `--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
244
|
| `--fail-on` | <error\|warning\|never> | Exit 2 when the verdict reaches this severity. Default `error`; an ambiguous or unknown statement is a warning, never an error. |
|
|
233
|
-
| `--complete` | |
|
|
234
|
-
| `--schema` | |
|
|
245
|
+
| `--complete` | | Candidates at the cursor: menus and verbs, argument names, or values. Live evidence — offline emits a tip and enumerates nothing. |
|
|
246
|
+
| `--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. |
|
|
247
|
+
| `--cursor` | `<byte>` | UTF-8 byte offset for --complete/--schema. Default: the end of the input. |
|
|
248
|
+
| `--full` | | Lift --complete/--schema sizing to its safety ceiling (1024 names, 128 expansion probes). |
|
|
235
249
|
| `--curl` | | Render a ready-to-edit REST curl for statements covered by a runtime-tested mapping rule. |
|
|
236
250
|
| `--tokens` | | Emit the total, gapless token partition behind `data.tokens[]` (every byte not claimed by an analyzer is `unclassified`). |
|
|
237
251
|
| `--format` | <text\|json\|yaml> | Output format for the CLI response. |
|
package/docs/CONSTITUTION.md
CHANGED
|
@@ -58,6 +58,10 @@ is unsafe the offline stage is what keeps the gate from degrading to nothing. A
|
|
|
58
58
|
clean offline pass is **necessary, never sufficient** — `explain` publishes
|
|
59
59
|
`runtimeAcceptance: "not-proven"` for exactly this reason. An offline `warn` is
|
|
60
60
|
an ABSTENTION and passes through to the device stage; only a `fail` rejects.
|
|
61
|
+
The one exception is `discarded-close`: a stray top-level `}` or `]` that the
|
|
62
|
+
device accepts and then silently stops reading at (#504). It is a warning,
|
|
63
|
+
because nothing is rejected, and the gate refuses it by code
|
|
64
|
+
(`src/offline-gate.ts`), because the device would run a different script.
|
|
61
65
|
|
|
62
66
|
`meta.validation.stages[]` reports what each stage concluded, including a stage
|
|
63
67
|
that was skipped and why. A reader must always be able to tell "the device
|
|
@@ -69,15 +73,20 @@ gate passed keeps `passed`. When something stopped the gate before it gave a
|
|
|
69
73
|
verdict (a `transport/*` or `auth/*` failure, a device error that is not a
|
|
70
74
|
gate answer, or a refusal before the gate ran), `result` and the
|
|
71
75
|
`syntax`/`semantic` summaries are absent. Each stage the gate did not reach is
|
|
72
|
-
then `skipped` with `reason: "not reached: <error.code>"` (GH#435).
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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`.
|
|
81
90
|
|
|
82
91
|
`--validate=false` disables **both** stages — one flag, one meaning. A bypass
|
|
83
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 | `
|
|
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 `
|
|
105
|
-
(
|
|
106
|
-
|
|
107
|
-
|
|
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,17 +186,35 @@ Wire, auth, TCP/UDP, and CI caveats live in `commands/btest/README.md` and the
|
|
|
182
186
|
|
|
183
187
|
## Current priority
|
|
184
188
|
|
|
185
|
-
|
|
186
|
-
|
|
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 context grounding.
|
|
193
|
+
|
|
194
|
+
**First: `explain`** (maintainer decision 2026-10-16 on
|
|
195
|
+
[#90](https://github.com/tikoci/centrs/issues/90)). In order: make the
|
|
196
|
+
published offline readings honest (#350, #211; complete), give the result a
|
|
197
|
+
statement tree (#497; complete), then the live probe foundation (#491;
|
|
198
|
+
complete). Those probes grounded #351, #358 and #371 (complete), #492
|
|
199
|
+
added the describe/completion facets, and #225 the live value types. The
|
|
200
|
+
closeout of #236 promoted both live cells (complete). The
|
|
201
|
+
plan, and what is deliberately out of this run, is the 2026-10-16 plan
|
|
202
|
+
comment on #90. It is step 3 of the validation track below.
|
|
203
|
+
|
|
204
|
+
**The CMR track is paused** (maintainer decision 2026-10-16; held for the
|
|
205
|
+
7.26beta caller-policy fix — #527 now carries direct 7.26beta3 evidence that a
|
|
206
|
+
restricted caller is refused, with the first refusing release still
|
|
207
|
+
unbracketed). It was first from the 2026-10-12 decision on
|
|
208
|
+
[#441](https://github.com/tikoci/centrs/issues/441). Done: `execute --cmr`
|
|
187
209
|
(#450), which runs one validated command on CMR clients through their
|
|
188
210
|
controller. The contract is in
|
|
189
211
|
[`commands/execute/README.md` → CMR](../commands/execute/README.md#cmr-clients-through-their-controller---cmr),
|
|
190
212
|
with examples CMR0–CMR15. Also done: the fleet blast-radius gate for
|
|
191
213
|
controller fleet commands run with plain `execute` or `api` (#453), with
|
|
192
214
|
examples CMR16–CMR18 and api CMR1. The feature map of CMR against centrs is
|
|
193
|
-
on #441 (2026-10-13).
|
|
194
|
-
of members and a default controller). Deferred: CDB linkage
|
|
195
|
-
|
|
215
|
+
on #441 (2026-10-13). When it resumes, next is the read-only slice of #449 (a
|
|
216
|
+
`devices` view of members and a default controller). Deferred: CDB linkage
|
|
217
|
+
(#449) and `retrieve --cmr` (#441).
|
|
196
218
|
|
|
197
219
|
**The native-api / NDJSON line** (maintainer decision on
|
|
198
220
|
[#396](https://github.com/tikoci/centrs/issues/396), 2026-10-07): agents in the
|
|
@@ -213,11 +235,11 @@ order agreed on [#236](https://github.com/tikoci/centrs/issues/236):
|
|
|
213
235
|
2. **Done** — front the `execute` / `api` device preflight with the offline
|
|
214
236
|
analyzer (#354). The gate is now two-stage; the contract is in
|
|
215
237
|
[`docs/CONSTITUTION.md` → Validation is the product](CONSTITUTION.md#validation-is-the-product).
|
|
216
|
-
3. **
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
[
|
|
238
|
+
3. **Done** — **live `explain` (#236)**, as stage two of a gate that now exists.
|
|
239
|
+
The contract is [`commands/explain/README.md`](../commands/explain/README.md)
|
|
240
|
+
and the live examples in
|
|
241
|
+
[`commands/explain/examples.md`](../commands/explain/examples.md);
|
|
242
|
+
[#90](https://github.com/tikoci/centrs/issues/90) indexes what follows.
|
|
221
243
|
|
|
222
244
|
The offline baseline stays `verified`: #264 closed the measured token vocabulary
|
|
223
245
|
and #322 closed the final bounded-execution gap, including the public depth-axis
|
|
@@ -226,20 +248,23 @@ guard.
|
|
|
226
248
|
The live pass is the device-backed cross-check on the offline result, not a
|
|
227
249
|
replacement for it. It adds read-only `/console/inspect` and `:parse` evidence,
|
|
228
250
|
the describe/completion facets, and the live `observedType`/`schemaType`
|
|
229
|
-
producers
|
|
251
|
+
producers (#225). REST and native-api cells advance only when their
|
|
230
252
|
applicable examples pass on CHR via `bun run test:integration`.
|
|
231
253
|
|
|
232
254
|
Open offline follow-ups remain explicit but do not reopen the verified baseline.
|
|
233
|
-
Issue #211
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
255
|
+
Issue #211 made an unlisted absolute bare path abstain (rule F, R14); rule
|
|
256
|
+
P's closed-world reading is still not adopted (#372). #272 owns
|
|
257
|
+
non-authoritative `.scratch/` citation cleanup. Issues #350 (the broad
|
|
258
|
+
optional `=`), #351 (a relative submenu brace read as a command operand)
|
|
259
|
+
and #355 (`'` is not a RouterOS token) are complete. A submenu block after a navigation statement
|
|
260
|
+
(`/ip⏎address { … }`) keeps the pre-#351 reading; see
|
|
261
|
+
`commands/explain/README.md`. #336, #342 and #498 are complete: the
|
|
262
|
+
corpus is pinned to schema 5, with one recorded session per release channel
|
|
263
|
+
(long-term 7.23.8, stable 7.24.5, testing 7.25rc2, development 7.26beta1) read
|
|
264
|
+
through `scripts/corpus-db.ts`. The highlight slice is cut from those sessions
|
|
265
|
+
and gated in CI, and the operator sweep is re-run on the same builds — plus
|
|
266
|
+
7.21.5 retained as an age anchor — so the operator axis and the token partition
|
|
267
|
+
are scored on the same firmware. The #335
|
|
243
268
|
source refresh pins both architectures of those versions beside the 7.10.2 /
|
|
244
269
|
7.16 history anchors and accepts the current CLI-Reference drift. Issue #223 is
|
|
245
270
|
a separate MCP response migration. If live evidence finds a wrong offline
|
|
@@ -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.
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
@@ -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.
|
|
3
|
+
"version": "0.1.9-next.24",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"repository": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"bin": {
|
|
41
41
|
"centrs": "src/cli.ts"
|
|
42
42
|
},
|
|
43
|
-
"description": "All-in-one TypeScript library/CLI/
|
|
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
|
},
|
|
@@ -103,11 +103,14 @@
|
|
|
103
103
|
"explain:catalog": "bun run scripts/gen-explain-catalog.ts",
|
|
104
104
|
"explain:catalog:check": "bun run scripts/gen-explain-catalog.ts --check",
|
|
105
105
|
"explain:corpus-census": "bun run scripts/explain-corpus-census.ts",
|
|
106
|
+
"explain:conformance": "bun run scripts/explain-conformance.ts",
|
|
107
|
+
"explain:conformance:check": "bun run scripts/explain-conformance.ts --check",
|
|
106
108
|
"explain:highlight-agreement": "bun run scripts/explain-highlight-agreement.ts",
|
|
107
109
|
"explain:highlight-agreement:check": "bun run scripts/explain-highlight-agreement.ts --check",
|
|
108
110
|
"explain:highlight-agreement:readme": "bun run scripts/explain-highlight-agreement.ts --readme",
|
|
109
111
|
"explain:highlight-agreement:readme:check": "bun run scripts/explain-highlight-agreement.ts --readme --check",
|
|
110
112
|
"explain:highlight-slice": "bun run scripts/explain-highlight-slice.ts",
|
|
113
|
+
"explain:highlight-slice:check": "bun run scripts/explain-highlight-slice.ts --check",
|
|
111
114
|
"explain:menus": "bun run scripts/gen-explain-menus.ts",
|
|
112
115
|
"explain:operator-census": "bun run scripts/explain-operator-census.ts",
|
|
113
116
|
"explain:operator-census:check": "bun run scripts/explain-operator-census.ts --check",
|
package/src/api.ts
CHANGED
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
promptForWriteConfirmation,
|
|
63
63
|
validateRouterOsScript,
|
|
64
64
|
} from "./execute.ts";
|
|
65
|
+
import { countExecutableStatements } from "./explain/segment.ts";
|
|
65
66
|
import {
|
|
66
67
|
assertOfflineSyntax,
|
|
67
68
|
isOfflineGateRejection,
|
|
@@ -541,7 +542,7 @@ async function* streamResolvedApi(
|
|
|
541
542
|
trace.validation = validation;
|
|
542
543
|
resolved = await withQueryBooleans(resolved, backend, result.queryMenu);
|
|
543
544
|
} else {
|
|
544
|
-
validation =
|
|
545
|
+
validation = disabledApiValidationMeta(resolved);
|
|
545
546
|
}
|
|
546
547
|
} catch (error) {
|
|
547
548
|
await backend.close();
|
|
@@ -824,16 +825,6 @@ function adapterForResolved(resolved: ResolvedApiRequest): ProtocolAdapter {
|
|
|
824
825
|
});
|
|
825
826
|
}
|
|
826
827
|
|
|
827
|
-
function disabledValidationMeta(): EnvelopeValidationMeta {
|
|
828
|
-
return {
|
|
829
|
-
enabled: false,
|
|
830
|
-
source: "disabled",
|
|
831
|
-
result: "skipped",
|
|
832
|
-
syntax: false,
|
|
833
|
-
semantic: false,
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
|
|
837
828
|
export async function resolveApiRequest(
|
|
838
829
|
request: ApiRequest,
|
|
839
830
|
env: Record<string, string | undefined>,
|
|
@@ -1347,7 +1338,8 @@ interface ApiValidationResult {
|
|
|
1347
1338
|
* validator is `/console/inspect`, never `:put [:parse]`. Path existence and
|
|
1348
1339
|
* add/set attribute validity both come from `request=child` — command-level
|
|
1349
1340
|
* `request=completion` is unsafe on RouterOS 7.12.2 (`core/inspect.ts`). A
|
|
1350
|
-
* `/execute` script is a CLI string
|
|
1341
|
+
* `/execute` script is a CLI string, so it has no `semantic` summary; a
|
|
1342
|
+
* structured request runs no `:parse`, so it has no `syntax` summary (#535).
|
|
1351
1343
|
*/
|
|
1352
1344
|
async function validateApiRequest(
|
|
1353
1345
|
resolved: ResolvedApiRequest,
|
|
@@ -1383,8 +1375,7 @@ async function validateApiRequest(
|
|
|
1383
1375
|
? "not-applicable"
|
|
1384
1376
|
: API_SCRIPT_DEVICE_STAGE_SOURCE,
|
|
1385
1377
|
result: "passed",
|
|
1386
|
-
syntax:
|
|
1387
|
-
semantic: "not-applicable",
|
|
1378
|
+
...(script !== undefined ? { syntax: true } : {}),
|
|
1388
1379
|
stages: [
|
|
1389
1380
|
{
|
|
1390
1381
|
stage: "offline",
|
|
@@ -1468,13 +1459,7 @@ async function validateApiRequest(
|
|
|
1468
1459
|
}
|
|
1469
1460
|
}
|
|
1470
1461
|
return {
|
|
1471
|
-
validation:
|
|
1472
|
-
enabled: true,
|
|
1473
|
-
source: "/console/inspect request=child",
|
|
1474
|
-
result: "passed",
|
|
1475
|
-
syntax: false,
|
|
1476
|
-
semantic: true,
|
|
1477
|
-
},
|
|
1462
|
+
validation: structuredValidationPassed(resolved),
|
|
1478
1463
|
tips,
|
|
1479
1464
|
queryMenu: isCommand ? `/${parent.join("/")}` : resolved.path,
|
|
1480
1465
|
};
|
|
@@ -1513,11 +1498,7 @@ async function validateApiRequest(
|
|
|
1513
1498
|
}
|
|
1514
1499
|
return {
|
|
1515
1500
|
validation: {
|
|
1516
|
-
|
|
1517
|
-
source: "/console/inspect request=child",
|
|
1518
|
-
result: "passed",
|
|
1519
|
-
syntax: false,
|
|
1520
|
-
semantic: true,
|
|
1501
|
+
...structuredValidationPassed(resolved),
|
|
1521
1502
|
availableAttributes: available,
|
|
1522
1503
|
},
|
|
1523
1504
|
tips,
|
|
@@ -1526,13 +1507,7 @@ async function validateApiRequest(
|
|
|
1526
1507
|
}
|
|
1527
1508
|
|
|
1528
1509
|
return {
|
|
1529
|
-
validation:
|
|
1530
|
-
enabled: true,
|
|
1531
|
-
source: "/console/inspect request=child",
|
|
1532
|
-
result: "passed",
|
|
1533
|
-
syntax: false,
|
|
1534
|
-
semantic: true,
|
|
1535
|
-
},
|
|
1510
|
+
validation: structuredValidationPassed(resolved),
|
|
1536
1511
|
tips,
|
|
1537
1512
|
queryMenu: resolved.path,
|
|
1538
1513
|
};
|
|
@@ -1857,6 +1832,12 @@ export function buildApiErrorEnvelopeFromResolved(
|
|
|
1857
1832
|
trace?: ValidationTrace,
|
|
1858
1833
|
): ApiErrorEnvelope {
|
|
1859
1834
|
const centrsError = asApiError(error);
|
|
1835
|
+
// A structured request is one operation; only a `/execute` script can
|
|
1836
|
+
// carry several statements into one dispatch (#549), at the top level or
|
|
1837
|
+
// inside an executed control scope.
|
|
1838
|
+
const script = resolved.scriptMode ? scriptOf(resolved) : undefined;
|
|
1839
|
+
const multiStatement =
|
|
1840
|
+
script !== undefined && countExecutableStatements(script) > 1;
|
|
1860
1841
|
return {
|
|
1861
1842
|
ok: false,
|
|
1862
1843
|
error: serializeCentrsError(centrsError),
|
|
@@ -1866,6 +1847,7 @@ export function buildApiErrorEnvelopeFromResolved(
|
|
|
1866
1847
|
isApiMutating(resolved.method, resolved.path),
|
|
1867
1848
|
centrsError,
|
|
1868
1849
|
trace,
|
|
1850
|
+
multiStatement,
|
|
1869
1851
|
),
|
|
1870
1852
|
],
|
|
1871
1853
|
tips: [],
|
|
@@ -1877,6 +1859,22 @@ export function buildApiErrorEnvelopeFromResolved(
|
|
|
1877
1859
|
};
|
|
1878
1860
|
}
|
|
1879
1861
|
|
|
1862
|
+
/** A structured request the device stage passed; stage 1 does not apply to it. */
|
|
1863
|
+
function structuredValidationPassed(
|
|
1864
|
+
resolved: ResolvedApiRequest,
|
|
1865
|
+
): EnvelopeValidationMeta {
|
|
1866
|
+
return {
|
|
1867
|
+
enabled: true,
|
|
1868
|
+
source: API_DEVICE_STAGE_SOURCE,
|
|
1869
|
+
result: "passed",
|
|
1870
|
+
semantic: true,
|
|
1871
|
+
stages: [
|
|
1872
|
+
offlineApiStage(resolved),
|
|
1873
|
+
{ stage: "device", source: API_DEVICE_STAGE_SOURCE, result: "passed" },
|
|
1874
|
+
],
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1880
1878
|
/**
|
|
1881
1879
|
* `meta.validation` for an api error envelope. A stage-1 rejection opened no
|
|
1882
1880
|
* connection, so the meta must not report `/console/inspect` as the validator
|
|
@@ -1896,7 +1894,6 @@ function failedApiValidationMeta(
|
|
|
1896
1894
|
source: OFFLINE_GATE_SOURCE,
|
|
1897
1895
|
result: "failed",
|
|
1898
1896
|
syntax: false,
|
|
1899
|
-
semantic: "not-applicable",
|
|
1900
1897
|
stages: [
|
|
1901
1898
|
{ stage: "offline", source: OFFLINE_GATE_SOURCE, result: "failed" },
|
|
1902
1899
|
{
|
|
@@ -1911,14 +1908,14 @@ function failedApiValidationMeta(
|
|
|
1911
1908
|
// Only a `validation/*` error identifies the device stage as the rejecter.
|
|
1912
1909
|
// Everything downstream of a passed gate — a RouterOS fault on the request,
|
|
1913
1910
|
// a transport drop — is served by `trace.validation` before this function is
|
|
1914
|
-
// ever reached.
|
|
1911
|
+
// ever reached. A script is refused by `:parse` (syntax); a structured
|
|
1912
|
+
// request by `/console/inspect` (names) (#535).
|
|
1915
1913
|
if (error.code.startsWith("validation/")) {
|
|
1916
1914
|
return {
|
|
1917
1915
|
enabled: true,
|
|
1918
1916
|
source: apiDeviceStageSource(resolved),
|
|
1919
1917
|
result: "failed",
|
|
1920
|
-
syntax: false,
|
|
1921
|
-
semantic: resolved.scriptMode ? "not-applicable" : false,
|
|
1918
|
+
...(resolved.scriptMode ? { syntax: false } : { semantic: false }),
|
|
1922
1919
|
stages: [offlineApiStage(resolved, trace), deviceStageFailed(resolved)],
|
|
1923
1920
|
};
|
|
1924
1921
|
}
|
|
@@ -2012,8 +2009,6 @@ function disabledApiValidationMeta(
|
|
|
2012
2009
|
enabled: false,
|
|
2013
2010
|
source: "disabled",
|
|
2014
2011
|
result: "skipped",
|
|
2015
|
-
syntax: false,
|
|
2016
|
-
semantic: resolved.scriptMode ? "not-applicable" : false,
|
|
2017
2012
|
stages: [
|
|
2018
2013
|
{ stage: "offline", source: OFFLINE_GATE_SOURCE, result: "skipped" },
|
|
2019
2014
|
{
|