@tikoci/centrs 0.1.1 → 0.1.3

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 (95) hide show
  1. package/CHANGELOG.md +107 -2
  2. package/GLOSSARY.txt +87 -0
  3. package/README.md +25 -9
  4. package/SECURITY.md +3 -3
  5. package/commands/AGENTS.md +3 -2
  6. package/commands/api/AGENTS.md +212 -0
  7. package/commands/api/README.md +177 -0
  8. package/commands/api/examples.md +428 -0
  9. package/commands/btest/README.md +56 -31
  10. package/commands/btest/examples.md +35 -10
  11. package/commands/check/README.md +371 -53
  12. package/commands/check/examples.md +390 -0
  13. package/commands/devices/README.md +177 -37
  14. package/commands/devices/examples.md +148 -0
  15. package/commands/execute/README.md +48 -7
  16. package/commands/execute/examples.md +90 -6
  17. package/commands/retrieve/README.md +37 -51
  18. package/commands/retrieve/examples.md +40 -9
  19. package/commands/settings/README.md +286 -22
  20. package/commands/settings/examples.md +408 -0
  21. package/commands/terminal/README.md +6 -0
  22. package/commands/transfer/README.md +32 -2
  23. package/commands/transfer/examples.md +66 -0
  24. package/docs/CONSTITUTION.md +40 -15
  25. package/docs/MATRIX.md +69 -142
  26. package/docs/errors/README.md +2 -1
  27. package/docs/errors/cdb/empty-selection.md +19 -0
  28. package/docs/errors/input/incomplete-gps.md +9 -0
  29. package/docs/errors/input/invalid-altitude.md +9 -0
  30. package/docs/errors/input/invalid-bbox.md +9 -0
  31. package/docs/errors/input/invalid-coordinate.md +9 -0
  32. package/docs/errors/input/invalid-radius.md +9 -0
  33. package/docs/errors/internal/settings-failed.md +9 -0
  34. package/docs/errors/routeros/btest-connection-count-single-stream.md +22 -0
  35. package/docs/errors/routeros/btest-udp-tx-size-ignored.md +15 -0
  36. package/docs/errors/settings/consequential-value.md +26 -0
  37. package/docs/errors/settings/invalid-boolean.md +21 -3
  38. package/docs/errors/settings/invalid-format.md +18 -3
  39. package/docs/errors/settings/invalid-integer.md +18 -3
  40. package/docs/errors/settings/invalid-timeout.md +20 -3
  41. package/docs/errors/settings/invalid-via.md +22 -3
  42. package/docs/errors/settings/reserved-key.md +29 -0
  43. package/docs/errors/settings/skip-env-file-active.md +25 -0
  44. package/docs/errors/settings/unknown-key.md +25 -0
  45. package/docs/errors/usage/fanout-not-supported.md +16 -0
  46. package/docs/errors/usage/invalid-method.md +23 -0
  47. package/package.json +9 -9
  48. package/src/api-fanout.ts +454 -0
  49. package/src/api.ts +1627 -0
  50. package/src/btest.ts +61 -1
  51. package/src/cli/api.ts +660 -0
  52. package/src/cli/btest.ts +20 -2
  53. package/src/cli/common.ts +75 -0
  54. package/src/cli/devices.ts +235 -6
  55. package/src/cli/discover.ts +3 -1
  56. package/src/cli/execute.ts +107 -6
  57. package/src/cli/mcp.ts +8 -1
  58. package/src/cli/retrieve.ts +135 -90
  59. package/src/cli/selection.ts +200 -0
  60. package/src/cli/settings.ts +286 -0
  61. package/src/cli/terminal.ts +22 -7
  62. package/src/cli/transfer.ts +171 -47
  63. package/src/cli.ts +29 -0
  64. package/src/core/error-catalog.ts +69 -0
  65. package/src/core/fanout.ts +301 -0
  66. package/src/core/inspect.ts +170 -0
  67. package/src/devices.ts +212 -20
  68. package/src/discover.ts +7 -2
  69. package/src/execute-fanout.ts +438 -0
  70. package/src/execute.ts +134 -70
  71. package/src/index.ts +166 -9
  72. package/src/mcp/config.ts +12 -2
  73. package/src/mcp/tools.ts +137 -2
  74. package/src/protocols/adapter.ts +357 -9
  75. package/src/protocols/btest-session.ts +279 -46
  76. package/src/protocols/btest.ts +6 -2
  77. package/src/protocols/index.ts +11 -5
  78. package/src/protocols/native-api.ts +223 -30
  79. package/src/resolver/cdb.ts +10 -2
  80. package/src/resolver/comment-kv.ts +58 -4
  81. package/src/resolver/config-file.ts +136 -0
  82. package/src/resolver/facts.ts +78 -0
  83. package/src/resolver/geo.ts +453 -0
  84. package/src/resolver/index.ts +46 -0
  85. package/src/resolver/mac.ts +12 -3
  86. package/src/resolver/selection.ts +304 -0
  87. package/src/resolver/settings.ts +54 -5
  88. package/src/resolver/target.ts +20 -2
  89. package/src/retrieve-fanout.ts +232 -268
  90. package/src/retrieve.ts +69 -94
  91. package/src/settings.ts +1078 -0
  92. package/src/terminal.ts +11 -1
  93. package/src/transfer-fanout.ts +527 -0
  94. package/src/transfer.ts +61 -11
  95. package/commands/stream/README.md +0 -70
package/CHANGELOG.md CHANGED
@@ -8,6 +8,111 @@ documenting cross-cutting shifts that affect contributors and consumers.
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 0.1.3 — 2026-07-06
12
+
13
+ Patch release. `@tikoci/centrs@0.1.3`.
14
+
15
+ ### Added
16
+
17
+ - **`api` command over rest-api + native-api is `CHR-passed`.** `centrs api`
18
+ brings a `gh api`-style structured passthrough to both transports, and folds
19
+ the old standalone `stream` command into `api --stream` (native-api only,
20
+ `--listen` alias): a `NativeApiSession.listen()` reader with `/cancel`
21
+ support emits NDJSON frames plus a run summary. Grounded against real CHR
22
+ 7.23.1.
23
+ - **Multi-target fan-out is uniform across `api`, `retrieve`, `execute`, and
24
+ `transfer`.** A shared `expandCdbSelection` + `runFanout` + `fanoutExitCode`
25
+ core (extracted once, consumed by all four commands) lets any command take a
26
+ CDB selector and run over every matching device. The positional boundary
27
+ that ends the selector differs per command (final positional for
28
+ `api`/`retrieve`, `--` for `execute`, the verb keyword for `transfer`);
29
+ `terminal` and `api --stream` explicitly reject `N > 1` selections since
30
+ neither has a sane multi-target framing.
31
+ - **`settings` command (`print` / `get` / `set` / `reset`).** A managed-key
32
+ registry for the real `CENTRS_*` environment keys backs an atomic
33
+ `centrs.env` writer: `set` validates per key, canonicalizes booleans to
34
+ `1`/`0` on disk, warns (never blocks) on consequential values
35
+ (`insecure=true`, `transfer-via=ftp`), and never partially writes on a
36
+ validation failure; `reset` deletes the managed line rather than blanking it
37
+ (a blank value would itself mean "set to empty" for a bash-sourced file) and
38
+ leaves foreign lines/comments untouched. Bare `centrs settings` behaves like
39
+ `print` regardless of TTY. Four new error/warning codes
40
+ (`settings/reserved-key`, `settings/unknown-key`,
41
+ `settings/consequential-value`, `settings/skip-env-file-active`) plus
42
+ `internal/settings-failed`, all cataloged. `CHR-passed` via 32
43
+ fixture-backed integration examples.
44
+ - **Device GPS: lat/lon/altitude storage + `--near` / `--bbox` geo
45
+ predicates.** `centrs devices` now stores GPS coordinates per device
46
+ (lat-first ordering, chosen so agents guess the field order correctly) and
47
+ supports proximity
48
+ (`--near`) and bounding-box (`--bbox`) selection predicates. `CHR-passed`
49
+ via fixture-backed integration coverage.
50
+ - **Registry⇄MATRIX drift guard + "did you mean?" flag errors.** A CI-checked
51
+ guard keeps the command/protocol registry and `docs/MATRIX.md` from
52
+ silently drifting apart; unrecognized CLI flags now suggest the closest
53
+ known flag instead of a bare rejection.
54
+ - **GitHub Pages: published API reference + error catalog.** `typedoc` output
55
+ and the generated error-code catalog now publish to GitHub Pages on push.
56
+
57
+ ### Fixed
58
+
59
+ - **Windows unit-test portability for `settings` and MNDP.** Path-separator
60
+ and related platform assumptions in the `settings` and MNDP unit suites are
61
+ now Windows-safe (#170).
62
+ - **A real CodeQL-caught ReDoS in the device GPS geo-predicate parser**, fixed
63
+ rather than suppressed.
64
+
65
+ ### Changed
66
+
67
+ - **CodeQL house-rules pack promoted to the default run**, alongside CodeRabbit
68
+ review-tone tuning and a reduced GitHub-checks timeout (15 → 7 minutes).
69
+
70
+ ## 0.1.2 — 2026-06-26
71
+
72
+ Patch release. `@tikoci/centrs@0.1.2`.
73
+
74
+ ### Added
75
+
76
+ - **`btest` TCP multi-connection fan-out is `CHR-passed`.** `centrs btest
77
+ --connection-count N` now realizes the full fan-out: the client reads the
78
+ session token from the primary's OK handshake and opens `N-1` additional TCP
79
+ data connections, each sending the grounded secondary-join
80
+ (`[token:u16 BE][0x02][0…]`). All connections drive into one shared
81
+ `BandwidthCounters` so throughput aggregates. Wire format confirmed
82
+ byte-for-byte against RouterOS 7.23.1 (the server sends a 4-byte HELLO per
83
+ connection then waits for all to join before streaming — sequential opens must
84
+ not block on a reply, or the first secondary deadlocks the server). Unauthenticated
85
+ TCP fan-out works end-to-end; authenticated (EC-SRP5) sessions stay single-stream
86
+ and warn when the realized `activeConnections` falls short of the requested count.
87
+ `BtestRunSummary` / `BtestClientData` gain an `activeConnections` field.
88
+ Grounded via unit tests (secondary-join byte format, loopback fan-out) and
89
+ integration example 11 on real CHR (4 connections open, data flows on each).
90
+
91
+ ### Fixed
92
+
93
+ - **`btest` UDP client receive reported 0 bps.** The UDP socket was bound with
94
+ `connect()` (filtering by remote address/port), which dropped the
95
+ bandwidth-server's probe packets because they arrive from the server's data
96
+ port, not the control port. The socket is now unconnected; `--remote-udp-tx-size`
97
+ emits a warning when unset (needed by the server-push direction only).
98
+ UDP `receive` and `both` cycles are now CHR-gated on real RouterOS (rx ≈ 474 KB
99
+ in CI).
100
+ - **`btest --connection-count` was silently ignored.** The count was validated and
101
+ accepted but never encoded into command byte 3 of the client hello, so RouterOS
102
+ always saw `count=0` and opened a single stream. The value is now threaded through
103
+ `BtestClientSessionOptions` → `clientHandshake`. A
104
+ `routeros/btest-connection-count-single-stream` warning fires for
105
+ `connection-count > 1` sessions that fall back to one stream (e.g. EC-SRP5 auth).
106
+ - **`btest direction=both` TX was unbounded, starving RX.** In TCP `both` the client
107
+ ran no status reader, so TX had no feedback, saturated the link, and collapsed
108
+ server→client RX. The `tcpRxLoop` now demuxes embedded 12-byte status frames
109
+ in-place by their structural marker (`0x07 ?? 00 00`) and passes them through the
110
+ shared `applyStatusFeedback` / `adaptTxFromStatus` gate used by
111
+ `tcpStatusReaderLoop`. A per-frame plausibility guard (CPU byte ≤ 100; sane
112
+ sequence ceiling) filters false matches from `--random-data` bulk streams.
113
+ Grounded: a CHR `both` run fires 5 feedback events (server bytesReceived
114
+ 78840 → 643860) and paces TX; RX is sustained instead of collapsing.
115
+
11
116
  ## 0.1.1 — 2026-06-22
12
117
 
13
118
  Patch release. `@tikoci/centrs@0.1.1`.
@@ -214,8 +319,8 @@ First published pre-release (odd minor → npm `next`). `@tikoci/centrs@0.1.0`.
214
319
  result envelope, error model + URL scheme, settings precedence, identity,
215
320
  protocol selection, done = CHR-passed).
216
321
  - `docs/MATRIX.md` — command×protocol grid; the only status surface.
217
- - `commands/<name>/{README,examples}.md` — per-command design and
218
- executable example list that gates "done."
322
+ - `commands/<name>/README.md` and `commands/<name>/examples.md` —
323
+ per-command design and executable example list that gates "done."
219
324
  - Done definition codified in
220
325
  `.github/instructions/done-definition.instructions.md`: a feature is done
221
326
  when its examples are green on real CHR via `bun run test:integration`.
package/GLOSSARY.txt CHANGED
@@ -25,6 +25,11 @@
25
25
  # https://github.com/tikoci/restraml
26
26
  # %lsp-routeros-ts: RouterOS parser and language-server utilities.
27
27
  # https://github.com/tikoci/lsp-routeros-ts
28
+ # Language Server Protocol — the "lsp" in lsp-routeros-ts, referenced as a
29
+ # grounding source for /console/inspect client patterns (e.g. commands/api).
30
+ lsp
31
+ # RouterOS maximum transmission unit property (e.g. mtu, actual-mtu).
32
+ mtu
28
33
  # %donny: Dude/discovery grounding for RouterOS device inventories.
29
34
  # https://github.com/tikoci/donny
30
35
  # %m2ir: protocol IR (WinBox, nova messages) + the declarative schema-as-data
@@ -89,6 +94,8 @@ datagram
89
94
  davidanson
90
95
  DDNS
91
96
  defconf
97
+ demux
98
+ demuxes
92
99
  dogtail
93
100
  donny
94
101
  dotenv
@@ -132,6 +139,7 @@ MNDP
132
139
  NAND # NAND flash memory; write-back caches file writes up to ~40s on some MikroTik boards
133
140
  netmiko # [Multi-vendor library to simplify Paramiko SSH connections to network devices](https://github.com/ktbyers/netmiko)
134
141
  netwatch
142
+ nullglob # bash shopt that makes an unmatched glob expand to zero words instead of the literal pattern
135
143
  npk # MikroTik package format
136
144
  nvme-over-tcp # supported by RouterOS **with** rose-storage.npk extra-package
137
145
  OID
@@ -141,11 +149,13 @@ PRGA
141
149
  proplist
142
150
  pywinauto
143
151
  QML
152
+ qll # CodeQL library file extension (.qll), e.g. CleartextLoggingCustomizations.qll
144
153
  qmljsdebugger
145
154
  quickchr
146
155
  RAML
147
156
  readcdb
148
157
  remotehub
158
+ retarget
149
159
  restraml
150
160
  romon
151
161
  ros
@@ -153,12 +163,15 @@ rosetta
153
163
  routeros
154
164
  routeros-skills
155
165
  rsc # RouterOS script / export file extension (.rsc)
166
+ SARIF # Static Analysis Results Interchange Format; CodeQL's code-scanning analysis download format
156
167
  scp
157
168
  scriptability
158
169
  scriptable
170
+ shopt # bash builtin for toggling shell options (e.g. nullglob)
159
171
  Sikuli
160
172
  SikuliX
161
173
  skillcheck
174
+ startswith # jq builtin filter that tests whether a string begins with a given prefix
162
175
  SNMP
163
176
  SPI
164
177
  SRP
@@ -176,6 +189,7 @@ TIKOCI
176
189
  tiktui
177
190
  TLV
178
191
  traceroute
192
+ tostring # jq builtin filter that renders a value as its JSON string representation
179
193
  tsc
180
194
  tzsp
181
195
  TZSP
@@ -418,3 +432,76 @@ trippable
418
432
  # btest integration-test fixtures.
419
433
  btestuser
420
434
  btestpass
435
+
436
+ # RouterOS scripting builtin referenced in commands/api grounding notes
437
+ # (:toarray coerces a comma string into an array; explains the inspect path form).
438
+ toarray
439
+
440
+ # Logic shorthand ("if and only if") used in api write-gate doc comments; cspell's
441
+ # markdown dictionary knows it but the TypeScript one does not.
442
+ iff
443
+
444
+ # GitHub Pages build vocabulary (scripts/build-pages-site.ts, pages.yaml)
445
+ # typedoc/pandoc: the API-reference generator and the markdown->HTML converter
446
+ # the Pages build chains together. gfm: GitHub-Flavored Markdown, pandoc's
447
+ # input format for the error pages. larr: the "&larr;" left-arrow HTML entity
448
+ # in the back-to-docs link. nojekyll: the marker file that disables GitHub
449
+ # Pages' Jekyll processing for an Actions-deployed artifact. extensionless:
450
+ # describes the `details_url` contract (docs/errors/README.md) — no `.html`.
451
+ typedoc
452
+ pandoc
453
+ gfm
454
+ larr
455
+ nojekyll
456
+ extensionless
457
+ # nothrow: Bun Shell's `$` method to disable throwing on a non-zero exit
458
+ # (scripts/agent-check.ts), so the caller can inspect output either way.
459
+ nothrow
460
+
461
+ # check-command spec vocabulary (commands/check/README.md, examples.md)
462
+ # TikTOML: centrs-owned readable device-inventory format (#137); check's
463
+ # metadata feeds it. routerboard: RouterBOARD firmware/hardware menu
464
+ # (/system/routerboard). rscmd: the tikbook RouterOS command-notebook file
465
+ # extension (.rscmd), the format the check spec was grounded against.
466
+ TikTOML
467
+ routerboard
468
+ rscmd
469
+ # RTT: round-trip time, the host->router connect-latency reach signal. tojson: the
470
+ # /system/script helper (:serialize to=json) the batched execute reads reuse.
471
+ # qrcode: the /ip/cloud vpn-wireguard-client-config-qrcode field token (redacted
472
+ # by the cloud check, referenced when documenting redaction).
473
+ RTT
474
+ tojson
475
+ qrcode
476
+
477
+ # Device GPS vocabulary (issue #146, commands/devices/README.md "Location /
478
+ # GPS", src/resolver/geo.ts). MSL/AGL: the two supported altitude-type vertical
479
+ # datums (mean sea level / above ground level); stored case-insensitively but
480
+ # normalized to upper case. EPSG: the geodetic coordinate-reference-system
481
+ # registry (EPSG:4326 = WGS84 lat/lon), cited for the lat-first ordering
482
+ # rationale. bbox: the --bbox geo selector (bounding box). wgs: WGS84, the
483
+ # geodetic datum GPS coordinates use; appears only as a deliberately-invalid
484
+ # --altitude-type value in tests/examples. antimeridian: the 180° meridian a
485
+ # --bbox may not cross in v1. GEOSEARCH: the Redis command whose m/km/mi/ft
486
+ # radius-unit vocabulary --near borrows. IUGG: the geodesy union whose mean
487
+ # Earth radius the haversine uses. SFO/OAK: airports used as a known-distance
488
+ # haversine fixture in tests. ele/GPX: `ele` is GPX's altitude element, kept as
489
+ # an input alias for `altitude` (GPX = the GPS Exchange Format). Geolocation:
490
+ # the W3C Geolocation API, cited (with ISO 6709 / EPSG / IETF geo:) for why
491
+ # `altitude` — not `elevation` — is the canonical field name.
492
+ MSL
493
+ msl
494
+ AGL
495
+ agl
496
+ Agl
497
+ EPSG
498
+ bbox
499
+ wgs
500
+ antimeridian
501
+ GEOSEARCH
502
+ IUGG
503
+ SFO
504
+ sfo
505
+ ele
506
+ GPX
507
+ Geolocation
package/README.md CHANGED
@@ -4,6 +4,12 @@
4
4
  CLI (with MCP, TUI, and HTTP proxy frontends planned) for talking to MikroTik
5
5
  RouterOS devices through a regularized, validated interface.
6
6
 
7
+ Preview builds are published on npm. To inspect the CLI without installing it:
8
+
9
+ ```bash
10
+ bunx @tikoci/centrs --help
11
+ ```
12
+
7
13
  It is a **friendly conduit**, not a high-level configuration abstraction. It
8
14
  helps humans and agents reach RouterOS over the right protocol, with the right
9
15
  credentials and ports, and validates RouterOS-shaped commands before running
@@ -11,9 +17,12 @@ them. It does **not** hide RouterOS behind helpers like `createVlanOnBridge()`
11
17
  — you still speak RouterOS. Validation and structured diagnostics are the
12
18
  product; without them this would just be a worse `curl`.
13
19
 
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.
20
+ > **Status:** early npm preview, active development. `@tikoci/centrs` is
21
+ > published on npm, but command flags and envelopes may still change before a
22
+ > stable release. Check current dist-tags with
23
+ > `npm view @tikoci/centrs dist-tags --json`. The library, device registry, and
24
+ > first MCP server phases are CHR-verified; the CLI is wired (`coded`);
25
+ > encrypted-CDB writes are still blocked, and the TUI/proxy frontends are planned.
17
26
  > `docs/MATRIX.md` is the single source of truth for what works today — treat
18
27
  > anything not green there as not-yet-shipped.
19
28
 
@@ -32,8 +41,8 @@ product; without them this would just be a worse `curl`.
32
41
  | Command | Purpose |
33
42
  | ---------- | ------- |
34
43
  | `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
44
  | `execute` | Run RouterOS CLI-shaped read/write commands (add/set/remove) over native API/REST/L2 surfaces. |
45
+ | `api` | Structured RouterOS API passthrough (gh-api style): one command per operation, structured in/out, can write, over REST or native API. Open-ended follow is `api … --stream` (native API; NDJSON). |
37
46
  | `terminal` | Open an interactive console, primarily SSH or MAC-Telnet. |
38
47
  | `check` | Probe reachability and management protocol availability. |
39
48
  | `devices` | View and maintain the CDB-backed device registry (the only writer). |
@@ -104,16 +113,23 @@ CDB tags, and `devices set` refuses to put them there. Unknown keys produce a
104
113
  `cdb/unknown-option` warning and are ignored, so a stray `=` in human prose
105
114
  never breaks anything.
106
115
 
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.
116
+ Settings resolve lowest-to-highest: **built-in defaults → user config file
117
+ (`centrs.env`) → comment-kv → `CENTRS_*` env → CLI flag / API arg.** Every
118
+ result envelope reports which source won each setting under `meta.settings`,
119
+ so you can always see *why* a given port or transport was chosen.
111
120
 
112
121
  ### Groups, discovery, and the MCP allowlist all ride on the CDB
113
122
 
114
123
  - **Groups** are CDB-native: an entry's `group` field is just a string, and
115
124
  `--group prod-edge` fans a single command out to every entry that shares it.
116
- There is no separate group-definition file.
125
+ There is no separate group-definition file. Groups are one selector in a
126
+ **uniform fan-out grammar** shared by `api`, `retrieve`, `execute`, and
127
+ `transfer`: mix `<router>` positionals, repeatable `--group`/`--where
128
+ <attr>=<value>`, `--all`, and `--default` (de-duped by CDB record index). Every
129
+ fan-out returns the locked `FanoutData` envelope (per-target results, not
130
+ metadata) with a granular exit code (`0` all ok / `2` partial / `1` all failed);
131
+ a multi-target write is gated once by `--yes` naming the blast radius. See
132
+ `docs/CONSTITUTION.md` (Target selection grammar).
117
133
  - **Discovery** writes back into the CDB: `discover --save` records MNDP
118
134
  neighbors as new entries tagged `group=discovered` with a `source=mndp`
119
135
  marker — hints to curate, not authoritative inventory.
package/SECURITY.md CHANGED
@@ -14,14 +14,14 @@ Please include the affected files or workflow, reproduction details, and impact.
14
14
 
15
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
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.
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) plus a small custom pack, [`.github/codeql-house-rules/`](.github/codeql-house-rules/), that structurally checks a handful of repo-specific invariants (currently: `apiFanout` must call its named protocol/validation gates) — see `.github/instructions/github-security-quality.instructions.md` for scope and the verify-before-promote discipline for adding more rules there. Languages: `javascript-typescript`, `actions`. Schedule: push to `main`, pull requests to `main`, weekly cron, and manual dispatch.
18
+ - **Code Quality (AI findings, preview)** — confirmed active in GitHub as of 2026-07-03 (not just "intended"; see [issue #166](https://github.com/tikoci/centrs/issues/166)). [`.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. Side effect: GitHub's own Code Quality scan runs in parallel with our Advanced Setup and posts a same-named `Analyze (javascript-typescript)` check, so PR checks lists show it twice — cosmetic, tracked in #166, not a required check either way (see issue #118).
19
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
20
  - **Dependabot security updates** — enabled and configured in [`.github/dependabot.yaml`](.github/dependabot.yaml).
21
21
  - **Secret scanning** — enabled in GitHub, with push protection.
22
22
  - **Private vulnerability reporting** — enabled via GitHub Security Advisories.
23
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.
24
+ Some GitHub Security tab data, especially Code Quality AI findings, is not reachable through the default workflow `GITHUB_TOKEN` (`repos/{owner}/{repo}/code-quality/findings` 403s there even with `security-events: read`) — it does respond with an elevated/owner-scoped token, but no findings have been recorded yet to confirm the response shape end-to-end (see #166). Human review in the GitHub UI remains part of the security workflow until that's wired up.
25
25
 
26
26
  ## Supported versions
27
27
 
@@ -18,8 +18,9 @@ README's "Status" line must agree with it.
18
18
 
19
19
  ## Verb vocabulary
20
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
21
+ Commands are centrs **verbs** (`retrieve`, `execute`, `api`, `devices`,
22
+ `discover`, `check`, `terminal`, `settings`), never one tool per RouterOS command. Open-ended follow
23
+ is folded into `api --stream` (no separate `stream` verb). Sub-verbs and their
23
24
  aliases are canonical across CLI / API / MCP:
24
25
 
25
26
  | Canonical | Aliases | Meaning |
@@ -0,0 +1,212 @@
1
+ # commands/api — local constraints + grounded RouterOS facts
2
+
3
+ This file is the **durable home** for the RouterOS API-behavior facts the `api`
4
+ command depends on. It exists so the knowledge survives context resets: a fresh
5
+ agent picking up any `api` phase reads this first. Add a fact here only with a
6
+ source (a doc URL, a `tikoci/*` project, or a CHR run); follow the global
7
+ grounding discipline — one CHR result is a signal, reproduce before treating it as
8
+ fact.
9
+
10
+ Read order for any `api` work: `docs/CONSTITUTION.md` → `commands/api/README.md` →
11
+ `commands/api/examples.md` → this file.
12
+
13
+ ## Local constraints
14
+
15
+ - `api` is the structured one-command-per-operation surface (the verb trichotomy
16
+ in `README.md`). It must not grow code-block / multi-command behavior — that is
17
+ `execute`'s job.
18
+ - Reuse the shared cores, do not fork: `src/core/inspect.ts` (the
19
+ `/console/inspect` client) and `src/core/fanout.ts` (the fan-out engine).
20
+ - Honor `--via` literally; never silently downgrade transport.
21
+
22
+ ## Grounded facts (doc sources)
23
+
24
+ REST API — <https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API> ·
25
+ native API — <https://help.mikrotik.com/docs/spaces/ROS/pages/47579160/API> ·
26
+ inspect-client TS patterns — `tikoci/lsp-routeros-ts`
27
+ (`server/src/routeros.ts` `InspectRequest`, `server/src/validation.ts`).
28
+
29
+ - **HTTP method map (REST):** `GET`→print, `PUT`→add (**create**), `PATCH`→set,
30
+ `DELETE`→remove, `POST`→universal (run any console command). PUT — not POST — is
31
+ RouterOS's create.
32
+ - **Multi-frame results: bounded → array; open-ended → native `/listen` only.**
33
+ *Nuance (do not over-read the REST docs).* RouterOS REST **does** run
34
+ `duration=` / `monitor` commands — it regularizes the native `!re` frames into a
35
+ JSON **array of `.section` records** and returns the whole array in one response,
36
+ bounded by the **60 s** REST cap (a command asked to run longer terminates early
37
+ with an error). Native has **no** such cap. So a `duration=`/`monitor` command is
38
+ an **ordinary bounded `api` call** that returns a `.section` array on both
39
+ transports (native's `talk()` accumulates the `!re` until `!done`) — **not**
40
+ NDJSON. The only thing that needs the NDJSON streaming path is **open-ended
41
+ follow** (native `/listen`, which never sends `!done`); REST genuinely cannot do
42
+ open-ended follow. ⇒ `--listen` (open-ended) is native-only; `duration=` commands
43
+ are normal one-shot calls. (CONFIRMED below.)
44
+ - **`/listen` semantics.** Emits `!re` on change; a deleted/disappeared item's
45
+ `!re` carries the dead flag (docs say `=.dead=yes`; **CHR 7.23.1 sends
46
+ `.dead=true`** — see CONFIRMED below); it never self-terminates. Cancel with
47
+ `/cancel tag=<listen-tag>` (the `/cancel` carries its own `.tag`); the listener
48
+ then gets `!trap … message=interrupted` followed by `!done`.
49
+ - **Query.** REST `.query` words are **identical to native `?` words minus the
50
+ `?` prefix** (CONFIRMED below). Forms: eq `name=val`; comparison `>name=val` /
51
+ `<name=val` (operator **before** the name); has-property `name`; absence
52
+ `-name`; stack ops `#!` (NOT top), `#&` (AND), `#|` (OR). Multiple words with no
53
+ stack op are **implicitly AND-ed**. See the centrs mapping section below.
54
+ - **Proplist.** REST `.proplist` accepts `"name,type"` or `["name","type"]`.
55
+ Native: `=.proplist=name,type`.
56
+ - **Object id.** Set/remove by id: REST id-in-URL (`PATCH /rest/ip/address/*1`),
57
+ native `=.id=*1` word. **No native get-one-by-id shorthand** — use
58
+ `print ?.id=*1`. REST has the convenience `GET /rest/ip/address/*1` → a single
59
+ object; native maps it to `print ?.id=*1`.
60
+ - **`/console/inspect` request modes** (`InspectRequest` in lsp): `request ∈
61
+ {child, completion, highlight, syntax}`, with `input?`, `path?`, `.proplist?`,
62
+ `.query?`. `child` lists menus/args (path + attribute validity); `completion`
63
+ + `input=` yields allowed values (tips/warnings); `highlight` + `input=` is
64
+ token-level error detection (lsp's primary validator); `syntax` is the parse
65
+ tree.
66
+
67
+ ## Query mapping (centrs `--query` / `--raw-query`)
68
+
69
+ Two layers, both grounded above:
70
+
71
+ - **`--query` / `--filter`** — structured convenience, **AND-combined** (RouterOS
72
+ default), repeatable. Each maps to one query word:
73
+ - `name=value` → `name=value` (eq)
74
+ - `name!=value` → two words `name=value`, `#!` (eq then NOT-top)
75
+ - `name>value` / `name<value` → `>name=value` / `<name=value`
76
+ - `name` (no `=`) → `name` (has-property)
77
+ Multiple `--query` emit their words in order; RouterOS's implicit AND applies (no
78
+ `#&` injected). `--query` cannot express OR or arbitrary stacks — that is
79
+ `--raw-query`.
80
+ - **`--raw-query <word>`** — power-user escape hatch, repeatable. Each value is a
81
+ **verbatim RouterOS query word** (absence `-name`, OR `#|`, AND `#&`, NOT `#!`,
82
+ or any `>`/`<`/`=` form). Emitted as-is: a `.query` element over REST, a
83
+ `?`-prefixed word over native. centrs does **not** parse or reorder raw words —
84
+ the caller owns the stack. When `--query` and `--raw-query` are both given, the
85
+ structured words are emitted first, then the raw words, in order.
86
+
87
+ This keeps the common case (`--query type=ether`) clean while giving full
88
+ stack-language access without centrs modelling the whole query grammar.
89
+
90
+ ## CONFIRMED ON CHR (Phase 0 grounding spikes — CHR 7.23.1, 2026-06-29)
91
+
92
+ Probes: scratchpad `api-grounding-spike.ts` (listen/CRUD/execute) and
93
+ `api-grounding-spike2.ts` (duration/monitor + query forms + inspect path). Raw
94
+ native codec for the listen probe; `fetch` for REST; `connectNativeApi().talk()`
95
+ for native.
96
+
97
+ - **CONFIRMED — native `/listen` + `/cancel`.** `/ip/address/listen .tag=2`
98
+ emits an `!re tag=2` carrying the **full new record** on add; on delete it emits
99
+ a **minimal** `!re tag=2 { ".id":"*3", ".dead":"true" }`. **Correction to the
100
+ docs:** the wire flag is **`.dead=true`**, not `=.dead=yes`. `/cancel =tag=2
101
+ .tag=3` yields `!trap tag=2 {category:2, message:interrupted}`, then `!done
102
+ tag=3` (the cancel) **and** `!done tag=2` (the listen closes). Modern plaintext
103
+ `/login` returns a bare `!done` (no challenge) on 7.23.1.
104
+ - **CONFIRMED — native CRUD by id.** `add` returns the new id in `=ret=`; `set`
105
+ and `remove` via `=.id=<id>` succeed with zero `!re` (only `!done`); get-one is
106
+ `print ?.id=<id>` → exactly one record. `numbers=` is **not** needed (that is
107
+ execute's CLI idiom, not the API's).
108
+ - **CONFIRMED — query + proplist.** Native `/interface/print ?type=ether` filters
109
+ server-side; `=.proplist=address,interface` returns only those keys. REST
110
+ `POST /rest/interface/print {".query":["type=ether"],".proplist":"name,type"}`
111
+ → `200 [{"name":"ether1","type":"ether"}]` (a complete **array**, not a stream).
112
+ - **CONFIRMED — duration/monitor returns a bounded `.section` array (both
113
+ transports).** `POST /rest/interface/monitor-traffic {interface:ether1,
114
+ duration:"5s"}` → `200`, ~5.0 s, a **JSON array of 5 records** keyed
115
+ `.section:"0".."4"` (each `…-bits-per-second` etc.). The native
116
+ `/interface/monitor-traffic duration=2s` returns the same frames via `talk()` (2
117
+ records, each with `.section`) before `!done`. So duration commands are ordinary
118
+ bounded calls, **not** NDJSON. (Corrects the first spike's over-strong "REST does
119
+ not support continuous commands" reading; the `/rest/interface/monitor` 400 there
120
+ was just a wrong command path — `monitor-traffic` is the real menu.) The 60 s
121
+ unbounded cap stays doc-grounded — not worth a 60 s CI wait to reproduce.
122
+ - **CONFIRMED — `/console/inspect` `path` is array-typed; pass COMMA tokens, not a
123
+ slash command.** `request=child path=system,license` → children `license:dir,
124
+ export:cmd, generate-new-id:cmd, get:cmd, print:cmd, renew:cmd`; the **slash**
125
+ forms (`system/license`, `/system/license`) return **nothing**. (This is why the
126
+ first spike's license probe was empty — wrong path form.) **Why (rationale, not
127
+ yet re-spiked):** RouterOS's inspect `path` argument is internally an **array**
128
+ type — a comma string like `ip,address,set` is `:toarray`-split into
129
+ `["ip","address","set"]`, the menu walk inspect expects. A `/`-prefixed
130
+ command-style string is **not** an array and isn't split, so it matches no menu.
131
+ A JSON **array** body over REST (`{"path":["ip","address"]}`) should therefore
132
+ work too — **plausible but unconfirmed; do not rely on it.** The contract centrs
133
+ uses is the **comma-joined string** (confirmed; what both `retrieve` and
134
+ `execute` already build via their duplicate `join(",")` helpers — the Phase-1
135
+ `src/core/inspect.ts` is the single home for it). And **`renew` is a `cmd`
136
+ node** → `/system/license/renew` is a write/command, confirming the "streaming ≠
137
+ read-only" rule concretely.
138
+ - **CONFIRMED — `.query` forms (REST word == native `?`-word minus `?`).** On
139
+ `/interface/print` (2 interfaces: ether, loopback): eq `type=ether` → 1; ne
140
+ `["type=ether","#!"]` → loopback; OR `["type=ether","type=loopback","#|"]` → 2;
141
+ gt `[">actual-mtu=1000"]` → 2; has `["running"]` → 2; absence `["-running"]` →
142
+ 0; implicit-AND `["type=ether","running"]` → 1. Identical results native
143
+ (`?`-prefixed). REST license singleton: `GET /rest/system/license` →
144
+ `{"level":"free","system-id":"…"}`.
145
+ - **CONFIRMED — `POST /rest/execute` modes.** With `"as-string":""` it runs
146
+ **synchronously** → `{"ret":"CHR"}`. **Without** `as-string` it is **fire-and-
147
+ forget**: it schedules a job and returns the **job id** (`{"ret":"*18"}`, HTTP
148
+ 200) — a `:error` does **not** surface synchronously. ⇒ `api`'s script-POST must
149
+ send `as-string` (reuse execute's `restPost("/execute",{script,"as-string":""})`,
150
+ `adapter.ts:199`) so output/errors are synchronous; the bare async-job form is
151
+ not the default.
152
+ - **RESOLVED — `/system/license`** (was inconclusive in spike 1 due to the slash
153
+ path; see the COMMA-path finding above). Independently, `isApiMutating` keys the
154
+ write gate on the method/verb, so even an async/streamable write like
155
+ `/system/license/renew` confirms regardless.
156
+
157
+ ## CONFIRMED ON CHR (Phase 2+3 integration — CHR 7.23.1, 2026-06-29)
158
+
159
+ Validated by `test/integration/api.test.ts` (rest, examples 1–20, 98 assertions)
160
+ and `test/integration/api-native.test.ts` (native, N1–N8, 31 assertions), both
161
+ green on CHR 7.23.1.
162
+
163
+ - **CONFIRMED — native `/execute` supports `=as-string=` for synchronous output.**
164
+ Native `talk /execute =script=…` **without** `as-string` is **fire-and-forget**:
165
+ it schedules a job and returns the **job id** in `=ret=` (observed `"*31"`), not
166
+ the script output — exactly the REST behavior. Adding `=as-string=` (empty value)
167
+ makes it run synchronously and return the captured output, identical to REST
168
+ `/rest/execute {as-string}`. **This corrects `src/execute.ts`'s long-standing
169
+ assumption that native script mode is unsupported** (execute still blocks it; the
170
+ `api` path proves native `/execute =as-string=` works). `api` always sends
171
+ `as-string` for a script run on both transports.
172
+ - **CONFIRMED — native CRUD re-mapping to rest-style.** `add` returns the new id
173
+ only in the `!done` `=ret=` word → centrs re-maps it to `{".id": ret}` (REST PUT
174
+ returns the full created object with `.id`; native gives just the id). `set`/
175
+ `remove` reply with a bare `!done` (no `!re`) → no body → centrs surfaces `null`.
176
+ get-one is `print ?.id=<id>` → exactly one record → returned as a single object
177
+ (not a 1-element array). All grounded in `restStyleMutationData` /
178
+ `restStyleRunData` (`src/protocols/adapter.ts`).
179
+ - **CONFIRMED — REST `monitor-traffic` over the `api` path returns a `.section`
180
+ array.** `POST /rest/interface/monitor-traffic {interface,duration}` →
181
+ `[{".section":"0",…}, {".section":"1",…}]` (re-confirms the spike-2 finding via
182
+ the real `api` REST adapter). It is a command (terminal verb `monitor-traffic`,
183
+ not `print`/`get`), so it is a `POST` and **write-classed** → needs `--yes`.
184
+ - **CONFIRMED — the inspect gate over both transports.** Path existence
185
+ (`request=child` empty ⇒ `validation/unknown-path`) and add/set attribute
186
+ validity (`request=child`+`completion` ⇒ `validation/unknown-attribute`) fire
187
+ identically over rest-api and native-api, **before** any write — no `:put
188
+ [:parse]` involved (api input is a path, not a CLI string). A `/execute` script
189
+ is a CLI string ⇒ `meta.validation.semantic = "not-applicable"`.
190
+
191
+ ## CONFIRMED ON CHR (Phase 4 `--stream` follow — CHR 7.23.1, 2026-06-30)
192
+
193
+ Validated by `test/integration/api-listen.test.ts` (L1–L4) on CHR 7.23.1.
194
+ `--stream` is the primary flag; `--listen` is an alias; a `/listen` endpoint
195
+ infers both. Native-api only (REST's 60s cap → `transport/capability-unsupported`).
196
+
197
+ - **CONFIRMED — `NativeApiSession.listen()` + `/cancel` end-to-end.** Opening
198
+ `/ip/address/listen` (own `.tag`) and adding an address over REST yields an
199
+ `!re` frame; stopping (`--count`/`--duration`/Ctrl-C) sends `/cancel =tag=<t>
200
+ .tag=<n>` and the listen closes via the `interrupted` trap (category 2) then
201
+ `!done` — the generator ends **without** throwing (the interrupted trap is the
202
+ normal cancel, not an error). A non-interrupted trap or transport closure does
203
+ throw. Re-confirms the Phase-0 spike through the real `apiListen()` path.
204
+ - **CONFIRMED — deletion frames carry `.dead=true`.** Removing a pre-seeded
205
+ address over REST while listening emits a minimal `{ ".id", ".dead":"true" }`
206
+ `!re` (re-confirms `.dead=true`, not the docs' `=.dead=yes`).
207
+ - **Stream shape:** each `!re` → one NDJSON envelope frame
208
+ (`meta.operation.stream.kind="frame"`, string values, `.dead` preserved); the
209
+ stream ends with a summary envelope (`stream.kind="summary"`,
210
+ `data.stopReason` ∈ `count-reached`/`duration-elapsed`/`interrupted`/
211
+ `transport-error`, plus `frames`/`durationMs`). The CLI exit code keys on
212
+ whether the stream *started* cleanly, not on per-frame `ok`.