@tikoci/centrs 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/GLOSSARY.txt +213 -0
  3. package/LICENSE +21 -0
  4. package/README.md +36 -8
  5. package/commands/AGENTS.md +7 -1
  6. package/commands/api/README.md +19 -18
  7. package/commands/api/examples.md +15 -0
  8. package/commands/btest/README.md +21 -32
  9. package/commands/check/README.md +6 -1
  10. package/commands/discover/README.md +5 -11
  11. package/commands/execute/README.md +41 -29
  12. package/commands/execute/examples.md +41 -3
  13. package/commands/explain/README.md +1716 -0
  14. package/commands/explain/examples.md +606 -0
  15. package/commands/retrieve/README.md +25 -27
  16. package/commands/retrieve/examples.md +16 -0
  17. package/commands/transfer/README.md +28 -20
  18. package/commands/transfer/examples.md +29 -0
  19. package/docs/CLI.md +353 -0
  20. package/docs/CONSTITUTION.md +66 -15
  21. package/docs/MATRIX.md +38 -1
  22. package/docs/errors/README.md +3 -0
  23. package/docs/errors/quickchr/machine-not-found.md +22 -0
  24. package/docs/errors/quickchr/machine-stopped.md +20 -0
  25. package/docs/errors/quickchr/package-unavailable.md +27 -0
  26. package/docs/errors/quickchr/unsupported-via.md +35 -0
  27. package/docs/errors/quickchr/unsupported.md +35 -0
  28. package/docs/errors/routeros/version-unsupported.md +26 -0
  29. package/docs/errors/transport/provider-trust.md +29 -0
  30. package/docs/errors/usage/stdin-ignored.md +28 -0
  31. package/docs/errors/validation/unknown-attribute.md +37 -4
  32. package/package.json +45 -10
  33. package/src/api-fanout.ts +29 -14
  34. package/src/api.ts +97 -54
  35. package/src/cli/api.ts +25 -6
  36. package/src/cli/btest.ts +18 -2
  37. package/src/cli/execute.ts +25 -6
  38. package/src/cli/explain.ts +503 -0
  39. package/src/cli/retrieve.ts +33 -1
  40. package/src/cli/selection.ts +71 -11
  41. package/src/cli/settings.ts +2 -2
  42. package/src/cli/transfer.ts +23 -6
  43. package/src/cli.ts +15 -2
  44. package/src/core/envelope.ts +5 -2
  45. package/src/core/error-catalog.ts +40 -0
  46. package/src/core/preflight.ts +306 -0
  47. package/src/core/routeros-string.ts +24 -0
  48. package/src/errors.ts +1 -0
  49. package/src/execute-fanout.ts +29 -14
  50. package/src/execute.ts +114 -63
  51. package/src/explain/arg-tokens.ts +114 -0
  52. package/src/explain/args.ts +1403 -0
  53. package/src/explain/blocks.ts +132 -0
  54. package/src/explain/brace-slots.ts +143 -0
  55. package/src/explain/brace-tokens.ts +38 -0
  56. package/src/explain/catalog.ts +1345 -0
  57. package/src/explain/coordinates.ts +329 -0
  58. package/src/explain/defects.ts +217 -0
  59. package/src/explain/is-known-menu.ts +17 -0
  60. package/src/explain/menus.ts +664 -0
  61. package/src/explain/operator-tokens.ts +310 -0
  62. package/src/explain/operators.ts +458 -0
  63. package/src/explain/path-tokens.ts +150 -0
  64. package/src/explain/pathresolve.ts +1324 -0
  65. package/src/explain/quoted-string.ts +241 -0
  66. package/src/explain/scope-brace.ts +308 -0
  67. package/src/explain/segment.ts +757 -0
  68. package/src/explain/string-tokens.ts +67 -0
  69. package/src/explain/symbol-values.ts +520 -0
  70. package/src/explain/symbols.ts +1597 -0
  71. package/src/explain/token-ranges.ts +72 -0
  72. package/src/explain/transport.ts +497 -0
  73. package/src/explain/value-tokens.ts +80 -0
  74. package/src/explain/values.ts +357 -0
  75. package/src/explain/verbs.ts +64 -0
  76. package/src/explain/verbsplit.ts +813 -0
  77. package/src/explain/write.ts +815 -0
  78. package/src/explain.ts +2152 -0
  79. package/src/index.ts +206 -4
  80. package/src/protocols/mac-telnet-console.ts +61 -17
  81. package/src/resolver/facts.ts +1 -1
  82. package/src/resolver/index.ts +23 -4
  83. package/src/resolver/quickchr-provider.ts +597 -0
  84. package/src/resolver/selection.ts +99 -12
  85. package/src/resolver/service-endpoint.ts +155 -0
  86. package/src/resolver/target.ts +1 -1
  87. package/src/retrieve-fanout.ts +26 -12
  88. package/src/retrieve.ts +81 -18
  89. package/src/transfer-fanout.ts +42 -25
  90. package/src/transfer.ts +87 -44
@@ -54,7 +54,7 @@ export interface SelectionWhereClause {
54
54
  value: string;
55
55
  }
56
56
 
57
- /** The parsed target selection a command hands to {@link expandCdbSelection}. */
57
+ /** The parsed target selection a command hands to {@link expandSelection}. */
58
58
  export interface TargetSelection {
59
59
  /** Literal/lookup positionals (NOT operation args — the caller splits those). */
60
60
  positionals: readonly string[];
@@ -70,23 +70,41 @@ export interface TargetSelection {
70
70
  near?: NearPredicate;
71
71
  /** `--bbox <south>,<west>,<north>,<east>`: a geo union predicate (device GPS in box). */
72
72
  bbox?: BboxPredicate;
73
+ /**
74
+ * Repeatable `--quickchr <name>`: quickchr machine names (the named-live-
75
+ * provider, #134). Exclusive in v1 — a selection targets quickchr machines OR
76
+ * CDB/literals, never both; the CLI enforces the usage error before expansion
77
+ * and {@link expandSelection} re-asserts it.
78
+ */
79
+ quickchr?: readonly string[];
73
80
  }
74
81
 
75
- export interface CdbSelectionResolveInput {
82
+ export interface SelectionResolveInput {
76
83
  cdbFile?: string;
77
84
  cdbPassword?: string;
78
85
  /** CLI: true (literal positionals allowed). MCP: false (CDB is the allowlist). */
79
86
  allowAdhoc: boolean;
80
87
  }
81
88
 
82
- /** One resolved selection member: a CDB record or an ad-hoc literal target. */
83
- export type CdbSelectionMember =
89
+ /**
90
+ * One resolved selection member: a CDB record or an ad-hoc literal target.
91
+ *
92
+ * This is the connection-fact origin ("Axis B") the fan-out callers consume. The
93
+ * selector-source (CDB) and literal arms carry a single endpoint materialized
94
+ * later by `./target.ts`. The `quickchr` arm is the named-live-provider (#134):
95
+ * it carries only the machine NAME here — the live descriptor is resolved in the
96
+ * per-member fan-out `resolve()` path (via `resolveQuickchrTarget`), so a
97
+ * stopped/unknown machine is an inner per-target failure, not an expansion
98
+ * error — see `docs/CONSTITUTION.md` → Resolution providers.
99
+ */
100
+ export type SelectionMember =
84
101
  | { kind: "cdb"; resolution: CdbResolution; recordIndex: number }
85
- | { kind: "literal"; input: string };
102
+ | { kind: "literal"; input: string }
103
+ | { kind: "quickchr"; name: string };
86
104
 
87
- export interface CdbSelectionExpansion {
105
+ export interface SelectionExpansion {
88
106
  /** Members ordered by record index; literals appended in positional order. */
89
- targets: readonly CdbSelectionMember[];
107
+ targets: readonly SelectionMember[];
90
108
  warnings: readonly ResolverWarning[];
91
109
  /** True when nothing matched (an empty/unknown selection is not an error). */
92
110
  empty: boolean;
@@ -113,12 +131,16 @@ const ENV_CDB_PASSWORD = "CENTRS_CDB_PASSWORD";
113
131
  * literal host when no CDB is present). An explicit `--cdb-file` that is missing
114
132
  * still throws.
115
133
  */
116
- export async function expandCdbSelection(
134
+ export async function expandSelection(
117
135
  selection: TargetSelection,
118
- input: CdbSelectionResolveInput,
136
+ input: SelectionResolveInput,
119
137
  env: Record<string, string | undefined>,
120
138
  config: Record<string, string | undefined> = {},
121
- ): Promise<CdbSelectionExpansion> {
139
+ ): Promise<SelectionExpansion> {
140
+ const quickchrNames = selection.quickchr ?? [];
141
+ if (quickchrNames.length > 0) {
142
+ return expandQuickchrSelection(selection, input, quickchrNames);
143
+ }
122
144
  const settings = resolveDevicesSettings({
123
145
  cdbFile: input.cdbFile,
124
146
  cdbPassword: input.cdbPassword,
@@ -224,7 +246,7 @@ export async function expandCdbSelection(
224
246
  }
225
247
  }
226
248
 
227
- const literals: CdbSelectionMember[] = [];
249
+ const literals: SelectionMember[] = [];
228
250
  const seenLiteral = new Set<string>();
229
251
  for (const positional of selection.positionals) {
230
252
  try {
@@ -255,7 +277,7 @@ export async function expandCdbSelection(
255
277
  }
256
278
  }
257
279
 
258
- const cdbMembers: CdbSelectionMember[] = [...indices]
280
+ const cdbMembers: SelectionMember[] = [...indices]
259
281
  .sort((a, b) => a - b)
260
282
  .map((index) => ({
261
283
  kind: "cdb",
@@ -302,3 +324,68 @@ export async function expandCdbSelection(
302
324
 
303
325
  return { targets, warnings, empty: false };
304
326
  }
327
+
328
+ /**
329
+ * Expand a quickchr-only selection. No CDB is loaded — a named-live-provider
330
+ * member never touches the registry, the `__default__` ladder, or the selector
331
+ * grammar. Members carry only the machine name (de-duped by exact name, flag
332
+ * order preserved); the live descriptor resolves per member inside the fan-out.
333
+ *
334
+ * Two guards are load-bearing:
335
+ * - Exclusivity (v1): mixing `--quickchr` with selectors/positionals is a
336
+ * usage error. The CLI throws first with flag-level wording; this re-assert
337
+ * protects library/MCP callers that build a {@link TargetSelection} directly.
338
+ * - `allowAdhoc: false` (the MCP surface) rejects quickchr members entirely:
339
+ * the CDB is the MCP allowlist, and a live provider must not widen it (#134
340
+ * non-goal).
341
+ */
342
+ function expandQuickchrSelection(
343
+ selection: TargetSelection,
344
+ input: SelectionResolveInput,
345
+ names: readonly string[],
346
+ ): SelectionExpansion {
347
+ if (!input.allowAdhoc) {
348
+ throw new CentrsError({
349
+ code: "cdb/target-not-registered",
350
+ summary:
351
+ "quickchr targets are not available on this surface; the CDB is the allowlist.",
352
+ remediation:
353
+ "Register the device with `centrs devices add` and select it by identity, or use the CLI for `--quickchr` targets.",
354
+ context: { quickchr: [...names] },
355
+ });
356
+ }
357
+ if (
358
+ selection.positionals.length > 0 ||
359
+ selection.groups.length > 0 ||
360
+ selection.all ||
361
+ selection.default ||
362
+ selection.where.length > 0 ||
363
+ selection.near !== undefined ||
364
+ selection.bbox !== undefined
365
+ ) {
366
+ throw new CentrsError({
367
+ code: "usage/conflicting-flags",
368
+ summary:
369
+ "`--quickchr` is exclusive: a command targets quickchr machines or CDB/literal targets, not both.",
370
+ remediation:
371
+ "Drop the positional/selector (or the `--quickchr` flag) so the selection has one target source.",
372
+ context: {
373
+ quickchr: [...names],
374
+ positionals: selection.positionals.length,
375
+ groups: selection.groups.length,
376
+ all: selection.all,
377
+ default: selection.default,
378
+ where: selection.where.length,
379
+ },
380
+ });
381
+ }
382
+ const seen = new Set<string>();
383
+ const targets: SelectionMember[] = [];
384
+ for (const name of names) {
385
+ if (!seen.has(name)) {
386
+ seen.add(name);
387
+ targets.push({ kind: "quickchr", name });
388
+ }
389
+ }
390
+ return { targets, warnings: [], empty: false };
391
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The neutral per-service endpoint shape — the connection-fact origin ("Axis B")
3
+ * for a resolved target, decoupled from *how* the member was selected ("Axis A").
4
+ *
5
+ * This is the load-bearing scaffold for the resolution-provider refactor
6
+ * (`docs/CONSTITUTION.md` → Resolution providers; centrs #174). Two provider
7
+ * kinds feed it:
8
+ *
9
+ * - **Selector-source** — participates in the `--group`/`--where`/`--all`/
10
+ * `--near`/`--bbox` selector grammar. **CDB** today; **TikTOML** next (#137).
11
+ * CDB is the *degenerate single-endpoint* case: one record yields one
12
+ * host/port/auth, materialized lazily by `./target.ts` once `--via` is known,
13
+ * so a CDB member never needs a populated {@link ServiceEndpointMap}.
14
+ * - **Named-live-provider** — addressed only by an explicit named flag, never
15
+ * matched by selectors, fanned out by repeating the flag; owns an ephemeral
16
+ * endpoint so it also bypasses the settings/`__default__` ladder and conflicts
17
+ * with `--host`/`--port`/`--username`/`--password`/`--ssh-key`. **quickchr**
18
+ * (`--quickchr <name>`, #134). A live provider resolves a member into a
19
+ * *populated* per-service map, because it genuinely exposes different
20
+ * host/port/auth per transport (e.g. a SLiRP-forwarded loopback port per
21
+ * service). Flattening that to a single host/port is the exact refactor #174
22
+ * exists to prevent — TikTOML's `services` block is per-service too.
23
+ *
24
+ * The shape here deliberately mirrors quickchr's published descriptor contract
25
+ * (`tikoci/quickchr` `docs/centrs-interface.md`, `ServiceEndpoint`/
26
+ * `SshServiceEndpoint`), which states that centrs reuses *this same internal
27
+ * type* for TikTOML resolution. It is provider-neutral: `source.provider` is a
28
+ * free string, not a quickchr literal. Mapping a resolved endpoint's auth onto
29
+ * centrs' {@link ../resolver/target.ts | ResolvedAuth} (notably
30
+ * `privateKeyPath` → `sshKey`) is the provider adapter's job, not this type's.
31
+ *
32
+ * Phase 1 (this file) defines the contract only; the quickchr provider adapter
33
+ * that *produces* a map, and the `--via` consumer that *reads* `map[via]`, land
34
+ * with the `--quickchr` plumbing (#134 Phase 2+).
35
+ */
36
+
37
+ import type { RouterOsProtocol } from "../protocols/index.ts";
38
+
39
+ /** Provenance for a resolved endpoint — which provider vouched for it. */
40
+ export interface ServiceEndpointSource {
41
+ /** Provider identity, e.g. `"quickchr"` or `"tiktoml"`. Neutral by design. */
42
+ provider: string;
43
+ /** Provider-internal forward name backing the service (e.g. `"api-ssl"`), for provenance. */
44
+ portMappingName?: string;
45
+ }
46
+
47
+ /** REST/native-api auth. RouterOS shares one credential across REST and native. */
48
+ export interface ServiceEndpointAuth {
49
+ username: string;
50
+ password?: string;
51
+ /** Pre-encoded HTTP Basic value, when the provider supplies one directly. */
52
+ basic?: string;
53
+ /** Pre-formed auth header value, when the provider supplies one directly. */
54
+ header?: string;
55
+ }
56
+
57
+ /** SSH auth. Same credential vocabulary `+ key path`, plus batch-mode capability. */
58
+ export interface SshEndpointAuth {
59
+ username: string;
60
+ /**
61
+ * Path to a usable private key. Populated only when the provider has actually
62
+ * verified a batch (non-interactive) login with it — maps to
63
+ * {@link ../resolver/target.ts | ResolvedAuth.sshKey} in the provider adapter.
64
+ */
65
+ privateKeyPath?: string;
66
+ /** Every auth mode the endpoint accepts. */
67
+ modes: Array<"private-key" | "agent-or-config" | "password">;
68
+ /**
69
+ * The batch-capable subset. This is the gate centrs enforces for `--via ssh`
70
+ * and `transfer --via sftp`: an empty list means no non-interactive handoff is
71
+ * possible, so centrs must emit a typed unsupported-capability error rather
72
+ * than prompt for a password or silently fall back (#134 Phase 4).
73
+ */
74
+ batchModes: Array<"private-key" | "agent-or-config">;
75
+ passwordAvailable?: boolean;
76
+ }
77
+
78
+ /** Fields shared by every available endpoint regardless of auth flavor. */
79
+ interface AvailableEndpointBase {
80
+ available: true;
81
+ /** Hostname/IP to dial — NOT a port. `"127.0.0.1"` for a SLiRP-forwarded service. */
82
+ host: string;
83
+ port: number;
84
+ guestPort?: number;
85
+ transport: "tcp" | "udp";
86
+ /** True for TLS-backed endpoints (https / api-ssl); SSH is always `false` (own transport security). */
87
+ tls: boolean;
88
+ url?: string;
89
+ source?: ServiceEndpointSource;
90
+ }
91
+
92
+ /** An endpoint the provider could not vouch for; a consumer MUST gate on `available` before dialing. */
93
+ export interface UnavailableEndpoint {
94
+ available: false;
95
+ unavailableReason: string;
96
+ /** Best-effort echoes only — never dial these without a re-resolve. */
97
+ host?: string;
98
+ port?: number;
99
+ guestPort?: number;
100
+ transport?: "tcp" | "udp";
101
+ tls?: boolean;
102
+ url?: string;
103
+ source?: ServiceEndpointSource;
104
+ }
105
+
106
+ /** One per-service endpoint (rest-api / native-api). */
107
+ export type ServiceEndpoint =
108
+ | (AvailableEndpointBase & { auth?: ServiceEndpointAuth })
109
+ | UnavailableEndpoint;
110
+
111
+ /** The SSH endpoint — same union, with SSH-specific auth substituted. */
112
+ export type SshServiceEndpoint =
113
+ | (AvailableEndpointBase & { auth: SshEndpointAuth })
114
+ | UnavailableEndpoint;
115
+
116
+ /** Any resolved endpoint, whatever the service. */
117
+ export type AnyServiceEndpoint = ServiceEndpoint | SshServiceEndpoint;
118
+
119
+ /**
120
+ * The endpoint type for a given `--via` id. `ssh` carries the SSH-specific
121
+ * {@link SshServiceEndpoint} (its available form guarantees `auth: SshEndpointAuth`
122
+ * with `batchModes`, the gate Phase 4 enforces for `--via ssh`/`transfer --via
123
+ * sftp`); every other transport uses the shared credential {@link ServiceEndpoint}.
124
+ */
125
+ export type ServiceEndpointFor<K extends RouterOsProtocol> = K extends "ssh"
126
+ ? SshServiceEndpoint
127
+ : ServiceEndpoint;
128
+
129
+ /**
130
+ * A member's per-service connection facts, keyed by centrs `--via` id.
131
+ *
132
+ * Partial by design: a provider may cover only a subset of the eight protocols
133
+ * (quickchr v1 covers `rest-api`/`native-api`/`ssh`). A `--via` whose key is
134
+ * absent is a typed unsupported-provider error at consume time, not a silent
135
+ * fallback. CDB never populates this map (its single endpoint is materialized by
136
+ * `./target.ts`); only named-live-providers do.
137
+ *
138
+ * Keyed by service so per-`--via` precision survives: `map.ssh` is a
139
+ * {@link SshServiceEndpoint}, not a widened `AnyServiceEndpoint`, so a consumer
140
+ * reading `map.ssh` keeps the SSH `auth`/`batchModes` guarantee without a cast.
141
+ */
142
+ export type ServiceEndpointMap = {
143
+ [K in RouterOsProtocol]?: ServiceEndpointFor<K>;
144
+ };
145
+
146
+ /**
147
+ * Narrow an endpoint to its available form (the only form safe to dial). Generic
148
+ * so the caller's precise union is preserved: an {@link SshServiceEndpoint} narrows
149
+ * to its available arm (`auth: SshEndpointAuth` required), not the widened base.
150
+ */
151
+ export function isEndpointAvailable<T extends AnyServiceEndpoint>(
152
+ endpoint: T,
153
+ ): endpoint is Extract<T, { available: true }> {
154
+ return endpoint.available;
155
+ }
@@ -455,6 +455,6 @@ export function defaultPortForScheme(scheme: "http" | "https"): number {
455
455
  return scheme === "https" ? 443 : 80;
456
456
  }
457
457
 
458
- function formatHostForUrl(host: string): string {
458
+ export function formatHostForUrl(host: string): string {
459
459
  return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
460
460
  }
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Boundaries (see `commands/retrieve/README.md`, `docs/CONSTITUTION.md`), shared
14
14
  * with `api-fanout.ts`:
15
- * - The CDB is loaded + decrypted ONCE (`expandCdbSelection`); a cdb member is
15
+ * - The CDB is loaded + decrypted ONCE (`expandSelection`); a cdb member is
16
16
  * resolved with its pre-resolved record, so `resolveCdb`'s `__default__`
17
17
  * synthetic fallback never collides ad-hoc literals.
18
18
  * - A `--default` / `__default__` member is guarded from being dialed as the
@@ -49,12 +49,12 @@ import {
49
49
  import { CentrsError, serializeCentrsError } from "./errors.ts";
50
50
  import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
51
51
  import {
52
- type CdbSelectionExpansion,
53
- type CdbSelectionMember,
54
- type CdbSelectionResolveInput,
55
- expandCdbSelection,
52
+ expandSelection,
56
53
  isDefaultRecordTarget,
57
54
  loadEnvFileDefaults,
55
+ type SelectionExpansion,
56
+ type SelectionMember,
57
+ type SelectionResolveInput,
58
58
  type TargetSelection,
59
59
  } from "./resolver/index.ts";
60
60
  import {
@@ -82,6 +82,8 @@ export interface RetrieveSelectionSummary {
82
82
  all: boolean;
83
83
  default: boolean;
84
84
  positionals: readonly string[];
85
+ /** quickchr machine names (`--quickchr`, repeatable; exclusive of the rest). */
86
+ quickchr?: readonly string[];
85
87
  }
86
88
 
87
89
  export interface RetrieveFanoutOperationMeta {
@@ -109,10 +111,10 @@ export interface RetrieveFanoutInternals {
109
111
  /** Selection expansion. Defaults to the real CDB-backed expansion. */
110
112
  expand?: (
111
113
  selection: TargetSelection,
112
- input: CdbSelectionResolveInput,
114
+ input: SelectionResolveInput,
113
115
  env: Record<string, string | undefined>,
114
116
  config?: Record<string, string | undefined>,
115
- ) => Promise<CdbSelectionExpansion>;
117
+ ) => Promise<SelectionExpansion>;
116
118
  /** Backoff sleeper. Defaults to a real timer; tests pass a no-op. */
117
119
  sleep?: (ms: number) => Promise<void>;
118
120
  }
@@ -132,10 +134,11 @@ function summarizeSelection(
132
134
  all: selection.all,
133
135
  default: selection.default,
134
136
  positionals: selection.positionals,
137
+ quickchr: selection.quickchr,
135
138
  };
136
139
  }
137
140
 
138
- function memberTargetMeta(member: CdbSelectionMember): {
141
+ function memberTargetMeta(member: SelectionMember): {
139
142
  input?: string;
140
143
  identity?: string;
141
144
  recordIndex?: number;
@@ -147,6 +150,9 @@ function memberTargetMeta(member: CdbSelectionMember): {
147
150
  recordIndex: member.recordIndex,
148
151
  };
149
152
  }
153
+ if (member.kind === "quickchr") {
154
+ return { input: member.name, identity: member.name };
155
+ }
150
156
  return { input: member.input };
151
157
  }
152
158
 
@@ -159,7 +165,7 @@ function memberTargetMeta(member: CdbSelectionMember): {
159
165
  */
160
166
  function relabelAdhocMember(
161
167
  envelope: RetrieveEnvelope,
162
- member: CdbSelectionMember,
168
+ member: SelectionMember,
163
169
  ): RetrieveEnvelope {
164
170
  if (member.kind !== "literal") {
165
171
  return envelope;
@@ -194,7 +200,7 @@ export async function retrieveFanout(
194
200
  );
195
201
  const selectionSummary = summarizeSelection(selection);
196
202
 
197
- const expand = internals.expand ?? expandCdbSelection;
203
+ const expand = internals.expand ?? expandSelection;
198
204
  const expansion = await expand(
199
205
  selection,
200
206
  {
@@ -234,7 +240,7 @@ export async function retrieveFanout(
234
240
  const sleep = internals.sleep ?? defaultFanoutSleep;
235
241
 
236
242
  const targets = await runFanout<
237
- CdbSelectionMember,
243
+ SelectionMember,
238
244
  ResolvedRetrieveRequest,
239
245
  RetrieveEnvelope
240
246
  >({
@@ -267,6 +273,14 @@ export async function retrieveFanout(
267
273
  config,
268
274
  );
269
275
  }
276
+ if (member.kind === "quickchr") {
277
+ // The live descriptor resolves here, per member — a stopped/unknown
278
+ // machine is an inner per-target failure, not an expansion error.
279
+ return resolveRetrieveRequest(
280
+ { ...request, targetInput: undefined, quickchr: member.name },
281
+ env,
282
+ );
283
+ }
270
284
  // Ad-hoc literal: full single-target resolution (CDB load → `__default__`
271
285
  // fallback creds), mirroring single-target `retrieve`.
272
286
  return resolveRetrieveRequest(
@@ -373,7 +387,7 @@ function resolveFanoutConcurrencyProtocol(
373
387
  request: RetrieveRequest,
374
388
  env: Record<string, string | undefined>,
375
389
  globalVia: RouterOsProtocol,
376
- expansion: CdbSelectionExpansion,
390
+ expansion: SelectionExpansion,
377
391
  ): RouterOsProtocol {
378
392
  // Deliberately CLI/env only, not `config` — a `centrs.env` default is the
379
393
  // weakest precedence tier and must not be treated as a hard pin the way an
package/src/retrieve.ts CHANGED
@@ -26,11 +26,14 @@ import {
26
26
  type RouterOsProtocol,
27
27
  } from "./protocols/index.ts";
28
28
  import {
29
+ assertNoQuickchrOverrideConflict,
29
30
  type CdbResolution,
30
31
  isIpTransport,
31
32
  loadEnvFileDefaults,
32
33
  parseDuration,
33
34
  parseResolvePolicy,
35
+ type QuickchrResolution,
36
+ quickchrConnection,
34
37
  type ResolvedAuth,
35
38
  type ResolvedSetting,
36
39
  type ResolvedTarget,
@@ -39,6 +42,7 @@ import {
39
42
  resolveCdb,
40
43
  resolveMacTarget,
41
44
  resolveOptionalIntegerSetting,
45
+ resolveQuickchrTarget,
42
46
  resolveStringSetting,
43
47
  resolveTarget,
44
48
  toCoreSource,
@@ -49,6 +53,12 @@ export type RetrieveOutputFormat = (typeof retrieveOutputFormats)[number];
49
53
 
50
54
  export interface RetrieveRequest {
51
55
  targetInput?: string;
56
+ /**
57
+ * quickchr machine name (`--quickchr <name>`): resolve host/port/auth from
58
+ * the live VM descriptor instead of CDB/env (`docs/CONSTITUTION.md` →
59
+ * Resolution providers). Conflicts with host/port/username/password.
60
+ */
61
+ quickchr?: string;
52
62
  path: string;
53
63
  via?: string;
54
64
  host?: string;
@@ -123,6 +133,14 @@ export interface ResolvedRetrieveRequest {
123
133
  timeoutMs: ResolvedSetting<number>;
124
134
  format: ResolvedSetting<RetrieveOutputFormat>;
125
135
  validate: ResolvedSetting<boolean>;
136
+ /**
137
+ * Relax TLS peer verification. retrieve has no `--insecure` flag; this is set
138
+ * only by the quickchr provider (trust-by-provenance for the VM's self-signed
139
+ * TLS forward — see `src/resolver/quickchr-provider.ts`), so when present its
140
+ * source is always `provider` and it is surfaced as `meta.settings.insecure`
141
+ * for auditability.
142
+ */
143
+ insecure?: ResolvedSetting<boolean>;
126
144
  maxResultsBytes?: ResolvedSetting<number>;
127
145
  attributes: readonly string[];
128
146
  allAttributes: boolean;
@@ -161,6 +179,7 @@ export async function runResolvedRetrieve(
161
179
  username: resolved.auth.username,
162
180
  password: resolved.auth.password,
163
181
  timeoutMs: resolved.timeoutMs.value,
182
+ insecure: resolved.insecure?.value,
164
183
  });
165
184
 
166
185
  try {
@@ -212,7 +231,8 @@ export async function runResolvedRetrieve(
212
231
  "Check the attribute name, or use `--list-attributes` to inspect the available properties first.",
213
232
  context: {
214
233
  path: resolved.path,
215
- requested: resolved.attributes,
234
+ parameter: missing[0],
235
+ requestedAttributes: resolved.attributes,
216
236
  availableAttributes,
217
237
  },
218
238
  });
@@ -462,6 +482,9 @@ function metaFromResolved(
462
482
  timeoutMs: toCoreSource(resolved.timeoutMs.source),
463
483
  format: toCoreSource(resolved.format.source),
464
484
  validate: toCoreSource(resolved.validate.source),
485
+ insecure: resolved.insecure
486
+ ? toCoreSource(resolved.insecure.source)
487
+ : undefined,
465
488
  maxResultsBytes: resolved.maxResultsBytes
466
489
  ? toCoreSource(resolved.maxResultsBytes.source)
467
490
  : undefined,
@@ -620,6 +643,7 @@ export function buildResolvedRetrieve(
620
643
  attributeSelections: readonly string[],
621
644
  macResolution?: { mac: string; ip: string },
622
645
  config: Record<string, string | undefined> = {},
646
+ quickchrResolution?: QuickchrResolution,
623
647
  ): ResolvedRetrieveRequest {
624
648
  const via = resolveProtocol(request, env, cdbResolution, config);
625
649
  const format = resolveFormat(request, env, config);
@@ -647,22 +671,33 @@ export function buildResolvedRetrieve(
647
671
  undefined,
648
672
  config,
649
673
  );
650
- const target = resolveTarget(
651
- {
652
- targetInput: request.targetInput,
653
- host: request.host,
654
- port: request.port,
655
- macResolution,
656
- },
657
- env,
658
- via.value,
659
- cdbResolution,
660
- );
661
- const auth = resolveAuth(
662
- { username: request.username, password: request.password },
663
- env,
664
- cdbResolution,
665
- );
674
+ // A quickchr member substitutes the live descriptor's per-`--via` endpoint
675
+ // for the CDB/env target+auth path (`docs/CONSTITUTION.md` → Resolution
676
+ // providers); everything else (via/format/validate/timeout) resolved above
677
+ // stays on the normal ladder.
678
+ const connection = quickchrResolution
679
+ ? quickchrConnection(quickchrResolution, via.value)
680
+ : null;
681
+ const target = connection
682
+ ? connection.target
683
+ : resolveTarget(
684
+ {
685
+ targetInput: request.targetInput,
686
+ host: request.host,
687
+ port: request.port,
688
+ macResolution,
689
+ },
690
+ env,
691
+ via.value,
692
+ cdbResolution,
693
+ );
694
+ const auth = connection
695
+ ? connection.auth
696
+ : resolveAuth(
697
+ { username: request.username, password: request.password },
698
+ env,
699
+ cdbResolution,
700
+ );
666
701
 
667
702
  return {
668
703
  path: request.path,
@@ -672,12 +707,24 @@ export function buildResolvedRetrieve(
672
707
  timeoutMs,
673
708
  format,
674
709
  validate,
710
+ insecure:
711
+ connection?.insecure === true && quickchrResolution
712
+ ? {
713
+ value: true,
714
+ source: {
715
+ kind: "provider" as const,
716
+ key: `quickchr:${quickchrResolution.name}`,
717
+ },
718
+ }
719
+ : undefined,
675
720
  maxResultsBytes,
676
721
  attributes: attributeSelections,
677
722
  allAttributes: request.allAttributes ?? false,
678
723
  listAttributes: request.listAttributes ?? false,
679
724
  verbose: request.verbose ?? false,
680
- warnings: cdbResolution?.warnings ?? [],
725
+ warnings: connection
726
+ ? [...connection.warnings]
727
+ : (cdbResolution?.warnings ?? []),
681
728
  };
682
729
  }
683
730
 
@@ -758,6 +805,22 @@ export async function resolveRetrieveRequest(
758
805
  const attributeSelections = validateRetrieveRequestShape(request);
759
806
  const config = await loadEnvFileDefaults(env);
760
807
 
808
+ // A quickchr target bypasses CDB and MAC resolution entirely — the live
809
+ // descriptor is the only connection-fact source for those fields.
810
+ if (request.quickchr !== undefined) {
811
+ assertNoQuickchrOverrideConflict(request, request.quickchr);
812
+ const quickchrResolution = await resolveQuickchrTarget(request.quickchr);
813
+ return buildResolvedRetrieve(
814
+ request,
815
+ env,
816
+ undefined,
817
+ attributeSelections,
818
+ undefined,
819
+ config,
820
+ quickchrResolution,
821
+ );
822
+ }
823
+
761
824
  const cdbResolution = await resolveCdb(
762
825
  {
763
826
  targetInput: request.targetInput,