Skip to content

feat(api): descriptor v1 — structured per-service contract for centrs (#71)#84

Merged
mobileskyfi merged 2 commits into
mainfrom
feat/71-descriptor-v1
Jul 10, 2026
Merged

feat(api): descriptor v1 — structured per-service contract for centrs (#71)#84
mobileskyfi merged 2 commits into
mainfrom
feat/71-descriptor-v1

Conversation

@mobileskyfi

@mobileskyfi mobileskyfi commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Rewrites 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.

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)

  • MachineDescriptor is deleted, replaced by Descriptor.
  • The env field is dropped from the descriptor entirely — quickchr env now calls ChrInstance.subprocessEnv() directly instead of reading
    descriptor().env. This also fixes a latent inaccuracy: README.md
    already documented env as subprocessEnv()-backed, but the code didn't
    match until now.

Implementation decisions beyond a literal transcription of the spec

These came up while implementing against current main and are folded back
into docs/centrs-interface.md so the doc stays the source of truth:

  • TLS/http-preference logic is new — there was no pre-existing
    "REST-URL preference logic" to keep (the doc's phrasing assumed one
    existed). Implemented: prefer the secure port (https/api-ssl) when its
    forward exists, fall back to the plain one, available:false only if
    neither does. Side effect: 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 (the actual
    base a consumer dials); services["native-api"].url 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/~/.ssh/config policy either way. Only
    "private-key" once batchVerified, matching the doc's own Done-when
    example for the unverified case. ("agent-or-config" still appears in
    the broader modes array.)
  • No new MACHINE_NOT_FOUND throw was added — QuickCHR.get(name)
    returning undefined is already the typed absent-value signal; this is
    now documented explicitly rather than inventing a getOrThrow-style
    method nobody asked for.

Testing

  • New test/unit/descriptor.test.ts covers the full Done-when checklist:
    descriptorVersion/packageVersion, unknown-field forward-compat
    tolerance, all three service keys with correct tls, SSH auth-mode
    reporting in verified/unverified/absent-key states, disableAdmin-no-user
    availability, customForwards shape, networks topology, and the
    excludePorts http/https fallback.
  • test/unit/cli-env-inspect.test.ts updated for the new CLI-level shape
    (including a new "env also refuses stopped machines" case).
  • test/integration/provisioning.test.ts extended to assert
    descriptor().services.ssh reflects a real batch-verified managed SSH key
    end-to-end (not just the raw managedSshKey state).
  • examples/harness/harness.ts updated 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).
  • Manually ran quickchr start --secure-login + quickchr inspect --json
    against a live CHR and eyeballed the v1 shape, including a real
    batch-verified SSH key reporting batchModes: ["private-key"].
  • Ran examples/harness/harness.ts end-to-end against a live CHR.

Test plan for reviewers

  • Skim src/lib/types.ts new types against docs/centrs-interface.md's
    "Descriptor v1 shape" section for shape fidelity.
  • Skim buildDescriptor() in src/lib/quickchr.ts for the mapping
    rules (SSH gating, TLS preference, disableAdmin availability).
  • Confirm test/unit/descriptor.test.ts scenarios match the Done-when
    checklist in the doc.

Summary by CodeRabbit

  • New Features

    • Added a stable, versioned machine descriptor with service availability, connection URLs, TLS, authentication, custom forwards, and network details.
    • Improved SSH authentication reporting, including verified private-key access.
    • Expanded the public API with descriptor and service endpoint types.
  • Bug Fixes

    • quickchr env now reliably provides subprocess environment variables.
    • Clarified handling for missing and stopped machines, including consistent errors.
  • Documentation

    • Updated CLI help and interface documentation with descriptor, service URL, and authentication rules.

…#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.
@mobileskyfi
mobileskyfi marked this pull request as ready for review July 9, 2026 23:08
Copilot AI review requested due to automatic review settings July 9, 2026 23:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) plus customForwards and networks.
  • Updates CLI behavior (quickchr env now calls subprocessEnv() 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.

Comment thread src/lib/quickchr.ts Outdated
Comment thread test/unit/descriptor.test.ts Outdated
Comment thread test/unit/descriptor.test.ts
…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.
@mobileskyfi

mobileskyfi commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@tikoci tikoci deleted a comment from coderabbitai Bot Jul 10, 2026
@tikoci tikoci deleted a comment from coderabbitai Bot Jul 10, 2026
@tikoci tikoci deleted a comment from coderabbitai Bot Jul 10, 2026
@tikoci tikoci deleted a comment from coderabbitai Bot Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3caa4ea1-4a61-45e0-8ef3-856151424bb6

📥 Commits

Reviewing files that changed from the base of the PR and between 22bfdb8 and b4e799c.

📒 Files selected for processing (9)
  • docs/centrs-interface.md
  • examples/harness/harness.ts
  • src/cli/index.ts
  • src/index.ts
  • src/lib/quickchr.ts
  • src/lib/types.ts
  • test/integration/provisioning.test.ts
  • test/unit/cli-env-inspect.test.ts
  • test/unit/descriptor.test.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: CodeRabbit / Review
⚠️ CI failures not shown inline (1)

GitHub Actions: CI / 1_Integration freshness.txt: feat(api): descriptor v1 — structured per-service contract for centrs (#71)

Conclusion: failure

View job details

##[group]Run bun run scripts/ci-freshness.ts
 �[36;1mbun run scripts/ci-freshness.ts�[0m
 shell: /usr/bin/bash -e {0}
 env:
   COVERAGE_MIN_FUNCS: 75
   COVERAGE_MIN_LINES: 60
   GH_REPO: tikoci/quickchr
   GH_***REDACTED***
 ##[endgroup]
 Integration freshness: FAIL — main integration is RED (failure) at 22bfdb8 — fix main first (or ground the failure via a targeted integration.yml dispatch): https://github.com/tikoci/quickchr/actions/runs/29051361192
 ##[error]Process completed with exit code 1.
🧰 Additional context used
📓 Path-based instructions (11)
test/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Write tests with bun:test (describe, test, expect).

Files:

  • test/integration/provisioning.test.ts
  • test/unit/cli-env-inspect.test.ts
  • test/unit/descriptor.test.ts
test/integration/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep test/integration/ tests gated behind QUICKCHR_INTEGRATION=1 because they require QEMU.

Files:

  • test/integration/provisioning.test.ts
**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.ts: Use tabs for indentation.
Avoid unnecessary comments on obvious code.
Throw QuickCHRError(code, message, installHint?) for errors.

Files:

  • test/integration/provisioning.test.ts
  • examples/harness/harness.ts
  • test/unit/cli-env-inspect.test.ts
  • src/index.ts
  • src/cli/index.ts
  • src/lib/types.ts
  • src/lib/quickchr.ts
  • test/unit/descriptor.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

In Bun-based TypeScript code, use Bun.spawn(), Bun.write(), Bun.sleep(), bun:test, and ESM imports with .ts extensions.

Files:

  • test/integration/provisioning.test.ts
  • examples/harness/harness.ts
  • test/unit/cli-env-inspect.test.ts
  • src/index.ts
  • src/cli/index.ts
  • src/lib/types.ts
  • src/lib/quickchr.ts
  • test/unit/descriptor.test.ts
test/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not turn a red integration test green by broadening timeouts, skipping it, or platform-gating it before reproducing and root-causing the failure.

Files:

  • test/integration/provisioning.test.ts
  • test/unit/cli-env-inspect.test.ts
  • test/unit/descriptor.test.ts
test/unit/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep test/unit/ tests fast and independent of QEMU.

Files:

  • test/unit/cli-env-inspect.test.ts
  • test/unit/descriptor.test.ts
src/index.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use src/index.ts as the barrel export for consumers.

Files:

  • src/index.ts
src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.ts: On ARM64 (virt machine), never use if=virtio for drives; always use explicit -device virtio-blk-pci,drive=drive0.
For HVF acceleration, use -cpu host rather than cortex-a710.
Ensure UEFI pflash code and vars units are the same size.
Treat QGA as x86-only; arm64 CHR does not start the guest agent.

Files:

  • src/index.ts
  • src/cli/index.ts
  • src/lib/types.ts
  • src/lib/quickchr.ts
src/**

📄 CodeRabbit inference engine (CLAUDE.md)

Follow the rules in general.instructions.md for files under src/**, including layer boundaries, error-code usage, port layout, and the RouterOS “expired admin” caveat.

Files:

  • src/index.ts
  • src/cli/index.ts
  • src/lib/types.ts
  • src/lib/quickchr.ts
src/cli/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep modules under src/cli/ as a thin CLI wrapper over the library.

Files:

  • src/cli/index.ts
src/lib/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep modules under src/lib/ as pure library code with no CLI dependencies and no process.exit().

Keep src/lib/ as pure library code: do not import from src/cli/ and do not call process.exit() there.

Files:

  • src/lib/types.ts
  • src/lib/quickchr.ts
🪛 LanguageTool
docs/centrs-interface.md

[grammar] ~261-~261: Ensure spelling is correct
Context: ...t is always included in modes (a mode centrs may try) but never added to `bat...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~281-~281: Ensure spelling is correct
Context: ...pick https/http; set tls to match which was chosen. - services["native-api"] ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (11)
src/lib/types.ts (1)

386-518: LGTM!

Also applies to: 676-678

src/index.ts (1)

35-36: LGTM!

Also applies to: 49-53, 64-71

src/lib/quickchr.ts (1)

10-28: LGTM!

Also applies to: 65-65, 251-397, 750-752

src/cli/index.ts (2)

1527-1544: LGTM!

Also applies to: 2862-2864


1493-1514: 🩺 Stability & Availability

Remove this comment: the top-level CLI catch already handles QuickCHRError and exits 1, so MACHINE_STOPPED will print a clean error instead of an unhandled rejection.

			> Likely an incorrect or invalid review comment.
test/integration/provisioning.test.ts (1)

489-499: LGTM!

docs/centrs-interface.md (1)

98-98: LGTM!

Also applies to: 122-130, 260-272, 281-298, 336-342

examples/harness/harness.ts (1)

10-10: LGTM!

Also applies to: 41-43

test/unit/cli-env-inspect.test.ts (2)

83-83: LGTM!

Also applies to: 92-110, 133-137


151-161: 🩺 Stability & Availability

quickchr env already guards stopped machines. src/cli/index.ts:1527-1536 calls getRunningInstance(name), which raises MACHINE_STOPPED before subprocessEnv() runs.

			> Likely an incorrect or invalid review comment.
test/unit/descriptor.test.ts (1)

1-295: LGTM!


📝 Walkthrough

Walkthrough

The PR replaces the legacy machine descriptor with Descriptor v1, derives service endpoints and authentication state, updates CLI inspection and environment handling, and adds contract, CLI, harness, and integration tests.

Changes

Descriptor v1 integration

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
Loading

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 /rest suffix.
  • 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 ⚠️ Warning 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mobileskyfi
mobileskyfi merged commit 0c42c46 into main Jul 10, 2026
13 of 14 checks passed
@mobileskyfi
mobileskyfi deleted the feat/71-descriptor-v1 branch July 10, 2026 00:58
mobileskyfi added a commit that referenced this pull request Jul 10, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants