feat(api): descriptor v1 — structured per-service contract for centrs (#71)#84
Conversation
…#71) Rewrite ChrInstance.descriptor()/quickchr inspect from the flat MachineDescriptor (ports/urls/auth/env blob) to the structured, versioned Descriptor contract specified in docs/centrs-interface.md: a per-service services map (rest-api/native-api/ssh) with tls, availability, and provenance, plus optional customForwards and networks topology. This is the bridge tikoci/centrs#134 (--quickchr <name>) resolves connection facts through instead of reading machine.json directly. Breaking change (accepted pre-1.0, no external consumers yet): MachineDescriptor is deleted and replaced by Descriptor. The env field is dropped from the descriptor entirely — quickchr env now calls ChrInstance.subprocessEnv() directly instead of reading descriptor().env, which also fixes a latent inaccuracy (README.md already documented env as subprocessEnv()-backed; the code didn't match). Implementation decisions beyond a literal transcription of the spec (folded back into docs/centrs-interface.md): - TLS/http-preference logic (secure port preferred, plain fallback) is new — there was no pre-existing "REST-URL preference logic" to keep. As a side effect this closes a latent bug where excluding "http" while keeping "https" left the REST base pointing at a port that didn't exist. - services["rest-api"].url includes the /rest path suffix; native-api's does not (raw TCP/TLS socket, not an HTTP path). - ssh.auth.batchModes never includes "agent-or-config" (quickchr can't vouch for host ssh-agent/config policy either way) — only "private-key" once batchVerified, matching this doc's own Done-when example for the unverified case. - No new MACHINE_NOT_FOUND throw was added: QuickCHR.get(name) returning undefined is already the typed absent-value signal; documented rather than inventing a getOrThrow-style method. Tests: new test/unit/descriptor.test.ts covers the full Done-when checklist (versioning, unknown-field tolerance, all three service keys with tls, SSH auth-mode reporting verified/unverified/absent, disableAdmin-no-user availability, customForwards, networks topology, excludePorts fallback). test/unit/cli-env-inspect.test.ts updated for the new CLI-level shape. test/integration/provisioning.test.ts extended to assert descriptor().services.ssh reflects a real batch-verified key end-to-end. examples/harness/harness.ts updated for the new shape. Docs-only follow-up (CHANGELOG, MANUAL/README, restraml/donny heads-up) tracked separately to keep this PR focused on the implementation.
There was a problem hiding this comment.
Pull request overview
Implements the new Descriptor v1 connection-handoff contract (per docs/centrs-interface.md) by replacing the legacy flat MachineDescriptor with a structured, versioned, per-service services map and related topology/forward metadata, aligning ChrInstance.descriptor() and quickchr inspect with centrs’ --quickchr <name> integration needs.
Changes:
- Introduces the public Descriptor v1 types/constants (
Descriptor,ServiceEndpoint,SshServiceEndpoint,SERVICE_IDS,QUICKCHR_DESCRIPTOR_VERSION) and updates exports. - Reworks
ChrInstance.descriptor()implementation to emit per-service endpoints (TLS preference, availability reasons, SSH auth-mode reporting) pluscustomForwardsandnetworks. - Updates CLI behavior (
quickchr envnow callssubprocessEnv()directly) and adds/updates unit + integration coverage and example usage.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/lib/types.ts |
Defines Descriptor v1 contract types and exports service ID/version constants. |
src/lib/quickchr.ts |
Implements Descriptor v1 construction from MachineState (services/customForwards/networks). |
src/index.ts |
Re-exports new descriptor-related types/constants from the public package entrypoint. |
src/cli/index.ts |
Updates CLI to guard stopped machines for env, uses subprocessEnv() directly, and refreshes inspect help text. |
test/unit/descriptor.test.ts |
Adds a comprehensive unit test suite for Descriptor v1 mapping rules and edge cases. |
test/unit/cli-env-inspect.test.ts |
Updates CLI tests to assert the new v1 descriptor shape and env behavior. |
test/integration/provisioning.test.ts |
Extends integration coverage to ensure descriptor SSH batch auth reflects real managed-key verification. |
examples/harness/harness.ts |
Updates example to use services["rest-api"] (v1 shape) instead of legacy flat URLs. |
docs/centrs-interface.md |
Updates/clarifies the contract text to match implementation decisions and semantics. |
…tinel Copilot review on #84: state.user existing wasn't sufficient to gate services["rest-api"]/["native-api"] availability — when state.user.password is the STORED_IN_SECRETS_PASSWORD sentinel but the per-instance credential store has no matching entry (deleted/corrupted out-of-band), resolveAuth/ resolveCreds fall through to returning the literal sentinel string as the "password". Split the availability check into disableAdminLockout (the existing disableAdmin-no-user case) and sentinelUnresolvable (new), so the sentinel case is caught regardless of disableAdmin. Also (same review): fixed the "no apiVersion" unit test assertion to check the correct nesting level (descriptor.quickchr, not the descriptor root). Copilot's third finding — that QuickCHR.get() returns undefined rather than null — doesn't hold: get()'s signature is `ChrInstance | null` (src/lib/quickchr.ts:1924), so `expect(instance).not.toBeNull()` is the correct assertion; left unchanged with a grounded reply on the PR thread.
|
@coderabbitai full review |
✅ Action performedFull review finished. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
📜 Recent review details⏰ Context from checks skipped due to timeout. (1)
|
| Layer / File(s) | Summary |
|---|---|
Descriptor contract and public exports src/lib/types.ts, src/index.ts, docs/centrs-interface.md |
Defines Descriptor v1 types, service identifiers, versioning, endpoint/auth rules, missing-machine semantics, TLS selection, and dropped fields. |
Service descriptor construction src/lib/quickchr.ts |
Builds service-based descriptors with TLS-aware endpoints, credential availability, SSH authentication modes, custom forwards, network topology, and version metadata. |
CLI running-state and environment handling src/cli/index.ts |
Centralizes running-machine validation, makes env use subprocessEnv(), and updates inspect help text. |
Descriptor and CLI behavior validation test/unit/descriptor.test.ts, test/unit/cli-env-inspect.test.ts, test/integration/provisioning.test.ts, examples/harness/harness.ts |
Validates descriptor shape, endpoint availability, credentials, managed SSH keys, forwards, networks, stopped-machine errors, CLI output, and REST URL mapping. |
Estimated code review effort: 4 (Complex) | ~45 minutes
Sequence Diagram(s)
sequenceDiagram
participant CLI
participant ChrInstance
participant buildDescriptor
CLI->>ChrInstance: request descriptor()
ChrInstance->>buildDescriptor: construct Descriptor
buildDescriptor-->>ChrInstance: services and authentication
ChrInstance-->>CLI: Descriptor JSON
Possibly related issues
- tikoci/quickchr#71 — Defines the descriptor contract, service keys, endpoint/auth semantics, versioning, and CLI/API integration implemented here.
Possibly related PRs
- tikoci/quickchr#22 — Related harness validation for the descriptor REST API URL and
/restsuffix. - tikoci/quickchr#82 — Adds the managed SSH key state consumed here to gate descriptor SSH authentication.
- tikoci/quickchr#83 — Updates managed SSH key verification assertions used by the descriptor SSH auth mapping.
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |
✅ Passed checks (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | Accurately summarizes the main change: a versioned per-service descriptor contract for centrs. |
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
✨ Finishing Touches
📝 Generate docstrings
- Create stacked PR
- Commit on current branch
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Commit unit tests in branch
feat/71-descriptor-v1
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
…85) ## Summary Docs-only follow-up to #84 (descriptor v1 implementation). Kept separate so the code PR stayed focused on implementation review; this one is a fast, low-stakes prose pass. - `CHANGELOG.md`: `[Unreleased]` entry for the breaking `MachineDescriptor` → `Descriptor` restructure. - `MANUAL.md` / `README.md`: `inspect`/`descriptor()`/`env` sections rewritten for the v1 `services` shape; note `env`'s removal and the `subprocessEnv()` split. - `BACKLOG.md`: drops the `#71` anchor entry now that the work has landed. Closes #71. ## Cross-repo heads-up Per `docs/centrs-interface.md`'s backward-compatibility section, a breaking `MachineDescriptor` restructure requires flagging it to any repo that reads `descriptor()`/`MachineDescriptor` today (`restraml`, `donny` per #50's consumer list). I checked both local checkouts directly (`grep -rl "MachineDescriptor\|\.descriptor(" src`) — **neither repo's own source actually calls `.descriptor()` or references `MachineDescriptor` today**; both only use `QuickCHR.start()` (unaffected) and `subprocessEnv()`-backed env vars (unaffected, untouched by this change). So this is a low-stakes FYI rather than an active break. I'll file a short heads-up issue on each repo referencing this change, for the record and in case future work there adds a `descriptor()` dependency. ## Test plan - [x] `bun run check` clean (biome, tsc, markdownlint, cspell, examples, shellcheck). - [x] Confirmed via grep that restraml/donny don't consume the changed surface today. - [ ] Reviewer: skim CHANGELOG/MANUAL/README changes for accuracy against the merged #84 shape.
Summary
Rewrites
ChrInstance.descriptor()/quickchr inspectfrom the flatMachineDescriptor(ports/urls/auth/envblob) to the structured,versioned
Descriptorcontract specified indocs/centrs-interface.md: a per-serviceservicesmap (rest-api/native-api/ssh) withtls, availability, andprovenance, plus optional
customForwardsandnetworkstopology. This isthe bridge
tikoci/centrs#134(--quickchr <name>) resolves connectionfacts through, instead of reading
machine.jsondirectly.Part of #71 (implementation phases 1–3 of the doc's staged plan). Issue stays open until the follow-up docs/heads-up PR also lands. A
follow-up PR handles CHANGELOG/MANUAL/README updates and the restraml/donny
consumer heads-up so this PR stays focused on the implementation itself.
Breaking change (accepted pre-1.0, no external consumers yet)
MachineDescriptoris deleted, replaced byDescriptor.envfield is dropped from the descriptor entirely —quickchr envnow callsChrInstance.subprocessEnv()directly instead of readingdescriptor().env. This also fixes a latent inaccuracy:README.mdalready documented
envassubprocessEnv()-backed, but the code didn'tmatch until now.
Implementation decisions beyond a literal transcription of the spec
These came up while implementing against current
mainand are folded backinto
docs/centrs-interface.mdso the doc stays the source of truth:"REST-URL preference logic" to keep (the doc's phrasing assumed one
existed). Implemented: prefer the secure port (
https/api-ssl) when itsforward exists, fall back to the plain one,
available:falseonly ifneither does. Side effect: closes a latent bug where excluding
"http"while keeping
"https"left the REST base pointing at a port that didn'texist.
services["rest-api"].urlincludes the/restpath suffix (the actualbase a consumer dials);
services["native-api"].urldoes not (rawTCP/TLS socket, not an HTTP path).
ssh.auth.batchModesnever includes"agent-or-config"— quickchr can'tvouch for host
ssh-agent/~/.ssh/configpolicy either way. Only"private-key"oncebatchVerified, matching the doc's own Done-whenexample for the unverified case. (
"agent-or-config"still appears inthe broader
modesarray.)MACHINE_NOT_FOUNDthrow was added —QuickCHR.get(name)returning
undefinedis already the typed absent-value signal; this isnow documented explicitly rather than inventing a
getOrThrow-stylemethod nobody asked for.
Testing
test/unit/descriptor.test.tscovers the full Done-when checklist:descriptorVersion/packageVersion, unknown-field forward-compattolerance, all three service keys with correct
tls, SSH auth-modereporting in verified/unverified/absent-key states,
disableAdmin-no-useravailability,
customForwardsshape,networkstopology, and theexcludePortshttp/https fallback.test/unit/cli-env-inspect.test.tsupdated for the new CLI-level shape(including a new "env also refuses stopped machines" case).
test/integration/provisioning.test.tsextended to assertdescriptor().services.sshreflects a real batch-verified managed SSH keyend-to-end (not just the raw
managedSshKeystate).examples/harness/harness.tsupdated for the new shape.Locally verified (this machine has QEMU/HVF):
bun run check— clean (biome, tsc, markdownlint, cspell, examples,shellcheck).
bun test test/unit/— 703 pass, 0 fail.QUICKCHR_INTEGRATION=1 bun test test/integration/(full suite,linux-independent local run) — 57 pass, 0 fail, 11 skipped (arm64/cross-arch,
expected on this Intel host).
quickchr start --secure-login+quickchr inspect --jsonagainst a live CHR and eyeballed the v1 shape, including a real
batch-verified SSH key reporting
batchModes: ["private-key"].examples/harness/harness.tsend-to-end against a live CHR.Test plan for reviewers
src/lib/types.tsnew types againstdocs/centrs-interface.md's"Descriptor v1 shape" section for shape fidelity.
buildDescriptor()insrc/lib/quickchr.tsfor the mappingrules (SSH gating, TLS preference,
disableAdminavailability).test/unit/descriptor.test.tsscenarios match the Done-whenchecklist in the doc.
Summary by CodeRabbit
New Features
Bug Fixes
quickchr envnow reliably provides subprocess environment variables.Documentation