@tikoci/centrs 0.1.0
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 +207 -0
- package/GLOSSARY.txt +420 -0
- package/README.md +196 -0
- package/SECURITY.md +30 -0
- package/commands/AGENTS.md +50 -0
- package/commands/btest/README.md +257 -0
- package/commands/btest/examples.md +190 -0
- package/commands/check/README.md +59 -0
- package/commands/devices/README.md +405 -0
- package/commands/devices/examples.md +488 -0
- package/commands/discover/README.md +170 -0
- package/commands/discover/examples.md +98 -0
- package/commands/execute/README.md +208 -0
- package/commands/execute/examples.md +353 -0
- package/commands/mcp/README.md +260 -0
- package/commands/mcp/examples.md +158 -0
- package/commands/retrieve/README.md +209 -0
- package/commands/retrieve/examples.md +306 -0
- package/commands/settings/README.md +69 -0
- package/commands/stream/README.md +70 -0
- package/commands/terminal/README.md +177 -0
- package/commands/terminal/examples.md +144 -0
- package/commands/transfer/README.md +315 -0
- package/commands/transfer/examples.md +368 -0
- package/docs/CONSTITUTION.md +417 -0
- package/docs/MATRIX.md +224 -0
- package/docs/errors/README.md +74 -0
- package/docs/errors/auth/failed.md +9 -0
- package/docs/errors/cdb/already-exists.md +9 -0
- package/docs/errors/cdb/created.md +9 -0
- package/docs/errors/cdb/decrypt-failed.md +9 -0
- package/docs/errors/cdb/empty-group.md +9 -0
- package/docs/errors/cdb/invalid-option.md +9 -0
- package/docs/errors/cdb/invalid-record.md +9 -0
- package/docs/errors/cdb/not-found-target.md +9 -0
- package/docs/errors/cdb/not-found.md +9 -0
- package/docs/errors/cdb/override-applied.md +9 -0
- package/docs/errors/cdb/parse-failed.md +9 -0
- package/docs/errors/cdb/password-not-needed.md +9 -0
- package/docs/errors/cdb/password-required.md +9 -0
- package/docs/errors/cdb/reserved-key.md +9 -0
- package/docs/errors/cdb/reserved-option.md +9 -0
- package/docs/errors/cdb/target-not-registered.md +9 -0
- package/docs/errors/cdb/unknown-field.md +9 -0
- package/docs/errors/cdb/unknown-option.md +9 -0
- package/docs/errors/cdb/write-not-permitted.md +9 -0
- package/docs/errors/discover/broadcast-unavailable.md +9 -0
- package/docs/errors/discover/failed.md +9 -0
- package/docs/errors/identity/ambiguous.md +9 -0
- package/docs/errors/identity/no-match.md +9 -0
- package/docs/errors/input/invalid-command.md +9 -0
- package/docs/errors/input/invalid-match.md +9 -0
- package/docs/errors/input/invalid-path.md +9 -0
- package/docs/errors/input/invalid-routeros-path.md +9 -0
- package/docs/errors/input/invalid-target.md +9 -0
- package/docs/errors/input/local-file-not-found.md +9 -0
- package/docs/errors/input/mac-address.md +9 -0
- package/docs/errors/input/max-results-exceeded.md +9 -0
- package/docs/errors/internal/devices-failed.md +9 -0
- package/docs/errors/internal/mcp-start.md +9 -0
- package/docs/errors/internal/mcp-tool.md +9 -0
- package/docs/errors/internal/native-api-length.md +9 -0
- package/docs/errors/internal/unhandled.md +9 -0
- package/docs/errors/internal/unreachable.md +9 -0
- package/docs/errors/mndp/encode-failed.md +9 -0
- package/docs/errors/mndp/listen-failed.md +9 -0
- package/docs/errors/mndp/malformed.md +9 -0
- package/docs/errors/routeros/api-fatal.md +9 -0
- package/docs/errors/routeros/api-protocol.md +9 -0
- package/docs/errors/routeros/api-trap.md +9 -0
- package/docs/errors/routeros/btest-protocol.md +12 -0
- package/docs/errors/routeros/btest-too-many-sessions.md +9 -0
- package/docs/errors/routeros/command-failed.md +14 -0
- package/docs/errors/routeros/ec-srp5-protocol.md +11 -0
- package/docs/errors/routeros/error.md +9 -0
- package/docs/errors/routeros/invalid-value.md +14 -0
- package/docs/errors/routeros/mac-telnet-error.md +9 -0
- package/docs/errors/routeros/mac-telnet-not-ready.md +9 -0
- package/docs/errors/routeros/mac-telnet-protocol.md +9 -0
- package/docs/errors/routeros/mac-telnet-unsupported-auth.md +9 -0
- package/docs/errors/routeros/protocol-not-implemented.md +9 -0
- package/docs/errors/routeros/request-failed.md +9 -0
- package/docs/errors/routeros/session-closed.md +13 -0
- package/docs/errors/routeros/unknown-attribute.md +13 -0
- package/docs/errors/routeros/unknown-path.md +15 -0
- package/docs/errors/routeros/unsupported-capability.md +9 -0
- package/docs/errors/settings/invalid-boolean.md +9 -0
- package/docs/errors/settings/invalid-format.md +9 -0
- package/docs/errors/settings/invalid-integer.md +9 -0
- package/docs/errors/settings/invalid-timeout.md +9 -0
- package/docs/errors/settings/invalid-via.md +9 -0
- package/docs/errors/settings/unsafe-protocol-blocked.md +9 -0
- package/docs/errors/target/mac-not-in-arp.md +14 -0
- package/docs/errors/target/mac-required.md +23 -0
- package/docs/errors/target/mac-unresolved.md +17 -0
- package/docs/errors/target/unresolved.md +9 -0
- package/docs/errors/transport/auth-failed.md +9 -0
- package/docs/errors/transport/auto-method.md +9 -0
- package/docs/errors/transport/capability-unsupported.md +22 -0
- package/docs/errors/transport/checksum-unavailable.md +9 -0
- package/docs/errors/transport/connection-closed.md +9 -0
- package/docs/errors/transport/connection-refused.md +9 -0
- package/docs/errors/transport/dns.md +9 -0
- package/docs/errors/transport/host-key-mismatch.md +9 -0
- package/docs/errors/transport/incomplete-transfer.md +9 -0
- package/docs/errors/transport/insecure-trust.md +9 -0
- package/docs/errors/transport/local-tool-missing.md +11 -0
- package/docs/errors/transport/network.md +9 -0
- package/docs/errors/transport/timeout.md +9 -0
- package/docs/errors/transport/tls-certificate.md +9 -0
- package/docs/errors/transport/unreachable.md +9 -0
- package/docs/errors/transport/unsupported-operation.md +9 -0
- package/docs/errors/usage/confirmation-required.md +9 -0
- package/docs/errors/usage/conflicting-flags.md +9 -0
- package/docs/errors/usage/invalid-concurrency.md +9 -0
- package/docs/errors/usage/missing-group.md +9 -0
- package/docs/errors/usage/not-implemented.md +9 -0
- package/docs/errors/usage/target-exists.md +9 -0
- package/docs/errors/usage/timeout-out-of-range.md +9 -0
- package/docs/errors/validation/not-implemented.md +9 -0
- package/docs/errors/validation/option.md +9 -0
- package/docs/errors/validation/syntax.md +9 -0
- package/docs/errors/validation/unknown-attribute.md +9 -0
- package/docs/errors/validation/unknown-path.md +9 -0
- package/package.json +96 -0
- package/src/AGENTS.md +7 -0
- package/src/btest.ts +906 -0
- package/src/cli/btest.ts +455 -0
- package/src/cli/common.ts +53 -0
- package/src/cli/devices.ts +527 -0
- package/src/cli/discover.ts +186 -0
- package/src/cli/execute.ts +333 -0
- package/src/cli/mcp.ts +109 -0
- package/src/cli/retrieve.ts +383 -0
- package/src/cli/terminal.ts +303 -0
- package/src/cli/transfer.ts +471 -0
- package/src/cli.ts +115 -0
- package/src/core/envelope.ts +221 -0
- package/src/core/error-catalog.ts +469 -0
- package/src/core/routeros-errors.ts +268 -0
- package/src/data/AGENTS.md +11 -0
- package/src/data/dude-db.ts +0 -0
- package/src/data/mndp-cache.ts +132 -0
- package/src/data/mndp.ts +405 -0
- package/src/data/winbox-cdb-write.ts +200 -0
- package/src/data/winbox-cdb.ts +913 -0
- package/src/devices-schema.ts +107 -0
- package/src/devices.ts +1717 -0
- package/src/discover.ts +802 -0
- package/src/errors.ts +189 -0
- package/src/execute.ts +1350 -0
- package/src/index.ts +353 -0
- package/src/mcp/config.ts +66 -0
- package/src/mcp/resources.ts +98 -0
- package/src/mcp/safety.ts +119 -0
- package/src/mcp/server.ts +225 -0
- package/src/mcp/tools.ts +657 -0
- package/src/mcp.ts +33 -0
- package/src/protocols/AGENTS.md +14 -0
- package/src/protocols/adapter.ts +866 -0
- package/src/protocols/btest-session.ts +1493 -0
- package/src/protocols/btest.ts +447 -0
- package/src/protocols/ec-srp5.ts +466 -0
- package/src/protocols/index.ts +118 -0
- package/src/protocols/mac-telnet-console.ts +655 -0
- package/src/protocols/mac-telnet.ts +1299 -0
- package/src/protocols/mtwei.ts +47 -0
- package/src/protocols/native-api.ts +796 -0
- package/src/protocols/sftp.ts +427 -0
- package/src/protocols/ssh.ts +297 -0
- package/src/resolver/cdb.ts +580 -0
- package/src/resolver/comment-kv.ts +360 -0
- package/src/resolver/index.ts +74 -0
- package/src/resolver/mac.ts +227 -0
- package/src/resolver/settings.ts +228 -0
- package/src/resolver/target.ts +442 -0
- package/src/retrieve-fanout.ts +533 -0
- package/src/retrieve.ts +1215 -0
- package/src/terminal.ts +444 -0
- package/src/transfer.ts +1554 -0
- package/src/tui.ts +22 -0
- package/src/webproxy.ts +16 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# mcp
|
|
2
|
+
|
|
3
|
+
The MCP frontend: an [Model Context Protocol](https://modelcontextprotocol.io)
|
|
4
|
+
server that exposes the centrs core to AI agents through a **small, scoped set of
|
|
5
|
+
verbs** over the same canonicalize → validate → run path the CLI/API use. It is
|
|
6
|
+
not a per-command tool firehose.
|
|
7
|
+
|
|
8
|
+
Status: `CHR-passed` (Phase 1 + first Phase 2 CDB mutation). The stdio server
|
|
9
|
+
(`src/mcp/server.ts`, `tools.ts`, `safety.ts`, `resources.ts`, `config.ts`) is
|
|
10
|
+
built and the examples below (1–10) run green against CHR 7.23 via
|
|
11
|
+
`test/integration/mcp.test.ts`, including the gated `mcp=rw` + `confirm:true`
|
|
12
|
+
RouterOS write and in-band CDB registration through `centrs_devices add`.
|
|
13
|
+
`src/mcp.ts` is the runtime barrel (`@tikoci/centrs/mcp`); `docs/MATRIX.md` is
|
|
14
|
+
the authoritative status.
|
|
15
|
+
|
|
16
|
+
Run it:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bunx @tikoci/centrs mcp # stdio transport (default)
|
|
20
|
+
centrs mcp # same, when installed
|
|
21
|
+
centrs mcp start # explicit verb; equivalent to the above
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Bun is required (centrs uses Bun APIs and the bun-native CDB codec), as with
|
|
25
|
+
`@tikoci/rosetta`.
|
|
26
|
+
|
|
27
|
+
## Client install snippets
|
|
28
|
+
|
|
29
|
+
For an MCP client that accepts JSON server config, use `bunx` directly:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"centrs": {
|
|
35
|
+
"command": "bunx",
|
|
36
|
+
"args": ["@tikoci/centrs", "mcp"],
|
|
37
|
+
"env": {
|
|
38
|
+
"CENTRS_CDB_FILE": "/absolute/path/to/winbox.cdb"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If `centrs` is already installed on `PATH`, the equivalent command is:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"centrs": {
|
|
51
|
+
"command": "centrs",
|
|
52
|
+
"args": ["mcp"],
|
|
53
|
+
"env": {
|
|
54
|
+
"CENTRS_CDB_FILE": "/absolute/path/to/winbox.cdb"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use `CENTRS_CDB_PASSWORD` to read or write an encrypted CDB. The write layer
|
|
62
|
+
decrypts under the loaded password, mutates the record set, and re-encrypts with
|
|
63
|
+
a fresh salt before the atomic rename. Prefer an unencrypted throwaway CDB for
|
|
64
|
+
bench harnesses and CI.
|
|
65
|
+
|
|
66
|
+
## Why a scoped surface (grounding)
|
|
67
|
+
|
|
68
|
+
`~/GitHub/bench-routeros-tools` (`REPORT.md`, `docs/REPORT_LIVE_CHR.md`)
|
|
69
|
+
benchmarked RouterOS agent support. Load-bearing findings that shape this design:
|
|
70
|
+
|
|
71
|
+
- **Never expose one tool per RouterOS command.** A 166-tool execution MCP costs
|
|
72
|
+
~28K always-on tokens and, even free, is ambiguous to select (36/166 tools
|
|
73
|
+
match a task; the correct tool is a clear top-3 pick only 62% of the time;
|
|
74
|
+
destructive tools sit lexically next to safe ones). The recommendation is a
|
|
75
|
+
*handful of verbs* over a canonical `{path, verb, args}`, gated by validation.
|
|
76
|
+
- **explain → validate → run split.** A cheap, safe knowledge/validation tier
|
|
77
|
+
gates a thin runner. centrs already has this shape internally.
|
|
78
|
+
- **The parse scheme catches parser-level truth that schema-inspect can miss.** In
|
|
79
|
+
the bench, `/console/inspect` accepted a command form the real console parser
|
|
80
|
+
rejected. RouterOS 7.23 no longer reproduces that exact `blackhole=yes` case,
|
|
81
|
+
but the lesson stands: schema-presence is not parser-acceptance.
|
|
82
|
+
`:put [:parse "<cmd>"]` runs the real parser, and centrs runs **both** `:parse`
|
|
83
|
+
and `/console/inspect`, so the MCP exposes validation as a first-class
|
|
84
|
+
**dry-run** tool (`centrs_validate`) that never mutates.
|
|
85
|
+
- **Stateful intents need device-validate-before-apply.** Gold using
|
|
86
|
+
`interface=ether2` fails on a CHR exposing only `ether1`. Pair `execute` with
|
|
87
|
+
cheap readback (`retrieve`).
|
|
88
|
+
|
|
89
|
+
## Intent
|
|
90
|
+
|
|
91
|
+
- One MCP tool per centrs **verb**, not per RouterOS command. Each tool is a thin
|
|
92
|
+
adapter over the existing core function and returns the standard envelope
|
|
93
|
+
(`{ ok, data?, warnings?, error?, meta }`) as JSON text content — the same
|
|
94
|
+
shape the CLI renders (constitution: result envelope; invariant: one settings
|
|
95
|
+
vocabulary across API/CLI/MCP/TUI/proxy).
|
|
96
|
+
- Validation is the product here too: `validate=true` by default, lead with
|
|
97
|
+
`:parse`, never disable to make a call pass (constitution: validation).
|
|
98
|
+
- The MCP server is an adapter over the root TypeScript API; it adds no new
|
|
99
|
+
RouterOS semantics, only the MCP transport, the tool schemas, and the
|
|
100
|
+
CDB-backed safety boundary below.
|
|
101
|
+
|
|
102
|
+
## Tools
|
|
103
|
+
|
|
104
|
+
| Tool | Core mapping | Reads/Writes | Target source |
|
|
105
|
+
| ---- | ------------ | ------------ | ------------- |
|
|
106
|
+
| `centrs_explain` | `canonicalizeExecuteCommand` | offline, none | none (string only) |
|
|
107
|
+
| `centrs_validate` | execute validation only (`:parse` + `/console/inspect`), **no run** | read-only | CDB |
|
|
108
|
+
| `centrs_retrieve` | `retrieve` / `retrieveGroup` | read-only | CDB |
|
|
109
|
+
| `centrs_execute` | `execute` | read or write | CDB |
|
|
110
|
+
| `centrs_devices` | `devices` (`list`/`show`/`groups`/`add`/`edit`/`set`/`remove`) | CDB read/write | CDB file |
|
|
111
|
+
| `centrs_discover` | `discover` (`listen` + optional `save`) | local read; `save` writes CDB | n/a |
|
|
112
|
+
|
|
113
|
+
- `centrs_explain` is the only tool that needs no CDB and no device — it
|
|
114
|
+
canonicalizes a CLI string to `{ path, verb, args, mode, writeShaped }` so an
|
|
115
|
+
agent can plan before touching anything. centrs owns the script-vs-structured
|
|
116
|
+
gate (constitution: canonicalizer ownership); the MCP must not widen what
|
|
117
|
+
counts as `structured`.
|
|
118
|
+
- `centrs_validate` is the headline distilled from the bench: a dry-run that runs
|
|
119
|
+
the real parser plus schema inspection against a registered target and returns
|
|
120
|
+
the validation envelope **without executing**. Use it to reject schema-invalid
|
|
121
|
+
commands before any write.
|
|
122
|
+
- `centrs_retrieve` mirrors `retrieve`, including attribute projection,
|
|
123
|
+
`listAttributes`, and `group` fanout.
|
|
124
|
+
- `centrs_execute` mirrors `execute`. Read-shaped commands run subject only to
|
|
125
|
+
CDB membership; write-shaped add/set/remove are gated by the CDB policy below.
|
|
126
|
+
Its MCP `destructiveHint` is **conservative — always `true`**, even though the
|
|
127
|
+
tool also serves read-shaped commands: a single execute tool cannot vary the
|
|
128
|
+
static annotation by argument, so it advertises the worst case and lets the
|
|
129
|
+
canonicalizer's write-shape check + the CDB `mcp=rw`/`confirm` gates do the real
|
|
130
|
+
enforcement. Agents that route purely on read-only commands should prefer
|
|
131
|
+
`centrs_retrieve` (truly `readOnlyHint`).
|
|
132
|
+
- `centrs_devices` is the in-band way to inspect and grow the allowlist:
|
|
133
|
+
`list`/`show`/`groups` are read-only; `add`/`set`/`remove` mutate the active CDB
|
|
134
|
+
and require `confirm: true`. `edit` is reserved for the future interactive
|
|
135
|
+
editor and returns `usage/not-implemented` (use `set` to modify a record). MCP
|
|
136
|
+
device envelopes redact saved passwords and expose only `passwordSet`.
|
|
137
|
+
- `centrs_discover` mirrors `discover`: it listens for MNDP neighbors, returns
|
|
138
|
+
the standard discovery envelope, and can persist found neighbors into the
|
|
139
|
+
active CDB with `save: true` + `confirm: true`. Saved records use the same
|
|
140
|
+
provenance/group behavior as the CLI and no RouterOS credentials are returned.
|
|
141
|
+
|
|
142
|
+
## Safety model — the CDB is the allowlist
|
|
143
|
+
|
|
144
|
+
The authorization boundary is the WinBox CDB (constitution: identity and CDB),
|
|
145
|
+
which is already centrs's device registry **and** native credential store. The
|
|
146
|
+
MCP server applies it as a hard allowlist:
|
|
147
|
+
|
|
148
|
+
- **Targets resolve through the CDB only** — by name, MAC, or `group`. Inline
|
|
149
|
+
ad-hoc `host` + `username` + `password` are **rejected by default** with
|
|
150
|
+
`cdb/target-not-registered`, whose `fix` points at `centrs_devices add`. An
|
|
151
|
+
agent therefore cannot reach an arbitrary router with arbitrary credentials.
|
|
152
|
+
- **CDB source** follows the constitution: default
|
|
153
|
+
`~/.config/tikoci/winbox.cdb`, overridden by `CENTRS_CDB_FILE` / `--cdb-file`,
|
|
154
|
+
or a freshly created CDB the operator hands the server at start. An encrypted
|
|
155
|
+
CDB is decrypted with `--cdb-password` (or `CENTRS_CDB_PASSWORD`). With no CDB,
|
|
156
|
+
the server still serves `centrs_explain` (offline); CDB mutations require a
|
|
157
|
+
loadable CDB file.
|
|
158
|
+
- **Credentials live in the CDB and are never returned by MCP.** `centrs_devices
|
|
159
|
+
add`/`edit` may write credentials into the active CDB, but tool results and
|
|
160
|
+
resources redact saved passwords and expose only `passwordSet`.
|
|
161
|
+
- **Per-device RouterOS read/write policy lives in the CDB.** A device is
|
|
162
|
+
writable only when its record opts in via the comment-kv key `mcp=rw` (default
|
|
163
|
+
`ro`). Reads are allowed for any registered device. Write-shaped
|
|
164
|
+
`centrs_execute` calls require the resolved target to be `mcp=rw`; `mcp` joins
|
|
165
|
+
the comment-kv allowlist (`src/resolver/comment-kv.ts`) and, per the
|
|
166
|
+
constitution, must round-trip through env/CLI/API as well as CDB.
|
|
167
|
+
- **Per-call `confirm: true`** stays as a belt-and-suspenders for write-shaped
|
|
168
|
+
calls (the non-TTY analogue of the CLI's `--yes`). A RouterOS write attempt
|
|
169
|
+
against an `mcp=ro` target fails with typed `cdb/write-not-permitted`
|
|
170
|
+
regardless of `confirm`; a local CDB mutation through `centrs_devices` or
|
|
171
|
+
`centrs_discover` `save` fails with `usage/confirmation-required` unless
|
|
172
|
+
explicitly confirmed.
|
|
173
|
+
- **`--allow-adhoc-targets`** (off by default) is reserved for one-off lab/power
|
|
174
|
+
user flows. Current RouterOS-facing tools expose no inline host+credential
|
|
175
|
+
schema, so the CDB allowlist remains the only active target source.
|
|
176
|
+
|
|
177
|
+
This makes the safety story declarative and auditable: *what the agent may touch*
|
|
178
|
+
and *whether it may write* are both data in a file the operator controls, not
|
|
179
|
+
flags buried in a prompt.
|
|
180
|
+
|
|
181
|
+
## Resources
|
|
182
|
+
|
|
183
|
+
The server publishes two read-only MCP **resources** and points its
|
|
184
|
+
`instructions` at them so a client discovers them without guessing:
|
|
185
|
+
|
|
186
|
+
- `centrs://devices` — the known devices in the active CDB (the allowlist
|
|
187
|
+
itself): target, group, user, record type, and the resolved `mcp` write policy
|
|
188
|
+
per record. No passwords. This is how an agent learns what it may act on.
|
|
189
|
+
- `centrs://errors` — the centrs error-code catalog: each known `family/code`
|
|
190
|
+
with its stable details URL (`https://tikoci.github.io/centrs/errors/<code>`),
|
|
191
|
+
including the MCP-specific `cdb/target-not-registered` and
|
|
192
|
+
`cdb/write-not-permitted`. Lets an agent resolve an envelope error without a
|
|
193
|
+
round-trip.
|
|
194
|
+
|
|
195
|
+
## Transports
|
|
196
|
+
|
|
197
|
+
- **stdio is the only MCP transport.** It matches the common local MCP client
|
|
198
|
+
model and keeps the agent boundary on the operator's machine.
|
|
199
|
+
- **HTTP is the proxy surface's job, not the MCP server's.** Remote/HTTP access
|
|
200
|
+
to CDB-gated RouterOS lives in `src/webproxy.ts` (the `centrs-proxy` surface),
|
|
201
|
+
which fronts the same CDB. The MCP server deliberately grows no HTTP/TLS
|
|
202
|
+
listener of its own, so there is one place that owns network exposure.
|
|
203
|
+
|
|
204
|
+
## Future work
|
|
205
|
+
|
|
206
|
+
- **Manifest dump for benchmarking (`centrs mcp --list-tools`).** A device-free
|
|
207
|
+
command that prints the registered tool + resource schemas and the server
|
|
208
|
+
`instructions` as JSON, so a harness can measure centrs's always-on context
|
|
209
|
+
cost the same way `bench-routeros-tools` counts mikrotik-mcp's 166 schemas —
|
|
210
|
+
without booting a CHR. Pairs with the stable example IDs already mapped in
|
|
211
|
+
`examples.md` (example N ↔ one assertion in `test/integration/mcp.test.ts`) so
|
|
212
|
+
the bench can cite per-trap results. Requested by the bench review
|
|
213
|
+
(`~/GitHub/bench-routeros-tools/docs/AGENTIC_FUTURES.md` → "benchmark centrs as
|
|
214
|
+
the realized scoped-execution tier").
|
|
215
|
+
- **`__default__` fallback-credentials CDB record** is now a **core** concept,
|
|
216
|
+
not MCP-specific — see `docs/CONSTITUTION.md` (Default-device record) and
|
|
217
|
+
`commands/devices/README.md`. It supplies fallback creds/metadata for a device
|
|
218
|
+
whose own record leaves them unset (precedence args → ENV → device →
|
|
219
|
+
`__default__` → built-in). MCP-specific rule: `__default__` **never widens the
|
|
220
|
+
allowlist** — an unregistered target is still rejected; `__default__` only
|
|
221
|
+
fills missing creds for an already-registered device (see the safety model
|
|
222
|
+
above). Implementation lands with packaging (Phase 4).
|
|
223
|
+
- **MikroTik docs enrichment.** `centrs_explain` may later annotate a canonical
|
|
224
|
+
`{path, verb}` with a link into MikroTik's new Docusaurus manual at
|
|
225
|
+
<https://manual.mikrotik.com> (CLI/API reference, still WIP upstream). This is
|
|
226
|
+
an independent, optional reference — centrs does not depend on rosetta or any
|
|
227
|
+
other MCP for its canonicalization.
|
|
228
|
+
|
|
229
|
+
## Open shape questions
|
|
230
|
+
|
|
231
|
+
These are resolved for the current design and recorded here for provenance:
|
|
232
|
+
|
|
233
|
+
- **HTTP transport** → owned by the proxy surface, not MCP (resolved).
|
|
234
|
+
- **Doc context** → centrs logic only; `manual.mikrotik.com` is a future,
|
|
235
|
+
optional enrichment source (resolved).
|
|
236
|
+
- **Resources vs tools** → ship both `centrs://devices` and `centrs://errors` as
|
|
237
|
+
resources, advertised via server instructions (resolved).
|
|
238
|
+
|
|
239
|
+
These refinements sit on top of the grounded surface above; none blocks the
|
|
240
|
+
Phase 1 stdio read/validate cells.
|
|
241
|
+
|
|
242
|
+
## CHR test shape
|
|
243
|
+
|
|
244
|
+
Per the done definition, MCP cells advance only with CHR evidence. The
|
|
245
|
+
integration test (`test/integration/mcp.test.ts`) boots a CHR via
|
|
246
|
+
`@tikoci/quickchr`, registers it in a throwaway CDB to exercise the allowlist
|
|
247
|
+
boundary, starts the server, and drives it with an in-process MCP client over
|
|
248
|
+
stdio. It asserts, at minimum:
|
|
249
|
+
|
|
250
|
+
- an unregistered target is rejected (`cdb/target-not-registered`);
|
|
251
|
+
- `centrs_validate` accepts parser-valid command text and rejects
|
|
252
|
+
schema-invalid attributes without executing;
|
|
253
|
+
- `centrs_retrieve` returns structured records;
|
|
254
|
+
- `centrs_execute` (read-shaped) runs and returns the envelope;
|
|
255
|
+
- a write against an `mcp=ro` target is refused (`cdb/write-not-permitted`) and
|
|
256
|
+
the same write against an `mcp=rw` target succeeds with `confirm: true`;
|
|
257
|
+
- `centrs_devices add` writes a new record into the throwaway CDB and returns no
|
|
258
|
+
saved password material.
|
|
259
|
+
|
|
260
|
+
Each example in `commands/mcp/examples.md` maps to one such assertion.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# mcp — examples
|
|
2
|
+
|
|
3
|
+
Each numbered example is an executable spec for the MCP surface. The integration
|
|
4
|
+
test `test/integration/mcp.test.ts` boots a CHR 7.23 router with
|
|
5
|
+
`@tikoci/quickchr`, registers it in a throwaway CDB, starts the centrs MCP server
|
|
6
|
+
over stdio, and drives it with an in-process MCP client. Each example below maps
|
|
7
|
+
to one assertion. If a line here is not exercised by a test, the test file is
|
|
8
|
+
wrong; if a line passes only with `validate=false`, the **implementation** is
|
|
9
|
+
wrong (see `docs/CONSTITUTION.md`).
|
|
10
|
+
|
|
11
|
+
These examples are MCP **tool calls** (JSON `params.arguments`), not shell
|
|
12
|
+
commands. `$NAME` is the CDB name the harness assigns the booted CHR; `$U` / `$P`
|
|
13
|
+
are CHR credentials stored in the CDB record for RouterOS actions. Example 10
|
|
14
|
+
also writes credentials into the throwaway CDB through `centrs_devices add`, but
|
|
15
|
+
MCP never returns saved password material. The result is the standard centrs
|
|
16
|
+
envelope returned as the tool's JSON text content.
|
|
17
|
+
|
|
18
|
+
## Phase 1 — read + validate (stdio)
|
|
19
|
+
|
|
20
|
+
### 1. `centrs_explain` canonicalizes offline (no CDB, no device)
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{ "name": "centrs_explain",
|
|
24
|
+
"arguments": { "command": "/ip/route/add dst-address=10.99.0.0/24 blackhole=yes" } }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Envelope: `ok: true`, `data.path = "/ip/route"`, `data.verb = "add"`,
|
|
28
|
+
`data.mode = "structured"`, `data.writeShaped = true`, `data.args` includes
|
|
29
|
+
`dst-address` and `blackhole`. No network or CDB access occurs.
|
|
30
|
+
|
|
31
|
+
### 2. Unregistered target is rejected (CDB is the allowlist)
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{ "name": "centrs_retrieve",
|
|
35
|
+
"arguments": { "target": "192.0.2.1", "path": "/system/resource" } }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Envelope: `ok: false`, `error.code = "cdb/target-not-registered"`, and
|
|
39
|
+
`error.fix`/`remediation` names `centrs_devices add`. No connection is attempted.
|
|
40
|
+
RouterOS-facing MCP tools accept only a CDB `target` (name/MAC/URL) — there is
|
|
41
|
+
no inline `host`/`username`/`password` executor parameter, so RouterOS actions
|
|
42
|
+
use credentials already stored in the CDB.
|
|
43
|
+
|
|
44
|
+
### 3. `centrs_validate` accepts the bare `blackhole` flag
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{ "name": "centrs_validate",
|
|
48
|
+
"arguments": { "target": "$NAME",
|
|
49
|
+
"command": "/ip/route/add dst-address=10.99.0.0/24 blackhole" } }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Envelope: `ok: true`, `meta.validation.source` includes `:put [:parse]` (the
|
|
53
|
+
RouterOS parser accepted the bare flag), and **no route is created** (dry-run
|
|
54
|
+
only — a follow-up `centrs_retrieve` of `/ip/route` shows no `10.99.0.0/24`). A
|
|
55
|
+
bare flag like `blackhole` routes through script-mode `:parse`, so the
|
|
56
|
+
`/console/inspect` semantic gate does not apply here — `:parse` is the gate that
|
|
57
|
+
matters for the bench gold-bug in example 4.
|
|
58
|
+
|
|
59
|
+
### 4. `centrs_validate` rejects a schema-invalid command (the dual gate)
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{ "name": "centrs_validate",
|
|
63
|
+
"arguments": { "target": "$NAME",
|
|
64
|
+
"command": "/ip/route/add dst-address=10.99.0.0/24 nonexistentattr=1" } }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Envelope: `ok: false`, `error.code = "validation/unknown-attribute"` — the
|
|
68
|
+
`/console/inspect` semantic gate rejects the unknown attribute and **no route is
|
|
69
|
+
created**. This proves the validate tool fails closed on bad input.
|
|
70
|
+
|
|
71
|
+
> Bench note: the bench's original gold-bug command
|
|
72
|
+
> `/ip/route/add dst-address=10.99.0.0/24 blackhole=yes` (where `/console/inspect`
|
|
73
|
+
> accepted a value the console `:parse` rejected) does **not** reproduce on
|
|
74
|
+
> RouterOS 7.23 — the device parser now accepts `blackhole=yes`. centrs still runs
|
|
75
|
+
> **both** gates (`:parse` + `/console/inspect`) so the stricter of the two wins
|
|
76
|
+
> whenever a version does reject it; this example asserts the still-true,
|
|
77
|
+
> version-robust behavior (unknown-attribute rejection) instead.
|
|
78
|
+
|
|
79
|
+
### 5. `centrs_retrieve` returns structured records
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{ "name": "centrs_retrieve",
|
|
83
|
+
"arguments": { "target": "$NAME", "path": "/system/resource",
|
|
84
|
+
"attributes": ["version", "board-name"] } }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Envelope: `ok: true`, `data` is a single object carrying `version` and
|
|
88
|
+
`board-name` (the `/system/resource` menu is a single record, not a list),
|
|
89
|
+
`meta.via = "rest-api"`.
|
|
90
|
+
|
|
91
|
+
### 6. `centrs_execute` runs a read-shaped command
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{ "name": "centrs_execute",
|
|
95
|
+
"arguments": { "target": "$NAME", "command": "/system/identity/print" } }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Envelope: `ok: true`, `meta.operation.request.write = false`, and
|
|
99
|
+
`meta.validation` shows the parse gate ran.
|
|
100
|
+
|
|
101
|
+
## CHR-verified gated writes
|
|
102
|
+
|
|
103
|
+
### 7. Write against an `mcp=ro` target is refused
|
|
104
|
+
|
|
105
|
+
With `$NAME` registered `mcp=ro` (default):
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{ "name": "centrs_execute",
|
|
109
|
+
"arguments": { "target": "$NAME",
|
|
110
|
+
"command": "/ip/address/add address=198.51.100.10/32 interface=ether1",
|
|
111
|
+
"confirm": true } }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Envelope: `ok: false`, `error.code = "cdb/write-not-permitted"`. The router is not
|
|
115
|
+
mutated even though `confirm` is true — the CDB policy is authoritative.
|
|
116
|
+
|
|
117
|
+
### 8. Write against an `mcp=rw` target succeeds with `confirm: true`
|
|
118
|
+
|
|
119
|
+
With `$NAME` registered `mcp=rw`:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{ "name": "centrs_execute",
|
|
123
|
+
"arguments": { "target": "$NAME",
|
|
124
|
+
"command": "/ip/address/add address=198.51.100.10/32 interface=ether1 comment=centrs-mcp",
|
|
125
|
+
"confirm": true } }
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Envelope: `ok: true`, `data.ret` matches `/^\*[0-9A-F]+$/`,
|
|
129
|
+
`meta.operation.request.write = true`, `meta.validation.source` includes both
|
|
130
|
+
`:parse` and `/console/inspect`.
|
|
131
|
+
|
|
132
|
+
### 9. Write without `confirm` on an `mcp=rw` target fails closed
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{ "name": "centrs_execute",
|
|
136
|
+
"arguments": { "target": "$NAME",
|
|
137
|
+
"command": "/ip/address/remove [find comment=centrs-mcp]" } }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Envelope: `ok: false`, `error.code = "usage/confirmation-required"`; the address
|
|
141
|
+
added in example 8 still exists. (Cleanup runs with `confirm: true`.)
|
|
142
|
+
|
|
143
|
+
## CHR-verified CDB mutations
|
|
144
|
+
|
|
145
|
+
### 10. `centrs_devices add` grows the allowlist in-band
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{ "name": "centrs_devices",
|
|
149
|
+
"arguments": { "op": "add", "target": "lab-edge",
|
|
150
|
+
"user": "$U", "password": "$P",
|
|
151
|
+
"comment": "mcp=ro",
|
|
152
|
+
"confirm": true } }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Envelope: `ok: true`, the CDB now resolves `lab-edge`, `data.entry.password` is
|
|
156
|
+
absent, and `data.entry.passwordSet` reports whether a password was saved. A
|
|
157
|
+
subsequent `centrs_devices show` for `lab-edge` succeeds. This uses the
|
|
158
|
+
throwaway CDB; no real device is contacted by the registration itself.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# retrieve
|
|
2
|
+
|
|
3
|
+
Read RouterOS state. RouterOS menu reads model `<path>/<verb>` where the verb
|
|
4
|
+
is `print`-style (`print`, `get`, and async POST-shaped reads as they're
|
|
5
|
+
added). SNMP reads use `retrieve <router> snmp <oid|MIB name>` and resolve
|
|
6
|
+
names through a MikroTik MIB cache downloaded from mikrotik.com.
|
|
7
|
+
|
|
8
|
+
> "POST-shaped reads" are RouterOS menu reads that require a request body
|
|
9
|
+
> (e.g. paged or query-driven `print`) and are therefore issued as REST `POST`
|
|
10
|
+
> rather than `GET`. centrs identifies them from the command tree and routes
|
|
11
|
+
> them accordingly as they're added.
|
|
12
|
+
|
|
13
|
+
Structured formats return an array of records, or — with `--attribute` — a
|
|
14
|
+
value or row of values wrapped in the standard envelope
|
|
15
|
+
(`docs/CONSTITUTION.md`). The default `text` format renders data and errors for
|
|
16
|
+
humans.
|
|
17
|
+
|
|
18
|
+
## Synopsis
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
centrs retrieve <router> <path>[/<shortcut>] [flags]
|
|
22
|
+
centrs retrieve <router> snmp <oid|MIB name> [flags]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- `<router>` — IP, DNS, MAC, or CDB-resolved name. See constitution: identity.
|
|
26
|
+
- `<path>` — RouterOS path beginning with `/`, e.g. `/ip/address`. The verb is
|
|
27
|
+
inferred (`print` for list paths, `get` for singletons like
|
|
28
|
+
`/system/resource`). If the unnamed arg does not start with `/`, it is a
|
|
29
|
+
reserved shortcut — currently none are defined; future examples include
|
|
30
|
+
`defconf` → `/system/default-configuration/get`.
|
|
31
|
+
- Reading one row by name is **flag-only** (there is no row positional):
|
|
32
|
+
`--query name=ether1` filters RouterOS rows and returns an **array**; the
|
|
33
|
+
caller takes the match. `--query`/`--filter` always return an array — there is
|
|
34
|
+
no singleton-by-name shape. (Decided: the old `<.id|name>` positional is
|
|
35
|
+
dropped.)
|
|
36
|
+
- `snmp <oid|MIB name>` — planned retrieve-only SNMP form. OIDs are used
|
|
37
|
+
directly; MIB names are resolved from the cached MikroTik MIB that matches
|
|
38
|
+
the selected RouterOS version/channel once that cache is implemented.
|
|
39
|
+
|
|
40
|
+
## Flags
|
|
41
|
+
|
|
42
|
+
| Flag | Behavior |
|
|
43
|
+
| ----------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
44
|
+
| `--attribute <name>` | Project a single attribute. Repeatable, or comma-separated. |
|
|
45
|
+
| `--attributes <a,b,c>` | Alias for `--attribute` with comma-separated input. |
|
|
46
|
+
| `--all-attributes` | Equivalent to RouterOS `details=true`. Mutually exclusive with `--attribute(s)`. |
|
|
47
|
+
| `--list-attributes` (alias `--list`) | Return the available attribute names for the path. No `print`/`get` is run. |
|
|
48
|
+
| `--once` | Bounded single read of a monitor-style menu (RouterOS `once`): returns **one** envelope and never follows. Open-ended follow is the separate `stream` verb. See constitution: protocol selection. |
|
|
49
|
+
| `--query <expr>` | RouterOS-side **row** filter (maps to `.query`), repeatable; returns matching rows as an **array**. This is how you read one row by name (`--query name=ether1`). **Not Implemented** yet — returns `validation/not-implemented`. |
|
|
50
|
+
| `--filter <expr>` | RouterOS row filter; same `.query` mapping and not-implemented handling as `--query`. |
|
|
51
|
+
| `--where <attr>=<value>` | Device-class selector: fan out across CDB records whose stored fact/comment-kv matches (e.g. `--where board=RB5009`). Repeatable (AND). Filters *which devices*, not RouterOS rows. See constitution: target selection. |
|
|
52
|
+
| `--max-bytes <n>` | Byte budget for the rendered payload. If the response would exceed it, centrs truncates to fit, keeps `ok: true`, and adds a warning + `meta.truncated`. Not an error. (Renamed from `--max-results`; the legacy flag still parses until the code split lands.) |
|
|
53
|
+
| `--max-rows <n>` | Maximum row count for list reads. Excess rows are clipped; `ok: true` with a warning + `meta.truncated`. |
|
|
54
|
+
| `--format text` (default) | Human-readable rendering (default). Errors print `[code] summary` + `Fix:` lines. |
|
|
55
|
+
| `--format json` (alias `--json`) | Structured JSON envelope (set `CENTRS_FORMAT=json` to make it the default). |
|
|
56
|
+
| `--format yaml` (alias `--yaml`) | YAML rendering of the same envelope. |
|
|
57
|
+
| `--resolve <none\|arp>` | Resolve a MAC-address target to an IP via the host ARP cache. Default `none`. |
|
|
58
|
+
| `--via <protocol>` | Pin the transport (`rest-api` default, or `native-api`). No silent downgrade. See constitution: protocol selection. |
|
|
59
|
+
| `--validate=false` | Escape hatch; default is `true`. See constitution: validation is the product. |
|
|
60
|
+
| `--timeout <ms>` | Request timeout. REST: ≤ 60000. native-api allows longer. |
|
|
61
|
+
| `--username` (alias `--user`, `-u`) / `--password` | Override CDB-resolved or env credentials. |
|
|
62
|
+
| `--host <host\|url>` | Override the resolved host or base URL for the target. |
|
|
63
|
+
| `--port <n>` | Override the transport port. native-api defaults to 8728 (TLS api-ssl when `--port 8729`).|
|
|
64
|
+
| `--cdb-file` / `--cdb-password` | Override CDB file location / decrypt password. |
|
|
65
|
+
| `--ros-version <version>` | SNMP MIB lookup only: pin the MikroTik MIB version to cache/download. |
|
|
66
|
+
| `--ros-channel <channel>` | SNMP MIB lookup only: `stable`, `long-term`, `testing`, or `development`; default `stable`. |
|
|
67
|
+
| `--group <name>` | Fan out across every CDB target in WinBox group `<name>`. Repeatable, and combinable with `<router>` positionals plus `--all`/`--default`; the union is de-duped by CDB record index. See **Group fanout**. |
|
|
68
|
+
| `--concurrency <n>` | Max in-flight targets during fanout (integer ≥ 1). Defaults are transport-aware: `rest-api` 8, `native-api` 4. Rejected with `usage/invalid-concurrency` otherwise. |
|
|
69
|
+
|
|
70
|
+
## Validation
|
|
71
|
+
|
|
72
|
+
Default validator: `/console/inspect request=syntax path=<comma-joined>,print`
|
|
73
|
+
(or `,get` for singleton paths). Validator must reject unknown paths and
|
|
74
|
+
unknown attributes with `validation/*` codes that include the inspect-suggested
|
|
75
|
+
alternatives in the error envelope.
|
|
76
|
+
|
|
77
|
+
If `--all-attributes` is combined with `--attribute(s)`, fail with
|
|
78
|
+
`usage/conflicting-flags` *before* hitting the network.
|
|
79
|
+
|
|
80
|
+
SNMP validation is separate from RouterOS `/console/inspect`: OIDs and MIB
|
|
81
|
+
names must resolve through the MIB cache before any SNMP request is sent.
|
|
82
|
+
|
|
83
|
+
## SNMP MIB cache policy
|
|
84
|
+
|
|
85
|
+
Signed off (2026-06-06): cache MikroTik MIBs under the XDG cache root,
|
|
86
|
+
`${XDG_CACHE_HOME:-~/.cache}/tikoci/snmp-mibs`, not beside the CDB. The cache is
|
|
87
|
+
derived data; deleting it must never remove inventory or credentials. Use one
|
|
88
|
+
subdirectory per exact RouterOS version, for example
|
|
89
|
+
`routeros/7.23/mikrotik.mib`, plus metadata containing source URL, ETag,
|
|
90
|
+
Last-Modified, SHA-256, fetched-at, and the channel (if a channel resolved it).
|
|
91
|
+
|
|
92
|
+
Version selection for MIB-name lookup is deterministic. `--ros-version` wins and
|
|
93
|
+
downloads `https://download.mikrotik.com/routeros/<version>/mikrotik.mib`.
|
|
94
|
+
Otherwise `--ros-channel` (default `stable`) is resolved through
|
|
95
|
+
`https://upgrade.mikrotik.com/routeros/NEWESTa7.<channel>` and that exact version
|
|
96
|
+
is used. If target metadata already carries a RouterOS version from a trusted
|
|
97
|
+
source, it may supply the default version, but the SNMP path must not make an
|
|
98
|
+
extra REST/native call merely to discover a version.
|
|
99
|
+
|
|
100
|
+
Invalidation is by exact version plus HTTP validators. A missing version cache is
|
|
101
|
+
downloaded before lookup; an existing cache is reused offline. When online, a
|
|
102
|
+
cache older than 24h is revalidated with ETag/Last-Modified and replaced
|
|
103
|
+
atomically if MikroTik republishes the file. Channel pointers are short-lived:
|
|
104
|
+
re-resolve the channel after 24h, then use the cache for the resulting exact
|
|
105
|
+
version.
|
|
106
|
+
|
|
107
|
+
Offline behavior is intentionally conservative. Numeric OIDs do not require the
|
|
108
|
+
MIB cache and proceed normally. MIB-name lookup succeeds only when the selected
|
|
109
|
+
exact version is already cached; if not, fail before SNMP I/O with
|
|
110
|
+
`snmp/mib-cache-miss` and a fix that tells the caller to go online once or pass a
|
|
111
|
+
cached `--ros-version`. Do not silently fall back to a different version's MIB.
|
|
112
|
+
|
|
113
|
+
RouterOS grounding: MikroTik documents SNMP as retrieve/write-capable but centrs
|
|
114
|
+
uses it as retrieve-only; the same page points users to the MikroTik MIB download
|
|
115
|
+
surface. The per-version `mikrotik.mib` URL under `download.mikrotik.com` has
|
|
116
|
+
been verified for current stable/development versions, but treating it as the
|
|
117
|
+
long-term canonical URL is an assumption until the implementation has fallback
|
|
118
|
+
handling.
|
|
119
|
+
|
|
120
|
+
## Output shape
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
{
|
|
124
|
+
ok: true,
|
|
125
|
+
data: [ { ".id": "*1", address: "192.0.2.1/24", ... }, ... ], // array of rows
|
|
126
|
+
// or for singletons (e.g. /system/resource):
|
|
127
|
+
// data: { uptime: "...", "cpu-load": 7, ... },
|
|
128
|
+
// or for --attribute:
|
|
129
|
+
// data: "192.0.2.1/24", // single value
|
|
130
|
+
meta: { target, via, settings, validation, timing }
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`--max-bytes` / `--max-rows` truncation populates
|
|
135
|
+
`meta.truncated = { returned, total, totalBytes }` and keeps `ok: true` with a
|
|
136
|
+
warning entry — it is not an error. (Earlier text said a byte overflow returns an
|
|
137
|
+
*error* with the needed size; superseded — truncation is an `ok: true` footnote so
|
|
138
|
+
partial data stays usable.)
|
|
139
|
+
|
|
140
|
+
## Group fanout
|
|
141
|
+
|
|
142
|
+
`--group <name>` loads the CDB once, expands every matching WinBox record in
|
|
143
|
+
that group, de-dupes by CDB record index, and runs the same per-target retrieve
|
|
144
|
+
pipeline for each target. Per the shared target-selector grammar, `--group` is
|
|
145
|
+
repeatable and combines freely with one or more `<router>` positionals, `--all`
|
|
146
|
+
(every CDB record), and `--default` (the `__default__` record); the union is
|
|
147
|
+
de-duped by CDB record index. (Earlier drafts made `--group` mutually exclusive
|
|
148
|
+
with a positional — superseded by the liberal selector model.)
|
|
149
|
+
|
|
150
|
+
Fanout output is one outer envelope whose `ok` reports orchestration success:
|
|
151
|
+
`ok: false` is reserved for failures before reliable per-target results exist
|
|
152
|
+
(for example invalid flags or CDB decrypt failure). Per-target success/failure
|
|
153
|
+
is data, not metadata:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
{
|
|
157
|
+
ok: true,
|
|
158
|
+
data: {
|
|
159
|
+
summary: { total: 2, ok: 1, failed: 1 },
|
|
160
|
+
targets: [
|
|
161
|
+
{ ok: true, data: { ... }, meta: { target: { recordIndex: 0 }, ... } },
|
|
162
|
+
{ ok: false, error: { code: "transport/connection-refused", ... }, meta: { target: { recordIndex: 1 }, ... } }
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
warnings: [],
|
|
166
|
+
meta: {
|
|
167
|
+
target: {},
|
|
168
|
+
via: "rest-api", // null if inner targets disagree on protocol
|
|
169
|
+
settings: { ... },
|
|
170
|
+
operation: {
|
|
171
|
+
kind: "fanout",
|
|
172
|
+
group: "prod-edge",
|
|
173
|
+
concurrency: 8,
|
|
174
|
+
summary: { total: 2, ok: 1, failed: 1 },
|
|
175
|
+
request: { path: "/system/resource", validate: true, format: "json", ... }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
An empty or unknown group is `ok: true` with
|
|
182
|
+
`data.summary = { total: 0, ok: 0, failed: 0 }`, `data.targets = []`, and a
|
|
183
|
+
`cdb/empty-group` warning. Validation runs per target because RouterOS schemas
|
|
184
|
+
can differ by version. Fanout retries each target up to two times only for
|
|
185
|
+
`transport/network` and `transport/connection-closed`; REST 5xx failures are
|
|
186
|
+
mapped to `transport/connection-closed`. It does not retry `routeros/*`,
|
|
187
|
+
`validation/*`, `auth/*`, `cdb/*`, `target/*`, timeouts, DNS, TLS, or refused
|
|
188
|
+
connections.
|
|
189
|
+
|
|
190
|
+
## Definition of done
|
|
191
|
+
|
|
192
|
+
This command is `CHR-passed` only when every line in `examples.md` runs green
|
|
193
|
+
against a real CHR through `bun run test:integration`. Disabling validation to
|
|
194
|
+
reach green is forbidden. See `docs/CONSTITUTION.md` for the full done rule.
|
|
195
|
+
|
|
196
|
+
## Notes for future cells
|
|
197
|
+
|
|
198
|
+
- **native-api** — implemented for retrieve (`--via native-api`, `CHR-passed`;
|
|
199
|
+
see `examples.md` N1–N11 and `test/integration/native-api-retrieve.test.ts`).
|
|
200
|
+
Validation still runs through `/console/inspect` (issued as a native-API
|
|
201
|
+
command). Attribute values arrive as strings because the binary API carries
|
|
202
|
+
no JSON scalar types; the envelope shape is otherwise identical to REST.
|
|
203
|
+
- **snmp** — retrieve-only OID/MIB reads. It does not validate through
|
|
204
|
+
`/console/inspect` and does not execute RouterOS CLI.
|
|
205
|
+
- **ssh / mac-telnet / romon / winbox-terminal** — execute surfaces, not
|
|
206
|
+
retrieve surfaces.
|
|
207
|
+
- **tips** — when a `<router>` fails to resolve against an empty CDB, retrieve
|
|
208
|
+
emits the same `tip/no-devices` advice as `devices list`, steering toward
|
|
209
|
+
`centrs devices discover` and `centrs settings`.
|