@tikoci/centrs 0.1.4 → 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 +417 -1
- package/GLOSSARY.txt +40 -1
- package/README.md +16 -8
- package/commands/AGENTS.md +7 -4
- package/commands/api/AGENTS.md +13 -1
- package/commands/api/README.md +50 -10
- package/commands/api/examples.md +100 -2
- package/commands/devices/README.md +10 -1
- package/commands/devices/examples.md +33 -2
- package/commands/execute/README.md +68 -25
- package/commands/execute/examples.md +168 -7
- package/commands/explain/README.md +1206 -155
- package/commands/explain/examples.md +99 -3
- package/commands/mcp/README.md +9 -5
- package/commands/mcp/examples.md +2 -2
- package/commands/retrieve/examples.md +20 -0
- package/commands/terminal/examples.md +7 -1
- package/docs/CLI.md +3 -3
- package/docs/CONSTITUTION.md +83 -21
- package/docs/MATRIX.md +89 -52
- 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/docs/errors/validation/syntax.md +29 -4
- package/package.json +13 -2
- package/src/api-fanout.ts +17 -4
- package/src/api.ts +346 -75
- package/src/cli/api.ts +1 -1
- package/src/cli/btest.ts +1 -1
- package/src/cli/devices.ts +16 -3
- package/src/cli/explain.ts +1 -1
- package/src/cli/terminal.ts +1 -1
- package/src/core/envelope.ts +53 -0
- package/src/core/error-catalog.ts +16 -1
- package/src/core/execute-command.ts +455 -0
- package/src/core/inspect.ts +58 -26
- package/src/core/routeros-errors.ts +66 -0
- package/src/core/version.ts +4 -0
- package/src/core/yaml.ts +86 -0
- package/src/data/winbox-cdb-write.ts +22 -8
- package/src/devices.ts +142 -28
- package/src/execute-fanout.ts +19 -5
- package/src/execute.ts +247 -295
- package/src/explain/arg-tokens.ts +62 -25
- package/src/explain/args.ts +398 -52
- package/src/explain/blocks.ts +54 -11
- package/src/explain/brace-tokens.ts +4 -3
- package/src/explain/catalog.ts +42 -44
- package/src/explain/coordinates.ts +159 -0
- package/src/explain/defects.ts +2 -1
- package/src/explain/escape-tokens.ts +77 -0
- package/src/explain/highlight-projection.ts +308 -0
- package/src/explain/menus.ts +9 -5
- package/src/explain/operator-tokens.ts +13 -7
- package/src/explain/operators.ts +27 -20
- package/src/explain/path-tokens.ts +1 -1
- package/src/explain/pathresolve.ts +358 -113
- package/src/explain/quoted-string.ts +110 -20
- package/src/explain/scope-brace.ts +175 -22
- package/src/explain/segment.ts +429 -21
- package/src/explain/separator.ts +187 -0
- package/src/explain/split-owner.ts +194 -0
- package/src/explain/string-tokens.ts +14 -4
- package/src/explain/symbol-values.ts +209 -173
- package/src/explain/symbols.ts +21 -1
- package/src/explain/transport.ts +17 -0
- package/src/explain/value-tokens.ts +15 -12
- package/src/explain/verbsplit.ts +165 -23
- package/src/explain/write.ts +47 -6
- package/src/explain.ts +522 -72
- package/src/index.ts +21 -2
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1 -1
- package/src/offline-gate.ts +148 -0
- 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-fanout.ts +1 -1
- package/src/retrieve.ts +18 -63
- package/src/settings.ts +2 -1
- package/src/transfer-fanout.ts +1 -1
- package/src/transfer.ts +1 -1
package/commands/api/README.md
CHANGED
|
@@ -99,19 +99,51 @@ vs `--query` distinction.
|
|
|
99
99
|
|
|
100
100
|
## Validation
|
|
101
101
|
|
|
102
|
-
Default on. Because the input is a structured path (not a CLI string), the
|
|
103
|
-
`/console/inspect` — **not** `:put [:parse]`:
|
|
102
|
+
Default on. Because the input is a structured path (not a CLI string), the device
|
|
103
|
+
gate is `/console/inspect` — **not** `:put [:parse]`:
|
|
104
104
|
|
|
105
105
|
- Path existence + per-verb attribute validity via `request=child`
|
|
106
106
|
(`validation/unknown-path`, `validation/unknown-attribute`).
|
|
107
|
-
-
|
|
107
|
+
- **Designed, not implemented:** value-level advice via `request=completion`
|
|
108
|
+
(+`input=`) feeding `tips`/`warnings`. Whatever implements it must not fall
|
|
109
|
+
back to `request=completion` at a *command* path (`path=` ending at the verb):
|
|
110
|
+
that shape hangs REST and closes native API on RouterOS 7.12.2, and no centrs
|
|
111
|
+
code uses it. Completion at an *argument* path (`print,proplist`, as `retrieve`
|
|
112
|
+
does) answers normally on the same build. See the 7.12.2 finding in
|
|
113
|
+
`AGENTS.md` (GH#343).
|
|
108
114
|
- **Carve-out:** a script-shaped `POST /rest/execute` is a CLI string, not a path,
|
|
109
|
-
so
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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.
|
|
127
|
+
- Structured path requests are **not** offline-gated. The input is a path plus a
|
|
128
|
+
body, not a CLI string, and an offline *path* gate collides with GH#211's
|
|
129
|
+
unlisted-path decision. On the success path `meta.validation.stages[]` is
|
|
130
|
+
absent there; on a `validation/*` rejection it is present and reports the
|
|
131
|
+
offline stage `skipped` with that reason, so the breakdown never implies an
|
|
132
|
+
analysis that did not run.
|
|
133
|
+
|
|
134
|
+
`--validate=false` skips **both** stages of the preflight — one flag, one meaning
|
|
135
|
+
(constitution: validation); RouterOS still re-validates writes server-side.
|
|
136
|
+
Disabling validation to make a call pass is forbidden (constitution: validation
|
|
137
|
+
is the product).
|
|
138
|
+
|
|
139
|
+
`--raw` **defaults** `--validate` to false — it does not override it (GH#154).
|
|
140
|
+
The resolution order is the explicit `--validate` flag, then `--raw`, then
|
|
141
|
+
`CENTRS_VALIDATE` / the CDB comment-kv / config, then the `true` default. So
|
|
142
|
+
`--raw` alone behaves as it always has and behaves the same on every machine
|
|
143
|
+
whatever the environment holds, while `--raw --validate=true` runs the gate and
|
|
144
|
+
reports a rejection in the `--raw` error shape (`{code,message}` on stderr,
|
|
145
|
+
nonzero exit, no `data` key) — which is how you debug `api` when centrs itself is
|
|
146
|
+
the suspect.
|
|
115
147
|
|
|
116
148
|
## Write confirmation
|
|
117
149
|
|
|
@@ -157,7 +189,15 @@ RouterOS produces multi-frame output two ways, and `api` treats them differently
|
|
|
157
189
|
deletion's `.dead=true` flag is preserved); the stream ends with a summary
|
|
158
190
|
envelope (`data.stopReason` ∈ `count-reached`/`duration-elapsed`/`interrupted`/
|
|
159
191
|
`transport-error`, plus `frames` and `durationMs`). `--duration`/`--count`
|
|
160
|
-
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` →
|
|
161
201
|
`transport/capability-unsupported`. The exit code reflects whether the stream
|
|
162
202
|
*started* cleanly, not whether every frame was `ok`.
|
|
163
203
|
|
package/commands/api/examples.md
CHANGED
|
@@ -155,7 +155,49 @@ centrs api $R execute -X POST -f script=':put [/system/identity/get name]' --use
|
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
Envelope: `ok: true`, `data` is string-shaped and contains the CHR identity;
|
|
158
|
-
`meta.validation.semantic=not-applicable` (script, not a path)
|
|
158
|
+
`meta.validation.semantic=not-applicable` (script, not a path) and
|
|
159
|
+
`meta.validation.stages` is `offline: passed` then live `device: passed` from
|
|
160
|
+
`:put [:parse ...]`.
|
|
161
|
+
|
|
162
|
+
### 16b. The `/execute` script is gated offline (GH#354)
|
|
163
|
+
|
|
164
|
+
Until GH#354 the script-mode carve-out meant this surface ran **no** preflight at
|
|
165
|
+
all. The offline analyzer is the whole stage-1 gate here, and it opens no
|
|
166
|
+
connection — the unreachable port proves it.
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
centrs api 127.0.0.1:1 execute -X POST -f script=':put [' --username $U --password $P --yes
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Envelope: `ok: false`, `error.code=validation/syntax`,
|
|
173
|
+
`error.context.validationStage="offline"`, `error.context.surface="api /execute"`,
|
|
174
|
+
`error.context.span={"start":5,"end":6}`, and **not** a `transport/*` code.
|
|
175
|
+
`meta.validation.stages` is `offline: failed` then `device: skipped`.
|
|
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`.
|
|
159
201
|
|
|
160
202
|
### 17. `--via rest-api --listen` is rejected
|
|
161
203
|
|
|
@@ -209,6 +251,20 @@ proplist). Combining an id with `--query`/`--proplist` folds the id into the RES
|
|
|
209
251
|
matching the native `?.id=` read. (Plain id-only reads still use the `GET …/$ID`
|
|
210
252
|
URL form; see example 5's re-read.)
|
|
211
253
|
|
|
254
|
+
### 22. `--raw --validate=true` still runs the gate (#154)
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
centrs api $R ip/no-such-menu --raw --validate=true --username $U --password $P
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`--raw` only **defaults** `--validate` to false, so an explicit `--validate=true`
|
|
261
|
+
wins and the `/console/inspect` preflight runs. The rejection comes back in the
|
|
262
|
+
`--raw` error shape, not the envelope: stdout empty, stderr a compact
|
|
263
|
+
`{"code":"validation/unknown-path","message":…}`, nonzero exit, and no `data`
|
|
264
|
+
key. Contrast example 13, where bare `--raw` skips the preflight entirely. This
|
|
265
|
+
pair is how you tell a RouterOS rejection from a centrs bug while staying on the
|
|
266
|
+
bare-passthrough surface.
|
|
267
|
+
|
|
212
268
|
## native-api (`--via native-api`)
|
|
213
269
|
|
|
214
270
|
The same contract over the binary API. Validation still runs through
|
|
@@ -278,7 +334,28 @@ stdout is the bare rest-style array (string values); exit code 0.
|
|
|
278
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
|
|
279
335
|
```
|
|
280
336
|
|
|
281
|
-
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`.
|
|
282
359
|
|
|
283
360
|
## listen / `--stream` (native-api only)
|
|
284
361
|
|
|
@@ -326,6 +403,27 @@ centrs api $A ip/address --stream --duration 2s --via native-api --port $API_POR
|
|
|
326
403
|
With no change during the window, the stream ends after ~2 s with a summary
|
|
327
404
|
envelope whose `data.stopReason=duration-elapsed`. Exit code 0.
|
|
328
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
|
+
|
|
329
427
|
## fanout (multi-target, F…)
|
|
330
428
|
|
|
331
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,21 +15,45 @@ 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
|
|
19
|
-
-
|
|
18
|
+
the standard envelope. This includes selector-, positional-id-, and
|
|
19
|
+
non-CRUD-shaped writes.
|
|
20
|
+
- Validation runs **offline first, then on the device** (see constitution:
|
|
21
|
+
validation; GH#354). Stage 1 is the corpus-gated analyzer over the exact
|
|
22
|
+
command string, with no connection: a syntax fault is rejected with a byte
|
|
23
|
+
span in `error.context.span` and the adapter is never dialed, and
|
|
24
|
+
`meta.validation.stages[]` then shows the device stage `skipped`. Stage 1
|
|
25
|
+
cannot see an unknown attribute — it has no per-menu schema — so stage 2 is
|
|
26
|
+
not optional.
|
|
27
|
+
- Stage 2 is itself two gates (grounded on CHR 7.23.3,
|
|
20
28
|
GH#230): a single `:put [:parse "<cmd>"]` covers both the syntax and the
|
|
21
29
|
unknown-attribute (name-level) gate on **every** transport — `:parse` never
|
|
22
30
|
throws; its return value's printed form (`(evl …)` / `(evl bad parameter …)` /
|
|
23
|
-
`(<%% bad command name …)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
35
|
+
it, and the `/console/inspect request=child` / server re-validation semantic
|
|
36
|
+
checks are separate. Command-level `request=completion` is deliberately not
|
|
37
|
+
used: it hangs REST and closes native API on RouterOS 7.12.2 (GH#343), while
|
|
38
|
+
`child` returns the complete argument-name set immediately.
|
|
27
39
|
The outer RouterOS string literal escapes backslash, quote, and dollar so
|
|
28
40
|
validation parses the caller's exact command rather than expanding variables
|
|
29
41
|
in the wrapper.
|
|
30
|
-
- centrs owns the **script-vs-structured gate** (`canonicalizeExecuteCommand`
|
|
31
|
-
`
|
|
32
|
-
|
|
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`
|
|
33
57
|
canonicalizer is for canonicalization only — never the structured predicate
|
|
34
58
|
(widening `structured` is a product regression). Pinned by
|
|
35
59
|
`test/unit/execute-canonicalize-contract.test.ts`; the parser-vendoring
|
|
@@ -59,12 +83,11 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
|
|
|
59
83
|
else passes its `detail`/body plus the HTTP status through
|
|
60
84
|
`mapRouterOsError({ transport: "rest-api", httpStatus })`. There is no longer
|
|
61
85
|
a hand-rolled `routeros/*` table on the REST side.
|
|
62
|
-
- **Run-time fault mapping
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
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.
|
|
68
91
|
- Output is *string*-shaped for script-mode; richer parsing is a future
|
|
69
92
|
concern. The envelope must still distinguish RouterOS errors from successful
|
|
70
93
|
runs (a 200 with a RouterOS error string is still an error — see
|
|
@@ -73,13 +96,33 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
|
|
|
73
96
|
terminal width, so each transport handles width differently. **mac-telnet** is
|
|
74
97
|
the only one that negotiates a size: `src/protocols/mac-telnet-console.ts`
|
|
75
98
|
answers the device's terminal-size probe with a deliberately tall/wide screen,
|
|
76
|
-
so captured columns are neither
|
|
77
|
-
*without* a pseudo-tty (`src/protocols/ssh.ts`),
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
so captured columns are neither paginated nor wrapped *at the negotiated
|
|
100
|
+
width*. **SSH execute** runs *without* a pseudo-tty (`src/protocols/ssh.ts`),
|
|
101
|
+
so no width is negotiated at all. **REST/native-api** are not consoles, so a
|
|
102
|
+
terminal width never applies (they return structured records, or a string
|
|
103
|
+
`ret` for script-mode). There is therefore no per-call `--width` knob today —
|
|
104
|
+
an explicit width override is deferred until a concrete need appears.
|
|
105
|
+
- **A device-side wrap that no width negotiation reaches (GH#352).** Declaring a
|
|
106
|
+
width is not sufficient, because RouterOS can wrap for reasons that are not
|
|
107
|
+
about width. On 7.23.6 / 7.24.3 / 7.25beta4 a settings menu with exactly one
|
|
108
|
+
property prints its value **one character per line**: `/system/identity/print`
|
|
109
|
+
returns `name: C` / `H` / `R` where 7.24.2 returns `name: CHR`. Measured on
|
|
110
|
+
stock CHR, the wrap is identical over REST `POST /rest/execute`, over
|
|
111
|
+
`ssh user@host "<cmd>"`, and over the mac-telnet console — including the
|
|
112
|
+
mac-telnet reader that has already answered the size probe with 512 columns,
|
|
113
|
+
and the ssh client that negotiates nothing. The console login modifiers
|
|
114
|
+
(`+c512w200h`, `+ct`, `+e`) do not change it, and it is not proportional to
|
|
115
|
+
the value's length. Every multi-property `print` is unaffected on the same
|
|
116
|
+
build, as is `:put [/system/identity/get name]`.
|
|
117
|
+
|
|
118
|
+
Consequence for this command: `data.ret` carries the device's console text
|
|
119
|
+
**verbatim**, wrap included — centrs cannot tell a layout newline from
|
|
120
|
+
content, and inventing a de-wrapper would corrupt legitimate output. Callers
|
|
121
|
+
that need a value rather than a rendering should read it structurally
|
|
122
|
+
(`retrieve`, or `api`) or ask the device for the scalar
|
|
123
|
+
(`:put [<menu>/get <property>]`). The integration examples compare identities
|
|
124
|
+
with `print` column wrapping collapsed for this reason — see
|
|
125
|
+
`collapsePrintWrapping` in `test/integration/chr.ts`.
|
|
83
126
|
- Attribute values pass through **verbatim** — centrs does not guess types or
|
|
84
127
|
coerce them (RouterOS REST tolerates string values; the native API is
|
|
85
128
|
all-strings). Unlike `jo`-style KV builders, there is no per-value
|
|
@@ -187,8 +230,8 @@ many tokens):
|
|
|
187
230
|
it fans out; mixing it with positionals or CDB selectors is
|
|
188
231
|
`usage/conflicting-flags` (constitution: resolution providers).
|
|
189
232
|
|
|
190
|
-
Because `execute` is the primary **write** path, a write-shaped fan-out
|
|
191
|
-
|
|
233
|
+
Because `execute` is the primary **write** path, a write-shaped fan-out is gated
|
|
234
|
+
by **`--yes`**, confirmed **once** up front; without it
|
|
192
235
|
the error names the blast radius (how many routers). Write-ness is determined from
|
|
193
236
|
the command alone (`canonicalizeExecuteCommand` + `isWriteShaped`), so the gate
|
|
194
237
|
fires before any target is dialed. Per-target validation still runs because
|
|
@@ -73,7 +73,7 @@ arbitrary script expressions. The `$a` variable must survive the validation
|
|
|
73
73
|
wrapper: if that outer string expands it before `:parse`, this example fails even
|
|
74
74
|
though RouterOS accepts the original command.
|
|
75
75
|
|
|
76
|
-
### 5. Syntax reject
|
|
76
|
+
### 5. Syntax reject before the write round-trip
|
|
77
77
|
|
|
78
78
|
Malformed CLI is rejected by the syntax gate before semantic validation or the
|
|
79
79
|
write round-trip.
|
|
@@ -82,9 +82,16 @@ write round-trip.
|
|
|
82
82
|
centrs execute $R '/ip/address/add address="unterminated interface=ether1' --via rest-api --username $U --password $P --yes
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
Envelope: `ok: false`, `error.code=validation/syntax`, `
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
Envelope: `ok: false`, `error.code=validation/syntax`, `meta.via=rest-api`, and
|
|
86
|
+
no address is added.
|
|
87
|
+
|
|
88
|
+
**Which stage rejects it changed in GH#354.** This input is now caught by the
|
|
89
|
+
*offline* stage, so `error.context.validationStage="offline"`,
|
|
90
|
+
`error.cause="explain/canonicalizer/unterminated-string"`,
|
|
91
|
+
`error.context.span={"start":24,"end":54}`, and the device is never contacted —
|
|
92
|
+
see V1–V3 below. It reached `:put [:parse ...]` before that. The code, the
|
|
93
|
+
`meta.via`, and the no-mutation guarantee are unchanged, which is why the
|
|
94
|
+
assertion in `test/integration/execute.test.ts` still reads `validation/syntax`.
|
|
88
95
|
|
|
89
96
|
### 6. Semantic reject that `:parse` accepts
|
|
90
97
|
|
|
@@ -202,14 +209,16 @@ centrs execute $A "/ip/address/remove numbers=$ID" --via native-api --port $API_
|
|
|
202
209
|
Envelope: `ok: true`, `meta.via=native-api`. A subsequent retrieve of
|
|
203
210
|
`/ip/address` does not contain `$ID`.
|
|
204
211
|
|
|
205
|
-
### 15. Native syntax reject
|
|
212
|
+
### 15. Native syntax reject
|
|
206
213
|
|
|
207
214
|
```bash
|
|
208
215
|
centrs execute $A '/ip/address/add address="unterminated interface=ether1' --via native-api --port $API_PORT --username $U --password $P --yes
|
|
209
216
|
```
|
|
210
217
|
|
|
211
|
-
Envelope: `ok: false`, `error.code=validation/syntax`, `
|
|
212
|
-
|
|
218
|
+
Envelope: `ok: false`, `error.code=validation/syntax`, `meta.via=native-api`, and
|
|
219
|
+
no address is added. The native twin of example 5, and it moved the same way:
|
|
220
|
+
the offline stage rejects this input now, so the verdict no longer depends on
|
|
221
|
+
the transport at all.
|
|
213
222
|
|
|
214
223
|
### 16. Native semantic reject after clean parse
|
|
215
224
|
|
|
@@ -285,6 +294,10 @@ Envelope: `ok: true`, `meta.via=mac-telnet`, `data.ret` is the console output an
|
|
|
285
294
|
contains the device identity (cross-checked against REST). `meta.validation.source`
|
|
286
295
|
is `:put [:parse ...] over mac-telnet`.
|
|
287
296
|
|
|
297
|
+
RouterOS 7.23.6/7.24.3/7.25beta4 print a one-property menu's value one
|
|
298
|
+
character per line (GH#352), so the comparison collapses `print` column
|
|
299
|
+
wrapping on both sides rather than asserting the device's layout.
|
|
300
|
+
|
|
288
301
|
### 21. Write (add) over mac-telnet
|
|
289
302
|
|
|
290
303
|
```bash
|
|
@@ -329,6 +342,10 @@ Envelope: `ok: true`, `meta.via=ssh`, `data.ret` is the cleaned console output a
|
|
|
329
342
|
contains the device identity (cross-checked against REST). `meta.validation.source`
|
|
330
343
|
is `:put [:parse ...] over ssh`.
|
|
331
344
|
|
|
345
|
+
RouterOS 7.23.6/7.24.3/7.25beta4 print a one-property menu's value one
|
|
346
|
+
character per line (GH#352), so the comparison collapses `print` column
|
|
347
|
+
wrapping on both sides rather than asserting the device's layout.
|
|
348
|
+
|
|
332
349
|
### S2. Multi-line read returns cleaned, column-aligned output
|
|
333
350
|
|
|
334
351
|
```bash
|
|
@@ -356,6 +373,150 @@ centrs execute 127.0.0.1 '/ip/address/add address=198.51.100.51/32 interface=eth
|
|
|
356
373
|
Envelope: `ok: false`, `error.code=validation/unknown-attribute`, `meta.via=ssh`,
|
|
357
374
|
and no address is added — the console `:parse` gate catches `bogus` pre-mutation.
|
|
358
375
|
|
|
376
|
+
## Offline gate (stage 1)
|
|
377
|
+
|
|
378
|
+
Validation runs offline before it runs on the device
|
|
379
|
+
([`docs/CONSTITUTION.md` → Validation is the product](../../docs/CONSTITUTION.md#validation-is-the-product),
|
|
380
|
+
GH#354). These two prove the stage exists and that a stage-1 rejection reaches no
|
|
381
|
+
router. Run by `test/integration/execute.test.ts`.
|
|
382
|
+
|
|
383
|
+
### V1. Offline reject, with the device stage never asked
|
|
384
|
+
|
|
385
|
+
`:put [` is an unclosed bracket. The old local quote preflight counted quotes
|
|
386
|
+
only, so this used to cost a full round trip to be told no; the analyzer rejects
|
|
387
|
+
it with a byte span before anything is dialed.
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
centrs execute $R ':put [' --via rest-api --username $U --password $P --json
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Envelope: `ok: false`, `error.code=validation/syntax`,
|
|
394
|
+
`error.context.validationStage="offline"`,
|
|
395
|
+
`error.context.span={"start":5,"end":6}`,
|
|
396
|
+
`error.context.diagnostics[0].code="explain/canonicalizer/unclosed"`,
|
|
397
|
+
`error.position` absent (the offline byte offset is never dressed up as a
|
|
398
|
+
RouterOS-reported one), `meta.validation.source` is the offline analyzer, and
|
|
399
|
+
`meta.validation.stages` is `offline: failed` followed by `device: skipped`.
|
|
400
|
+
|
|
401
|
+
### V2. The same rejection with no reachable router at all
|
|
402
|
+
|
|
403
|
+
The proof that stage 1 opens no connection: point the same command at a port
|
|
404
|
+
nothing is listening on. A gate that dialed first would report a
|
|
405
|
+
`transport/*` error or hang until the timeout.
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
centrs execute 127.0.0.1:1 ':put [' --via rest-api --username $U --password $P --json
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Envelope: identical to V1 — `ok: false`, `error.code=validation/syntax`,
|
|
412
|
+
`error.context.validationStage="offline"` — and **not** a `transport/*` code.
|
|
413
|
+
|
|
414
|
+
### V3. `--validate=false` disables the offline stage too
|
|
415
|
+
|
|
416
|
+
One flag, one meaning. The escape hatch has to reach stage 1, or a caller hitting
|
|
417
|
+
an analyzer defect would have no way past it. RouterOS then rejects the command
|
|
418
|
+
itself on the run.
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
centrs execute $R ':put [' --via rest-api --username $U --password $P --json --validate=false
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Envelope: `ok: false`, `error.code=routeros/request-failed` with RouterOS's own
|
|
425
|
+
`syntax error (line 1 column 7)` in the summary — the **device** rejecting it, not
|
|
426
|
+
centrs — plus `meta.validation.enabled=false`, `meta.validation.result="skipped"`,
|
|
427
|
+
and `meta.validation.stages` listing **both** stages as `skipped`. A gated
|
|
428
|
+
surface always reports every stage, so a consumer reading `stages` never has to
|
|
429
|
+
special-case the disabled shape.
|
|
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
|
+
|
|
359
520
|
## Target selection (fan-out)
|
|
360
521
|
|
|
361
522
|
These exercise the shared target-selection grammar
|