@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
package/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # centrs
2
+
3
+ `centrs` is the tikoci RouterOS interaction hub: a Bun/TypeScript library and
4
+ CLI (with MCP, TUI, and HTTP proxy frontends planned) for talking to MikroTik
5
+ RouterOS devices through a regularized, validated interface.
6
+
7
+ It is a **friendly conduit**, not a high-level configuration abstraction. It
8
+ helps humans and agents reach RouterOS over the right protocol, with the right
9
+ credentials and ports, and validates RouterOS-shaped commands before running
10
+ them. It does **not** hide RouterOS behind helpers like `createVlanOnBridge()`
11
+ — you still speak RouterOS. Validation and structured diagnostics are the
12
+ product; without them this would just be a worse `curl`.
13
+
14
+ > **Status:** early and in active development. The library, device registry, and
15
+ > first MCP server phases are CHR-verified; the CLI is wired (`coded`); encrypted-CDB
16
+ > writes are still blocked, and the TUI/proxy frontends are planned.
17
+ > `docs/MATRIX.md` is the single source of truth for what works today — treat
18
+ > anything not green there as not-yet-shipped.
19
+
20
+ ## What it talks to, and how
21
+
22
+ - **Frontends:** TypeScript API, CLI, MCP server (Phase 1 + first Phase 2 CDB
23
+ mutation `CHR-passed` — see `commands/mcp/`), TUI and HTTP proxy/daemon
24
+ (future).
25
+ - **Protocols:** REST API (preferred), native API, and SNMP for reads;
26
+ SSH / MAC-Telnet / RoMON / WinBox Terminal for interactive and L2 execute;
27
+ MNDP for discovery. centrs picks the protocol for you (and lets you pin one
28
+ with `via=` or `--via`).
29
+ - **Commands:** every command resolves a *target* — a router you name on the
30
+ CLI — to a real address, credentials, and a protocol, then runs.
31
+
32
+ | Command | Purpose |
33
+ | ---------- | ------- |
34
+ | `retrieve` | Read RouterOS state over REST/native API and SNMP OID/MIB values. |
35
+ | `stream` | Follow live RouterOS output (`print follow`/monitor/sniffer) as an NDJSON stream of envelopes. |
36
+ | `execute` | Run RouterOS CLI-shaped read/write commands (add/set/remove) over native API/REST/L2 surfaces. |
37
+ | `terminal` | Open an interactive console, primarily SSH or MAC-Telnet. |
38
+ | `check` | Probe reachability and management protocol availability. |
39
+ | `devices` | View and maintain the CDB-backed device registry (the only writer). |
40
+ | `discover` | Discover MNDP neighbors and optionally save them into the CDB. |
41
+ | `settings` | Manage centrs's own settings (`centrs.env`, `__default__`) and run first-time setup. |
42
+
43
+ Each command's `commands/<name>/` directory carries its design and the
44
+ executable example list that gates "done".
45
+
46
+ ## Your WinBox address book *is* the device registry
47
+
48
+ centrs deliberately has **no config store, credential vault, or inventory
49
+ database of its own**. Instead it piggybacks on the one most RouterOS admins
50
+ already keep: the WinBox connection database (CDB), the `winbox.cdb` file that
51
+ WinBox writes when you save a router in its address book.
52
+
53
+ centrs reads (and, through `devices`, writes) that same file at its well-known
54
+ location, `~/.config/tikoci/winbox.cdb` (override with `--cdb-file` /
55
+ `CENTRS_CDB_FILE`). From each saved entry it takes the fields WinBox already
56
+ stores:
57
+
58
+ - **`target`** — the address you saved (IPv4, IPv6, MAC, or DNS name); a literal
59
+ you can type at the CLI to reach the box. centrs *also* resolves a `<router>`
60
+ against three sanctioned comment lookup keys — `identity=`, `mac=`, and `ip=` —
61
+ so a router whose `target` is `192.0.2.5` is still reachable as `edge1` when its
62
+ comment carries `identity=edge1`. Every other comment token stays inert prose.
63
+ See `commands/devices/README.md` (Identity model) for the full resolution rules.
64
+ - **`user` / `password`** — the credentials centrs connects with. They stay in
65
+ the CDB and are redacted from MCP results/resources.
66
+ - **`group`, `profile`, `session`, port** — used as-is.
67
+
68
+ So `centrs retrieve edge1 /ip/address` works with no flags: `edge1` is looked
69
+ up in the CDB, its credentials and address come along for the ride. A target
70
+ that *isn't* in the CDB still works, but then `--username` / `--password` (or
71
+ `CENTRS_USERNAME` / `CENTRS_PASSWORD`) become mandatory.
72
+
73
+ ### Overloading the comment: per-device settings as `key=value`
74
+
75
+ A WinBox CDB entry has no field for "always use SSH for this box" or "skip
76
+ validation here" — those are centrs concepts WinBox knows nothing about. Rather
77
+ than invent a parallel store, centrs **overlays its own settings onto the one
78
+ free-text field WinBox does have: the comment.** It parses `key=value` tokens
79
+ (the "comment kv-soup") out of that free text and treats the recognized ones as
80
+ per-device defaults. Everything else in the comment stays inert human prose.
81
+
82
+ A comment like:
83
+
84
+ ```text
85
+ edge1 prod border router via=ssh validate=false mcp=rw
86
+ ```
87
+
88
+ leaves `edge1 prod border router` as plain text for you and WinBox, while
89
+ centrs reads three settings from it. The recognized keys:
90
+
91
+ | Key | Effect |
92
+ | ---------- | ----------------------------------------------------------------------- |
93
+ | `via` | Default transport for this device (`rest-api`, `native-api`, `ssh`, …). |
94
+ | `validate` | `true` / `false`. `false` is the escape hatch; a CLI flag still wins. |
95
+ | `timeout` | Default request timeout in ms (per-transport caps still apply). |
96
+ | `port` | Non-default transport port. |
97
+ | `ssh-key` | Path to a per-device SSH private key (a path only — never key material).|
98
+ | `source` | Provenance marker for discovered/imported records (`mndp`, `dude`, …). |
99
+ | `mcp` | MCP write policy: `ro` (default) or `rw`. See below. |
100
+
101
+ The first-class fields WinBox already stores (`user`, `password`, `group`,
102
+ `profile`, `session`) must **not** be written into the comment — they have real
103
+ CDB tags, and `devices set` refuses to put them there. Unknown keys produce a
104
+ `cdb/unknown-option` warning and are ignored, so a stray `=` in human prose
105
+ never breaks anything.
106
+
107
+ Settings resolve lowest-to-highest: **built-in defaults → project config →
108
+ comment-kv → `CENTRS_*` env → CLI flag / API arg.** Every result envelope
109
+ reports which source won each setting under `meta.settings`, so you can always
110
+ see *why* a given port or transport was chosen.
111
+
112
+ ### Groups, discovery, and the MCP allowlist all ride on the CDB
113
+
114
+ - **Groups** are CDB-native: an entry's `group` field is just a string, and
115
+ `--group prod-edge` fans a single command out to every entry that shares it.
116
+ There is no separate group-definition file.
117
+ - **Discovery** writes back into the CDB: `discover --save` records MNDP
118
+ neighbors as new entries tagged `group=discovered` with a `source=mndp`
119
+ marker — hints to curate, not authoritative inventory.
120
+ - **The MCP server uses the CDB as its authorization boundary.** Agents can
121
+ only reach targets that exist in the CDB; RouterOS write-shaped execution
122
+ requires the resolved entry to carry `mcp=rw` plus per-call confirmation.
123
+ `centrs_devices` can also add/set/remove CDB records in-band, and
124
+ `centrs_discover` can save MNDP neighbors, with `confirm: true`. The allowlist
125
+ and the write policy are *the same CDB data* you already manage — see
126
+ `commands/mcp/`.
127
+
128
+ `devices` is the only command that writes the CDB, and it does so safely
129
+ (timestamped backup, atomic rename, unknown WinBox fields preserved verbatim).
130
+ Encrypted-CDB **reads** work with `--cdb-password` / `CENTRS_CDB_PASSWORD`;
131
+ encrypted **writes** are currently blocked pending a verified WinBox round-trip.
132
+ The full contract — identity resolution, ambiguity handling, write strategy,
133
+ and the comment grammar — lives in `commands/devices/README.md` and
134
+ `docs/CONSTITUTION.md` (Identity and CDB).
135
+
136
+ ## The result envelope, validation, and errors
137
+
138
+ - **Validation is on by default** (`validate=true`). Reads are checked through
139
+ `/console/inspect`; CLI-shaped `execute` commands through `[:parse]`. Set
140
+ `validate=false` (per device or per call) only as a deliberate escape hatch.
141
+ - **Every call returns one envelope:** `{ ok, data?, warnings?, error?, meta }`,
142
+ with provenance and source-of-truth reporting in `meta`.
143
+ - **Errors are typed values**, not thrown strings, with slash-namespaced codes
144
+ (`routeros/…`, `transport/…`, `validation/…`, `cdb/…`) and a `details_url`
145
+ of the form `https://tikoci.github.io/centrs/errors/<code>` for the
146
+ human-readable explanation.
147
+
148
+ The load-bearing contract — envelope shape, error model, protocol selection,
149
+ settings precedence, and the "done" definition — lives in
150
+ `docs/CONSTITUTION.md`. The command×protocol status grid is `docs/MATRIX.md`.
151
+
152
+ ## Repository layout
153
+
154
+ For contributors. An admin only needs the CLI and their CDB; this is where the
155
+ code and authority docs live.
156
+
157
+ - **`docs/CONSTITUTION.md`** — load-bearing rules (validation, envelope, error
158
+ model, settings precedence, identity/CDB, protocol selection, done).
159
+ - **`docs/MATRIX.md`** — command×protocol grid; the only status surface.
160
+ - **`commands/<name>/`** — per-command README and executable examples.
161
+ - **`src/`** — Bun/TypeScript source.
162
+ - **`test/`** — unit and CHR-backed integration tests.
163
+
164
+ There is no `docs/specs/`, no `work/`, no roadmap doc. The matrix is the
165
+ roadmap.
166
+
167
+ ## Development
168
+
169
+ Requirements:
170
+
171
+ - Bun 1.3.11 or newer (CI pinned to 1.3.13; Bun 2.x not yet validated).
172
+ - Git.
173
+ - For integration tests: QEMU plus `@tikoci/quickchr` (handles CHR image
174
+ download and boot).
175
+
176
+ ```bash
177
+ bun install
178
+ bun run lint
179
+ bun run test
180
+ bun run test:integration # CHR-backed; required before claiming "done" on transport code
181
+ bun run test:integration:long-term # channel-specific CHR gate
182
+ bun run build
183
+ ```
184
+
185
+ Generated API docs through TypeDoc:
186
+
187
+ ```bash
188
+ bun run build:doc:api
189
+ ```
190
+
191
+ ## Done definition (short version)
192
+
193
+ A feature is done when every line in `commands/<name>/examples.md` is green on
194
+ real CHR via `bun run test:integration`. Code existing is not done. Unit tests
195
+ passing is not done. Full rule:
196
+ `.github/instructions/done-definition.instructions.md`.
package/SECURITY.md ADDED
@@ -0,0 +1,30 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ Report privately via [GitHub Security Advisories](https://github.com/tikoci/centrs/security/advisories/new). Do **not** open a public issue for an undisclosed vulnerability.
6
+
7
+ Please include the affected files or workflow, reproduction details, and impact. Initial response within a few business days; fixes land on `main` and flow into the next published artifact.
8
+
9
+ ## Scope
10
+
11
+ `centrs` will mediate RouterOS device access, credentials, local discovery data, and optional HTTP/MCP proxy surfaces. Security-sensitive areas include credential import/cache, RouterOS write execution, local network discovery, file transfer, and any daemon that binds a network port.
12
+
13
+ ## Code scanning
14
+
15
+ The repository's [Security tab](https://github.com/tikoci/centrs/security) is the live source of current alerts and advisories. This section describes *what* checks run and *why*, so the doc stays meaningful even when the badge is at 0.
16
+
17
+ - **CodeQL** — repo-managed workflow at [`.github/workflows/codeql.yaml`](.github/workflows/codeql.yaml) with config [`.github/codeql-config.yml`](.github/codeql-config.yml). Query suite: `security-and-quality` (security-extended + code-quality). Languages: `javascript-typescript`, `actions`. Schedule: push to `main`, pull requests to `main`, weekly cron, and manual dispatch.
18
+ - **Code Quality (AI findings, preview)** — intended to be enabled in GitHub. [`.github/workflows/codeql.yaml`](.github/workflows/codeql.yaml) includes a non-failing forward-compat probe for a future AI-findings API. AI findings are noisy and self-contradicting; we accept the noise because the second-opinion catches real issues that the static suite misses. Steady-state goal is 0 open findings. False positives are dismissed via the GitHub UI with a written justification — that text is the audit-log contract.
19
+ - **Dependency review** — not enabled. GitHub dependency review follows dependency-graph ecosystem support, and this Bun-only repository currently does not have a supported lockfile for that gate.
20
+ - **Dependabot security updates** — enabled and configured in [`.github/dependabot.yaml`](.github/dependabot.yaml).
21
+ - **Secret scanning** — enabled in GitHub, with push protection.
22
+ - **Private vulnerability reporting** — enabled via GitHub Security Advisories.
23
+
24
+ Some GitHub Security tab data, especially Code Quality AI findings, is not currently available through stable unauthenticated APIs. Human review in the GitHub UI remains part of the security workflow.
25
+
26
+ ## Supported versions
27
+
28
+ | Version | Supported |
29
+ | --- | --- |
30
+ | `main` | yes |
@@ -0,0 +1,50 @@
1
+ # commands/
2
+
3
+ Each `commands/<name>/` holds the **executable spec** for one command:
4
+
5
+ - `README.md` — intent, flags, behavior. The "designed" tier.
6
+ - `examples.md` — numbered, runnable examples. Each example is one assertion in
7
+ the matching `test/integration/<name>.test.ts`; example N ↔ assertion N. This
8
+ is what `CHR-passed` is measured against.
9
+
10
+ Do not restate the constitution here or in a command README — link to
11
+ `docs/CONSTITUTION.md` for envelope, errors, settings precedence, target
12
+ selection, and protocol selection. The identity/CDB **resolution depth** (lookup
13
+ keys, ambiguity, `__default__`, comment-kv allowlist, write strategy) lives in
14
+ `commands/devices/README.md` — the registry command owns it; the constitution
15
+ keeps only the identity/CDB principle. A command file covers only what is
16
+ specific to that command. `docs/MATRIX.md` is the only status surface; a
17
+ README's "Status" line must agree with it.
18
+
19
+ ## Verb vocabulary
20
+
21
+ Commands are centrs **verbs** (`retrieve`, `stream`, `execute`, `devices`,
22
+ `discover`, `check`, `terminal`, `settings`), never one tool per RouterOS command. Sub-verbs and their
23
+ aliases are canonical across CLI / API / MCP:
24
+
25
+ | Canonical | Aliases | Meaning |
26
+ | --------- | ---------------- | ---------------------------------------- |
27
+ | `list` | `print` | Read many records. (`print` is RouterOS muscle-memory.) |
28
+ | `show` | `get` | Read one resolved record. |
29
+ | `add` | — | Create; refuses an existing target. |
30
+ | `set` | — | Modify an existing record (same flags as `add`). |
31
+ | `remove` | `rm`, `delete` | Delete a record. |
32
+ | `edit` | — | **Reserved** for the future clack/TUI wizard. Not a field-editing verb. |
33
+ | `groups` | — | List distinct groups. |
34
+
35
+ Canonical names win in help text and docs; aliases resolve to the canonical verb
36
+ silently. `add` and `set` are symmetric (same flags + `k=v` tokens), differing
37
+ only on existence. There is no `update` verb — RouterOS add/set/remove ride
38
+ `execute` (constitution: protocol selection).
39
+
40
+ ## Help system
41
+
42
+ Shared across CLI / API / MCP help rendering:
43
+
44
+ - **Level-aware help.** `<command> <subcommand> --help` is more specific than
45
+ `<command> --help` — e.g. `centrs devices remove --help` shows the `remove`
46
+ usage and flags, not the whole `devices` surface.
47
+ - **"Did you mean?"** On an unknown verb/sub-verb or flag, the error
48
+ (`input/invalid-command` / usage error) lists the closest canonical matches
49
+ (Levenshtein) plus accepted aliases — extending the invalid-command behavior
50
+ that already enumerates canonical verbs + aliases.
@@ -0,0 +1,257 @@
1
+ # btest
2
+
3
+ Run MikroTik's **bandwidth test** as a cross-platform client or server. `btest`
4
+ is centrs's peer-measurement surface: it mirrors `/tool/bandwidth-test` (client)
5
+ and `/tool/bandwidth-server` (server), so it is a drop-in for the Windows-only
6
+ `btest.exe` on macOS/Linux.
7
+
8
+ Status: server **and** client both `CHR-passed`. The protocol **codec**
9
+ (`src/protocols/btest.ts`), the shared **EC-SRP5** core including the net-new
10
+ server role (`src/protocols/ec-srp5.ts`), the **session state machine + TCP/UDP
11
+ data engines** (`src/protocols/btest-session.ts`), the **orchestrator**
12
+ (`src/btest.ts`), and the **CLI** (`centrs btest client|server`) all exist and are
13
+ unit/loopback-tested. Two gated CHR integration tests **pass on CHR 7.23.1**:
14
+ `test/integration/btest.test.ts` lands a real RouterOS `/tool/bandwidth-test`
15
+ client's TCP-receive, UDP-transmit (with loss accounting), and **EC-SRP5** sessions
16
+ on the centrs **server**; `test/integration/btest-client.test.ts` runs the centrs
17
+ **client** against a real RouterOS `/tool/bandwidth-server` over a host→guest
18
+ `tcp:2000` forward — unauthenticated TCP receive, an **EC-SRP5 client proof
19
+ verified by RouterOS's own verifier**, and a wrong-password reject. TCP
20
+ multi-connection (`connection-count > 1`) data fan-out is a follow-up. See
21
+ `docs/MATRIX.md` ("Peer measurement (`btest`)") for the cell states — that is the
22
+ only status surface. v1 scope: **both** modes (client + server), **EC-SRP5 +
23
+ unauthenticated** auth, **TCP and UDP** tests. Legacy pre-6.43 double-MD5 is out of
24
+ scope.
25
+
26
+ ## Why this is not `execute`
27
+
28
+ btest is **not** a RouterOS-shaped command — it issues no CLI string and there is
29
+ no `:parse` / `/console/inspect` gate. It is a **peer protocol** (TCP/UDP port
30
+ 2000) that centrs speaks directly to a RouterOS bandwidth server/client (or to
31
+ another `btest.exe`/centrs). `docs/CONSTITUTION.md` adds it as a deliberate
32
+ capability axis (*measure*, see Protocol selection): the envelope, identity
33
+ resolution, and friendly-error contract still hold, but the RouterOS-command
34
+ validation gate does not apply. btest is **explicit-only** — never auto-selected,
35
+ never a downgrade target, and no other command rides it.
36
+
37
+ ## Synopsis
38
+
39
+ ```text
40
+ centrs btest client <router> [--protocol udp|tcp] [--direction receive|transmit|both] \
41
+ [--duration 15s] [--interval 1s] [--connection-count N] \
42
+ [--local-udp-tx-size N] [--remote-udp-tx-size N] \
43
+ [--local-tx-speed bps] [--remote-tx-speed bps] [--random-data] \
44
+ [--user U] [--password P] [--port 2000] [--csv | --format text|csv|json|yaml]
45
+
46
+ centrs btest server [--authenticate[=false]] [--user U] [--password P] \
47
+ [--bind 127.0.0.1] [--port 2000] \
48
+ [--allocate-udp-ports-from 2001] [--max-sessions 100] \
49
+ [--duration 1h] [--csv | --format text|csv|json|yaml]
50
+ ```
51
+
52
+ ## Intent
53
+
54
+ - **Speak RouterOS, not an abstraction.** Flag names mirror
55
+ `/tool/bandwidth-test` and `/tool/bandwidth-server` verbatim (`direction`,
56
+ `protocol`, `local-udp-tx-size`, `connection-count`, `authenticate`,
57
+ `allocate-udp-ports-from`, …) so RouterOS muscle memory transfers. centrs adds
58
+ only `--bind` (a server safety control) and the shared `--format`.
59
+ - **Client** connects to a RouterOS bandwidth server (or a `btest.exe`/centrs
60
+ server), authenticates if required, runs the test in the requested direction,
61
+ and streams live throughput reports plus a final summary.
62
+ - **Server** listens on TCP/UDP port 2000, answers the handshake, performs the
63
+ EC-SRP5 server role when `authenticate=yes`, runs the requested test, and
64
+ streams session events. It runs in the foreground until `--duration` elapses or
65
+ Ctrl-C.
66
+ - **Identity resolution (client).** `<router>` resolves through the CDB exactly
67
+ like every command (constitution: identity and CDB) — `target` and the
68
+ `identity=`/`mac=`/`ip=` lookup keys yield the host, and `user`/`password` come
69
+ from the record (or `__default__`, env, CLI). A target not in the CDB still
70
+ works when `--user`/`--password` (or `CENTRS_USERNAME`/`CENTRS_PASSWORD`) are
71
+ given. The server takes no target.
72
+ - **Validation is option-grammar validation** (constitution: validation is the
73
+ product). centrs rejects incoherent option combinations *before* opening a
74
+ socket — e.g. `connection-count` with `protocol=udp`, `*-udp-tx-size` with
75
+ `protocol=tcp`, an out-of-range size (28..64000) or direction — with a
76
+ `validation/*` / `usage/*` error. There is no `:parse` step.
77
+
78
+ ## Flags
79
+
80
+ ### `btest client <router>`
81
+
82
+ | Flag | RouterOS analog / default | Notes |
83
+ | ---- | ------------------------- | ----- |
84
+ | `--protocol <udp\|tcp>` | `protocol`, default `udp` | UDP measures raw throughput + loss; TCP measures goodput. |
85
+ | `--direction <receive\|transmit\|both>` | `direction`, default `receive` | `receive` = download (server→client), `transmit` = upload. |
86
+ | `--duration <dur>` | `duration` | Bounds the run; reuses `parseDuration`. Omit for open-ended (Ctrl-C). |
87
+ | `--interval <dur>` | `interval`, default `1s` | Report cadence (`20ms`..`5s`). One report frame per interval. |
88
+ | `--connection-count <n>` | `connection-count` (1..255) | **TCP only.** Parallel TCP data connections. |
89
+ | `--local-udp-tx-size <n>` | `local-udp-tx-size` (28..64000) | **UDP only.** Client→server packet size. |
90
+ | `--remote-udp-tx-size <n>` | `remote-udp-tx-size` (28..64000) | **UDP only.** Server→client packet size. |
91
+ | `--local-tx-speed <bps>` | `local-tx-speed` | Cap on client→server rate (bits/sec). |
92
+ | `--remote-tx-speed <bps>` | `remote-tx-speed` | Cap on server→client rate (bits/sec). |
93
+ | `--random-data` | `random-data`, default off | Incompressible payload (defeats link compression; CPU-heavier). |
94
+ | `--user` / `--password` | `user` / `password` | Fall back to CDB / `CENTRS_*`. Required when the server has `authenticate=yes`. |
95
+ | `--port <n>` | — (default `2000`) | Control port override. |
96
+ | `--format <text\|csv\|json\|yaml>` | — (default `text`) | `text` = live human reports; `csv` = streaming CSV records; `json`/`yaml` = final summary envelope. |
97
+ | `--csv` | — | Shortcut for `--format csv`. |
98
+
99
+ ### `btest server`
100
+
101
+ | Flag | RouterOS analog / default | Notes |
102
+ | ---- | ------------------------- | ----- |
103
+ | `--authenticate[=false]` | `authenticate`, **default `true`** | Require EC-SRP5 auth; `--authenticate=false` accepts anonymous clients. |
104
+ | `--user` / `--password` | — | The accepted credential when `authenticate=true` (v1: one user). Falls back to `CENTRS_*`. |
105
+ | `--bind <addr>` | **centrs-only** | Listen address; **default `127.0.0.1`**. Use `0.0.0.0` to expose on the LAN. |
106
+ | `--port <n>` | — (default `2000`) | Control-connection port. |
107
+ | `--allocate-udp-ports-from <n>` | `allocate-udp-ports-from` | Base of the UDP data-port range; default `2001` (just above the TCP/2000 control port). |
108
+ | `--max-sessions <n>` | `max-sessions` (1..1000), default `100` | Concurrent test cap; further clients get `routeros/btest-too-many-sessions`. |
109
+ | `--duration <dur>` | — | Optional auto-stop; otherwise runs until Ctrl-C. |
110
+ | `--format <text\|csv\|json\|yaml>` | — (default `text`) | `text` = human session log; `csv` = streaming CSV records; `json`/`yaml` = final summary envelope. |
111
+ | `--csv` | — | Shortcut for `--format csv`. |
112
+
113
+ ## How it works
114
+
115
+ The control channel is TCP on port 2000. The server sends a hello; the client
116
+ replies with a 16-byte command packet (protocol type, direction, data type, TCP
117
+ connection count, UDP tx size, buffer size, remote/local speed limits). For a UDP
118
+ test, data then flows on the server's allocated UDP ports
119
+ (`allocate-udp-ports-from`+) and the client's `+256`-offset ports; loss is derived
120
+ from sequence gaps. For a TCP test, the bulk data rides `connection-count` TCP
121
+ connections and goodput is measured on the data stream (TCP/IP headers excluded,
122
+ matching RouterOS accounting). For multi-connection TCP the server's OK response
123
+ carries a 2-byte **session token**; the extra connections reconnect and present
124
+ that token to join the same test (no re-auth). Throughput and CPU are exchanged
125
+ once per `interval` as a 12-byte **status message** on the TCP channel
126
+ (`[0x07][0x80|cpu][00 00][seq u32 LE][bytesReceived u32 LE]`).
127
+
128
+ ### Authentication (EC-SRP5)
129
+
130
+ RouterOS ≥ 6.43 (so 7.x, including the 7.23 target) authenticates btest with
131
+ **EC-SRP5** on the Curve25519-in-short-Weierstrass curve — the same curve centrs
132
+ already ships for mac-telnet. The password never crosses the wire. The curve math
133
+ is shared from `src/protocols/ec-srp5.ts` (extracted from `mtwei.ts`); btest adds
134
+ its own control-stream framing on top. The **server** role (generate a keypair,
135
+ accept the client offer, derive the shared secret, and *verify* the client proof)
136
+ is net-new — `mtwei.ts` only implements the client. A bad proof (or a client that
137
+ will not offer EC-SRP5 against an `authenticate=yes` server) maps to
138
+ `transport/auth-failed`. Legacy pre-6.43 double-MD5 is out of scope for v1.
139
+
140
+ ## Output
141
+
142
+ btest runs over time, so it emits **live records** during the run plus a final
143
+ **summary**. v1 keeps the formats simple and **defers a broader JSON-streaming
144
+ decision** — it does *not* adopt the `stream` command's NDJSON-stream-of-envelopes
145
+ contract yet (see Open questions):
146
+
147
+ - **`text`** (default) — human-readable live reports during the run
148
+ (RouterOS / `btest.exe`-style `tx`/`rx` current/average/total; the server
149
+ prints a session log), then a final summary line.
150
+ - **`--csv`** (shortcut for `--format csv`) — **the streaming format**: one CSV
151
+ record per `--interval` report (client) or per session event (server), after a
152
+ header row. Pipe-friendly for logging/plotting.
153
+ - **`--format json` / `yaml`** — a **single summary envelope** emitted at
154
+ completion (`--duration` elapsed or Ctrl-C): the standard
155
+ `{ ok, data, warnings, tips, error, meta }` with `meta.via = "btest"`. `data`
156
+ aggregates the whole run — totals plus `data.reports[]` (client interval
157
+ samples) / `data.sessions[]` (server) — so it is **lossless** against what
158
+ `text` and `csv` streamed. It is not itself streamed (one envelope, at the end).
159
+
160
+ CSV columns — client:
161
+ `seq,direction,protocol,tx_bps,rx_bps,lost_packets,tx_bytes,rx_bytes` (one row per
162
+ interval; `seq` is the status sequence number, `tx_bytes`/`rx_bytes` the per-interval
163
+ byte counts); server:
164
+ `duration_ms,event,client,protocol,direction,user,tx_bps,rx_bps,lost_packets` (one row
165
+ per session; `tx_bps`/`rx_bps` are session averages, `lost_packets` is the UDP loss
166
+ total — 0 for TCP). A mid-run error ends
167
+ the stream and is carried in the summary envelope's `error` (`json`) or a final
168
+ `error` row (`csv`); the process exit code reflects whether the test/server
169
+ *started* cleanly.
170
+
171
+ ## Server is a network listener
172
+
173
+ `centrs btest server` opens a TCP/UDP listener on port 2000 — a security-sensitive
174
+ surface (`docs/CONSTITUTION.md`, Network listeners). It is an **explicit,
175
+ foreground, user-invoked** command, **not** a daemon and **not** the proxy. It
176
+ **binds `127.0.0.1` by default** (pass `--bind 0.0.0.0` to expose on the LAN) and
177
+ **requires auth by default** (`authenticate=true`, mirroring RouterOS). The
178
+ accepted credential (`--user`/`--password`/`CENTRS_*`) and any client credentials
179
+ are redactable in bug-report output.
180
+
181
+ ## Errors
182
+
183
+ - `validation/option` — an option is incompatible with the chosen
184
+ protocol/direction (e.g. `connection-count` with `protocol=udp`,
185
+ `*-udp-tx-size` with `protocol=tcp`) **or** out of range (UDP size `28..64000`,
186
+ `connection-count 1..255`, `interval 20ms..5s`, `max-sessions 1..1000`). Caught
187
+ before any socket opens; `error.context.option` names the offending flag.
188
+ - `transport/connection-refused` — no bandwidth server answered on port 2000
189
+ (server disabled, firewalled, or wrong host). Fix: enable
190
+ `/tool/bandwidth-server`, check the port/`--bind`.
191
+ - `transport/auth-failed` — EC-SRP5 proof rejected, or the server requires auth
192
+ and the client offered none / wrong credentials.
193
+ - `routeros/btest-too-many-sessions` — (server) a client was refused because
194
+ `max-sessions` is reached.
195
+ - `routeros/btest-protocol` — a malformed/unexpected control packet from the peer.
196
+
197
+ ## Validation policy (CHR + loopback + btest.exe)
198
+
199
+ Decided with the user. Both cells have direct gated `CHR-passed` evidence:
200
+
201
+ - **Server cell — CHR client → centrs server** (`test/integration/btest.test.ts`):
202
+ `@tikoci/quickchr` boots a CHR whose `/tool/bandwidth-test` dials the **host**
203
+ (the QEMU SLIRP gateway `10.0.2.2`) where the centrs server listens on
204
+ `--bind 0.0.0.0 --port 2000`. Guest→host needs **no hostfwd**. Proven paths are
205
+ **TCP** (any direction — TCP is bidirectional on the outbound control connection)
206
+ and **UDP transmit** (client→server, which the guest originates, so SLIRP NATs it
207
+ cleanly), with unauth + EC-SRP5 and throughput cross-checked against the CHR's own
208
+ status. **UDP receive / both** depend on the server's packets reaching the guest's
209
+ UDP port *back* through SLIRP NAT (a one-shot smoke cycle only — see Open
210
+ questions).
211
+ - **Client cell — centrs client → CHR `/tool/bandwidth-server`**
212
+ (`test/integration/btest-client.test.ts`): the CHR boots with a host→guest
213
+ `extraPorts` forward `{ name: "btest", host: 0, guest: 2000, proto: "tcp" }`, so a host TCP port
214
+ maps onto the guest bandwidth server on 2000 (the same SLIRP inbound path that
215
+ already carries REST/SSH — no firewall change). The centrs client dials
216
+ `127.0.0.1:<host port>` and proves **TCP receive**, unauth and **EC-SRP5** (its
217
+ client proof verified by RouterOS's real server verifier), plus a wrong-password
218
+ reject. **TCP only** — UDP would need the server's datagrams to traverse SLIRP
219
+ back to the host, which this forward does not provide, so UDP client→server stays
220
+ loopback/transitive.
221
+
222
+ The fast/deterministic backstop under both is **centrs client ↔ centrs server
223
+ loopback** (`127.0.0.1`, injected sockets) covering the full client↔server matrix,
224
+ EC-SRP5 both roles, and the option-grammar rejects.
225
+ **`btest.exe` (via wine) is a coding-time grounding aid only** — used by hand
226
+ while implementing the EC-SRP5 framing and the server-side verifier; it is **not**
227
+ an integration test (it cannot run in CI) and **not** part of the long-term plan.
228
+
229
+ **Honest grounding caveat:** both cells are now directly CHR-passed. The remaining
230
+ transitive-only paths are **UDP client→CHR-server** (the SLIRP reverse-NAT path the
231
+ TCP forward does not cover) and TCP **multi-connection** fan-out — both deferred,
232
+ not yet gated.
233
+
234
+ ## Open questions
235
+
236
+ - **JSON streaming is deliberately deferred.** btest does not adopt the `stream`
237
+ command's NDJSON-stream-of-envelopes contract yet; v1 streams `text` / `csv` and
238
+ returns a single summary envelope for `json` / `yaml`. Revisit a streaming-JSON
239
+ shape — ideally shared with `stream` — once that contract is settled.
240
+ - **UDP receive/both through SLIRP is unproven.** Before the integration suite
241
+ relies on server→guest UDP, a **one-shot CHR smoke test** must confirm the path
242
+ works — i.e. that the RouterOS client originates an initial UDP/NAT probe that
243
+ opens the SLIRP reverse mapping. Until then the gated UDP coverage is
244
+ **transmit** (guest→host); TCP already covers both directions.
245
+ - **UDP client→CHR-server and TCP multi-connection are not yet gated.** The direct
246
+ client→CHR-server gate (`test/integration/btest-client.test.ts`) is **TCP only**;
247
+ a UDP variant needs the server's datagrams to traverse SLIRP back to the host
248
+ (the reverse-NAT path), and `connection-count > 1` needs the parallel-stream
249
+ fan-out. Both remain loopback/transitive until gated. `btest.exe` (wine) is the
250
+ coding-time grounding peer, not CI.
251
+
252
+ ## Out of scope (v1)
253
+
254
+ Legacy pre-6.43 double-MD5 auth; a UDP client→CHR-server gated test (TCP is gated;
255
+ UDP needs SLIRP reverse-NAT); the ≥3-router "test-through" topology RouterOS
256
+ recommends for measuring a *transit* device (centrs is an endpoint peer); TUI/proxy
257
+ btest frontends (later, over the stable core).