@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.
- package/CHANGELOG.md +207 -0
- package/GLOSSARY.txt +420 -0
- package/README.md +196 -0
- package/SECURITY.md +30 -0
- package/commands/AGENTS.md +50 -0
- package/commands/btest/README.md +257 -0
- package/commands/btest/examples.md +190 -0
- package/commands/check/README.md +59 -0
- package/commands/devices/README.md +405 -0
- package/commands/devices/examples.md +488 -0
- package/commands/discover/README.md +170 -0
- package/commands/discover/examples.md +98 -0
- package/commands/execute/README.md +208 -0
- package/commands/execute/examples.md +353 -0
- package/commands/mcp/README.md +260 -0
- package/commands/mcp/examples.md +158 -0
- package/commands/retrieve/README.md +209 -0
- package/commands/retrieve/examples.md +306 -0
- package/commands/settings/README.md +69 -0
- package/commands/stream/README.md +70 -0
- package/commands/terminal/README.md +177 -0
- package/commands/terminal/examples.md +144 -0
- package/commands/transfer/README.md +315 -0
- package/commands/transfer/examples.md +368 -0
- package/docs/CONSTITUTION.md +417 -0
- package/docs/MATRIX.md +224 -0
- package/docs/errors/README.md +74 -0
- package/docs/errors/auth/failed.md +9 -0
- package/docs/errors/cdb/already-exists.md +9 -0
- package/docs/errors/cdb/created.md +9 -0
- package/docs/errors/cdb/decrypt-failed.md +9 -0
- package/docs/errors/cdb/empty-group.md +9 -0
- package/docs/errors/cdb/invalid-option.md +9 -0
- package/docs/errors/cdb/invalid-record.md +9 -0
- package/docs/errors/cdb/not-found-target.md +9 -0
- package/docs/errors/cdb/not-found.md +9 -0
- package/docs/errors/cdb/override-applied.md +9 -0
- package/docs/errors/cdb/parse-failed.md +9 -0
- package/docs/errors/cdb/password-not-needed.md +9 -0
- package/docs/errors/cdb/password-required.md +9 -0
- package/docs/errors/cdb/reserved-key.md +9 -0
- package/docs/errors/cdb/reserved-option.md +9 -0
- package/docs/errors/cdb/target-not-registered.md +9 -0
- package/docs/errors/cdb/unknown-field.md +9 -0
- package/docs/errors/cdb/unknown-option.md +9 -0
- package/docs/errors/cdb/write-not-permitted.md +9 -0
- package/docs/errors/discover/broadcast-unavailable.md +9 -0
- package/docs/errors/discover/failed.md +9 -0
- package/docs/errors/identity/ambiguous.md +9 -0
- package/docs/errors/identity/no-match.md +9 -0
- package/docs/errors/input/invalid-command.md +9 -0
- package/docs/errors/input/invalid-match.md +9 -0
- package/docs/errors/input/invalid-path.md +9 -0
- package/docs/errors/input/invalid-routeros-path.md +9 -0
- package/docs/errors/input/invalid-target.md +9 -0
- package/docs/errors/input/local-file-not-found.md +9 -0
- package/docs/errors/input/mac-address.md +9 -0
- package/docs/errors/input/max-results-exceeded.md +9 -0
- package/docs/errors/internal/devices-failed.md +9 -0
- package/docs/errors/internal/mcp-start.md +9 -0
- package/docs/errors/internal/mcp-tool.md +9 -0
- package/docs/errors/internal/native-api-length.md +9 -0
- package/docs/errors/internal/unhandled.md +9 -0
- package/docs/errors/internal/unreachable.md +9 -0
- package/docs/errors/mndp/encode-failed.md +9 -0
- package/docs/errors/mndp/listen-failed.md +9 -0
- package/docs/errors/mndp/malformed.md +9 -0
- package/docs/errors/routeros/api-fatal.md +9 -0
- package/docs/errors/routeros/api-protocol.md +9 -0
- package/docs/errors/routeros/api-trap.md +9 -0
- package/docs/errors/routeros/btest-protocol.md +12 -0
- package/docs/errors/routeros/btest-too-many-sessions.md +9 -0
- package/docs/errors/routeros/command-failed.md +14 -0
- package/docs/errors/routeros/ec-srp5-protocol.md +11 -0
- package/docs/errors/routeros/error.md +9 -0
- package/docs/errors/routeros/invalid-value.md +14 -0
- package/docs/errors/routeros/mac-telnet-error.md +9 -0
- package/docs/errors/routeros/mac-telnet-not-ready.md +9 -0
- package/docs/errors/routeros/mac-telnet-protocol.md +9 -0
- package/docs/errors/routeros/mac-telnet-unsupported-auth.md +9 -0
- package/docs/errors/routeros/protocol-not-implemented.md +9 -0
- package/docs/errors/routeros/request-failed.md +9 -0
- package/docs/errors/routeros/session-closed.md +13 -0
- package/docs/errors/routeros/unknown-attribute.md +13 -0
- package/docs/errors/routeros/unknown-path.md +15 -0
- package/docs/errors/routeros/unsupported-capability.md +9 -0
- package/docs/errors/settings/invalid-boolean.md +9 -0
- package/docs/errors/settings/invalid-format.md +9 -0
- package/docs/errors/settings/invalid-integer.md +9 -0
- package/docs/errors/settings/invalid-timeout.md +9 -0
- package/docs/errors/settings/invalid-via.md +9 -0
- package/docs/errors/settings/unsafe-protocol-blocked.md +9 -0
- package/docs/errors/target/mac-not-in-arp.md +14 -0
- package/docs/errors/target/mac-required.md +23 -0
- package/docs/errors/target/mac-unresolved.md +17 -0
- package/docs/errors/target/unresolved.md +9 -0
- package/docs/errors/transport/auth-failed.md +9 -0
- package/docs/errors/transport/auto-method.md +9 -0
- package/docs/errors/transport/capability-unsupported.md +22 -0
- package/docs/errors/transport/checksum-unavailable.md +9 -0
- package/docs/errors/transport/connection-closed.md +9 -0
- package/docs/errors/transport/connection-refused.md +9 -0
- package/docs/errors/transport/dns.md +9 -0
- package/docs/errors/transport/host-key-mismatch.md +9 -0
- package/docs/errors/transport/incomplete-transfer.md +9 -0
- package/docs/errors/transport/insecure-trust.md +9 -0
- package/docs/errors/transport/local-tool-missing.md +11 -0
- package/docs/errors/transport/network.md +9 -0
- package/docs/errors/transport/timeout.md +9 -0
- package/docs/errors/transport/tls-certificate.md +9 -0
- package/docs/errors/transport/unreachable.md +9 -0
- package/docs/errors/transport/unsupported-operation.md +9 -0
- package/docs/errors/usage/confirmation-required.md +9 -0
- package/docs/errors/usage/conflicting-flags.md +9 -0
- package/docs/errors/usage/invalid-concurrency.md +9 -0
- package/docs/errors/usage/missing-group.md +9 -0
- package/docs/errors/usage/not-implemented.md +9 -0
- package/docs/errors/usage/target-exists.md +9 -0
- package/docs/errors/usage/timeout-out-of-range.md +9 -0
- package/docs/errors/validation/not-implemented.md +9 -0
- package/docs/errors/validation/option.md +9 -0
- package/docs/errors/validation/syntax.md +9 -0
- package/docs/errors/validation/unknown-attribute.md +9 -0
- package/docs/errors/validation/unknown-path.md +9 -0
- package/package.json +96 -0
- package/src/AGENTS.md +7 -0
- package/src/btest.ts +906 -0
- package/src/cli/btest.ts +455 -0
- package/src/cli/common.ts +53 -0
- package/src/cli/devices.ts +527 -0
- package/src/cli/discover.ts +186 -0
- package/src/cli/execute.ts +333 -0
- package/src/cli/mcp.ts +109 -0
- package/src/cli/retrieve.ts +383 -0
- package/src/cli/terminal.ts +303 -0
- package/src/cli/transfer.ts +471 -0
- package/src/cli.ts +115 -0
- package/src/core/envelope.ts +221 -0
- package/src/core/error-catalog.ts +469 -0
- package/src/core/routeros-errors.ts +268 -0
- package/src/data/AGENTS.md +11 -0
- package/src/data/dude-db.ts +0 -0
- package/src/data/mndp-cache.ts +132 -0
- package/src/data/mndp.ts +405 -0
- package/src/data/winbox-cdb-write.ts +200 -0
- package/src/data/winbox-cdb.ts +913 -0
- package/src/devices-schema.ts +107 -0
- package/src/devices.ts +1717 -0
- package/src/discover.ts +802 -0
- package/src/errors.ts +189 -0
- package/src/execute.ts +1350 -0
- package/src/index.ts +353 -0
- package/src/mcp/config.ts +66 -0
- package/src/mcp/resources.ts +98 -0
- package/src/mcp/safety.ts +119 -0
- package/src/mcp/server.ts +225 -0
- package/src/mcp/tools.ts +657 -0
- package/src/mcp.ts +33 -0
- package/src/protocols/AGENTS.md +14 -0
- package/src/protocols/adapter.ts +866 -0
- package/src/protocols/btest-session.ts +1493 -0
- package/src/protocols/btest.ts +447 -0
- package/src/protocols/ec-srp5.ts +466 -0
- package/src/protocols/index.ts +118 -0
- package/src/protocols/mac-telnet-console.ts +655 -0
- package/src/protocols/mac-telnet.ts +1299 -0
- package/src/protocols/mtwei.ts +47 -0
- package/src/protocols/native-api.ts +796 -0
- package/src/protocols/sftp.ts +427 -0
- package/src/protocols/ssh.ts +297 -0
- package/src/resolver/cdb.ts +580 -0
- package/src/resolver/comment-kv.ts +360 -0
- package/src/resolver/index.ts +74 -0
- package/src/resolver/mac.ts +227 -0
- package/src/resolver/settings.ts +228 -0
- package/src/resolver/target.ts +442 -0
- package/src/retrieve-fanout.ts +533 -0
- package/src/retrieve.ts +1215 -0
- package/src/terminal.ts +444 -0
- package/src/transfer.ts +1554 -0
- package/src/tui.ts +22 -0
- 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.
|