@tikoci/centrs 0.1.9-next.0 → 0.1.9-next.23

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 (88) hide show
  1. package/CHANGELOG.md +177 -40
  2. package/GLOSSARY.txt +32 -0
  3. package/README.md +12 -8
  4. package/SECURITY.md +11 -3
  5. package/commands/api/README.md +39 -15
  6. package/commands/api/examples.md +44 -6
  7. package/commands/execute/README.md +108 -14
  8. package/commands/execute/examples.md +137 -3
  9. package/commands/explain/README.md +430 -129
  10. package/commands/explain/examples.md +260 -1
  11. package/commands/mcp/README.md +11 -0
  12. package/commands/mcp/examples.md +6 -0
  13. package/commands/retrieve/README.md +30 -13
  14. package/commands/retrieve/examples.md +10 -6
  15. package/commands/transfer/examples.md +9 -0
  16. package/docs/CLI.md +8 -8
  17. package/docs/CONSTITUTION.md +29 -2
  18. package/docs/MATRIX.md +26 -13
  19. package/docs/errors/cdb/empty-group.md +19 -3
  20. package/docs/errors/cdb/empty-selection.md +9 -3
  21. package/docs/errors/cmr/caller-policy-not-enforced.md +46 -0
  22. package/docs/errors/cmr/empty-selection.md +17 -3
  23. package/docs/errors/cmr/rule-selects-every-device.md +32 -0
  24. package/docs/errors/cmr/selection-includes-controller.md +27 -0
  25. package/docs/errors/usage/flag-after-separator.md +34 -0
  26. package/package.json +10 -5
  27. package/src/api-fanout.ts +11 -1
  28. package/src/api.ts +205 -65
  29. package/src/btest.ts +7 -2
  30. package/src/cli/api.ts +30 -16
  31. package/src/cli/common.ts +180 -1
  32. package/src/cli/devices.ts +0 -2
  33. package/src/cli/discover.ts +0 -1
  34. package/src/cli/execute.ts +44 -20
  35. package/src/cli/explain.ts +2 -0
  36. package/src/cli/mcp.ts +0 -1
  37. package/src/cli/retrieve.ts +40 -18
  38. package/src/cli/settings.ts +0 -3
  39. package/src/cli/terminal.ts +0 -2
  40. package/src/cli/transfer.ts +10 -8
  41. package/src/cli/version.ts +87 -0
  42. package/src/cli.ts +5 -3
  43. package/src/core/cmr-fleet.ts +486 -0
  44. package/src/core/cmr.ts +78 -0
  45. package/src/core/envelope.ts +32 -3
  46. package/src/core/error-catalog.ts +25 -3
  47. package/src/core/execute-command.ts +144 -19
  48. package/src/core/fanout.ts +130 -6
  49. package/src/errors.ts +2 -1
  50. package/src/execute-cmr.ts +95 -78
  51. package/src/execute-fanout.ts +11 -1
  52. package/src/execute.ts +105 -23
  53. package/src/explain/args.ts +69 -8
  54. package/src/explain/blocks.ts +3 -4
  55. package/src/explain/brace-slots.ts +5 -0
  56. package/src/explain/catalog.ts +13 -6
  57. package/src/explain/defects.ts +1 -0
  58. package/src/explain/group-faults.ts +927 -0
  59. package/src/explain/highlight-projection.ts +10 -8
  60. package/src/explain/operators.ts +20 -19
  61. package/src/explain/pathresolve.ts +102 -20
  62. package/src/explain/quoted-string.ts +41 -6
  63. package/src/explain/run-tokens.ts +223 -0
  64. package/src/explain/scope-brace.ts +103 -6
  65. package/src/explain/segment.ts +41 -3
  66. package/src/explain/spaced-args.ts +116 -0
  67. package/src/explain/symbols.ts +17 -1
  68. package/src/explain/transport.ts +13 -0
  69. package/src/explain/verbsplit.ts +14 -188
  70. package/src/explain/write.ts +8 -1
  71. package/src/explain.ts +378 -18
  72. package/src/index.ts +18 -1
  73. package/src/mcp/safety.ts +11 -0
  74. package/src/mcp/tools.ts +4 -1
  75. package/src/offline-gate.ts +30 -9
  76. package/src/protocols/btest-session.ts +30 -10
  77. package/src/protocols/mac-telnet-console.ts +16 -0
  78. package/src/resolver/cdb.ts +13 -1
  79. package/src/resolver/index.ts +4 -0
  80. package/src/resolver/selection.ts +37 -1
  81. package/src/resolver/target.ts +96 -8
  82. package/src/retrieve-fanout.ts +4 -1
  83. package/src/retrieve-follow.ts +29 -33
  84. package/src/retrieve-sample.ts +14 -8
  85. package/src/retrieve-stream.ts +38 -6
  86. package/src/retrieve.ts +73 -59
  87. package/src/transfer-fanout.ts +8 -1
  88. package/src/transfer.ts +15 -19
@@ -2,8 +2,24 @@
2
2
 
3
3
  The named CDB group contains no records.
4
4
 
5
+ ## Typical trigger
6
+
7
+ A fan-out selected only by `--group` (no other selector) whose group matched
8
+ no record: a misspelled or case-mismatched group name, or a group whose last
9
+ member was removed.
10
+
11
+ - For a **read**, it is a warning on an `ok: true` fan-out envelope with
12
+ `summary { total: 0, ok: 0, failed: 0 }` and exit code 0.
13
+ - For a **write-shaped** fan-out (`execute` write, a mutating `api` method,
14
+ `transfer upload|remove|mkdir|copy`) it is an outer error (`ok: false`, exit
15
+ code 1), with or without `--yes`. `context.write` is `true`.
16
+
17
+ Any other empty selection (`--where`, `--all`, positionals, mixed) is
18
+ [`cdb/empty-selection`](empty-selection.md).
19
+
5
20
  ## Fix
6
21
 
7
- See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) for the centrs error
8
- contract. This stub will be expanded with the typical trigger and remediation
9
- for `cdb/empty-group`.
22
+ Run `centrs devices groups` to see the groups that exist and their member
23
+ counts, then re-run with the right name. See
24
+ [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) (Target selection) for the
25
+ selector grammar.
@@ -5,9 +5,15 @@ The target selection matched no CDB records.
5
5
  ## Fix
6
6
 
7
7
  A fan-out selection involving `--where` / `--all` / `--default` / positionals (on
8
- its own or mixed with `--group`) resolved to zero targets. This is reported as a
9
- warning on an `ok: true` fan-out envelope with `summary { total: 0, ok: 0, failed:
10
- 0 }`, not a hard failure — there was simply nothing to run.
8
+ its own or mixed with `--group`) resolved to zero targets.
9
+
10
+ - For a **read**, it is a warning on an `ok: true` fan-out envelope with
11
+ `summary { total: 0, ok: 0, failed: 0 }` and exit code 0: there was simply
12
+ nothing to read.
13
+ - For a **write-shaped** fan-out (`execute` write, a mutating `api` method,
14
+ `transfer upload|remove|mkdir|copy`) it is an outer error (`ok: false`, exit
15
+ code 1), with or without `--yes`, so a mistyped selector does not look like
16
+ a successful bulk write. `context.write` is `true`.
11
17
 
12
18
  A **group-only** empty (bare `--group`, no other selector) is reported as
13
19
  [`cdb/empty-group`](empty-group.md) instead, naming the group that matched nothing;
@@ -0,0 +1,46 @@
1
+ # `cmr/caller-policy-not-enforced`
2
+
3
+ A warning on `execute --cmr`, and on `/cmr/device/run-script` or
4
+ `/cmr/device/reboot` run directly with `execute` or `api`: the controller login
5
+ lacks a policy that the command runs with on the clients, and RouterOS does
6
+ not check it. The login does not limit what a CMR command can change.
7
+
8
+ ## Typical trigger
9
+
10
+ A write through `--cmr` with a controller login whose group lacks `write`,
11
+ `test` or `reboot`, such as a "reader" account given to an agent. CMR runs
12
+ every `run-script` script on each selected client with `read`, `write`,
13
+ `test` and `reboot`, the controller's own row included. On 7.26beta1,
14
+ RouterOS checked only that the caller had `read` and `api`. These results
15
+ come from the CHR reproduction on #456 (REST, two-CHR lab, 2026-10-13); the
16
+ same `run-script` write was seen on real hardware, and the integration test
17
+ re-proves it (CMR15 in `commands/execute/examples.md`):
18
+
19
+ | Controller login | Direct write or reboot | Through CMR |
20
+ | --- | --- | --- |
21
+ | `read,api,rest-api` | `not enough permissions (9)` | `run-script` wrote to a client and rebooted it |
22
+ | `read,test,api,rest-api` | `not enough permissions (9)` | `run-script` wrote to the controller's own row; `/cmr/device/reboot` rebooted a client |
23
+
24
+ MikroTik [says](https://forum.mikrotik.com/t/7-26beta-development-is-released/273582/139) this is fixed for 7.26beta2: a `read` group will not
25
+ be allowed to do it. That has not been checked on a beta2 build yet. centrs
26
+ still warns on any version, because the warning reads the login's policy, not
27
+ the RouterOS version. On a build that refuses the write, the members fail
28
+ instead and the warning only says what the login lacks.
29
+
30
+ `context` names the login (`username`, `group`), the policies it lacks
31
+ (`missing`) and the ones the command runs with (`runsWith`): `read`, `write`,
32
+ `test` and `reboot` for `run-script`, and `reboot` for `/cmr/device/reboot`.
33
+ centrs reads the policy once per run, from the login's own `/user` row and
34
+ `/user/group`, and only for a write. When that read fails (for example a
35
+ RADIUS login with no `/user` row) there is no warning. Other fleet commands
36
+ (`/cmr/device/upgrade`, `pair`, the rule menus) were not measured, so they
37
+ carry no such warning.
38
+
39
+ ## Fix
40
+
41
+ Nothing failed; this is information. Do not treat a controller login as
42
+ read-only once the `cmr` package is installed (on 7.26beta1 at least). The
43
+ gates that hold are centrs' own: pass `--yes` only when you intend the write
44
+ on every selected client, and over MCP keep the controller's record at
45
+ `mcp=ro` (the default), which refuses CMR fleet commands outright. Give an
46
+ agent a login on the controller only if it may change every paired device.
@@ -1,8 +1,22 @@
1
1
  # `cmr/empty-selection`
2
2
 
3
- A warning: the `--labels` expression matched no device in the controller's
4
- `/cmr/device` inventory, so nothing ran. The result is an empty selection with
5
- exit code 0, the same as an empty CDB group.
3
+ The `--labels` expression of `execute --cmr` matched no device in the
4
+ controller's `/cmr/device` inventory, so nothing ran.
5
+
6
+ - For a **read**, it is a warning: `ok: true` with an empty selection and exit
7
+ code 0, the same as an empty CDB group. A collector that polls a sometimes
8
+ empty label stays quiet.
9
+ - For a **write-shaped** command it is an error (`ok: false`, exit code 1),
10
+ with or without `--yes`, so a misspelled label does not look like a
11
+ successful bulk write. RouterOS refuses an empty selection the same way
12
+ (`no devices selected`). `context.write` is `true`.
13
+
14
+ The same code is a **warning** on a CMR fleet command run directly with
15
+ `execute` or `api`, such as `/cmr/vlan/add labels=EDGE` or
16
+ `/cmr/device/reboot labels=Edge`: by the controller's inventory now, its
17
+ selection reaches no device. RouterOS refuses an empty selection for a
18
+ `/cmr/device` command itself; it accepts a rule whose labels match nothing,
19
+ and the rule applies to devices that get the label later.
6
20
 
7
21
  ## Typical trigger
8
22
 
@@ -0,0 +1,32 @@
1
+ # `cmr/rule-selects-every-device`
2
+
3
+ A warning on `execute` or `api` to a CMR controller: a `/cmr/vlan`,
4
+ `/cmr/wifi`, `/cmr/wifi/radio` or `/cmr/upgrade` rule command has no device
5
+ `labels`, so the rule applies to every device the controller manages, the
6
+ controller included. For `/cmr/vlan` it fires only when `port-labels` is
7
+ missing too.
8
+
9
+ ## Typical trigger
10
+
11
+ - `/cmr/vlan/add vlan-ids=10` with no `labels` and no `port-labels`. The
12
+ manual warns that such a rule moves every port of every connected device
13
+ into `cmr-bridge`, the controller included, which cuts the whole fleet's
14
+ management access.
15
+ - `/cmr/wifi/add ssid=Office` with no `labels`, or labels that are only band
16
+ labels (`+5ghz`). The network takes over the radios of every device.
17
+ - `set`, `remove`, `enable` or `disable` of an existing rule that has no
18
+ `labels`. Its reach is every device too.
19
+
20
+ centrs reads the rule's `labels` from the command (`add`) or from the rule
21
+ itself (the other verbs). A rule added without `labels` stores none (7.26beta1
22
+ CHR). The command still runs as typed; the warning rides the envelope, and
23
+ the write gate's prompt and `usage/confirmation-required` summary repeat it.
24
+
25
+ ## Fix
26
+
27
+ Give the rule explicit selectors, for example
28
+ `/cmr/vlan/add labels=office-ap port-labels=ether1 vlan-ids=10 role=trunk`.
29
+ Check which devices a label selects with
30
+ `centrs retrieve <controller> /cmr/device` (`labels`, `auto-labels`). Answer
31
+ `yes` or pass `--yes` only if every device, the controller included, is
32
+ intended.
@@ -0,0 +1,27 @@
1
+ # `cmr/selection-includes-controller`
2
+
3
+ A warning on `execute` or `api` to a CMR controller: a fleet command such as
4
+ `/cmr/device/reboot` or `/cmr/device/run-script` selects the controller's own
5
+ `/cmr/device` row (`controller=true`) as well as its clients. The command acts
6
+ on the controller too.
7
+
8
+ ## Typical trigger
9
+
10
+ - `labels=all`: per the manual, `all` selects every device, the CMR server
11
+ included.
12
+ - An exclusion-only expression such as `labels=-core`, which starts from every
13
+ device.
14
+ - A label the controller also carries, such as its architecture (`x86`) or
15
+ RouterOS version, from `auto-labels`.
16
+
17
+ centrs reads the controller's inventory and evaluates the manual's label
18
+ rules over each row's `labels` and `auto-labels`. On a 7.26beta1 lab this
19
+ matched the device's own selection for every expression tried. The result is
20
+ the controller's current view: a disconnected device's `auto-labels` can be
21
+ stale. `context` names the controller and the number of devices selected.
22
+
23
+ ## Fix
24
+
25
+ Nothing failed; this is information. To leave the controller out, exclude it
26
+ explicitly, for example `labels=all,-<controller identity>` (its identity is
27
+ an automatic label while `/cmr auto-labels` includes identity).
@@ -0,0 +1,34 @@
1
+ # `usage/flag-after-separator`
2
+
3
+ A centrs flag was written after `--`, where every token is RouterOS input.
4
+
5
+ ## Fix
6
+
7
+ Move the flag before `--`. The error's remediation prints the corrected
8
+ command line:
9
+
10
+ ```sh
11
+ centrs execute r1 -- '/system/identity/print' --json # refused
12
+ centrs execute r1 --json -- '/system/identity/print' # fixed
13
+ ```
14
+
15
+ A bare `--` ends centrs option parsing, so without this check a trailing
16
+ `--json` was sent to RouterOS and failed there as `unknown-attribute`. The check
17
+ covers `execute` (including `--cmr`) and `explain`. It matches only a token that
18
+ is exactly one of the command's long flags (or its `--flag=value` spelling);
19
+ short aliases such as `-u` pass through as RouterOS input. If RouterOS really
20
+ needs such a token, put it inside the quoted command. Password values are
21
+ replaced by `<password>` in the printed command line.
22
+ Credentials embedded in a positional target or a `--host` value are redacted
23
+ to `***@host`, including when `--host` is moved before the separator.
24
+ Credentials in an explicit URL are redacted even when its colon is percent-encoded;
25
+ bare CDB identities such as `rtr@site` remain unchanged. A bare target with
26
+ a percent-encoded colon is masked in diagnostics while remaining eligible
27
+ for CDB identity lookup.
28
+
29
+ For boolean flags that accept no value (such as `--json` and `--yes`), the
30
+ error explains how to remove an unsupported `=value` rather than printing
31
+ a command that the parser would reject.
32
+
33
+ See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) for the centrs error
34
+ contract.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tikoci/centrs",
3
- "version": "0.1.9-next.0",
3
+ "version": "0.1.9-next.23",
4
4
  "main": "src/index.ts",
5
5
  "module": "src/index.ts",
6
6
  "repository": {
@@ -9,12 +9,12 @@
9
9
  },
10
10
  "homepage": "https://github.com/tikoci/centrs#readme",
11
11
  "bugs": "https://github.com/tikoci/centrs/issues",
12
- "packageManager": "bun@1.3.14",
12
+ "packageManager": "bun@1.4.2",
13
13
  "license": "MIT",
14
14
  "devDependencies": {
15
15
  "@biomejs/biome": "2.5.15",
16
16
  "@secretlint/secretlint-rule-preset-recommend": "^13.0.7",
17
- "@types/bun": "1.3.14",
17
+ "@types/bun": "1.4.2",
18
18
  "cloc": "^2.6.0-cloc",
19
19
  "cspell": "^10.3.6",
20
20
  "has-flag": "4.0.0",
@@ -74,12 +74,14 @@
74
74
  "lint:biome": "biome check .",
75
75
  "lint:fix": "biome check --write .",
76
76
  "lint:secretlint": "secretlint \"**/*\"",
77
+ "lint:actions": "uvx zizmor --offline .",
77
78
  "test": "bun test",
78
79
  "test:smoke": "bun test test/integration/cli-smoke.test.ts",
79
80
  "test:integration:fast": "bun test test/integration/devices.test.ts test/integration/settings.test.ts",
80
81
  "test:integration:settings": "bun test test/integration/settings.test.ts",
81
- "test:integration:smoke": "CENTRS_RUN_FAST_INTEGRATION=1 bun test test/integration/chr-smoke.test.ts",
82
- "test:integration": "CENTRS_RUN_FAST_INTEGRATION=1 bun test test/integration/",
82
+ "chr:prefetch": "bun run scripts/chr-prefetch.ts",
83
+ "test:integration:smoke": "bun run chr:prefetch && CENTRS_RUN_FAST_INTEGRATION=1 bun test test/integration/chr-smoke.test.ts",
84
+ "test:integration": "bun run chr:prefetch && CENTRS_RUN_FAST_INTEGRATION=1 bun test test/integration/",
83
85
  "test:integration:stable": "CENTRS_CHR_CHANNEL=stable bun run test:integration",
84
86
  "test:integration:long-term": "CENTRS_CHR_CHANNEL=long-term bun run test:integration",
85
87
  "test:integration:testing": "CENTRS_CHR_CHANNEL=testing bun run test:integration",
@@ -101,11 +103,14 @@
101
103
  "explain:catalog": "bun run scripts/gen-explain-catalog.ts",
102
104
  "explain:catalog:check": "bun run scripts/gen-explain-catalog.ts --check",
103
105
  "explain:corpus-census": "bun run scripts/explain-corpus-census.ts",
106
+ "explain:conformance": "bun run scripts/explain-conformance.ts",
107
+ "explain:conformance:check": "bun run scripts/explain-conformance.ts --check",
104
108
  "explain:highlight-agreement": "bun run scripts/explain-highlight-agreement.ts",
105
109
  "explain:highlight-agreement:check": "bun run scripts/explain-highlight-agreement.ts --check",
106
110
  "explain:highlight-agreement:readme": "bun run scripts/explain-highlight-agreement.ts --readme",
107
111
  "explain:highlight-agreement:readme:check": "bun run scripts/explain-highlight-agreement.ts --readme --check",
108
112
  "explain:highlight-slice": "bun run scripts/explain-highlight-slice.ts",
113
+ "explain:highlight-slice:check": "bun run scripts/explain-highlight-slice.ts --check",
109
114
  "explain:menus": "bun run scripts/gen-explain-menus.ts",
110
115
  "explain:operator-census": "bun run scripts/explain-operator-census.ts",
111
116
  "explain:operator-census:check": "bun run scripts/explain-operator-census.ts --check",
package/src/api-fanout.ts CHANGED
@@ -62,9 +62,11 @@ import {
62
62
  import { promptForWriteConfirmation } from "./execute.ts";
63
63
  import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
64
64
  import {
65
+ emptyWriteSelectionError,
65
66
  expandSelection,
66
67
  isDefaultRecordTarget,
67
68
  loadEnvFileDefaults,
69
+ redactTargetInput,
68
70
  type SelectionExpansion,
69
71
  type SelectionMember,
70
72
  type SelectionResolveInput,
@@ -87,6 +89,8 @@ export interface ApiFanoutOperationMeta {
87
89
  concurrency: number;
88
90
  summary: FanoutSummary;
89
91
  request: ApiRequestSummary;
92
+ /** Hoisted from the targets when every target used the same login (#461). */
93
+ auth?: { username?: string; passwordProvided: boolean };
90
94
  selection: ApiSelectionSummary;
91
95
  }
92
96
 
@@ -125,7 +129,7 @@ function summarizeSelection(selection: TargetSelection): ApiSelectionSummary {
125
129
  where: selection.where.map((clause) => `${clause.key}=${clause.value}`),
126
130
  all: selection.all,
127
131
  default: selection.default,
128
- positionals: selection.positionals,
132
+ positionals: selection.positionals.map((value) => redactTargetInput(value)),
129
133
  quickchr: selection.quickchr,
130
134
  };
131
135
  }
@@ -242,6 +246,12 @@ export async function apiFanout(
242
246
  const concurrency = resolveFanoutConcurrency(options.concurrency, via);
243
247
 
244
248
  if (expansion.empty) {
249
+ if (requestSummary.write) {
250
+ throw emptyWriteSelectionError(
251
+ expansion,
252
+ `${method} ${requestSummary.path}`,
253
+ );
254
+ }
245
255
  return buildApiFanoutSuccess({
246
256
  summary: { total: 0, ok: 0, failed: 0 },
247
257
  targets: [],