@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,190 @@
|
|
|
1
|
+
# btest — examples
|
|
2
|
+
|
|
3
|
+
Each numbered example is an executable spec. btest is a peer protocol (no router
|
|
4
|
+
*command* is run), so the examples are validated by two peers — matching the
|
|
5
|
+
matrix's honest grounding split (`docs/MATRIX.md`, "Peer measurement (`btest`)"):
|
|
6
|
+
|
|
7
|
+
- **Gated `CHR-passed` (server cell): CHR client → centrs server** — examples
|
|
8
|
+
**1–3** are the asserted set. `@tikoci/quickchr` boots a CHR; its
|
|
9
|
+
`/tool/bandwidth-test` dials the host at the SLIRP gateway `$GW` (`10.0.2.2`)
|
|
10
|
+
where the centrs server listens on `--bind 0.0.0.0`. No hostfwd (guest→host).
|
|
11
|
+
Gated UDP coverage is **transmit** (guest-originated, so SLIRP NATs it cleanly);
|
|
12
|
+
UDP **receive/both** through SLIRP is a non-asserting smoke cycle (README, Open
|
|
13
|
+
questions). `test/integration/btest.test.ts` drives the CHR side with
|
|
14
|
+
centrs's already-`CHR-passed` `execute`/REST path and cross-checks throughput /
|
|
15
|
+
loss / direction against the CHR's own `/tool/bandwidth-test` status output
|
|
16
|
+
(CHR 7.23.1, `bun run test:integration`). Examples **4** (wrong-credentials) and
|
|
17
|
+
**5** (multi-connection TCP) are **designed but not yet gated** — the auth-reject
|
|
18
|
+
path is covered by the loopback unit test, and the multi-stream fan-out is a
|
|
19
|
+
documented follow-up — so they are not part of the asserted run.
|
|
20
|
+
- **Gated `CHR-passed` (client cell): centrs client → CHR bandwidth-server** —
|
|
21
|
+
examples **6** (TCP receive) and **8** (EC-SRP5 client role + wrong-password
|
|
22
|
+
reject) are *also* asserted directly against a real RouterOS
|
|
23
|
+
`/tool/bandwidth-server`, over a host→guest `tcp:2000` forward
|
|
24
|
+
(`test/integration/btest-client.test.ts`, CHR 7.23.1). This is what makes the
|
|
25
|
+
**client** cell `CHR-passed`: centrs's EC-SRP5 **client proof** is verified by
|
|
26
|
+
RouterOS's own server verifier. TCP only — the EC-SRP5 example is run over TCP
|
|
27
|
+
receive here (the loopback variant below uses UDP); UDP client→server stays
|
|
28
|
+
loopback (README, Open questions).
|
|
29
|
+
- **Unit / loopback (client cell + codec)** — examples 6–10. centrs client ↔
|
|
30
|
+
centrs server on `127.0.0.1` with injected sockets; the EC-SRP5 both-roles math
|
|
31
|
+
and the pre-socket option-grammar rejects (`test/unit/btest.test.ts`). These are
|
|
32
|
+
deterministic and need no router, and remain the fast backstop under the gated
|
|
33
|
+
client-cell coverage above.
|
|
34
|
+
|
|
35
|
+
`btest.exe` (via wine) is **not** in this example set: it cannot run in CI, so it
|
|
36
|
+
is a **coding-time grounding aid** only — run by hand against the centrs client and
|
|
37
|
+
server while developing the EC-SRP5 framing and server verifier — not a gated
|
|
38
|
+
example or integration test.
|
|
39
|
+
|
|
40
|
+
`$GW` is `10.0.2.2` (the host as seen from the CHR). `$U` / `$P` are the
|
|
41
|
+
credentials shared by the centrs server and the CHR client. `$SRV` is the centrs
|
|
42
|
+
server endpoint a client dials (the loopback server in unit tests). The real
|
|
43
|
+
`~/.config/tikoci/winbox.cdb` is never touched.
|
|
44
|
+
|
|
45
|
+
Output formats under test (see `README.md`, Output): `text` (default, live human
|
|
46
|
+
reports), `--csv` (streaming CSV records), and `--format json` (a single summary
|
|
47
|
+
envelope at completion, with `data.sessions[]` for the server / `data.reports[]`
|
|
48
|
+
for the client). The examples below assert on `json` (the structured summary) or
|
|
49
|
+
`csv` (the stream); `text` is the human render of the same data.
|
|
50
|
+
|
|
51
|
+
## CHR client → centrs server (gated; server cell)
|
|
52
|
+
|
|
53
|
+
These examples show the **`centrs btest server`** command and the result it
|
|
54
|
+
produces; `test/integration/btest.test.ts` separately drives the CHR's
|
|
55
|
+
`/tool/bandwidth-test` against `$GW`.
|
|
56
|
+
|
|
57
|
+
### 1. Unauthenticated TCP receive
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
centrs btest server --authenticate=false --bind 0.0.0.0 --duration 20s --format json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
While the server runs, the CHR executes `/tool/bandwidth-test $GW protocol=tcp
|
|
64
|
+
direction=receive duration=10s`. At completion the **summary envelope** is
|
|
65
|
+
`ok: true`, `meta.via: "btest"`, with `data.sessions[0]` =
|
|
66
|
+
`{ client, protocol: "tcp", direction: "transmit", user: "" }` (the server
|
|
67
|
+
transmits on the client's `receive`) and a non-zero aggregate tx rate;
|
|
68
|
+
`data.stopReason: "duration-elapsed"`. The server's measured tx is within
|
|
69
|
+
tolerance of the CHR's reported `rx-total-average`.
|
|
70
|
+
|
|
71
|
+
### 2. Unauthenticated UDP transmit (CSV stream)
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
centrs btest server --authenticate=false --bind 0.0.0.0 --duration 20s --csv
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
CHR runs `/tool/bandwidth-test $GW protocol=udp direction=transmit
|
|
78
|
+
local-udp-tx-size=1000 duration=10s` — client→server, which the guest originates,
|
|
79
|
+
so SLIRP NATs the UDP flow to the host cleanly. stdout is a CSV stream: a header
|
|
80
|
+
row then one record per session event, with at least one row carrying
|
|
81
|
+
`protocol=udp`, `direction=receive` (the server receives the client's transmit),
|
|
82
|
+
non-zero `rx_bps`, and a populated `lost_packets` column. UDP data rode the
|
|
83
|
+
allocated ports (`allocate-udp-ports-from` base). UDP **receive/both** (server→guest)
|
|
84
|
+
is gated behind the SLIRP smoke test and is not asserted here yet.
|
|
85
|
+
|
|
86
|
+
### 3. EC-SRP5-authenticated TCP receive
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
centrs btest server --authenticate --user $U --password $P --bind 0.0.0.0 --duration 20s --format json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
CHR runs `/tool/bandwidth-test $GW protocol=tcp direction=receive user=$U
|
|
93
|
+
password=$P duration=10s`. The EC-SRP5 **server role** verifies the client proof;
|
|
94
|
+
the summary `data.sessions[0]` carries `user: "$U"`, `protocol: "tcp"`,
|
|
95
|
+
`direction: "transmit"` (server transmits on the client's `receive`), `ok: true`,
|
|
96
|
+
and non-zero throughput. This is the example that exercises the net-new
|
|
97
|
+
server-side EC-SRP5 verifier against real RouterOS (TCP keeps the gated path off
|
|
98
|
+
the SLIRP UDP-reverse-NAT question).
|
|
99
|
+
|
|
100
|
+
### 4. Wrong credentials are rejected
|
|
101
|
+
|
|
102
|
+
> *Designed, not yet gated* — the auth-reject path is asserted by the loopback
|
|
103
|
+
> unit test (example 8's mismatched-password case); this CHR variant is a
|
|
104
|
+
> documented follow-up.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
centrs btest server --authenticate --user $U --password $P --bind 0.0.0.0 --duration 20s --format json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
CHR runs `/tool/bandwidth-test $GW user=$U password=wrong …`. The server keeps
|
|
111
|
+
listening and finishes its run, so the summary is `ok: true` but `warnings[]`
|
|
112
|
+
contains a `transport/auth-failed` entry (the EC-SRP5 proof did not verify) and
|
|
113
|
+
`data.sessions` is empty — no session opened, no throughput. The CHR side reports
|
|
114
|
+
an authentication failure.
|
|
115
|
+
|
|
116
|
+
### 5. Multi-connection TCP transmit
|
|
117
|
+
|
|
118
|
+
> *Designed, not yet gated* — the server's parallel-stream fan-out for
|
|
119
|
+
> `connection-count > 1` is not built yet (the session token is negotiated, but
|
|
120
|
+
> secondary TCP joins are not served). Documented follow-up; not part of the
|
|
121
|
+
> asserted run.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
centrs btest server --authenticate=false --bind 0.0.0.0 --duration 20s --format json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
CHR runs `/tool/bandwidth-test $GW protocol=tcp direction=transmit
|
|
128
|
+
connection-count=4 duration=10s`. The intended summary `data.sessions[0]` shows
|
|
129
|
+
`protocol: "tcp"`, `direction: "receive"` (server receives the client's
|
|
130
|
+
`transmit`) and `connectionCount: 4`; the server accepts the parallel TCP data
|
|
131
|
+
connections and the aggregate goodput is non-zero. Will confirm `connection-count`
|
|
132
|
+
is honored end to end once the fan-out lands.
|
|
133
|
+
|
|
134
|
+
## centrs client ↔ centrs server loopback (unit; client cell + codec)
|
|
135
|
+
|
|
136
|
+
### 6. Client TCP receive against a centrs server
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
centrs btest client $SRV --protocol tcp --direction receive --duration 3s --format json
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
With a centrs server on `$SRV` (`127.0.0.1:2000` in-test, `--authenticate=false`),
|
|
143
|
+
the summary envelope is `ok: true`, `meta.via: "btest"`, with `data.reports[]`
|
|
144
|
+
holding the per-`interval` samples (each carrying `rxBps` / `rxAvgBps`,
|
|
145
|
+
`direction: "receive"`) and aggregate `rxTotalAvgBps`; `data.stopReason:
|
|
146
|
+
"duration-elapsed"`.
|
|
147
|
+
|
|
148
|
+
### 7. Client UDP both directions (CSV stream)
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
centrs btest client $SRV --protocol udp --direction both --local-udp-tx-size 1000 --remote-udp-tx-size 1000 --duration 3s --csv
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
stdout is a CSV stream: a header then one row per `--interval` report, each with
|
|
155
|
+
non-zero `tx_bps` and `rx_bps`, a `lost_packets` column, and non-zero per-interval
|
|
156
|
+
`tx_bytes`/`rx_bytes`. Proves the client drives the same codec the CHR validated in
|
|
157
|
+
examples 1–3, and exercises the streaming CSV path.
|
|
158
|
+
|
|
159
|
+
### 8. EC-SRP5 handshake, client and server roles
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
centrs btest client $SRV --protocol udp --direction receive --user $U --password $P --duration 3s --format json
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Against a centrs server started with `--authenticate --user $U --password $P`, the
|
|
166
|
+
client's EC-SRP5 proof verifies against the server's verifier: `ok: true`,
|
|
167
|
+
throughput non-zero. A run with a mismatched `--password` instead returns
|
|
168
|
+
`ok: false`, `error.code = "transport/auth-failed"`, and no throughput. Exercises
|
|
169
|
+
both EC-SRP5 roles in one process without a router.
|
|
170
|
+
|
|
171
|
+
### 9. Option-grammar reject: `connection-count` with UDP
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
centrs btest client $SRV --protocol udp --connection-count 4
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`ok: false`, `error.code = "validation/option"`,
|
|
178
|
+
`error.context.option = "connection-count"`, and **no socket is opened**.
|
|
179
|
+
`connection-count` applies only to `protocol=tcp`. This is the product claim: btest
|
|
180
|
+
validates its option grammar before touching the network, with no `:parse` step.
|
|
181
|
+
|
|
182
|
+
### 10. Option-grammar reject: out-of-range UDP size
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
centrs btest client $SRV --protocol udp --local-udp-tx-size 99999
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`ok: false`, `error.code = "validation/option"` (UDP size must be `28..64000`), no
|
|
189
|
+
socket opened. The same gate rejects `connection-count` outside `1..255`,
|
|
190
|
+
`interval` outside `20ms..5s`, and (server) `max-sessions` outside `1..1000`.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# check
|
|
2
|
+
|
|
3
|
+
Probe a router and report which management paths are usable.
|
|
4
|
+
|
|
5
|
+
Status: `not-started`. This file is a stub.
|
|
6
|
+
|
|
7
|
+
## Intent
|
|
8
|
+
|
|
9
|
+
- TCP reachability per protocol (rest-api, native-api, ssh).
|
|
10
|
+
- Auth check on whichever protocols answered.
|
|
11
|
+
- Optional `--via` to limit the probe surface.
|
|
12
|
+
- L2 probing (mac-telnet reachability / ARP) is **opt-in, off by default** —
|
|
13
|
+
enabled by a MAC target, `--via mac-telnet`, or `--l2`. It needs L2 adjacency
|
|
14
|
+
and is slower, so IP-level probing (rest/native/ssh) stays the default.
|
|
15
|
+
- Output is the [standard envelope](../../docs/CONSTITUTION.md#result-envelope);
|
|
16
|
+
`data` is a per-protocol probe result map.
|
|
17
|
+
- `--fix` (opt-in, write-shaped): once `check` has authenticated over *one*
|
|
18
|
+
protocol, it reads the live device and writes back through the `devices` layer
|
|
19
|
+
(the only CDB writer), gated like any write (`--yes` / `confirm`). It refreshes
|
|
20
|
+
two things in one authenticated pass:
|
|
21
|
+
- **Connection metadata** — most usefully ports. Reached over MAC/mac-telnet,
|
|
22
|
+
it reads `/ip/service` to learn the real REST/native-API ports and writes
|
|
23
|
+
`port=` into the record's comment-kv so later calls connect directly.
|
|
24
|
+
- **Derived facts** — `board`, `version`, `software-id`, plus the `updated=`
|
|
25
|
+
stamp, from the same session (the same keys `devices add/set --check` and
|
|
26
|
+
`discover --save` populate). These stay queryable-but-stale facts that never
|
|
27
|
+
override a live read (see `commands/devices/README.md`, Derived facts).
|
|
28
|
+
|
|
29
|
+
`--fix` is **always explicit**, never implicit; plain `check` (no `--fix`) is a
|
|
30
|
+
pure read-only probe that writes nothing.
|
|
31
|
+
|
|
32
|
+
## Default service ports (grounding)
|
|
33
|
+
|
|
34
|
+
centrs assumes RouterOS defaults when a port is not overridden (CDB `port=` /
|
|
35
|
+
`--port`). Default `/ip/service` ports: `ftp` 21, `ssh` 22, `telnet` 23,
|
|
36
|
+
`www` 80 (often moved, e.g. 7080), `www-ssl` 443, `winbox` 8291, `api` 8728,
|
|
37
|
+
`api-ssl` 8729. (native API rides `api`/`api-ssl`; REST rides `www`/`www-ssl`.)
|
|
38
|
+
|
|
39
|
+
Once `check` (or any command) has authenticated access over *one* protocol, it
|
|
40
|
+
can read the live service map to discover non-default ports for *other*
|
|
41
|
+
protocols — e.g. reach a box over SSH, then learn the REST port without the user
|
|
42
|
+
specifying it, and "upgrade" the connection:
|
|
43
|
+
|
|
44
|
+
```routeros
|
|
45
|
+
:put [:serialize to=json options=json.pretty \
|
|
46
|
+
[ip/service/print detail as-value where !dynamic ]]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Open questions
|
|
50
|
+
|
|
51
|
+
- `--fix` scope **decided** (above): connection metadata (ports) **plus** the
|
|
52
|
+
derived facts (`board`/`version`/`software-id`/`updated=`), refreshed in one
|
|
53
|
+
authenticated pass through the `devices` write layer; always explicit opt-in,
|
|
54
|
+
never implicit. Plain `check` stays a read-only probe that writes nothing.
|
|
55
|
+
- L2 probing opt-in **decided** (Intent above): off by default; enabled by a MAC
|
|
56
|
+
target, `--via mac-telnet`, or `--l2`.
|
|
57
|
+
|
|
58
|
+
Defer until `retrieve` is `CHR-passed`. `check` overlaps with discovery and is
|
|
59
|
+
easier to design once name resolution lands.
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# devices
|
|
2
|
+
|
|
3
|
+
List, inspect, and edit RouterOS targets known to centrs. `devices` is the
|
|
4
|
+
user-facing surface over the device registry described in
|
|
5
|
+
`docs/CONSTITUTION.md`. It is the only command that may *write* to the CDB.
|
|
6
|
+
|
|
7
|
+
Status: `CHR-passed` for the surface described by `examples.md`. The read subset
|
|
8
|
+
(`list`/`show`/`groups`) and the CDB mutation surface (`add`/`edit`/`set`/
|
|
9
|
+
`remove`), ambiguity / `--match`, and the provenance/override examples are green
|
|
10
|
+
via `bun run test:integration` (`test/integration/devices.test.ts`,
|
|
11
|
+
fixture-backed — `devices` does no network IO, so its evidence is the
|
|
12
|
+
integration run, not a booted CHR). Encrypted-CDB **writes** round-trip through
|
|
13
|
+
decrypt → mutate → re-encrypt under the loaded password; encrypted **reads**
|
|
14
|
+
work with `--cdb-password`. See `docs/MATRIX.md` for the matrix row.
|
|
15
|
+
|
|
16
|
+
The redesign this README describes is now implemented: the `identity`/`mac`/`ip`
|
|
17
|
+
comment lookup keys and broadened `--match` selectors (`user=`/`target=`), the
|
|
18
|
+
`identity` field rename (was `name`), the symmetric `add`/`set` model (`edit`
|
|
19
|
+
reserved for a future interactive editor), the `(target, user)` record identity,
|
|
20
|
+
the `--profile-none`/`--profile-own` sentinels, the `__default__` fallback
|
|
21
|
+
record, and the top-level `tips[]` channel are all live (examples 10–16, 32–40;
|
|
22
|
+
`__default__` is resolver-level, unit-tested in `test/unit/resolver.test.ts`).
|
|
23
|
+
Treat this README as the spec, `docs/MATRIX.md` as the implemented status.
|
|
24
|
+
|
|
25
|
+
`devices` does not use a transport in the protocol sense and does not contact a
|
|
26
|
+
RouterOS device in phase 1. Its sources are:
|
|
27
|
+
|
|
28
|
+
- explicit CLI input,
|
|
29
|
+
- environment variables (`CENTRS_*`),
|
|
30
|
+
- the CDB at `~/.config/tikoci/winbox.cdb` (resolved under `$HOME`, not
|
|
31
|
+
`$XDG_CONFIG_HOME`; override with `--cdb-file` / `CENTRS_CDB_FILE`; if
|
|
32
|
+
encrypted, decrypt with `--cdb-password` / `CENTRS_CDB_PASSWORD`, or
|
|
33
|
+
`Bun.secret()` on the CLI when that lands),
|
|
34
|
+
- local ARP cache for MAC → IP resolution when explicitly enabled,
|
|
35
|
+
- (later) the MNDP cache and `dude.db` import via `tikoci/donny`.
|
|
36
|
+
|
|
37
|
+
## Identity model
|
|
38
|
+
|
|
39
|
+
The CDB record's natural identity is the **(target, user)** pair, matching
|
|
40
|
+
WinBox: saving the same address under a different user creates a second record,
|
|
41
|
+
not an update. The `target` is the connectable literal WinBox/REST/native use,
|
|
42
|
+
and is one of:
|
|
43
|
+
|
|
44
|
+
- an IPv4 address (`192.0.2.5`, optionally `host:port`),
|
|
45
|
+
- an IPv6 address (`2001:db8::5`, optionally `[2001:db8::5]:port`),
|
|
46
|
+
- a MAC (`AA:BB:CC:DD:EE:FF`, case-insensitive, separators normalized),
|
|
47
|
+
- a DNS name (`edge1.lan`).
|
|
48
|
+
|
|
49
|
+
WinBox has no separate "name" field, so a human handle is overlaid in the
|
|
50
|
+
comment as **lookup keys** (constitution: identity and CDB). centrs resolves
|
|
51
|
+
`<router>` against the `target` field **and** these comment-kv keys:
|
|
52
|
+
|
|
53
|
+
| Lookup key | Meaning |
|
|
54
|
+
| ----------- | ---------------------------------------------------------------------- |
|
|
55
|
+
| `identity=` | The device `/system/identity`. Human handle; deliberately may repeat. |
|
|
56
|
+
| `mac=` | A MAC, when `target` is an IP/DNS name rather than the MAC. |
|
|
57
|
+
| `ip=` | An IP, when `target` is an identity/DNS name rather than the IP. |
|
|
58
|
+
|
|
59
|
+
Whichever identifiers are not the `target` ride the comment as lookup keys, so
|
|
60
|
+
one record is resolvable by identity **or** IP **or** MAC **or** DNS-name
|
|
61
|
+
regardless of which is stored as `target`. These three keys are the *only*
|
|
62
|
+
sanctioned exception to "the comment is free text"; every other token stays
|
|
63
|
+
inert. (Earlier drafts said "the comment is not a lookup key" — superseded.)
|
|
64
|
+
|
|
65
|
+
When `<router>` matches an entry (by `target` or a lookup key), credentials and
|
|
66
|
+
other CDB-resident fields load from that entry. If nothing matches, the call
|
|
67
|
+
still proceeds but `--username` / `--password` (or `CENTRS_USERNAME` /
|
|
68
|
+
`CENTRS_PASSWORD`, or a `__default__` record) become mandatory.
|
|
69
|
+
|
|
70
|
+
### Ambiguity
|
|
71
|
+
|
|
72
|
+
Because identity is `(target, user)`, one address legitimately maps to several
|
|
73
|
+
records — the same host saved under two users, or `ipAdmin` + `ipUser` for one
|
|
74
|
+
host. That is not an error in itself; it only becomes ambiguous when `<router>`
|
|
75
|
+
alone cannot pick one (the caller named an address that several records share,
|
|
76
|
+
or a duplicated `identity=`):
|
|
77
|
+
|
|
78
|
+
- **TTY:** centrs prompts interactively, listing the matches and asking which
|
|
79
|
+
to use.
|
|
80
|
+
- **Non-TTY:** centrs errors with `identity/ambiguous`. The error envelope
|
|
81
|
+
lists all matching entries (by `cdbRecordIndex`, `target`, `user`, and
|
|
82
|
+
`recordType`). The caller pins the choice with `--match` by `user`
|
|
83
|
+
(`--match user=admin`), by record-type (`--match ipUser`), or by exact target.
|
|
84
|
+
|
|
85
|
+
`--prefer-family=ipv4|ipv6` is a non-interactive way to break v4/v6 ties
|
|
86
|
+
deterministically; with it set, no prompt is raised. It emits a warning
|
|
87
|
+
when it actually selected between candidates.
|
|
88
|
+
|
|
89
|
+
## Comment kv-soup (per-device overrides)
|
|
90
|
+
|
|
91
|
+
The CDB `comment` field is free text. centrs additionally parses tokens
|
|
92
|
+
shaped like `key=value` out of it. Recognized keys (allowlist) fall in two
|
|
93
|
+
groups: **lookup keys** affect how `<router>` resolves (see "Identity model");
|
|
94
|
+
**override keys** are per-device defaults for centrs settings.
|
|
95
|
+
|
|
96
|
+
| Key | Group | Effect |
|
|
97
|
+
| ---------- | -------- | ---------------------------------------------------------------------- |
|
|
98
|
+
| `identity` | lookup | Device `/system/identity`; a resolvable handle. May repeat across records. |
|
|
99
|
+
| `mac` | lookup | A MAC, when `target` is not the MAC. |
|
|
100
|
+
| `ip` | lookup | An IP, when `target` is not the IP. |
|
|
101
|
+
| `via` | override | Default transport for this device (`rest-api`, `native-api`, `ssh`, …). |
|
|
102
|
+
| `validate` | override | `true` / `false`. `false` is the escape hatch; CLI overrides. |
|
|
103
|
+
| `timeout` | override | Default request timeout in ms. Per-transport caps still apply. |
|
|
104
|
+
| `port` | override | Non-default transport port. Omitted when equal to the protocol default. |
|
|
105
|
+
| `ssh-key` | override | Per-device private key file path for SSH transports. |
|
|
106
|
+
| `source` | override | Provenance marker for discovered/imported records (`mndp`, `dude`, …). |
|
|
107
|
+
| `mcp` | override | MCP write policy for this device: `ro` (default) or `rw`. See `commands/mcp/`. |
|
|
108
|
+
|
|
109
|
+
Rules:
|
|
110
|
+
|
|
111
|
+
- First-class CDB fields (`user`, `password`, `group`, `profile`, `session`)
|
|
112
|
+
must **not** appear in the kv-soup — they already have CDB tags. This has two
|
|
113
|
+
distinct outcomes by path: a reserved token found while *parsing* an existing
|
|
114
|
+
comment is inert and only warns `cdb/reserved-option` (centrs won't act on it,
|
|
115
|
+
but won't reject a hand-written comment); explicitly passing one as a `devices
|
|
116
|
+
set` positional (`user=x`) is a hard `cdb/reserved-key` error, because you are
|
|
117
|
+
asking centrs to *write* it — use the dedicated flag (`--user`) instead.
|
|
118
|
+
- `ssh-key` stores a path only, never private key material. Key contents belong
|
|
119
|
+
in the user's filesystem / SSH agent and are always treated as sensitive if an
|
|
120
|
+
error needs to mention them. It joins the comment-kv allowlist *with* the SSH
|
|
121
|
+
transport (see `commands/terminal/README.md`), not before — a key path with no
|
|
122
|
+
working SSH transport is the kind of half-feature we avoid.
|
|
123
|
+
- Unknown keys → `cdb/unknown-option` warning. The call still succeeds.
|
|
124
|
+
- Tokens are shell-word tokenized. Values with spaces require double quotes
|
|
125
|
+
(`"…"`), with `\"` and `\\` escapes. `=` inside a quoted value is literal.
|
|
126
|
+
- An **empty value removes** the token: `devices set <t> via=` deletes the `via`
|
|
127
|
+
override (matching the internal upsert/remove model in `comment-kv.ts`). There
|
|
128
|
+
is no current need to store a literally-empty value; revisit if one arises.
|
|
129
|
+
- `commentMirror` is kept in sync with `comment` on every write.
|
|
130
|
+
|
|
131
|
+
Precedence (lowest → highest): built-in defaults → project config →
|
|
132
|
+
comment-kv → env (`CENTRS_*`) → CLI flag / API arg. `meta.settings` reports
|
|
133
|
+
the winner and source per setting.
|
|
134
|
+
|
|
135
|
+
### Derived facts (queryable, may be stale)
|
|
136
|
+
|
|
137
|
+
Beyond lookup keys and setting overrides, a record may carry **derived facts**
|
|
138
|
+
learned from a probe — `board`, `version`, `software-id` (from MNDP or `check`),
|
|
139
|
+
plus an `updated=<iso>` stamp marking when they were last refreshed. These are
|
|
140
|
+
*facts, not settings*: they are queryable/selectable (e.g. `--where board=RB5009`
|
|
141
|
+
to fan a command across a device class) but must **never override a live read**,
|
|
142
|
+
and they can go stale. `devices add`/`set --check` and `check --fix` refresh them
|
|
143
|
+
from a live device; `discover --save` seeds them. `software-id` is a
|
|
144
|
+
**verification** key
|
|
145
|
+
("is this still the same box?"), not a resolution key (you don't type it to reach
|
|
146
|
+
a device); `license-id` is convenience metadata to match by hand against
|
|
147
|
+
MikroTik's CHR licensing page. The `--where` selector plus this derived-fact
|
|
148
|
+
allowlist are the planned home for "operate on a class of devices," distinct from
|
|
149
|
+
`group`. (Promoting these keys to the comment-kv allowlist is a tracked code
|
|
150
|
+
follow-up.)
|
|
151
|
+
|
|
152
|
+
## Groups
|
|
153
|
+
|
|
154
|
+
Groups are CDB-native: each entry's `group` field is a single string. There
|
|
155
|
+
is no group-definition record. A group is the set of all entries whose
|
|
156
|
+
`group` value matches by exact, case-sensitive string compare. An entry with
|
|
157
|
+
`group=""` belongs to no group.
|
|
158
|
+
|
|
159
|
+
`--group <G>` on any command resolves to the membership set and fans out.
|
|
160
|
+
`discover --save` writes MNDP records with `group=discovered`
|
|
161
|
+
unless the caller supplies another group.
|
|
162
|
+
|
|
163
|
+
### Promotion (discovered → managed)
|
|
164
|
+
|
|
165
|
+
There is no `promote` verb. Moving one device out of `group=discovered` and
|
|
166
|
+
giving it credentials is `devices set <t> --new-group prod --user … --password
|
|
167
|
+
…`. A **bulk** promotion (a whole discovered class at once) is the same call with
|
|
168
|
+
a target selector picking the records:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
devices set --group discovered --new-group prod \
|
|
172
|
+
--user admin --password … --force
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`--group` (and `--where`/`--all`/positionals) always **selects** which records to
|
|
176
|
+
modify; `--new-group` always **writes** the group field. The two are distinct
|
|
177
|
+
flags, so there is no select-vs-set overload — this resolves the former wrinkle.
|
|
178
|
+
(Code follow-up: the CLI still overloads `--group` as the `add`/`set` field
|
|
179
|
+
setter today; `examples.md` reflects that current flag until the `--new-group`
|
|
180
|
+
split lands.)
|
|
181
|
+
|
|
182
|
+
## Fanout (multi-target invocations)
|
|
183
|
+
|
|
184
|
+
Any command may receive multiple positional targets, one or more `--group`
|
|
185
|
+
flags, `--all`, `--default`, and/or `--where <attr>=<value>` (device-class) — see
|
|
186
|
+
`docs/CONSTITUTION.md` (Target selection grammar). The resolved member set is
|
|
187
|
+
de-duplicated by CDB record index.
|
|
188
|
+
|
|
189
|
+
- Each member runs in parallel up to `--concurrency` (default
|
|
190
|
+
`max(1, floor(os.cpus().length / 2))`).
|
|
191
|
+
- `--fail-fast` aborts pending members on the first failure.
|
|
192
|
+
- The outer envelope's `data` is an array of inner envelopes, one per member,
|
|
193
|
+
in the resolved member order (CDB record index — see `devices` group
|
|
194
|
+
expansion), not completion order, so repeated runs produce stable diffs.
|
|
195
|
+
Members still execute in parallel and may *complete* out of order; progress
|
|
196
|
+
is surfaced via `meta`, but the `data` array is always reassembled in the
|
|
197
|
+
deterministic resolved order. `ok` of the outer envelope is `true` iff every
|
|
198
|
+
inner envelope is `ok: true`.
|
|
199
|
+
- Commands that are not safe to fan out (e.g. `terminal`) reject N > 1 with
|
|
200
|
+
`usage/fanout-not-supported`.
|
|
201
|
+
|
|
202
|
+
## Subcommands
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
centrs devices list [--group G] [--format json|yaml|text]
|
|
206
|
+
centrs devices show <target> [--explain] [--via <protocol>] [--match <type>]
|
|
207
|
+
centrs devices groups [--members]
|
|
208
|
+
centrs devices discover [--timeout 15s] [--group G] # = discover --save (save implied)
|
|
209
|
+
centrs devices add <target> [--user U] [--password P] [--new-group G]
|
|
210
|
+
[--profile P|--profile-none|--profile-own]
|
|
211
|
+
[--session S] [--comment "text k=v"]
|
|
212
|
+
[--record-type ipAdmin|ipUser|macTarget|...]
|
|
213
|
+
centrs devices set <selector…> [--user U] [--password P] [--new-group G]
|
|
214
|
+
[--profile P|--profile-none|--profile-own]
|
|
215
|
+
[--session S] [k=v ...]
|
|
216
|
+
centrs devices remove <selector…>
|
|
217
|
+
centrs devices edit <target> # future: clack/TUI wizard
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`add` and `set` are deliberately symmetric: both take the same first-class
|
|
221
|
+
flags and the same `k=v` comment tokens. The only difference is existence —
|
|
222
|
+
`add` creates (and refuses an existing target), `set` modifies (and refuses a
|
|
223
|
+
missing target). `edit` is reserved for a future interactive wizard (the
|
|
224
|
+
clack-style prompt used by `@tikoci/quickchr`); it is **not** a separate
|
|
225
|
+
field-editing verb, and invoking it today returns `usage/not-implemented`. There
|
|
226
|
+
is no `update`.
|
|
227
|
+
|
|
228
|
+
- `list` (alias `print`) shows resolved targets, their record type, group, and
|
|
229
|
+
a one-line provenance summary. No network IO.
|
|
230
|
+
- `show` (alias `get`) returns a single resolved target with the full per-field
|
|
231
|
+
source map in `meta.target`. `--explain` adds the raw CDB record dump under
|
|
232
|
+
`data.record`; `--via <protocol>` reports the protocol that would be selected
|
|
233
|
+
for the target without connecting.
|
|
234
|
+
- `groups` lists distinct non-empty group strings with member counts.
|
|
235
|
+
`--members` expands to the full membership per group.
|
|
236
|
+
- `discover` runs MNDP discovery with `--save` **implied** (you invoked the
|
|
237
|
+
registry surface, so you mean to populate it) — equivalent to `centrs discover
|
|
238
|
+
--save` with the same `group=discovered` / `source=mndp` provenance. It is the
|
|
239
|
+
natural onboarding step and where centrs steers toward setting fallback
|
|
240
|
+
credentials (`__default__`). See `commands/discover/README.md`. Still routes
|
|
241
|
+
through the atomic write layer below, so `devices` remains the only writer.
|
|
242
|
+
- `add` creates a record from first-class flags + comment tokens. Against an
|
|
243
|
+
existing `(target, user)` it errors `cdb/already-exists` unless `--force`
|
|
244
|
+
overwrites; the same `target` under a *different* `--user` is a new record,
|
|
245
|
+
not a collision.
|
|
246
|
+
- `set` modifies existing record(s). First-class fields change via flags
|
|
247
|
+
(`--user`, `--password`, `--new-group`, `--profile[-none|-own]`, `--session`);
|
|
248
|
+
comment lookup/override keys change via `k=v` positionals. **Which** records to
|
|
249
|
+
modify is the target selector — positionals, `--group`, `--where`, `--all`
|
|
250
|
+
(constitution: target selection) — so `--group` here *selects* and
|
|
251
|
+
`--new-group` *writes* the group field (no overload). Against a missing target
|
|
252
|
+
it errors `cdb/not-found-target`; a multi-record `set` needs `--force`. Writing
|
|
253
|
+
a first-class field name as a `k=v` positional (e.g. `user=x`) stays refused
|
|
254
|
+
with `cdb/reserved-key` — the flag is the only path. Unknown comment keys warn
|
|
255
|
+
(`cdb/unknown-option`), or error under `--strict`.
|
|
256
|
+
- `remove` (aliases `rm`, `delete`) removes a single entry. Group-wide deletes
|
|
257
|
+
(`--group G`) require `--force` (writes are large).
|
|
258
|
+
|
|
259
|
+
`--profile <name>` sets a named WinBox profile; `--profile-none` /
|
|
260
|
+
`--profile-own` write the WinBox sentinels `<none>` / `<own>`. `profile` and
|
|
261
|
+
`session` are **preserved for WinBox compatibility only** — centrs round-trips
|
|
262
|
+
them and lets you edit them, but never acts on them itself. Unset, `profile`
|
|
263
|
+
defaults to `<none>` (or is inherited from the `__default__` record).
|
|
264
|
+
|
|
265
|
+
Grounding: `profile` is WinBox's "Workspace" (a saved window layout). `<none>`
|
|
266
|
+
means start with a clean/empty layout; `<own>` means reuse the last session's
|
|
267
|
+
layout *for that router* (per-router, not global). Both are meaningless to
|
|
268
|
+
centrs's headless use, which is exactly why they are preserve-only.
|
|
269
|
+
|
|
270
|
+
## Default device (`__default__`)
|
|
271
|
+
|
|
272
|
+
A reserved record (`target=__default__`) holds global fallback creds + comment
|
|
273
|
+
overrides for any device whose own record leaves them unset. Create/edit it like
|
|
274
|
+
any other record (`devices add __default__ --user admin --password …`); WinBox
|
|
275
|
+
can edit it too. Precedence is per-field: args → env → device record →
|
|
276
|
+
`__default__` → built-in default (constitution: identity and CDB).
|
|
277
|
+
|
|
278
|
+
- `devices list` shows it; tag it `group=default` for clarity if you like.
|
|
279
|
+
- On the CLI/API it can supply creds even for a target with no CDB record. On
|
|
280
|
+
**MCP** it never widens the allowlist — an unregistered target is still
|
|
281
|
+
rejected; `__default__` only fills missing creds for a registered device.
|
|
282
|
+
|
|
283
|
+
## CDB write strategy
|
|
284
|
+
|
|
285
|
+
All writes go through one helper (`writeWinBoxCdb`) that:
|
|
286
|
+
|
|
287
|
+
1. Snapshots the current file to `winbox.cdb.bak.<timestamp>` beside it (the
|
|
288
|
+
timestamp is a filesystem-safe, lexicographically-sortable ISO variant).
|
|
289
|
+
2. Keeps the last 5 backups, deletes older ones.
|
|
290
|
+
3. Builds the new record set with unknown fields preserved verbatim (see
|
|
291
|
+
below).
|
|
292
|
+
4. Writes to `winbox.cdb.tmp.<pid>.<rand>` in the same directory and `fsync`s
|
|
293
|
+
it (then best-effort `fsync`s the directory).
|
|
294
|
+
5. `rename()`s over the original (atomic on POSIX), so a crash never leaves a
|
|
295
|
+
partially-written CDB.
|
|
296
|
+
|
|
297
|
+
Encrypted CDBs: the write layer decrypts on load, mutates the open record set,
|
|
298
|
+
then re-encrypts with the same password loaded from settings before the atomic
|
|
299
|
+
rename. A fresh salt is rolled on every write; the backup is the verbatim prior
|
|
300
|
+
ciphertext.
|
|
301
|
+
|
|
302
|
+
Refusals:
|
|
303
|
+
|
|
304
|
+
- Writing to an encrypted CDB without `--cdb-password` /
|
|
305
|
+
`CENTRS_CDB_PASSWORD` still fails earlier at load with
|
|
306
|
+
`cdb/password-required`.
|
|
307
|
+
- Writing to an unencrypted CDB with `--cdb-password` succeeds with the
|
|
308
|
+
existing `cdb/password-not-needed` warning. centrs does **not** silently
|
|
309
|
+
upgrade the file to encrypted.
|
|
310
|
+
|
|
311
|
+
### Unknown CDB fields
|
|
312
|
+
|
|
313
|
+
The current decoder knows tags 1–4, 6, 8, 9, 11, 12 and a fixed set of tcodes.
|
|
314
|
+
WinBox writes additional fields (RoMON IPv6 addresses, future settings) that
|
|
315
|
+
must round-trip.
|
|
316
|
+
|
|
317
|
+
- On read: any field with an unknown tag is kept verbatim on
|
|
318
|
+
`WinBoxCdbRecord.fields`. A field with an unknown tcode is captured as a
|
|
319
|
+
`rawTail` field — its `value` is a `Uint8Array` carrying the remainder of
|
|
320
|
+
the record (its own bytes plus any subsequent fields) verbatim, since the
|
|
321
|
+
decoder cannot know the value length without the schema.
|
|
322
|
+
- On write: known fields are re-encoded normally. A `rawTail` field is
|
|
323
|
+
emitted as its header (tag, marker, tcode) followed by `value` as raw
|
|
324
|
+
bytes. This round-trips WinBox-authored records that contain unknown
|
|
325
|
+
tcodes byte-for-byte. `devices add/edit/set/remove` will emit a
|
|
326
|
+
`cdb/unknown-field` warning when it preserves a record that carries
|
|
327
|
+
`rawTail` fields, listing the affected tags.
|
|
328
|
+
|
|
329
|
+
## Envelope shape (target provenance)
|
|
330
|
+
|
|
331
|
+
Every centrs envelope (not just `devices show`) carries:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
meta.target = {
|
|
335
|
+
target: "edge1.lan", // what the user typed, normalized
|
|
336
|
+
resolvedTarget: "edge1.lan", // what we connect to (post DNS, post --match)
|
|
337
|
+
addressFamily: "ipv4" | "ipv6" | "mac" | "dns",
|
|
338
|
+
mac?: "AA:BB:CC:DD:EE:FF",
|
|
339
|
+
user: "admin",
|
|
340
|
+
group?: "prod-edge",
|
|
341
|
+
via: "rest-api",
|
|
342
|
+
cdbRecordIndex?: 7, // index into CDB record array, if any
|
|
343
|
+
sources: {
|
|
344
|
+
user: "cdb",
|
|
345
|
+
password: "cdb",
|
|
346
|
+
via: "comment-kv",
|
|
347
|
+
validate: "cli",
|
|
348
|
+
timeout: "default",
|
|
349
|
+
// …per resolved setting
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
For fanout calls, each inner envelope carries its own `meta.target`. The
|
|
355
|
+
outer envelope's `meta.target` is replaced with `meta.targets` (the resolved
|
|
356
|
+
member list and the group(s) that expanded into it).
|
|
357
|
+
|
|
358
|
+
## Open questions (remaining work beyond the current `CHR-passed`)
|
|
359
|
+
|
|
360
|
+
`devices` is `CHR-passed` for the currently-implemented reads and writes against
|
|
361
|
+
both unencrypted and encrypted CDBs. The comment-kv *grammar* is settled — see
|
|
362
|
+
"Comment kv-soup" above and `test/unit/comment-kv.test.ts`. The decided redesign
|
|
363
|
+
(lookup keys, identity rename, symmetric `add`/`set`, `(target, user)` identity,
|
|
364
|
+
profile sentinels, `__default__`, `tips[]`) is now implemented; what remains:
|
|
365
|
+
|
|
366
|
+
**Still genuinely open:**
|
|
367
|
+
|
|
368
|
+
| Question | Affects | Notes |
|
|
369
|
+
| --- | --- | --- |
|
|
370
|
+
| ARP resolver test scheme | retrieve / execute when target is a MAC | Need deterministic fixtures per OS plus one live same-L2 proof before relying on ARP in integration. |
|
|
371
|
+
|
|
372
|
+
When an item ships with CHR examples, fold it into the matrix and delete the row.
|
|
373
|
+
|
|
374
|
+
## Residual risks
|
|
375
|
+
|
|
376
|
+
- **Encrypted-CDB writes have not been verified against WinBox.** centrs
|
|
377
|
+
decrypts → mutates → re-encrypts with a fresh salt, and the bytes round-trip
|
|
378
|
+
through centrs' own decoder. The "open in WinBox after a centrs write"
|
|
379
|
+
manual proof still needs to happen; until then, treat encrypted writes as
|
|
380
|
+
centrs-only and keep the backup the write layer leaves behind.
|
|
381
|
+
- **Unknown-tcode preservation is opaque past the first unknown tcode.** The
|
|
382
|
+
decoder captures one `rawTail` blob; fields that follow the unknown tcode
|
|
383
|
+
inside the same record are kept inside that blob and cannot be surfaced
|
|
384
|
+
one-by-one. Acceptable for round-trip writes (the bytes survive); not
|
|
385
|
+
sufficient if a future cell needs to expose those individual fields.
|
|
386
|
+
- **MAC → IP without CDB.** Resolver order is CDB first, then local ARP when
|
|
387
|
+
the caller opts into IP-level access. For `execute`, an unresolved MAC
|
|
388
|
+
defaults to mac-telnet instead. MNDP arrives through `discover --save` and
|
|
389
|
+
records provenance under `group=discovered`; it is not authoritative
|
|
390
|
+
inventory.
|
|
391
|
+
- **Comment kv-soup collisions with human prose.** Users may have existing
|
|
392
|
+
comments that contain `=` for non-kv reasons. The parser must require a
|
|
393
|
+
bare-word key with no leading whitespace inside the token and treat
|
|
394
|
+
free-form text outside `key=value` tokens as inert.
|
|
395
|
+
|
|
396
|
+
## Notes for future cells
|
|
397
|
+
|
|
398
|
+
- `devices` proper has no transport, but several flags on other commands
|
|
399
|
+
depend on its resolver (`--group`, `--match`, `--prefer-family`,
|
|
400
|
+
multi-target positionals). Those flags ship with their host command, not
|
|
401
|
+
with `devices`.
|
|
402
|
+
- `dude.db` import lives in `tikoci/donny` and feeds the resolver via the
|
|
403
|
+
same envelope as the CDB. Provenance source label: `dude`.
|
|
404
|
+
- The `centrs check` command will reuse the resolver and add a network probe;
|
|
405
|
+
`devices` itself stays RouterOS-IO-free.
|