Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions docs/centrs-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ interface Descriptor {
descriptorVersion: 1;
quickchr: { packageVersion: string }; // no apiVersion — see "Dropped fields"
status: "running"; // descriptor() only ever returns a running shape;
// stopped/missing throw MACHINE_STOPPED / MACHINE_NOT_FOUND as today
// stopped machines throw MACHINE_STOPPED as today

// machine identity — carried over from the current MachineDescriptor as top-level fields
name: string;
Expand All @@ -119,6 +119,15 @@ interface Descriptor {
}
```

**Missing-machine semantics:** a missing machine name never reaches this type at all —
`QuickCHR.get(name)` returns `undefined` for that case, which is already a typed (TS)
absent-value signal. There is no separate `MACHINE_NOT_FOUND` throw from `descriptor()`
or anywhere else reachable from it; a consumer should treat `QuickCHR.get(name) ===
undefined` as its own not-found case rather than expecting a thrown error. (Confirmed
during implementation: no code path existed that threw `MACHINE_NOT_FOUND` for a missing
name before this work, and none was added — inventing a `getOrThrow`-style method for
this wasn't asked for and isn't needed.)

### `ServiceEndpoint` (rest-api, native-api)

One per-service endpoint shape. Centrs reuses this same internal type for TikTOML
Expand Down Expand Up @@ -248,17 +257,19 @@ Mapping rules:
emit an unverified path.
- `auth.batchModes` = `["private-key"]` when `batchVerified === true`, else `[]`.
Never advertise `"private-key"` in `batchModes` off an absent/unverified key.
- `"agent-or-config"` may appear in `batchModes` independent of `managedSshKey` — that
is host `ssh-agent` / `~/.ssh/config` policy, which quickchr cannot verify. Include
it as a mode centrs *may* try, not one quickchr vouches for.
- `"agent-or-config"` reflects host `ssh-agent` / `~/.ssh/config` policy, which
quickchr cannot verify either way — it is always included in `modes` (a mode centrs
*may* try) but **never** added to `batchModes` (implemented: `batchModes` only ever
contains `"private-key"` when `batchVerified === true`, or is empty — matching this
doc's own Done-when example for the unverified/absent case, `batchModes: []`).
- `auth.username` = `state.user?.name` (the managed/provisioned user, e.g.
`"quickchr"`) when a managed user exists, else `"admin"`.
- `auth.passwordAvailable` = whether the credential store has a resolvable password for
that user today (same source `resolveAuth`/`resolveCreds` use — `src/lib/auth.ts`).
- **Absent `managedSshKey`** (machine started with `--no-secure-login`, or an explicit
`--user`/`--password` override that skips managed-key install per
`provision.ts:396`): no key path, `batchModes: []`, and `modes` still includes
`"password"` when `passwordAvailable`.
`provision.ts:484-489` / `:506` / `:528`): no key path, `batchModes: []`, and `modes`
still includes `"password"` when `passwordAvailable`.
- Algorithm/version grounding: `test/lab/ssh-keys/REPORT.md`.

### TLS
Expand All @@ -267,11 +278,24 @@ Mapping rules:
quickchr already tracks plain/TLS pairs separately (`ChrPorts.http`/`https`,
`.api`/`.apiSsl` — `src/lib/types.ts:376`). Populate:

- `services["rest-api"]` → pick `https`/`http` per the existing REST-URL preference
logic; set `tls` to match which was chosen.
- `services["rest-api"]` → pick `https`/`http`; set `tls` to match which was chosen.
- `services["native-api"]` → pick `apiSsl`/`api`; set `tls` to match.
- `services.ssh` → always `tls: false`.

**Implementation note:** there was no pre-existing "REST-URL preference logic" to reuse
here — before this issue, `restUrl` (used for every REST call, and for the old flat
descriptor's `urls.http`/`rest`/`restBase`) was unconditionally built from `ports.http`;
`https`/`apiSsl` were just additive fields, never preferred. The secure-preferred,
plain-fallback logic above was written fresh as part of implementing this contract. As a
side effect it also closes a latent bug: `excludePorts` had no guard against excluding
`"http"` while keeping `"https"`, which previously left the REST base pointing at a port
that didn't exist — `services["rest-api"]` now falls back to the surviving port instead.

`services["rest-api"].url` includes the `/rest` path suffix (e.g.
`https://127.0.0.1:19101/rest`) since that's the actual base a consumer dials for
RouterOS's REST endpoints; `services["native-api"].url` has no such suffix (it's a raw
TCP/TLS socket, not an HTTP path).

Consumer scope note (do **not** build this into quickchr): centrs auto-preferring the
secure variant when both are open is a centrs-side (`tikoci/centrs#134`) decision.
Leave a pointer comment; don't add preference logic to quickchr.
Expand Down Expand Up @@ -309,6 +333,13 @@ name. In the new shapes, `host` is always the literal `"127.0.0.1"` string;
one for this issue.
- **Old flat `ports` / `urls` / top-level `auth`** — replaced by `services`. (The
restructure is allowed; see below.)
- **`env` — dropped, not carried into `Descriptor` at all.** The old `MachineDescriptor`
embedded a subprocess-env map (`env: Record<string,string>`, built the same way as
`ChrInstance.subprocessEnv()`) and the CLI's `quickchr env` command read it via
`descriptor().env` instead of calling `subprocessEnv()` directly. Fixed as part of
this implementation: `quickchr env` now calls `instance.subprocessEnv()` directly,
matching what `README.md` already documented. `subprocessEnv()` itself is unchanged
and remains the one place to get env-var-shaped connection facts.

---

Expand Down
6 changes: 4 additions & 2 deletions examples/harness/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* read `machine.json`. Use the stable connection surface:
*
* - `instance.subprocessEnv()` → env vars (`URLBASE`, `BASICAUTH`, …) for a child
* - `instance.descriptor()` → a structured `{ urls, auth, ports, status, … }`
* - `instance.descriptor()` → a structured `{ services, status, … }` (issue #71)
*
* Both are **secret-bearing** (real credentials) — treat their output like a
* password: don't log it, don't write it to artifacts. `BASICAUTH` is the raw
Expand Down Expand Up @@ -38,7 +38,9 @@ if (import.meta.main) {
// The structured descriptor — what a harness records instead of machine.json.
const desc = await chr.descriptor();
check(desc.status === "running", "descriptor status should be running");
check(desc.urls.rest.includes("/rest"), "descriptor REST url should contain /rest");
const restApi = desc.services["rest-api"];
check(restApi.available, "rest-api service should be available");
check(restApi.available && restApi.url?.includes("/rest") === true, "descriptor REST url should contain /rest");

// Hand the connection env to a separate process; let it talk to the CHR.
const env = await chr.subprocessEnv();
Expand Down
26 changes: 20 additions & 6 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1490,14 +1490,26 @@ function shellQuoteEnv(value: string): string {
return `'${value.replaceAll("'", "'\\''")}'`;
}

async function getRunningDescriptor(name: string) {
async function getRunningInstance(name: string) {
const { QuickCHR } = await import("../lib/quickchr.ts");
const { machineNotFoundMessage } = await import("./format.ts");
const instance = QuickCHR.get(name);
if (!instance) {
console.error(machineNotFoundMessage(name));
process.exit(1);
}
if (instance.state.status !== "running") {
const { QuickCHRError } = await import("../lib/types.ts");
throw new QuickCHRError(
"MACHINE_STOPPED",
`Machine "${name}" must be running to inspect its connection environment (current status: ${instance.state.status}).`,
);
}
return instance;
}

async function getRunningDescriptor(name: string) {
const instance = await getRunningInstance(name);
return instance.descriptor();
}

Expand All @@ -1520,12 +1532,13 @@ async function cmdEnv(argv: string[] = []) {
console.error("Usage: quickchr env <name> [--json]");
process.exit(1);
}
const descriptor = await getRunningDescriptor(name);
const instance = await getRunningInstance(name);
const env = await instance.subprocessEnv();
if (asJson) {
console.log(JSON.stringify(descriptor.env, null, 2));
console.log(JSON.stringify(env, null, 2));
return;
}
for (const [key, value] of Object.entries(descriptor.env)) {
for (const [key, value] of Object.entries(env)) {
console.log(`${key}=${shellQuoteEnv(value)}`);
}
}
Expand Down Expand Up @@ -2846,8 +2859,9 @@ List all CHR instances or show detailed info for one.
case "inspect":
console.log(`quickchr inspect <name> [--json]

Print a stable JSON descriptor for a running CHR instance: ports, URLs,
credentials, status, and subprocess env vars.
Print a stable JSON descriptor for a running CHR instance: status, machine
identity, and per-service connection facts (rest-api, native-api, ssh).
Use 'quickchr env' for subprocess env vars.

<name> Name of a running CHR instance.
--json Accepted for parity; output is always JSON.`);
Expand Down
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export type {
Channel,
ChrInstance,
ChrPorts,
CustomForward,
Descriptor,
DeviceModeOptions,
DoctorCheck,
DoctorResult,
Expand All @@ -44,11 +46,11 @@ export type {
LicenseLevel,
LicenseOptions,
MachineConfig,
MachineDescriptor,
MachineState,
NetworkConfig,
NetworkMode,
NetworkSpecifier,
NetworkTopologyEntry,
PackageManager,
PlatformInfo,
PortMapping,
Expand All @@ -59,12 +61,14 @@ export type {
QgaNetworkIpAddress,
QgaOsInfo,
QgaTimezone,
ServiceEndpoint,
ServiceName,
SnapshotInfo,
SshServiceEndpoint,
StartOptions,
} from "./lib/types.ts";

export { QuickCHRError } from "./lib/types.ts";
export { QuickCHRError, SERVICE_IDS, QUICKCHR_DESCRIPTOR_VERSION } from "./lib/types.ts";
export type { ErrorCode, HostInterface } from "./lib/types.ts";

// Interface detection
Expand Down
Loading