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

15 KiB

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:

{"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.