@tikoci/centrs 0.1.6 → 0.1.7
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 +108 -1
- package/README.md +8 -4
- package/commands/api/README.md +21 -10
- package/commands/api/examples.md +70 -2
- package/commands/devices/README.md +10 -1
- package/commands/devices/examples.md +33 -2
- package/commands/execute/README.md +29 -15
- package/commands/execute/examples.md +89 -0
- package/commands/explain/README.md +12 -1
- package/commands/mcp/README.md +2 -1
- package/commands/retrieve/examples.md +20 -0
- package/docs/CLI.md +1 -1
- package/docs/CONSTITUTION.md +11 -6
- package/docs/errors/cdb/create-failed.md +20 -0
- package/docs/errors/cdb/file-exists.md +23 -0
- package/docs/errors/input/invalid-target.md +25 -3
- package/docs/errors/routeros/invalid-value.md +3 -2
- package/docs/errors/transport/cancel-unacknowledged.md +24 -0
- package/package.json +1 -1
- package/src/api.ts +109 -25
- package/src/cli/devices.ts +16 -3
- package/src/core/error-catalog.ts +14 -0
- package/src/core/execute-command.ts +313 -6
- package/src/core/inspect.ts +22 -22
- package/src/core/routeros-errors.ts +66 -0
- package/src/data/winbox-cdb-write.ts +22 -8
- package/src/devices.ts +142 -28
- package/src/execute-fanout.ts +1 -1
- package/src/execute.ts +22 -51
- package/src/explain/catalog.ts +8 -8
- package/src/explain/menus.ts +4 -4
- package/src/index.ts +2 -0
- package/src/mcp/tools.ts +1 -1
- package/src/protocols/adapter.ts +12 -1
- package/src/protocols/mac-telnet-console.ts +6 -4
- package/src/protocols/native-api.ts +29 -3
- package/src/resolver/target.ts +32 -12
- package/src/retrieve.ts +17 -4
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,114 @@ The product evolves as a stream of changes; there are no alpha gates.
|
|
|
6
6
|
Per-feature status is tracked in `docs/MATRIX.md`. Use this file for
|
|
7
7
|
documenting cross-cutting shifts that affect contributors and consumers.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## 0.1.7 — 2026-09-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`centrs devices init` creates an empty CDB, `git init`-style (#376).**
|
|
14
|
+
Bootstrapping a device book previously meant conjuring the file some other
|
|
15
|
+
way. `centrs devices init [--cdb-file PATH]` (or `CENTRS_CDB_FILE`, or the
|
|
16
|
+
default path) creates the parent directories and an exclusive `0600` file.
|
|
17
|
+
**It never clobbers**, so a bootstrap script may run it unconditionally: an
|
|
18
|
+
existing readable CDB succeeds with a `cdb/file-exists` warning and nothing
|
|
19
|
+
is written. An existing file that fails to load (`cdb/parse-failed`,
|
|
20
|
+
`cdb/password-required`) reports that error and is left untouched, and a
|
|
21
|
+
location that cannot be written fails with the new `cdb/create-failed`. CLI
|
|
22
|
+
and library (`initCdb`) only; not exposed on the MCP devices tool.
|
|
23
|
+
|
|
24
|
+
- **`devices list` reports the lookup keys it matches on (#378).** Rows now
|
|
25
|
+
carry `identity` / `mac` / `ip` when the comment sets them, with
|
|
26
|
+
`comment-kv` provenance in `sources`, and the text table gains an `IDENTITY`
|
|
27
|
+
column. `comment` itself stays `show`-only.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **Write-shaped commands now require confirmation, on every transport
|
|
32
|
+
(#365, #366).** Write authorization was entangled with script-vs-structured
|
|
33
|
+
transport selection, so several shapes reached a device without passing the
|
|
34
|
+
`--yes` gate: selectors, positional ids, non-CRUD verbs, root mutators,
|
|
35
|
+
indirect execution and relative menu writes. Authorization is now decided
|
|
36
|
+
independently of how the command is carried. Read spellings are unaffected —
|
|
37
|
+
the verb is the first known verb among the path's last segment and the bare
|
|
38
|
+
words following the path token, so `/system identity print` and
|
|
39
|
+
`:put [/system identity get name]` stay reads, while a statement with no
|
|
40
|
+
recognizable verb still fails closed. Caller-addressed API `/execute` script
|
|
41
|
+
strings additionally run offline analysis, then live `:parse`, then execute;
|
|
42
|
+
structured API requests remain on `/console/inspect`. This advances the
|
|
43
|
+
synchronous half of #154 without closing it — caller-controlled `file=`,
|
|
44
|
+
`as-string` and async job-id semantics remain open there.
|
|
45
|
+
|
|
46
|
+
- **RouterOS rejections returned inside a successful transport envelope are no
|
|
47
|
+
longer read as success.** `/execute` can answer 200 while the device's own
|
|
48
|
+
text says it refused. Those are now classified as runtime failures, with the
|
|
49
|
+
passed validation-stage history preserved so the envelope still shows what
|
|
50
|
+
each stage concluded.
|
|
51
|
+
|
|
52
|
+
- **Device-stage validation missed RouterOS's bare `missing …` diagnostics
|
|
53
|
+
(#375).** `classifyParseResult` matched `syntax error`, `bad command name`
|
|
54
|
+
and `expected …` from `:put [:parse …]`, but not `missing closing brace` or
|
|
55
|
+
`missing value for where` — so the device stage reported those rejections as
|
|
56
|
+
a pass and the command ran. `/ip/address/find where=` passes offline, which
|
|
57
|
+
made the device classifier its only gate. Replaying every bare diagnostic in
|
|
58
|
+
the pinned corpus across 7.20.8 → 7.25beta3 finds exactly seven heads, and
|
|
59
|
+
the two `missing …` forms were the only misses; both are now recognized, with
|
|
60
|
+
the device's own position in `error.context.detail`. Echoed script text is
|
|
61
|
+
still not a verdict.
|
|
62
|
+
|
|
63
|
+
- **A stream's `/cancel` acknowledgement is now bounded (#385).** `api
|
|
64
|
+
--stream` stopped by `--duration` or Ctrl-C sent `/cancel` and then waited
|
|
65
|
+
with no bound, so a connected router that never answered held the CLI open
|
|
66
|
+
forever — `backend.close()` in the outer `finally` was unreachable while that
|
|
67
|
+
wait was pending. The wait is now bounded by the request `--timeout`, the
|
|
68
|
+
same bound as every other reply wait; past it the listen ends locally and the
|
|
69
|
+
session closes. The summary keeps its real `stopReason` and adds a
|
|
70
|
+
`transport/cancel-unacknowledged` **warning**, so the outcome never claims an
|
|
71
|
+
acknowledgement that did not arrive. A cooperating router is unchanged.
|
|
72
|
+
|
|
73
|
+
- **`retrieve --attributes` rejected real singleton properties (#377).**
|
|
74
|
+
`isKnownSingletonPath` knew only `/system/resource` and `/system/identity`,
|
|
75
|
+
so every other singleton (`/tool/romon`, `/system/routerboard`, `/ip/dns`, …)
|
|
76
|
+
was inspected as a list menu. `print,proplist` comes back empty on a
|
|
77
|
+
singleton, so validation fell back to `print`'s own flags (`as-value`,
|
|
78
|
+
`file`, `interval`, …) and offered those as the attribute list. The singleton
|
|
79
|
+
bit now comes from the device: a singleton's `get` takes only `value-name`,
|
|
80
|
+
while a list menu's `get` also takes `number`. The hardcoded pair survives
|
|
81
|
+
only as the `--validate=false` fallback, which skips inspect. Costs one extra
|
|
82
|
+
`request=child` for a menu that has `get`.
|
|
83
|
+
|
|
84
|
+
- **Completion candidates mixed help text and syntax rows into property names
|
|
85
|
+
(#380).** `extractCompletionNames` read `completion`/`name`/`value`/`text`
|
|
86
|
+
from every row, so the help string ("DNS cache size in kB") and the
|
|
87
|
+
`show: "false"` syntax rows (`[`, `(`, `$`, `"`, `*`, `<value>`) were offered
|
|
88
|
+
as attributes — which is why `--attributes '$'` passed validation. Rows carry
|
|
89
|
+
only `completion`/`offset`/`preference`/`show`/`style`/`text`/`type` on every
|
|
90
|
+
build recorded 7.9.2 → 7.25beta3, so `name`/`value` never matched anything.
|
|
91
|
+
On CHR 7.23.7, `show: "true"` is exactly the property set. The exported
|
|
92
|
+
`InspectCompletionItem` drops the never-present `name`/`value` and adds
|
|
93
|
+
`show`/`style`/`offset`/`preference`.
|
|
94
|
+
|
|
95
|
+
- **An unparseable `<router>` crashed instead of reporting (#381).**
|
|
96
|
+
`parseHostCandidate` passed the target straight to `new URL` — the only
|
|
97
|
+
`new URL` on user input in `src/` — so with the default CDB, `quickchr:NAME`,
|
|
98
|
+
`a b` or `bad:host:name` failed every command with a `TypeError: Invalid URL`
|
|
99
|
+
surfaced as `internal/unhandled`. It now raises `input/invalid-target` and
|
|
100
|
+
names the valid target forms; a `quickchr:NAME` guess is told to use
|
|
101
|
+
`--quickchr NAME`. With an explicit CDB the same input already stopped at a
|
|
102
|
+
classified `cdb/not-found-target`, so that path is unchanged.
|
|
103
|
+
|
|
104
|
+
### Changed
|
|
105
|
+
|
|
106
|
+
- **The pinned restraml inspect trees advance to the live channel builds.** The
|
|
107
|
+
pins named "current stable" and "next beta" while pointing a patch behind
|
|
108
|
+
both: stable 7.24.2 → 7.24.4 and development 7.25beta3 → 7.25beta5. History
|
|
109
|
+
anchors 7.10.2 and 7.16 are untouched and both current versions stay
|
|
110
|
+
architecture-paired. The refresh is structurally a no-op — the inspect union
|
|
111
|
+
holds at 617 menus and the catalog at 1,120 paths, with no path added or
|
|
112
|
+
removed and no kind change. What moved is current-only publication: the CLI
|
|
113
|
+
Reference grew 1,053 → 1,055 pages (1,060 → 1,062 entries) and now documents
|
|
114
|
+
`/interface/ethernet/pon` and `/ip/ssh/known-hosts`, so both move `inspect` →
|
|
115
|
+
`both` (961 `both`, 65 `inspect`, 94 `published`). Zero
|
|
116
|
+
navigation-vs-command contradictions, unchanged.
|
|
10
117
|
|
|
11
118
|
## 0.1.6 — 2026-09-15
|
|
12
119
|
|
package/README.md
CHANGED
|
@@ -11,9 +11,13 @@ installing centrs or connecting to a router:
|
|
|
11
11
|
bunx @tikoci/centrs@next explain '/ip/address print' --json
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Use `bunx @tikoci/centrs@next --help` to inspect the full preview CLI.
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
Use `bunx @tikoci/centrs@next --help` to inspect the full preview CLI. Since
|
|
15
|
+
0.1.6 the dist-tag follows standard SemVer semantics rather than the old
|
|
16
|
+
even/odd-minor rule: a plain version (`0.1.6`) publishes to `latest` and carries
|
|
17
|
+
`next` forward with it, while a pre-release suffix (`0.1.7-next.0`) publishes to
|
|
18
|
+
`next` only and never moves `latest`. So `@next` is the leading edge and can
|
|
19
|
+
equal `@latest` between cuts — `npm view @tikoci/centrs dist-tags` is the
|
|
20
|
+
authority.
|
|
17
21
|
|
|
18
22
|
The full flag reference for every command is generated from the CLI metadata
|
|
19
23
|
into [`docs/CLI.md`](docs/CLI.md); per-command behavior lives in
|
|
@@ -58,7 +62,7 @@ product; without them this would just be a worse `curl`.
|
|
|
58
62
|
| `execute` | Run RouterOS CLI-shaped read/write commands (add/set/remove) over native API/REST/L2 surfaces. |
|
|
59
63
|
| `api` | Structured RouterOS API passthrough (gh-api style): one command per operation, structured in/out, can write, over REST or native API. Open-ended follow is `api … --stream` (native API; NDJSON). |
|
|
60
64
|
| `terminal` | Open an interactive console, primarily SSH or MAC-Telnet. |
|
|
61
|
-
| `check` | Probe reachability and management protocol availability. |
|
|
65
|
+
| `check` | Probe reachability and management protocol availability. **Designed, not implemented** — see `docs/MATRIX.md`. |
|
|
62
66
|
| `explain` | Analyze a RouterOS command before running it: canonical form, syntax diagnostics, and what is valid at a path. Offline analysis runs today; the live-device probes are designed. |
|
|
63
67
|
| `devices` | View and maintain the CDB-backed device registry (the only writer). |
|
|
64
68
|
| `discover` | Discover MNDP neighbors and optionally save them into the CDB. |
|
package/commands/api/README.md
CHANGED
|
@@ -112,15 +112,18 @@ gate is `/console/inspect` — **not** `:put [:parse]`:
|
|
|
112
112
|
does) answers normally on the same build. See the 7.12.2 finding in
|
|
113
113
|
`AGENTS.md` (GH#343).
|
|
114
114
|
- **Carve-out:** a script-shaped `POST /rest/execute` is a CLI string, not a path,
|
|
115
|
-
so
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
`
|
|
121
|
-
the
|
|
122
|
-
|
|
123
|
-
|
|
115
|
+
so structured inspect is `not-applicable` (`meta.validation.semantic`). It
|
|
116
|
+
follows the CLI-string pipeline instead: offline analysis, then live
|
|
117
|
+
`:put [:parse ...]`, then run. An offline rejection is still a byte span with
|
|
118
|
+
no connection; an offline pass or abstention continues to the device stage.
|
|
119
|
+
`meta.validation.stages[]` reports both decisions. A missing or blank `script`
|
|
120
|
+
is still `input/invalid-command`, not a syntax rejection. Validation passing
|
|
121
|
+
is not the run succeeding: `as-string` makes the console's own text the reply
|
|
122
|
+
body, so a runtime rejection arrives as an HTTP-200 `ret`. `/execute` output
|
|
123
|
+
is normalized through the same grounded result table `execute` uses, so
|
|
124
|
+
`no such item` fails the envelope with a `routeros/*` code while the stages
|
|
125
|
+
stay `passed`. Ordinary output that merely contains a fault string is
|
|
126
|
+
unaffected — the result matchers are anchored, not substring.
|
|
124
127
|
- Structured path requests are **not** offline-gated. The input is a path plus a
|
|
125
128
|
body, not a CLI string, and an offline *path* gate collides with GH#211's
|
|
126
129
|
unlisted-path decision. On the success path `meta.validation.stages[]` is
|
|
@@ -186,7 +189,15 @@ RouterOS produces multi-frame output two ways, and `api` treats them differently
|
|
|
186
189
|
deletion's `.dead=true` flag is preserved); the stream ends with a summary
|
|
187
190
|
envelope (`data.stopReason` ∈ `count-reached`/`duration-elapsed`/`interrupted`/
|
|
188
191
|
`transport-error`, plus `frames` and `durationMs`). `--duration`/`--count`
|
|
189
|
-
bound it; Ctrl-C stops and still emits the summary. `--
|
|
192
|
+
bound it; Ctrl-C stops and still emits the summary. The `--duration` window
|
|
193
|
+
starts once validation is done, so it excludes validation (and the connect and
|
|
194
|
+
login validation needs). With `--no-validate`, connect and login happen inside
|
|
195
|
+
the window. Stopping on `--duration` or Ctrl-C sends RouterOS `/cancel`, and
|
|
196
|
+
centrs waits up to `--timeout` for the router to acknowledge it. A router that
|
|
197
|
+
never does gets its session closed locally, and the summary carries a
|
|
198
|
+
`transport/cancel-unacknowledged` warning (#385). Reaching `--count` sends
|
|
199
|
+
`/cancel` best-effort and closes the session without waiting, so it never
|
|
200
|
+
carries that warning (examples L5). `--via rest-api --stream` →
|
|
190
201
|
`transport/capability-unsupported`. The exit code reflects whether the stream
|
|
191
202
|
*started* cleanly, not whether every frame was `ok`.
|
|
192
203
|
|
package/commands/api/examples.md
CHANGED
|
@@ -156,7 +156,8 @@ centrs api $R execute -X POST -f script=':put [/system/identity/get name]' --use
|
|
|
156
156
|
|
|
157
157
|
Envelope: `ok: true`, `data` is string-shaped and contains the CHR identity;
|
|
158
158
|
`meta.validation.semantic=not-applicable` (script, not a path) and
|
|
159
|
-
`meta.validation.stages` is `offline: passed` then `device:
|
|
159
|
+
`meta.validation.stages` is `offline: passed` then live `device: passed` from
|
|
160
|
+
`:put [:parse ...]`.
|
|
160
161
|
|
|
161
162
|
### 16b. The `/execute` script is gated offline (GH#354)
|
|
162
163
|
|
|
@@ -173,6 +174,31 @@ Envelope: `ok: false`, `error.code=validation/syntax`,
|
|
|
173
174
|
`error.context.span={"start":5,"end":6}`, and **not** a `transport/*` code.
|
|
174
175
|
`meta.validation.stages` is `offline: failed` then `device: skipped`.
|
|
175
176
|
|
|
177
|
+
### 16c. Offline pass continues to live `:parse`
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
centrs api $R execute -X POST -f script='/ip/address/add no-such-arg=x' --username $U --password $P --yes
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Envelope: `ok: false` with a device validation code
|
|
184
|
+
(`validation/unknown-attribute`, or `validation/syntax` on older RouterOS), and
|
|
185
|
+
`meta.validation.stages` is `offline: passed` then `device: failed`. Offline
|
|
186
|
+
acceptance remains `runtimeAcceptance: not-proven`; it never suppresses the live
|
|
187
|
+
stage.
|
|
188
|
+
|
|
189
|
+
### 16d. A runtime rejection in `/execute` output is not `ok: true`
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
centrs api $R execute -X POST -f script='/ip/service/set www-ssl certificate=nope' --username $U --password $P --yes
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Envelope: `ok: false`, `error.code=routeros/invalid-value`. `as-string` makes
|
|
196
|
+
the console's own text the reply body, so RouterOS carries this rejection in an
|
|
197
|
+
HTTP-200 `ret`; the validation gate stays `passed` because it did pass — the
|
|
198
|
+
command was rejected at run, not at validation. Ordinary output that merely
|
|
199
|
+
mentions a fault string (`:put "status: no such item appears in help"`) stays
|
|
200
|
+
`ok: true`.
|
|
201
|
+
|
|
176
202
|
### 17. `--via rest-api --listen` is rejected
|
|
177
203
|
|
|
178
204
|
```bash
|
|
@@ -308,7 +334,28 @@ stdout is the bare rest-style array (string values); exit code 0.
|
|
|
308
334
|
centrs api $A execute -X POST -f script=':put [/system/identity/get name]' --via native-api --port $API_PORT --username $U --password $P --yes
|
|
309
335
|
```
|
|
310
336
|
|
|
311
|
-
Envelope: `ok: true`, `data` contains the CHR identity
|
|
337
|
+
Envelope: `ok: true`, `data` contains the CHR identity and validation reports
|
|
338
|
+
offline plus live `:parse` stages passed.
|
|
339
|
+
|
|
340
|
+
### N8b. Native `/execute` also rejects at live `:parse`
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
centrs api $A execute -X POST -f script='/ip/address/add no-such-arg=x' --via native-api --port $API_PORT --username $U --password $P --yes
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Envelope: `ok: false`; the accepted version-specific validation code is
|
|
347
|
+
`validation/unknown-attribute` or `validation/syntax`, with stages
|
|
348
|
+
`offline: passed`, `device: failed`.
|
|
349
|
+
|
|
350
|
+
### N8c. Native `/execute` runtime rejection fails the envelope
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
centrs api $A execute -X POST -f script='/ip/service/set www-ssl certificate=nope' --via native-api --port $API_PORT --username $U --password $P --yes
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Envelope: `ok: false`, `error.code=routeros/invalid-value`, with the validation
|
|
357
|
+
stages still `passed` — the same normalization as REST, since both adapters
|
|
358
|
+
surface the `as-string` console text as `data`.
|
|
312
359
|
|
|
313
360
|
## listen / `--stream` (native-api only)
|
|
314
361
|
|
|
@@ -356,6 +403,27 @@ centrs api $A ip/address --stream --duration 2s --via native-api --port $API_POR
|
|
|
356
403
|
With no change during the window, the stream ends after ~2 s with a summary
|
|
357
404
|
envelope whose `data.stopReason=duration-elapsed`. Exit code 0.
|
|
358
405
|
|
|
406
|
+
### L5. A router that ignores `/cancel` cannot hold the process open
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
centrs api $A ip/address --stream --duration 200ms --timeout 1s --via native-api --port $API_PORT --username $U --password $P
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
A stream stopped by `--duration` or Ctrl-C sends `/cancel` and waits up to
|
|
413
|
+
`--timeout` for the router's acknowledgement (the interrupted `!trap` and
|
|
414
|
+
`!done`). If none arrives, centrs closes the session itself. The summary still
|
|
415
|
+
reports its stop reason (`duration-elapsed` or `interrupted`), exit code is 0,
|
|
416
|
+
and `warnings` carries `transport/cancel-unacknowledged`. A router that answers
|
|
417
|
+
inside the window gets no warning. `--count` sends `/cancel` without waiting,
|
|
418
|
+
so it exits at once and never warns (#385).
|
|
419
|
+
|
|
420
|
+
RouterOS on CHR always acknowledges `/cancel`, so this example runs against a
|
|
421
|
+
loopback native-API peer that ignores it, as a real CLI process:
|
|
422
|
+
`test/integration/api-stream-cancel.test.ts` covers `--duration`, SIGINT,
|
|
423
|
+
`--count` and the cooperative control. The session-level cases (silent peer,
|
|
424
|
+
chatty peer, trap without `!done`, close after cancel) are in
|
|
425
|
+
`test/unit/native-api.test.ts`.
|
|
426
|
+
|
|
359
427
|
## fanout (multi-target, F…)
|
|
360
428
|
|
|
361
429
|
These run against a CDB fixture with two records sharing group `$G`: record 0 is
|
|
@@ -332,6 +332,7 @@ runs produce stable diffs.
|
|
|
332
332
|
## Subcommands
|
|
333
333
|
|
|
334
334
|
```text
|
|
335
|
+
centrs devices init # create an empty CDB at the resolved path
|
|
335
336
|
centrs devices list [--group G] [--where attr=value]
|
|
336
337
|
[--near lat,lon,radius] [--bbox south,west,north,east]
|
|
337
338
|
[--format json|yaml|text]
|
|
@@ -361,8 +362,16 @@ clack-style prompt used by `@tikoci/quickchr`); it is **not** a separate
|
|
|
361
362
|
field-editing verb, and invoking it today returns `usage/not-implemented`. There
|
|
362
363
|
is no `update`.
|
|
363
364
|
|
|
365
|
+
- `init` creates an empty open CDB at the resolved path (`--cdb-file`,
|
|
366
|
+
`CENTRS_CDB_FILE`, or the default), with parent directories and mode `0600`.
|
|
367
|
+
It never overwrites: on an existing, readable CDB it succeeds with a
|
|
368
|
+
`cdb/file-exists` warning and writes nothing. An explicit path is created only
|
|
369
|
+
by `init`; every other subcommand reports `cdb/not-found` for it (typo
|
|
370
|
+
protection), and that error names `init`.
|
|
364
371
|
- `list` (alias `print`) shows resolved targets, their record type, group, and
|
|
365
|
-
a one-line provenance summary.
|
|
372
|
+
a one-line provenance summary. Each row carries the `identity=`/`mac=`/`ip=`
|
|
373
|
+
lookup keys it resolves by (when set), so one call maps names to records. No
|
|
374
|
+
network IO. `--where attr=value`
|
|
366
375
|
(repeatable, AND-combined) filters by CDB-stored facts/comment-kv plus core
|
|
367
376
|
record fields (`target`/`group`/`identity`/`mac`) — e.g. `--where
|
|
368
377
|
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,7 +1,7 @@
|
|
|
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
|
|
@@ -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
|
|
@@ -216,8 +230,8 @@ many tokens):
|
|
|
216
230
|
it fans out; mixing it with positionals or CDB selectors is
|
|
217
231
|
`usage/conflicting-flags` (constitution: resolution providers).
|
|
218
232
|
|
|
219
|
-
Because `execute` is the primary **write** path, a write-shaped fan-out
|
|
220
|
-
|
|
233
|
+
Because `execute` is the primary **write** path, a write-shaped fan-out is gated
|
|
234
|
+
by **`--yes`**, confirmed **once** up front; without it
|
|
221
235
|
the error names the blast radius (how many routers). Write-ness is determined from
|
|
222
236
|
the command alone (`canonicalizeExecuteCommand` + `isWriteShaped`), so the gate
|
|
223
237
|
fires before any target is dialed. Per-target validation still runs because
|
|
@@ -428,6 +428,95 @@ and `meta.validation.stages` listing **both** stages as `skipped`. A gated
|
|
|
428
428
|
surface always reports every stage, so a consumer reading `stages` never has to
|
|
429
429
|
special-case the disabled shape.
|
|
430
430
|
|
|
431
|
+
### V4. A device-only `missing …` rejection is still a rejection
|
|
432
|
+
|
|
433
|
+
The offline stage passes `/ip/address/find where=`: an empty `where` is not a
|
|
434
|
+
structural fault it can see. RouterOS's `:parse` returns
|
|
435
|
+
`missing value for where (line 1 column 24)` (CHR 7.24.4), a bare diagnostic
|
|
436
|
+
like `syntax error`, so the device stage must reject it (GH#375). Before that
|
|
437
|
+
fix the classifier only knew `syntax error`, `bad command name` and
|
|
438
|
+
`expected …`, so it reported this reply as a pass and the command ran.
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
centrs execute $R '/ip/address/find where=' --via rest-api --username $U --password $P --json
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Envelope: `ok: false`, `error.code=validation/syntax`, `error.context.detail`
|
|
445
|
+
is RouterOS's own `missing value for where (line 1 column 24)`,
|
|
446
|
+
`error.position={"line":1,"column":24}`, and `meta.validation.stages` is
|
|
447
|
+
`offline: passed` followed by `device: failed`.
|
|
448
|
+
|
|
449
|
+
CHR 7.24.4 controls, via `:put [:parse "…"]`: `find where` without `=` parses
|
|
450
|
+
(IL). `print where=` is rejected differently (`expected end of command`, at
|
|
451
|
+
`where`). An unclosed bare `{` block or menu block (`/ip address {` then
|
|
452
|
+
newlines) returns `missing closing brace (line 3 column 1)`, while an unclosed
|
|
453
|
+
`do={` returns `expected closing brace`. Offline analysis usually catches the
|
|
454
|
+
unclosed-brace cases first.
|
|
455
|
+
|
|
456
|
+
## Write authorization and returned runtime faults
|
|
457
|
+
|
|
458
|
+
These regressions are transport-independent and run in
|
|
459
|
+
`test/integration/execute.test.ts` (with MCP's selector form also covered by
|
|
460
|
+
`test/integration/mcp.test.ts`).
|
|
461
|
+
|
|
462
|
+
### W1. Script-mode selector and positional writes still require confirmation
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
centrs execute $R '/ip/address/remove [find comment=centrs-write-gate]' --via rest-api --username $U --password $P </dev/null
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
The same contract holds for `/ip/address/remove *1`. Both stay script-mode so
|
|
469
|
+
RouterOS receives the raw selector/id syntax, but the independent safety gate
|
|
470
|
+
returns `usage/confirmation-required` before validation or mutation.
|
|
471
|
+
|
|
472
|
+
### W2. Non-CRUD mutation verbs still require confirmation
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
centrs execute $R '/ip/address/disable numbers=*1' --via rest-api --username $U --password $P </dev/null
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Envelope: `ok: false`, `error.code=usage/confirmation-required`; the address
|
|
479
|
+
remains enabled. Unknown RouterOS menu verbs also fail closed as write-shaped.
|
|
480
|
+
Root mutators such as `/import` and indirect execution through `:execute` use
|
|
481
|
+
the same gate.
|
|
482
|
+
|
|
483
|
+
### W3. Space-separated menu reads run without confirmation
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
centrs execute $R '/system identity print' --via rest-api --username $U --password $P </dev/null
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Envelope: `ok: true`. The space-separated spelling puts menu segments and the
|
|
490
|
+
verb in bare words, so the gate walks the known menu prefix and reads the first
|
|
491
|
+
word past it as the verb. `/ip address print where …` and
|
|
492
|
+
`:put [/system identity get name]` are reads the same way. The walk is also what
|
|
493
|
+
keeps an unlearned command gated: `/user expire-password print` names
|
|
494
|
+
`expire-password`, a published command outside both verb allowlists, whose
|
|
495
|
+
argument happens to be `print`, so it returns `usage/confirmation-required`.
|
|
496
|
+
Every command head in a statement is checked, including `[…]` and `do={…}`
|
|
497
|
+
bodies.
|
|
498
|
+
|
|
499
|
+
### R1. A post-validation `/execute` rejection fails the overall envelope
|
|
500
|
+
|
|
501
|
+
```bash
|
|
502
|
+
centrs execute $R '/ip/service/set www-ssl certificate=nope' --via rest-api --username $U --password $P --yes --json
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Envelope: `ok: false`, `error.code=routeros/invalid-value`, exit `1`, while
|
|
506
|
+
`meta.validation.result="passed"` and both stages remain `passed`: the rejection
|
|
507
|
+
happened at run time, after the scoped checks completed. The same result is
|
|
508
|
+
asserted over native API.
|
|
509
|
+
|
|
510
|
+
### R2. Error-like prose remains ordinary script output
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
centrs execute $R ':put "status: no such item appears in ordinary output"' --via rest-api --username $U --password $P --json
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Envelope: `ok: true`. Returned-text classification uses anchored grounded
|
|
517
|
+
shapes; it does not fail arbitrary output merely because it contains an error
|
|
518
|
+
phrase.
|
|
519
|
+
|
|
431
520
|
## Target selection (fan-out)
|
|
432
521
|
|
|
433
522
|
These exercise the shared target-selection grammar
|
|
@@ -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.
|
|
@@ -221,6 +221,17 @@ today's path/verb/args split and script-vs-structured gate:
|
|
|
221
221
|
coherent generated catalog has 1,120 paths: 959 `both`, 67 `inspect`, and 94
|
|
222
222
|
`published`; it still reports zero navigation-vs-command contradictions.
|
|
223
223
|
|
|
224
|
+
**Source refresh (accepted 2026-09-27).** The two current pins advance to the
|
|
225
|
+
channels then live — stable 7.24.2 → 7.24.4 and development 7.25beta3 →
|
|
226
|
+
7.25beta5 — both still architecture-paired, both history anchors untouched.
|
|
227
|
+
The refresh is a structural no-op: the inspect union holds at 617 menus and
|
|
228
|
+
the catalog at 1,120 paths, with no path added or removed and no kind change.
|
|
229
|
+
What moved is current-only publication: the CLI Reference grows from 1,053
|
|
230
|
+
pages / 1,060 entries to 1,055 / 1,062, which documents
|
|
231
|
+
`/interface/ethernet/pon` and `/ip/ssh/known-hosts` for the first time and so
|
|
232
|
+
moves both from `inspect` to `both` — 961 `both`, 65 `inspect`, 94
|
|
233
|
+
`published`. Zero navigation-vs-command contradictions, unchanged.
|
|
234
|
+
|
|
224
235
|
There are real unknowns in how far offline parsing can go (expression
|
|
225
236
|
grammar, scope fidelity vs `:parse`, `[]`-nesting corner cases). Grounding
|
|
226
237
|
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
|
|
@@ -84,6 +84,18 @@ Rejected before any network call, with `usage/conflicting-flags`.
|
|
|
84
84
|
centrs retrieve $R /system/resource --attribute uptime --all-attributes --username $U --password $P --json
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
### 20. Comma-list --attributes on any singleton
|
|
88
|
+
|
|
89
|
+
Singletons are recognized from `/console/inspect` — their `get` takes no
|
|
90
|
+
`number` argument — so every singleton validates against its `get value-name`
|
|
91
|
+
properties, not against `print`'s flags (#377).
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
centrs retrieve $R /tool/romon --attributes enabled,id --username $U --password $P --json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Envelope: `data` is an object containing only `enabled` and `id`.
|
|
98
|
+
|
|
87
99
|
## --list-attributes
|
|
88
100
|
|
|
89
101
|
### 9. List attributes for a path
|
|
@@ -336,6 +348,14 @@ centrs retrieve $A /system/resource --attribute bogus-attr --via native-api --po
|
|
|
336
348
|
centrs retrieve $A /system/resource --via native-api --port $API_PORT --username wrong --password wrong
|
|
337
349
|
```
|
|
338
350
|
|
|
351
|
+
### N12. Singleton `--attributes` projection (#377)
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
centrs retrieve $A /tool/romon --attributes enabled,id --via native-api --port $API_PORT --username $U --password $P
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`data` is an object containing only `enabled` and `id`.
|
|
358
|
+
|
|
339
359
|
## quickchr targets (#134)
|
|
340
360
|
|
|
341
361
|
`$NAME` is the machine name of a running quickchr-managed CHR
|
package/docs/CLI.md
CHANGED
|
@@ -234,7 +234,7 @@ Usage: centrs explain '<input>' [flags]
|
|
|
234
234
|
Inspect and mutate the CDB-backed device registry. `devices` is the only command that writes the CDB.
|
|
235
235
|
|
|
236
236
|
```text
|
|
237
|
-
Usage: centrs devices <list|show|groups|add|edit|set|remove> [args] [flags]
|
|
237
|
+
Usage: centrs devices <init|list|show|groups|add|edit|set|remove> [args] [flags]
|
|
238
238
|
```
|
|
239
239
|
|
|
240
240
|
| Flag | Value | Description |
|