Files
Mjolnir/docs/cli-contract.md
T
2026-07-31 13:24:47 +05:30

320 lines
15 KiB
Markdown

# 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 <command> <subcommand> [args]`
- Sole alias: `mjolnirctl --why <write-id>` is the required equivalent of `mjolnirctl why <write-id>`.
- 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 <auto|powersaver|normal>` — 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 <RFC3339> --to <RFC3339>`.
- 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 <percent> --end <percent>`.
- 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-id>`
- `rule delete <rule-id>`
- 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 <path>`
- `config import <path>`
- 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 <process>...` — 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 <write-id>` and `--why <write-id>`; 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 <duration>`
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 <duration>` with `s|m|h` suffix, or
- `--from <RFC3339> --to <RFC3339>`.
- 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 <percent> --end <percent>` 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.