mechutil 0.8.13

Utility structures and functions for mechatronics applications.
Documentation
# Autocore Tool & Editor Registry

A small ecosystem primitive that lets external packages contribute **tools and
editors** to an autocore installation — discovered by `autocore-server` (to
supervise the ones that are long-running services) and by `autocore-ide` (to
offer rich, custom editors for module configuration) — without either of them
hard-coding any knowledge of a specific tool.

The first citizen is `labelit-studio`, the web-based vision-configuration
editor for `autocore-labelit`. The mechanism is general: future tools
(an EtherCAT slave-tree editor, an NI channel-grid, diagnostics) register the
same way.

This document is the contract. The implementation lives in
`mechutil::tool_registry` and `mechutil::tool_settings`.

## Design principles

1. **Package-owned manifest, separately-owned settings.** A *manifest*
   describes what a tool *is* (executable, what it edits, defaults). It ships
   inside the package and is replaced freely on upgrade. *Settings* describe
   what an admin *chose* (the actual port, enabled/disabled, bind address).
   They live in a different directory, are never shipped by the package, and
   therefore survive upgrades. Conflating the two would let `apt upgrade`
   silently reset a field-configured port.

2. **dpkg owns the files; no shared-file editing.** A package contributes by
   *shipping a manifest file* into a drop-in directory. Install registers it;
   uninstall removes it — handled entirely by the package manager's own file
   tracking. This replaces the older approach of awk-editing a shared
   `config.ini`, which is fragile under concurrent installs and messy to
   purge.

3. **One registry, multiple consumers.** `autocore-server` reads it to
   supervise service tools. `autocore-ide` reads it to learn which module
   domains have a custom editor. A CLI could read it to list installed tools.
   None of them needs tool-specific code.

4. **Supervision stays at the autocore tier.** Service tools are supervised by
   `autocore-server` (the same machinery that launches modules), not by
   systemd. This keeps the forthcoming user-authentication layer at the
   autocore tier, decoupled from OS users.

## Filesystem layout

All under the autocore config directory, resolved by
`mechutil::resolve_config_dir()` (`AUTOCORE_CONFIG_DIR` env →
`/srv/autocore/config` → `./target/autocore/config` writable fallback for
unprivileged dev runs — mirrors `resolve_log_dir`):

```text
/srv/autocore/config/
├── config.ini                         # existing server/module config
├── tools.d/                           # MANIFESTS — package-owned (dpkg)
│   ├── labelit-studio.json
│   └── <tool>.json   (or <tool>/manifest.json + bundled resources)
└── tool-settings/                     # SETTINGS — runtime, survives upgrades
    └── labelit-studio.json
```

`tools.d/` accepts either a flat `<tool>.json` or a `<tool>/manifest.json`
inside a per-tool directory (use the directory form when a tool also ships
resources such as an icon or extra schemas).

## The manifest

Contribution metadata. Shipped by the package; read-only at runtime.

```json
{
  "schema_version": 1,
  "name": "labelit-studio",
  "version": "1.1.3",
  "description": "Vision-pipeline configuration editor for autocore-labelit",
  "executable": "/opt/autocore/bin/modules/labelit-studio",
  "serves_http": true,
  "ui_path": "/",
  "launch": {
    "mode": "service",
    "default_port": 7878,
    "autostart": true,
    "args": []
  },
  "editors": [
    {
      "target_domain": "labelit",
      "target_path": null,
      "label": "Vision Editor"
    }
  ]
}
```

Fields:

- `name` — unique tool id (kebab-case). Must match the manifest's file stem.
- `executable` — absolute path to the binary.
- `serves_http` / `ui_path` — whether the tool serves a web UI and where its
  root is. Drives the security gate and lets a consumer point a browser /
  webview at it.
- `launch.mode``"service"` (long-running; `autocore-server` supervises it)
  or `"on_demand"` (invoked by the IDE/CLI when needed; never auto-launched).
- `launch.default_port` — default for the HTTP port; the *effective* port
  comes from settings.
- `launch.autostart` — default for whether a service tool starts with the
  server; the *effective* value comes from settings.
- `launch.args` — extra arguments inserted before the tool's standard args
  (mirrors `ModuleConfig.args`).
- `editors[]` — the config editors this tool contributes. One tool can
  contribute several. Each binds to a module `target_domain` and an optional
  `target_path` (a JSON pointer into that module's config, e.g.
  `/robot_calibration`) so a tool can be the whole-section editor *and* a
  focused widget for a sub-tree. `null` path = the whole module config.

## Settings (two-tier)

`mechutil::tool_settings::ToolSettings` is what every tool — and the server —
reads and writes. It is deliberately two-tier so the server can read launch
settings *without understanding any tool's internals*:

```jsonc
{
  "launch": {                 // reserved schema; the SERVER reads this
    "enabled": true,
    "port": 7878,
    "bind": "127.0.0.1",
    "autostart": true
  },
  "extra": {                  // free-form; tool-private, the server never parses it
    "last_image_dir": "/home/me/snaps"
  }
}
```

- `launch.bind` defaults to `127.0.0.1`. **Until autocore user-auth lands, a
  `serves_http` tool must not be bound to a routable address by default** — it
  is an unauthenticated config-and-camera surface. Exposing it wider is an
  explicit admin choice (and ideally Tailscale-only).
- Settings are created on first read, seeded from the manifest's `launch`
  defaults (`open_seeded`). Writes are atomic (temp file + rename).
- `extra` is an arbitrary JSON object; tools store whatever they like there
  without breaking the `launch` contract the server depends on.

## API surface (mechutil)

```rust
// paths
mechutil::resolve_config_dir() -> PathBuf

// registry (read + register)
mechutil::tool_registry::{
    ToolManifest, EditorContribution, LaunchSpec, LaunchMode,
    tools_dir,                                   // <config>/tools.d
    list_tools()       -> Vec<ToolManifest>,     // scan + validate, skip bad ones (logged)
    load_tool(name)    -> Result<ToolManifest>,
    find_editor_for(domain, path) -> Option<(ToolManifest, EditorContribution)>,
    register_tool(&ToolManifest) -> Result<()>,  // for non-deb tools / dev installs
    unregister_tool(name)        -> Result<()>,
}

// settings (read/write)
mechutil::tool_settings::{
    ToolSettings, LaunchSettings,
    settings_dir,                                // <config>/tool-settings
    ToolSettings::open(name)            -> Result<ToolSettings>,
    ToolSettings::open_seeded(&manifest)-> Result<ToolSettings>, // defaults from manifest
    ToolSettings::save(&self)           -> Result<()>,
}
```

Debs ship the manifest as a packaged file (dpkg = register/deregister);
`register_tool`/`unregister_tool` exist for tools that aren't deb-packaged and
for tests. Both validate against the manifest rules above.

## Consumers (outside mechutil — specified here, built later)

- **autocore-server** scans `tools.d/` at startup, and for each manifest with
  `launch.mode == "service"` whose settings have `enabled && autostart`,
  supervises `executable [launch.args] --port <settings.launch.port> --bind
  <settings.launch.bind>` using the existing module-supervisor machinery. It
  exposes `system.list_tools` (discovery for the IDE) and `system.rescan_tools`
  (so an install/uninstall takes effect without a server restart — debs call
  it from `postinst`/`postrm`).

- **autocore-ide** consults the registry to decide whether a module domain has
  a custom editor. When connected to a target it uses `system.list_tools`;
  offline it reads `tools.d/` directly or falls back to a bundled manifest —
  the same IPC → bundled → cache chain it already uses for `<module>.schema`.
  For `labelit`, finding the `labelit-studio` editor lets the IDE offer "Open
  Vision Editor" (point a webview at the target's running studio, or spawn a
  local sidecar) instead of only the generic schema form.

## The three config scopes (so nothing gets muddled)

1. **Module operational config**`modules.labelit.config` in `project.json`.
   The thing the editor *edits*. Project data; synced to targets.
2. **Tool manifest**`tools.d/<tool>.json`. Package metadata describing the
   tool to the registry.
3. **Tool settings**`tool-settings/<tool>.json`. The tool's own runtime
   settings (port, bind, private state). Not project data, not package
   metadata.

The editors a tool contributes act on scope 1, and compose with the existing
`mechutil::ipc::schema` infrastructure: a module already serves a JSON Schema +
`ui_hints` at `<domain>.schema`; the registry adds "and a richer editor exists
for this domain."
```