@tikoci/centrs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/GLOSSARY.txt +420 -0
  3. package/README.md +196 -0
  4. package/SECURITY.md +30 -0
  5. package/commands/AGENTS.md +50 -0
  6. package/commands/btest/README.md +257 -0
  7. package/commands/btest/examples.md +190 -0
  8. package/commands/check/README.md +59 -0
  9. package/commands/devices/README.md +405 -0
  10. package/commands/devices/examples.md +488 -0
  11. package/commands/discover/README.md +170 -0
  12. package/commands/discover/examples.md +98 -0
  13. package/commands/execute/README.md +208 -0
  14. package/commands/execute/examples.md +353 -0
  15. package/commands/mcp/README.md +260 -0
  16. package/commands/mcp/examples.md +158 -0
  17. package/commands/retrieve/README.md +209 -0
  18. package/commands/retrieve/examples.md +306 -0
  19. package/commands/settings/README.md +69 -0
  20. package/commands/stream/README.md +70 -0
  21. package/commands/terminal/README.md +177 -0
  22. package/commands/terminal/examples.md +144 -0
  23. package/commands/transfer/README.md +315 -0
  24. package/commands/transfer/examples.md +368 -0
  25. package/docs/CONSTITUTION.md +417 -0
  26. package/docs/MATRIX.md +224 -0
  27. package/docs/errors/README.md +74 -0
  28. package/docs/errors/auth/failed.md +9 -0
  29. package/docs/errors/cdb/already-exists.md +9 -0
  30. package/docs/errors/cdb/created.md +9 -0
  31. package/docs/errors/cdb/decrypt-failed.md +9 -0
  32. package/docs/errors/cdb/empty-group.md +9 -0
  33. package/docs/errors/cdb/invalid-option.md +9 -0
  34. package/docs/errors/cdb/invalid-record.md +9 -0
  35. package/docs/errors/cdb/not-found-target.md +9 -0
  36. package/docs/errors/cdb/not-found.md +9 -0
  37. package/docs/errors/cdb/override-applied.md +9 -0
  38. package/docs/errors/cdb/parse-failed.md +9 -0
  39. package/docs/errors/cdb/password-not-needed.md +9 -0
  40. package/docs/errors/cdb/password-required.md +9 -0
  41. package/docs/errors/cdb/reserved-key.md +9 -0
  42. package/docs/errors/cdb/reserved-option.md +9 -0
  43. package/docs/errors/cdb/target-not-registered.md +9 -0
  44. package/docs/errors/cdb/unknown-field.md +9 -0
  45. package/docs/errors/cdb/unknown-option.md +9 -0
  46. package/docs/errors/cdb/write-not-permitted.md +9 -0
  47. package/docs/errors/discover/broadcast-unavailable.md +9 -0
  48. package/docs/errors/discover/failed.md +9 -0
  49. package/docs/errors/identity/ambiguous.md +9 -0
  50. package/docs/errors/identity/no-match.md +9 -0
  51. package/docs/errors/input/invalid-command.md +9 -0
  52. package/docs/errors/input/invalid-match.md +9 -0
  53. package/docs/errors/input/invalid-path.md +9 -0
  54. package/docs/errors/input/invalid-routeros-path.md +9 -0
  55. package/docs/errors/input/invalid-target.md +9 -0
  56. package/docs/errors/input/local-file-not-found.md +9 -0
  57. package/docs/errors/input/mac-address.md +9 -0
  58. package/docs/errors/input/max-results-exceeded.md +9 -0
  59. package/docs/errors/internal/devices-failed.md +9 -0
  60. package/docs/errors/internal/mcp-start.md +9 -0
  61. package/docs/errors/internal/mcp-tool.md +9 -0
  62. package/docs/errors/internal/native-api-length.md +9 -0
  63. package/docs/errors/internal/unhandled.md +9 -0
  64. package/docs/errors/internal/unreachable.md +9 -0
  65. package/docs/errors/mndp/encode-failed.md +9 -0
  66. package/docs/errors/mndp/listen-failed.md +9 -0
  67. package/docs/errors/mndp/malformed.md +9 -0
  68. package/docs/errors/routeros/api-fatal.md +9 -0
  69. package/docs/errors/routeros/api-protocol.md +9 -0
  70. package/docs/errors/routeros/api-trap.md +9 -0
  71. package/docs/errors/routeros/btest-protocol.md +12 -0
  72. package/docs/errors/routeros/btest-too-many-sessions.md +9 -0
  73. package/docs/errors/routeros/command-failed.md +14 -0
  74. package/docs/errors/routeros/ec-srp5-protocol.md +11 -0
  75. package/docs/errors/routeros/error.md +9 -0
  76. package/docs/errors/routeros/invalid-value.md +14 -0
  77. package/docs/errors/routeros/mac-telnet-error.md +9 -0
  78. package/docs/errors/routeros/mac-telnet-not-ready.md +9 -0
  79. package/docs/errors/routeros/mac-telnet-protocol.md +9 -0
  80. package/docs/errors/routeros/mac-telnet-unsupported-auth.md +9 -0
  81. package/docs/errors/routeros/protocol-not-implemented.md +9 -0
  82. package/docs/errors/routeros/request-failed.md +9 -0
  83. package/docs/errors/routeros/session-closed.md +13 -0
  84. package/docs/errors/routeros/unknown-attribute.md +13 -0
  85. package/docs/errors/routeros/unknown-path.md +15 -0
  86. package/docs/errors/routeros/unsupported-capability.md +9 -0
  87. package/docs/errors/settings/invalid-boolean.md +9 -0
  88. package/docs/errors/settings/invalid-format.md +9 -0
  89. package/docs/errors/settings/invalid-integer.md +9 -0
  90. package/docs/errors/settings/invalid-timeout.md +9 -0
  91. package/docs/errors/settings/invalid-via.md +9 -0
  92. package/docs/errors/settings/unsafe-protocol-blocked.md +9 -0
  93. package/docs/errors/target/mac-not-in-arp.md +14 -0
  94. package/docs/errors/target/mac-required.md +23 -0
  95. package/docs/errors/target/mac-unresolved.md +17 -0
  96. package/docs/errors/target/unresolved.md +9 -0
  97. package/docs/errors/transport/auth-failed.md +9 -0
  98. package/docs/errors/transport/auto-method.md +9 -0
  99. package/docs/errors/transport/capability-unsupported.md +22 -0
  100. package/docs/errors/transport/checksum-unavailable.md +9 -0
  101. package/docs/errors/transport/connection-closed.md +9 -0
  102. package/docs/errors/transport/connection-refused.md +9 -0
  103. package/docs/errors/transport/dns.md +9 -0
  104. package/docs/errors/transport/host-key-mismatch.md +9 -0
  105. package/docs/errors/transport/incomplete-transfer.md +9 -0
  106. package/docs/errors/transport/insecure-trust.md +9 -0
  107. package/docs/errors/transport/local-tool-missing.md +11 -0
  108. package/docs/errors/transport/network.md +9 -0
  109. package/docs/errors/transport/timeout.md +9 -0
  110. package/docs/errors/transport/tls-certificate.md +9 -0
  111. package/docs/errors/transport/unreachable.md +9 -0
  112. package/docs/errors/transport/unsupported-operation.md +9 -0
  113. package/docs/errors/usage/confirmation-required.md +9 -0
  114. package/docs/errors/usage/conflicting-flags.md +9 -0
  115. package/docs/errors/usage/invalid-concurrency.md +9 -0
  116. package/docs/errors/usage/missing-group.md +9 -0
  117. package/docs/errors/usage/not-implemented.md +9 -0
  118. package/docs/errors/usage/target-exists.md +9 -0
  119. package/docs/errors/usage/timeout-out-of-range.md +9 -0
  120. package/docs/errors/validation/not-implemented.md +9 -0
  121. package/docs/errors/validation/option.md +9 -0
  122. package/docs/errors/validation/syntax.md +9 -0
  123. package/docs/errors/validation/unknown-attribute.md +9 -0
  124. package/docs/errors/validation/unknown-path.md +9 -0
  125. package/package.json +96 -0
  126. package/src/AGENTS.md +7 -0
  127. package/src/btest.ts +906 -0
  128. package/src/cli/btest.ts +455 -0
  129. package/src/cli/common.ts +53 -0
  130. package/src/cli/devices.ts +527 -0
  131. package/src/cli/discover.ts +186 -0
  132. package/src/cli/execute.ts +333 -0
  133. package/src/cli/mcp.ts +109 -0
  134. package/src/cli/retrieve.ts +383 -0
  135. package/src/cli/terminal.ts +303 -0
  136. package/src/cli/transfer.ts +471 -0
  137. package/src/cli.ts +115 -0
  138. package/src/core/envelope.ts +221 -0
  139. package/src/core/error-catalog.ts +469 -0
  140. package/src/core/routeros-errors.ts +268 -0
  141. package/src/data/AGENTS.md +11 -0
  142. package/src/data/dude-db.ts +0 -0
  143. package/src/data/mndp-cache.ts +132 -0
  144. package/src/data/mndp.ts +405 -0
  145. package/src/data/winbox-cdb-write.ts +200 -0
  146. package/src/data/winbox-cdb.ts +913 -0
  147. package/src/devices-schema.ts +107 -0
  148. package/src/devices.ts +1717 -0
  149. package/src/discover.ts +802 -0
  150. package/src/errors.ts +189 -0
  151. package/src/execute.ts +1350 -0
  152. package/src/index.ts +353 -0
  153. package/src/mcp/config.ts +66 -0
  154. package/src/mcp/resources.ts +98 -0
  155. package/src/mcp/safety.ts +119 -0
  156. package/src/mcp/server.ts +225 -0
  157. package/src/mcp/tools.ts +657 -0
  158. package/src/mcp.ts +33 -0
  159. package/src/protocols/AGENTS.md +14 -0
  160. package/src/protocols/adapter.ts +866 -0
  161. package/src/protocols/btest-session.ts +1493 -0
  162. package/src/protocols/btest.ts +447 -0
  163. package/src/protocols/ec-srp5.ts +466 -0
  164. package/src/protocols/index.ts +118 -0
  165. package/src/protocols/mac-telnet-console.ts +655 -0
  166. package/src/protocols/mac-telnet.ts +1299 -0
  167. package/src/protocols/mtwei.ts +47 -0
  168. package/src/protocols/native-api.ts +796 -0
  169. package/src/protocols/sftp.ts +427 -0
  170. package/src/protocols/ssh.ts +297 -0
  171. package/src/resolver/cdb.ts +580 -0
  172. package/src/resolver/comment-kv.ts +360 -0
  173. package/src/resolver/index.ts +74 -0
  174. package/src/resolver/mac.ts +227 -0
  175. package/src/resolver/settings.ts +228 -0
  176. package/src/resolver/target.ts +442 -0
  177. package/src/retrieve-fanout.ts +533 -0
  178. package/src/retrieve.ts +1215 -0
  179. package/src/terminal.ts +444 -0
  180. package/src/transfer.ts +1554 -0
  181. package/src/tui.ts +22 -0
  182. package/src/webproxy.ts +16 -0
@@ -0,0 +1,488 @@
1
+ # devices — examples
2
+
3
+ Each numbered example is an executable spec. `devices` performs no network
4
+ IO, so its integration tests run against a fixture CDB rather than a live
5
+ CHR. The fixtures are built in-test from the known CDB primitives and written
6
+ into a temporary directory at runtime (open and encrypted, encrypted password
7
+ `centrs-test`); there are no checked-in `.cdb` files. See
8
+ `test/integration/devices.test.ts`.
9
+
10
+ Writes operate on a copy of the fixture per test; the source bytes are never
11
+ mutated in place. `$CDB` is the per-test path.
12
+
13
+ > The decided `devices` redesign is fully implemented and exercised below:
14
+ > lookup-key resolution (`identity=`/`mac=`/`ip=`, examples 32–34), the broadened
15
+ > `--match` selectors (`user=`/`target=`, examples 6/35/36), the symmetric
16
+ > `add`/`set` model (`edit` reserved for the interactive editor, example 37), the
17
+ > `(target, user)` record identity (example 36), the `--profile-none`/
18
+ > `--profile-own` sentinels (example 38), the `tips[]` channel (example 40), and
19
+ > the CLI verb aliases (example 41). The `__default__` fallback record is
20
+ > resolver-level (unit-tested in `test/unit/resolver.test.ts`). Do not reconcile
21
+ > the README back to these examples; reconcile forward.
22
+
23
+ ## list
24
+
25
+ ### 1. List all targets
26
+
27
+ ```bash
28
+ centrs devices list --cdb-file $CDB
29
+ ```
30
+
31
+ Envelope: `ok: true`, `data` is an array of `{ target, recordType, group?,
32
+ user, sources }`. No `meta.timing.request`.
33
+
34
+ ### 2. List a single group
35
+
36
+ ```bash
37
+ centrs devices list --cdb-file $CDB --group prod-edge
38
+ ```
39
+
40
+ `data` contains only members whose `group="prod-edge"`. Empty array is a
41
+ valid success.
42
+
43
+ ### 3. List with no CDB present
44
+
45
+ ```bash
46
+ centrs devices list --cdb-file /tmp/does-not-exist.cdb
47
+ ```
48
+
49
+ Errors with `cdb/not-found`. `details_url` populated.
50
+
51
+ ## show
52
+
53
+ ### 4. Show by exact target
54
+
55
+ ```bash
56
+ centrs devices show 192.0.2.5 --cdb-file $CDB
57
+ ```
58
+
59
+ `data` is one entry. `meta.target.sources` maps every resolved field to
60
+ `cdb` or `default`. `meta.target.cdbRecordIndex` is the entry's index.
61
+
62
+ ### 5. Show ambiguous target (non-TTY)
63
+
64
+ The CDB holds two entries with the same `target` (`2001:db8::5`) — the same
65
+ host saved once as an `ipAdmin` record and once as an `ipUser` record. With no
66
+ disambiguator and no interactive prompt available, `show` refuses to guess.
67
+
68
+ ```bash
69
+ centrs devices show 2001:db8::5 --cdb-file $CDB </dev/null
70
+ ```
71
+
72
+ `identity/ambiguous`. `error.context.matches` is an array of
73
+ `{ cdbRecordIndex, target, recordType }`, one per colliding entry.
74
+
75
+ ### 6. Show with --match disambiguation
76
+
77
+ `--match <record-type>` selects among duplicate targets by record type (one of
78
+ the `winBoxCdbRecordType` names: `ipAdmin`, `ipUser`, `macTarget`, …).
79
+
80
+ ```bash
81
+ centrs devices show 2001:db8::5 --cdb-file $CDB --match ipUser
82
+ ```
83
+
84
+ `ok: true`. The returned entry is the `ipUser` record; `meta.target.resolvedTarget`
85
+ is `2001:db8::5`. A `--match` token that names no colliding entry errors with
86
+ `identity/no-match`; an unknown record-type token errors with `input/invalid-match`.
87
+
88
+ ### 7. Show --explain dumps raw record
89
+
90
+ ```bash
91
+ centrs devices show 192.0.2.5 --cdb-file $CDB --explain
92
+ ```
93
+
94
+ `data.record` is the raw `WinBoxCdbRecord` (tag/marker/tcode/value). Useful
95
+ for diagnosing unknown-field preservation.
96
+
97
+ ## groups
98
+
99
+ ### 8. List distinct groups with counts
100
+
101
+ ```bash
102
+ centrs devices groups --cdb-file $CDB
103
+ ```
104
+
105
+ `data` is an array of `{ group: string, members: number }`, sorted by name.
106
+
107
+ ### 9. Groups --members expands to membership
108
+
109
+ ```bash
110
+ centrs devices groups --cdb-file $CDB --members
111
+ ```
112
+
113
+ `data` is an array of `{ group, members: [{ target, recordType }] }`.
114
+
115
+ ## add
116
+
117
+ ### 10. Add an ipAdmin entry
118
+
119
+ ```bash
120
+ centrs devices add 198.51.100.10 --user admin --password secret \
121
+ --group prod-edge --comment "site=NYC via=ssh" --cdb-file $CDB
122
+ ```
123
+
124
+ Envelope: `ok: true`. Re-reading the CDB shows the entry. Backup file
125
+ `$CDB.bak.<iso8601>` exists.
126
+
127
+ ### 11. Add the same (target, user) without --force
128
+
129
+ Record identity is `(target, user)`, so a collision requires the **same user**
130
+ (`192.0.2.5` is saved under `admin`).
131
+
132
+ ```bash
133
+ centrs devices add 192.0.2.5 --user admin --password other --cdb-file $CDB
134
+ ```
135
+
136
+ Errors with `cdb/already-exists`. No file mutation. No backup written.
137
+
138
+ ### 12. Replace the same (target, user) with --force
139
+
140
+ ```bash
141
+ centrs devices add 192.0.2.5 --user admin --password new --force --cdb-file $CDB
142
+ ```
143
+
144
+ `ok: true`. Backup written. The previous entry's unknown fields are
145
+ preserved on the replacement record; `meta.warnings` contains a
146
+ `cdb/unknown-field` entry listing the preserved tags.
147
+
148
+ ### 13. Add against encrypted CDB without password
149
+
150
+ ```bash
151
+ centrs devices add 198.51.100.20 --user u --password p \
152
+ --cdb-file $CDB_ENCRYPTED
153
+ ```
154
+
155
+ Errors with `cdb/password-required` (load fails before any mutation).
156
+
157
+ ### 14. Add against encrypted CDB with password
158
+
159
+ ```bash
160
+ centrs devices add 198.51.100.20 --user u --password p \
161
+ --cdb-file $CDB_ENCRYPTED --cdb-password centrs-test
162
+ ```
163
+
164
+ `ok: true`. The write layer decrypts → mutates → re-encrypts under the same
165
+ password with a fresh salt, then atomically renames over the original. The
166
+ backup (`.bak.<timestamp>`) is the verbatim prior ciphertext.
167
+
168
+ ## set (first-class fields)
169
+
170
+ `set` modifies an existing record. First-class fields change via flags; comment
171
+ override/lookup keys change via `k=v` positionals (next section). `add` takes the
172
+ same flags + positionals — the only difference is existence (`add` creates,
173
+ `set` requires the record to exist).
174
+
175
+ ### 15. Set credentials (first-class fields)
176
+
177
+ ```bash
178
+ centrs devices set 192.0.2.5 --user admin2 --password new --cdb-file $CDB
179
+ ```
180
+
181
+ `ok: true`. Other fields (group, profile, comment, unknowns) unchanged.
182
+
183
+ ### 16. Set a missing target
184
+
185
+ ```bash
186
+ centrs devices set 203.0.113.1 --user x --password y --cdb-file $CDB
187
+ ```
188
+
189
+ Errors with `cdb/not-found-target`.
190
+
191
+ ## set (comment kv-soup)
192
+
193
+ ### 17. Set a recognized override
194
+
195
+ ```bash
196
+ centrs devices set 192.0.2.5 via=ssh validate=false --cdb-file $CDB
197
+ ```
198
+
199
+ `ok: true`. Re-running `devices show 192.0.2.5` shows
200
+ `meta.target.sources.via = "comment-kv"`.
201
+
202
+ ### 18. Set a key that maps to a first-class CDB field
203
+
204
+ ```bash
205
+ centrs devices set 192.0.2.5 user=hacker --cdb-file $CDB
206
+ ```
207
+
208
+ Errors with `cdb/reserved-key` listing the forbidden keys
209
+ (`user`, `password`, `group`, `profile`, `session`).
210
+
211
+ ### 19. Set unknown key (lenient)
212
+
213
+ ```bash
214
+ centrs devices set 192.0.2.5 future=maybe --cdb-file $CDB
215
+ ```
216
+
217
+ `ok: true` with `cdb/unknown-option` warning. The token is written to
218
+ `comment` verbatim.
219
+
220
+ ### 20. Set unknown key with --strict
221
+
222
+ ```bash
223
+ centrs devices set 192.0.2.5 future=maybe --strict --cdb-file $CDB
224
+ ```
225
+
226
+ Errors with `cdb/unknown-option`. No write.
227
+
228
+ ### 21. Set value with spaces (quoting)
229
+
230
+ ```bash
231
+ centrs devices set 192.0.2.5 'note="rack 7 row B"' --cdb-file $CDB
232
+ ```
233
+
234
+ `ok: true`. Re-reading parses the value back as `rack 7 row B` (one token).
235
+
236
+ ## remove
237
+
238
+ ### 22. Remove a single entry
239
+
240
+ ```bash
241
+ centrs devices remove 192.0.2.5 --cdb-file $CDB
242
+ ```
243
+
244
+ `ok: true`. Backup written. Subsequent `devices show` returns
245
+ `cdb/not-found-target`.
246
+
247
+ ### 23. Remove unknown target
248
+
249
+ ```bash
250
+ centrs devices remove 203.0.113.99 --cdb-file $CDB
251
+ ```
252
+
253
+ `cdb/not-found-target`. No backup written.
254
+
255
+ ## Provenance and overrides
256
+
257
+ ### 24. CLI overrides comment-kv
258
+
259
+ CDB entry has `via=ssh` in its comment. CLI passes `--via rest-api`.
260
+
261
+ ```bash
262
+ centrs devices show 192.0.2.5 --via rest-api --cdb-file $CDB
263
+ ```
264
+
265
+ `meta.target.via = "rest-api"`, `meta.target.sources.via = "cli"`.
266
+ `meta.warnings` includes `cdb/override-applied`.
267
+
268
+ ### 25. Env overrides comment-kv but loses to CLI
269
+
270
+ ```bash
271
+ CENTRS_VIA=native-api centrs devices show 192.0.2.5 \
272
+ --via rest-api --cdb-file $CDB
273
+ ```
274
+
275
+ `meta.target.via = "rest-api"`, `meta.target.sources.via = "cli"`.
276
+
277
+ ## Fanout (resolved here, executed by other commands)
278
+
279
+ `devices` itself doesn't fan out — these tests assert the resolver behavior
280
+ that other commands consume.
281
+
282
+ ### 26. Group expansion is deterministic
283
+
284
+ ```bash
285
+ centrs devices list --group prod-edge --cdb-file $CDB --format json
286
+ ```
287
+
288
+ The order of resolved members matches CDB record order. This deterministic
289
+ ordering is canonical across centrs: fanout consumers reassemble their `data`
290
+ array in this same resolved order (not completion order) so repeated runs
291
+ produce stable diffs.
292
+
293
+ ### 27. Unknown group
294
+
295
+ ```bash
296
+ centrs devices list --group does-not-exist --cdb-file $CDB
297
+ ```
298
+
299
+ `ok: true`, `data: []`, `meta.warnings` includes `cdb/empty-group`.
300
+
301
+ ### 28. Mixed positional + group dedupe (assertion via `devices show`)
302
+
303
+ The resolver's de-dupe rule: positional targets that are also group members
304
+ appear once. Exercised through a downstream command's test once `retrieve`
305
+ gains multi-target support; tracked here so the rule is anchored.
306
+
307
+ ## Encrypted CDB read
308
+
309
+ ### 29. Read encrypted CDB with password
310
+
311
+ ```bash
312
+ centrs devices list --cdb-file $CDB_ENCRYPTED --cdb-password centrs-test
313
+ ```
314
+
315
+ `ok: true`. `meta.settings.cdbFile.kind = "cli"`,
316
+ `meta.settings.cdbPassword.kind = "cli"`.
317
+
318
+ ### 30. Wrong password
319
+
320
+ ```bash
321
+ centrs devices list --cdb-file $CDB_ENCRYPTED --cdb-password wrong
322
+ ```
323
+
324
+ `cdb/decrypt-failed`. `error.cause` includes the decoder's detected magic
325
+ mismatch.
326
+
327
+ ## Discovery provenance
328
+
329
+ ### 31. MNDP-discovered entries are marked as discovered
330
+
331
+ `discover --save` writes CDB records that `devices list` surfaces
332
+ with `group="discovered"` and `source=mndp` in comment-kv metadata.
333
+
334
+ ```bash
335
+ centrs devices list --cdb-file $CDB --group discovered
336
+ ```
337
+
338
+ `ok: true`; each member reports CDB provenance and discovery metadata.
339
+
340
+ ## Lookup-key resolution
341
+
342
+ `<router>` resolves against the `target` field **and** the `identity=` / `mac=` /
343
+ `ip=` comment lookup keys (see `README.md`, Identity model). MAC values compare
344
+ case- and separator-insensitively. These run against a dedicated lookup fixture
345
+ (`$CDB_LOOKUP`): one `ipAdmin` record `192.0.2.60` with
346
+ `comment="identity=edge1 mac=AA:BB:CC:DD:EE:11"`, and one `macTarget` record
347
+ `AA:BB:CC:DD:EE:22` with `comment="identity=edge2 ip=192.0.2.61"`.
348
+
349
+ ### 32. Resolve by the identity= lookup key
350
+
351
+ ```bash
352
+ centrs devices show edge1 --cdb-file $CDB_LOOKUP
353
+ ```
354
+
355
+ `ok: true`. The resolved entry's `target` is `192.0.2.60`;
356
+ `meta.target.identity` is `edge1`.
357
+
358
+ ### 33. Resolve by the mac= lookup key (case/separator-insensitive)
359
+
360
+ ```bash
361
+ centrs devices show aa-bb-cc-dd-ee-11 --cdb-file $CDB_LOOKUP
362
+ ```
363
+
364
+ `ok: true`. Resolves the same `192.0.2.60` record even though the typed MAC uses
365
+ dashes and lower case.
366
+
367
+ ### 34. Resolve by the ip= lookup key
368
+
369
+ ```bash
370
+ centrs devices show 192.0.2.61 --cdb-file $CDB_LOOKUP
371
+ ```
372
+
373
+ `ok: true`. Resolves the `AA:BB:CC:DD:EE:22` record; `meta.target.identity` is
374
+ `edge2`.
375
+
376
+ ### 35. A duplicated identity= is ambiguous; --match user= pins it
377
+
378
+ A second fixture (`$CDB_DUP_IDENTITY`) holds two distinct hosts that share
379
+ `identity=dup` (the deliberately non-unique identity case).
380
+
381
+ ```bash
382
+ centrs devices show dup --cdb-file $CDB_DUP_IDENTITY </dev/null
383
+ ```
384
+
385
+ `identity/ambiguous`; `error.context.matches` lists both records (with `user`).
386
+
387
+ ```bash
388
+ centrs devices show dup --match user=ops --cdb-file $CDB_DUP_IDENTITY
389
+ ```
390
+
391
+ `ok: true`; selects the `ops` record. `--match target=<addr>` and
392
+ `--match <record-type>` are the other selectors.
393
+
394
+ ## (target, user) identity
395
+
396
+ ### 36. The same address under a different user coexists
397
+
398
+ `192.0.2.5` is saved under `admin`. Adding it under a different user is a new
399
+ record, not a collision.
400
+
401
+ ```bash
402
+ centrs devices add 192.0.2.5 --user ops --password ops-pw --cdb-file $CDB
403
+ ```
404
+
405
+ `ok: true`, `data.replaced: false`. The CDB now holds two records for
406
+ `192.0.2.5`, so the bare address is ambiguous:
407
+
408
+ ```bash
409
+ centrs devices show 192.0.2.5 --cdb-file $CDB </dev/null
410
+ ```
411
+
412
+ `identity/ambiguous`. `centrs devices show 192.0.2.5 --match user=ops` resolves
413
+ the new record; `--match user=admin` the original.
414
+
415
+ ## edit (reserved for the interactive editor)
416
+
417
+ ### 37. `edit` reports usage/not-implemented
418
+
419
+ ```bash
420
+ centrs devices edit 192.0.2.5 --cdb-file $CDB
421
+ ```
422
+
423
+ Errors with `usage/not-implemented`. The interactive (clack/TUI) editor is not
424
+ built yet; use `set` for non-interactive field and metadata changes.
425
+
426
+ ## Profile sentinels
427
+
428
+ ### 38. Write the `<none>` / `<own>` profile sentinels
429
+
430
+ ```bash
431
+ centrs devices set 192.0.2.5 --profile-none --cdb-file $CDB
432
+ ```
433
+
434
+ `ok: true`; the record's `profile` becomes `<none>`. `--profile-own` writes
435
+ `<own>`; `--profile <name>` writes a named profile. The three are mutually
436
+ exclusive (passing more than one is a usage error).
437
+
438
+ ## Symmetric add (fields + k=v)
439
+
440
+ ### 39. Add with bare k=v positionals
441
+
442
+ ```bash
443
+ centrs devices add 198.51.100.30 --user admin --password p \
444
+ identity=spare via=ssh --cdb-file $CDB
445
+ ```
446
+
447
+ `ok: true`. The record's comment carries `identity=spare via=ssh`, so the device
448
+ is then resolvable as `spare`. A first-class field written as a positional
449
+ (`user=x`) is refused with `cdb/reserved-key` — use the `--user` flag.
450
+
451
+ ## Tips
452
+
453
+ Tips are advice, not anomalies (see `docs/CONSTITUTION.md`, Result envelope).
454
+ They ride the top-level `tips[]` channel, always present (`[]` when empty),
455
+ and render in text mode under a `Tips:` footer.
456
+
457
+ ### 40. An empty registry suggests adding a device
458
+
459
+ ```bash
460
+ centrs devices list --cdb-file $EMPTY_CDB
461
+ ```
462
+
463
+ `ok: true`, `data: []`, and `tips` carries a `tip/no-devices` entry (with a
464
+ `fix`). `--json` includes the same `tips[]` array. `devices show` on a record
465
+ with no stored password emits `tip/credentials-missing` unless a `__default__`
466
+ record supplies fallback creds.
467
+
468
+ ## Verb aliases
469
+
470
+ The RouterOS-muscle-memory aliases (`commands/AGENTS.md`, Verb vocabulary)
471
+ resolve silently to the canonical verb: `print`→`list`, `get`→`show`,
472
+ `rm`/`delete`→`remove`. Canonical names still win in help text and the usage
473
+ line; aliases are a CLI input affordance only (the MCP `op` enum and the API
474
+ functions stay canonical-only).
475
+
476
+ ### 41. Aliases resolve to the canonical verb
477
+
478
+ ```bash
479
+ centrs devices print --cdb-file $CDB # = list
480
+ centrs devices get 192.0.2.5 --cdb-file $CDB # = show
481
+ centrs devices rm 192.0.2.5 --cdb-file $CDB # = remove
482
+ centrs devices delete 192.0.2.5 --cdb-file $CDB # = remove
483
+ ```
484
+
485
+ Each alias produces the same envelope as its canonical verb — `meta.operation`
486
+ reports the canonical command (`list`/`show`/`remove`), never the alias. An
487
+ unknown verb still errors with `input/invalid-command`, and the error lists the
488
+ canonical verbs plus the accepted aliases.
@@ -0,0 +1,170 @@
1
+ # discover
2
+
3
+ Discover RouterOS neighbors over MNDP and optionally save them into the CDB.
4
+
5
+ Status: `CHR-passed` (see `docs/MATRIX.md`). The MNDP codec, TTL cache, UDP
6
+ listener, and `discover --save` are implemented; the real-L2 receive/decode/save
7
+ path is green against RouterOS CHR 7.23.1 via `test/integration/discover.test.ts`
8
+ (socket-connect bridge), and the network-independent paths stay unit-tested. The
9
+ matrix remains the only status surface. The `identity=`/`mac=` lookup-key writing and
10
+ MAC-keyed de-dupe described below are the decided target; the current `--save`
11
+ de-dupes on `target` and keeps identity/MAC in the inert detail — that gap moves
12
+ with the `devices` lookup-key work.
13
+
14
+ ## Synopsis
15
+
16
+ ```text
17
+ centrs discover [--timeout 15s] [--save] [--group discovered] \
18
+ [--port 5678] [--cdb-file <path>] [--cdb-password <secret>] \
19
+ [--format text|json|yaml]
20
+ ```
21
+
22
+ ## Intent
23
+
24
+ - MNDP is the first discovery source. It is a passive hint source, not
25
+ authoritative inventory.
26
+ - `--save` writes discovered targets into the configured CDB with provenance
27
+ metadata in comment kv-soup and default `group=discovered`.
28
+ - Saved records preserve enough metadata for later resolution: MAC, advertised
29
+ identity, platform, RouterOS version when present, source, and discovery
30
+ timestamp.
31
+ - Discovery never supplies credentials. A later `retrieve`, `execute`, or
32
+ `devices set` must still resolve credentials from CDB, env, CLI, or prompt.
33
+
34
+ ## How it works
35
+
36
+ MNDP is UDP broadcast on port `5678`; each RouterOS interface answers a refresh
37
+ with a TLV-encoded announcement (MAC, identity, version, platform, board,
38
+ uptime, software-id, interface, IPv4/IPv6). `discover`:
39
+
40
+ 1. Binds a UDP socket (default `0.0.0.0:5678`) with address/port reuse so
41
+ it can coexist with other MNDP listeners when the platform permits it.
42
+ 2. Broadcasts a refresh request **immediately**, then every 5s, and listens for
43
+ the `--timeout` window. Sending the first refresh up front (rather than after
44
+ the initial 5s tick) is what makes a short default window viable: responders
45
+ reply within a round-trip instead of waiting for their own ~60s passive cycle.
46
+ 3. Decodes each datagram with the pure codec in `src/data/mndp.ts`, tolerating
47
+ unknown TLV types and ignoring malformed datagrams and its own refresh echo.
48
+ 4. Stores neighbors in an in-memory TTL cache keyed by MAC.
49
+ 5. Returns a canonical envelope whose `data.neighbors` is sorted by MAC.
50
+
51
+ The codec is pure and the socket layer is thin and injectable, so both are
52
+ tested without a router (crafted packet fixtures + a loopback socket).
53
+
54
+ ## Flags and defaults
55
+
56
+ | Flag | Default | Meaning |
57
+ | ---------------- | ------------- | ---------------------------------------------------- |
58
+ | `--timeout` | `15s` | Listen window before results are returned. First refresh is sent immediately. |
59
+ | `--save` | off | Persist discovered neighbors into the CDB. |
60
+ | `--group` | `discovered` | First-class CDB group assigned to saved entries. |
61
+ | `--port` | `5678` | UDP port to bind for MNDP. |
62
+ | `--cdb-file` | resolved CDB | CDB path override for `--save`. |
63
+ | `--cdb-password` | env/none | Decrypt an encrypted CDB; also used to re-encrypt on `--save`. |
64
+ | `--format` | `text` | `text`, `json`, or `yaml`; renders the same envelope. |
65
+
66
+ `--save` without `--timeout` uses the default 15s window.
67
+
68
+ Bare `centrs discover` is **read-only** — it returns the envelope and never
69
+ writes the (WinBox-shared) CDB; `--save` is the explicit opt-in. The registry
70
+ surface `centrs devices discover` is the inverse: invoking `devices` means you
71
+ intend to populate it, so **`--save` is implied** there, and it is the home for
72
+ the "set default credentials so centrs starts useful" onboarding nudge. Both
73
+ paths write through the `devices` atomic write layer, so "devices is the only
74
+ writer" holds.
75
+
76
+ ## MNDP cache shape and TTL policy
77
+
78
+ (Answers the former matrix open question.)
79
+
80
+ - **Shape:** an in-memory `MndpCache` (`src/data/mndp-cache.ts`) keyed by the
81
+ lower-cased MAC address. MAC-less announcements fall back to a synthetic
82
+ `noaddr:<identity>:<interface>` key. Each entry records the latest decoded
83
+ neighbor plus `firstSeenAt` / `lastSeenAt` epoch-ms timestamps. The cache is
84
+ process-local only — the CDB stays the single durable datastore.
85
+ - **TTL:** entries expire `180s` (three ~60s announce cycles) after they were
86
+ last seen. Three cycles tolerates a couple of dropped broadcasts before a
87
+ still-present neighbor is forgotten. Re-observing a neighbor refreshes
88
+ `lastSeenAt` and keeps `firstSeenAt`. The clock is injectable for
89
+ deterministic tests.
90
+
91
+ ## `--save` provenance and de-dupe
92
+
93
+ - **Target:** a neighbor's CDB target is its advertised IPv4 when present
94
+ (record type `ipAdmin`), else its MAC (record type `macTarget`). One record
95
+ per device — the connectable address — not a MAC record plus an IP record.
96
+ - **Lookup keys:** the identifiers that are *not* the target are written as
97
+ comment lookup keys so the device resolves by any of them (see
98
+ `commands/devices/README.md`, Identity model): `identity=<advertised
99
+ identity>` always, and `mac=` when the target is the IP. The advertised
100
+ identity is therefore usable directly — `centrs retrieve <identity> …` — even
101
+ though it is duplicative across factory-default devices.
102
+ - **Provenance:** the comment also carries the allowlisted `source=mndp` token
103
+ plus a free-form, parenthesized detail (`discovered <iso> via MNDP (platform:
104
+ …; board: …; version: …; interface: …; software-id: …)`). The detail is inert
105
+ so it preserves provenance without emitting `cdb/unknown-option` warnings.
106
+ - **Group:** new entries get `group=discovered` (override with `--group`).
107
+ - **De-dupe rule:** keyed on the **MAC**, which MNDP always carries and which is
108
+ globally unique — `identity` is *not* the de-dupe key, because factory-default
109
+ devices all report `MikroTik`. A neighbor whose MAC already names a CDB entry
110
+ (as the `target` of a `macTarget` record or as a `mac=` lookup key on any
111
+ record) is **skipped, never overwritten** — hand-curated records win. Only
112
+ genuinely new devices are added. Writes reuse the `devices` atomic write path
113
+ (`addDevice`); the CDB is reloaded between writes so each add sees prior ones.
114
+ - **Encrypted CDBs:** `--save` against an encrypted CDB decrypts the file under
115
+ the loaded password, appends new neighbors, and re-encrypts with a fresh salt
116
+ before the atomic rename. The backup beside the CDB is the verbatim prior
117
+ ciphertext. Reading an encrypted CDB stays supported.
118
+ - **No auto-deletion / aging.** centrs never removes or rewrites a CDB record it
119
+ did not just create in this run. Discovered records age via their discovery /
120
+ `updated=` timestamp, and staleness is surfaced as a **tip** ("N discovered
121
+ entries are >30d old"), never an automatic delete — deleting records from a
122
+ WinBox-shared file unprompted is unsafe.
123
+
124
+ ## Errors
125
+
126
+ - `mndp/listen-failed` — the UDP bind failed (`EADDRINUSE`, e.g. WinBox holds
127
+ 5678 without compatible port sharing; or `EACCES`). Remediation: stop the
128
+ holder, or pass a different `--port`.
129
+ - `mndp/malformed` — the pure codec rejects a structurally invalid packet
130
+ (too-short header or a TLV length that overruns the buffer). The listener
131
+ ignores such datagrams rather than failing the whole scan.
132
+
133
+ ## L2 validation policy
134
+
135
+ Decided (2026-06-06), wired and proven (2026-06-07): the real-L2 integration
136
+ path for MNDP is `@tikoci/quickchr`'s host-side L2 capture. The host runs a TCP
137
+ server and the CHR gets a `socket-connect` NIC; QEMU streams every guest Ethernet
138
+ frame to the host length-prefixed (4-byte BE length + raw frame), and a frame
139
+ written back is injected into the guest. A small host shim
140
+ (`test/integration/mndp-l2-bridge.ts`) bridges that frame stream to centrs's
141
+ UDP/5678 listener — lifting the UDP/5678 payload out of each frame and
142
+ re-delivering it to the **unmodified** `discover()`/`listenMndp` path, and
143
+ injecting MNDP refresh frames back over the same TCP connection so RouterOS
144
+ answers within a round-trip — a real L2 path with no root and no native
145
+ raw-frame helper, while REST/native-API keep a separate user-mode NIC with
146
+ hostfwd. centrs runs with `sendRefresh: false` here (its own broadcast cannot
147
+ reach the guest through the bridge, so the shim does the L2 injection; the
148
+ refresh-send path stays covered by the loopback unit test). Prefer
149
+ `socket-connect` over `socket-mcast`: the multicast netdev is broken on macOS
150
+ (QEMU sets only `SO_REUSEADDR` where macOS needs `SO_REUSEPORT`; mcast works on
151
+ Linux/CI). Grounding: quickchr `docs/mndp.md`, `examples/mndp/`,
152
+ `test/lab/mndp/REPORT.md`.
153
+
154
+ `test/integration/discover.test.ts` exercises this end to end on RouterOS CHR
155
+ 7.23.1: it boots `["user", { type: "socket-connect", port }]`, captures a real
156
+ MNDP announcement, decodes it with centrs's codec, cross-checks
157
+ `identity`/`platform`/`board`/`version`/`mac` against REST, and `--save`s a
158
+ `macTarget`/`group=discovered`/`source=mndp` record (examples 1, 2, 4). It runs
159
+ under `bun run test:integration` (gated by `CENTRS_RUN_FAST_INTEGRATION`). The
160
+ network-independent examples (3 port-in-use, 5 custom group, 6 de-dupe, 7
161
+ encrypted CDB) are CDB/bind logic that is identical regardless of where the
162
+ neighbor came from, so they stay validated by `test/unit/discover.test.ts`
163
+ against a loopback socket and crafted fixtures.
164
+
165
+ Live-router finding worth carrying into mac-telnet: MNDP's board TLV (type 12)
166
+ advertises the short board id (`CHR`), while REST `/system/resource` `board-name`
167
+ is the verbose hardware string (`CHR QEMU Standard PC (i440FX + PIIX, 1996)`)
168
+ that *begins with* it — so cross-checks must use `startsWith`, not equality. The
169
+ same `socket-connect` bridge (with its frame-injection write-back) is the L2
170
+ harness the mac-telnet execute/terminal cells will reuse.