@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,417 @@
1
+ # Constitution
2
+
3
+ The load-bearing rules for `centrs`. Everything else (per-command READMEs, the
4
+ matrix, code, tests) must be consistent with this file. If something here is
5
+ wrong, fix it here first, then propagate. If a rule needs an exception, add the
6
+ exception here — not in scattered prose.
7
+
8
+ This file is short on purpose. Resist the urge to grow it.
9
+
10
+ ## What centrs is
11
+
12
+ A typed library, with CLI/MCP/TUI/proxy frontends, that reaches RouterOS over
13
+ multiple protocols and gives the caller **validated** access plus structured,
14
+ actionable diagnostics. The TypeScript API is the root surface; everything else
15
+ is an adapter over it.
16
+
17
+ The product is *not* "REST/SSH/native API wrappers." The product is
18
+ **validation + structured envelopes + identity resolution** on top of those
19
+ transports. Without those, this is a worse `curl`.
20
+
21
+ centrs also speaks a small set of MikroTik **peer protocols** that are *not*
22
+ RouterOS-command transports — currently the **bandwidth test** (`btest`), which
23
+ mirrors `/tool/bandwidth-test` and `/tool/bandwidth-server`. This is a
24
+ deliberate, separate capability axis (*measurement*, not "run a validated
25
+ command"): centrs still gives structured envelopes, identity resolution, and
26
+ friendly errors, but the "validate a RouterOS-shaped command" gate below does
27
+ **not** apply to it (see "Validation is the product" and Protocol selection's
28
+ `measure` row).
29
+
30
+ ## Validation is the product
31
+
32
+ Every RouterOS-shaped call goes through canonicalize → validate → run →
33
+ re-validate-server-side. Validation is not optional polish.
34
+
35
+ - `retrieve` and other read-shaped calls validate against
36
+ `/console/inspect request=syntax path=...,print` (path joined by commas) and
37
+ attribute inspection. The path syntax (commas, no leading slash, last token
38
+ is the verb) is part of the validator contract.
39
+ - `execute` and other CLI-shaped calls validate with `:put [:parse "..."]` plus
40
+ a semantic gate. **How `:parse` surfaces an error is transport-specific**
41
+ (grounded on CHR 7.23.1): `:parse` *does* reject an unknown attribute
42
+ (`bad parameter <name>`) and malformed CLI (`syntax error`), but over REST that
43
+ text rides the **HTTP-200 `ret` value** (no error status), and over the
44
+ **native API** `:put [:parse]` returns an opaque `*NN` handle that reveals
45
+ nothing at all. centrs's REST/native syntax gate runs `:parse` but does not
46
+ read the `ret`, so for those transports the unknown-attribute catch comes from
47
+ the separate **`/console/inspect`** semantic gate (or the server's own
48
+ write re-validation); the `:parse` round-trip there mainly backstops transport
49
+ errors and the local quote preflight. Over **mac-telnet** the interactive
50
+ console *prints* the `:parse` result, so centrs reads it and a single console
51
+ `:parse` covers both syntax and the unknown-attribute (semantic) gate — no
52
+ `/console/inspect` table needed. In every case a clean parse is necessary, not
53
+ sufficient on its own; semantic validation is a distinct, transport-appropriate
54
+ step (`/console/inspect`, the console `:parse` text, or server re-validation).
55
+ - **Peer-measurement commands (`btest`) are exempt from this gate** — they issue
56
+ no RouterOS command, so they validate *option-grammar* + the **EC-SRP5** auth
57
+ exchange instead (the envelope, identity-resolution, and friendly-error
58
+ contract still hold; only the RouterOS-command gate is absent). The grammar
59
+ rules are in `commands/btest/README.md`.
60
+ - A failing validator is a real result. Surface it with the structured error
61
+ envelope below — do not bypass.
62
+
63
+ There is no `update` verb: generalizing RouterOS add/set/remove into one abstract
64
+ write would need too many heuristics, so writes ride `execute`'s
65
+ canonicalize → validate → run path while `retrieve` stays read-only (see Protocol
66
+ selection).
67
+
68
+ **Disabling validation to make a call work is forbidden.** If the validator
69
+ rejects something a real router accepts, the validator is wrong; fix the
70
+ validator with CHR evidence. If a feature only passes its integration test
71
+ with `validate=false`, the feature is not done.
72
+
73
+ The default is `validate=true`. `--validate=false` exists as an escape hatch
74
+ for probing undocumented edges, never as a workaround for a centrs bug.
75
+
76
+ ## Result envelope
77
+
78
+ Every API/CLI call returns the same shape, regardless of transport:
79
+
80
+ ```ts
81
+ {
82
+ ok: boolean, // true if the operation succeeded end-to-end
83
+ data?: unknown, // the payload (array of records, single object, etc.)
84
+ warnings?: Warning[], // non-fatal anomalies (stale cache, unused --cdb-password, etc.)
85
+ tips?: Tip[], // advice, not anomalies (no username set, no CDB found, …)
86
+ error?: CentrsError, // present iff ok=false
87
+ meta: {
88
+ target: { ... }, // resolved target + provenance (where each field came from)
89
+ via: RouterOsProtocol, // chosen protocol id
90
+ settings: { ... }, // settings winners + sources (env / cli / cdb / default)
91
+ validation?: { ... }, // validator name + result, if validation ran
92
+ timing?: { ... }, // request/response durations
93
+ truncated?: { ... }, // populated when --max-bytes/--max-rows clipped output
94
+ }
95
+ }
96
+ ```
97
+
98
+ Notes:
99
+
100
+ - "Success with footnotes" is supported: `ok: true` may carry warnings.
101
+ - `warnings` and `tips` are both always present (`[]` when empty) so consumers
102
+ never branch on existence. They are distinct on purpose: a **warning** is a
103
+ non-fatal anomaly about *this* result (stale cache, unused `--cdb-password`); a
104
+ **tip** is advice that is explicitly *not* an error or anomaly (no username
105
+ set, no CDB found, consider a default device). The "this is not a problem,
106
+ just a pointer" signal is the whole reason tips are a separate channel rather
107
+ than folded into warnings. A `Tip` has the same shape as a warning
108
+ (`{ code, message, fix? }`) with `tip/*` codes and a `details_url`.
109
+ - CLI render must be lossless against the JSON envelope; `--format json` and
110
+ `--format yaml` are aliases that select serialization, not different shapes.
111
+ Text mode renders tips under a `Tips:` footer, distinct from warnings. This
112
+ lossless rule applies to **stdout** only: **stderr** carries progress and
113
+ interactive prompts (discover scan progress, write/ambiguity confirmation) and
114
+ is exempt — use it sparingly, since most operations are fast.
115
+ - `data` is the only field whose *shape* varies by command. Everything else —
116
+ including `warnings` and `tips` — is invariant in shape; only contents differ.
117
+
118
+ ## Error model
119
+
120
+ Errors are typed values, not thrown strings. Every error has:
121
+
122
+ - `code` — slash-namespaced, RouterOS-style, no spaces. Example:
123
+ `routeros/unsupported-path`, `transport/connection-refused`,
124
+ `validation/unknown-attribute`, `cdb/decrypt-failed`.
125
+ - `message` — one human sentence. No stack trace.
126
+ - `fix` — one human sentence describing the next step the caller should take.
127
+ - `details_url` — pointer to the canonical **per-code** page (the URL scheme is
128
+ in `docs/errors/README.md`).
129
+ - `cause?` — structured sub-error, when relevant (RouterOS nova error,
130
+ transport detail). Stack traces and raw exception text are summarized, not
131
+ embedded raw.
132
+ - `position?` — RouterOS's `(line N column M)` parse location, when the fault
133
+ carries one (console `:parse` rejections do). `column` is RouterOS's
134
+ authoritative 1-based **byte** offset — the console is not Unicode-aware — so
135
+ it is carried verbatim, never re-derived into a character index.
136
+ - `redactable_fields?` — names of fields the bug-report renderer must redact.
137
+
138
+ Credentials and private key material are always redactable. Private key paths are
139
+ configuration values and may appear in `meta.settings`; redact them only when an
140
+ error also carries sensitive key material or a caller explicitly marks the path
141
+ as sensitive.
142
+
143
+ The bug-report renderer is invoked **inline** via `--bug-report` on any command —
144
+ there is no separate `bug-report` verb. It re-renders the just-produced envelope
145
+ with `redactable_fields` stripped and each error's `details_url` kept, so a
146
+ shareable report needs no extra capture step (the envelope is already rich enough
147
+ to be the report).
148
+
149
+ Two error sources must be visually distinguishable:
150
+
151
+ 1. **centrs errors** — bad usage, validator rejection, transport plumbing.
152
+ 2. **RouterOS errors** — surfaced by the router (HTTP 4xx/5xx, parse refusal,
153
+ nova error). Map to a normalized `routeros/*` code, preserving the original
154
+ string.
155
+
156
+ RouterOS faults map to a normalized `routeros/*` code (preserving the original
157
+ string) via a CHR-grounded table shared by REST `detail` and native `!trap`
158
+ (`src/core/routeros-errors.ts`). Every error's `details_url` is the stable
159
+ `https://tikoci.github.io/centrs/errors/<code>`. The mapping shape, the
160
+ ground-on-CHR-evidence rule, and the URL-scheme rules live in
161
+ `docs/errors/README.md`.
162
+
163
+ ## Settings precedence
164
+
165
+ Lowest to highest priority:
166
+
167
+ 1. Built-in defaults
168
+ 2. Config file: `${XDG_CONFIG_HOME:-~/.config}/tikoci/centrs.env` — optional
169
+ dotenv-style `CENTRS_*=value` **global** defaults (centrs-only preferences
170
+ with no per-device home; **not** an inventory or credential store — per-device
171
+ settings stay in the CDB). Applied as fallbacks, never injected into
172
+ `process.env`. The `centrs settings` command is its front-end; the file
173
+ mechanics, `--skip-env-file`, and the settings-key vocabulary live in
174
+ `commands/settings/README.md`.
175
+ 3. CDB comment-kv metadata
176
+ 4. Environment variables (`CENTRS_*`)
177
+ 5. CLI flags / API call args
178
+
179
+ `meta.settings` reports the winning source for each resolved setting.
180
+
181
+ ### Identity and CDB
182
+
183
+ The `<router>` argument may be an IP, a DNS name, a MAC, or an **identity**
184
+ (the device's `/system/identity`); centrs resolves it through the CDB (then
185
+ through ARP / MNDP-derived metadata as fallbacks, once implemented). Two
186
+ principles are load-bearing:
187
+
188
+ - **Record identity is the `(target, user)` pair** — matching WinBox, the same
189
+ address saved under a different user is a second record, not an update.
190
+ `group` is a flat attribute, never part of the key.
191
+ - **The CDB is both the device datastore and the credential store.** The WinBox
192
+ CDB file at its well-known location *is* the inventory; there is no separate
193
+ cache. centrs-specific meaning is overlaid through an allowlist of comment-kv keys —
194
+ the lookup keys `identity=`/`mac=`/`ip=` (the one sanctioned exception to "the
195
+ comment is free text") plus override keys like `via`/`port`/`ssh-key`; every
196
+ other comment token stays inert. **Anything expressible in the CDB must also
197
+ be expressible via env/CLI/API**, so tests and ad-hoc use never require one.
198
+ `dude.db` import is out of scope (→ `tikoci/donny`).
199
+
200
+ The lookup-key grammar, the CDB resolution and ambiguity rules (`--match`,
201
+ `identity/ambiguous`, `--prefer-family`), the comment-kv allowlist, encrypted-CDB
202
+ handling, and the atomic write strategy live in `commands/devices/README.md` —
203
+ `devices` is the registry command and the CDB's only writer.
204
+
205
+ #### Default-device record (`__default__`)
206
+
207
+ A reserved CDB record (`target=__default__`) supplies fallback metadata +
208
+ credentials, **per field**, for a resolved device whose own record leaves them
209
+ unset. Precedence is per-field: per-call args → env (`CENTRS_*`) → matched device
210
+ record → `__default__` → built-in default. It is **core** (honored by CLI, API,
211
+ and MCP) and **never widens the MCP allowlist**: on MCP an unregistered target is
212
+ still `cdb/target-not-registered`, and `__default__` only fills *missing* creds
213
+ for an already-registered device. Create/edit it like any record; mechanics and
214
+ examples are in `commands/devices/README.md`.
215
+
216
+ Group selectors (e.g. `--group prod-edge`) target CDB groups so a single
217
+ `retrieve`/`execute`/etc. fans out to multiple routers. Group output shape
218
+ must round-trip through the same envelope.
219
+
220
+ #### Target selection grammar
221
+
222
+ One grammar across every non-terminal command. A call selects targets from any
223
+ mix of: `<router>` positionals (one or more), repeatable `--group <name>`,
224
+ `--all` (every CDB record), `--default` (the `__default__` record), and
225
+ repeatable `--where <attr>=<value>` — a **device-class** selector that matches
226
+ CDB-stored facts/comment-kv (e.g. `--where board=RB5009`), AND-combined across
227
+ repeats. The union is **de-duped by CDB record index** and run through the same
228
+ per-target pipeline; the `data` array is reassembled in resolved record order,
229
+ not completion order, so repeated runs diff cleanly. Destructive multi-target
230
+ operations (e.g. `devices remove` across a group) require `--force`. `terminal`
231
+ and single-session `stream` are not fan-out surfaces and reject N>1 with
232
+ `usage/fanout-not-supported`.
233
+
234
+ `--where` filters *which devices* by CDB-stored facts; keep it distinct from
235
+ `retrieve`/`execute`'s `--query`/`--filter`, which filter *RouterOS rows* in the
236
+ response. Two layers, two flag families.
237
+
238
+ `--group` is purely a **selector** in this grammar; the group *field* is written
239
+ by a distinct `--new-group` flag on `devices add`/`set`, so record selection and
240
+ field-setting never collide on one flag.
241
+
242
+ ### MCP surface: the CDB is the allowlist
243
+
244
+ The MCP frontend (`commands/mcp/`) is an adapter over this core, not a new set of
245
+ RouterOS semantics. Two boundaries are load-bearing:
246
+
247
+ - **Authorization is the CDB allowlist.** RouterOS-facing tools resolve targets
248
+ through the CDB only; an unregistered target is rejected
249
+ (`cdb/target-not-registered`), and `__default__` never widens the allowlist (see
250
+ Identity and CDB). The off-by-default `--allow-adhoc-targets` flag is reserved
251
+ for future inline-target flows — today no RouterOS-facing tool exposes an inline
252
+ host+credential schema, so the CDB allowlist is the only active target source.
253
+ - **Transport is stdio only.** HTTP/remote access is the proxy surface's job
254
+ (`src/webproxy.ts`), which fronts the same CDB; the MCP server grows no listener
255
+ of its own. One surface owns network exposure.
256
+
257
+ The scoped tool surface (verbs mirroring the CLI/API, never one tool per RouterOS
258
+ command), the per-device `mcp=ro|rw` + `confirm: true` write gate, the
259
+ `centrs://devices` / `centrs://errors` resources, and the full safety model are in
260
+ `commands/mcp/README.md`.
261
+
262
+ ## Protocol selection
263
+
264
+ Per-operation preferences, downgrade order in parens:
265
+
266
+ | Operation | Preferred | Downgrade order |
267
+ | --------- | --------- | --------------- |
268
+ | retrieve | rest-api, native-api; snmp for OID/MIB reads (future) | rest-api, native-api |
269
+ | stream | native-api, ssh | (REST cannot follow — 60s cap; bounded or rejected) |
270
+ | execute | native-api → rest-api → mac-telnet | native-api, rest-api, mac-telnet, ssh, romon, winbox-terminal |
271
+ | terminal | ssh | mac-telnet (L2 only when ssh fails or MAC given) |
272
+ | transfer | size/direction-aware: rest-api/native-api for ≤60 KB writes & all reads (chunked), sftp for large uploads | sftp ⇄ rest/native by size; scp · fetch · ftp explicit-only |
273
+ | discover | mndp | (not a transport for command operations) |
274
+ | measure | btest | (explicit-only — no downgrade) |
275
+
276
+ Rules:
277
+
278
+ - **Read vs write:** `retrieve` is read-only (rest-api/native-api); `execute` is
279
+ the read **and** write surface — writes (add/set/remove) ride `execute`, and
280
+ there is no `update` operation.
281
+ - **Never silently downgrade across `--via`.** If the caller pinned a transport
282
+ that cannot do the operation, error out with a `transport/*` code.
283
+ Auto-selection (no `--via`) may downgrade *within* the table, but every hop is
284
+ reported in `meta.warnings` with its reason.
285
+ - **Execute vs retrieve surfaces:** native-api, rest-api, ssh, mac-telnet (and
286
+ later RoMON / WinBox Terminal) are execute surfaces; SNMP is **retrieve-only**.
287
+ mac-telnet is the default execute path for an unresolved-MAC target — IP-level
288
+ access needs explicit ARP opt-in.
289
+ - **`measure` (btest) is explicit-only** — never in the auto-select/downgrade
290
+ chain, never a substitute, and no other command rides it.
291
+ - Per-command selection **depth** lives in the command READMEs: `stream`'s
292
+ follow/NDJSON contract and the REST-cannot-follow constraint
293
+ (`commands/stream/README.md`), `transfer`'s size/direction-aware method
294
+ selection and the explicit-only `scp`/`ftp`/`fetch`
295
+ (`commands/transfer/README.md`), SNMP's MIB-cache
296
+ (`commands/retrieve/README.md`), and btest's port/auth
297
+ (`commands/btest/README.md`).
298
+
299
+ REST-specific constraint: RouterOS REST has a 60-second hard timeout. Do not
300
+ let `--timeout` exceed 60s when `via=rest-api`; reject with a clear error.
301
+ Other transports may accept longer timeouts.
302
+
303
+ ### Transport trust (TLS / SSH host keys)
304
+
305
+ One opt-out across every transport: `--insecure` (`CENTRS_INSECURE`, CDB
306
+ `insecure=` comment-kv). Default is **verify**; `--insecure` disables peer
307
+ verification and adds a `transport/insecure-trust` warning to the envelope so the
308
+ downgrade is always visible. Because RouterOS ships **self-signed** certificates
309
+ and host keys, the *default posture per transport follows that transport's
310
+ ecosystem norm* rather than one literal value:
311
+
312
+ - **TLS** (REST `https`, native-api `api-ssl`) **verifies** by default; a
313
+ self-signed cert fails with `transport/tls-certificate` whose remediation names
314
+ `--insecure`. (Both transports honor the same knob — previously native-api
315
+ silently accepted any cert; that is fixed.)
316
+ - **SSH host keys** (sftp/scp) default to **trust-on-first-use**
317
+ (`StrictHostKeyChecking=accept-new` into the user's `known_hosts`): a new key is
318
+ accepted, a *changed* key fails with `transport/host-key-mismatch`. `--insecure`
319
+ disables the check entirely. TOFU is the universal SSH norm, so the SSH default
320
+ is laxer than the TLS default by design.
321
+
322
+ SSH identity selection: the `sshKey` setting (`--ssh-key`/`CENTRS_SSH_KEY`/CDB
323
+ `ssh-key=`) carries a private-key **path only** — never key material; when unset,
324
+ the host SSH agent / `~/.ssh/config` is used. RouterOS refuses password login once
325
+ an SSH key is set for a user (`password-authentication=yes-if-no-key`, the
326
+ default), so key auth is the normal sftp path.
327
+
328
+ ## Done definition
329
+
330
+ A feature is done when its **CHR integration test is green** against a real
331
+ RouterOS CHR booted by `@tikoci/quickchr`. Not when code exists. Not when unit
332
+ tests pass. Not when the spec says so. This is the **normative** done
333
+ definition; `AGENTS.md` and
334
+ `.github/instructions/done-definition.instructions.md` point here (the latter
335
+ carries the Copilot-workflow procedure for satisfying it).
336
+
337
+ `docs/MATRIX.md` is the single source of truth for *what* is done and defines the
338
+ cell states (`not-started` / `designed` / `coded` / `CHR-passed`). A cell
339
+ advances only with the matching evidence in the same change, and the commit that
340
+ advances it must include the test name and CHR version.
341
+
342
+ ## Boundaries
343
+
344
+ centrs does:
345
+
346
+ - Resolve devices/groups from explicit input, env, CDB, MNDP cache.
347
+ - Choose protocols per the table above and configure them.
348
+ - Validate before execute; re-validate server-side immediately before run.
349
+ - Return structured envelopes with provenance and warnings.
350
+ - Use the WinBox CDB at its well-known location as the device datastore and
351
+ cache; overlay centrs metadata via comment-kv and groups.
352
+ - Speak MikroTik peer-measurement protocols on explicit request — the bandwidth
353
+ test (`btest`), as both client and server — with the same envelope / identity /
354
+ error contract (but not the RouterOS-command validation gate).
355
+
356
+ centrs does not:
357
+
358
+ - Replace RouterOS syntax with high-level abstractions.
359
+ - Treat passive discovery (MNDP) as authoritative inventory.
360
+ - Run write-shaped operations without an explicit target and validation policy.
361
+ - Make generated output the hand-edited source of truth.
362
+
363
+ ### Network listeners
364
+
365
+ centrs opens a network listener in exactly two sanctioned places: the **proxy**
366
+ surface (HTTP / remote access; `src/webproxy.ts`) and the **btest server**
367
+ (`centrs btest server`, TCP/UDP port 2000). The btest server is an **explicit,
368
+ foreground, user-invoked** command — not an always-on daemon, and not the proxy.
369
+ It **binds loopback by default** (`--bind` to expose), **requires auth by
370
+ default** (`authenticate=yes`, mirroring RouterOS), and is security-sensitive
371
+ surface: treat credentials as redactable (`SECURITY.md`). The MCP server stays
372
+ stdio-only and grows no listener of its own.
373
+
374
+ ## Invariants
375
+
376
+ - One settings vocabulary across API/CLI/MCP/TUI/proxy.
377
+ - Friendly, structured errors are part of the API contract — not optional polish.
378
+ - Help text, source reporting, redaction, and bug-report envelopes are
379
+ shared-core, not per-frontend.
380
+ - Integration tests prefer real CHR via `quickchr` over elaborate mocks when
381
+ behavior depends on RouterOS.
382
+ - Generated docs are preferred when the source of truth is code, CLI metadata,
383
+ or schema.
384
+
385
+ ## Adjacent projects (grounding sources)
386
+
387
+ - `tikoci/rosetta` — RouterOS docs / RAG.
388
+ - `tikoci/restraml` — REST schema and inspect output.
389
+ - `tikoci/m2ir` — protocol IR (WinBox, etc.), and the home for the
390
+ declarative "schema-as-data" approach to representing protocol internals
391
+ (consult it when WinBox-terminal / RoMON / heavy protocol work lands, rather
392
+ than hand-rolling protocol knowledge in centrs). Not an accessible source for
393
+ the RouterOS error-string vocabulary; ground error mappings on live CHR
394
+ strings.
395
+ - `tikoci/lsp-routeros-ts` — canonicalization, parse-validation patterns.
396
+ - `tikoci/vscode-tikbook` — peer RouterOS front-end (notebook UX); a source for
397
+ run-time error-string mapping (faults that pass `:parse` but fail at run).
398
+ - `tikoci/quickchr` — CHR-backed integration test harness.
399
+
400
+ When one of these owns a question, defer to it instead of restating here.
401
+
402
+ External reference implementations to compare centrs protocol/transport work
403
+ against (SSH patterns, native-API behavior, CDB format, WinBox framing) are kept
404
+ as bibliography entries in `GLOSSARY.txt` — e.g. `netmiko`,
405
+ `terraform-provider-routeros`, `librouteros-api`, `RouterOS_Tools`,
406
+ `Winbox_Protocol_Dissector`.
407
+
408
+ ### Canonicalizer ownership
409
+
410
+ centrs owns the **script-vs-structured execution gate** — the load-bearing
411
+ discriminator for which validation runs and whether the write-confirmation prompt
412
+ fires; widening what counts as `structured` is a product regression. The shared,
413
+ pure command canonicalizer that `rosetta` / `lsp-routeros-ts` publish is for
414
+ canonicalization only, never the structured-mode predicate. The gate contract,
415
+ its pinning test, and the parser-vendoring preconditions are documented in
416
+ `commands/execute/README.md` and the `canonicalizeExecuteCommand` doc-comment
417
+ (`src/execute.ts`).
package/docs/MATRIX.md ADDED
@@ -0,0 +1,224 @@
1
+ # Matrix
2
+
3
+ The product is a 2D grid of commands × protocols. This file is the **only**
4
+ source of truth for what is done. No alpha gates, no milestones, no roadmap
5
+ prose.
6
+
7
+ Pick the highest-priority cell that is not `CHR-passed`. That is the next
8
+ work. See `docs/CONSTITUTION.md` for the cell-state definitions.
9
+
10
+ ## Cell states
11
+
12
+ | State | Meaning |
13
+ | -------------- | --------------------------------------------------------------- |
14
+ | `—` | Not applicable / out of scope for this protocol |
15
+ | `not-started` | No code and no design |
16
+ | `designed` | `commands/<name>/README.md` describes intent and flags |
17
+ | `coded` | Implementation exists in `src/` |
18
+ | `CHR-passed` | Every example in `commands/<name>/examples.md` is green on CHR |
19
+
20
+ A cell advances only with the matching evidence in the same change.
21
+
22
+ ## Grid
23
+
24
+ | Command | rest-api | native-api | ssh | mac-telnet | snmp | mndp | romon | winbox-terminal |
25
+ | -------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ---------------- |
26
+ | retrieve | `CHR-passed` | `CHR-passed` | — | — | `not-started` | — | — | — |
27
+ | stream | — | `designed` | `designed` | — | — | — | — | — |
28
+ | execute | `CHR-passed` | `CHR-passed` | `CHR-passed` | `CHR-passed` | — | — | `not-started` | `not-started` |
29
+ | terminal | — | — | `CHR-passed` | `CHR-passed` | — | — | — | — |
30
+ | transfer | `CHR-passed` | `CHR-passed` | `CHR-passed` | — | — | — | — | — |
31
+ | devices | — | — | — | — | — | — | — | — |
32
+ | discover | — | — | — | — | — | `CHR-passed` | — | — |
33
+ | check | `not-started` | `not-started` | `not-started` | `not-started` | `not-started` | `not-started` | `not-started` | `not-started` |
34
+ | settings | — | — | — | — | — | — | — | — |
35
+
36
+ `devices` does not use a transport in the protocol sense, so its grid row stays
37
+ `—`; its cell state is `CHR-passed`. `devices` performs no network IO, so that
38
+ evidence is a fixture-backed `bun run test:integration` run
39
+ (`test/integration/devices.test.ts`, every example in
40
+ `commands/devices/examples.md`) against an in-test CDB (open + encrypted), not a
41
+ booted CHR. The implemented surface (read subset, CDB mutation, lookup-key
42
+ resolution, `(target, user)` identity, ambiguity / `--match`, profile sentinels,
43
+ verb aliases, `__default__`, `tips[]`, provenance/override) and the one open item
44
+ (ARP test scheme) are documented in `commands/devices/README.md`.
45
+
46
+ `discover` is `CHR-passed`: the MNDP codec (`src/data/mndp.ts`), the TTL neighbor
47
+ cache (`src/data/mndp-cache.ts`), the UDP listener, and `discover --save`
48
+ (through `devices`' atomic write layer) live in `src/discover.ts`. The real-L2
49
+ receive/decode/save path is green on RouterOS CHR 7.23.1 via
50
+ `test/integration/discover.test.ts` (the quickchr `socket-connect` L2 bridge,
51
+ `test/integration/mndp-l2-bridge.ts`; examples 1, 2, 4); the network-independent
52
+ codec/cache/listener/`--save` paths stay unit-tested (examples 3, 5, 6, 7). The
53
+ wire format, the de-dupe rule (`target`-keyed today, MAC-keyed is the decided
54
+ target), TTL/timeout defaults, the live board-TLV finding (short board id vs the
55
+ verbose REST `board-name`), and the L2 validation policy are documented in
56
+ `commands/discover/README.md`.
57
+
58
+ There is no `update` command: `execute` is the single read/write surface for
59
+ RouterOS add/set/remove, and `retrieve` stays read-only. See
60
+ `docs/CONSTITUTION.md` (protocol selection).
61
+
62
+ `stream` is `designed` (no code yet): the read-only follow surface (NDJSON
63
+ stream-of-envelopes; native-api/ssh only — REST's 60s cap cannot follow) is
64
+ specified in `commands/stream/README.md`. The native-api streaming reader it will
65
+ consume already exists (`src/protocols/native-api.ts`).
66
+
67
+ `transfer` is `CHR-passed` for `rest-api`/`native-api` and for **sftp** (the
68
+ `ssh` column's first method): `src/transfer.ts` + `src/cli/transfer.ts`
69
+ (size/direction-aware method selection, leading-slash normalization, the
70
+ `print`-probe existence guard, `--verify`) over the shared `FileBackend` seam,
71
+ with the SFTP client in `src/protocols/sftp.ts` (host OpenSSH `sftp` subsystem;
72
+ `scp` deferred). `test/integration/transfer.test.ts` is green on real CHR 7.23.1
73
+ (110 assertions): the rest + native round-trip, the native `N1`–`N4` mirror, the
74
+ sftp `S1`–`S5` round-trip, the stdin/stdout forms via the subprocess harness
75
+ (examples 8–10), and example 17 (chunked REST read of an sftp-seeded >60 KB file).
76
+ The method-selection grammar, the SFTP-vs-SCP rationale, the `--verify` behavior
77
+ (sftp trusts the transfer guarantee; rest/native re-read the `/file` size), and the
78
+ deferred `scp`/`fetch`/`ftp` methods are documented in
79
+ `commands/transfer/README.md`.
80
+
81
+ `execute / ssh` is `CHR-passed` (the second SSH consumer): a per-command batch
82
+ client (`SshExecClient` in `src/protocols/ssh.ts` — one `ssh user@host "<cmd>"`
83
+ per command, sharing `sftp.ts`'s host-`ssh` plumbing) behind the `SshExecAdapter`
84
+ console transport (execute-only, like mac-telnet; structured reads/inspect
85
+ unsupported). It reuses the mac-telnet `:put [:parse …]` gate verbatim. Green via
86
+ `test/integration/execute-ssh.test.ts` (S1–S4) + `test/unit/ssh.test.ts`; the
87
+ no-PTY / clean-output / `:parse` wire grounding lives in the `src/protocols/ssh.ts`
88
+ header and `commands/execute/README.md`.
89
+
90
+ `terminal / ssh` is `CHR-passed` (the third and last SSH consumer): it execs the
91
+ host `ssh` with inherited stdio (`buildSshTerminalArgv` in `src/terminal.ts`,
92
+ reusing `sshCommonOptions`) and lets the OS relay RouterOS's already-clean no-PTY
93
+ console — no screen emulation. No `-t` is passed (`-tt` hangs RouterOS);
94
+ `runTerminal` returns `ssh`'s exit code. A host target defaults to ssh, a MAC
95
+ target to mac-telnet. Green via `test/integration/terminal-ssh.test.ts` (TS1/TS2),
96
+ with argv unit-tested in `test/unit/terminal.test.ts`. **All three SSH cells
97
+ (transfer/execute/terminal) are `CHR-passed`.** The no-PTY / exit-code semantics are documented in
98
+ `src/terminal.ts`; the no-multi-line-brace limitation is in
99
+ `commands/terminal/README.md`.
100
+
101
+ `fetch` (centrs-as-HTTP-server + `/tool/fetch`) is a **deferred, explicit-only
102
+ method within the rest-api/native-api cells**, not a grid column — it needs
103
+ inbound reachability (router → centrs) so it is never auto-selected.
104
+
105
+ `settings` is `designed` and transport-less (like `devices`), so its grid row
106
+ stays `—`. `commands/settings/README.md` describes the `centrs.env` + `__default__`
107
+ front-end (interactive first-time setup plus `settings get/set/reset/print`); no
108
+ code yet.
109
+
110
+ ### Transport-base readiness (below the command grid)
111
+
112
+ The grid above tracks command cells. The transports those cells ride are
113
+ implemented and grounded in their `src/protocols/*.ts` **module headers** — the
114
+ single home for wire-format, auth, and delivery facts (one fact, one place).
115
+ Status here is terse; the module header carries the detail.
116
+
117
+ - **native-api** (`src/protocols/native-api.ts`): word/sentence codec, streaming
118
+ reader, login (post-6.43 plaintext + legacy MD5 challenge), tagged command
119
+ multiplexing, typed error mapping. Live cell status is in the grid (`retrieve`,
120
+ `execute`, and `transfer` over `native-api` are all `CHR-passed`). Covered by
121
+ `test/unit/native-api.test.ts`, `test/integration/native-api.test.ts`
122
+ (transport), and `test/integration/native-api-retrieve.test.ts` (command).
123
+ - **mac-telnet** (`src/protocols/mac-telnet.ts` + `mtwei.ts` +
124
+ `mac-telnet-console.ts` + `MacTelnetAdapter` in `adapter.ts`): packet/control
125
+ codec, session state machine, both auth methods (classic MD5 + MTWEI EC-SRP),
126
+ the interactive-console reader, and the UDP datagram transport with route
127
+ discovery. The deep findings — auth (MTWEI-required / MD5-refused /
128
+ `END_AUTH`≠success), console (ANSI terminal-size probe, ~10s prime stall,
129
+ one-time license gate, `:put [:parse]` syntax+semantic gate, silent writes),
130
+ reliability (byte-counter retransmit + empty-ACK keepalive), and the
131
+ real-device UDP-delivery facts (real egress MAC required; broadcast-only reply;
132
+ per-NIC directed-broadcast reach via `discoverMacTelnetRoute`; bind `0.0.0.0`)
133
+ — are documented in those module headers. Two cells are `CHR-passed`:
134
+ - **`execute / mac-telnet`**: `executeEnvelope` (resolver → `MacTelnetAdapter` →
135
+ UDP transport) runs reads, REST-verified writes, and the validation-reject
136
+ path. Green via `test/integration/mac-telnet-console.test.ts` (examples
137
+ 19–21); transport/auth alone via `test/integration/mac-telnet.test.ts` (MTWEI
138
+ login + MD5 refusal). Unit: `test/unit/{mtwei,mac-telnet,mac-telnet-console}.test.ts`.
139
+ - **`terminal / mac-telnet`**: the interactive relay over the same console
140
+ reader (`MacTelnetConsole.attachInteractive`), orchestrated in
141
+ `src/terminal.ts` over an injectable `TerminalIo`, reusing the execute
142
+ resolver and the shared `resolveMacTelnetRoute`. Green via
143
+ `test/integration/terminal-mac-telnet.test.ts` (T1–T3) driving the real
144
+ `centrs terminal` binary through the subprocess harness.
145
+ - **Evidence caveat:** the CHR integration runs over the quickchr
146
+ `socket-connect` **L2 bridge** (`test/integration/mactelnet-l2-bridge.ts`), so
147
+ the real `createUdpMacTelnetTransport` egress/broadcast path is *not* exercised
148
+ in CI — the RB1100AHx4 / RouterOS 7.24beta1 real-device run grounded the
149
+ UDP-delivery facts (and caught the synthetic-source-MAC break). A full-TTY
150
+ `terminal` test (real PTY via `script(1)`/node-pty) is likewise deferred.
151
+
152
+ ### Frontend surfaces (orthogonal to the command grid)
153
+
154
+ The grid tracks the core (commands × protocols). Frontends are adapters over that
155
+ core and carry their own state, tracked here:
156
+
157
+ | Surface | State | Spec | Notes |
158
+ | ------- | ----- | ---- | ----- |
159
+ | api (TS) | `CHR-passed` | `src/index.ts` | Root surface; everything else adapts it. |
160
+ | cli | `coded` | `src/cli/` | `retrieve`/`execute`/`transfer`/`terminal`/`devices`/`discover`/`btest` wired. |
161
+ | mcp | `CHR-passed` | `commands/mcp/` | Scoped-verb stdio MCP server; CDB-as-allowlist safety model. Phase 1 (explain/validate/retrieve/execute, resources) plus first Phase 2 CDB mutation (`centrs_devices add`) green on CHR 7.23 via `test/integration/mcp.test.ts`, including gated write execution. |
162
+ | tui | `not-started` | `src/tui.ts` | Stub. |
163
+ | proxy | `not-started` | `src/webproxy.ts` | Stub. |
164
+
165
+ A surface advances to `CHR-passed` only when every example in its spec
166
+ (`commands/<surface>/examples.md`, where one exists) is green via
167
+ `bun run test:integration`. The `mcp` surface's tool list, CDB-as-allowlist safety
168
+ model, and CHR test shape (examples 1–10, the `mcp=rw` + `confirm:true` write gate)
169
+ are documented in `commands/mcp/README.md` and `commands/mcp/examples.md`.
170
+
171
+ ### Peer measurement (`btest`) — orthogonal to the command grid
172
+
173
+ `btest` is the MikroTik **bandwidth test** (peer measurement, TCP/UDP port 2000)
174
+ — *not* a RouterOS-command transport, so it sits outside the command×protocol
175
+ grid as its own capability axis (`measure` in `src/protocols/index.ts`), exempt
176
+ from the `:parse` / `/console/inspect` gate. v1 scope (decided with the user):
177
+ **both** client and server, **EC-SRP5 + unauthenticated**, **TCP and UDP** (legacy
178
+ pre-6.43 double-MD5 is out of scope).
179
+
180
+ | Mode | State | Evidence |
181
+ | -------------- | ------------ | -------- |
182
+ | btest / server | `CHR-passed` | CHR `/tool/bandwidth-test` client → centrs server (TCP+UDP, unauth + EC-SRP5), CHR 7.23.1, `test/integration/btest.test.ts`. |
183
+ | btest / client | `CHR-passed` | centrs client → CHR `/tool/bandwidth-server` over a host→guest `tcp:2000` forward — unauth + **EC-SRP5 client proof verified by real RouterOS** + wrong-pass reject (TCP receive), CHR 7.23.1, `test/integration/btest-client.test.ts`; plus client↔server loopback (`test/unit/btest-*.test.ts`). |
184
+
185
+ The wire codec, the EC-SRP5 session/handshake (shared core byte-identical with
186
+ mac-telnet's MTWEI, plus the net-new server role), the TCP/UDP data engines, the
187
+ orchestrator + CLI, and the honest grounding caveat (the server gets direct CHR
188
+ evidence over the QEMU SLIRP gateway `10.0.2.2`; the client gets direct CHR
189
+ evidence over a host→guest `tcp:2000` forward — TCP only; UDP-receive is a soft
190
+ smoke test and UDP client→CHR-server stays loopback/transitive over the SLIRP
191
+ reverse path; TCP `connection-count > 1` fan-out is deferred; NDJSON is not
192
+ adopted) are documented in `commands/btest/README.md` and the
193
+ `src/protocols/btest.ts`, `src/protocols/btest-session.ts`, and
194
+ `src/protocols/ec-srp5.ts` module headers.
195
+
196
+ ## Priority order
197
+
198
+ Do not start a later item until the earlier cell or dependency checkpoint has
199
+ matching evidence. The grid above is the live status; per-item detail lives in
200
+ the linked `commands/<name>/`.
201
+
202
+ 1. **retrieve / rest-api** — `CHR-passed`. The shakedown for everything else (`commands/retrieve/`).
203
+ 2. **CDB resolution + metadata overrides** — `CHR-passed` (`commands/devices/`).
204
+ 3. **CDB groups + fanout** — `CHR-passed` (`commands/devices/`, Fanout).
205
+ 4. **execute / native-api + rest-api** — `CHR-passed` (`commands/execute/`). The single write path; there is no `update`.
206
+ 5. **devices** (CDB mutation + provenance) — `CHR-passed` (`commands/devices/`).
207
+ 6. **retrieve / snmp** — `not-started` (future: SNMP OID/MIB reads + MikroTik MIB cache).
208
+ 7. **ssh** for transfer/execute/terminal — **all three `CHR-passed`** (landed transfer-first; the `transfer`/`execute`/`terminal` READMEs + `src/protocols/ssh.ts` and `src/protocols/sftp.ts`).
209
+ 8. **mac-telnet** for execute/terminal — both `CHR-passed` (`src/protocols/mac-telnet.ts` + `src/protocols/mac-telnet-console.ts`). Optional polish: the ~10s prime-latency fix and a full-TTY terminal test under a real PTY.
210
+ 9. **RoMON / WinBox Terminal for execute** — `not-started` (see Open questions).
211
+ 10. **discover / mndp** — `CHR-passed` (`commands/discover/`).
212
+ 11. **MCP, TUI, proxy** — frontends over the stable core. **MCP** is `CHR-passed` for Phase 1 + the first Phase 2 CDB mutation (`commands/mcp/`); next small step is the device-free `centrs mcp --list-tools` manifest dump (bench token-footprint measurement). MCP stays **stdio-only** (HTTP is the proxy's job). **TUI / proxy** `not-started`.
213
+ 12. **btest (peer measurement)** — server **and** client both `CHR-passed` (`commands/btest/`). Own protocol axis; shares EC-SRP5 with mac-telnet, so it can proceed in parallel.
214
+
215
+ ## Open questions (decisions needed before the affected cell can advance)
216
+
217
+ | Question | Affects cell | Notes |
218
+ | -------- | ------------ | ----- |
219
+ | RoMON / WinBox Terminal validation and CI | execute / romon, execute / winbox-terminal | Lower priority than mac-telnet; need reference tooling and typed failure mapping before advancing. |
220
+
221
+ Recently closed (folded into the specs): SNMP MIB-cache policy → `commands/retrieve/README.md`; bug-report rendering (inline `--bug-report` flag, no separate verb) → `docs/CONSTITUTION.md`; L2-in-CI for mac-telnet/MNDP (quickchr `socket-connect` host-side L2 capture; `socket-mcast` is macOS-broken) → `commands/discover/README.md` + `commands/execute/README.md`.
222
+
223
+ When a question is answered, fold the answer into the relevant
224
+ `commands/<name>/README.md` or `docs/CONSTITUTION.md`, then delete the row.