@tikoci/centrs 0.1.9-next.24 → 0.1.9
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 +121 -6
- package/GLOSSARY.txt +9 -1
- package/README.md +43 -29
- package/commands/api/AGENTS.md +3 -2
- package/commands/api/README.md +8 -1
- package/commands/btest/README.md +9 -0
- package/commands/btest/examples.md +7 -0
- package/commands/devices/README.md +160 -30
- package/commands/devices/examples.md +168 -2
- package/commands/execute/README.md +13 -2
- package/commands/explain/README.md +223 -3
- package/commands/explain/examples.md +8 -1
- package/commands/retrieve/README.md +24 -3
- package/commands/retrieve/examples.md +84 -0
- package/commands/settings/README.md +6 -0
- package/commands/settings/examples.md +17 -0
- package/commands/transfer/README.md +25 -0
- package/commands/transfer/examples.md +46 -0
- package/docs/CLI.md +53 -11
- package/docs/MATRIX.md +5 -3
- package/docs/errors/cdb/encrypt-failed.md +10 -0
- package/docs/errors/cdb/password-required.md +5 -4
- package/docs/errors/cdb/plaintext-backups.md +11 -0
- package/docs/errors/input/empty-stdin.md +27 -0
- package/docs/errors/input/invalid-routeros-path.md +25 -3
- package/docs/errors/input/invalid-stdin.md +28 -0
- package/docs/errors/usage/stdin-is-tty.md +21 -0
- package/package.json +11 -7
- package/src/api-fanout.ts +2 -0
- package/src/api.ts +12 -0
- package/src/btest.ts +7 -0
- package/src/cli/api.ts +19 -2
- package/src/cli/btest.ts +9 -0
- package/src/cli/common.ts +1 -0
- package/src/cli/devices.ts +248 -15
- package/src/cli/execute.ts +12 -3
- package/src/cli/retrieve.ts +9 -0
- package/src/cli/settings.ts +28 -6
- package/src/cli/terminal.ts +9 -0
- package/src/cli/transfer.ts +10 -1
- package/src/core/error-catalog.ts +28 -2
- package/src/core/menu-path.ts +89 -0
- package/src/devices.ts +378 -33
- package/src/execute-fanout.ts +2 -0
- package/src/execute.ts +7 -0
- package/src/explain/blocks.ts +2 -2
- package/src/explain/catalog.ts +22 -12
- package/src/explain/pasted-prompt.ts +198 -0
- package/src/explain/pathresolve.ts +2 -2
- package/src/explain/segment.ts +2 -2
- package/src/explain/symbols.ts +2 -2
- package/src/explain/verbsplit.ts +2 -2
- package/src/explain/write.ts +2 -2
- package/src/explain.ts +2 -0
- package/src/index.ts +5 -0
- package/src/mcp/safety.ts +7 -6
- package/src/mcp/tools.ts +30 -68
- package/src/protocols/adapter.ts +2 -0
- package/src/protocols/btest-session.ts +10 -30
- package/src/protocols/ec-srp5.ts +19 -10
- package/src/protocols/mac-telnet-console.ts +2 -2
- package/src/resolver/cdb.ts +66 -9
- package/src/resolver/index.ts +1 -0
- package/src/resolver/quickchr-provider.ts +5 -0
- package/src/resolver/selection.ts +15 -4
- package/src/retrieve-fanout.ts +2 -0
- package/src/retrieve.ts +11 -0
- package/src/settings.ts +24 -8
- package/src/terminal.ts +7 -0
- package/src/transfer-fanout.ts +2 -0
- package/src/transfer.ts +79 -14
|
@@ -257,6 +257,22 @@ today's path/verb/args split and script-vs-structured gate:
|
|
|
257
257
|
`/interface/wifi/dashboard` and `/system/dashboard` are `inspect` only: the
|
|
258
258
|
trees carry them, the manual publishes no menu entry for them.
|
|
259
259
|
|
|
260
|
+
**Source refresh (accepted 2026-10-30).** The CLI Reference added
|
|
261
|
+
`compare-versions`, a version-comparison widget page with no `**Type:**`
|
|
262
|
+
entry. `llms.txt` lists it as a page and the sitemap serves it as a category
|
|
263
|
+
URL, so it aborted the parser and would have aborted the orphan-dir check.
|
|
264
|
+
It joins `index` as a hand-audited non-command slug, excluded from both
|
|
265
|
+
inventories. The CLI Reference moves from 1,099 pages / 1,106 entries to
|
|
266
|
+
1,109 / 1,116; the inspect pins and the 636-menu union are unchanged. The
|
|
267
|
+
catalog grows from 1,166 to 1,176 paths: 1,005 `both`, 67 `inspect`, 104
|
|
268
|
+
`published`. It adds 10 paths and removes none, with no kind change and no
|
|
269
|
+
provenance loss. The additions are `/app/settings/remove-certificate`, three
|
|
270
|
+
`/system/resource/hardware` commands (`power-cycle`, `power-reset`,
|
|
271
|
+
`reset-overcurrent`), and `/interface/wifi/easymesh` with five children. The
|
|
272
|
+
three `/cmr/client` gate strings drop `!powerpc`. easymesh is published with
|
|
273
|
+
no gate and is in no CHR tree through 7.26beta3, so the unexplained residue
|
|
274
|
+
grows from `/root/terminal` alone to seven paths.
|
|
275
|
+
|
|
260
276
|
There are real unknowns in how far offline parsing can go (expression
|
|
261
277
|
grammar, scope fidelity vs `:parse`, `[]`-nesting corner cases). Grounding
|
|
262
278
|
those unknowns was **phase 0** (#185); its findings are folded in throughout
|
|
@@ -3007,6 +3023,202 @@ a process boundary. A B5 rename is therefore a visible break here rather than a
|
|
|
3007
3023
|
silent one — which is the point of settling the vocabulary before an external
|
|
3008
3024
|
consumer commits to it.
|
|
3009
3025
|
|
|
3026
|
+
## Topic checks and related live reads (#493, design — no implementation)
|
|
3027
|
+
|
|
3028
|
+
This section is the bounded design #493 asks for. It proposes shapes, ownership,
|
|
3029
|
+
and acceptance gates; **no topic check, special-command rule, or contextual
|
|
3030
|
+
read ships with it**. Each check needs its own accepted/rejected CHR controls
|
|
3031
|
+
and committed evidence before an implementation slice lands, and every new
|
|
3032
|
+
RouterOS semantic claim is grounded then — not here. The three examples below
|
|
3033
|
+
are **SYNTHETIC shape illustrations, not device claims**: they show the envelope
|
|
3034
|
+
a check would produce, with invented content, so reviewers can judge the
|
|
3035
|
+
contract without mistaking illustration for evidence.
|
|
3036
|
+
|
|
3037
|
+
**Ownership: one rule table, three consumers.** Fact and check definitions live
|
|
3038
|
+
in one module beside the analyzer (sibling of `src/explain/write.ts`, which is
|
|
3039
|
+
the precedent: an explain-only inference over the shipped structure). The
|
|
3040
|
+
CLI renders it, the library returns it, and the MCP adapter reshapes the same
|
|
3041
|
+
object — no copies of rule logic per surface. Checks evaluate against the
|
|
3042
|
+
**statement tree** (`structure.statements[]` ids plus `parent`/`owner` links),
|
|
3043
|
+
never against byte containment: a finding anchors to a statement id and its
|
|
3044
|
+
span, so a reordered document moves the finding with its statement.
|
|
3045
|
+
|
|
3046
|
+
**Findings reuse the shipped envelope — no new finding object.** A check emits
|
|
3047
|
+
what explain already emits: an `ExplainDiagnostic` (`code`, `severity`,
|
|
3048
|
+
`message`, `span`, `ev`) for the finding, the `ExplainEvidence` entry it cites
|
|
3049
|
+
(`id`, `source`, `probe`, `basis`, `outcome`, `statementIds`) for how it was
|
|
3050
|
+
decided and which statement it anchors to, and a `tip` (`code`, `message`,
|
|
3051
|
+
`fix`) for the next step. Codes follow `explain/<source>/<slug>`, with the
|
|
3052
|
+
check family as the slug prefix (`topic-…`, `special-command-…`). Topic
|
|
3053
|
+
findings are `info`: like the pasted-prompt tip, advice never moves
|
|
3054
|
+
`data.verdict`, which stays the validity answer. A check that should raise the
|
|
3055
|
+
verdict is a separate, explicitly decided change, not a default. Certainty is
|
|
3056
|
+
the cited evidence's `basis` — `heuristic` for a ratified rule over the
|
|
3057
|
+
offline structure (the `containsWrite` precedent), `derived` once it rests on
|
|
3058
|
+
live responses — so how sure a finding is reads from the same field as every
|
|
3059
|
+
other explain fact.
|
|
3060
|
+
|
|
3061
|
+
**What is decided where.** Source-only: structural facts the canonicalizer
|
|
3062
|
+
already proves (unresolved statements, write-shape, symbol bindings). Live
|
|
3063
|
+
schema: what a menu accepts (argument names, enums) — only `--schema` /
|
|
3064
|
+
`--complete` against a named router, never a bundled snapshot (decision 3).
|
|
3065
|
+
Device observations: current state (`print` rows) — only through an explicitly
|
|
3066
|
+
requested contextual read (below). Partial script input is never treated as
|
|
3067
|
+
final device state, and a finding never grants execution permission: advice
|
|
3068
|
+
and the execution gate are separate answers, and a lost read response is
|
|
3069
|
+
reported as a failed read, never as a successful execution.
|
|
3070
|
+
|
|
3071
|
+
**Contextual reads are opt-in and bounded.** The default offline entry stays
|
|
3072
|
+
network-free; naming a router does not authorize reads. A contextual read
|
|
3073
|
+
fires only under an explicit flag (name TBD, e.g. `--with-print`), one bounded
|
|
3074
|
+
`print` per confidently resolved menu, projected and truncated, each reported
|
|
3075
|
+
independently with its read command, time, projection, and truncation — so an
|
|
3076
|
+
agent can distinguish current observed state from the proposed script. The
|
|
3077
|
+
target is the envelope's `meta.target`, not a per-read field. Deduplication is by (statement id, resolved path): two
|
|
3078
|
+
statements in one menu share one read, and a failed read fails alone. The
|
|
3079
|
+
read is a validated centrs `retrieve` on the resolved path — never the user's
|
|
3080
|
+
`[find]`, expression, or analyzed script executed to get context.
|
|
3081
|
+
|
|
3082
|
+
**Secrets follow the existing redaction contract.** Related reads ride the
|
|
3083
|
+
redaction paths from #359: secret-bearing menus and verbose file output are
|
|
3084
|
+
high-risk surfaces, so a contextual read defaults to a non-sensitive
|
|
3085
|
+
projection, names its projection explicitly, and never exports key material
|
|
3086
|
+
into advice artifacts or logs. A check whose finding would need a sensitive
|
|
3087
|
+
field states that need in its definition and is refused without an explicit
|
|
3088
|
+
sensitivity opt-in — no automatic sensitive exports.
|
|
3089
|
+
|
|
3090
|
+
**Shape 1 — a grounded topic finding (synthetic illustration).**
|
|
3091
|
+
Input `SYNTHETIC-1` (illustrative only):
|
|
3092
|
+
`/ip service set telnet disabled=no` — a statement the analyzer resolves with
|
|
3093
|
+
id `s0`, span 0–34. A management-plane topic check (definition owned beside
|
|
3094
|
+
the analyzer, evaluated against `s0`) adds one diagnostic, the evidence entry
|
|
3095
|
+
it cites, and a tip; the ids and the check's `probe` name are invented. A
|
|
3096
|
+
partial envelope — only the fields the finding adds or reads are shown:
|
|
3097
|
+
|
|
3098
|
+
```json
|
|
3099
|
+
{
|
|
3100
|
+
"data": {
|
|
3101
|
+
"verdict": "pass",
|
|
3102
|
+
"diagnostics": [
|
|
3103
|
+
{
|
|
3104
|
+
"code": "explain/canonicalizer/topic-plaintext-management-service",
|
|
3105
|
+
"severity": "info",
|
|
3106
|
+
"message": "This enables telnet, a plaintext management service.",
|
|
3107
|
+
"span": { "start": 0, "end": 34 },
|
|
3108
|
+
"ev": "e10"
|
|
3109
|
+
}
|
|
3110
|
+
],
|
|
3111
|
+
"evidence": [
|
|
3112
|
+
{
|
|
3113
|
+
"id": "e10",
|
|
3114
|
+
"source": "canonicalizer",
|
|
3115
|
+
"probe": "topicPlaintextManagementService",
|
|
3116
|
+
"basis": "heuristic",
|
|
3117
|
+
"outcome": "ok",
|
|
3118
|
+
"statementIds": ["s0"]
|
|
3119
|
+
}
|
|
3120
|
+
]
|
|
3121
|
+
},
|
|
3122
|
+
"tips": [
|
|
3123
|
+
{
|
|
3124
|
+
"code": "tip/explain-topic-plaintext-management-service",
|
|
3125
|
+
"message": "Telnet sends credentials in clear text.",
|
|
3126
|
+
"fix": "Disable the plaintext service or restrict it to a management address list."
|
|
3127
|
+
}
|
|
3128
|
+
]
|
|
3129
|
+
}
|
|
3130
|
+
```
|
|
3131
|
+
|
|
3132
|
+
The verdict stays `pass`; probe outcome and `runtimeAcceptance` are untouched —
|
|
3133
|
+
the finding does not claim the router accepted anything.
|
|
3134
|
+
|
|
3135
|
+
**Shape 2 — a special-command fact (synthetic illustration).** #363 records
|
|
3136
|
+
the real cases (deferred there, not duplicated here); the shape is Shape 1's,
|
|
3137
|
+
with the `special-command-` slug family and a consequence, not a
|
|
3138
|
+
configuration judgement. Illustrative only:
|
|
3139
|
+
|
|
3140
|
+
```json
|
|
3141
|
+
{
|
|
3142
|
+
"code": "explain/canonicalizer/special-command-connection-loss",
|
|
3143
|
+
"severity": "info",
|
|
3144
|
+
"message": "This command can drop the session that runs it.",
|
|
3145
|
+
"span": { "start": 64, "end": 79 },
|
|
3146
|
+
"ev": "e11"
|
|
3147
|
+
}
|
|
3148
|
+
```
|
|
3149
|
+
|
|
3150
|
+
`e11` cites `statementIds: ["s2"]`, and the paired tip's `fix` says to run it
|
|
3151
|
+
from a path that survives the change.
|
|
3152
|
+
|
|
3153
|
+
No isolated device-mode table ships first (the #363 deferral stands); a
|
|
3154
|
+
special-command fact names its consequence and its evidence, nothing more.
|
|
3155
|
+
|
|
3156
|
+
**Shape 3 — an explicitly requested contextual read (synthetic illustration).**
|
|
3157
|
+
Input (illustrative): `/ip address add address=198.51.100.1/32 interface=ether1`
|
|
3158
|
+
with an explicit `--with-print`-style opt-in. The read follows the live
|
|
3159
|
+
argument-name precedent (`data.live.arguments[]` cites a `live-inspect`
|
|
3160
|
+
evidence entry): one evidence entry per read, and the rows in `data.live`
|
|
3161
|
+
citing it, independent of the analysis verdict. A partial `data` object —
|
|
3162
|
+
`evidence` is `data.evidence`, `live` is `data.live`:
|
|
3163
|
+
|
|
3164
|
+
```json
|
|
3165
|
+
{
|
|
3166
|
+
"evidence": [
|
|
3167
|
+
{
|
|
3168
|
+
"id": "e12",
|
|
3169
|
+
"source": "live-read",
|
|
3170
|
+
"probe": "print",
|
|
3171
|
+
"basis": "direct",
|
|
3172
|
+
"outcome": "ok",
|
|
3173
|
+
"version": "SYNTHETIC-version",
|
|
3174
|
+
"observedAt": "SYNTHETIC-timestamp",
|
|
3175
|
+
"statementIds": ["s0"],
|
|
3176
|
+
"path": "/ip/address",
|
|
3177
|
+
"projection": ["address", "interface", "disabled"],
|
|
3178
|
+
"truncation": { "returned": 3, "total": 12 }
|
|
3179
|
+
}
|
|
3180
|
+
],
|
|
3181
|
+
"live": {
|
|
3182
|
+
"reads": [
|
|
3183
|
+
{
|
|
3184
|
+
"ev": "e12",
|
|
3185
|
+
"path": "/ip/address",
|
|
3186
|
+
"statementIds": ["s0"],
|
|
3187
|
+
"rows": [
|
|
3188
|
+
{ "address": "192.0.2.1/24", "interface": "ether1", "disabled": "false" },
|
|
3189
|
+
{ "address": "198.51.100.2/24", "interface": "ether2", "disabled": "false" },
|
|
3190
|
+
{ "address": "203.0.113.1/24", "interface": "bridge", "disabled": "true" }
|
|
3191
|
+
]
|
|
3192
|
+
}
|
|
3193
|
+
]
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
```
|
|
3197
|
+
|
|
3198
|
+
The target is the envelope's existing `meta.target`, not a per-read field.
|
|
3199
|
+
Three additions are proposed, not shipped: the `live-read` evidence source,
|
|
3200
|
+
the `projection` and `truncation` evidence fields, and `data.live.reads[]`.
|
|
3201
|
+
Originating statement and path, time, projection, and truncation are all
|
|
3202
|
+
present, so observed state cannot be mistaken for the proposed script. A
|
|
3203
|
+
failed read reports its `outcome` with `reason`/`errorCode` and fails alone.
|
|
3204
|
+
|
|
3205
|
+
**Acceptance controls and dependencies before implementation slices.**
|
|
3206
|
+
|
|
3207
|
+
- Each check ships with accepted/rejected CHR controls and committed evidence;
|
|
3208
|
+
synthetic shapes above are replaced by measured rows, never promoted as-is.
|
|
3209
|
+
- Controls assert statement-id anchoring (finding moves with its statement),
|
|
3210
|
+
verdict independence (a finding leaves `data.verdict` unchanged), the cited
|
|
3211
|
+
evidence's `basis`, and read independence (one failed read does not fail
|
|
3212
|
+
the analysis or sibling reads).
|
|
3213
|
+
- A check that needs a sensitive field is refused without the sensitivity
|
|
3214
|
+
opt-in; the redaction contract is asserted, not assumed.
|
|
3215
|
+
- Dependencies: published offline readings stay honest (#211, #350, #351,
|
|
3216
|
+
#358, #371); live path/argument/probe facts come from #236; live value
|
|
3217
|
+
types are distinct evidence (#225); the MCP result migration is separate
|
|
3218
|
+
(#223); text stdout/stderr routing is unchanged (#477); device-mode
|
|
3219
|
+
provisioning stays in #95. No new plugin DSL or framework is preselected —
|
|
3220
|
+
existing `warnings`/`tips` and explain diagnostics are reused where they fit.
|
|
3221
|
+
|
|
3010
3222
|
## Non-goals
|
|
3011
3223
|
|
|
3012
3224
|
- No LSP server in centrs — the LSP is a consumer the library must support,
|
|
@@ -3272,14 +3484,22 @@ a missing mandatory value, and a name collision. That was verified rather than
|
|
|
3272
3484
|
assumed — each blocker statement was replayed through `:put [:parse …]` and
|
|
3273
3485
|
`/console/inspect request=highlight` on both versions and **accepted every
|
|
3274
3486
|
time**, with controls (`no-such-attr=`, an unterminated string, a nonexistent
|
|
3275
|
-
menu) correctly rejected
|
|
3487
|
+
menu) correctly rejected by `:parse`. `highlight` is the weaker of the two: it
|
|
3488
|
+
marks only the nonexistent menu with an `error` class, reading the bad
|
|
3489
|
+
attribute as `obj-inactive` and the unterminated string as ordinary tokens, so
|
|
3490
|
+
the controls' rejection rests on `:parse`. So these results carry **no** implication about
|
|
3276
3491
|
whether `explain` can parse the forms, and they do not license claims about the
|
|
3277
3492
|
export styles in general: the captures come from one synthetic configuration on
|
|
3278
3493
|
CHR, which has a single NIC and no switch or wireless hardware, so its defconf
|
|
3279
3494
|
and its available menus differ from any real device's. Treat the provenance
|
|
3280
3495
|
label as **L1-emitted, L2-refuted for these captures on CHR**. Harness and
|
|
3281
|
-
per-document results:
|
|
3282
|
-
|
|
3496
|
+
per-document results: the q203 import-verify lab (history — see #203; its
|
|
3497
|
+
scripts and corpora were never promoted, so the counts above rest on those
|
|
3498
|
+
lab captures). Validity check: `bun run explain:probe:import-blocker-validity`
|
|
3499
|
+
(promoted from the q203 lab for #272; replays the four blockers plus the
|
|
3500
|
+
three rejecting controls through `:parse` and `highlight` on a disposable
|
|
3501
|
+
CHR per version; re-run on 7.23.2 and 7.24rc2 for #568, 6/6 and 7/7 rows
|
|
3502
|
+
agree).
|
|
3283
3503
|
|
|
3284
3504
|
**Non-blocking / deferred (not ratification-gating):** Q5 expression depth, Q7
|
|
3285
3505
|
tokenizer corners, Q9 potential-command taxonomy, Q12 span-vocabulary draft —
|
|
@@ -888,7 +888,14 @@ centrs explain '[admin@MikroTik] > /ip address print' --json
|
|
|
888
888
|
`data.verdict` is `fail` with `explain/canonicalizer/invalid-at-sign` at the
|
|
889
889
|
`@`. The device answers `syntax error (line 1 column 7)`, the same byte. This
|
|
890
890
|
is a pasted console prompt. `:put a@b` and `comment=a@b` pass, because a `@` is
|
|
891
|
-
a value byte in an argument.
|
|
891
|
+
a value byte in an argument. The envelope also carries
|
|
892
|
+
`tip/explain-pasted-prompt` offering the stripped `/ip address print` as
|
|
893
|
+
repair text (never applied). A `/menu>` prompt such as
|
|
894
|
+
`/interface/lte> monitor lte1` keeps a `warn` abstention with the same tip
|
|
895
|
+
shape, offering `monitor lte1` typed at `/interface/lte`. The manual's
|
|
896
|
+
in-menu prompt `[admin@MikroTik] /ip/address> add …` fails the same way and
|
|
897
|
+
offers `add …` typed at `/ip/address`; a HotLock `>>` is stripped with the
|
|
898
|
+
prompt.
|
|
892
899
|
|
|
893
900
|
### 41. A named comparison brace inside a `[find where …]` is rejected
|
|
894
901
|
|
|
@@ -96,6 +96,23 @@ error code, so its reason is `not reached: interrupted` (#537). With
|
|
|
96
96
|
If `--all-attributes` is combined with `--attribute(s)`, fail with
|
|
97
97
|
`usage/conflicting-flags` *before* hitting the network.
|
|
98
98
|
|
|
99
|
+
A read only ever dispatches a `print`/`get`, on every transport and even with
|
|
100
|
+
`--validate=false`: that flag skips command validation, not the read/write
|
|
101
|
+
split (#586). Each path segment must be a RouterOS menu name — letters,
|
|
102
|
+
digits and hyphens (case folds), the grammar of every dir and command in the
|
|
103
|
+
7.9–7.26 tree — or the read fails with `input/invalid-routeros-path` before
|
|
104
|
+
resolution. Before this check, a `#` or `?` cut the appended `/print` off the
|
|
105
|
+
REST URL, so `retrieve '/system/reboot#' --no-validate --attribute uptime`
|
|
106
|
+
posted `/rest/system/reboot` and rebooted CHR 7.24.5. A well-formed command
|
|
107
|
+
path is safe: RouterOS answers `POST /rest/system/reboot/print` (and a `GET` of
|
|
108
|
+
a command) with "no such command", and native API answers
|
|
109
|
+
`/system/reboot/print` the same way (CHR 7.24.5). Under the REST adapter, a
|
|
110
|
+
second guard refuses any request path containing `?`, `#`, `\`, a control
|
|
111
|
+
character, or a `.`/`..` segment (including the percent-encoded `%2e%2e` and
|
|
112
|
+
`.%2E` forms, which the URL parser also collapses) for every command. RouterOS
|
|
113
|
+
does not decode percent-escapes in REST paths, so nothing else changes the
|
|
114
|
+
target.
|
|
115
|
+
|
|
99
116
|
SNMP validation is separate from RouterOS `/console/inspect`: OIDs and MIB
|
|
100
117
|
names must resolve through the MIB cache before any SNMP request is sent.
|
|
101
118
|
|
|
@@ -524,7 +541,11 @@ stays the single-target envelope; a selector flag or a second positional target
|
|
|
524
541
|
switches to fan-out. `--quickchr <name>` (the named-live-provider) behaves like a
|
|
525
542
|
lone positional: one flag stays single-target, repeating it fans out, and mixing
|
|
526
543
|
it with positionals or CDB selectors is `usage/conflicting-flags` (constitution:
|
|
527
|
-
resolution providers).
|
|
544
|
+
resolution providers). When several CDB records share one `<router>` (the
|
|
545
|
+
same address saved under two users), `--match user=<name>` (or
|
|
546
|
+
`target=<addr>`, or a record type) picks one, as on `devices show`; it is
|
|
547
|
+
single-target only (see
|
|
548
|
+
[devices → Ambiguity](../devices/README.md#ambiguity), #583). The `--host` **flag** with no `<router>` positional
|
|
528
549
|
(`retrieve --host http://192.0.2.10:8080 /system/resource`) is a single target
|
|
529
550
|
too: no CDB lookup, so auth comes from flags, env or config. `CENTRS_HOST` never
|
|
530
551
|
stands in for `<router>`.
|
|
@@ -562,5 +583,5 @@ reach green is forbidden. See `docs/CONSTITUTION.md` for the full done rule.
|
|
|
562
583
|
- **ssh / mac-telnet / romon / winbox-terminal** — execute surfaces, not
|
|
563
584
|
retrieve surfaces.
|
|
564
585
|
- **tips** — when a `<router>` fails to resolve against an empty CDB, retrieve
|
|
565
|
-
emits
|
|
566
|
-
`centrs devices
|
|
586
|
+
emits `tip/no-devices`, steering toward `centrs discover --save` and
|
|
587
|
+
`centrs devices add`.
|
|
@@ -146,6 +146,43 @@ centrs retrieve $R /system/license --username $U --password $P --json
|
|
|
146
146
|
centrs retrieve $R /system/resource --attribute bogus --username $U --password $P --json
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
+
## Read paths dispatch only reads (#586)
|
|
150
|
+
|
|
151
|
+
A read sends only a `print`/`get`, even with `--validate=false`. Covered by
|
|
152
|
+
the CHR test `test/integration/read-path-guard.test.ts` (RP1–RP2, over REST
|
|
153
|
+
and native-api, for `retrieve` and `api`; the router's uptime must never go
|
|
154
|
+
backwards) and the offline grammar and REST-guard tests in
|
|
155
|
+
`test/unit/menu-path.test.ts` (RP3).
|
|
156
|
+
|
|
157
|
+
### RP1. A `#` or `?` in the path is refused before dispatch
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
centrs retrieve $R '/system/reboot#' --no-validate --attribute uptime --username $U --password $P --json
|
|
161
|
+
centrs api $R '/system/reboot?' --validate=false --attribute uptime --username $U --password $P --json
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Envelope: `ok: false`, `error.code=input/invalid-routeros-path`,
|
|
165
|
+
`error.context.invalidSegments` names the bad segment, and nothing is sent.
|
|
166
|
+
Before #586 the first line posted `/rest/system/reboot` and rebooted the CHR.
|
|
167
|
+
|
|
168
|
+
### RP2. A well-formed command path is RouterOS's refusal, not a dispatch
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
centrs retrieve $R /system/reboot --no-validate --username $U --password $P --json
|
|
172
|
+
centrs retrieve $R /system/reset-configuration --no-validate --attribute uptime --username $U --password $P --json
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Envelope: `ok: false` with a `routeros/*` code. RouterOS answers
|
|
176
|
+
`<command>/print` and a `GET` of a command with "no such command", so the
|
|
177
|
+
appended `/print` is the guarantee once the path cannot cut it off.
|
|
178
|
+
|
|
179
|
+
### RP3. Menu-name grammar and the REST URL guard
|
|
180
|
+
|
|
181
|
+
Path segments are letters, digits and hyphens (case folds:
|
|
182
|
+
`/IP/Address` is accepted). Under every REST caller, a request path with
|
|
183
|
+
`?`, `#`, `\`, a control character or a `.`/`..` segment (including
|
|
184
|
+
`%2e%2e` and `.%2E`) fails `input/invalid-routeros-path` before `fetch`.
|
|
185
|
+
|
|
149
186
|
## Transport / error contract
|
|
150
187
|
|
|
151
188
|
### 12. Bad credentials
|
|
@@ -821,3 +858,50 @@ centrs retrieve $R /interface --until name=missing --wait 500ms --username $U --
|
|
|
821
858
|
One envelope goes to stdout, with `stopReason: "deadline-elapsed"`; exit 1.
|
|
822
859
|
No intermediate sample frames are printed. Unit anchors cover slow validation,
|
|
823
860
|
unfinished reads, cancellation and narrow retry classification.
|
|
861
|
+
|
|
862
|
+
## Record selection (`--match`, #583)
|
|
863
|
+
|
|
864
|
+
Covered by the named tests `MA1`–`MA3` in `test/integration/cdb-match.test.ts`
|
|
865
|
+
on disposable CHR; MA4 is the offline contract in the `MA4.` tests of
|
|
866
|
+
`test/unit/cdb-match.test.ts`, which drive `retrieve`/`api` through the CLI. `$CDB` holds
|
|
867
|
+
two `ipAdmin` records for the same target `$R`: user `$U` with a deliberately
|
|
868
|
+
wrong password, and a `read`-group user `reader` with its real password.
|
|
869
|
+
The same selector works on `execute`, `api`, `transfer`, `terminal` and
|
|
870
|
+
`btest client`.
|
|
871
|
+
|
|
872
|
+
### MA1. Two records for one target are ambiguous
|
|
873
|
+
|
|
874
|
+
```bash
|
|
875
|
+
centrs retrieve $R /system/identity --cdb-file $CDB --json
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
Envelope: `ok: false`, `error.code=identity/ambiguous`, `context.matches`
|
|
879
|
+
lists both records, and the remediation names `--match user=<name>`.
|
|
880
|
+
|
|
881
|
+
### MA2. `--match user=` picks the record and its credentials
|
|
882
|
+
|
|
883
|
+
```bash
|
|
884
|
+
centrs retrieve $R /system/identity --cdb-file $CDB --match user=reader --json
|
|
885
|
+
centrs execute $R /system/identity/print --cdb-file $CDB --match user=reader --json
|
|
886
|
+
centrs api $R system/identity --cdb-file $CDB --match user=reader --json
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
Each is `ok: true` with `meta.target.recordIndex=1`. With
|
|
890
|
+
`--match user=$U` instead, each fails `transport/auth-failed`: the decoy's
|
|
891
|
+
wrong password went on the wire, so the selection is real.
|
|
892
|
+
|
|
893
|
+
### MA3. A selector that matches nothing
|
|
894
|
+
|
|
895
|
+
```bash
|
|
896
|
+
centrs retrieve $R /system/identity --cdb-file $CDB --match user=nobody --json
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
Envelope: `ok: false`, `error.code=identity/no-match`.
|
|
900
|
+
|
|
901
|
+
### MA4. `--match` is single-target and never ignored
|
|
902
|
+
|
|
903
|
+
With a fan-out selection (`--group`, `--where`, `--all`, several
|
|
904
|
+
positionals) or `--quickchr`, `--match` is `usage/conflicting-flags`. When no
|
|
905
|
+
CDB record is in play (no CDB file, an unregistered literal host, a
|
|
906
|
+
`__default__`-only fallback) it is `identity/no-match`, never a silent
|
|
907
|
+
literal-host read.
|
|
@@ -156,6 +156,12 @@ centrs settings reset [<attr>] # clear one/all back to built-in
|
|
|
156
156
|
elsewhere (`src/cli/api.ts`'s `exitCode`), never a bare non-zero without an
|
|
157
157
|
envelope. `settings` has no fan-out surface, so the constitution's `2`
|
|
158
158
|
("partial" fan-out result) never applies here.
|
|
159
|
+
- **`get` output:** text mode renders the one entry exactly as `print`
|
|
160
|
+
renders that key — `key: value [source: kind (key)]` under the canonical
|
|
161
|
+
attr name, plus any `Tips:` footer (examples.md #33). `get`'s `data` is the
|
|
162
|
+
bare `{ source, value }` entry, so the JSON/YAML envelope names the key in
|
|
163
|
+
`meta.operation.attr` (canonical name, whichever spelling was asked for).
|
|
164
|
+
A blank `<attr>` is refused at parse time as `input/invalid-command`.
|
|
159
165
|
- **Agent-facing:** every subcommand emits the standard envelope
|
|
160
166
|
(`ok`/`data`/`meta`/`error`) regardless of TTY — `--format json` is
|
|
161
167
|
honored on `settings` itself exactly like every other command, so an agent
|
|
@@ -406,3 +406,20 @@ XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings print --cdb-file /tmp/does-not-exi
|
|
|
406
406
|
device configured" rather than `cdb/not-found`, since reporting settings
|
|
407
407
|
should not require a CDB to exist at all). `data.defaultDevice = {
|
|
408
408
|
configured: false }`.
|
|
409
|
+
|
|
410
|
+
## Text output
|
|
411
|
+
|
|
412
|
+
### 33. Get renders one entry in text mode, like print
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
XDG_CONFIG_HOME=$SETTINGS_DIR CENTRS_MAX_RESULTS=7 centrs settings get CENTRS_MAX_RESULTS
|
|
416
|
+
XDG_CONFIG_HOME=$SETTINGS_DIR centrs settings get ssh-key
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
`ok: true`, exit 0. Text mode prints the single entry as one line in the
|
|
420
|
+
same `key: value [source: kind (key)]` format `print` uses, under the
|
|
421
|
+
canonical attr name whichever spelling was asked for: `max-results: 7
|
|
422
|
+
[source: env (CENTRS_MAX_RESULTS)]`, then `ssh-key: (unset) [source:
|
|
423
|
+
default (ssh-key)]` followed by its `Tips:` footer. `get`'s `data` is the bare
|
|
424
|
+
entry, so the JSON envelope names the key in `meta.operation.attr`. A
|
|
425
|
+
credential-shaped key renders `(redacted)`, never the secret.
|
|
@@ -118,6 +118,31 @@ past 60 KB) rather than quietly switching to sftp. `scp`, `fetch`, and `ftp` are
|
|
|
118
118
|
explicit-only; `ftp` additionally requires `ALLOW_UNSAFE_PROTOCOLS=ftp` because
|
|
119
119
|
it is cleartext.
|
|
120
120
|
|
|
121
|
+
The method resolves `--via` → `CENTRS_TRANSFER_VIA` → the CDB record's `via=`
|
|
122
|
+
preference → config → auto ([constitution: settings](../../docs/CONSTITUTION.md)).
|
|
123
|
+
A record's `via=` names a *transport* (shared with `execute`), so transfer maps
|
|
124
|
+
it to a method (#591):
|
|
125
|
+
|
|
126
|
+
| CDB / `--via` transport name | Transfer method |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `ssh` | `sftp` |
|
|
129
|
+
| `rest-api` (also `--via rest`) | `rest` |
|
|
130
|
+
| `native-api` (also `--via native`) | `native` |
|
|
131
|
+
| `mac-telnet`, `romon`, `winbox-terminal`, `snmp`, `mndp`, `btest` | none — `settings/invalid-via` |
|
|
132
|
+
|
|
133
|
+
`--via ssh` (and `CENTRS_TRANSFER_VIA=ssh`) is accepted as an alias of
|
|
134
|
+
`sftp`. A record whose `via=` cannot carry files fails `settings/invalid-via`
|
|
135
|
+
naming the record, rather than being ignored; pass `--via sftp|rest|native`
|
|
136
|
+
for that transfer. A record `via=` holds a transport id: method names
|
|
137
|
+
(`sftp`, `rest`, `ftp`) and unknown values are rejected when the record is
|
|
138
|
+
read (`cdb/invalid-option` warning) and leave the method on auto. A record
|
|
139
|
+
preference is a setting, not an auto hop: a `via=rest-api` record uploading
|
|
140
|
+
more than 60 KB fails `transport/unsupported-operation` like a pinned
|
|
141
|
+
`--via rest` does, and the remediation names the record. The endpoint and
|
|
142
|
+
credentials still come from the same record; the mapping never selects a
|
|
143
|
+
different one. A record's `port=` travels with a `--via` pin, so if the
|
|
144
|
+
record's port belongs to another service, point `--port` at the pinned one.
|
|
145
|
+
|
|
121
146
|
**File contents are bytes on `rest`/`native`.** RouterOS carries `/file`
|
|
122
147
|
contents as raw bytes on both wires — REST puts bytes 0x80–0xff unescaped in the
|
|
123
148
|
JSON string, so the body is not UTF-8 — and stores the UTF-8 encoding of every
|
|
@@ -569,3 +569,49 @@ list succeeds over sftp: `meta.via=sftp`, `meta.target.source.kind=provider`,
|
|
|
569
569
|
handed over) carried as a provider-sourced `sshKey`. Otherwise the envelope is
|
|
570
570
|
`ok: false` with `error.code=quickchr/unsupported-via` and exit code 1 — a
|
|
571
571
|
typed gate, **never** a password prompt and never a silent fallback to REST.
|
|
572
|
+
|
|
573
|
+
## CDB record `via=` (#591)
|
|
574
|
+
|
|
575
|
+
A record's `via=` names a transport, shared with `execute`; transfer maps it
|
|
576
|
+
to a method (`ssh`→`sftp`, `rest-api`→`rest`, `native-api`→`native`). `$CDB`
|
|
577
|
+
holds one record for `127.0.0.1` whose comment is
|
|
578
|
+
`via=ssh port=$SSH_PORT ssh-key=$KEY insecure=true`; the key is imported as
|
|
579
|
+
in the sftp section. C1–C2 are covered by
|
|
580
|
+
`test/integration/transfer-cdb-via.test.ts`; C3–C4 are offline error
|
|
581
|
+
contracts covered by `test/unit/transfer.test.ts`
|
|
582
|
+
(`resolveTransferRequest takes the CDB record's via=`).
|
|
583
|
+
|
|
584
|
+
### C1. A `via=ssh` record runs execute over ssh and transfer over sftp
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
centrs execute 127.0.0.1 --cdb-file $CDB /system/identity/print --json
|
|
588
|
+
centrs transfer 127.0.0.1 --cdb-file $CDB upload $SRC centrs-591.txt --json
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
Both `ok: true` with `meta.via="ssh"`, and no `--via` on either. The
|
|
592
|
+
transfer's `meta.settings.via` is `{kind: "comment-kv", key: "record:0:via"}`.
|
|
593
|
+
|
|
594
|
+
### C2. Download from the same record, with and without `--via sftp`
|
|
595
|
+
|
|
596
|
+
```bash
|
|
597
|
+
centrs transfer 127.0.0.1 --cdb-file $CDB download centrs-591.txt $TMP/c2-auto.txt --json
|
|
598
|
+
centrs transfer 127.0.0.1 --cdb-file $CDB --via sftp download centrs-591.txt $TMP/c2-sftp.txt --json
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
Both `ok: true`, `meta.via="ssh"`; each file is byte-identical to `$SRC`.
|
|
602
|
+
`--via ssh` is accepted as the same method as `--via sftp`.
|
|
603
|
+
|
|
604
|
+
### C3. A record transport that cannot carry files
|
|
605
|
+
|
|
606
|
+
A record with `via=mac-telnet` (or `romon`, `snmp`, `winbox-terminal`,
|
|
607
|
+
`mndp`, `btest`) and no `--via`: `ok: false`, `error.code=settings/invalid-via`,
|
|
608
|
+
`error.context.source="record:N:via"`, and the remediation suggests
|
|
609
|
+
`--via sftp`, `--via rest` or `--via native`. An explicit `--via` wins over
|
|
610
|
+
the record and the transfer proceeds.
|
|
611
|
+
|
|
612
|
+
### C4. A `via=rest-api` record does not auto-hop a large upload
|
|
613
|
+
|
|
614
|
+
A `via=rest-api` record uploading more than 60 KB with no `--via`:
|
|
615
|
+
`ok: false`, `error.code=transport/unsupported-operation`, nothing sent. The
|
|
616
|
+
remediation names `record:N:via` and suggests `--via sftp`, matching a
|
|
617
|
+
pinned `--via rest`.
|