@tikoci/centrs 0.1.2 → 0.1.4
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.
- package/CHANGELOG.md +98 -0
- package/GLOSSARY.txt +284 -0
- package/LICENSE +21 -0
- package/README.md +55 -11
- package/SECURITY.md +3 -3
- package/commands/AGENTS.md +7 -1
- package/commands/api/README.md +46 -27
- package/commands/api/examples.md +124 -6
- package/commands/btest/README.md +21 -32
- package/commands/check/README.md +376 -53
- package/commands/check/examples.md +390 -0
- package/commands/devices/README.md +177 -37
- package/commands/devices/examples.md +148 -0
- package/commands/discover/README.md +5 -11
- package/commands/execute/README.md +73 -26
- package/commands/execute/examples.md +131 -9
- package/commands/explain/README.md +1716 -0
- package/commands/explain/examples.md +606 -0
- package/commands/retrieve/README.md +55 -71
- package/commands/retrieve/examples.md +56 -9
- package/commands/settings/README.md +286 -22
- package/commands/settings/examples.md +408 -0
- package/commands/terminal/README.md +6 -0
- package/commands/transfer/README.md +57 -19
- package/commands/transfer/examples.md +95 -0
- package/docs/CLI.md +353 -0
- package/docs/CONSTITUTION.md +102 -26
- package/docs/MATRIX.md +106 -173
- package/docs/errors/README.md +5 -1
- package/docs/errors/cdb/empty-selection.md +19 -0
- package/docs/errors/input/incomplete-gps.md +9 -0
- package/docs/errors/input/invalid-altitude.md +9 -0
- package/docs/errors/input/invalid-bbox.md +9 -0
- package/docs/errors/input/invalid-coordinate.md +9 -0
- package/docs/errors/input/invalid-radius.md +9 -0
- package/docs/errors/internal/settings-failed.md +9 -0
- package/docs/errors/quickchr/machine-not-found.md +22 -0
- package/docs/errors/quickchr/machine-stopped.md +20 -0
- package/docs/errors/quickchr/package-unavailable.md +27 -0
- package/docs/errors/quickchr/unsupported-via.md +35 -0
- package/docs/errors/quickchr/unsupported.md +35 -0
- package/docs/errors/routeros/version-unsupported.md +26 -0
- package/docs/errors/settings/consequential-value.md +26 -0
- package/docs/errors/settings/invalid-boolean.md +21 -3
- package/docs/errors/settings/invalid-format.md +18 -3
- package/docs/errors/settings/invalid-integer.md +18 -3
- package/docs/errors/settings/invalid-timeout.md +20 -3
- package/docs/errors/settings/invalid-via.md +22 -3
- package/docs/errors/settings/reserved-key.md +29 -0
- package/docs/errors/settings/skip-env-file-active.md +25 -0
- package/docs/errors/settings/unknown-key.md +25 -0
- package/docs/errors/transport/provider-trust.md +29 -0
- package/docs/errors/usage/fanout-not-supported.md +16 -0
- package/docs/errors/usage/stdin-ignored.md +28 -0
- package/docs/errors/validation/unknown-attribute.md +37 -4
- package/package.json +49 -14
- package/src/api-fanout.ts +469 -0
- package/src/api.ts +171 -56
- package/src/cli/api.ts +156 -13
- package/src/cli/btest.ts +38 -4
- package/src/cli/common.ts +11 -0
- package/src/cli/devices.ts +235 -6
- package/src/cli/discover.ts +3 -1
- package/src/cli/execute.ts +129 -9
- package/src/cli/explain.ts +503 -0
- package/src/cli/mcp.ts +8 -1
- package/src/cli/retrieve.ts +167 -90
- package/src/cli/selection.ts +260 -0
- package/src/cli/settings.ts +286 -0
- package/src/cli/terminal.ts +22 -7
- package/src/cli/transfer.ts +192 -51
- package/src/cli.ts +22 -2
- package/src/core/envelope.ts +5 -2
- package/src/core/error-catalog.ts +95 -0
- package/src/core/fanout.ts +143 -2
- package/src/core/preflight.ts +306 -0
- package/src/core/routeros-string.ts +24 -0
- package/src/devices.ts +212 -20
- package/src/discover.ts +7 -2
- package/src/errors.ts +1 -0
- package/src/execute-fanout.ts +453 -0
- package/src/execute.ts +216 -68
- package/src/explain/arg-tokens.ts +114 -0
- package/src/explain/args.ts +1403 -0
- package/src/explain/blocks.ts +132 -0
- package/src/explain/brace-slots.ts +143 -0
- package/src/explain/brace-tokens.ts +38 -0
- package/src/explain/catalog.ts +1345 -0
- package/src/explain/coordinates.ts +329 -0
- package/src/explain/defects.ts +217 -0
- package/src/explain/is-known-menu.ts +17 -0
- package/src/explain/menus.ts +664 -0
- package/src/explain/operator-tokens.ts +310 -0
- package/src/explain/operators.ts +458 -0
- package/src/explain/path-tokens.ts +150 -0
- package/src/explain/pathresolve.ts +1324 -0
- package/src/explain/quoted-string.ts +241 -0
- package/src/explain/scope-brace.ts +308 -0
- package/src/explain/segment.ts +757 -0
- package/src/explain/string-tokens.ts +67 -0
- package/src/explain/symbol-values.ts +520 -0
- package/src/explain/symbols.ts +1597 -0
- package/src/explain/token-ranges.ts +72 -0
- package/src/explain/transport.ts +497 -0
- package/src/explain/value-tokens.ts +80 -0
- package/src/explain/values.ts +357 -0
- package/src/explain/verbs.ts +64 -0
- package/src/explain/verbsplit.ts +813 -0
- package/src/explain/write.ts +815 -0
- package/src/explain.ts +2152 -0
- package/src/index.ts +307 -1
- package/src/mcp/config.ts +12 -2
- package/src/mcp/tools.ts +137 -2
- package/src/protocols/adapter.ts +38 -18
- package/src/protocols/index.ts +11 -5
- package/src/protocols/mac-telnet-console.ts +61 -17
- package/src/resolver/cdb.ts +10 -2
- package/src/resolver/comment-kv.ts +58 -4
- package/src/resolver/config-file.ts +136 -0
- package/src/resolver/facts.ts +78 -0
- package/src/resolver/geo.ts +453 -0
- package/src/resolver/index.ts +65 -0
- package/src/resolver/mac.ts +12 -3
- package/src/resolver/quickchr-provider.ts +597 -0
- package/src/resolver/selection.ts +391 -0
- package/src/resolver/service-endpoint.ts +155 -0
- package/src/resolver/settings.ts +54 -5
- package/src/resolver/target.ts +21 -3
- package/src/retrieve-fanout.ts +239 -133
- package/src/retrieve.ts +120 -25
- package/src/settings.ts +1078 -0
- package/src/terminal.ts +11 -1
- package/src/transfer-fanout.ts +544 -0
- package/src/transfer.ts +133 -40
package/commands/check/README.md
CHANGED
|
@@ -1,59 +1,382 @@
|
|
|
1
1
|
# check
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
3
|
+
Run a battery of health/diagnostic **checks** against a router and return
|
|
4
|
+
findings (errors / warnings / tips) plus judgment-free, **allowlisted** metadata
|
|
5
|
+
in one envelope — a device *doctor* that also hands later `retrieve` / `execute`
|
|
6
|
+
/ `api` calls the context they'd otherwise splunk for themselves.
|
|
7
|
+
|
|
8
|
+
Status: `designed` over `rest-api`, `native-api`, `ssh`, and `mac-telnet` (the
|
|
9
|
+
execute-capable transports the battery can ride); `snmp` and
|
|
10
|
+
`romon` / `winbox-terminal` stay `not-started`. See `docs/MATRIX.md` for the row.
|
|
11
|
+
Load-bearing rules — envelope, errors, settings precedence, identity, validation,
|
|
12
|
+
protocol selection, fan-out — live in
|
|
13
|
+
[`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md); the `(constitution: …)`
|
|
14
|
+
notes below point there rather than restating them.
|
|
15
|
+
|
|
16
|
+
## What changed (this is a revision, not greenfield)
|
|
17
|
+
|
|
18
|
+
The prior stub scoped `check` as a **reachability probe** — "probe a router and
|
|
19
|
+
report which management paths are usable" — with the health data an afterthought
|
|
20
|
+
behind `--fix`. That reachability probe survives, but as *one phase* of a richer
|
|
21
|
+
command, and the CDB-write flag is now `--save` (it never touches the router; see
|
|
22
|
+
[`--save`](#--save)). The old L2 rationale the stub stated —
|
|
23
|
+
|
|
24
|
+
> L2 probing (mac-telnet reachability / ARP) is **opt-in, off by default** …
|
|
25
|
+
> It needs L2 adjacency and is slower, so IP-level probing (rest/native/ssh)
|
|
26
|
+
> stays the default.
|
|
27
|
+
|
|
28
|
+
is **preserved but re-grounded** (see [L2 timing experiment](#l2-timing-experiment)):
|
|
29
|
+
under the new model mac-telnet is not a parallel "L2 reachability" default — it is
|
|
30
|
+
a *transport the whole battery can ride* (via `execute`) when it is the only way
|
|
31
|
+
in (a bare-MAC target, or IP paths that failed). The opt-in default holds; the
|
|
32
|
+
experiment measures the cost that justifies it.
|
|
33
|
+
|
|
34
|
+
## Where `check` sits
|
|
35
|
+
|
|
36
|
+
The built-in `check` battery is **read-only** — it never writes RouterOS. The
|
|
37
|
+
lone exception is `--check-script*`, a **trusted** escape hatch that *can* mutate
|
|
38
|
+
the router (see [Custom check script](#custom-check-script)); `--save` reconciles
|
|
39
|
+
centrs's own CDB, never the device (see [`--save`](#--save)).
|
|
40
|
+
Think `doctor`: a list of self-describing checks runs, each yields the standard
|
|
41
|
+
finding channels, and configuration lets a caller silence known-and-accepted
|
|
42
|
+
findings to reach a green result. Its probe machinery is deliberately reusable —
|
|
43
|
+
`discover`'s future IP-scan (#149) iterates `check` per host, and the metadata it
|
|
44
|
+
collects seeds TikTOML (#137).
|
|
45
|
+
|
|
46
|
+
## Two phases
|
|
47
|
+
|
|
48
|
+
- **Reach** — TCP reachability per protocol, host→router connect **latency**
|
|
49
|
+
(RTT) captured from that same probe, an auth check on whichever answered, plus
|
|
50
|
+
**ARP presence** as an L2-adjacency fact. Creds are *optional to start*: a
|
|
51
|
+
no-cred `check` still reports reachability + latency and tips that credentials
|
|
52
|
+
are needed for the health phase. When auth fails entirely (or the target is a
|
|
53
|
+
bare MAC), reach enriches the profile **for free** from the MNDP cache —
|
|
54
|
+
advertised identity / version / platform / board / software-id — best-effort on
|
|
55
|
+
L2 adjacency (the codec `discover` already ships). This is the slice a subnet
|
|
56
|
+
sweep wants (`--preset reach`), not the full battery against a /24.
|
|
57
|
+
- **Health** — over the best working transport (constitution: protocol
|
|
58
|
+
selection), run the battery of RouterOS reads → findings + metadata. The reads
|
|
59
|
+
are ordinary `print`/`get` commands, so they ride `retrieve` / `api` on
|
|
60
|
+
rest/native **and** ride `execute` on ssh / mac-telnet — the battery is
|
|
61
|
+
transport-portable, which is exactly what lets mac-telnet serve as a slow
|
|
62
|
+
full-battery path. `resource` runs **first** (see
|
|
63
|
+
[Execution model](#execution-model)).
|
|
64
|
+
|
|
65
|
+
## Checks are modules, not a conditional block
|
|
66
|
+
|
|
67
|
+
Each check is an independent, self-describing unit
|
|
68
|
+
(`{ id, title, description, tags, phase, cost, defaultOn, requires, dependsOn, run }`)
|
|
69
|
+
registered with an orchestrator. That registry is what powers `--list-checks`
|
|
70
|
+
(list **and explain** every check), clean extension (add a module, don't refactor
|
|
71
|
+
the flags), and the **progressive** default: expensive checks run only when a
|
|
72
|
+
cheaper one flags something.
|
|
73
|
+
|
|
74
|
+
- `cpu-detail` runs iff `resource` reported high CPU load.
|
|
75
|
+
- `certificates` runs iff `services` found `www-ssl` disabled or a cert near
|
|
76
|
+
expiry.
|
|
77
|
+
- Opt-off checks (`traffic`, `health`, `packages`, `connections`) never run
|
|
78
|
+
unless named (`--checks`) or pulled in by a preset.
|
|
79
|
+
|
|
80
|
+
"Smart" here is a first cut, expected to be tuned against real devices over time.
|
|
81
|
+
Because slow or hung probes are the main risk to usefulness, every check runs
|
|
82
|
+
under a per-check timeout (`--check-timeout`, default `8s`); a check that exceeds
|
|
83
|
+
it yields a `check/<id>/timed-out` finding and the battery continues with partial
|
|
84
|
+
results rather than hanging.
|
|
85
|
+
|
|
86
|
+
## Execution model
|
|
87
|
+
|
|
88
|
+
The naive "15 checks → 15 calls" plan is fine over rest-api / native-api (calls
|
|
89
|
+
pipeline cheaply) but **excruciating** over ssh / mac-telnet, where every call
|
|
90
|
+
pays session setup. So the orchestrator batches per **round**, not per check:
|
|
91
|
+
|
|
92
|
+
1. **Foundational read.** `resource` (version / arch / board) runs first — its
|
|
93
|
+
output is what the capability pre-flight needs (below).
|
|
94
|
+
2. **Round 1.** All selected static reads are issued together. Over an
|
|
95
|
+
execute-transport centrs composes them into **one consolidated `:put`-JSON
|
|
96
|
+
script** (one `:serialize to=json` object — the `tojson` helper in the
|
|
97
|
+
grounding sample) that returns a single object in one session; over
|
|
98
|
+
rest/native they pipeline as individual calls.
|
|
99
|
+
3. **Round 2 (only if triggered).** Progressive checks (`cpu-detail`,
|
|
100
|
+
`certificates`, …) whose predicate fired are collected and issued as **one**
|
|
101
|
+
additional batched round — never a round-trip per triggered check.
|
|
102
|
+
|
|
103
|
+
This keeps the mac-telnet full battery to ~two sessions instead of ~fifteen, and
|
|
104
|
+
the batched script only pulls **allowlisted fields** (proplist / explicit
|
|
105
|
+
attributes), which also bounds payload and avoids scraping secrets.
|
|
106
|
+
|
|
107
|
+
## Findings, severity, and reaching "green"
|
|
108
|
+
|
|
109
|
+
Each check emits into the standard channels (constitution: result envelope),
|
|
110
|
+
plus a metadata block:
|
|
111
|
+
|
|
112
|
+
| Channel | Meaning for `check` |
|
|
113
|
+
| ---------- | -------------------------------------------------------------------------- |
|
|
114
|
+
| `error` | Blocks the check's purpose (no transport reachable, auth fails everywhere). |
|
|
115
|
+
| `warning` | A real anomaly about *this* device (telnet/ftp enabled, disk < 16 MB free, EOL version, cert expiring, SNMP community open to `0.0.0.0/0` or `::/0`). |
|
|
116
|
+
| `tip` | Advice, not a fault (enable `www-ssl`; reverse-proxy on 443, here's the mapping). |
|
|
117
|
+
| `metadata` | Facts, no verdict (arch, board, version + channel, ddns-name, license level, port map, latency, health sensors). Feeds `data.profile`. |
|
|
118
|
+
|
|
119
|
+
Finding codes are slash-namespaced (constitution: error model):
|
|
120
|
+
`check/<checkId>/<slug>` for errors/warnings, `tip/<checkId>/<slug>` for tips —
|
|
121
|
+
so a caller can suppress one finding, or a whole check's findings, by code.
|
|
122
|
+
|
|
123
|
+
**Verdict, `ok`, and exit code.** `check` computes `data.verdict` ∈ `pass` /
|
|
124
|
+
`tip` / `warn` / `fail` (the max finding severity after suppression). A run that
|
|
125
|
+
reached a transport and executed the battery is **`ok: true`** and always carries
|
|
126
|
+
`data` — an unhealthy device is a *finding*, not a command failure. Only a genuine
|
|
127
|
+
failure (no transport reachable, auth fails on every transport, orchestration
|
|
128
|
+
error) is `ok: false` (`error`, no data), matching the shared envelope.
|
|
129
|
+
|
|
130
|
+
Health gates **the exit code**, derived from the verdict vs `--fail-on` — a
|
|
131
|
+
per-command rule, like fan-out's granular codes:
|
|
132
|
+
|
|
133
|
+
- `--fail-on <error|warning|tip|never>` (default `error`) is the threshold.
|
|
134
|
+
- Exit `0` when the verdict is **below** it (at the default, warnings and tips are
|
|
135
|
+
footnotes → `0`); exit `2` when the run is `ok:true` but the verdict
|
|
136
|
+
**met/exceeded** it (reachable, but unhealthy per the chosen bar); exit `1` on
|
|
137
|
+
an `ok:false` command failure. `--fail-on never` forces exit `0`.
|
|
138
|
+
|
|
139
|
+
`--ignore <code|checkId>` (repeatable) suppresses matching findings so a
|
|
140
|
+
known-and-accepted condition stops counting toward the verdict and stops
|
|
141
|
+
cluttering output; `--ignore <checkId>` mutes a check's *findings* but keeps its
|
|
142
|
+
metadata, while `--skip <checkId>` doesn't run it at all. Suppression follows the
|
|
143
|
+
normal settings precedence (constitution): `--ignore` arg → `CENTRS_CHECK_IGNORE`
|
|
144
|
+
env → per-device CDB `check-ignore=` comment-kv. The CDB override lets a caller
|
|
145
|
+
durably accept findings without re-passing flags (adding `check-ignore=` to the
|
|
146
|
+
comment-kv allowlist is a follow-up against `commands/devices/README.md`).
|
|
147
|
+
|
|
148
|
+
### Metadata is allowlisted, not raw
|
|
149
|
+
|
|
150
|
+
Built-in checks never dump raw `as-value` output into `metadata` — each maps a
|
|
151
|
+
**typed, allowlisted** subset, and credential-class fields are omitted or
|
|
152
|
+
redacted before entering the envelope (and, if an error ever mentions them,
|
|
153
|
+
listed in `redactable_fields`; constitution: error model). Concretely, from the
|
|
154
|
+
grounding sample: `/ip/cloud` keeps `dns-name` / `status` / `back-to-home-vpn`
|
|
155
|
+
but **drops** `vpn-wireguard-client-config`(-`qrcode`) and the VPN keys;
|
|
156
|
+
`/snmp/community` reports version / scope / count and **redacts the community
|
|
157
|
+
string** (it is a credential); `/certificate` keeps names / validity / fingerprint
|
|
158
|
+
and never key material. A custom check (below) owns its own output and is exempt
|
|
159
|
+
from this allowlist — that is part of why it is a trusted, gated path.
|
|
160
|
+
|
|
161
|
+
## Check catalog (first cut)
|
|
162
|
+
|
|
163
|
+
Grounded in the RouterOS command set surveyed for this spec — each row's `reads`
|
|
164
|
+
column names its source command. `default`: `on` runs in the smart battery;
|
|
165
|
+
`trig` runs only when a dependency flags it; `off` runs only when named/preset.
|
|
166
|
+
|
|
167
|
+
| id | phase | default | emits | reads |
|
|
168
|
+
| -------------- | ------ | ------- | -------------- | ---------------------------------- |
|
|
169
|
+
| `reach` | reach | on | error/tip/meta | TCP + auth per protocol; host→router RTT; ARP presence; port map; MNDP-fact enrichment on auth-fail; tips `credentials-needed` when creds are absent |
|
|
170
|
+
| `resource` | health | on¹ | warn/tip/meta | `/system/resource` — version vs long-term, arch/board (**foundational, runs first**), cpu/mem/disk vs limits, disk < 16 MB, short uptime |
|
|
171
|
+
| `services` | health | on | warn/tip/meta | `/ip/service` — ports, `vrf`, `allowed-address`, disabled/invalid, `www-ssl` off, telnet/ftp on, reverse-proxy on 443, **CDB `port=` mismatch** |
|
|
172
|
+
| `cloud` | health | on | tip/meta | `/ip/cloud` — ddns-name, `ddns-enabled=auto` semantics; Back To Home fields may be absent on stock/trial CHR |
|
|
173
|
+
| `dns` | health | on | warn/meta | `/resolve` — warn (not error) if resolution fails |
|
|
174
|
+
| `ipv6` | health | on | tip/meta | `/ipv6/settings` — `disable-ipv6` |
|
|
175
|
+
| `snmp` | health | on | warn/tip/meta | `/snmp/community` — version/scope/count; community string **redacted**; warns on unrestricted enabled read scope (`0.0.0.0/0` or `::/0`) or default `public` with `security=none`; tips toward `snmp-community=` for future `retrieve --via snmp` |
|
|
176
|
+
| `certificates` | health | trig | warn/meta | `/certificate` when `www-ssl` off / near expiry |
|
|
177
|
+
| `cpu-detail` | health | trig | warn/meta | `/system/resource/cpu` when `resource` load is high |
|
|
178
|
+
| `routerboard` | health | on² | tip/meta | `/system/routerboard` — firmware vs routeros; absent on CHR |
|
|
179
|
+
| `license` | health | on³ | warn/meta | `/system/license` — `level`; matters most on CHR |
|
|
180
|
+
| `health` | health | off | meta-only | `/system/health` — variable shape, no verdicts |
|
|
181
|
+
| `packages` | health | off | meta | `/system/package` — installed/available + arch |
|
|
182
|
+
| `console` | health | off | tip/meta | `/system/console` — serial exists → tip when all paths fail |
|
|
183
|
+
| `traffic` | health | off | meta | `/interface/monitor-traffic` — **~10 s**, slow |
|
|
184
|
+
| `connections` | health | off | meta | firewall connection `count-only` |
|
|
185
|
+
|
|
186
|
+
¹ foundational — always runs first. ² on for RouterBOARD hardware, auto-skipped
|
|
187
|
+
on CHR. ³ on for CHR, informational on hardware.
|
|
188
|
+
|
|
189
|
+
`--preset` bundles: `reach` (reach phase only), `health` (reach + on-by-default
|
|
190
|
+
health), `full` (every check including opt-off — slow), and `exposure` — a
|
|
191
|
+
**rollup** over the findings `services` / `certificates` / `snmp` already
|
|
192
|
+
produce. `exposure` reads **no extra paths** and does **no vulnerability or
|
|
193
|
+
misconfiguration scanning**; it is a filtered access-surface view (insecure
|
|
194
|
+
service enabled, weak/expiring cert, open community), named to avoid implying a
|
|
195
|
+
security audit it does not perform. `--tag <t>` filters by tag; `--deep` forces
|
|
196
|
+
triggered/enrichment checks; `--fast` suppresses progressive triggers.
|
|
197
|
+
|
|
198
|
+
## Flags
|
|
199
|
+
|
|
200
|
+
The whole command is spec-tier (`designed` — nothing is implemented yet), so
|
|
201
|
+
this table is the design, not a reference; once `check` lands, its
|
|
202
|
+
implemented flags will be generated into `docs/CLI.md` like every other
|
|
203
|
+
command's.
|
|
204
|
+
|
|
205
|
+
| Flag | Behavior |
|
|
206
|
+
| ---- | -------- |
|
|
207
|
+
| `--preset <name>` | `reach` / `health` (default) / `exposure` / `full`. |
|
|
208
|
+
| `--checks <a,b,c>` | Run exactly these checks (+ their `dependsOn`). |
|
|
209
|
+
| `--skip <a,b,c>` | Run the selection except these. |
|
|
210
|
+
| `--tag <t>` | Restrict to checks carrying tag `t` (repeatable). |
|
|
211
|
+
| `--deep` / `--fast` | Force / suppress progressive (triggered) checks. |
|
|
212
|
+
| `--list-checks` | Print the catalog with descriptions; no network IO. |
|
|
213
|
+
| `--fail-on <severity>` | Verdict threshold that gates the **exit code** (`0`/`2`), not `ok`. Default `error`. |
|
|
214
|
+
| `--ignore <code\|checkId>` | Suppress matching findings (repeatable). |
|
|
215
|
+
| `--check-timeout <dur>` | Per-check timeout. Default `8s`. |
|
|
216
|
+
| `--save` | Reconcile the CDB from what was probed (write-shaped, gated). |
|
|
217
|
+
| `--check-script <str>` / `--check-script-file <file>` | Custom check; both **repeatable** (see below). Requires `--yes`. |
|
|
218
|
+
| `--via <protocol>` | Pin the battery transport. No silent downgrade (constitution). |
|
|
219
|
+
| `--l2` | Opt into L2 (mac-telnet / ARP) probing in the reach phase even when IP paths answer. Off by default — L2 is opt-in (see [L2 timing experiment](#l2-timing-experiment)); also implied by a bare-MAC target or `--via mac-telnet`. |
|
|
220
|
+
| `--group` / `--where` / `--all` / `--default` / `--concurrency` | Fan-out (see below). |
|
|
221
|
+
| target/auth | `--host`, `--port`, `--username`/`-u`, `--password`, `--insecure`, `--timeout`, `--resolve`, `--cdb-file`, `--cdb-password` — same resolver as `retrieve`/`execute`. |
|
|
222
|
+
|
|
223
|
+
## Custom check script
|
|
224
|
+
|
|
225
|
+
The built-ins stay minimal on purpose; `--check-script` / `--check-script-file`
|
|
226
|
+
is the escape hatch for site- or policy-specific checks. Both are **repeatable**,
|
|
227
|
+
so an agent can compose checks from several sources; each is merged as its own
|
|
228
|
+
synthetic check (`check/custom/<n>`). A script must `:put [:serialize to=json …]`
|
|
229
|
+
an envelope-shaped result:
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{ "errors": [], "warnings": [], "tips": [], "metadata": {} }
|
|
47
233
|
```
|
|
48
234
|
|
|
49
|
-
|
|
235
|
+
**Trust and gating.** A custom script runs through `execute`'s script-mode syntax
|
|
236
|
+
gate (`:put [:parse]`; structured commands add inspect, but arbitrary script mode
|
|
237
|
+
does not). execute only classifies *structured* add/set/remove as write-shaped —
|
|
238
|
+
arbitrary **script mode is not caught**, so a custom script *can* mutate RouterOS.
|
|
239
|
+
It is therefore a **trusted** path: centrs requires `--yes` to run any
|
|
240
|
+
`--check-script*` and attaches a `check/custom-script/ran` audit warning to the
|
|
241
|
+
envelope. RouterOS-side ergonomics (outer block for `:local`, appending to arrays,
|
|
242
|
+
dashed-variable quoting, bool-vs-`yes|no` enums) are demonstrated in
|
|
243
|
+
`examples.md`.
|
|
244
|
+
|
|
245
|
+
## Capability pre-flight (ties to #129)
|
|
246
|
+
|
|
247
|
+
RouterOS commands vary by version and arch, so a check must not blow up on a
|
|
248
|
+
device that lacks the path it reads. `resource` runs first and yields the **live**
|
|
249
|
+
version / arch / board; the orchestrator pre-flights each remaining check's
|
|
250
|
+
`requires` paths against that live value (falling back to the stored
|
|
251
|
+
`version` derived fact, or one batched `/console/inspect` when neither is
|
|
252
|
+
conclusive), and marks an unsupported check `ran:false` with
|
|
253
|
+
`skipReason:"unsupported-on-<version>"` — a graceful skip, not a raw protocol
|
|
254
|
+
error. Grounding is offline-first against restraml's historic `inspect.json`
|
|
255
|
+
(`jq`), CHR only to confirm. `check` is the first multi-consumer of the #129
|
|
256
|
+
pre-flight utility (alongside `transfer mkdir`).
|
|
257
|
+
|
|
258
|
+
## `--save`
|
|
259
|
+
|
|
260
|
+
`--save` (was `--fix`; renamed because it never fixes the *router*) is the only
|
|
261
|
+
write-shaped path, and it writes **centrs's CDB, never RouterOS** — matching
|
|
262
|
+
`discover --save`. After the battery authenticates over one transport it
|
|
263
|
+
reconciles the resolved device's record in one pass (through the `devices` write
|
|
264
|
+
layer — the only CDB writer), gated like any write (`--yes` / confirm; `--force`
|
|
265
|
+
for a multi-record selection):
|
|
266
|
+
|
|
267
|
+
- **Connection metadata** — the resolved/`via` transport's `port=` (learned from
|
|
268
|
+
`services`) when non-default, so later calls connect directly. The CDB carries a
|
|
269
|
+
single `port=`, so the *full* per-protocol port map stays in `data.profile.ports`
|
|
270
|
+
(queryable); persisting per-protocol ports needs new `devices` keys — a tracked
|
|
271
|
+
follow-up, not this pass.
|
|
272
|
+
- **Derived facts** — `board`, `version`, `software-id`, `updated=` (the same
|
|
273
|
+
queryable-but-stale keys `devices add/set --check` and `discover --save`
|
|
274
|
+
populate; constitution: identity — derived facts never override a live read).
|
|
275
|
+
- **Newly-learned lookup keys** — `mac=` / `identity=` / `ip=` when the record
|
|
276
|
+
lacks them.
|
|
277
|
+
|
|
278
|
+
**SNMP boundary.** The `snmp` check *reports* a usable community (redacted) and
|
|
279
|
+
*tips* toward storing it — but `--save` does **not** silently persist a community,
|
|
280
|
+
because it is a credential and there may be several. Persisting it is an explicit,
|
|
281
|
+
separate act (`devices set <t> snmp-community=…`, consumed by the future
|
|
282
|
+
`retrieve --via snmp`); whether `--save --with-snmp` should opt into that is an
|
|
283
|
+
open item below.
|
|
284
|
+
|
|
285
|
+
Plain `check` (no `--save`) writes nothing.
|
|
286
|
+
|
|
287
|
+
## Fan-out
|
|
288
|
+
|
|
289
|
+
`check` is a clean per-target pipeline, so it fans out like `retrieve` / `execute`
|
|
290
|
+
/ `api` (constitution: target selection). Any selector (`--group` / `--where` /
|
|
291
|
+
`--all` / `--default`) or more than one positional switches to the locked
|
|
292
|
+
`FanoutData` envelope (`data = { summary, targets[] }`) with the granular
|
|
293
|
+
`0`/`2`/`1` exit code. Because an unhealthy device is now `ok:true` per target,
|
|
294
|
+
the fan-out exit code tracks **orchestration** (inner `ok`), not health — so the
|
|
295
|
+
`summary` aggregates per-target verdicts (a `verdicts` tally) and each target's
|
|
296
|
+
`data.verdict` stays in its inner envelope, which is how #149's IP-scan reads
|
|
297
|
+
per-host health. A multi-target `--save` is a write and needs `--yes`. This
|
|
298
|
+
per-host fan-out is exactly the machinery `discover`'s IP-scan (#149) will drive.
|
|
299
|
+
|
|
300
|
+
## Output
|
|
301
|
+
|
|
302
|
+
Standard envelope (constitution: result envelope). `data` has three parts:
|
|
303
|
+
|
|
304
|
+
- `data.verdict` — `pass` / `tip` / `warn` / `fail`, the max finding severity after
|
|
305
|
+
suppression (gates the **exit code** via `--fail-on`; `ok` stays `true` when the
|
|
306
|
+
command ran).
|
|
307
|
+
- `data.checks[]` — per-check detail: `{ id, ran, skipReason?, verdict, findings[], metadata, timing }`.
|
|
308
|
+
- `data.profile` — a consolidated, flat **device profile** (board, arch, version,
|
|
309
|
+
channel, identity, ddns-name, license-level, `ports{}`, latency, reachable
|
|
310
|
+
transports …) assembled from the allowlisted check metadata. This is the
|
|
311
|
+
reusable "device-type" surface for #149 and #137. **Its field set is emitted
|
|
312
|
+
best-effort now but is not yet normative** — TikTOML (#137) / IP-scan (#149)
|
|
313
|
+
will freeze the schema; until then treat `data.profile` as additive and don't
|
|
314
|
+
hard-code its shape.
|
|
315
|
+
|
|
316
|
+
Check-level warnings/tips also roll up into the top envelope's `warnings[]` /
|
|
317
|
+
`tips[]` so a consumer that ignores `data.checks[]` still sees them.
|
|
318
|
+
|
|
319
|
+
## L2 timing experiment
|
|
320
|
+
|
|
321
|
+
Required evidence for this spec (issue #136), re-scoped to the new model and
|
|
322
|
+
**run** on a real CHR via `@tikoci/quickchr` (mac-telnet uses the `socket-connect`
|
|
323
|
+
host-side L2 bridge that `discover` / mac-telnet cells already use — see
|
|
324
|
+
`commands/discover/README.md`, L2 validation policy):
|
|
325
|
+
|
|
326
|
+
- **Measured:** mac-telnet session setup + a representative health-battery round
|
|
327
|
+
(the consolidated `resource` + `services` script) vs the same battery over
|
|
328
|
+
rest-api / native-api on the same CHR.
|
|
329
|
+
- **Decided:** whether L2 is cheap enough to belong in the *default* reach sweep,
|
|
330
|
+
or stays opt-in (enabled by a MAC target, `--via mac-telnet`, or `--l2`).
|
|
331
|
+
|
|
332
|
+
Findings, run 2026-07-02 against quickchr CHR `7.23.1 (stable)` on local macOS
|
|
333
|
+
x86_64. Each row is five measured samples after one warm-up; each sample creates
|
|
334
|
+
a fresh adapter/session, matching the per-target battery cost.
|
|
335
|
+
|
|
336
|
+
| Path | Median | Samples |
|
|
337
|
+
| ---- | ------ | ------- |
|
|
338
|
+
| rest-api direct `resource` + `services` reads | 4 ms | 4, 4, 4, 4, 4 |
|
|
339
|
+
| native-api direct `resource` + `services` reads | 129 ms | 128, 130, 129, 131, 129 |
|
|
340
|
+
| rest-api consolidated execute script | 3 ms | 3, 3, 3, 3, 3 |
|
|
341
|
+
| native-api consolidated execute script | 130 ms | 130, 131, 131, 121, 118 |
|
|
342
|
+
| mac-telnet consolidated execute script | 10,888 ms | 10,888, 10,885, 10,932, 10,880, 10,895 |
|
|
343
|
+
|
|
344
|
+
Verdict: preserve L2 as **opt-in**. mac-telnet is useful as a rescue/full-battery
|
|
345
|
+
transport when it is the only way in, but a single consolidated session is ~10.9s;
|
|
346
|
+
a two-round progressive battery is roughly double that. That is not acceptable
|
|
347
|
+
as a default per-host cost for #149 subnet sweeps.
|
|
348
|
+
|
|
349
|
+
## Definition of done
|
|
350
|
+
|
|
351
|
+
`designed` on the strength of this README (intent + flags). A protocol cell
|
|
352
|
+
advances to `CHR-passed` only when every example in `examples.md` runs green
|
|
353
|
+
against a real CHR through `bun run test:integration`. See
|
|
354
|
+
[`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md) for the full done rule.
|
|
50
355
|
|
|
51
|
-
|
|
52
|
-
derived facts (`board`/`version`/`software-id`/`updated=`), refreshed in one
|
|
53
|
-
authenticated pass through the `devices` write layer; always explicit opt-in,
|
|
54
|
-
never implicit. Plain `check` stays a read-only probe that writes nothing.
|
|
55
|
-
- L2 probing opt-in **decided** (Intent above): off by default; enabled by a MAC
|
|
56
|
-
target, `--via mac-telnet`, or `--l2`.
|
|
356
|
+
## Open questions / decisions made this round
|
|
57
357
|
|
|
58
|
-
|
|
59
|
-
|
|
358
|
+
- **Default breadth = smart battery** (reach + on-by-default health +
|
|
359
|
+
progressive), biased against slow/hang via `--check-timeout`. "Smart"
|
|
360
|
+
triggering thresholds are a first cut to tune against devices.
|
|
361
|
+
- **Red-verdict model** = `ok:true` with `data` retained; the health verdict
|
|
362
|
+
(`data.verdict`) gates the **exit code** (`0` pass / `2` unhealthy / `1` command
|
|
363
|
+
failure), like fan-out's granular codes. No envelope carve-out — the shared
|
|
364
|
+
discriminated-union envelope is unchanged.
|
|
365
|
+
- **Reach latency = host→router RTT** (+ ARP presence); router→internet health is
|
|
366
|
+
the `dns` check's job.
|
|
367
|
+
- **`exposure` (not `security`) is a rollup preset** — no extra reads, no
|
|
368
|
+
vulnerability/misconfiguration scanning; named honestly.
|
|
369
|
+
- **`--save` (not `--fix`)** — CDB-only, consistent with `discover --save`.
|
|
370
|
+
- **Metadata is allowlisted + secrets redacted**; batched execution over
|
|
371
|
+
execute-transports; `resource` foundational-first for capability gating; custom
|
|
372
|
+
scripts repeatable + `--yes`-gated + audited.
|
|
373
|
+
- **Suppression persists** per-device (CDB `check-ignore=`) and via env.
|
|
374
|
+
- **SNMP open threshold** includes unrestricted enabled read scopes
|
|
375
|
+
(`0.0.0.0/0` or `::/0`); stock CHR 7.23.1 exposes enabled `public` with
|
|
376
|
+
`security=none` and `addresses=::/0`, so that counts as a warning.
|
|
377
|
+
- **`data.profile` shape not yet normative** — deferred to #137/#149.
|
|
378
|
+
- **Naming is provisional** — flag/check/preset names here are for the concept;
|
|
379
|
+
rationalize holistically before code.
|
|
380
|
+
- **Still open:** whether `--save` should grow `--with-snmp` to persist a
|
|
381
|
+
community as `snmp-community=` comment-kv (and adding that key to the `devices`
|
|
382
|
+
allowlist); whether `--save` should also capture `ddns-name`.
|