@tikoci/centrs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/GLOSSARY.txt +420 -0
  3. package/README.md +196 -0
  4. package/SECURITY.md +30 -0
  5. package/commands/AGENTS.md +50 -0
  6. package/commands/btest/README.md +257 -0
  7. package/commands/btest/examples.md +190 -0
  8. package/commands/check/README.md +59 -0
  9. package/commands/devices/README.md +405 -0
  10. package/commands/devices/examples.md +488 -0
  11. package/commands/discover/README.md +170 -0
  12. package/commands/discover/examples.md +98 -0
  13. package/commands/execute/README.md +208 -0
  14. package/commands/execute/examples.md +353 -0
  15. package/commands/mcp/README.md +260 -0
  16. package/commands/mcp/examples.md +158 -0
  17. package/commands/retrieve/README.md +209 -0
  18. package/commands/retrieve/examples.md +306 -0
  19. package/commands/settings/README.md +69 -0
  20. package/commands/stream/README.md +70 -0
  21. package/commands/terminal/README.md +177 -0
  22. package/commands/terminal/examples.md +144 -0
  23. package/commands/transfer/README.md +315 -0
  24. package/commands/transfer/examples.md +368 -0
  25. package/docs/CONSTITUTION.md +417 -0
  26. package/docs/MATRIX.md +224 -0
  27. package/docs/errors/README.md +74 -0
  28. package/docs/errors/auth/failed.md +9 -0
  29. package/docs/errors/cdb/already-exists.md +9 -0
  30. package/docs/errors/cdb/created.md +9 -0
  31. package/docs/errors/cdb/decrypt-failed.md +9 -0
  32. package/docs/errors/cdb/empty-group.md +9 -0
  33. package/docs/errors/cdb/invalid-option.md +9 -0
  34. package/docs/errors/cdb/invalid-record.md +9 -0
  35. package/docs/errors/cdb/not-found-target.md +9 -0
  36. package/docs/errors/cdb/not-found.md +9 -0
  37. package/docs/errors/cdb/override-applied.md +9 -0
  38. package/docs/errors/cdb/parse-failed.md +9 -0
  39. package/docs/errors/cdb/password-not-needed.md +9 -0
  40. package/docs/errors/cdb/password-required.md +9 -0
  41. package/docs/errors/cdb/reserved-key.md +9 -0
  42. package/docs/errors/cdb/reserved-option.md +9 -0
  43. package/docs/errors/cdb/target-not-registered.md +9 -0
  44. package/docs/errors/cdb/unknown-field.md +9 -0
  45. package/docs/errors/cdb/unknown-option.md +9 -0
  46. package/docs/errors/cdb/write-not-permitted.md +9 -0
  47. package/docs/errors/discover/broadcast-unavailable.md +9 -0
  48. package/docs/errors/discover/failed.md +9 -0
  49. package/docs/errors/identity/ambiguous.md +9 -0
  50. package/docs/errors/identity/no-match.md +9 -0
  51. package/docs/errors/input/invalid-command.md +9 -0
  52. package/docs/errors/input/invalid-match.md +9 -0
  53. package/docs/errors/input/invalid-path.md +9 -0
  54. package/docs/errors/input/invalid-routeros-path.md +9 -0
  55. package/docs/errors/input/invalid-target.md +9 -0
  56. package/docs/errors/input/local-file-not-found.md +9 -0
  57. package/docs/errors/input/mac-address.md +9 -0
  58. package/docs/errors/input/max-results-exceeded.md +9 -0
  59. package/docs/errors/internal/devices-failed.md +9 -0
  60. package/docs/errors/internal/mcp-start.md +9 -0
  61. package/docs/errors/internal/mcp-tool.md +9 -0
  62. package/docs/errors/internal/native-api-length.md +9 -0
  63. package/docs/errors/internal/unhandled.md +9 -0
  64. package/docs/errors/internal/unreachable.md +9 -0
  65. package/docs/errors/mndp/encode-failed.md +9 -0
  66. package/docs/errors/mndp/listen-failed.md +9 -0
  67. package/docs/errors/mndp/malformed.md +9 -0
  68. package/docs/errors/routeros/api-fatal.md +9 -0
  69. package/docs/errors/routeros/api-protocol.md +9 -0
  70. package/docs/errors/routeros/api-trap.md +9 -0
  71. package/docs/errors/routeros/btest-protocol.md +12 -0
  72. package/docs/errors/routeros/btest-too-many-sessions.md +9 -0
  73. package/docs/errors/routeros/command-failed.md +14 -0
  74. package/docs/errors/routeros/ec-srp5-protocol.md +11 -0
  75. package/docs/errors/routeros/error.md +9 -0
  76. package/docs/errors/routeros/invalid-value.md +14 -0
  77. package/docs/errors/routeros/mac-telnet-error.md +9 -0
  78. package/docs/errors/routeros/mac-telnet-not-ready.md +9 -0
  79. package/docs/errors/routeros/mac-telnet-protocol.md +9 -0
  80. package/docs/errors/routeros/mac-telnet-unsupported-auth.md +9 -0
  81. package/docs/errors/routeros/protocol-not-implemented.md +9 -0
  82. package/docs/errors/routeros/request-failed.md +9 -0
  83. package/docs/errors/routeros/session-closed.md +13 -0
  84. package/docs/errors/routeros/unknown-attribute.md +13 -0
  85. package/docs/errors/routeros/unknown-path.md +15 -0
  86. package/docs/errors/routeros/unsupported-capability.md +9 -0
  87. package/docs/errors/settings/invalid-boolean.md +9 -0
  88. package/docs/errors/settings/invalid-format.md +9 -0
  89. package/docs/errors/settings/invalid-integer.md +9 -0
  90. package/docs/errors/settings/invalid-timeout.md +9 -0
  91. package/docs/errors/settings/invalid-via.md +9 -0
  92. package/docs/errors/settings/unsafe-protocol-blocked.md +9 -0
  93. package/docs/errors/target/mac-not-in-arp.md +14 -0
  94. package/docs/errors/target/mac-required.md +23 -0
  95. package/docs/errors/target/mac-unresolved.md +17 -0
  96. package/docs/errors/target/unresolved.md +9 -0
  97. package/docs/errors/transport/auth-failed.md +9 -0
  98. package/docs/errors/transport/auto-method.md +9 -0
  99. package/docs/errors/transport/capability-unsupported.md +22 -0
  100. package/docs/errors/transport/checksum-unavailable.md +9 -0
  101. package/docs/errors/transport/connection-closed.md +9 -0
  102. package/docs/errors/transport/connection-refused.md +9 -0
  103. package/docs/errors/transport/dns.md +9 -0
  104. package/docs/errors/transport/host-key-mismatch.md +9 -0
  105. package/docs/errors/transport/incomplete-transfer.md +9 -0
  106. package/docs/errors/transport/insecure-trust.md +9 -0
  107. package/docs/errors/transport/local-tool-missing.md +11 -0
  108. package/docs/errors/transport/network.md +9 -0
  109. package/docs/errors/transport/timeout.md +9 -0
  110. package/docs/errors/transport/tls-certificate.md +9 -0
  111. package/docs/errors/transport/unreachable.md +9 -0
  112. package/docs/errors/transport/unsupported-operation.md +9 -0
  113. package/docs/errors/usage/confirmation-required.md +9 -0
  114. package/docs/errors/usage/conflicting-flags.md +9 -0
  115. package/docs/errors/usage/invalid-concurrency.md +9 -0
  116. package/docs/errors/usage/missing-group.md +9 -0
  117. package/docs/errors/usage/not-implemented.md +9 -0
  118. package/docs/errors/usage/target-exists.md +9 -0
  119. package/docs/errors/usage/timeout-out-of-range.md +9 -0
  120. package/docs/errors/validation/not-implemented.md +9 -0
  121. package/docs/errors/validation/option.md +9 -0
  122. package/docs/errors/validation/syntax.md +9 -0
  123. package/docs/errors/validation/unknown-attribute.md +9 -0
  124. package/docs/errors/validation/unknown-path.md +9 -0
  125. package/package.json +96 -0
  126. package/src/AGENTS.md +7 -0
  127. package/src/btest.ts +906 -0
  128. package/src/cli/btest.ts +455 -0
  129. package/src/cli/common.ts +53 -0
  130. package/src/cli/devices.ts +527 -0
  131. package/src/cli/discover.ts +186 -0
  132. package/src/cli/execute.ts +333 -0
  133. package/src/cli/mcp.ts +109 -0
  134. package/src/cli/retrieve.ts +383 -0
  135. package/src/cli/terminal.ts +303 -0
  136. package/src/cli/transfer.ts +471 -0
  137. package/src/cli.ts +115 -0
  138. package/src/core/envelope.ts +221 -0
  139. package/src/core/error-catalog.ts +469 -0
  140. package/src/core/routeros-errors.ts +268 -0
  141. package/src/data/AGENTS.md +11 -0
  142. package/src/data/dude-db.ts +0 -0
  143. package/src/data/mndp-cache.ts +132 -0
  144. package/src/data/mndp.ts +405 -0
  145. package/src/data/winbox-cdb-write.ts +200 -0
  146. package/src/data/winbox-cdb.ts +913 -0
  147. package/src/devices-schema.ts +107 -0
  148. package/src/devices.ts +1717 -0
  149. package/src/discover.ts +802 -0
  150. package/src/errors.ts +189 -0
  151. package/src/execute.ts +1350 -0
  152. package/src/index.ts +353 -0
  153. package/src/mcp/config.ts +66 -0
  154. package/src/mcp/resources.ts +98 -0
  155. package/src/mcp/safety.ts +119 -0
  156. package/src/mcp/server.ts +225 -0
  157. package/src/mcp/tools.ts +657 -0
  158. package/src/mcp.ts +33 -0
  159. package/src/protocols/AGENTS.md +14 -0
  160. package/src/protocols/adapter.ts +866 -0
  161. package/src/protocols/btest-session.ts +1493 -0
  162. package/src/protocols/btest.ts +447 -0
  163. package/src/protocols/ec-srp5.ts +466 -0
  164. package/src/protocols/index.ts +118 -0
  165. package/src/protocols/mac-telnet-console.ts +655 -0
  166. package/src/protocols/mac-telnet.ts +1299 -0
  167. package/src/protocols/mtwei.ts +47 -0
  168. package/src/protocols/native-api.ts +796 -0
  169. package/src/protocols/sftp.ts +427 -0
  170. package/src/protocols/ssh.ts +297 -0
  171. package/src/resolver/cdb.ts +580 -0
  172. package/src/resolver/comment-kv.ts +360 -0
  173. package/src/resolver/index.ts +74 -0
  174. package/src/resolver/mac.ts +227 -0
  175. package/src/resolver/settings.ts +228 -0
  176. package/src/resolver/target.ts +442 -0
  177. package/src/retrieve-fanout.ts +533 -0
  178. package/src/retrieve.ts +1215 -0
  179. package/src/terminal.ts +444 -0
  180. package/src/transfer.ts +1554 -0
  181. package/src/tui.ts +22 -0
  182. package/src/webproxy.ts +16 -0
@@ -0,0 +1,98 @@
1
+ # discover — examples
2
+
3
+ Each numbered example is an executable spec. `discover` listens on a UDP
4
+ socket; MNDP needs a real layer-2 broadcast segment, which a `user`-mode SLIRP
5
+ CHR cannot carry. The L2 fabric now comes from a second `socket-connect` NIC
6
+ plus a host bridge (`README.md`, L2 validation policy). Examples are validated
7
+ by:
8
+
9
+ - **real L2 against CHR** — examples 1 (passive discover + neighbor fields), 2
10
+ (scan packet-count meta), and 4 (`--save` into a CDB): a CHR boots with a
11
+ `socket-connect` NIC and `test/integration/discover.test.ts` runs the
12
+ unmodified `discover()` path against a genuine RouterOS announcement,
13
+ cross-checked against REST (CHR 7.23.1, `bun run test:integration`);
14
+ - the pure codec against crafted packet fixtures
15
+ (`test/unit/mndp.test.ts`),
16
+ - the TTL cache with an injected clock (`test/unit/mndp-cache.test.ts`),
17
+ - the UDP listener over a loopback socket and the `--save` / port-in-use /
18
+ custom-group / de-dupe / encrypted-CDB paths (examples 3, 5, 6, 7) against an
19
+ ignored, per-test CDB path in the working tree (`test/unit/discover.test.ts`).
20
+
21
+ `$CDB` is a per-test CDB path; the real `~/.config/tikoci/winbox.cdb` is never
22
+ touched.
23
+
24
+ ## discover
25
+
26
+ ### 1. Passive discover with the default window
27
+
28
+ ```bash
29
+ centrs discover
30
+ ```
31
+
32
+ Listens for the default `15s`, broadcasting an immediate refresh then every 5s. Envelope:
33
+ `ok: true`, `meta.via: "mndp"`, `data` is `{ count, neighbors }` with
34
+ `neighbors` sorted by MAC. Each neighbor carries `mac`, optional `identity`,
35
+ `version`, `platform`, `board`, `uptimeSeconds`, `softwareId`,
36
+ `interfaceName`, `ipv4`, `ipv6`, plus `firstSeenAt` / `lastSeenAt`.
37
+
38
+ ### 2. Short scan with JSON output
39
+
40
+ ```bash
41
+ centrs discover --timeout 5s --format json
42
+ ```
43
+
44
+ `meta.operation.timeoutMs` is `5000`; `meta.operation.packetsReceived`,
45
+ `packetsDecoded`, and `packetsRejected` summarize the scan. The JSON envelope
46
+ is the lossless representation the text format renders.
47
+
48
+ ### 3. Bind a non-default port
49
+
50
+ ```bash
51
+ centrs discover --port 5678
52
+ ```
53
+
54
+ Binding a port already held (e.g. by WinBox, without `SO_REUSEPORT`) errors
55
+ with `mndp/listen-failed` and an actionable remediation.
56
+
57
+ ## discover --save
58
+
59
+ ### 4. Save discovered neighbors into the CDB
60
+
61
+ ```bash
62
+ centrs discover --save --cdb-file $CDB
63
+ ```
64
+
65
+ New neighbors are added through the `devices` atomic write path with
66
+ `group=discovered`. Each saved comment contains the `source=mndp` kv token and
67
+ free-form provenance (`identity:`, `board:`, `version:`, `mac:`, …).
68
+ `meta.operation.saved` reports `{ group, added, skipped, records }`.
69
+
70
+ ### 5. Save with a custom group
71
+
72
+ ```bash
73
+ centrs discover --save --group lab --cdb-file $CDB
74
+ ```
75
+
76
+ New entries get `group=lab` instead of `discovered`.
77
+
78
+ ### 6. De-dupe against existing records
79
+
80
+ ```bash
81
+ centrs discover --save --cdb-file $CDB
82
+ ```
83
+
84
+ A neighbor whose target (IPv4, else MAC) already names a CDB entry is skipped,
85
+ never overwritten. The skip is reported in
86
+ `meta.operation.saved.records[].action = "skipped-existing"`; hand-curated
87
+ records win.
88
+
89
+ ### 7. Save against an encrypted CDB
90
+
91
+ ```bash
92
+ centrs discover --save --cdb-file $ENC_CDB --cdb-password centrs-test
93
+ ```
94
+
95
+ `ok: true`. The write layer decrypts the CDB under the supplied password,
96
+ appends new neighbors, and re-encrypts with a fresh salt before the atomic
97
+ rename. The `.bak.<timestamp>` beside the CDB is the verbatim prior
98
+ ciphertext.
@@ -0,0 +1,208 @@
1
+ # execute
2
+
3
+ Run a RouterOS CLI command and return its (semi-structured) output. `execute`
4
+ is the single read/write surface for RouterOS add/set/remove and other
5
+ CLI-shaped commands — there is no separate `update` command.
6
+
7
+ Status: `rest-api`, `native-api`, `mac-telnet`, and `ssh` are `CHR-passed` (see
8
+ `docs/MATRIX.md` and `commands/execute/examples.md`, examples 1–11 over REST,
9
+ 12–18 over the native API, 19–21 over mac-telnet, and S1–S4 over ssh, green via
10
+ `bun run test:integration`). `romon` and `winbox-terminal` remain
11
+ `not-started`. SNMP is retrieve-only and rejects `execute`.
12
+
13
+ ## Intent
14
+
15
+ - Mirror RouterOS `execute` semantics. Input is a CLI string; output is
16
+ console-shaped text (or structured records for path+verb writes) wrapped in
17
+ the standard envelope. This includes write-shaped add/set/remove.
18
+ - Validation is two-stage (see constitution: validation): `:put [:parse
19
+ "<cmd>"]` is a **syntax** gate only — it does not catch unknown attributes or
20
+ bad values. Semantic validation needs `/console/inspect` or the server's own
21
+ re-validation on the write round-trip. A clean parse is necessary, not
22
+ sufficient.
23
+ - centrs owns the **script-vs-structured gate** (`canonicalizeExecuteCommand` +
24
+ `isWriteShaped` in `src/execute.ts`): it decides which validation runs and
25
+ whether the write-confirm prompt fires. The shared `rosetta`/`lsp-routeros-ts`
26
+ canonicalizer is for canonicalization only — never the structured predicate
27
+ (widening `structured` is a product regression). Pinned by
28
+ `test/unit/execute-canonicalize-contract.test.ts`; the parser-vendoring
29
+ preconditions live in the `canonicalizeExecuteCommand` doc-comment.
30
+ - REST adapter prefers structured path-POST when the CLI canonicalizes to
31
+ path+verb+attrs (`POST /rest/<path>/<verb>` → `{"ret":"<.id>"}`, clean typed
32
+ errors); falls back to `POST /rest/execute {"script":"<cli>"}` for
33
+ non-path-shaped console commands.
34
+ - **RouterOS REST verb mapping — the `POST` ≠ `add` trap (surfaced explicitly).**
35
+ RouterOS REST maps `GET`→print, **`PUT`→add**, `PATCH`→set, `DELETE`→remove,
36
+ and `POST`→the *universal* "run any console command" method ([REST API → HTTP
37
+ Methods](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST%2BAPI#RESTAPI-HTTPMethods)).
38
+ So the naive REST assumption `POST`=create is **wrong** — `PUT` is RouterOS's
39
+ create. centrs sidesteps the trap with the explicit verb-in-path form
40
+ `POST /rest/<path>/<verb>` (e.g. `POST /rest/ip/route/add`): add/set/remove are
41
+ **named**, never inferred from the HTTP method. The mapping is documented here
42
+ so the trap is impossible to miss at centrs's `api` surface.
43
+ - native-api adapter issues the same write as a tagged `talk` sentence; its
44
+ `!trap` strings share one error table with REST `detail`. REST and native
45
+ classify the same RouterOS fault to the same `routeros/*` code: both feed the
46
+ router-side fault string through `mapRouterOsError`
47
+ (`src/core/routeros-errors.ts`). In `RestAdapter.mapHttpFailure`
48
+ (`src/protocols/adapter.ts`), transport-level HTTP statuses are classified
49
+ first — 401/403 → `transport/auth-failed`, 5xx → retryable
50
+ `transport/connection-closed` (kept distinct so the fanout retry allowlist
51
+ can retry drops without retrying real router rejections) — and everything
52
+ else passes its `detail`/body plus the HTTP status through
53
+ `mapRouterOsError({ transport: "rest-api", httpStatus })`. There is no longer
54
+ a hand-rolled `routeros/*` table on the REST side.
55
+ - Output is *string*-shaped for script-mode; richer parsing is a future
56
+ concern. The envelope must still distinguish RouterOS errors from successful
57
+ runs (a 200 with a RouterOS error string is still an error — see
58
+ constitution: error model).
59
+ - **Print width / column wrapping (JG-04).** RouterOS wraps `print` output to the
60
+ terminal width, so each transport handles width differently. **mac-telnet** is
61
+ the only one that negotiates a size: `src/protocols/mac-telnet-console.ts`
62
+ answers the device's terminal-size probe with a deliberately tall/wide screen,
63
+ so captured columns are neither wrapped nor paginated. **SSH execute** runs
64
+ *without* a pseudo-tty (`src/protocols/ssh.ts`), so RouterOS emits no
65
+ terminal-wrapped output to begin with — there is nothing to negotiate.
66
+ **REST/native-api** are not consoles, so terminal width never applies (they
67
+ return structured records, or a string `ret` for script-mode). There is
68
+ therefore no per-call `--width` knob today — an explicit width override is
69
+ deferred until a concrete need appears.
70
+ - Attribute values pass through **verbatim** — centrs does not guess types or
71
+ coerce them (RouterOS REST tolerates string values; the native API is
72
+ all-strings). Unlike `jo`-style KV builders, there is no per-value
73
+ type-forcing escape, because the device, not centrs, owns value typing.
74
+ - On an empty-CDB resolution failure, execute (like retrieve) emits the
75
+ `tip/no-devices` tip, steering toward `centrs devices discover` /
76
+ `centrs settings`.
77
+ - Protocol surfaces are native API, REST, mac-telnet, and SSH. RoMON and
78
+ WinBox Terminal are later. SNMP is retrieve-only and must reject execute.
79
+ - For a MAC target not resolved from CDB, auto-selection defaults to
80
+ mac-telnet. Callers that want IP-level execution may opt into ARP-based
81
+ MAC → IP resolution with `--resolve arp` (plus `--via native-api`/`rest-api`)
82
+ before protocol selection.
83
+ - When the resolved transport is mac-telnet and **no** username or password is
84
+ resolved from any source, execute emits a `tip/mac-telnet-no-credentials`
85
+ pointer: mac-telnet logs in with RouterOS credentials over **MTWEI** (MD5 is
86
+ refused by current RouterOS), so an empty login is rejected at connect time.
87
+ It is a tip, not an error — the call is still attempted.
88
+ - Default output is human-readable `text`; pass `--json`/`--format json`
89
+ (or set `CENTRS_FORMAT=json`) for the structured envelope. Errors always
90
+ render as `[code] summary` + `Fix:` lines in text mode.
91
+ - CDB comment-kv metadata may provide per-target `via`, `port`, and `ssh-key`
92
+ overrides; CLI/API arguments still win.
93
+ - RoMON and WinBox Terminal are lower priority than mac-telnet until their
94
+ validation, tooling, and CI story are grounded.
95
+
96
+ ## Flags
97
+
98
+ | Flag | Behavior |
99
+ | ------------------------------- | ------------------------------------------------------------------------------------- |
100
+ | `--via <protocol>` | Pin the transport (`native-api`, `rest-api`, `ssh`, or `mac-telnet`). No silent downgrade. A bare MAC target defaults to `mac-telnet`. |
101
+ | `--host <host\|url>` | Override the resolved host or base URL for the target. |
102
+ | `--port <n>` | Override the resolved management port. |
103
+ | `--username` (alias `--user`, `-u`) / `--password` | RouterOS credentials; fall back to `CENTRS_USERNAME` / `CENTRS_PASSWORD`. |
104
+ | `--ssh-key <path>` | `--via ssh`: explicit private-key path. Falls back to `CENTRS_SSH_KEY` / the ssh-agent. |
105
+ | `--insecure` | Accept a new SSH host key (`--via ssh`) or a self-signed `api-ssl` TLS cert. Default verifies. |
106
+ | `--cdb-file` / `--cdb-password` | Read target credentials from (and decrypt) a WinBox CDB file. |
107
+ | `--resolve <none\|arp>` | Resolve a MAC-address target to an IP via the host ARP cache. Default `none`. |
108
+ | `--timeout <duration>` | Per-request timeout. REST: ≤ 60s; other transports may allow longer. |
109
+ | `--validate[=false]` | Run transport-appropriate preflight validation before execution (default `true`): REST/native use `:parse` + `/console/inspect`; mac-telnet and ssh use a single console `:parse` (covers syntax + unknown-attribute). |
110
+ | `--strict` | Treat any warning on a successful run as an error (`ok: false`, nonzero exit), like `-Werror`. Default is lenient (`ok: true` + `warnings`). |
111
+ | `--yes` | Confirm write-shaped add/set/remove commands in non-interactive runs. |
112
+ | `--max-bytes <n>` | Byte budget for the rendered envelope; excess output is truncated with a warning + `meta.truncated` (not an error), matching `retrieve`. (Renamed from `--max-results`.) |
113
+ | `--format <text\|json\|yaml>` | Output format (alias `--json`). Defaults to `text`; `CENTRS_FORMAT` sets the default. |
114
+ | `--where <attr>=<value>` | Device-class selector — run across every CDB record whose stored fact/comment-kv matches (e.g. `--where board=RB5009 /system/package/check-for-updates`). Repeatable (AND). See constitution: target selection. |
115
+ | `--` | End-of-options marker. Every token after `--` is taken as the literal RouterOS command, even flag-shaped ones — e.g. `centrs execute $R -- /interface print where disabled=yes`. Use it when the command contains tokens that would otherwise be claimed as centrs flags; otherwise the command must be quoted as a single argument. |
116
+
117
+ ## SSH key selection
118
+
119
+ Signed off (settings names): SSH transports use one shared setting, `sshKey`,
120
+ exposed as `--ssh-key <path>`, `CENTRS_SSH_KEY`, and the CDB comment-kv token
121
+ `ssh-key=<path>`. Precedence follows the constitution: defaults → config → CDB
122
+ comment-kv → env → CLI/API. When unset, centrs honors the user's system SSH
123
+ configuration and agent exactly as `ssh` would; setting `--ssh-key` is an
124
+ explicit per-invocation override. The `ssh-key` setting (and `--insecure`) landed
125
+ with the first SSH consumer, `transfer`/sftp; `execute / ssh` (this command) and
126
+ `terminal / ssh` followed as separate transports over the same host-`ssh`
127
+ plumbing (`src/protocols/ssh.ts`) — all three SSH cells are `CHR-passed`.
128
+
129
+ Over SSH — like mac-telnet — `execute` is a **console transport**: it runs a
130
+ CLI line and returns text (`SshExecClient`, one `ssh user@host "<command>"` per
131
+ command; validation reuses the same `:put [:parse …]` gate), so structured
132
+ path-POST reads and `/console/inspect` are `rest-api`/`native-api`-only. Green via
133
+ `test/integration/execute-ssh.test.ts` (S1–S4); see `src/protocols/ssh.ts` for the
134
+ no-PTY/clean-output grounding.
135
+
136
+ `ssh-key` stores a private key **path**, never private key material. The resolved
137
+ path may appear in `meta.settings.sshKey` with its source so bug reports can
138
+ explain why a key was selected. Private key contents, passphrases, agent socket
139
+ contents, and any inline key material are always sensitive and must be listed in
140
+ `error.redactable_fields` if they appear in structured error data; key paths are
141
+ redacted only if the caller marks them sensitive or the error couples them with
142
+ key material.
143
+
144
+ ## mac-telnet L2 validation
145
+
146
+ Decided (2026-06-06): the real-L2 integration path is `@tikoci/quickchr`'s
147
+ host-side L2 capture. The host runs a TCP server and the CHR gets a
148
+ `socket-connect` NIC; QEMU streams every guest Ethernet frame to the host
149
+ length-prefixed (4-byte BE length + raw frame), and **a frame written back is
150
+ injected into the guest — exactly the MAC-Telnet primitive**. Loopback-only,
151
+ cross-platform, no root and no native raw-L2 helper; REST/native-API keep a
152
+ separate user-mode NIC with hostfwd. Prefer `socket-connect` over `socket-mcast`
153
+ (the multicast netdev is broken on macOS — QEMU sets only `SO_REUSEADDR` where
154
+ macOS needs `SO_REUSEPORT`; mcast works on Linux/CI). Grounding: quickchr
155
+ `docs/mndp.md`, `examples/mndp/`, `test/lab/mndp/REPORT.md`.
156
+
157
+ `execute / mac-telnet` is now **`CHR-passed`** end to end over real L2 against
158
+ stock CHR 7.23.1 (`test/integration/mac-telnet-console.test.ts` +
159
+ `mactelnet-l2-bridge.ts`, examples 19–21): `executeEnvelope` resolves a MAC
160
+ target, the `MacTelnetAdapter` opens a `MacTelnetConsole` over a UDP datagram
161
+ transport, and reads, writes (REST-verified), and validation-rejects all work.
162
+
163
+ How it is wired:
164
+
165
+ - **Console transport, not request/response.** mac-telnet drives the RouterOS
166
+ interactive console (`src/protocols/mac-telnet-console.ts`): it answers the
167
+ login terminal-size probe, auto-clears the first-login license, syncs on the
168
+ `[user@identity] >` prompt, emulates the CR/LF screen, and strips the echoed
169
+ command + trailing prompt to return clean per-command output. The orchestrator
170
+ runs the **raw CLI line** over the console (no structured path-POST / tagged
171
+ talk); the `MacTelnetAdapter` reports `retrieve`/`inspect` as
172
+ `transport/capability-unsupported` (mac-telnet is execute/terminal only).
173
+ - **Validation is one console `:parse`.** Over the console, `:put [:parse "<cmd>"]`
174
+ prints both `syntax error` and `bad parameter <name>`, so a single gate covers
175
+ syntax **and** the unknown-attribute (semantic) check — no `/console/inspect`
176
+ table parsing. Maps to `validation/syntax` / `validation/unknown-attribute`,
177
+ same codes as REST/native (see `docs/CONSTITUTION.md`, validation).
178
+ - **Auth (grounded):** MTWEI is required; a stock device offers a 16-byte MD5 salt
179
+ but rejects the MD5 proof, so centrs advertises MTWEI (`src/protocols/mtwei.ts`)
180
+ by default. `END_AUTH` ≠ success (a failed login emits `END_AUTH` then "Login
181
+ failed" + `END`) → `transport/auth-failed`.
182
+ - **Console quirks to know:** a successful write **prints no output** (no `.id`,
183
+ unlike REST/native — `data.ret` is empty on a successful add); and there is a
184
+ ~10s terminal-negotiation stall on every login (a latency cost, not a
185
+ correctness issue — see `commands/terminal/README.md` for the optional fix).
186
+ - **Delivery:** the UDP transport sends to `--host`/`--port` (default L2 broadcast
187
+ `255.255.255.255:20561`); the device replies to our source IP:port, and the
188
+ in-packet MACs do the addressing. The CHR test points `--host`/`--port` at the
189
+ loopback L2 bridge.
190
+
191
+ Protocol selection: an unresolved MAC target auto-selects mac-telnet (covered by
192
+ `test/unit/execute.test.ts`); callers wanting IP-level execution opt into ARP
193
+ (`--resolve arp` + `--via native-api`/`rest-api`).
194
+
195
+ ## Open shape questions
196
+
197
+ - ~~How to expose multi-line / async / progress output~~ — **resolved:**
198
+ open-ended follow/streaming reads are the separate read-only `stream` verb
199
+ (NDJSON stream of envelopes; see `commands/stream/README.md`). `execute` stays
200
+ single-shot read/write; it does not grow a follow mode.
201
+ - ~~Whether `--strict` should reject "succeeded with stderr-like content."~~ —
202
+ **decided:** `--strict` promotes any warning on an otherwise-successful run to
203
+ `ok: false` (nonzero exit), like `-Werror`; the default stays lenient
204
+ (`ok: true` with the warning in the `warnings` channel). Tracked code
205
+ follow-up; the flag is in the table above.
206
+
207
+ These are deferred refinements to the already-grounded single-shot envelope,
208
+ not blockers for the `rest-api`/`native-api` cells.
@@ -0,0 +1,353 @@
1
+ # execute — examples
2
+
3
+ Each numbered example is an executable spec, run against a CHR 7.23 router booted
4
+ by `@tikoci/quickchr`: `test/integration/execute.test.ts` covers examples 1–11
5
+ over REST and 12–18 over the native API; `test/integration/mac-telnet-console.test.ts`
6
+ covers examples 19–21 over mac-telnet (real L2 via the `socket-connect` bridge).
7
+ If a line here is not exercised by a test, the test file is wrong; if a line
8
+ passes only with `validate=false`, the **implementation** is wrong (see
9
+ `docs/CONSTITUTION.md`).
10
+
11
+ `$R` is `<host>:<rest-port>` resolved by quickchr. `$A` is `<host>` and
12
+ `$API_PORT` is the native API port (`chr.ports.api`). `$MAC` is the device MAC and
13
+ `$MT_HOST`/`$MT_PORT` are the mac-telnet UDP delivery endpoint (the loopback L2
14
+ bridge in CI). `$U` / `$P` are CHR credentials provided by the test harness.
15
+ `$ID` is the `.id` returned by the preceding add example in the same transport
16
+ section.
17
+
18
+ All write-shaped examples use `--yes` as the explicit non-interactive
19
+ confirmation flag. Without `--yes`, non-TTY writes fail before validation or any
20
+ RouterOS mutation; with a TTY, the CLI prompts before running the write.
21
+
22
+ ## rest-api (`--via rest-api`)
23
+
24
+ ### 1. Add an address with structured path-POST
25
+
26
+ Must succeed with `validate=true` (default). The CLI canonicalizes the command
27
+ as path `/ip/address`, verb `add`, and attributes `address`, `interface`, and
28
+ `comment`; the REST adapter uses the structured path-command endpoint rather
29
+ than script mode.
30
+
31
+ ```bash
32
+ centrs execute $R '/ip/address/add address=198.51.100.10/32 interface=ether1 comment="centrs-execute-rest"' --via rest-api --username $U --password $P --yes
33
+ ```
34
+
35
+ Envelope: `ok: true`, `data.ret` is a RouterOS internal id matching `/^\*[0-9A-F]+$/`,
36
+ `data[".id"]` carries the same value for downstream set/remove examples,
37
+ `meta.via=rest-api`, and `meta.validation.source` includes both
38
+ `:put [:parse ...]` and `/console/inspect`.
39
+
40
+ ### 2. Set the address by `.id`
41
+
42
+ ```bash
43
+ centrs execute $R "/ip/address/set numbers=$ID comment=centrs-execute-rest-set" --via rest-api --username $U --password $P --yes
44
+ ```
45
+
46
+ Envelope: `ok: true`, `data` is empty or contains the RouterOS success record,
47
+ `meta.via=rest-api`, and re-reading `/ip/address/$ID` shows
48
+ `comment="centrs-execute-rest-set"`.
49
+
50
+ ### 3. Remove the address by `.id`
51
+
52
+ ```bash
53
+ centrs execute $R "/ip/address/remove numbers=$ID" --via rest-api --username $U --password $P --yes
54
+ ```
55
+
56
+ Envelope: `ok: true`, `meta.via=rest-api`. A subsequent retrieve of
57
+ `/ip/address` does not contain `$ID`.
58
+
59
+ ### 4. Run a non-path-shaped console command through `/rest/execute`
60
+
61
+ The command is a script-shaped console expression, not a slash path plus verb,
62
+ so REST must use the `/rest/execute` fallback.
63
+
64
+ ```bash
65
+ centrs execute $R ':put [/system/identity/get name]' --via rest-api --username $U --password $P
66
+ ```
67
+
68
+ Envelope: `ok: true`, `data` is string-shaped (`data.output` or `data.ret`) and
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.
73
+
74
+ ### 5. Syntax reject from `:put [:parse ...]`
75
+
76
+ Malformed CLI is rejected by the syntax gate before semantic validation or the
77
+ write round-trip.
78
+
79
+ ```bash
80
+ centrs execute $R '/ip/address/add address="unterminated interface=ether1' --via rest-api --username $U --password $P --yes
81
+ ```
82
+
83
+ Envelope: `ok: false`, `error.code=validation/syntax`, `error.cause` preserves
84
+ the RouterOS parse error string from `:put [:parse ...]`, `meta.via=rest-api`,
85
+ and no address is added.
86
+
87
+ ### 6. Semantic reject that `:parse` accepts
88
+
89
+ Grounded on CHR 7.23: `:put [:parse "/ip/address/add no-such-arg=x"]` accepts
90
+ unknown attributes, so centrs must not treat a clean parse as semantic success.
91
+ `/console/inspect` rejects the attribute before the write.
92
+
93
+ ```bash
94
+ centrs execute $R '/ip/address/add address=198.51.100.11/32 interface=ether1 no-such-arg=x' --via rest-api --username $U --password $P --yes
95
+ ```
96
+
97
+ Envelope: `ok: false`, `error.code=validation/unknown-attribute`,
98
+ `error.cause.attribute="no-such-arg"` (or equivalent structured field),
99
+ `meta.validation.syntax=true`, `meta.validation.source=/console/inspect`, and
100
+ no address is added. This example is the product claim: syntax validation and
101
+ semantic validation are separate gates.
102
+
103
+ ### 7. RouterOS error on `/rest/execute` HTTP 200
104
+
105
+ Assumption to verify on CHR 7.23: a script-mode RouterOS `:error` returns HTTP
106
+ 200 from `/rest/execute` with a RouterOS failure string in the body. That body is
107
+ still a failed RouterOS result, not successful command output.
108
+
109
+ ```bash
110
+ centrs execute $R ':error "centrs execute fixture trap"' --via rest-api --username $U --password $P
111
+ ```
112
+
113
+ Envelope: `ok: false`, `error.code=routeros/command-failed` (or the more
114
+ specific grounded `routeros/*` mapping for the returned string),
115
+ `error.cause` preserves the original RouterOS string, and `meta.via=rest-api`.
116
+ This uses the same mapping table as native API `!trap` errors.
117
+
118
+ ### 8. Pinned REST has no silent downgrade
119
+
120
+ `/rest/execute` is required for arbitrary script-shaped commands over REST. If
121
+ that capability is unavailable or disabled, `--via rest-api` must fail instead
122
+ of silently trying native API, SSH, or mac-telnet.
123
+
124
+ ```bash
125
+ centrs execute $R ':put "requires rest execute"' --via rest-api --username $U --password $P
126
+ ```
127
+
128
+ Envelope when REST cannot perform the operation: `ok: false`,
129
+ `error.code` starts with `transport/`, `meta.via=rest-api`, and
130
+ `meta.warnings` does not contain a protocol-downgrade warning. When REST can
131
+ perform it, the command succeeds over REST and still does not downgrade.
132
+
133
+ ### 9. Non-TTY write without `--yes` is refused
134
+
135
+ ```bash
136
+ centrs execute $R '/ip/address/add address=198.51.100.12/32 interface=ether1 comment="missing-confirm"' --via rest-api --username $U --password $P </dev/null
137
+ ```
138
+
139
+ Envelope: `ok: false`, `error.code=usage/confirmation-required`,
140
+ `meta.via=rest-api`, no validation transport call is made, and no address is
141
+ added.
142
+
143
+ ### 10. TTY write prompts before mutation
144
+
145
+ ```bash
146
+ centrs execute $R '/ip/address/add address=198.51.100.13/32 interface=ether1 comment="tty-confirm"' --via rest-api --username $U --password $P
147
+ ```
148
+
149
+ When stdin is a TTY, the CLI prompts for write confirmation. Answering `no` (or
150
+ EOF) returns `ok: false` with `usage/confirmation-required` and no mutation;
151
+ answering `yes` continues to validation and the write, returning the same
152
+ success envelope shape as example 1.
153
+
154
+ ### 11. `--validate=false` probes without preflight gates
155
+
156
+ This escape hatch skips centrs syntax and semantic preflight validation but does
157
+ not suppress RouterOS errors from the write round-trip.
158
+
159
+ ```bash
160
+ centrs execute $R '/ip/address/add address=198.51.100.14/32 interface=ether1 no-such-arg=x' --via rest-api --username $U --password $P --yes --validate=false
161
+ ```
162
+
163
+ Envelope: `ok: false`, `error.code=routeros/unknown-attribute` (or the grounded
164
+ RouterOS `routeros/*` code for `unknown parameter no-such-arg`),
165
+ `error.cause` preserves the original RouterOS string, `meta.via=rest-api`, and
166
+ `meta.validation.enabled=false`.
167
+
168
+ ## native-api (`--via native-api`)
169
+
170
+ The same execute contract over the RouterOS binary API. Validation still runs
171
+ through `:put [:parse ...]` and `/console/inspect`, issued as native API
172
+ commands. Attribute values are strings because the binary API does not carry
173
+ JSON scalar types.
174
+
175
+ ### 12. Add an address with native `talk`
176
+
177
+ ```bash
178
+ centrs execute $A '/ip/address/add address=198.51.100.20/32 interface=ether1 comment="centrs-execute-api"' --via native-api --port $API_PORT --username $U --password $P --yes
179
+ ```
180
+
181
+ Envelope: `ok: true`, `data[".id"]` (or `data.ret`) is the new RouterOS
182
+ internal id, `meta.via=native-api`, and `meta.validation.source` includes both
183
+ `:put [:parse ...]` and `/console/inspect`.
184
+
185
+ ### 13. Set the native-created address by `.id`
186
+
187
+ ```bash
188
+ centrs execute $A "/ip/address/set numbers=$ID comment=centrs-execute-api-set" --via native-api --port $API_PORT --username $U --password $P --yes
189
+ ```
190
+
191
+ Envelope: `ok: true`, `meta.via=native-api`, and re-reading `/ip/address/$ID`
192
+ shows `comment="centrs-execute-api-set"`.
193
+
194
+ ### 14. Remove the native-created address by `.id`
195
+
196
+ ```bash
197
+ centrs execute $A "/ip/address/remove numbers=$ID" --via native-api --port $API_PORT --username $U --password $P --yes
198
+ ```
199
+
200
+ Envelope: `ok: true`, `meta.via=native-api`. A subsequent retrieve of
201
+ `/ip/address` does not contain `$ID`.
202
+
203
+ ### 15. Native syntax reject from `:parse`
204
+
205
+ ```bash
206
+ centrs execute $A '/ip/address/add address="unterminated interface=ether1' --via native-api --port $API_PORT --username $U --password $P --yes
207
+ ```
208
+
209
+ Envelope: `ok: false`, `error.code=validation/syntax`, `error.cause` preserves
210
+ the RouterOS parse error string, `meta.via=native-api`, and no address is added.
211
+
212
+ ### 16. Native semantic reject after clean parse
213
+
214
+ ```bash
215
+ centrs execute $A '/ip/address/add address=198.51.100.21/32 interface=ether1 no-such-arg=x' --via native-api --port $API_PORT --username $U --password $P --yes
216
+ ```
217
+
218
+ Envelope: `ok: false`, `error.code=validation/unknown-attribute`,
219
+ `meta.validation.syntax=true`, `meta.validation.source=/console/inspect`, and
220
+ no address is added. The failure must be classified as validation, not as a
221
+ native API `!trap`, because the semantic gate catches it before the write.
222
+
223
+ ### 17. Native `!trap` uses the shared RouterOS error table
224
+
225
+ With validation disabled, the unknown attribute reaches RouterOS and native API
226
+ returns a `!trap` message.
227
+
228
+ ```bash
229
+ centrs execute $A '/ip/address/add address=198.51.100.22/32 interface=ether1 no-such-arg=x' --via native-api --port $API_PORT --username $U --password $P --yes --validate=false
230
+ ```
231
+
232
+ Envelope: `ok: false`, `error.code=routeros/unknown-attribute` (the same code as
233
+ REST for the same RouterOS string), `error.cause` preserves the original
234
+ `!trap` message, `meta.via=native-api`, and `meta.validation.enabled=false`.
235
+
236
+ ### 18. Native non-TTY write without `--yes` is refused
237
+
238
+ ```bash
239
+ centrs execute $A '/ip/address/add address=198.51.100.23/32 interface=ether1 comment="api-missing-confirm"' --via native-api --port $API_PORT --username $U --password $P </dev/null
240
+ ```
241
+
242
+ Envelope: `ok: false`, `error.code=usage/confirmation-required`,
243
+ `meta.via=native-api`, no validation command is sent, and no address is added.
244
+
245
+ ## mac-telnet (`--via mac-telnet`)
246
+
247
+ Layer-2 execute over the RouterOS interactive console (UDP/20561). `$MAC` is the
248
+ device MAC; `$MT_HOST`/`$MT_PORT` are the UDP delivery endpoint (L2 broadcast in
249
+ the field, the `quickchr` loopback L2 bridge in CI). Validation over mac-telnet
250
+ is a **single console `:put [:parse …]`** — it reports both `syntax error` and
251
+ `bad parameter <name>`, so it covers the syntax *and* the unknown-attribute gate
252
+ at once (no `/console/inspect` table parsing). The console opens through a
253
+ first-login license screen (auto-answered) and a ~10s terminal-negotiation stall;
254
+ **a successful write prints nothing** over the console (no `.id`/`ret`, unlike
255
+ REST/native). Examples 19–21 are green via `bun run test:integration`
256
+ (`test/integration/mac-telnet-console.test.ts`, CHR 7.23.1) over the real L2
257
+ bridge; the console reader is also exercised directly in the same test.
258
+
259
+ ### 19. Read a command over mac-telnet
260
+
261
+ ```bash
262
+ centrs execute $MAC '/system/identity/print' --via mac-telnet --host $MT_HOST --port $MT_PORT --username $U --password $P
263
+ ```
264
+
265
+ Envelope: `ok: true`, `meta.via=mac-telnet`, `data.ret` is the console output and
266
+ contains the device identity (cross-checked against REST). `meta.validation.source`
267
+ is `:put [:parse ...] over mac-telnet`.
268
+
269
+ ### 20. Write (add) over mac-telnet
270
+
271
+ ```bash
272
+ centrs execute $MAC '/ip/address/add address=198.51.100.40/32 interface=ether1' --via mac-telnet --host $MT_HOST --port $MT_PORT --username $U --password $P --yes
273
+ ```
274
+
275
+ Envelope: `ok: true`, `meta.via=mac-telnet`, `data.ret` is empty (a successful
276
+ console write prints nothing — there is no `.id` to return). A subsequent REST
277
+ read of `/ip/address` shows `198.51.100.40`.
278
+
279
+ ### 21. Validation rejects an unknown attribute over mac-telnet
280
+
281
+ ```bash
282
+ centrs execute $MAC '/ip/address/add address=198.51.100.41/32 interface=ether1 no-such-arg=x' --via mac-telnet --host $MT_HOST --port $MT_PORT --username $U --password $P --yes
283
+ ```
284
+
285
+ Envelope: `ok: false`, `error.code=validation/unknown-attribute`,
286
+ `meta.via=mac-telnet`, and no address is added — the console `:parse` gate
287
+ catches `no-such-arg` before any mutation.
288
+
289
+ ## ssh (`--via ssh`)
290
+
291
+ Execute over the RouterOS SSH server using the **host `ssh`** client. RouterOS
292
+ grants no pseudo-tty, but `ssh user@host "<command>"` runs one single-line console
293
+ command and returns **clean** output (no prompt, no ANSI, no echo) — so each
294
+ execute is one `ssh` invocation (a fresh login, like the SFTP batch client), and
295
+ the only post-processing is trimming the console's column padding. Validation is
296
+ the **same single console `:put [:parse …]`** as mac-telnet (over SSH it returns
297
+ the identical `(evl …)` / `bad parameter <name>` strings), so syntax and the
298
+ unknown-attribute gate are covered at once. Key auth is the path (`--ssh-key` /
299
+ agent); `--insecure` accepts the device's host key. `$SSH_PORT` is the SSH port,
300
+ `$KEY` the private key. Examples S1–S4 are green via `bun run test:integration`
301
+ (`test/integration/execute-ssh.test.ts`, CHR 7.23.1).
302
+
303
+ ### S1. Read a command over ssh
304
+
305
+ ```bash
306
+ centrs execute 127.0.0.1 '/system/identity/print' --via ssh --port $SSH_PORT --username $U --ssh-key $KEY --insecure
307
+ ```
308
+
309
+ Envelope: `ok: true`, `meta.via=ssh`, `data.ret` is the cleaned console output and
310
+ contains the device identity (cross-checked against REST). `meta.validation.source`
311
+ is `:put [:parse ...] over ssh`.
312
+
313
+ ### S2. Multi-line read returns cleaned, column-aligned output
314
+
315
+ ```bash
316
+ centrs execute 127.0.0.1 '/system/resource/print' --via ssh --port $SSH_PORT --username $U --ssh-key $KEY --insecure
317
+ ```
318
+
319
+ Envelope: `ok: true`; `data.ret` contains the running RouterOS version (each line
320
+ trimmed of trailing padding, CRLF normalized).
321
+
322
+ ### S3. Write (add) over ssh
323
+
324
+ ```bash
325
+ centrs execute 127.0.0.1 '/ip/address/add address=198.51.100.50/32 interface=ether1' --via ssh --port $SSH_PORT --username $U --ssh-key $KEY --insecure --yes
326
+ ```
327
+
328
+ Envelope: `ok: true`, `meta.via=ssh`, `data.ret` empty (a successful console write
329
+ prints nothing). A subsequent REST read of `/ip/address` shows `198.51.100.50`.
330
+
331
+ ### S4. Validation rejects an unknown attribute over ssh
332
+
333
+ ```bash
334
+ centrs execute 127.0.0.1 '/ip/address/add address=198.51.100.51/32 interface=ether1 bogus=x' --via ssh --port $SSH_PORT --username $U --ssh-key $KEY --insecure --yes
335
+ ```
336
+
337
+ Envelope: `ok: false`, `error.code=validation/unknown-attribute`, `meta.via=ssh`,
338
+ and no address is added — the console `:parse` gate catches `bogus` pre-mutation.
339
+
340
+ ## Protocol selection notes
341
+
342
+ For an unresolved MAC target, execute auto-selection defaults to mac-telnet
343
+ rather than ARP-based IP resolution:
344
+
345
+ ```bash
346
+ centrs execute $MAC '/system/identity/set name=chr-via-mac' --username $U --password $P --yes
347
+ ```
348
+
349
+ Envelope: `meta.via=mac-telnet` when `$MAC` is not resolved from CDB and no
350
+ `--via` is pinned (delivery defaults to L2 broadcast; override with
351
+ `--host`/`--port`). If the caller wants IP-level REST or native API execution,
352
+ they must opt into MAC-to-IP resolution (`--resolve arp` + `--via …`) before
353
+ protocol selection.