From 86f4f5e2ea1c546a582da1213bad5e4fe163d1ab Mon Sep 17 00:00:00 2001 From: soubarnak Date: Fri, 31 Jul 2026 13:24:47 +0530 Subject: [PATCH] docs(cli): define v1 command contract Refs #15 --- docs/cli-contract.md | 319 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 docs/cli-contract.md diff --git a/docs/cli-contract.md b/docs/cli-contract.md new file mode 100644 index 0000000..db2438e --- /dev/null +++ b/docs/cli-contract.md @@ -0,0 +1,319 @@ +# Mjolnir CLI v1 Contract + +`mjolnirctl` — user-accepted command-line contract for Mjolnir v1. + +## 1. Context and constraints + +- `mjolnirctl` is an unprivileged client. +- All communication uses the `org.bongbetic.Mjolnir` name on the D-Bus system bus. +- No direct hardware or store access. +- No daemon auto-start; a missing daemon is reported immediately. + +## 2. Command hierarchy + +Canonical hierarchy: + +``` +mode status telemetry battery rule config calibration +validate diagnostics capabilities gpu never-touch why +``` + +- `mjolnirctl [args]` +- Sole alias: `mjolnirctl --why ` is the required equivalent of `mjolnirctl why `. +- No other aliases, short options, or inferred subcommands. + +## 3. Command contract + +### 3.1 `mode` + +- `mode get` — report the active mode plus thermal-override and external-profile annotations. +- `mode set ` — wait for daemon confirmation and report the resulting mode. +- Invalid enum values are rejected locally before any daemon call. +- Bare `mode` (no subcommand) does nothing. + +### 3.2 `status` + +Reports: daemon version, active mode, probe completion, thermal override, external profile, module/autoload state, degradation reason. + +- Capability detail stays in `capabilities`. +- Current sample stays in `telemetry`. + +### 3.3 `telemetry` + +- `telemetry get` — current sample. +- `telemetry history` — time-series history. + - Default window: `--since 1h`. + - Alternative: `--from --to `. + - Output ascending by timestamp. + - Empty result is success (exit 0). +- No watch in v1. + +### 3.4 `battery` + +- `battery health` — current health record. +- `battery history` — time-series; same time flags as `telemetry history`. +- `battery thresholds get` — current charge thresholds. +- `battery thresholds set --start --end `. + - Both threshold values required; applied atomically. + - Local validation: `0 <= start < end <= 100`, integers. + - Daemon validates against hardware. + - Preset aliases deferred to the charge-preset decision. + - Dependency note: ADR-003 currently exposes `ChargeStartThreshold` and `ChargeEndThreshold` as independent readwrite properties, so the open D-Bus contract must provide or define an atomic two-value update mechanism before implementation. This CLI document does not choose that wire shape; see section 5 and section 6. + +### 3.5 `rule` + +- `rule list` +- `rule add` +- `rule update ` +- `rule delete ` +- Canonical rule fields deferred to the per-app rule decision. +- No arbitrary D-Bus dictionary input. +- Delete has no confirmation prompt. + +### 3.6 `config` + +- `config export ` +- `config import ` +- Export refuses to overwrite an existing file unless `--force`. +- Import validates the complete document before atomic apply. +- Unreadable path fails. +- No prompts. +- `--dry-run` validates without mutation. + +### 3.7 `never-touch` + +- `never-touch list` +- `never-touch set ...` — atomically replaces the full list. +- `never-touch set --clear` — clears the list. +- No add/remove variants; avoid them in v1. + +### 3.8 `calibration` + +- `calibration start` +- `calibration status` +- `start` remains attached through terminal daemon state and relays progress. +- No cancel, stop, or background mode. + +### 3.9 `validate` + +- Sole validation command. +- Starts interactive validation, relays progress, stays attached through terminal state. +- No status or cancel subcommands. + +### 3.10 `diagnostics` + +- `diagnostics generate` +- No destination path argument. +- Daemon creates a redacted bundle and returns its path. +- Human output prints path and size. +- JSON output: `{"path": ..., "size_bytes": ...}`. +- CLI does not copy, rename, or inspect the bundle. + +### 3.11 `capabilities` + +- `capabilities get` — normalized stable fields: + - knob ID + - support state + - reason + - range + - topology + +### 3.12 `gpu` + +- `gpu get` — dGPU power and display topology. +- Unsupported hardware is data, not an error: exit 0. + +### 3.13 `why` / `--why` + +- `why ` and `--why `; write ID is an opaque daemon-issued identifier. +- Output full attribution: + - `write_id` + - `timestamp` + - `knob_id` + - `trigger_event_id` + - `policy_rule` + - `principal` + - `prior_value` + - `new_value` + - `mode` + - `external_profile_state` +- Daemon resolves `policy_rule`; non-policy writes use `"none"`. +- Unknown write ID: exit 2. + +## 4. Cross-cutting decisions + +### 4.1 Global options + +Only: + +- `--help` +- `--version` +- `--output human|json` +- `--timeout ` + +No CSV, watch, shell completion, color/verbose/quiet flags, cancellation, background workflows, daemon start, or retries. + +### 4.2 Output + +- Human is the default regardless of TTY. +- Singleton resources use key/value blocks; lists and history use aligned tables. +- Explicit units on all values. +- Lowercase enums. +- Status annotations included. +- No required color or Unicode. + +JSON success is one object: + +```json +{"schema_version": 1, "command": "...", "data": {...}} +``` + +- snake_case keys, lowercase enums, UTC RFC3339 timestamps, explicit units. +- Long workflows use JSON Lines with the same versioned event shape. +- JSON errors: one structured object on stderr; stdout carries only successful data; no partial-success output. + +### 4.3 Exit codes + +| Code | Meaning | +| ---- | -------------------------------------------- | +| 0 | Success (incl. degraded/unsupported, exit 0) | +| 2 | Usage / validation / not-found | +| 3 | Daemon / module unavailable | +| 4 | Authorization denied | +| 5 | Unsupported capability | +| 6 | Policy conflict / rate limit | +| 7 | Internal operation failure | +| 124 | Timeout | +| 130 | Ctrl-C detach | + +Successful degraded/unsupported-capability responses remain exit 0. + +D-Bus errors from `org.bongbetic.Mjolnir.Error.*` map to exit codes as follows; these are CLI mappings of settled D-Bus errors: + +| D-Bus error | Exit | +| ----------------------- | ---- | +| `UnknownKnob` | 2 | +| `InvalidConfig` | 2 | +| `ModuleAbsent` | 3 | +| `NotAuthorized` | 4 | +| `CapabilityLocked` | 5 | +| `UnsupportedOperation` | 5 | +| `RateLimited` | 6 | +| `ThermalOverride` | 6 | +| `CalibrationInProgress` | 6 | + +### 4.4 Timeouts + +- D-Bus default deadline 5 seconds, overridable by global `--timeout`. +- Daemon absence reported immediately; no retries. +- Long workflows (calibration/validate) default 30 minutes and have a separate timeout override. +- Ctrl-C on calibration/validate detaches, returns 130, daemon safely continues. + +### 4.5 Time arguments + +- `--since ` with `s|m|h` suffix, or +- `--from --to `. +- No mixing of forms. +- Output always UTC RFC3339. + +### 4.6 Numeric and ID rules + +- Percentages: integer 0..100, no suffix. +- Reject locale-dependent numbers and implicit conversions. +- IDs opaque; CLI never interprets them. +- Human labels units according to D-Bus fields. + +### 4.7 Authorization + +- PolKit delegated to the active agent; CLI never prompts or embeds credentials. +- No agent or denied authorization gives actionable stderr and exit 4. +- JSON error stable: `authorization_denied` plus action ID. This is the stable CLI code mapped from the D-Bus error `org.bongbetic.Mjolnir.Error.NotAuthorized`. + +### 4.8 Battery health record + +`battery health` output must include the complete record fields: + +- `timestamp` +- `cycle_count` +- `design_capacity` +- `full_charge_capacity` +- `wear_percent` +- `charge_temperature` +- `validity_status` +- `staleness_status` + +The exact types/enums and source contract for `charge_temperature`, `validity_status`, and `staleness_status` must be settled by the data-model/D-Bus work; no enum values or wire types are invented here. No derived fade prediction. + +## 5. Required and Pending D-Bus Dependencies + +This section records CLI prerequisites for the open D-Bus contract ticket and separates them from the surface already settled in ADR-003 and the XML. `EditNeverTouchList(as)` is already settled in ADR-003 and the current XML and is therefore **not** labelled required or `[REQUIRED]` here. + +### Required additions — absent from ADR-003/XML + +These four additions are absent from ADR-003 and the current XML. They are **required** CLI prerequisites for the open D-Bus contract ticket; they are not settled ADR-003 interfaces. + +| Addition | Type / shape | Purpose | +| -------------------- | ------------------------------ | ---------------------------------------- | +| `NeverTouchList` | read-only property `as` | read the current never-touch list | +| `StartValidationRun` | method | start validation | +| `ValidationStage` | progress signal | validate progress reporting | +| `ExplainWrite` | method `(s write_id) -> a{sv}` | `why` full attribution; read/audit-gated | + +`NeverTouchList`, `StartValidationRun`, `ValidationStage`, and `ExplainWrite` are absent from ADR-003 and the current XML. `ValidationStage`'s signature is pending: ADR-006 shows both a single-stage string form and a failed-with-reason form, so this document does not invent a wire signature. + +### Pending read surfaces for `status` + +The accepted `status` command reports module/autoload state and degradation reason. Both are required read surfaces, but their final property names and wire types are pending; descriptive labels only are used here: + +- current module/autoload state — wire shape pending +- overall degradation reason — wire shape pending + +The existing `AutoloadDisabled` signal is transition-only: it fires when autoload is disabled and cannot replace a current-state read. `status` needs read surfaces for the present module/autoload state and degradation reason; no final property names are invented here. + +### Pending atomic threshold mechanism + +`battery thresholds set --start --end ` applies both values atomically. The current `ChargeStartThreshold`/`ChargeEndThreshold` independent readwrite properties are insufficient for atomicity. The open D-Bus contract ticket must settle a two-value atomic update mechanism; its exact wire shape is pending. This is separate from the four absent additions and the two pending read surfaces above: it concerns the existing settled threshold properties rather than a new named surface. + +## 6. Command-to-D-Bus mapping + +Where known. `[REQUIRED]` marks one of the four absent additions from section 5 — a required CLI prerequisite for the open D-Bus contract, not a settled ADR-003 interface. `[PENDING]` marks a surface whose wire shape is unresolved: the two `status` read surfaces, the `ValidationStage` signature, and the atomic threshold mechanism. `EditNeverTouchList(as)` is current settled ADR-003/XML surface and carries no tag. + +| Command | D-Bus surface | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `mode get` / `mode set` | mode property / setter; thermal-override, external-profile properties | +| `status` | daemon version, mode, probe, override, profile properties; module/autoload state read `[PENDING]`; degradation reason read `[PENDING]` | +| `telemetry get` / `history` | telemetry property; history method | +| `battery health` / `history` | battery health property; history method | +| `battery thresholds get` / `set` | `ChargeStartThreshold`/`ChargeEndThreshold` properties; atomic two-value update mechanism `[PENDING]` (see 3.4) | +| `rule list/add/update/delete` | rule methods (fields deferred to per-app rule decision) | +| `config export/import` | `ExportConfig(s path)` / `ImportConfig(s path)` with validation and atomic semantics as CLI requirements | +| `calibration start/status` | calibration start/status; progress signal | +| `validate` | `StartValidationRun()` `[REQUIRED]`; `ValidationStage` signal `[REQUIRED]` (signature `[PENDING]`) | +| `diagnostics generate` | diagnostics method returning bundle path | +| `capabilities get` | capabilities introspection | +| `gpu get` | dGPU power/topology properties | +| `never-touch list` | `NeverTouchList` read-only property `as` `[REQUIRED]` | +| `never-touch set` | `EditNeverTouchList(as)` — current settled ADR-003/XML surface | +| `why` / `--why` | `ExplainWrite(s write_id) -> a{sv}` `[REQUIRED]`, read/audit-gated | + +## 7. Scope exclusions and deferred work + +Excluded in v1: + +- CSV, watch, shell completion, color/verbose/quiet flags +- Cancellation, background workflows +- Daemon start, retries + +Deferred: + +- Rule field syntax — waits for _Per-app rule contract — identity, precedence, and restoration_. +- Charge preset aliases/wiring — waits for _Charge preset mapping — start hysteresis for 60, 80, and 100 percent_. + +## 8. Rationale + +- Single-command, explicit-hierarchy CLI keeps v1 small and auditable. +- Unprivileged D-Bus-only client keeps the surface safe; no direct hardware paths. +- Degraded/unsupported hardware is data (exit 0) so scripts can distinguish capability from failure. +- Atomicity (thresholds, never-touch replace, config import) avoids partial states. +- Human default regardless of TTY keeps output deterministic for users and tools alike. +- JSON Lines for long workflows keeps progress streamable without breaking the single-object success shape.