@tikoci/centrs 0.1.3 → 0.1.6

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 (107) hide show
  1. package/CHANGELOG.md +348 -0
  2. package/GLOSSARY.txt +252 -0
  3. package/LICENSE +21 -0
  4. package/README.md +44 -12
  5. package/commands/AGENTS.md +14 -5
  6. package/commands/api/AGENTS.md +13 -1
  7. package/commands/api/README.md +56 -26
  8. package/commands/api/examples.md +46 -1
  9. package/commands/btest/README.md +21 -32
  10. package/commands/check/README.md +6 -1
  11. package/commands/discover/README.md +5 -11
  12. package/commands/execute/README.md +77 -36
  13. package/commands/execute/examples.md +120 -10
  14. package/commands/explain/README.md +2756 -0
  15. package/commands/explain/examples.md +702 -0
  16. package/commands/mcp/README.md +7 -4
  17. package/commands/mcp/examples.md +2 -2
  18. package/commands/retrieve/README.md +25 -27
  19. package/commands/retrieve/examples.md +16 -0
  20. package/commands/terminal/examples.md +7 -1
  21. package/commands/transfer/README.md +28 -20
  22. package/commands/transfer/examples.md +29 -0
  23. package/docs/CLI.md +353 -0
  24. package/docs/CONSTITUTION.md +138 -30
  25. package/docs/MATRIX.md +92 -18
  26. package/docs/errors/README.md +3 -0
  27. package/docs/errors/quickchr/machine-not-found.md +22 -0
  28. package/docs/errors/quickchr/machine-stopped.md +20 -0
  29. package/docs/errors/quickchr/package-unavailable.md +27 -0
  30. package/docs/errors/quickchr/unsupported-via.md +35 -0
  31. package/docs/errors/quickchr/unsupported.md +35 -0
  32. package/docs/errors/routeros/version-unsupported.md +26 -0
  33. package/docs/errors/transport/provider-trust.md +29 -0
  34. package/docs/errors/usage/stdin-ignored.md +28 -0
  35. package/docs/errors/validation/syntax.md +29 -4
  36. package/docs/errors/validation/unknown-attribute.md +37 -4
  37. package/package.json +56 -10
  38. package/src/api-fanout.ts +46 -18
  39. package/src/api.ts +351 -121
  40. package/src/cli/api.ts +26 -7
  41. package/src/cli/btest.ts +19 -3
  42. package/src/cli/execute.ts +25 -6
  43. package/src/cli/explain.ts +503 -0
  44. package/src/cli/retrieve.ts +33 -1
  45. package/src/cli/selection.ts +71 -11
  46. package/src/cli/settings.ts +2 -2
  47. package/src/cli/terminal.ts +1 -1
  48. package/src/cli/transfer.ts +23 -6
  49. package/src/cli.ts +15 -2
  50. package/src/core/envelope.ts +58 -2
  51. package/src/core/error-catalog.ts +42 -1
  52. package/src/core/execute-command.ts +148 -0
  53. package/src/core/inspect.ts +36 -4
  54. package/src/core/preflight.ts +306 -0
  55. package/src/core/routeros-string.ts +24 -0
  56. package/src/core/version.ts +4 -0
  57. package/src/core/yaml.ts +86 -0
  58. package/src/errors.ts +1 -0
  59. package/src/execute-fanout.ts +47 -18
  60. package/src/execute.ts +339 -307
  61. package/src/explain/arg-tokens.ts +151 -0
  62. package/src/explain/args.ts +1749 -0
  63. package/src/explain/blocks.ts +175 -0
  64. package/src/explain/brace-slots.ts +143 -0
  65. package/src/explain/brace-tokens.ts +39 -0
  66. package/src/explain/catalog.ts +1343 -0
  67. package/src/explain/coordinates.ts +488 -0
  68. package/src/explain/defects.ts +218 -0
  69. package/src/explain/escape-tokens.ts +77 -0
  70. package/src/explain/highlight-projection.ts +308 -0
  71. package/src/explain/is-known-menu.ts +17 -0
  72. package/src/explain/menus.ts +668 -0
  73. package/src/explain/operator-tokens.ts +316 -0
  74. package/src/explain/operators.ts +465 -0
  75. package/src/explain/path-tokens.ts +150 -0
  76. package/src/explain/pathresolve.ts +1569 -0
  77. package/src/explain/quoted-string.ts +331 -0
  78. package/src/explain/scope-brace.ts +461 -0
  79. package/src/explain/segment.ts +1165 -0
  80. package/src/explain/separator.ts +187 -0
  81. package/src/explain/split-owner.ts +194 -0
  82. package/src/explain/string-tokens.ts +77 -0
  83. package/src/explain/symbol-values.ts +556 -0
  84. package/src/explain/symbols.ts +1617 -0
  85. package/src/explain/token-ranges.ts +72 -0
  86. package/src/explain/transport.ts +514 -0
  87. package/src/explain/value-tokens.ts +83 -0
  88. package/src/explain/values.ts +357 -0
  89. package/src/explain/verbs.ts +64 -0
  90. package/src/explain/verbsplit.ts +955 -0
  91. package/src/explain/write.ts +856 -0
  92. package/src/explain.ts +2602 -0
  93. package/src/index.ts +225 -6
  94. package/src/mcp/server.ts +2 -1
  95. package/src/offline-gate.ts +148 -0
  96. package/src/protocols/mac-telnet-console.ts +61 -17
  97. package/src/resolver/facts.ts +1 -1
  98. package/src/resolver/index.ts +23 -4
  99. package/src/resolver/quickchr-provider.ts +597 -0
  100. package/src/resolver/selection.ts +99 -12
  101. package/src/resolver/service-endpoint.ts +155 -0
  102. package/src/resolver/target.ts +1 -1
  103. package/src/retrieve-fanout.ts +27 -13
  104. package/src/retrieve.ts +82 -77
  105. package/src/settings.ts +2 -1
  106. package/src/transfer-fanout.ts +43 -26
  107. package/src/transfer.ts +88 -45
@@ -155,7 +155,23 @@ centrs api $R execute -X POST -f script=':put [/system/identity/get name]' --use
155
155
  ```
156
156
 
157
157
  Envelope: `ok: true`, `data` is string-shaped and contains the CHR identity;
158
- `meta.validation.semantic=not-applicable` (script, not a path).
158
+ `meta.validation.semantic=not-applicable` (script, not a path) and
159
+ `meta.validation.stages` is `offline: passed` then `device: skipped`.
160
+
161
+ ### 16b. The `/execute` script is gated offline (GH#354)
162
+
163
+ Until GH#354 the script-mode carve-out meant this surface ran **no** preflight at
164
+ all. The offline analyzer is the whole stage-1 gate here, and it opens no
165
+ connection — the unreachable port proves it.
166
+
167
+ ```bash
168
+ centrs api 127.0.0.1:1 execute -X POST -f script=':put [' --username $U --password $P --yes
169
+ ```
170
+
171
+ Envelope: `ok: false`, `error.code=validation/syntax`,
172
+ `error.context.validationStage="offline"`, `error.context.surface="api /execute"`,
173
+ `error.context.span={"start":5,"end":6}`, and **not** a `transport/*` code.
174
+ `meta.validation.stages` is `offline: failed` then `device: skipped`.
159
175
 
160
176
  ### 17. `--via rest-api --listen` is rejected
161
177
 
@@ -209,6 +225,20 @@ proplist). Combining an id with `--query`/`--proplist` folds the id into the RES
209
225
  matching the native `?.id=` read. (Plain id-only reads still use the `GET …/$ID`
210
226
  URL form; see example 5's re-read.)
211
227
 
228
+ ### 22. `--raw --validate=true` still runs the gate (#154)
229
+
230
+ ```bash
231
+ centrs api $R ip/no-such-menu --raw --validate=true --username $U --password $P
232
+ ```
233
+
234
+ `--raw` only **defaults** `--validate` to false, so an explicit `--validate=true`
235
+ wins and the `/console/inspect` preflight runs. The rejection comes back in the
236
+ `--raw` error shape, not the envelope: stdout empty, stderr a compact
237
+ `{"code":"validation/unknown-path","message":…}`, nonzero exit, and no `data`
238
+ key. Contrast example 13, where bare `--raw` skips the preflight entirely. This
239
+ pair is how you tell a RouterOS rejection from a centrs bug while staying on the
240
+ bare-passthrough surface.
241
+
212
242
  ## native-api (`--via native-api`)
213
243
 
214
244
  The same contract over the binary API. Validation still runs through
@@ -426,3 +456,18 @@ The reserved record is a credential fallback, not a connectable router, so its o
426
456
  target fails deterministically: inner `ok:false` with
427
457
  `error.code=target/unresolved` (never a `transport/dns` attempt on `"__default__"`).
428
458
  `data.summary = { total: 1, ok: 0, failed: 1 }`. Exit code 1 (every target failed).
459
+
460
+ ## quickchr targets (#134)
461
+
462
+ `$NAME` is the machine name of a running quickchr-managed CHR. Host/port/auth
463
+ come from the live descriptor (see `commands/retrieve/examples.md` Q-series).
464
+ Covered by `test/integration/quickchr-target.test.ts`.
465
+
466
+ ### Q1. GET against a quickchr target
467
+
468
+ ```bash
469
+ centrs api --quickchr $NAME system/resource --json
470
+ ```
471
+
472
+ Envelope: `ok: true`, `data` is the resource object,
473
+ `meta.target.source.kind=provider`.
@@ -80,38 +80,27 @@ centrs btest server [--authenticate[=false]] [--user U] [--password P] \
80
80
 
81
81
  ## Flags
82
82
 
83
- ### `btest client <router>`
84
-
85
- | Flag | RouterOS analog / default | Notes |
86
- | ---- | ------------------------- | ----- |
87
- | `--protocol <udp\|tcp>` | `protocol`, default `udp` | UDP measures raw throughput + loss; TCP measures goodput. |
88
- | `--direction <receive\|transmit\|both>` | `direction`, default `receive` | `receive` = download (server→client), `transmit` = upload. |
89
- | `--duration <dur>` | `duration` | Bounds the run; reuses `parseDuration`. Omit for open-ended (Ctrl-C). |
90
- | `--interval <dur>` | `interval`, default `1s` | Report cadence (`20ms`..`5s`). One report frame per interval. |
91
- | `--connection-count <n>` | `connection-count` (1..255) | **TCP only.** Parallel TCP data connections. centrs opens the negotiated extra connections (join format grounded against RouterOS 7.23.1) so throughput aggregates across streams. **Authenticated (EC-SRP5) sessions stay single-stream** — the post-auth token is not captured — and emit a `routeros/btest-connection-count-single-stream` warning. |
92
- | `--local-udp-tx-size <n>` | `local-udp-tx-size` (28..64000) | **UDP only.** Client→server packet size. |
93
- | `--remote-udp-tx-size <n>` | `remote-udp-tx-size` (28..64000) | **UDP only.** Server→client packet size. |
94
- | `--local-tx-speed <bps>` | `local-tx-speed` | Cap on client→server rate (bits/sec). |
95
- | `--remote-tx-speed <bps>` | `remote-tx-speed` | Cap on server→client rate (bits/sec). |
96
- | `--random-data` | `random-data`, default off | Incompressible payload (defeats link compression; CPU-heavier). |
97
- | `--user` / `--password` | `user` / `password` | Fall back to CDB / `CENTRS_*`. Required when the server has `authenticate=yes`. |
98
- | `--port <n>` | — (default `2000`) | Control port override. |
99
- | `--format <text\|csv\|json\|yaml>` | — (default `text`) | `text` = live human reports; `csv` = streaming CSV records; `json`/`yaml` = final summary envelope. |
100
- | `--csv` | — | Shortcut for `--format csv`. |
101
-
102
- ### `btest server`
103
-
104
- | Flag | RouterOS analog / default | Notes |
105
- | ---- | ------------------------- | ----- |
106
- | `--authenticate[=false]` | `authenticate`, **default `true`** | Require EC-SRP5 auth; `--authenticate=false` accepts anonymous clients. |
107
- | `--user` / `--password` | — | The accepted credential when `authenticate=true` (v1: one user). Falls back to `CENTRS_*`. |
108
- | `--bind <addr>` | **centrs-only** | Listen address; **default `127.0.0.1`**. Use `0.0.0.0` to expose on the LAN. |
109
- | `--port <n>` | — (default `2000`) | Control-connection port. |
110
- | `--allocate-udp-ports-from <n>` | `allocate-udp-ports-from` | Base of the UDP data-port range; default `2001` (just above the TCP/2000 control port). |
111
- | `--max-sessions <n>` | `max-sessions` (1..1000), default `100` | Concurrent test cap; further clients get `routeros/btest-too-many-sessions`. |
112
- | `--duration <dur>` | — | Optional auto-stop; otherwise runs until Ctrl-C. |
113
- | `--format <text\|csv\|json\|yaml>` | — (default `text`) | `text` = human session log; `csv` = streaming CSV records; `json`/`yaml` = final summary envelope. |
114
- | `--csv` | — | Shortcut for `--format csv`. |
83
+ Implemented flags are generated from the CLI metadata into
84
+ [`docs/CLI.md` → btest](../../docs/CLI.md#btest) — one table for both
85
+ subcommands, with `(client)`/`(server)` markers scoping each flag; "did you
86
+ mean?" hints are scoped to the active mode. This file does not duplicate that
87
+ table. Behavior notes the generated reference cannot carry:
88
+
89
+ - Flag names mirror the RouterOS analogs verbatim (`protocol`, `direction`,
90
+ `local-udp-tx-size`, `connection-count`, `authenticate`,
91
+ `allocate-udp-ports-from`, …); centrs-only additions are `--bind`, `--port`,
92
+ the shared `--format`/`--csv`, and the CDB credential flags.
93
+ - `--direction receive` = download (server→client), `transmit` = upload.
94
+ - `--connection-count` is TCP-only; **authenticated (EC-SRP5) sessions stay
95
+ single-stream** — the post-auth token is not captured — and emit a
96
+ `routeros/btest-connection-count-single-stream` warning (see Open
97
+ questions).
98
+ - `--user`/`--password` fall back to CDB / `CENTRS_*`; required when the
99
+ server has `authenticate=yes`. On the server they are the accepted
100
+ credential (v1: one user).
101
+ - Option ranges (`28..64000` UDP sizes, `1..255` connections, `20ms..5s`
102
+ interval, `1..1000` sessions) are validated before any socket opens (see
103
+ Errors).
115
104
 
116
105
  ## How it works
117
106
 
@@ -197,6 +197,11 @@ triggered/enrichment checks; `--fast` suppresses progressive triggers.
197
197
 
198
198
  ## Flags
199
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
+
200
205
  | Flag | Behavior |
201
206
  | ---- | -------- |
202
207
  | `--preset <name>` | `reach` / `health` (default) / `exposure` / `full`. |
@@ -243,7 +248,7 @@ RouterOS commands vary by version and arch, so a check must not blow up on a
243
248
  device that lacks the path it reads. `resource` runs first and yields the **live**
244
249
  version / arch / board; the orchestrator pre-flights each remaining check's
245
250
  `requires` paths against that live value (falling back to the stored
246
- `routeros_version` derived fact, or one batched `/console/inspect` when neither is
251
+ `version` derived fact, or one batched `/console/inspect` when neither is
247
252
  conclusive), and marks an unsupported check `ran:false` with
248
253
  `skipReason:"unsupported-on-<version>"` — a graceful skip, not a raw protocol
249
254
  error. Grounding is offline-first against restraml's historic `inspect.json`
@@ -53,17 +53,11 @@ tested without a router (crafted packet fixtures + a loopback socket).
53
53
 
54
54
  ## Flags and defaults
55
55
 
56
- | Flag | Default | Meaning |
57
- | ---------------- | ------------- | ---------------------------------------------------- |
58
- | `--timeout` | `15s` | Listen window before results are returned. First refresh is sent immediately. |
59
- | `--save` | off | Persist discovered neighbors into the CDB. |
60
- | `--group` | `discovered` | First-class CDB group assigned to saved entries. |
61
- | `--port` | `5678` | UDP port to bind for MNDP. |
62
- | `--cdb-file` | resolved CDB | CDB path override for `--save`. |
63
- | `--cdb-password` | env/none | Decrypt an encrypted CDB; also used to re-encrypt on `--save`. |
64
- | `--format` | `text` | `text`, `json`, or `yaml`; renders the same envelope. |
65
-
66
- `--save` without `--timeout` uses the default 15s window.
56
+ Implemented flags (with their defaults) are generated from the CLI metadata
57
+ into [`docs/CLI.md` → discover](../../docs/CLI.md#discover); this file does
58
+ not duplicate that table. The first refresh is sent immediately, so the
59
+ `--timeout` listen window (default 15s) bounds the wait, and `--save` without
60
+ `--timeout` uses the same default window.
67
61
 
68
62
  Bare `centrs discover` is **read-only** — it returns the envelope and never
69
63
  writes the (WinBox-shared) CDB; `--save` is the explicit opt-in. When it finds
@@ -16,11 +16,26 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
16
16
  - Mirror RouterOS `execute` semantics. Input is a CLI string; output is
17
17
  console-shaped text (or structured records for path+verb writes) wrapped in
18
18
  the standard envelope. This includes write-shaped add/set/remove.
19
- - Validation is two-stage (see constitution: validation): `:put [:parse
20
- "<cmd>"]` is a **syntax** gate only — it does not catch unknown attributes or
21
- bad values. Semantic validation needs `/console/inspect` or the server's own
22
- re-validation on the write round-trip. A clean parse is necessary, not
23
- sufficient.
19
+ - Validation runs **offline first, then on the device** (see constitution:
20
+ validation; GH#354). Stage 1 is the corpus-gated analyzer over the exact
21
+ command string, with no connection: a syntax fault is rejected with a byte
22
+ span in `error.context.span` and the adapter is never dialed, and
23
+ `meta.validation.stages[]` then shows the device stage `skipped`. Stage 1
24
+ cannot see an unknown attribute — it has no per-menu schema — so stage 2 is
25
+ not optional.
26
+ - Stage 2 is itself two gates (grounded on CHR 7.23.3,
27
+ GH#230): a single `:put [:parse "<cmd>"]` covers both the syntax and the
28
+ unknown-attribute (name-level) gate on **every** transport — `:parse` never
29
+ throws; its return value's printed form (`(evl …)` / `(evl bad parameter …)` /
30
+ `(<%% bad command name …)`) is read from the REST/native `ret` *and* the
31
+ console output. That check is necessary, not sufficient: value-type errors pass
32
+ it, and the `/console/inspect request=child` / server re-validation semantic
33
+ checks are separate. Command-level `request=completion` is deliberately not
34
+ used: it hangs REST and closes native API on RouterOS 7.12.2 (GH#343), while
35
+ `child` returns the complete argument-name set immediately.
36
+ The outer RouterOS string literal escapes backslash, quote, and dollar so
37
+ validation parses the caller's exact command rather than expanding variables
38
+ in the wrapper.
24
39
  - centrs owns the **script-vs-structured gate** (`canonicalizeExecuteCommand` +
25
40
  `isWriteShaped` in `src/execute.ts`): it decides which validation runs and
26
41
  whether the write-confirm prompt fires. The shared `rosetta`/`lsp-routeros-ts`
@@ -67,13 +82,33 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
67
82
  terminal width, so each transport handles width differently. **mac-telnet** is
68
83
  the only one that negotiates a size: `src/protocols/mac-telnet-console.ts`
69
84
  answers the device's terminal-size probe with a deliberately tall/wide screen,
70
- so captured columns are neither wrapped nor paginated. **SSH execute** runs
71
- *without* a pseudo-tty (`src/protocols/ssh.ts`), so RouterOS emits no
72
- terminal-wrapped output to begin with — there is nothing to negotiate.
73
- **REST/native-api** are not consoles, so terminal width never applies (they
74
- return structured records, or a string `ret` for script-mode). There is
75
- therefore no per-call `--width` knob today — an explicit width override is
76
- deferred until a concrete need appears.
85
+ so captured columns are neither paginated nor wrapped *at the negotiated
86
+ width*. **SSH execute** runs *without* a pseudo-tty (`src/protocols/ssh.ts`),
87
+ so no width is negotiated at all. **REST/native-api** are not consoles, so a
88
+ terminal width never applies (they return structured records, or a string
89
+ `ret` for script-mode). There is therefore no per-call `--width` knob today —
90
+ an explicit width override is deferred until a concrete need appears.
91
+ - **A device-side wrap that no width negotiation reaches (GH#352).** Declaring a
92
+ width is not sufficient, because RouterOS can wrap for reasons that are not
93
+ about width. On 7.23.6 / 7.24.3 / 7.25beta4 a settings menu with exactly one
94
+ property prints its value **one character per line**: `/system/identity/print`
95
+ returns `name: C` / `H` / `R` where 7.24.2 returns `name: CHR`. Measured on
96
+ stock CHR, the wrap is identical over REST `POST /rest/execute`, over
97
+ `ssh user@host "<cmd>"`, and over the mac-telnet console — including the
98
+ mac-telnet reader that has already answered the size probe with 512 columns,
99
+ and the ssh client that negotiates nothing. The console login modifiers
100
+ (`+c512w200h`, `+ct`, `+e`) do not change it, and it is not proportional to
101
+ the value's length. Every multi-property `print` is unaffected on the same
102
+ build, as is `:put [/system/identity/get name]`.
103
+
104
+ Consequence for this command: `data.ret` carries the device's console text
105
+ **verbatim**, wrap included — centrs cannot tell a layout newline from
106
+ content, and inventing a de-wrapper would corrupt legitimate output. Callers
107
+ that need a value rather than a rendering should read it structurally
108
+ (`retrieve`, or `api`) or ask the device for the scalar
109
+ (`:put [<menu>/get <property>]`). The integration examples compare identities
110
+ with `print` column wrapping collapsed for this reason — see
111
+ `collapsePrintWrapping` in `test/integration/chr.ts`.
77
112
  - Attribute values pass through **verbatim** — centrs does not guess types or
78
113
  coerce them (RouterOS REST tolerates string values; the native API is
79
114
  all-strings). Unlike `jo`-style KV builders, there is no per-value
@@ -102,30 +137,31 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
102
137
 
103
138
  ## Flags
104
139
 
105
- | Flag | Behavior |
140
+ Implemented flags are generated from the CLI metadata into
141
+ [`docs/CLI.md` → execute](../../docs/CLI.md#execute); this file does not
142
+ duplicate that table. Behavior notes the generated reference cannot carry:
143
+
144
+ - `--validate` is transport-appropriate but now one gate on every transport —
145
+ a single `:put [:parse ...]` that is read as a value (GH#230): REST/native
146
+ read the `ret` text (HTTP 200 / `as-string`); mac-telnet and ssh read the
147
+ console's printed value.
148
+ - `--format` defaults to `text`; `CENTRS_FORMAT` sets the default.
149
+ - `--quickchr` resolves host/port/auth from the live `@tikoci/quickchr`
150
+ (0.4.5+, optional dependency) descriptor, bypassing CDB/env resolution for
151
+ those fields.
152
+ - `--` — use it when the command contains tokens that would otherwise be
153
+ claimed as centrs flags; otherwise the command must be quoted as a single
154
+ argument. In fan-out, positional targets come BEFORE `--` and the command
155
+ AFTER it (see **Target selection**).
156
+
157
+ ### Designed, not implemented
158
+
159
+ Spec-tier flags with no implementation yet — today they fail as unknown flags:
160
+
161
+ | Flag | Designed behavior |
106
162
  | ------------------------------- | ------------------------------------------------------------------------------------- |
107
- | `--via <protocol>` | Pin the transport (`native-api`, `rest-api`, `ssh`, or `mac-telnet`). No silent downgrade. A bare MAC target defaults to `mac-telnet`. |
108
- | `--host <host\|url>` | Override the resolved host or base URL for the target. |
109
- | `--port <n>` | Override the resolved management port. |
110
- | `--username` (alias `--user`, `-u`) / `--password` | RouterOS credentials; fall back to `CENTRS_USERNAME` / `CENTRS_PASSWORD`. |
111
- | `--ssh-key <path>` | `--via ssh`: explicit private-key path. Falls back to `CENTRS_SSH_KEY` / the ssh-agent. |
112
- | `--insecure` | Accept a new SSH host key (`--via ssh`) or a self-signed `api-ssl` TLS cert. Default verifies. |
113
- | `--cdb-file` / `--cdb-password` | Read target credentials from (and decrypt) a WinBox CDB file. |
114
- | `--resolve <none\|arp>` | Resolve a MAC-address target to an IP via the host ARP cache. Default `none`. |
115
- | `--timeout <duration>` | Per-request timeout. REST: ≤ 60s; other transports may allow longer. |
116
- | `--validate[=false]` | Run transport-appropriate preflight validation before execution (default `true`): REST/native use `:parse` + `/console/inspect`; mac-telnet and ssh use a single console `:parse` (covers syntax + unknown-attribute). |
117
163
  | `--strict` | Treat any warning on a successful run as an error (`ok: false`, nonzero exit), like `-Werror`. Default is lenient (`ok: true` + `warnings`). |
118
- | `--yes` | Confirm write-shaped add/set/remove commands in non-interactive runs. |
119
- | `--max-bytes <n>` | Byte budget for the rendered envelope; excess output is truncated with a warning + `meta.truncated` (not an error), matching `retrieve`. (Renamed from `--max-results`.) |
120
- | `--format <text\|json\|yaml>` | Output format (alias `--json`). Defaults to `text`; `CENTRS_FORMAT` sets the default. |
121
- | `--group <name>` | Run across every CDB record in WinBox group `<name>`. Repeatable, combinable with `<target>` positionals plus `--where`/`--all`/`--default`; de-duped by record index. See **Target selection**. |
122
- | `--where <attr>=<value>` | Device-class selector — run across every CDB record whose stored fact/comment-kv matches (e.g. `--where board=RB5009 /system/package/check-for-updates`). Repeatable (AND). See constitution: target selection. |
123
- | `--all` | Run across every CDB record (excludes the reserved `__default__`). See **Target selection**. |
124
- | `--default` | Select the reserved `__default__` record. See **Target selection**. |
125
- | `--near <lat>,<lon>,<radius>` | Geo selector — run across CDB records whose GPS is within the great-circle radius (units `m`/`km`/`mi`/`ft`; bare = km). Lat-first. Union predicate; geo-less devices never match. See **Target selection**. |
126
- | `--bbox <south>,<west>,<north>,<east>` | Geo selector — run across CDB records whose GPS is inside the lat-first bounding box. Union predicate. See **Target selection**. |
127
- | `--concurrency <n>` | Max in-flight targets during fan-out (integer ≥ 1). Transport-aware default: `rest-api` 8, others 4. |
128
- | `--` | End-of-options marker. Every token after `--` is taken as the literal RouterOS command, even flag-shaped ones — e.g. `centrs execute $R -- /interface print where disabled=yes`. In fan-out, positional targets come BEFORE `--` and the command AFTER it (`centrs execute r1 r2 -- /system/reboot`). Use it when the command contains tokens that would otherwise be claimed as centrs flags; otherwise the command must be quoted as a single argument. |
164
+ | `--max-bytes <n>` | Byte budget for the rendered envelope; excess output is truncated with a warning + `meta.truncated` (not an error), matching `retrieve`. (Will replace the implemented `--max-results`, which fails instead of truncating.) |
129
165
 
130
166
  ## SSH key selection
131
167
 
@@ -174,6 +210,11 @@ many tokens):
174
210
  - `execute r1 /system/resource/print` — a single positional target with no
175
211
  selector stays **single-target** (legacy split: first positional is the target,
176
212
  the rest is the command), never `FanoutData`.
213
+ - `execute --quickchr lab /system/resource/print` — the quickchr
214
+ named-live-provider claims the target slots like a selector (positionals are
215
+ the command; no `--` needed). One `--quickchr` stays single-target; repeating
216
+ it fans out; mixing it with positionals or CDB selectors is
217
+ `usage/conflicting-flags` (constitution: resolution providers).
177
218
 
178
219
  Because `execute` is the primary **write** path, a write-shaped fan-out
179
220
  (add/set/remove) is gated by **`--yes`**, confirmed **once** up front; without it
@@ -213,7 +254,7 @@ How it is wired:
213
254
  `transport/capability-unsupported` (mac-telnet is execute/terminal only).
214
255
  - **Validation is one console `:parse`.** Over the console, `:put [:parse "<cmd>"]`
215
256
  prints both `syntax error` and `bad parameter <name>`, so a single gate covers
216
- syntax **and** the unknown-attribute (semantic) check — no `/console/inspect`
257
+ syntax **and** the unknown-attribute (name-level) check — no `/console/inspect`
217
258
  table parsing. Maps to `validation/syntax` / `validation/unknown-attribute`,
218
259
  same codes as REST/native (see `docs/CONSTITUTION.md`, validation).
219
260
  - **Auth (grounded):** MTWEI is required; a stock device offers a 16-byte MD5 salt
@@ -243,7 +284,7 @@ Protocol selection: an unresolved MAC target auto-selects mac-telnet (covered by
243
284
  **decided:** `--strict` promotes any warning on an otherwise-successful run to
244
285
  `ok: false` (nonzero exit), like `-Werror`; the default stays lenient
245
286
  (`ok: true` with the warning in the `warnings` channel). Tracked code
246
- follow-up; the flag is in the table above.
287
+ follow-up; the flag is under **Designed, not implemented** above.
247
288
 
248
289
  These are deferred refinements to the already-grounded single-shot envelope,
249
290
  not blockers for the `rest-api`/`native-api` cells.
@@ -62,16 +62,18 @@ The command is a script-shaped console expression, not a slash path plus verb,
62
62
  so REST must use the `/rest/execute` fallback.
63
63
 
64
64
  ```bash
65
- centrs execute $R ':put [/system/identity/get name]' --via rest-api --username $U --password $P
65
+ centrs execute $R ':local a true; :if ($a = true) do={:put [/system/identity/get name]}' --via rest-api --username $U --password $P
66
66
  ```
67
67
 
68
68
  Envelope: `ok: true`, `data` is string-shaped (`data.output` or `data.ret`) and
69
69
  contains the CHR identity name, `meta.via=rest-api`, and
70
70
  `meta.validation.syntax=true`. `meta.validation.semantic` is absent or marked
71
71
  `not-applicable` because `/console/inspect` validates path-shaped commands, not
72
- arbitrary script expressions.
72
+ arbitrary script expressions. The `$a` variable must survive the validation
73
+ wrapper: if that outer string expands it before `:parse`, this example fails even
74
+ though RouterOS accepts the original command.
73
75
 
74
- ### 5. Syntax reject from `:put [:parse ...]`
76
+ ### 5. Syntax reject before the write round-trip
75
77
 
76
78
  Malformed CLI is rejected by the syntax gate before semantic validation or the
77
79
  write round-trip.
@@ -80,9 +82,16 @@ write round-trip.
80
82
  centrs execute $R '/ip/address/add address="unterminated interface=ether1' --via rest-api --username $U --password $P --yes
81
83
  ```
82
84
 
83
- Envelope: `ok: false`, `error.code=validation/syntax`, `error.cause` preserves
84
- the RouterOS parse error string from `:put [:parse ...]`, `meta.via=rest-api`,
85
- and no address is added.
85
+ Envelope: `ok: false`, `error.code=validation/syntax`, `meta.via=rest-api`, and
86
+ no address is added.
87
+
88
+ **Which stage rejects it changed in GH#354.** This input is now caught by the
89
+ *offline* stage, so `error.context.validationStage="offline"`,
90
+ `error.cause="explain/canonicalizer/unterminated-string"`,
91
+ `error.context.span={"start":24,"end":54}`, and the device is never contacted —
92
+ see V1–V3 below. It reached `:put [:parse ...]` before that. The code, the
93
+ `meta.via`, and the no-mutation guarantee are unchanged, which is why the
94
+ assertion in `test/integration/execute.test.ts` still reads `validation/syntax`.
86
95
 
87
96
  ### 6. Semantic reject that `:parse` accepts
88
97
 
@@ -95,7 +104,7 @@ centrs execute $R '/ip/address/add address=198.51.100.11/32 interface=ether1 no-
95
104
  ```
96
105
 
97
106
  Envelope: `ok: false`, `error.code=validation/unknown-attribute`,
98
- `error.cause.attribute="no-such-arg"` (or equivalent structured field),
107
+ `error.context.parameter="no-such-arg"`,
99
108
  `meta.validation.syntax=true`, `meta.validation.source=/console/inspect`, and
100
109
  no address is added. This example is the product claim: syntax validation and
101
110
  semantic validation are separate gates.
@@ -200,14 +209,16 @@ centrs execute $A "/ip/address/remove numbers=$ID" --via native-api --port $API_
200
209
  Envelope: `ok: true`, `meta.via=native-api`. A subsequent retrieve of
201
210
  `/ip/address` does not contain `$ID`.
202
211
 
203
- ### 15. Native syntax reject from `:parse`
212
+ ### 15. Native syntax reject
204
213
 
205
214
  ```bash
206
215
  centrs execute $A '/ip/address/add address="unterminated interface=ether1' --via native-api --port $API_PORT --username $U --password $P --yes
207
216
  ```
208
217
 
209
- Envelope: `ok: false`, `error.code=validation/syntax`, `error.cause` preserves
210
- the RouterOS parse error string, `meta.via=native-api`, and no address is added.
218
+ Envelope: `ok: false`, `error.code=validation/syntax`, `meta.via=native-api`, and
219
+ no address is added. The native twin of example 5, and it moved the same way:
220
+ the offline stage rejects this input now, so the verdict no longer depends on
221
+ the transport at all.
211
222
 
212
223
  ### 16. Native semantic reject after clean parse
213
224
 
@@ -283,6 +294,10 @@ Envelope: `ok: true`, `meta.via=mac-telnet`, `data.ret` is the console output an
283
294
  contains the device identity (cross-checked against REST). `meta.validation.source`
284
295
  is `:put [:parse ...] over mac-telnet`.
285
296
 
297
+ RouterOS 7.23.6/7.24.3/7.25beta4 print a one-property menu's value one
298
+ character per line (GH#352), so the comparison collapses `print` column
299
+ wrapping on both sides rather than asserting the device's layout.
300
+
286
301
  ### 21. Write (add) over mac-telnet
287
302
 
288
303
  ```bash
@@ -327,6 +342,10 @@ Envelope: `ok: true`, `meta.via=ssh`, `data.ret` is the cleaned console output a
327
342
  contains the device identity (cross-checked against REST). `meta.validation.source`
328
343
  is `:put [:parse ...] over ssh`.
329
344
 
345
+ RouterOS 7.23.6/7.24.3/7.25beta4 print a one-property menu's value one
346
+ character per line (GH#352), so the comparison collapses `print` column
347
+ wrapping on both sides rather than asserting the device's layout.
348
+
330
349
  ### S2. Multi-line read returns cleaned, column-aligned output
331
350
 
332
351
  ```bash
@@ -354,6 +373,61 @@ centrs execute 127.0.0.1 '/ip/address/add address=198.51.100.51/32 interface=eth
354
373
  Envelope: `ok: false`, `error.code=validation/unknown-attribute`, `meta.via=ssh`,
355
374
  and no address is added — the console `:parse` gate catches `bogus` pre-mutation.
356
375
 
376
+ ## Offline gate (stage 1)
377
+
378
+ Validation runs offline before it runs on the device
379
+ ([`docs/CONSTITUTION.md` → Validation is the product](../../docs/CONSTITUTION.md#validation-is-the-product),
380
+ GH#354). These two prove the stage exists and that a stage-1 rejection reaches no
381
+ router. Run by `test/integration/execute.test.ts`.
382
+
383
+ ### V1. Offline reject, with the device stage never asked
384
+
385
+ `:put [` is an unclosed bracket. The old local quote preflight counted quotes
386
+ only, so this used to cost a full round trip to be told no; the analyzer rejects
387
+ it with a byte span before anything is dialed.
388
+
389
+ ```bash
390
+ centrs execute $R ':put [' --via rest-api --username $U --password $P --json
391
+ ```
392
+
393
+ Envelope: `ok: false`, `error.code=validation/syntax`,
394
+ `error.context.validationStage="offline"`,
395
+ `error.context.span={"start":5,"end":6}`,
396
+ `error.context.diagnostics[0].code="explain/canonicalizer/unclosed"`,
397
+ `error.position` absent (the offline byte offset is never dressed up as a
398
+ RouterOS-reported one), `meta.validation.source` is the offline analyzer, and
399
+ `meta.validation.stages` is `offline: failed` followed by `device: skipped`.
400
+
401
+ ### V2. The same rejection with no reachable router at all
402
+
403
+ The proof that stage 1 opens no connection: point the same command at a port
404
+ nothing is listening on. A gate that dialed first would report a
405
+ `transport/*` error or hang until the timeout.
406
+
407
+ ```bash
408
+ centrs execute 127.0.0.1:1 ':put [' --via rest-api --username $U --password $P --json
409
+ ```
410
+
411
+ Envelope: identical to V1 — `ok: false`, `error.code=validation/syntax`,
412
+ `error.context.validationStage="offline"` — and **not** a `transport/*` code.
413
+
414
+ ### V3. `--validate=false` disables the offline stage too
415
+
416
+ One flag, one meaning. The escape hatch has to reach stage 1, or a caller hitting
417
+ an analyzer defect would have no way past it. RouterOS then rejects the command
418
+ itself on the run.
419
+
420
+ ```bash
421
+ centrs execute $R ':put [' --via rest-api --username $U --password $P --json --validate=false
422
+ ```
423
+
424
+ Envelope: `ok: false`, `error.code=routeros/request-failed` with RouterOS's own
425
+ `syntax error (line 1 column 7)` in the summary — the **device** rejecting it, not
426
+ centrs — plus `meta.validation.enabled=false`, `meta.validation.result="skipped"`,
427
+ and `meta.validation.stages` listing **both** stages as `skipped`. A gated
428
+ surface always reports every stage, so a consumer reading `stages` never has to
429
+ special-case the disabled shape.
430
+
357
431
  ## Target selection (fan-out)
358
432
 
359
433
  These exercise the shared target-selection grammar
@@ -435,3 +509,39 @@ Envelope: `meta.via=mac-telnet` when `$MAC` is not resolved from CDB and no
435
509
  `--host`/`--port`). If the caller wants IP-level REST or native API execution,
436
510
  they must opt into MAC-to-IP resolution (`--resolve arp` + `--via …`) before
437
511
  protocol selection.
512
+
513
+ ## quickchr targets (#134)
514
+
515
+ `$NAME` is the machine name of a running quickchr-managed CHR. Host/port/auth
516
+ come from the live descriptor — the CDB, `CENTRS_HOST`-style env, and the
517
+ `__default__` ladder are bypassed for those fields. Covered by
518
+ `test/integration/quickchr-target.test.ts`.
519
+
520
+ ### Q1. Run a command on a quickchr target (native API default)
521
+
522
+ ```bash
523
+ centrs execute --quickchr $NAME ':put [/system/identity/get name]' --json
524
+ ```
525
+
526
+ Envelope: `ok: true`, `meta.via=native-api` (execute's normal default),
527
+ `meta.target.source.kind=provider`, `meta.target.identity=$NAME`.
528
+
529
+ ### Q2. A `--via` the descriptor does not forward → `quickchr/unsupported-via`
530
+
531
+ ```bash
532
+ centrs execute --quickchr $NAME ':put 1' --via mac-telnet --json
533
+ ```
534
+
535
+ Envelope: `ok: false`, `error.code=quickchr/unsupported-via` — quickchr v1
536
+ forwards `rest-api`/`native-api`/`ssh` only; centrs never falls back to another
537
+ transport.
538
+
539
+ ### Q3. Repeated `--quickchr` fans out; an unknown machine is an inner failure
540
+
541
+ ```bash
542
+ centrs execute --quickchr $NAME --quickchr no-such-machine -- ':put 1' --json
543
+ ```
544
+
545
+ Envelope: outer `ok: true`, `data.summary = { total: 2, ok: 1, failed: 1 }`;
546
+ the unknown machine's inner error is `quickchr/machine-not-found`. Exit code 2
547
+ (partial).