@tikoci/centrs 0.1.6 → 0.1.8-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +342 -0
- package/GLOSSARY.txt +32 -0
- package/README.md +10 -6
- package/SECURITY.md +1 -1
- package/commands/AGENTS.md +2 -1
- package/commands/api/AGENTS.md +55 -30
- package/commands/api/README.md +135 -42
- package/commands/api/examples.md +275 -19
- package/commands/devices/README.md +21 -4
- package/commands/devices/examples.md +33 -2
- package/commands/execute/README.md +60 -18
- package/commands/execute/examples.md +224 -0
- package/commands/explain/README.md +42 -5
- package/commands/mcp/README.md +2 -1
- package/commands/retrieve/README.md +348 -4
- package/commands/retrieve/examples.md +442 -5
- package/commands/terminal/README.md +14 -4
- package/commands/terminal/examples.md +22 -3
- package/commands/transfer/README.md +55 -9
- package/commands/transfer/examples.md +106 -7
- package/docs/CLI.md +29 -21
- package/docs/CONSTITUTION.md +41 -16
- package/docs/MATRIX.md +38 -1
- package/docs/errors/README.md +3 -1
- package/docs/errors/cdb/create-failed.md +20 -0
- package/docs/errors/cdb/file-exists.md +23 -0
- package/docs/errors/input/invalid-query.md +36 -0
- package/docs/errors/input/invalid-target.md +25 -3
- package/docs/errors/input/local-destination.md +20 -0
- package/docs/errors/input/unsupported-query.md +19 -0
- package/docs/errors/internal/byte-string.md +13 -0
- package/docs/errors/routeros/invalid-value.md +3 -2
- package/docs/errors/settings/invalid-timeout.md +6 -0
- package/docs/errors/transport/cancel-unacknowledged.md +24 -0
- package/docs/errors/transport/stream-overflow.md +21 -0
- package/docs/errors/usage/removed-flag.md +20 -0
- package/docs/errors/validation/menu-unavailable.md +22 -0
- package/docs/errors/validation/not-followable.md +23 -0
- package/docs/errors/validation/package-missing.md +29 -0
- package/docs/errors/validation/unknown-path.md +16 -4
- package/docs/errors/wait/deadline-exceeded.md +14 -0
- package/docs/errors/wait/interrupted.md +10 -0
- package/package.json +14 -11
- package/src/absent-menu.ts +280 -0
- package/src/api-fanout.ts +22 -12
- package/src/api.ts +603 -194
- package/src/btest.ts +19 -17
- package/src/cli/api.ts +126 -34
- package/src/cli/devices.ts +16 -3
- package/src/cli/execute.ts +18 -5
- package/src/cli/retrieve.ts +216 -22
- package/src/cli/terminal.ts +10 -5
- package/src/cli/transfer.ts +23 -8
- package/src/core/envelope.ts +14 -0
- package/src/core/error-catalog.ts +70 -1
- package/src/core/execute-command.ts +313 -6
- package/src/core/inspect.ts +65 -27
- package/src/core/query.ts +508 -0
- package/src/core/routeros-errors.ts +67 -1
- package/src/data/winbox-cdb-write.ts +22 -8
- package/src/devices.ts +148 -37
- package/src/discover.ts +7 -8
- package/src/errors.ts +116 -7
- package/src/execute-fanout.ts +14 -9
- package/src/execute.ts +128 -116
- package/src/explain/catalog.ts +64 -16
- package/src/explain/menus.ts +27 -6
- package/src/explain.ts +6 -6
- package/src/index.ts +31 -1
- package/src/mcp/tools.ts +10 -15
- package/src/protocols/adapter.ts +235 -118
- package/src/protocols/btest-session.ts +3 -2
- package/src/protocols/endpoint.ts +10 -0
- package/src/protocols/mac-telnet-console.ts +12 -7
- package/src/protocols/native-api.ts +252 -73
- package/src/protocols/sftp.ts +44 -14
- package/src/protocols/ssh.ts +42 -14
- package/src/resolver/cdb.ts +28 -7
- package/src/resolver/index.ts +1 -0
- package/src/resolver/target.ts +84 -18
- package/src/retrieve-fanout.ts +51 -12
- package/src/retrieve-follow.ts +862 -0
- package/src/retrieve-sample.ts +244 -0
- package/src/retrieve-stream.ts +115 -0
- package/src/retrieve-wait.ts +286 -0
- package/src/retrieve.ts +741 -109
- package/src/settings.ts +15 -15
- package/src/transfer-fanout.ts +40 -15
- package/src/transfer.ts +234 -32
|
@@ -65,7 +65,15 @@ inert. (Earlier drafts said "the comment is not a lookup key" — superseded.)
|
|
|
65
65
|
When `<router>` matches an entry (by `target` or a lookup key), credentials and
|
|
66
66
|
other CDB-resident fields load from that entry. If nothing matches, the call
|
|
67
67
|
still proceeds but `--username` / `--password` (or `CENTRS_USERNAME` /
|
|
68
|
-
`CENTRS_PASSWORD`, or a `__default__` record) become mandatory.
|
|
68
|
+
`CENTRS_PASSWORD`, or a `__default__` record) become mandatory. The one
|
|
69
|
+
exception is a **pinned** CDB (`--cdb-file`, `CENTRS_CDB_FILE`,
|
|
70
|
+
`CENTRS_CDB_PASSWORD`, or a `centrs.env` CDB path) with no `__default__`
|
|
71
|
+
record: a DNS name or identity that matches nothing fails closed with
|
|
72
|
+
`cdb/not-found-target`, because it can't be told apart from a mistyped record.
|
|
73
|
+
An address that matches nothing still proceeds as a literal host: an IPv4 or
|
|
74
|
+
IPv6 literal (optionally with a port, IPv6 as `[…]:port`), an `http(s)://` URL,
|
|
75
|
+
or a MAC. On MCP every target must still be registered
|
|
76
|
+
(`cdb/target-not-registered`).
|
|
69
77
|
|
|
70
78
|
### Ambiguity
|
|
71
79
|
|
|
@@ -326,12 +334,13 @@ runs produce stable diffs.
|
|
|
326
334
|
- A multi-target **write** is gated by `--yes`, confirmed once up front (not per
|
|
327
335
|
target); without it the error/tip names the blast radius (how many routers).
|
|
328
336
|
`--force` stays scoped to destructive `devices` CDB mutations.
|
|
329
|
-
- Commands that are not safe to fan out (`terminal`, and `api --stream`
|
|
330
|
-
|
|
337
|
+
- Commands that are not safe to fan out (`terminal`, and `api --stream` or a
|
|
338
|
+
`/listen` endpoint) reject a multi-target selection with `usage/fanout-not-supported`.
|
|
331
339
|
|
|
332
340
|
## Subcommands
|
|
333
341
|
|
|
334
342
|
```text
|
|
343
|
+
centrs devices init # create an empty CDB at the resolved path
|
|
335
344
|
centrs devices list [--group G] [--where attr=value]
|
|
336
345
|
[--near lat,lon,radius] [--bbox south,west,north,east]
|
|
337
346
|
[--format json|yaml|text]
|
|
@@ -361,8 +370,16 @@ clack-style prompt used by `@tikoci/quickchr`); it is **not** a separate
|
|
|
361
370
|
field-editing verb, and invoking it today returns `usage/not-implemented`. There
|
|
362
371
|
is no `update`.
|
|
363
372
|
|
|
373
|
+
- `init` creates an empty open CDB at the resolved path (`--cdb-file`,
|
|
374
|
+
`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
|
|
376
|
+
`cdb/file-exists` warning and writes nothing. An explicit path is created only
|
|
377
|
+
by `init`; every other subcommand reports `cdb/not-found` for it (typo
|
|
378
|
+
protection), and that error names `init`.
|
|
364
379
|
- `list` (alias `print`) shows resolved targets, their record type, group, and
|
|
365
|
-
a one-line provenance summary.
|
|
380
|
+
a one-line provenance summary. Each row carries the `identity=`/`mac=`/`ip=`
|
|
381
|
+
lookup keys it resolves by (when set), so one call maps names to records. No
|
|
382
|
+
network IO. `--where attr=value`
|
|
366
383
|
(repeatable, AND-combined) filters by CDB-stored facts/comment-kv plus core
|
|
367
384
|
record fields (`target`/`group`/`identity`/`mac`) — e.g. `--where
|
|
368
385
|
lat=37.7749`. `--near lat,lon,radius` and `--bbox south,west,north,east`
|
|
@@ -29,7 +29,8 @@ centrs devices list --cdb-file $CDB
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Envelope: `ok: true`, `data` is an array of `{ target, recordType, group?,
|
|
32
|
-
user, sources }
|
|
32
|
+
user, identity?, mac?, ip?, sources }` — the lookup keys appear only when the
|
|
33
|
+
record's comment sets them. No `meta.timing.request`.
|
|
33
34
|
|
|
34
35
|
### 2. List a single group
|
|
35
36
|
|
|
@@ -46,7 +47,8 @@ valid success.
|
|
|
46
47
|
centrs devices list --cdb-file /tmp/does-not-exist.cdb
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
Errors with `cdb/not-found`. `details_url` populated.
|
|
50
|
+
Errors with `cdb/not-found`. `details_url` populated. The remediation names
|
|
51
|
+
`centrs devices init --cdb-file <path>` (example 54).
|
|
50
52
|
|
|
51
53
|
## show
|
|
52
54
|
|
|
@@ -634,3 +636,32 @@ Both succeed: the `add` stores the freeform comment verbatim, and the later
|
|
|
634
636
|
`set timeout=5000` does **not** raise `input/incomplete-gps` even though the
|
|
635
637
|
record's GPS pair is incomplete. Touching `lat`/`lon` (e.g. `set lat=…`) would
|
|
636
638
|
re-enable the pairing check.
|
|
639
|
+
|
|
640
|
+
## init
|
|
641
|
+
|
|
642
|
+
`init` is the explicit way to start a CDB at a path you choose (#376). Reads
|
|
643
|
+
and mutations never create an explicit `--cdb-file`/`CENTRS_CDB_FILE`; only the
|
|
644
|
+
default path is created implicitly on first run.
|
|
645
|
+
|
|
646
|
+
### 54. Start a repo-local CDB
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
centrs devices init --cdb-file ./lab/winbox.cdb --json
|
|
650
|
+
centrs devices add 192.0.2.1 --user robot-reader --cdb-file ./lab/winbox.cdb --json
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Creates missing parent directories and an empty open CDB with mode `0600`.
|
|
654
|
+
`data` is `{ cdbFile, created: true, recordCount: 0 }`. The `add` then succeeds.
|
|
655
|
+
|
|
656
|
+
### 55. `init` never clobbers
|
|
657
|
+
|
|
658
|
+
```bash
|
|
659
|
+
centrs devices init --cdb-file ./lab/winbox.cdb --json
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
On an existing, readable CDB: `ok: true`, `data.created: false`,
|
|
663
|
+
`data.recordCount` is the current count, and a `cdb/file-exists` warning says
|
|
664
|
+
nothing was written. As with `git init`, re-running is safe. An existing file that
|
|
665
|
+
does not load fails with that load error (`cdb/parse-failed`,
|
|
666
|
+
`cdb/password-required`) and is left untouched. A location that cannot be
|
|
667
|
+
written fails with `cdb/create-failed`.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# execute
|
|
2
2
|
|
|
3
3
|
Run a RouterOS CLI command and return its (semi-structured) output. `execute`
|
|
4
|
-
is the single read/write surface for RouterOS
|
|
4
|
+
is the single read/write surface for RouterOS configuration and operational
|
|
5
5
|
CLI-shaped commands — there is no separate `update` command.
|
|
6
6
|
|
|
7
7
|
Status: `rest-api`, `native-api`, `mac-telnet`, and `ssh` are `CHR-passed` (see
|
|
8
8
|
`docs/MATRIX.md` and `commands/execute/examples.md`, examples 1–11 over REST,
|
|
9
|
-
12–19 over the native API, 20–22 over mac-telnet, and S1–
|
|
9
|
+
12–19 over the native API, 20–22 over mac-telnet, and S1–S5 over ssh, green via
|
|
10
10
|
`bun run test:integration`), including multi-target fan-out (see **Target
|
|
11
11
|
selection**, examples F1–F5). `romon` and `winbox-terminal` remain
|
|
12
12
|
`not-started`. SNMP is retrieve-only and rejects `execute`.
|
|
@@ -15,7 +15,8 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
|
|
|
15
15
|
|
|
16
16
|
- Mirror RouterOS `execute` semantics. Input is a CLI string; output is
|
|
17
17
|
console-shaped text (or structured records for path+verb writes) wrapped in
|
|
18
|
-
the standard envelope. This includes
|
|
18
|
+
the standard envelope. This includes selector-, positional-id-, and
|
|
19
|
+
non-CRUD-shaped writes.
|
|
19
20
|
- Validation runs **offline first, then on the device** (see constitution:
|
|
20
21
|
validation; GH#354). Stage 1 is the corpus-gated analyzer over the exact
|
|
21
22
|
command string, with no connection: a syntax fault is rejected with a byte
|
|
@@ -27,8 +28,10 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
|
|
|
27
28
|
GH#230): a single `:put [:parse "<cmd>"]` covers both the syntax and the
|
|
28
29
|
unknown-attribute (name-level) gate on **every** transport — `:parse` never
|
|
29
30
|
throws; its return value's printed form (`(evl …)` / `(evl bad parameter …)` /
|
|
30
|
-
`(<%% bad command name …)
|
|
31
|
-
|
|
31
|
+
`(<%% bad command name …)`, or a bare line-anchored `syntax error` /
|
|
32
|
+
`expected …` / `missing … (line N column M)`, examples V4) is read from the
|
|
33
|
+
REST/native `ret` *and* the console output. An accepted return can echo
|
|
34
|
+
those words, so only the anchored forms reject. That check is necessary, not sufficient: value-type errors pass
|
|
32
35
|
it, and the `/console/inspect request=child` / server re-validation semantic
|
|
33
36
|
checks are separate. Command-level `request=completion` is deliberately not
|
|
34
37
|
used: it hangs REST and closes native API on RouterOS 7.12.2 (GH#343), while
|
|
@@ -36,9 +39,21 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
|
|
|
36
39
|
The outer RouterOS string literal escapes backslash, quote, and dollar so
|
|
37
40
|
validation parses the caller's exact command rather than expanding variables
|
|
38
41
|
in the wrapper.
|
|
39
|
-
- centrs owns the **script-vs-structured gate** (`canonicalizeExecuteCommand`
|
|
40
|
-
`
|
|
41
|
-
|
|
42
|
+
- centrs owns the **script-vs-structured gate** (`canonicalizeExecuteCommand` in
|
|
43
|
+
`src/execute.ts`): it decides which validation and transport path runs.
|
|
44
|
+
`isWriteShaped` is deliberately independent: script fallback for `[find]`, a
|
|
45
|
+
positional id, or other non-`key=value` input never bypasses confirmation.
|
|
46
|
+
Known read verbs are allowlisted; an unknown menu verb fails closed as a
|
|
47
|
+
write-shaped operation. In script mode every command head in a statement
|
|
48
|
+
(the statement start and each `[…]` or `{…}` body) must be a read: its verb
|
|
49
|
+
is the first word past the known menu prefix, so `/ip address print` is a
|
|
50
|
+
read and `/user expire-password print` is not (examples W3). Root mutators such as `/import` and indirect
|
|
51
|
+
execution through `:execute` are write-shaped too, and so is any command
|
|
52
|
+
carrying `file=`: RouterOS redirects command output into a device file, so
|
|
53
|
+
`/ip/address/export file=address` creates `address.rsc` and
|
|
54
|
+
`/file/print file=test` creates `test.txt` even though `export` and `print`
|
|
55
|
+
are read verbs. The shared
|
|
56
|
+
`rosetta`/`lsp-routeros-ts`
|
|
42
57
|
canonicalizer is for canonicalization only — never the structured predicate
|
|
43
58
|
(widening `structured` is a product regression). Pinned by
|
|
44
59
|
`test/unit/execute-canonicalize-contract.test.ts`; the parser-vendoring
|
|
@@ -68,12 +83,11 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
|
|
|
68
83
|
else passes its `detail`/body plus the HTTP status through
|
|
69
84
|
`mapRouterOsError({ transport: "rest-api", httpStatus })`. There is no longer
|
|
70
85
|
a hand-rolled `routeros/*` table on the REST side.
|
|
71
|
-
- **Run-time fault mapping
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
scheduled — captured here so it isn't lost (no GitHub issue by design).
|
|
86
|
+
- **Run-time fault mapping.** A successful transport can still carry a RouterOS
|
|
87
|
+
rejection in `/execute`'s `ret`. Result detection lives beside the grounded
|
|
88
|
+
mappings in `src/core/routeros-errors.ts`, uses anchored result shapes so
|
|
89
|
+
ordinary output containing error-like prose remains output, and preserves a
|
|
90
|
+
passed validation history while failing the overall envelope.
|
|
77
91
|
- Output is *string*-shaped for script-mode; richer parsing is a future
|
|
78
92
|
concern. The envelope must still distinguish RouterOS errors from successful
|
|
79
93
|
runs (a 200 with a RouterOS error string is still an error — see
|
|
@@ -122,14 +136,28 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
|
|
|
122
136
|
mac-telnet. Callers that want IP-level execution may opt into ARP-based
|
|
123
137
|
MAC → IP resolution with `--resolve arp` (plus `--via native-api`/`rest-api`)
|
|
124
138
|
before protocol selection.
|
|
139
|
+
- An explicit `http(s)://host[:port]` target (positional, `--host`, or
|
|
140
|
+
`CENTRS_HOST`) names the REST service: with no `via` pinned anywhere,
|
|
141
|
+
auto-selection picks `rest-api` and keeps the URL's scheme and port
|
|
142
|
+
(`meta.settings.via` source `default` / `via:target-url`). Otherwise
|
|
143
|
+
auto-selection picks native-api. On the IP transports (native-api, REST,
|
|
144
|
+
ssh), a port typed into the target (`host:port`, `http://host:port`,
|
|
145
|
+
including an explicit `:80`/`:443`) is honored. The port settings ladder
|
|
146
|
+
(`--port`, `CENTRS_PORT`, a CDB `port=` comment, config) wins over it, and
|
|
147
|
+
the transport default comes last. The port carried by a matched CDB
|
|
148
|
+
record's `target` is not applied to native-api. mac-telnet addresses the MAC,
|
|
149
|
+
so its `--host`/`--port` are only the UDP delivery endpoint.
|
|
125
150
|
- When the resolved transport is mac-telnet and **no** username or password is
|
|
126
151
|
resolved from any source, execute emits a `tip/mac-telnet-no-credentials`
|
|
127
152
|
pointer: mac-telnet logs in with RouterOS credentials over **MTWEI** (MD5 is
|
|
128
153
|
refused by current RouterOS), so an empty login is rejected at connect time.
|
|
129
154
|
It is a tip, not an error — the call is still attempted.
|
|
130
155
|
- Default output is human-readable `text`; pass `--json`/`--format json`
|
|
131
|
-
(or set `CENTRS_FORMAT=json`) for the structured envelope.
|
|
132
|
-
|
|
156
|
+
(or set `CENTRS_FORMAT=json`) for the structured envelope. In text mode every
|
|
157
|
+
command renders errors the same way (`formatCentrsErrorText`): `[code] summary`,
|
|
158
|
+
then `At:` (RouterOS position or offline byte span), `Device said:` (RouterOS's
|
|
159
|
+
own words), `Fix:` and `Details:` when present. `--verbose` adds the full
|
|
160
|
+
`context`.
|
|
133
161
|
- CDB comment-kv metadata may provide per-target `via`, `port`, and `ssh-key`
|
|
134
162
|
overrides; CLI/API arguments still win.
|
|
135
163
|
- RoMON and WinBox Terminal are lower priority than mac-telnet until their
|
|
@@ -175,6 +203,10 @@ with the first SSH consumer, `transfer`/sftp; `execute / ssh` (this command) and
|
|
|
175
203
|
`terminal / ssh` followed as separate transports over the same host-`ssh`
|
|
176
204
|
plumbing (`src/protocols/ssh.ts`) — all three SSH cells are `CHR-passed`.
|
|
177
205
|
|
|
206
|
+
A selected key adds `IdentitiesOnly=yes`, preventing fallback to unrelated
|
|
207
|
+
agent keys. Explicit OpenSSH `IdentityFile` configuration still applies; see
|
|
208
|
+
[`terminal` → SSH key selection](../terminal/README.md#ssh-key-selection).
|
|
209
|
+
|
|
178
210
|
Over SSH — like mac-telnet — `execute` is a **console transport**: it runs a
|
|
179
211
|
CLI line and returns text (`SshExecClient`, one `ssh user@host "<command>"` per
|
|
180
212
|
command; validation reuses the same `:put [:parse …]` gate), so structured
|
|
@@ -215,9 +247,19 @@ many tokens):
|
|
|
215
247
|
the command; no `--` needed). One `--quickchr` stays single-target; repeating
|
|
216
248
|
it fans out; mixing it with positionals or CDB selectors is
|
|
217
249
|
`usage/conflicting-flags` (constitution: resolution providers).
|
|
250
|
+
- `execute --host http://192.0.2.10:8080 '/system/resource/print'` (or
|
|
251
|
+
`execute --host <host|url> -- <command>`) — the `--host` **flag** stands in
|
|
252
|
+
for a missing `<target>`: a lone positional is the command, and nothing
|
|
253
|
+
before `--` is fine. There is no `<target>`, so there is no CDB lookup and no
|
|
254
|
+
record credential can be picked up; auth comes from flags, env or config.
|
|
255
|
+
Two or more positionals without `--` keep the legacy split, so
|
|
256
|
+
`execute <mac> <command> --via mac-telnet --host <udp-host>` (the mac-telnet
|
|
257
|
+
delivery override) is unchanged. `CENTRS_HOST` never changes positional
|
|
258
|
+
parsing: `execute '<command>'` with it set still reads the lone positional as
|
|
259
|
+
the target.
|
|
218
260
|
|
|
219
|
-
Because `execute` is the primary **write** path, a write-shaped fan-out
|
|
220
|
-
|
|
261
|
+
Because `execute` is the primary **write** path, a write-shaped fan-out is gated
|
|
262
|
+
by **`--yes`**, confirmed **once** up front; without it
|
|
221
263
|
the error names the blast radius (how many routers). Write-ness is determined from
|
|
222
264
|
the command alone (`canonicalizeExecuteCommand` + `isWriteShaped`), so the gate
|
|
223
265
|
fires before any target is dialed. Per-target validation still runs because
|
|
@@ -373,6 +373,23 @@ centrs execute 127.0.0.1 '/ip/address/add address=198.51.100.51/32 interface=eth
|
|
|
373
373
|
Envelope: `ok: false`, `error.code=validation/unknown-attribute`, `meta.via=ssh`,
|
|
374
374
|
and no address is added — the console `:parse` gate catches `bogus` pre-mutation.
|
|
375
375
|
|
|
376
|
+
### S5. An explicit rejected key cannot fall back to an unrelated agent key
|
|
377
|
+
|
|
378
|
+
`test/integration/ssh-identity.test.ts` imports `$KEY` on a disposable CHR and
|
|
379
|
+
loads it into a private SSH agent. `$WRONG_KEY` is a second, untrusted key.
|
|
380
|
+
The test uses an empty OpenSSH config so no other `IdentityFile` is selected.
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
centrs execute 127.0.0.1 ':put [/system/identity/get name]' --via ssh --port $SSH_PORT --username $U --ssh-key $WRONG_KEY --insecure --json
|
|
384
|
+
centrs transfer 127.0.0.1 list --via sftp --port $SSH_PORT --username $U --ssh-key $WRONG_KEY --insecure --json
|
|
385
|
+
printf ':put ("centrs-identity:" . [/system/identity/get name])\n/quit\n' | centrs terminal 127.0.0.1 --via ssh --port $SSH_PORT --username $U --ssh-key $WRONG_KEY --insecure
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Execute and transfer fail with `auth/failed`; the terminal relay preserves
|
|
389
|
+
OpenSSH's nonzero exit and `Permission denied` diagnostic. The test disables
|
|
390
|
+
interactive prompts on its terminal relay. Controls using `$KEY`, or omitting
|
|
391
|
+
`--ssh-key` to use the private agent, succeed for all three commands.
|
|
392
|
+
|
|
376
393
|
## Offline gate (stage 1)
|
|
377
394
|
|
|
378
395
|
Validation runs offline before it runs on the device
|
|
@@ -428,6 +445,155 @@ and `meta.validation.stages` listing **both** stages as `skipped`. A gated
|
|
|
428
445
|
surface always reports every stage, so a consumer reading `stages` never has to
|
|
429
446
|
special-case the disabled shape.
|
|
430
447
|
|
|
448
|
+
### V4. A device-only `missing …` rejection is still a rejection
|
|
449
|
+
|
|
450
|
+
The offline stage passes `/ip/address/find where=`: an empty `where` is not a
|
|
451
|
+
structural fault it can see. RouterOS's `:parse` returns
|
|
452
|
+
`missing value for where (line 1 column 24)` (CHR 7.24.4), a bare diagnostic
|
|
453
|
+
like `syntax error`, so the device stage must reject it (GH#375). Before that
|
|
454
|
+
fix the classifier only knew `syntax error`, `bad command name` and
|
|
455
|
+
`expected …`, so it reported this reply as a pass and the command ran.
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
centrs execute $R '/ip/address/find where=' --via rest-api --username $U --password $P --json
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Envelope: `ok: false`, `error.code=validation/syntax`, `error.context.detail`
|
|
462
|
+
is RouterOS's own `missing value for where (line 1 column 24)`,
|
|
463
|
+
`error.position={"line":1,"column":24}`, and `meta.validation.stages` is
|
|
464
|
+
`offline: passed` followed by `device: failed`. `meta.validation.syntax=false`,
|
|
465
|
+
and `semantic` is absent: `:parse` refused the command, so the name-level check
|
|
466
|
+
was never asked (GH#435).
|
|
467
|
+
|
|
468
|
+
CHR 7.24.4 controls, via `:put [:parse "…"]`: `find where` without `=` parses
|
|
469
|
+
(IL). `print where=` is rejected differently (`expected end of command`, at
|
|
470
|
+
`where`). An unclosed bare `{` block or menu block (`/ip address {` then
|
|
471
|
+
newlines) returns `missing closing brace (line 3 column 1)`, while an unclosed
|
|
472
|
+
`do={` returns `expected closing brace`. Offline analysis usually catches the
|
|
473
|
+
unclosed-brace cases first.
|
|
474
|
+
|
|
475
|
+
### V5. A menu from a package that is not installed is not a syntax error
|
|
476
|
+
|
|
477
|
+
RouterOS's `:parse` reports a menu the device does not have as a syntax fault —
|
|
478
|
+
`syntax error (line 1 column 10)` for `/zerotier/print`, pointing at the `/`
|
|
479
|
+
after the absent segment, and `bad command name container (line 1 column 2)` for
|
|
480
|
+
the space spelling `/container print` (CHR 7.23.7). centrs locates the segment,
|
|
481
|
+
looks the path up in its path catalog, and names the package (GH#361).
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
centrs execute $R '/zerotier/print' --via rest-api --username $U --password $P --json
|
|
485
|
+
centrs execute $R '/container print' --via rest-api --username $U --password $P --json
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Envelope: `ok: false`, `error.code=validation/package-missing`,
|
|
489
|
+
`error.context.path` is `/zerotier` (then `/container`),
|
|
490
|
+
`error.context.packages` names the published package, the remediation points at
|
|
491
|
+
`/system/package/print` and never at the syntax, and `error.position` and
|
|
492
|
+
`error.context.detail` keep RouterOS's own column and words. V5–V7 all report
|
|
493
|
+
`meta.validation.syntax=false`. `semantic` is absent, or `not-applicable` for
|
|
494
|
+
the script-shaped `/container print` (GH#435).
|
|
495
|
+
|
|
496
|
+
### V6. A menu no RouterOS build has is an unknown path
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
centrs execute $R '/ip/nosuchmenu/print' --via rest-api --username $U --password $P --json
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Envelope: `ok: false`, `error.code=validation/unknown-path`,
|
|
503
|
+
`error.context.path=/ip/nosuchmenu`, `error.context.segment=nosuchmenu`. The
|
|
504
|
+
path catalog covers every 7.10–7.25 build with all extra packages, so a path it
|
|
505
|
+
lacks is not a missing package.
|
|
506
|
+
|
|
507
|
+
### V7. A published menu gated on more than a package
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
centrs execute $R '/interface/w60g/print' --via rest-api --username $U --password $P --json
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Envelope: `ok: false`, `error.code=validation/menu-unavailable`,
|
|
514
|
+
`error.context.path=/interface/w60g`, and `error.context.gates` carries both
|
|
515
|
+
published gates — package `wireless-rep` and hardware capability `60ghz` —
|
|
516
|
+
because a 60 GHz menu can be absent with the package installed. Nested and later
|
|
517
|
+
statements locate the same way (`:put [/interface/w60g/get [find] disabled]`
|
|
518
|
+
points at column 22). A menu-scope block (`/ip { nosuchmenu print }`) or a menu
|
|
519
|
+
named on an earlier line abstains and stays `validation/syntax`: the offline
|
|
520
|
+
analyzer does not resolve that parent, and centrs does not guess it.
|
|
521
|
+
|
|
522
|
+
### V8. Wrong credentials: the device stage is not reached (GH#435)
|
|
523
|
+
|
|
524
|
+
```bash
|
|
525
|
+
centrs execute $R '/system/identity/print' --via rest-api --username centrs-wrong --password centrs-wrong --json
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Envelope: `ok: false`, `error.code=transport/auth-failed`. `meta.validation`
|
|
529
|
+
has no `result`, `syntax` or `semantic`. Its stages are `offline: passed`, then
|
|
530
|
+
`device: skipped` with `reason: "not reached: transport/auth-failed"`. RouterOS
|
|
531
|
+
answered 401 before `:parse` could give a verdict.
|
|
532
|
+
|
|
533
|
+
## Write authorization and returned runtime faults
|
|
534
|
+
|
|
535
|
+
These regressions are transport-independent and run in
|
|
536
|
+
`test/integration/execute.test.ts` (with MCP's selector form also covered by
|
|
537
|
+
`test/integration/mcp.test.ts`).
|
|
538
|
+
|
|
539
|
+
### W1. Script-mode selector and positional writes still require confirmation
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
centrs execute $R '/ip/address/remove [find comment=centrs-write-gate]' --via rest-api --username $U --password $P </dev/null
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
The same contract holds for `/ip/address/remove *1`. Both stay script-mode so
|
|
546
|
+
RouterOS receives the raw selector/id syntax, but the independent safety gate
|
|
547
|
+
returns `usage/confirmation-required` before validation or mutation.
|
|
548
|
+
|
|
549
|
+
### W2. Non-CRUD mutation verbs still require confirmation
|
|
550
|
+
|
|
551
|
+
```bash
|
|
552
|
+
centrs execute $R '/ip/address/disable numbers=*1' --via rest-api --username $U --password $P </dev/null
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Envelope: `ok: false`, `error.code=usage/confirmation-required`; the address
|
|
556
|
+
remains enabled. Unknown RouterOS menu verbs also fail closed as write-shaped.
|
|
557
|
+
Root mutators such as `/import` and indirect execution through `:execute` use
|
|
558
|
+
the same gate.
|
|
559
|
+
|
|
560
|
+
### W3. Space-separated menu reads run without confirmation
|
|
561
|
+
|
|
562
|
+
```bash
|
|
563
|
+
centrs execute $R '/system identity print' --via rest-api --username $U --password $P </dev/null
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Envelope: `ok: true`. The space-separated spelling puts menu segments and the
|
|
567
|
+
verb in bare words, so the gate walks the known menu prefix and reads the first
|
|
568
|
+
word past it as the verb. `/ip address print where …` and
|
|
569
|
+
`:put [/system identity get name]` are reads the same way. The walk is also what
|
|
570
|
+
keeps an unlearned command gated: `/user expire-password print` names
|
|
571
|
+
`expire-password`, a published command outside both verb allowlists, whose
|
|
572
|
+
argument happens to be `print`, so it returns `usage/confirmation-required`.
|
|
573
|
+
Every command head in a statement is checked, including `[…]` and `do={…}`
|
|
574
|
+
bodies.
|
|
575
|
+
|
|
576
|
+
### R1. A post-validation `/execute` rejection fails the overall envelope
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
centrs execute $R '/ip/service/set www-ssl certificate=nope' --via rest-api --username $U --password $P --yes --json
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Envelope: `ok: false`, `error.code=routeros/invalid-value`, exit `1`, while
|
|
583
|
+
`meta.validation.result="passed"` and both stages remain `passed`: the rejection
|
|
584
|
+
happened at run time, after the scoped checks completed. The same result is
|
|
585
|
+
asserted over native API.
|
|
586
|
+
|
|
587
|
+
### R2. Error-like prose remains ordinary script output
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
centrs execute $R ':put "status: no such item appears in ordinary output"' --via rest-api --username $U --password $P --json
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Envelope: `ok: true`. Returned-text classification uses anchored grounded
|
|
594
|
+
shapes; it does not fail arbitrary output merely because it contains an error
|
|
595
|
+
phrase.
|
|
596
|
+
|
|
431
597
|
## Target selection (fan-out)
|
|
432
598
|
|
|
433
599
|
These exercise the shared target-selection grammar
|
|
@@ -495,6 +661,17 @@ command). With no `--cdb-file`, both are ad-hoc literals, labeled by
|
|
|
495
661
|
`meta.target.input` with no `recordIndex`: `data.summary = { total: 2, ok: 1,
|
|
496
662
|
failed: 1 }`, exit `2`.
|
|
497
663
|
|
|
664
|
+
### F7. An address that misses a pinned CDB is a literal; a name is not (#427)
|
|
665
|
+
|
|
666
|
+
```bash
|
|
667
|
+
centrs execute $A:$API_PORT no-such-router --cdb-file $CDB --username $U --password $P --json -- /system/resource/print
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
Neither positional matches a record. The address is dialed as a literal host
|
|
671
|
+
(inner `ok: true`). The name fails closed with `cdb/not-found-target`, because
|
|
672
|
+
it can't be told apart from a mistyped record name. `data.summary = { total: 2,
|
|
673
|
+
ok: 1, failed: 1 }`, exit `2`.
|
|
674
|
+
|
|
498
675
|
## Protocol selection notes
|
|
499
676
|
|
|
500
677
|
For an unresolved MAC target, execute auto-selection defaults to mac-telnet
|
|
@@ -510,6 +687,53 @@ Envelope: `meta.via=mac-telnet` when `$MAC` is not resolved from CDB and no
|
|
|
510
687
|
they must opt into MAC-to-IP resolution (`--resolve arp` + `--via …`) before
|
|
511
688
|
protocol selection.
|
|
512
689
|
|
|
690
|
+
### P1. An `http(s)://` target selects rest-api on its own port (#426)
|
|
691
|
+
|
|
692
|
+
```bash
|
|
693
|
+
centrs execute http://$A:$REST_PORT ':put [/system/identity/get name]' --username $U --password $P --json
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
Envelope: `ok: true`, `meta.via=rest-api`, `meta.settings.via =
|
|
697
|
+
{ kind: "default", key: "via:target-url" }`. No `--via` is pinned, and the URL's
|
|
698
|
+
scheme and port are what get dialed.
|
|
699
|
+
|
|
700
|
+
### P2. A typed `host:port` keeps native-api on that port (#426)
|
|
701
|
+
|
|
702
|
+
```bash
|
|
703
|
+
centrs execute $A:$API_PORT ':put [/system/identity/get name]' --username $U --password $P --json
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
Envelope: `ok: true`, `meta.via=native-api`, dialing `$API_PORT` (no
|
|
707
|
+
`--port` flag), the same way ssh and REST already treat a typed port.
|
|
708
|
+
|
|
709
|
+
### P3. A pinned CDB doesn't refuse an address it has no record for (#427)
|
|
710
|
+
|
|
711
|
+
```bash
|
|
712
|
+
centrs execute $A:$API_PORT ':put [/system/identity/get name]' --cdb-file $CDB --username $U --password $P --json
|
|
713
|
+
centrs execute http://$A:$REST_PORT ':put [/system/identity/get name]' --cdb-file $CDB --username $U --password $P --json
|
|
714
|
+
centrs execute no-such-router ':put [/system/identity/get name]' --cdb-file $CDB --username $U --password $P --json
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
`$CDB` has no record for any of the three. The first two are `ok: true` over
|
|
718
|
+
native-api and rest-api: an IP, `[IPv6]`, `http(s)://` URL or MAC that misses a
|
|
719
|
+
pinned CDB is a literal host, exactly as with no CDB. The third is `ok: false`,
|
|
720
|
+
`error.code = cdb/not-found-target`: a name or identity that misses still fails
|
|
721
|
+
closed.
|
|
722
|
+
|
|
723
|
+
## `--host` as the target (#428)
|
|
724
|
+
|
|
725
|
+
### H1. `--host` stands in for `<target>`; a lone positional is the command
|
|
726
|
+
|
|
727
|
+
```bash
|
|
728
|
+
centrs execute --host http://$A:$REST_PORT ':put [/system/identity/get name]' --username $U --password $P --json
|
|
729
|
+
centrs execute --host $A:$API_PORT --username $U --password $P --json -- ':put [/system/identity/get name]'
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Both are `ok: true`, the first over rest-api (the URL names REST) and the second
|
|
733
|
+
over native-api on `$API_PORT`. `meta.target.source = { kind: "cli", key:
|
|
734
|
+
"host" }`, with no `recordIndex`: there is no `<target>`, so there is no CDB
|
|
735
|
+
lookup. `retrieve`, `api` and `transfer` have the same rule (their H1).
|
|
736
|
+
|
|
513
737
|
## quickchr targets (#134)
|
|
514
738
|
|
|
515
739
|
`$NAME` is the machine name of a running quickchr-managed CHR. Host/port/auth
|
|
@@ -172,7 +172,7 @@ today's path/verb/args split and script-vs-structured gate:
|
|
|
172
172
|
The *container* half of that pair is since decidable. `src/explain/menus.ts`
|
|
173
173
|
(#207) bakes only the menu-vs-command node type — not a schema — and it is
|
|
174
174
|
version-less because the pinned trees show no `dir`↔`cmd` flip across
|
|
175
|
-
7.10.2 → 7.
|
|
175
|
+
7.10.2 → 7.25beta5 or across architectures. So `/ip/route` now resolves as
|
|
176
176
|
`navigation` (#210). The floor is unmoved: the table is deliberately
|
|
177
177
|
incomplete, absence from it decides nothing, and every rule above it still
|
|
178
178
|
refuses rather than guesses.
|
|
@@ -199,10 +199,11 @@ today's path/verb/args split and script-vs-structured gate:
|
|
|
199
199
|
Discovery is the union of MikroTik's two published inventories, not the
|
|
200
200
|
sitemap alone. A *branching* menu is served as a trailing-slash category URL
|
|
201
201
|
with no `.md` of its own, while that menu's own entry is published at
|
|
202
|
-
`<dir>/<basename>.md` and listed only in
|
|
203
|
-
discovery dropped 256 of 1,070 pages, silently
|
|
204
|
-
asserts that every category dir still
|
|
205
|
-
|
|
202
|
+
`<dir>/<basename>.md` (since 2026-10-06, `<dir>.md`) and listed only in
|
|
203
|
+
`llms.txt` — so sitemap-only discovery dropped 256 of 1,070 pages, silently
|
|
204
|
+
and field-heavily. Generation asserts that every category dir still
|
|
205
|
+
contributes its own page under one of those two spellings, so the next
|
|
206
|
+
inventory reshape fails loudly instead of shrinking the table (#285, #417,
|
|
206
207
|
tikoci/rosetta#137).
|
|
207
208
|
|
|
208
209
|
**Source refresh (#335, accepted 2026-09-13).** The inspect pins retain the
|
|
@@ -221,6 +222,42 @@ today's path/verb/args split and script-vs-structured gate:
|
|
|
221
222
|
coherent generated catalog has 1,120 paths: 959 `both`, 67 `inspect`, and 94
|
|
222
223
|
`published`; it still reports zero navigation-vs-command contradictions.
|
|
223
224
|
|
|
225
|
+
**Source refresh (accepted 2026-09-27).** The two current pins advance to the
|
|
226
|
+
channels then live — stable 7.24.2 → 7.24.4 and development 7.25beta3 →
|
|
227
|
+
7.25beta5 — both still architecture-paired, both history anchors untouched.
|
|
228
|
+
The refresh is a structural no-op: the inspect union holds at 617 menus and
|
|
229
|
+
the catalog at 1,120 paths, with no path added or removed and no kind change.
|
|
230
|
+
What moved is current-only publication: the CLI Reference grows from 1,053
|
|
231
|
+
pages / 1,060 entries to 1,055 / 1,062, which documents
|
|
232
|
+
`/interface/ethernet/pon` and `/ip/ssh/known-hosts` for the first time and so
|
|
233
|
+
moves both from `inspect` to `both` — 961 `both`, 65 `inspect`, 94
|
|
234
|
+
`published`. Zero navigation-vs-command contradictions, unchanged.
|
|
235
|
+
|
|
236
|
+
**Source refresh (accepted 2026-10-11, #417).** MikroTik changed the CLI
|
|
237
|
+
Reference layout on 2026-10-06: a branching menu's own entry moved from
|
|
238
|
+
`<dir>/<basename>.md` to `<dir>.md`, and pages dropped their MDX `import`
|
|
239
|
+
preamble, so the `# title` line is no longer fenced off by an import. Both
|
|
240
|
+
guards refused, correctly, and the catalog check could not report anything
|
|
241
|
+
for days. The parser now accepts either layout (a category dir needs one of
|
|
242
|
+
the two page spellings, and both at once is reported); losing the page
|
|
243
|
+
entirely still aborts. The inventories reconcile exactly: 846 sitemap pages
|
|
244
|
+
plus 253 category pages equal the 1,099 pages `llms.txt` lists. The inspect
|
|
245
|
+
pins add 7.26beta1 (x86 and arm64) because the manual documents the
|
|
246
|
+
development channel, and a path new in it with no matching tree is
|
|
247
|
+
published-only with no gate to explain it. Reviewed against the committed
|
|
248
|
+
catalog: 47 paths added, 1 removed, 3 gate strings changed, and no kind
|
|
249
|
+
change, no provenance loss and no navigation-vs-command contradiction. The
|
|
250
|
+
additions are the 32 `/cmr` paths, `/interface/wifi/dashboard`,
|
|
251
|
+
`/system/dashboard`, `/tool/apptraffic`, `/interface/ethernet/pon/monitor`,
|
|
252
|
+
`/interface/wireless/snooper/snoop` and
|
|
253
|
+
`/interface/ethernet/switch/rule/reset-counters-all`; the removal is the
|
|
254
|
+
published-only `/system/routerboard/settings/keep-frequency`, which its page
|
|
255
|
+
no longer lists. The CLI Reference moves from 1,055 pages / 1,062 entries to
|
|
256
|
+
1,099 / 1,106; the inspect union from 617 to 636 menus; the catalog from
|
|
257
|
+
1,120 to 1,166 paths: 1,005 `both`, 67 `inspect`, 94 `published`.
|
|
258
|
+
`/interface/wifi/dashboard` and `/system/dashboard` are `inspect` only: the
|
|
259
|
+
trees carry them, the manual publishes no menu entry for them.
|
|
260
|
+
|
|
224
261
|
There are real unknowns in how far offline parsing can go (expression
|
|
225
262
|
grammar, scope fidelity vs `:parse`, `[]`-nesting corner cases). Grounding
|
|
226
263
|
those unknowns was **phase 0** (#185); its findings are folded in throughout
|
package/commands/mcp/README.md
CHANGED
|
@@ -125,7 +125,8 @@ benchmarked RouterOS agent support. Load-bearing findings that shape this design
|
|
|
125
125
|
- `centrs_retrieve` mirrors `retrieve`, including attribute projection,
|
|
126
126
|
`listAttributes`, and `group` fanout.
|
|
127
127
|
- `centrs_execute` mirrors `execute`. Read-shaped commands run subject only to
|
|
128
|
-
CDB membership; write-shaped
|
|
128
|
+
CDB membership; every write-shaped command is gated by the CDB policy below,
|
|
129
|
+
including selector/positional script forms and non-CRUD mutation verbs.
|
|
129
130
|
Its MCP `destructiveHint` is **conservative — always `true`**, even though the
|
|
130
131
|
tool also serves read-shaped commands: a single execute tool cannot vary the
|
|
131
132
|
static annotation by argument, so it advertises the worst case and lets the
|