@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,306 @@
1
+ # retrieve — examples
2
+
3
+ Each numbered example is an executable spec. The integration test under
4
+ `test/integration/rest-retrieve.test.ts` runs every example against a CHR
5
+ booted by `@tikoci/quickchr`. If a line here is not exercised by a test, the
6
+ test file is wrong; if a line passes only with `validate=false`, the
7
+ **implementation** is wrong (see `docs/CONSTITUTION.md`).
8
+
9
+ `$R` is `<host>:<rest-port>` resolved by quickchr.
10
+ `$U` / `$P` are CHR credentials provided by the test harness.
11
+ Examples that assert envelope fields pass `--json`; plain text is the default
12
+ for human CLI use.
13
+
14
+ ## Singletons (read a single record)
15
+
16
+ ### 1. /system/resource
17
+
18
+ Must succeed with `validate=true` (default).
19
+
20
+ ```bash
21
+ centrs retrieve $R /system/resource --username $U --password $P --json
22
+ ```
23
+
24
+ Envelope: `ok: true`, `data` is an object, `meta.via=rest-api`,
25
+ `meta.validation.source=/console/inspect`.
26
+
27
+ ### 2. /system/identity
28
+
29
+ Same shape, different menu.
30
+
31
+ ```bash
32
+ centrs retrieve $R /system/identity --username $U --password $P --json
33
+ ```
34
+
35
+ ## Lists (read an array of rows)
36
+
37
+ ### 3. /ip/address
38
+
39
+ Empty or populated, both must succeed with `validate=true`.
40
+
41
+ ```bash
42
+ centrs retrieve $R /ip/address --username $U --password $P --json
43
+ ```
44
+
45
+ Envelope: `ok: true`, `data` is an array.
46
+
47
+ ### 4. /interface
48
+
49
+ List of interfaces present on a default CHR.
50
+
51
+ ```bash
52
+ centrs retrieve $R /interface --username $U --password $P --json
53
+ ```
54
+
55
+ ## Attribute projection
56
+
57
+ ### 5. Single --attribute on a singleton
58
+
59
+ ```bash
60
+ centrs retrieve $R /system/resource --attribute uptime --username $U --password $P --json
61
+ ```
62
+
63
+ Envelope: `data` is the bare value, not an object.
64
+
65
+ ### 6. Comma-list --attributes on a list
66
+
67
+ ```bash
68
+ centrs retrieve $R /interface --attributes name,type --username $U --password $P --json
69
+ ```
70
+
71
+ Envelope: `data` is an array of objects each containing only `name` and `type`.
72
+
73
+ ### 7. --all-attributes (RouterOS details=true)
74
+
75
+ ```bash
76
+ centrs retrieve $R /system/resource --all-attributes --username $U --password $P --json
77
+ ```
78
+
79
+ ### 8. Conflict: --attribute + --all-attributes
80
+
81
+ Rejected before any network call, with `usage/conflicting-flags`.
82
+
83
+ ```bash
84
+ centrs retrieve $R /system/resource --attribute uptime --all-attributes --username $U --password $P --json
85
+ ```
86
+
87
+ ## --list-attributes
88
+
89
+ ### 9. List attributes for a path
90
+
91
+ Without running `print`/`get`.
92
+
93
+ ```bash
94
+ centrs retrieve $R /system/resource --list-attributes --username $U --password $P --json
95
+ ```
96
+
97
+ Envelope: `data` is `string[]`. No `meta.timing.request` (no transport call).
98
+
99
+ ## Validation surface
100
+
101
+ ### 10. Unknown path
102
+
103
+ Must fail with `validation/unknown-path` (not `routeros/unsupported-path`
104
+ from a server round-trip; the validator catches it first). The error
105
+ envelope must include suggested alternatives from `/console/inspect` when
106
+ available.
107
+
108
+ ```bash
109
+ centrs retrieve $R /not/a/real/path --username $U --password $P --json
110
+ ```
111
+
112
+ ### 11. Unknown attribute
113
+
114
+ `validation/unknown-attribute` with suggestions.
115
+
116
+ ```bash
117
+ centrs retrieve $R /system/resource --attribute bogus --username $U --password $P --json
118
+ ```
119
+
120
+ ## Transport / error contract
121
+
122
+ ### 12. Bad credentials
123
+
124
+ `transport/auth-failed`, `details_url` populated.
125
+
126
+ ```bash
127
+ centrs retrieve $R /system/resource --username wrong --password wrong --json
128
+ ```
129
+
130
+ ### 13. Unreachable host
131
+
132
+ `transport/connection-refused` (not the more general `transport/network`).
133
+
134
+ ```bash
135
+ centrs retrieve 127.0.0.1:1 /system/resource --username $U --password $P --json
136
+ ```
137
+
138
+ ### 14. REST timeout ceiling
139
+
140
+ `--timeout 70000` against `--via rest-api` is rejected up front with
141
+ `usage/timeout-out-of-range` (REST hard cap is 60s).
142
+
143
+ ```bash
144
+ centrs retrieve $R /system/resource --via rest-api --timeout 70000 --username $U --password $P --json
145
+ ```
146
+
147
+ ## CDB resolution
148
+
149
+ ### 15. Name resolution
150
+
151
+ `<router>` matches a CDB-stored target; user/password are filled from CDB.
152
+
153
+ ```bash
154
+ centrs retrieve $R /system/resource --cdb-file $CDB --json
155
+ ```
156
+
157
+ ### 16. Unused --cdb-password on an unencrypted CDB
158
+
159
+ Call succeeds with a `cdb/password-not-needed` warning in `meta.warnings`.
160
+
161
+ ```bash
162
+ centrs retrieve $R /system/resource --cdb-file $CDB --cdb-password ignored --json
163
+ ```
164
+
165
+ ## Group fanout
166
+
167
+ These are exercised by `test/integration/fanout-retrieve.test.ts` when CHR
168
+ integration is enabled.
169
+
170
+ ### F1. Group fanout with one inner success and one inner failure
171
+
172
+ `$CDB` contains two records in group `fanout-chr`: record 0 is the live CHR and
173
+ record 1 is an unreachable REST URL.
174
+
175
+ ```bash
176
+ centrs retrieve --group fanout-chr /system/resource --cdb-file $CDB --json
177
+ ```
178
+
179
+ Outer envelope: `ok: true`, `data.summary = { total: 2, ok: 1, failed: 1 }`,
180
+ `data.targets` is ordered by CDB `recordIndex`, and `meta.operation.kind` is
181
+ `fanout`. The unreachable target is an inner `ok: false` envelope with
182
+ `transport/connection-refused`.
183
+
184
+ ### F2. Empty / unknown group
185
+
186
+ ```bash
187
+ centrs retrieve --group no-such-group /system/resource --cdb-file $CDB --json
188
+ ```
189
+
190
+ Envelope: `ok: true`, `data.summary = { total: 0, ok: 0, failed: 0 }`,
191
+ `data.targets = []`, and warnings include `cdb/empty-group`.
192
+
193
+ ## Format
194
+
195
+ ### 17. --format yaml
196
+
197
+ `text` is the default; `--json` (or `--format json`) emits the structured
198
+ envelope, and `--format yaml` renders the same envelope. The two structured
199
+ outputs round-trip to the same JS value.
200
+
201
+ ```bash
202
+ centrs retrieve $R /system/resource --format yaml --username $U --password $P
203
+ ```
204
+
205
+ ## Out-of-scope (must surface as not-implemented)
206
+
207
+ ### 18. --query
208
+
209
+ Returns `validation/not-implemented` immediately.
210
+
211
+ ```bash
212
+ centrs retrieve $R /ip/address --query 'address~"192"' --username $U --password $P --json
213
+ ```
214
+
215
+ ### 19. --filter
216
+
217
+ Same handling as `--query`.
218
+
219
+ ```bash
220
+ centrs retrieve $R /ip/address --filter 'disabled=no' --username $U --password $P --json
221
+ ```
222
+
223
+ ## native-api (`--via native-api`)
224
+
225
+ The same retrieve contract over the RouterOS binary API (TCP 8728, or TLS 8729
226
+ when `--port 8729`). `$A` is `<host>` and `--port` is the resolved api port
227
+ from quickchr (`chr.ports.api`). Validation still runs through
228
+ `/console/inspect`, issued as a native-API command rather than over REST.
229
+
230
+ These are exercised by `test/integration/native-api-retrieve.test.ts`.
231
+
232
+ One transport-specific note: native-API attribute values are **strings**
233
+ (the binary API does not carry JSON scalar types), so `data` scalars are
234
+ strings even where the REST path returns a number/boolean. The envelope shape
235
+ (object vs array vs bare value, projection, validation source) is identical.
236
+
237
+ ### N1. Singleton over native-api
238
+
239
+ ```bash
240
+ centrs retrieve $A /system/resource --via native-api --port $API_PORT --username $U --password $P
241
+ ```
242
+
243
+ Envelope: `ok: true`, `data` is an object, `meta.via=native-api`,
244
+ `meta.validation.source` contains `/console/inspect`.
245
+
246
+ ### N2. Second singleton
247
+
248
+ ```bash
249
+ centrs retrieve $A /system/identity --via native-api --port $API_PORT --username $U --password $P
250
+ ```
251
+
252
+ ### N3. List menu → array
253
+
254
+ ```bash
255
+ centrs retrieve $A /interface --via native-api --port $API_PORT --username $U --password $P
256
+ ```
257
+
258
+ ### N4. Possibly-empty list
259
+
260
+ ```bash
261
+ centrs retrieve $A /ip/address --via native-api --port $API_PORT --username $U --password $P
262
+ ```
263
+
264
+ ### N5. Singleton single `--attribute` → bare value
265
+
266
+ ```bash
267
+ centrs retrieve $A /system/resource --attribute uptime --via native-api --port $API_PORT --username $U --password $P
268
+ ```
269
+
270
+ ### N6. List `--attributes` projection
271
+
272
+ ```bash
273
+ centrs retrieve $A /interface --attributes name,type --via native-api --port $API_PORT --username $U --password $P
274
+ ```
275
+
276
+ `data` is an array of objects each containing only `name` and `type`.
277
+
278
+ ### N7. `--all-attributes` (native `print detail`)
279
+
280
+ ```bash
281
+ centrs retrieve $A /system/resource --all-attributes --via native-api --port $API_PORT --username $U --password $P
282
+ ```
283
+
284
+ ### N8. `--list-attributes` (inspect only, no data call)
285
+
286
+ ```bash
287
+ centrs retrieve $A /system/resource --list-attributes --via native-api --port $API_PORT --username $U --password $P
288
+ ```
289
+
290
+ ### N9. Unknown path → `validation/unknown-path`
291
+
292
+ ```bash
293
+ centrs retrieve $A /not/a/real/path --via native-api --port $API_PORT --username $U --password $P
294
+ ```
295
+
296
+ ### N10. Unknown attribute → `validation/unknown-attribute`
297
+
298
+ ```bash
299
+ centrs retrieve $A /system/resource --attribute bogus-attr --via native-api --port $API_PORT --username $U --password $P
300
+ ```
301
+
302
+ ### N11. Bad credentials → `transport/auth-failed`
303
+
304
+ ```bash
305
+ centrs retrieve $A /system/resource --via native-api --port $API_PORT --username wrong --password wrong
306
+ ```
@@ -0,0 +1,69 @@
1
+ # settings
2
+
3
+ Read and write centrs's own settings — the global `centrs.env` preferences and
4
+ the `__default__` fallback record — and run first-time setup.
5
+
6
+ Status: `designed`. This file describes intent and flags; no implementation yet.
7
+ See `docs/MATRIX.md` for the cell state. `settings` is the front-end to the
8
+ settings the constitution defines (Settings precedence: the `centrs.env` config
9
+ file at tier 2; `__default__` fallback creds): it does not invent new settings,
10
+ only edits the ones already specified.
11
+
12
+ ## Why
13
+
14
+ centrs deliberately has no store of its own for *inventory* — that is the CDB.
15
+ But it does have **global, centrs-only preferences** (default discovery window,
16
+ default `--format`, concurrency, …) that have no per-device home, and a
17
+ **`__default__` fallback record** for credentials. Today both are hand-edited
18
+ (`centrs.env`, or `devices add __default__`). `settings` makes that ergonomic and
19
+ is where onboarding flows steer the user.
20
+
21
+ ## Surface
22
+
23
+ ```text
24
+ centrs settings # interactive setup (TTY)
25
+ centrs settings print [<attr>] # show resolved settings + sources
26
+ centrs settings get <attr> # read one value
27
+ centrs settings set <attr> <value> # write one value to centrs.env
28
+ centrs settings reset [<attr>] # clear one/all back to built-in default
29
+ ```
30
+
31
+ - **Interactive (`centrs settings`, TTY):** prompts (clack-style, like
32
+ `@tikoci/quickchr`) to optionally collect a default `user`/`password` (written
33
+ to the `__default__` record so a fresh CDB "starts useful"), confirm/update
34
+ global preferences, and report where the `centrs.env` lives. Non-TTY runs of
35
+ bare `settings` print the resolved settings instead of prompting.
36
+ - **Non-interactive (`set`/`get`/`reset`/`print`):** scriptable edits to
37
+ `centrs.env`. `print` shows each resolved setting with its winning source
38
+ (`default` / `centrs.env` / `env` / `cli`), mirroring `meta.settings`.
39
+ - Credentials collected for `__default__` route through the `devices` write
40
+ layer (the only CDB writer) and are redacted from any structured output.
41
+
42
+ ## Boundaries
43
+
44
+ - `settings` edits **global** preferences (`centrs.env`) and the **`__default__`**
45
+ record only. Per-device settings stay in `devices` (CDB comment-kv).
46
+ - It writes no RouterOS device; it is local-only, like `devices`.
47
+ - `--skip-env-file` / `CENTRS_SKIP_ENV_FILE=1` makes a run ignore `centrs.env`
48
+ (tests, dev-agent usage); `settings` still reports what *would* apply.
49
+
50
+ ## Onboarding
51
+
52
+ `settings` is the steering target for "centrs starts useful": an empty CDB plus a
53
+ first `retrieve`/`devices` should tip toward `centrs settings` (collect default
54
+ creds) and `centrs devices discover` (populate the registry). See
55
+ `commands/discover/README.md` (save-implied `devices discover`) and
56
+ `docs/CONSTITUTION.md` (Default-device record).
57
+
58
+ ## Settings keys
59
+
60
+ `centrs.env` holds **canonical `CENTRS_*=value` lines** — identical to the
61
+ environment-variable spellings, prefix included (`CENTRS_FORMAT=json`,
62
+ `CENTRS_DISCOVER_TIMEOUT=15s`, `CENTRS_CONCURRENCY=8`). One vocabulary across
63
+ file / env / CLI: the file *is* "just default env vars," so `set -a && source
64
+ centrs.env` works and the loader applies each key as a default **only when the
65
+ same `CENTRS_*` is not already in the process environment** (it never injects
66
+ into `process.env`). `settings set <attr>` accepts the setting name with or
67
+ without the prefix for convenience but always writes the canonical prefixed
68
+ line. It is **user-global only** — the single XDG path above; there is no
69
+ project-local config file discovered from the working directory.
@@ -0,0 +1,70 @@
1
+ # stream
2
+
3
+ Follow live RouterOS output — `print follow`, `monitor`-style commands, and
4
+ sniffer feeds — as a stream of envelopes.
5
+
6
+ Status: `designed`. This file describes intent and flags; no implementation yet.
7
+ See `docs/MATRIX.md` for the cell states. `stream` lands with a transport that
8
+ can hold an open read (native-api first, then ssh); it is **not** a REST cell.
9
+
10
+ ## Why a separate verb
11
+
12
+ `stream` exists because open-ended follow is a different I/O contract than a
13
+ one-shot read, and conflating it into `retrieve` would force every retrieve
14
+ consumer to handle "maybe this is a stream." A caller that picks `stream` knows
15
+ it is getting a sequence of frames, not a single envelope. Bounded, finite
16
+ captures stay on `retrieve` (`--once`); interactive PTY stays on `terminal`
17
+ (constitution: protocol selection).
18
+
19
+ ## Intent
20
+
21
+ - **Read-only**, validated like `retrieve` (path-shaped commands gate through
22
+ `/console/inspect`). `stream` never writes.
23
+ - Wraps RouterOS follow semantics: `once`, `follow`, `duration=`,
24
+ `freeze-frame-interval=`. Targets are the `monitor`/`print follow`/sniffer
25
+ family of menus.
26
+ - **Transport:** true follow cannot ride REST (RouterOS REST has a 60s hard
27
+ cap), so `stream` is **native-api / ssh-first**. `--via rest-api` is
28
+ bounded-or-rejected, never a silent open-ended poll.
29
+ - Not a fan-out surface in single-session mode: a single `stream` rejects N>1
30
+ targets with `usage/fanout-not-supported` (constitution: target selection).
31
+
32
+ ## Output contract (NDJSON stream of envelopes)
33
+
34
+ - Each frame is **one envelope** (`{ ok, data?, warnings?, error?, meta }`),
35
+ emitted as one NDJSON line under `--format json`. Text mode renders
36
+ human-readable frames.
37
+ - The stream is terminated by a final **summary envelope**: frame count,
38
+ duration, and stop reason (`duration-elapsed`, `interrupted`, `eof`,
39
+ `transport-error`).
40
+ - A mid-stream RouterOS or transport error is itself a frame with `ok: false`;
41
+ the **process exit code** reflects whether the stream *started* cleanly, not
42
+ whether every frame was `ok`.
43
+ - `--duration` / `--count` bound an otherwise open-ended follow; Ctrl-C stops it
44
+ and still emits the summary envelope.
45
+
46
+ ## Flags (planned)
47
+
48
+ | Flag | Behavior |
49
+ | ---- | -------- |
50
+ | `--via <protocol>` | Pin transport (`native-api` or `ssh`). REST is bounded-or-rejected. |
51
+ | `--duration <dur>` | Stop after this wall-clock window (maps to RouterOS `duration=`). |
52
+ | `--count <n>` | Stop after N frames. |
53
+ | `--freeze-frame-interval <dur>` | RouterOS `freeze-frame-interval=` passthrough. |
54
+ | `--format <text\|json>` | `json` is NDJSON (one envelope per line); `text` is human frames. |
55
+
56
+ `tail` is a log-scoped alias: `centrs tail <router> /log` ≈ `tail -f` over
57
+ `/log print follow`.
58
+
59
+ ## Open questions
60
+
61
+ - Whether `stream` over ssh shells out to the RouterOS CLI follow or rides a
62
+ structured channel; settle during the SSH transport work.
63
+ - Backpressure / max-buffered-frames policy for a fast producer.
64
+
65
+ Sequencing (decided): do **not** start `stream` until (1) a native-api
66
+ *streaming* spike confirms the reader is solid as a long-lived follow channel
67
+ (not just request/response), and (2) `retrieve` is further along and re-reviewed
68
+ for UX — `stream` should inherit a settled retrieve shape, not race it. The
69
+ native-api streaming reader already exists (`src/protocols/native-api.ts`) and is
70
+ the first follow consumer.
@@ -0,0 +1,177 @@
1
+ # terminal
2
+
3
+ Open an interactive RouterOS console.
4
+
5
+ Status: `terminal / mac-telnet` **and** `terminal / ssh` are both **`CHR-passed`**
6
+ (see `docs/MATRIX.md` and `examples.md` T1–T3 / TS1–TS2). The two transports take
7
+ different paths: **mac-telnet** is the in-process raw passthrough over
8
+ `MacTelnetConsole.attachInteractive`; **ssh** execs the host `ssh` with inherited
9
+ stdio (RouterOS grants no PTY, but `ssh user@host` opens the console and the OS
10
+ relays the already-clean stream — no screen emulation; a host target defaults to
11
+ ssh, a MAC target to mac-telnet). The mac-telnet path is described below;
12
+ `src/terminal.ts` is the raw passthrough over `MacTelnetConsole.attachInteractive`,
13
+ driven through an injectable `TerminalIo` (real stdin/stdout/`SIGWINCH` in
14
+ `src/cli/terminal.ts`). Two input modes by whether stdin is a TTY: interactive
15
+ (raw-mode keystrokes, real size) and batch (pipe a command, close on EOF after an
16
+ output drain). The pipe integration test does not exercise raw-mode/real-TTY-size
17
+ (a piped subprocess stdin is not a TTY); a full-TTY test would need a PTY and is
18
+ deferred.
19
+
20
+ `terminal` is also the documentation home for SSH as a centrs transport. **Re-scoped
21
+ (decided with the user): SSH lands transfer-first, not as one monolithic unit.**
22
+ The SSH transport *base* shipped as the **SFTP transfer client** (`transfer / ssh`,
23
+ `src/protocols/sftp.ts` over the host OpenSSH `sftp` subsystem); the `ssh-key` and
24
+ `insecure` settings landed **with** it (so no setting is half-wired without a
25
+ working transport — the rule still holds). `execute / ssh` and `terminal / ssh`
26
+ then followed and are both `CHR-passed`. **A CHR spike corrected the earlier "no
27
+ exec channel, needs an interactive-shell reader" framing:** RouterOS's SSH server
28
+ has **no pseudo-tty**, but a single-line `ssh user@host "<command>"` runs on the
29
+ console and returns clean output — so `execute / ssh` is a per-command batch client
30
+ and `terminal / ssh` is a thin inherited-stdio exec of `ssh`. Neither needed a
31
+ `MacTelnetConsole`-style screen-emulating reader. The one no-PTY limitation is the
32
+ device's, not centrs's: multi-line brace blocks (`{ … }`) do not work over
33
+ `terminal / ssh` — reach for `mac-telnet` when you need them.
34
+
35
+ ## Intent
36
+
37
+ - `ssh` is the default. It honors system `ssh` config and the user's SSH agent;
38
+ `--ssh-key` overrides with an explicit private key path. CDB metadata may carry
39
+ the path to a per-router private key.
40
+ - `mac-telnet` is the right transport when `<router>` is a MAC address, or
41
+ when SSH is unreachable but a MAC is on file (Layer-2 emergency access).
42
+ - REST and native API have no terminal capability; `--via rest-api` for
43
+ `terminal` must error with `transport/capability-unsupported`.
44
+ - RoMON and WinBox Terminal are execute protocol surfaces in centrs, not
45
+ terminal routes, unless future matrix cells explicitly add terminal support.
46
+
47
+ ## MAC target over SSH (`--resolve`)
48
+
49
+ A MAC `<router>` defaults to `mac-telnet`, which addresses the device by MAC —
50
+ no IP, no resolution. Pinning `--via ssh` asks for the IP-level console, so the
51
+ MAC must first resolve to an IP. That resolution follows the shared `--resolve`
52
+ policy (CDB record → `--resolve arp` host-ARP opt-in → error; never a silent ARP
53
+ or transport swap) — see `docs/CONSTITUTION.md`, *Target selection grammar* and
54
+ *Protocol selection*.
55
+
56
+ Terminal-specific: when no IP resolves, the `target/mac-unresolved` (and
57
+ `target/mac-not-in-arp`) tip **leads with `--via mac-telnet`**, since for terminal
58
+ reaching the device over L2 needs no IP at all — it is the first thing to try.
59
+ `--resolve` is a no-op for the `mac-telnet` default (the MAC is the target).
60
+
61
+ ## SSH key selection
62
+
63
+ Signed off (settings names): terminal uses the same `sshKey` setting as
64
+ `execute`: `--ssh-key <path>`, `CENTRS_SSH_KEY`, and CDB comment-kv
65
+ `ssh-key=<path>`. Precedence is defaults → config → CDB comment-kv → env →
66
+ CLI/API. When no key is set, terminal delegates identity selection to system
67
+ `ssh` config and the SSH agent; `--ssh-key` is an explicit override and must not
68
+ silently merge with a conflicting CDB/env key.
69
+
70
+ Unlike the batch sftp/execute clients, the **interactive** `terminal / ssh` argv
71
+ omits `BatchMode=yes` (`interactive: true` in `sshCommonOptions`), so the host
72
+ `ssh` can prompt on the inherited TTY for an encrypted key's passphrase or a
73
+ password. centrs does not forward `--password` to ssh — OpenSSH accepts no
74
+ password on the argv — so password auth is satisfied by that interactive prompt,
75
+ not by the centrs-resolved credential.
76
+
77
+ Resolved by the sftp transfer client (`src/protocols/sftp.ts`), so they are no
78
+ longer open for the file path: **host-key verification** rides the unified
79
+ `--insecure` knob (default `StrictHostKeyChecking=accept-new` trust-on-first-use;
80
+ a changed key → `transport/host-key-mismatch`; `--insecure` →
81
+ `StrictHostKeyChecking=no` — see `docs/CONSTITUTION.md`, Transport trust);
82
+ **agent vs explicit-key** interplay and **algorithm negotiation** are delegated to
83
+ the host OpenSSH (`-i <ssh-key>` when set, else the agent / `~/.ssh/config`). The
84
+ MAC-on-file-but-SSH-pinned case is settled (see *MAC target over SSH* above):
85
+ centrs resolves a MAC for `--via ssh` CDB-first, then tips `--via mac-telnet` —
86
+ it does **not** silently fall back to L2 (constitution: a pinned `--via` is never
87
+ swapped). The no-pseudo-tty console reader is likewise resolved — a CHR spike
88
+ proved `ssh user@host` opens a clean console with no screen emulation.
89
+
90
+ The setting stores a key path only. Private key material, passphrases, and agent
91
+ contents are always sensitive and belong in `error.redactable_fields` if an
92
+ error carries them; the selected key path may appear in `meta.settings.sshKey`
93
+ with its source unless the caller marks paths sensitive.
94
+
95
+ ## RouterOS SSH surface (device-side option alignment)
96
+
97
+ What RouterOS's SSH server exposes (`/ip/ssh`, grounded on the
98
+ [SSH](https://help.mikrotik.com/docs/spaces/ROS/pages/132350014/SSH) and
99
+ [User](https://help.mikrotik.com/docs/spaces/ROS/pages/8978504/User) pages,
100
+ RouterOS 7.x) and how the centrs client (host OpenSSH) lines up — so there are no
101
+ gaps between what the device offers and what centrs can negotiate:
102
+
103
+ | RouterOS `/ip/ssh` | Values (default) | How centrs aligns |
104
+ | ------------------ | ---------------- | ----------------- |
105
+ | `host-key-type` | `ed25519` \| `rsa` (**rsa**) | Host OpenSSH accepts both; the default device host key is RSA, so the client must offer `rsa-sha2-256/512` and `ssh-ed25519`. TOFU pins whichever the device presents. |
106
+ | `strong-crypto` | `yes` \| `no` (**no**) | `yes` disables ssh-rsa/SHA1, null ciphers, and MD5, and uses sha256 MACs + a 2048-bit DH prime. OpenSSH already prefers `rsa-sha2-*` / curve25519, so a strong-crypto device negotiates cleanly with no client change. |
107
+ | `ciphers` | `3des-cbc`\|`aes-cbc`\|`aes-ctr`\|`aes-gcm`\|`auto`\|`null` (**auto**) | Per-cipher control added ~7.17. OpenSSH's default cipher set covers `aes-ctr`/`aes-gcm`; `null` (no encryption) is never offered by the client. |
108
+ | `password-authentication` | `yes-if-no-key` \| `yes` \| `no` (**yes-if-no-key**) | The default refuses password login once a user has an imported SSH key, so centrs's normal path is key auth (`--ssh-key` / agent). No password is ever placed on the `sftp` argv. |
109
+ | `publickey-authentication-options` | `none`\|`touch-required`\|`verify-required` (**none**) | FIDO touch/verify is handled by the host ssh-agent when the key is a FIDO key; transparent to centrs. |
110
+ | `forwarding-enabled` | `both`\|`local`\|`remote`\|`no` (**no**) | centrs does not use SSH forwarding; no dependency. |
111
+ | — (subsystem) | SFTP subsystem | The reliable **file** channel (no pseudo-tty; a single-line `ssh host "cmd"` exec runs, but it's for `execute`, not files); `transfer --via sftp` speaks it. scp would ride the same subsystem on modern OpenSSH. **CHR-confirmed (7.23.1):** `put`/`get`/`ls`/`mkdir`/`rm` all work over the subsystem. |
112
+ | `/user` group policy | `ftp` (+ `ssh`, `read`/`write`) | SFTP file access needs the user group's `ftp` policy. **CHR-confirmed:** the `full`-group admin (which includes `ftp`) authenticates and transfers; a restricted user must be granted `ftp`. |
113
+
114
+ **CHR finding (7.23.1):** RouterOS's sftp `ls -l` does **not** report a reliable
115
+ byte size (the long-name size column is unpopulated / server-format-specific). So
116
+ centrs's sftp client treats `ls` as an existence + name probe only, and
117
+ `transfer --verify size` over sftp trusts the **SFTP transfer guarantee** (a
118
+ partial `put`/`get` errors) rather than re-reading a size. REST/native keep their
119
+ JSON `/file` size cross-check. See `src/protocols/sftp.ts` (`parseLsOutput`) and
120
+ `src/transfer.ts` (`SftpFileBackend.findFile`).
121
+
122
+ These are device facts the host OpenSSH negotiates for us; the alignment table is
123
+ the audit trail that the centrs file path has no hole versus what RouterOS offers.
124
+
125
+ ## mac-telnet L2 validation
126
+
127
+ Decided (2026-06-06): terminal/mac-telnet uses the same `@tikoci/quickchr`
128
+ host-side L2 capture path as execute/mac-telnet — a host TCP server with the CHR
129
+ on a `socket-connect` NIC (QEMU streams guest frames length-prefixed; a frame
130
+ written back injects L2). Loopback-only, cross-platform, no root. Prefer
131
+ `socket-connect` over `socket-mcast` (macOS-broken — `SO_REUSEPORT`). That
132
+ harness is wired and `execute / mac-telnet` is **`CHR-passed`** over it
133
+ (`test/integration/mac-telnet-console.test.ts`): MTWEI login, the console reader,
134
+ clean command output, writes, and the `:parse` validation gate all work end to
135
+ end. The bridge also relays UDP both ways (`udpPort`), so the production UDP
136
+ transport is exercised, not just the in-process callbacks. See
137
+ `commands/execute/README.md` (mac-telnet L2 validation) for the console wiring and
138
+ auth findings (MTWEI required, MD5 refused, `END_AUTH` ≠ success).
139
+
140
+ The unresolved-MAC default remains load-bearing: a MAC target chooses
141
+ mac-telnet for terminal unless an IP-level resolution path is explicitly
142
+ requested. Covered with resolver/selection tests plus packet/session coverage in
143
+ `test/unit/mac-telnet.test.ts` and EC-SRP coverage in `test/unit/mtwei.test.ts`.
144
+
145
+ ## Interactive-console work (advances terminal/mac-telnet to CHR-passed)
146
+
147
+ The console reader landed with `execute / mac-telnet` (`CHR-passed`): it answers
148
+ the login terminal-size probe, auto-clears the first-login license, syncs on the
149
+ prompt, and emulates the CR/LF screen — grounded on CHR 7.23.1 (the login probe
150
+ is actually a multi-step ANSI cursor probe; `ESC[6n` DSR is the operative query,
151
+ answered with `ESC[rows;colsR`). `terminal / mac-telnet` reuses that reader; what
152
+ it adds on top is **interactive relay**, not protocol work:
153
+
154
+ - **PTY relay**: wire local stdin → `session.sendInput` and console output →
155
+ stdout in raw mode, instead of the `run(cli)` capture path `execute` uses.
156
+ - **Real terminal size**: report the actual PTY rows/cols to the probe (and on
157
+ `SIGWINCH`), where `execute` reports an oversized screen to avoid paging/wrap.
158
+ - **Idle keepalive + retransmit (already landed):** `MacTelnetSession.tick(now)`
159
+ (driven by `MacTelnetConsole` on a ~200ms interval) sends an empty-ACK keepalive
160
+ after ~8s idle and retransmits the last unacked frame on the reference backoff
161
+ schedule. So a held-open `terminal` is already kept alive; the relay just needs
162
+ to keep `tick` running (it does, via the console reader).
163
+ - **~10s prime stall (optional polish):** every mac-telnet login waits ~10s for
164
+ terminal negotiation before the prompt; answering the DSR sets the width but
165
+ does not remove the stall. A cursor-tracking emulator (track row/col, answer
166
+ each `ESC[6n` with the real clamped position) would likely kill it. Latency
167
+ only — not a blocker.
168
+
169
+ Protocol facts to honor: **MTWEI does not encrypt the terminal stream** — treat
170
+ it as management-plane traffic on a trusted L2 segment.
171
+
172
+ ## Open questions
173
+
174
+ - How to surface keystroke recording / replay for tests.
175
+
176
+ The transport, auth, console reader, and data path are all grounded; only the
177
+ interactive-relay layer above remains for terminal/mac-telnet.