@tikoci/centrs 0.1.9-next.23 → 0.1.9-next.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/GLOSSARY.txt +14 -0
  3. package/README.md +2 -2
  4. package/commands/api/AGENTS.md +18 -1
  5. package/commands/api/README.md +10 -6
  6. package/commands/api/examples.md +1 -1
  7. package/commands/execute/README.md +26 -6
  8. package/commands/execute/examples.md +24 -5
  9. package/commands/explain/README.md +452 -61
  10. package/commands/explain/examples.md +191 -41
  11. package/commands/retrieve/README.md +13 -2
  12. package/commands/retrieve/examples.md +30 -1
  13. package/commands/transfer/README.md +8 -4
  14. package/docs/CLI.md +19 -5
  15. package/docs/CONSTITUTION.md +14 -9
  16. package/docs/MATRIX.md +34 -21
  17. package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
  18. package/docs/errors/routeros/permission-denied.md +24 -0
  19. package/docs/errors/transport/edge-auth-failed.md +22 -0
  20. package/docs/errors/transport/write-outcome-unknown.md +3 -0
  21. package/docs/errors/usage/invalid-cursor.md +20 -0
  22. package/docs/errors/validation/unknown-path.md +9 -0
  23. package/package.json +2 -2
  24. package/src/api.ts +34 -39
  25. package/src/cli/explain.ts +206 -47
  26. package/src/core/envelope.ts +10 -3
  27. package/src/core/error-catalog.ts +15 -0
  28. package/src/core/execute-command.ts +53 -0
  29. package/src/core/fanout.ts +12 -2
  30. package/src/core/inspect.ts +4 -5
  31. package/src/core/routeros-errors.ts +25 -0
  32. package/src/execute-cmr.ts +2 -8
  33. package/src/execute.ts +48 -20
  34. package/src/explain/args.ts +115 -10
  35. package/src/explain/group-faults.ts +162 -29
  36. package/src/explain/operators.ts +2 -1
  37. package/src/explain/scope-brace.ts +226 -11
  38. package/src/explain/segment.ts +136 -1
  39. package/src/explain/symbols.ts +146 -6
  40. package/src/explain/values.ts +55 -4
  41. package/src/explain-live-facets.ts +739 -0
  42. package/src/explain-live-types.ts +490 -0
  43. package/src/explain-live.ts +593 -0
  44. package/src/explain.ts +395 -43
  45. package/src/index.ts +6 -0
  46. package/src/protocols/adapter.ts +88 -10
  47. package/src/protocols/mac-telnet.ts +38 -10
  48. package/src/protocols/native-api.ts +13 -12
  49. package/src/retrieve-follow.ts +2 -1
  50. package/src/retrieve-sample.ts +2 -1
  51. package/src/retrieve-wait.ts +10 -6
  52. package/src/retrieve.ts +77 -27
  53. package/src/transfer.ts +38 -13
package/CHANGELOG.md CHANGED
@@ -17,6 +17,29 @@ part of this section.
17
17
 
18
18
  ### Added
19
19
 
20
+ - **Live `explain` over rest-api + native-api is `CHR-passed`** (#236).
21
+ Examples 7–16, 19, 24, 48 and 52–53 pass on both transports on stable
22
+ 7.24.5 and long-term 7.23.8. `runtimeAcceptance` stays `not-proven`.
23
+ - `explain <router> '<input>'` and `explainLiveEnvelope()` add read-only parse,
24
+ highlight and command-argument evidence over REST/native API. Probe outcomes,
25
+ byte coverage and provenance remain explicit; analyzed input never executes (#491).
26
+ - Live `explain` fills the two live value facts (#225). `observedType` names a
27
+ type only where the device visibly converts a bare scripting literal
28
+ (`2.2` → `2.0.0.2` is `ip`, `0x10` → `16` is `num`), read from an escaped
29
+ `:parse` of the literal alone; an unchanged rendering abstains, and no
30
+ `:typeof` is sent. `schemaType` normalizes the argument's live `syntax`
31
+ declaration into leaf types (`gateway` → `ip`, `ip6`, `enum`), with the
32
+ device's own term in `declared`. Menu arguments are never observed, and
33
+ `op` has no producer.
34
+ - Live `explain --schema` describes the path at the cursor: a menu's verbs and
35
+ submenus with counts, a command's arguments with their declared syntax, and
36
+ `print`'s proplist. `--complete [--cursor <byte>]` returns menu, verb,
37
+ argument-name, `where` variable or value candidates. They are the device's
38
+ own input completion at the cursor, cited by `ev`; declared names and
39
+ syntax stay in `--schema`. `--full` lifts the sizing limits up to a safety
40
+ ceiling.
41
+ Only probe shapes that answer on RouterOS 7.12.2 and 7.20.8 are sent (#492).
42
+
20
43
  - `centrs --version` / `-V` prints the package version alone; `--json` (or
21
44
  `--format json|yaml`) returns an envelope with `data.version` (#463).
22
45
  - `execute --cmr` warns `cmr/caller-policy-not-enforced` when a write runs
@@ -72,6 +95,34 @@ part of this section.
72
95
 
73
96
  ### Changed
74
97
 
98
+ - **Breaking:** permission and edge-auth failures get their own error codes
99
+ (#541). Three codes change for `--json` and library readers:
100
+ - A REST policy refusal (HTTP 500 `not enough permissions (9)` or `std
101
+ failure: not allowed (9)`): `transport/connection-closed` →
102
+ `routeros/permission-denied`. It is a deterministic router answer, so it
103
+ is never retried and no longer carries
104
+ `transport/write-outcome-unknown`.
105
+ - A native-API `not enough permissions (9)` trap: `routeros/api-trap` →
106
+ `routeros/permission-denied`. Retrieve's path probe and live explain's
107
+ child probe surface it instead of reading "path doesn't exist".
108
+ - A 401/403 from a layer in front of RouterOS (auth proxy, tunnel edge,
109
+ WAF): `transport/auth-failed` → `transport/edge-auth-failed`, with the
110
+ challenge scheme, server, content type and `cf-ray` presence as context
111
+ (no secrets). RouterOS's own 401 — JSON `{"error":401,...}`, measured on
112
+ CHR 7.12.1, 7.16.2, 7.23.8, 7.25, 7.26beta1 and 7.26beta3 — stays
113
+ `transport/auth-failed`.
114
+ - **`meta.validation` success shapes (#535).** This is a breaking change for
115
+ `--json` readers of the flat fields. `syntax` and `semantic` are now present
116
+ only when a gate stage gave that verdict; absent means no stage was asked.
117
+ - `api` structured requests (no `:parse`) drop `syntax: false`.
118
+ - Script input drops `semantic: "not-applicable"`; the value is gone from
119
+ the type.
120
+ - `--validate=false` drops both fields on `execute` and `api`.
121
+ - `retrieve`, `transfer` and `api` structured successes now report
122
+ `stages[]`. `transfer list` runs no existence probe, so it reports
123
+ `{ enabled: false }` instead of claiming one.
124
+ - `explain` reports `meta.validation: { enabled: false }` with no `result`
125
+ in all three cases (offline, live, error); it is not a gate.
75
126
  - Text stdout is data (#470). In text mode, single-target `execute`, `api`,
76
127
  `retrieve` and `transfer` write only the result to stdout. Everything else
77
128
  goes to **stderr** on success and on failure: the `--verbose` header, one
@@ -150,6 +201,13 @@ part of this section.
150
201
 
151
202
  ### Fixed
152
203
 
204
+ - `retrieve --sample`, `--follow` and `--wait` no longer report a validation
205
+ verdict the gate never gave (#537). A cancellation before the gate ran now
206
+ reports the device stage `not reached` (`interrupted` for `--sample` and
207
+ `--follow`, `wait/interrupted` for `--wait`) instead of
208
+ `{enabled: true, result: "skipped"}`. A `--wait` cancelled after retried,
209
+ unanswered attempts no longer reports `result: "failed"`. With validation
210
+ off, all three report `{enabled: false, source: "disabled"}`.
153
211
  - Abbreviated output-file arguments (`fi=`, `file-n=`), and the spaced
154
212
  `file-name value` form under print/export/serialize, require write
155
213
  confirmation in `execute`, including inside scripts, and are refused for
@@ -181,6 +239,50 @@ part of this section.
181
239
  a native API login. A write that failed after it was sent, in single-target
182
240
  or fan-out mode, carries the new warning `transport/write-outcome-unknown`,
183
241
  because it may have run. Reads keep the retry.
242
+ - A bare known-menu path (`/system/identity`) reads as a read in `execute`,
243
+ not `usage/confirmation-required` (#543). It still answers nothing, so the
244
+ envelope steers to `/print` / `retrieve` with `tip/execute-bare-menu-path`.
245
+ Unknown paths, known-mutator verbs, and any attribute or query stay gated.
246
+ - `retrieve /system/routerboard` on a router without the menu (CHR) names the
247
+ workaround: the `validation/unknown-path` remediation points at
248
+ `/system/license --attributes system-id` instead of the generic text (#545).
249
+ - `execute` warns when a multi-statement write is refused for permission after
250
+ earlier statements may have run (#549). Such a refusal carries
251
+ `transport/write-outcome-unknown` alongside `routeros/permission-denied`,
252
+ counting statements inside executed `:if`/`:for`/`:foreach`/`:while`/`:do`/`:retry`/`:onerror` scopes and
253
+ command substitutions (`[…]`) as well as top-level ones; a `/menu { … }`
254
+ prefix is navigation, not a statement. Single refused statements, including
255
+ `remove [find …]` and one wrapped in a menu block, stay silent. The refusal itself
256
+ now surfaces as the error instead of `ok:true`: `/execute` answers HTTP 200
257
+ with `not enough permissions (9) (…; line N)` as the last `ret` line after
258
+ earlier statements' output (CHR 7.25, both transports).
259
+ - The offline gate no longer hard-rejects first-segment script-body
260
+ abbreviations it cannot prove (#525). `down={ :beep }`, accepted on device
261
+ as `down-script=`, warns `explain/canonicalizer/possible-script-body-abbreviation` and
262
+ reaches device validation instead of failing with `invalid-command-brace`. Bodies
263
+ opening with one bare command word or after a comment line warn too, and
264
+ the brace reads as a body for every reader, and whitespace after the `=`
265
+ (`down= { :beep }`) warns like the glued spelling. A bare word followed by
266
+ `;`-separated members keeps its error, as do the other array-content
267
+ braces (`comment= {1;2}` included), the space-for-`=` spelling
268
+ (`comment { :beep }`), positional braces, root directive slots
269
+ (`:put x={…}`), and `where`; `comment={ :beep }` deliberately warns though
270
+ the device rejects it.
271
+ - Native-API traps classify authentication by sentence, never trap text
272
+ (#540). Only a trap on `/login` is `transport/auth-failed`; a later command
273
+ rejected with login-like text (`/user/add name=-2` answering `invalid user
274
+ name`) is `routeros/command-failed`, agreeing with REST.
275
+ - `explain` symbols declare the value of named `name=`/`counter=` spellings in
276
+ any argument order (#533). `:local name=no` declares `no`, and
277
+ `:for from=1 counter=i to=2` binds `i`, so later `$` references link;
278
+ previously the argument name was declared and a reordered `counter=` left
279
+ `$i` unbound.
280
+ - mac-telnet with an explicit directed-broadcast `--host` (`192.168.74.255`)
281
+ sends the owning interface's real MAC (#546, #547). On Linux, `connect()` to
282
+ a broadcast reports `0.0.0.0`, so the session fell back to a synthetic
283
+ source MAC, which hardware ignores: a 30 s timeout on an RB1100Dx4 running
284
+ 7.26beta3. On that unit a real MAC gets a reply in ~0.35 s and a synthetic
285
+ one none. CHR answers both, so the CHR suite never saw it.
184
286
 
185
287
  ### Maintenance
186
288
 
@@ -200,6 +302,10 @@ part of this section.
200
302
  (Security tab), and locally with `bun run lint:actions` (#266). Its first
201
303
  run found two `${{ … }}` expansions inside shell scripts, in `lab.yaml` and
202
304
  `qa.yaml`; both now go through `env:`.
305
+ - The unit tier no longer reads the developer's `~/.config/tikoci` state
306
+ (settings, CDB) (#550, #557). A `bunfig.toml` preload sandboxes `HOME` and
307
+ `XDG_CONFIG_HOME`, so a real and a fake home give the same result. Ambient
308
+ `CENTRS_*` variables are still read.
203
309
 
204
310
  ## 0.1.8 — 2026-10-12
205
311
 
package/GLOSSARY.txt CHANGED
@@ -821,3 +821,17 @@ apptraffic # two paths new in 7.26beta1 with different published gates: the `/to
821
821
  rle # the corpus's `highlight-rle-v1` response encoding: each highlight stream stored as run-length `[class, count]` pairs, decoded by `decodeHighlightRuns` in scripts/corpus-db.ts (#498)
822
822
  removenumbers # the IL head `/ip/firewall/address-list/removenumbers=…` that `remove [find …]` renders as, quoted verbatim in the bracket-`=` corroboration test (#498)
823
823
  vscod # the last segment of a corpus path the forum importer truncated at its slug limit (`…-editors-like-vscod/post-0014-snippet-02.rsc`); cited as the path, so the spelling must match the corpus row (#498)
824
+
825
+ # #358/#371 unquoted-byte controls. Deliberate NON-words: each puts a non-ASCII
826
+ # byte where RouterOS reads only ASCII, so the spelling is the probe.
827
+ systemé # `/systemé/identity print` — a non-ASCII byte in a path, `expected command name` at the `é` on CHR 7.24.5/7.23.8 (#371)
828
+ printé # `/system/identity printé` — a non-ASCII byte in a verb, rejected at the `é` (#371)
829
+ namé # `/system/identity/set namé=x` — a non-ASCII byte in a menu command's argument name, rejected at the name's first byte (#371)
830
+ addré # `/ip/address/add addré=1` — as `namé`: the device reports the argument name's first byte, not the `é` (#371)
831
+ caf # the ASCII prefix the device reads of `café` before the `é`: `:put "a$café"` is `$caf` then `é` (#371)
832
+
833
+ # #225 live value types: CLI Reference `Type` cell spellings the schema vocabulary maps.
834
+ iface # the CLI Reference type `iface_enum`: an argument that names an interface, which `schemaType` calls `name` (#225)
835
+ ubit # the CLI Reference type `ubit (…)`: a set of listed bits, e.g. `ubit (pap, chap, mschap1, mschap2)`, a list of `enum` (#225)
836
+ forcounter # the IL head `/forcounter=$i;…` that `:for i …` renders as; quoted verbatim in the #225 directive-argument probe test (#225)
837
+ foreachcounter # the IL head `/foreachcounter=$i;…` that `:foreach i …` renders as, in any `counter=` order; quoted verbatim in the #533 reorder test (#533)
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # centrs
2
2
 
3
3
  `centrs` is the tikoci RouterOS interaction hub: a Bun/TypeScript library and
4
- CLI (with MCP, TUI, and HTTP proxy frontends planned) for talking to MikroTik
5
- RouterOS devices through a regularized, validated interface.
4
+ CLI (with an MCP server; TUI and HTTP proxy frontends planned) for talking to
5
+ MikroTik RouterOS devices through a regularized, validated interface.
6
6
 
7
7
  Preview builds are published on npm. To try the current offline analyzer without
8
8
  installing centrs or connecting to a router:
@@ -184,7 +184,7 @@ green on CHR 7.23.1.
184
184
  validity (`request=child` ⇒ `validation/unknown-attribute`) fire
185
185
  identically over rest-api and native-api, **before** any write — no `:put
186
186
  [:parse]` involved (api input is a path, not a CLI string). A `/execute` script
187
- is a CLI string ⇒ `meta.validation.semantic = "not-applicable"`.
187
+ is a CLI string ⇒ no `meta.validation.semantic` field (#535).
188
188
 
189
189
  ## CONFIRMED ON CHR (RouterOS 7.12.2 compatibility — GH#343, 2026-09-14)
190
190
 
@@ -197,6 +197,23 @@ green on CHR 7.23.1.
197
197
  validation therefore uses command-level `request=child` only. This does not
198
198
  prohibit argument-level completion probes such as `print,proplist`, which are
199
199
  a separate shape and returned immediately on the same 7.12.2 controls.
200
+ - **Command-level `request=syntax` is unsafe too, on 7.12.2 and 7.20.8** (#492,
201
+ 2026-10-19, reproduced on a fresh boot of each).
202
+ - REST: `syntax` with `path=` set to a command (`ip,address,set`,
203
+ `system,identity,set`, `ip,route,add`, `interface,print`) got no answer
204
+ within 12 s, 5 of 5 times. The `child` controls queued behind it timed out
205
+ until the handler recovered about 30 s later.
206
+ - Native API closed the connection.
207
+ - The same syntax requests at a menu (`ip,address`) or an argument
208
+ (`ip,address,set,address`, `put,message`, `do,command`) answered in about
209
+ 25 ms. 7.23.8 and 7.24.5 answer every shape.
210
+ - restraml's crawlers send `syntax` to `arg` nodes only, so `inspect.json`
211
+ never exercised the hanging shape.
212
+ - Input-based `completion` (`input="/ip/address add comm"`) answered at every
213
+ position tried on both builds, including scripting keywords and `do={`
214
+ bodies on 7.20.8.
215
+ - Explain's `--schema`/`--complete` therefore send `syntax` for argument
216
+ paths and `completion` with `input=` only.
200
217
 
201
218
  ## CONFIRMED ON CHR (Phase 4 `--stream` follow — CHR 7.23.1, 2026-06-30)
202
219
 
@@ -114,7 +114,9 @@ vs `--query` distinction.
114
114
  before they were sent, keep the `transport/network` /
115
115
  `transport/connection-closed` retry. A mutating request that failed after it
116
116
  was sent carries the warning `transport/write-outcome-unknown`, in single-target
117
- and fan-out mode alike.
117
+ and fan-out mode alike. A multi-statement `/execute` script refused with
118
+ `routeros/permission-denied` carries the same warning (#549); a single
119
+ refused statement, and a refused structured request, stay silent.
118
120
  - **`--stream` (or a `/listen` endpoint) is single-session** → `usage/fanout-not-supported` in
119
121
  fan-out mode. **`--raw` strips the envelope** → `usage/conflicting-flags` in
120
122
  fan-out mode (per-target envelopes can't be bare).
@@ -134,7 +136,8 @@ gate is `/console/inspect` — **not** `:put [:parse]`:
134
136
  does) answers normally on the same build. See the 7.12.2 finding in
135
137
  `AGENTS.md` (GH#343).
136
138
  - **Carve-out:** a script-shaped `POST /rest/execute` is a CLI string, not a path,
137
- so structured inspect is `not-applicable` (`meta.validation.semantic`). It
139
+ so structured inspect does not apply and `meta.validation.semantic` is
140
+ absent. It
138
141
  follows the CLI-string pipeline instead: offline analysis, then live
139
142
  `:put [:parse ...]`, then run. An offline rejection is still a byte span with
140
143
  no connection; an offline pass or abstention continues to the device stage.
@@ -148,10 +151,11 @@ gate is `/console/inspect` — **not** `:put [:parse]`:
148
151
  unaffected — the result matchers are anchored, not substring.
149
152
  - Structured path requests are **not** offline-gated. The input is a path plus a
150
153
  body, not a CLI string, and an offline *path* gate collides with GH#211's
151
- unlisted-path decision. On the success path `meta.validation.stages[]` is
152
- absent there; on a `validation/*` rejection it is present and reports the
153
- offline stage `skipped` with that reason, so the breakdown never implies an
154
- analysis that did not run.
154
+ unlisted-path decision. `meta.validation.stages[]` reports the offline stage
155
+ `skipped` with that reason, on success and on a `validation/*` rejection
156
+ alike, so the breakdown never implies an analysis that did not run. On
157
+ success the device stage is `passed`, `semantic` is `true`, and there is no
158
+ `syntax` field, because no `:parse` ran (#535).
155
159
 
156
160
  `--validate=false` skips **both** stages of the preflight — one flag, one meaning
157
161
  (constitution: validation); RouterOS still re-validates writes server-side.
@@ -156,7 +156,7 @@ centrs api $R execute -X POST -f script=':put [/system/identity/get name]' --use
156
156
  ```
157
157
 
158
158
  Envelope: `ok: true`, `data` is string-shaped and contains the CHR identity;
159
- `meta.validation.semantic=not-applicable` (script, not a path) and
159
+ `meta.validation.syntax=true` with no `semantic` field (script, not a path) and
160
160
  `meta.validation.stages` is `offline: passed` then live `device: passed` from
161
161
  `:put [:parse ...]`.
162
162
 
@@ -44,7 +44,14 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
44
44
  `isWriteShaped` is deliberately independent: script fallback for `[find]`, a
45
45
  positional id, or other non-`key=value` input never bypasses confirmation.
46
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
47
+ write-shaped operation. A truly bare known-menu path (`/system/identity`,
48
+ example W6) is the one exception: structured mode proves the input is a
49
+ single path token, so it is console navigation — a no-op read — rather than
50
+ a command. The device still answers truthfully (`no such command` on the
51
+ APIs) and the envelope carries a steering tip naming the read that answers.
52
+ A bare no-argument *command* (`/system/reboot`) stays gated, as does
53
+ script-branch navigation (#387), where a later line may carry a relative
54
+ verb. In script mode every command head in a statement
48
55
  (the statement start and each `[…]` or `{…}` body) must be a read: its verb
49
56
  is the first word past the known menu prefix, so `/ip address print` is a
50
57
  read and `/user expire-password print` is not (examples W3). Script
@@ -283,7 +290,14 @@ A sent write is **never retried** (#448). Fan-out retries a target only for
283
290
  when the failure came before the request was sent (for example while it was
284
291
  being validated). A write that failed after it was sent may have run, so its
285
292
  error envelope, single-target or fan-out, carries the warning
286
- `transport/write-outcome-unknown`.
293
+ `transport/write-outcome-unknown`. A multi-statement write refused with
294
+ `routeros/permission-denied` carries the same warning: statements before the
295
+ refused one may already have run (#549). Statements inside executed control
296
+ scopes (`:if … do={…}`) and command substitutions (`[…]`) count; stored bodies
297
+ (`source={…}`, `:global f do={…}`) do not, and a `/menu { … }` prefix is
298
+ navigation, not a statement. A single refused statement did not
299
+ run and stays silent, including one fed by a lone substitution
300
+ (`remove [find …]`) or wrapped in a menu block.
287
301
 
288
302
  ## CMR clients through their controller (`--cmr`)
289
303
 
@@ -361,8 +375,9 @@ when the target is itself a CMR client.
361
375
  controller's own row through `run-script`, and rebooted a client through
362
376
  both. So a "reader" login on a 7.26beta1 controller is not read-only, and
363
377
  centrs' write gate (and `mcp=ro` on MCP) is the only gate. MikroTik
364
- [says](https://forum.mikrotik.com/t/7-26beta-development-is-released/273582/139) 7.26beta2 refuses this for a `read` group; that is not yet
365
- checked on a beta2 build. For a write,
378
+ [says](https://forum.mikrotik.com/t/7-26beta-development-is-released/273582/139) 7.26beta2 refuses this for a `read` group; #527 now carries
379
+ direct 7.26beta3 evidence that a restricted caller is refused, with the
380
+ first refusing release still unbracketed. For a write,
366
381
  centrs reads the login's own `/user` row and `/user/group` once (both are
367
382
  readable with just `read` + `api`). When the login lacks `write`, `test` or
368
383
  `reboot`, the result and the confirmation error carry
@@ -520,8 +535,13 @@ How it is wired:
520
535
  correctness issue — see `commands/terminal/README.md` for the optional fix).
521
536
  - **Delivery:** the UDP transport sends to `--host`/`--port` (default L2 broadcast
522
537
  `255.255.255.255:20561`); the device replies to our source IP:port, and the
523
- in-packet MACs do the addressing. The CHR test points `--host`/`--port` at the
524
- loopback L2 bridge.
538
+ in-packet MACs do the addressing. The device answers only broadcast delivery
539
+ (unicast gets no reply), and only when the in-packet *source* MAC is the
540
+ egress interface's real MAC — a synthetic source gets no reply on hardware
541
+ (CHR answers it, so the suite cannot catch a wrong source). With the default
542
+ host centrs sprays per-interface discovery with real MACs; with an explicit
543
+ `--host <directed-broadcast>` it resolves the owning interface's MAC. The CHR
544
+ test points `--host`/`--port` at the loopback L2 bridge.
525
545
 
526
546
  Protocol selection: an unresolved MAC target auto-selects mac-telnet (covered by
527
547
  `test/unit/execute.test.ts`); callers wanting IP-level execution opt into ARP
@@ -67,9 +67,9 @@ centrs execute $R ':local a true; :if ($a = true) do={:put [/system/identity/get
67
67
 
68
68
  Envelope: `ok: true`, `data` is string-shaped (`data.output` or `data.ret`) and
69
69
  contains the CHR identity name, `meta.via=rest-api`, and
70
- `meta.validation.syntax=true`. `meta.validation.semantic` is absent or marked
71
- `not-applicable` because `/console/inspect` validates path-shaped commands, not
72
- arbitrary script expressions. The `$a` variable must survive the validation
70
+ `meta.validation.syntax=true`. `meta.validation.semantic` is absent because
71
+ `/console/inspect` validates path-shaped commands, not arbitrary script
72
+ expressions (#535). 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
 
@@ -490,8 +490,8 @@ Envelope: `ok: false`, `error.code=validation/package-missing`,
490
490
  `error.context.packages` names the published package, the remediation points at
491
491
  `/system/package/print` and never at the syntax, and `error.position` and
492
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).
493
+ `meta.validation.syntax=false`, and `semantic` is absent because the
494
+ name-level check was never asked (GH#435, #535).
495
495
 
496
496
  ### V6. A menu no RouterOS build has is an unknown path
497
497
 
@@ -633,6 +633,25 @@ This is a grounding version, not an introduction boundary. Unknown or
633
633
  ambiguous output-file prefixes stay write-shaped until the device resolves
634
634
  them.
635
635
 
636
+ ### W6. A bare menu path needs no confirmation and answers with a steering tip
637
+
638
+ ```bash
639
+ centrs execute $R '/system/identity' --via rest-api --username $U --password $P
640
+ ```
641
+
642
+ No `--yes` gate: a truly bare known-menu path is console navigation, a no-op
643
+ read, and structured mode proves the input is a single path token with no
644
+ follow-up. The device still answers truthfully — REST reports
645
+ `routeros/request-failed`, native API `routeros/api-trap`, both carrying
646
+ RouterOS's own `no such command` — and the envelope carries
647
+ `tip/execute-bare-menu-path`, naming `/system/identity/print` (or
648
+ `centrs retrieve $R /system/identity`) as the read that answers. A bare
649
+ no-argument *command* such as `/system/reboot` stays write-shaped and still
650
+ requires confirmation.
651
+
652
+ `test/integration/execute-write-gate.test.ts` proves the gate stays open for
653
+ navigation and shut for no-arg writes, on REST and native API.
654
+
636
655
  ### R1. A post-validation `/execute` rejection fails the overall envelope
637
656
 
638
657
  ```bash