Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions projects/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ layout.

Current projects:

- `openshell-middleware-init`: Typer CLI that generates version-matched Python
and Rust OpenShell supervisor middleware projects.
- `middleware-kit`: `mkit` CLI that creates and updates version-matched
Python and Rust OpenShell supervisor middleware projects.
- `python-project-template`: Minimal, production-ready Python project scaffold
managed with uv.
- `reachy-mini-openshell`: Reachy Mini conversation demo for OpenShell.
59 changes: 59 additions & 0 deletions projects/middleware-kit/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Agent instructions

Read `README.md` and `pyproject.toml` before changing this project.

## Safety rules

- `mkit create` must never write into, follow, or replace an existing output
path, including a symlink.
- Build and check the project in a temporary directory next to its destination.
Move it into place only after every check passes.
- Check lock ownership before writing. Creation must not overwrite an existing
path. Updates must swap each generated file atomically and undo earlier swaps
if one fails.
- Support only Linux and macOS. Do not add a platform unless it can provide the
same file-safety guarantees.
- Keep the OpenShell tag, downloaded proto, bindings or Rust build files,
lockfile, and manifest on the same version.
- Never install, replace, or configure OpenShell.

## Dependencies

- Use `uv`. `pyproject.toml` and `uv.lock` define the dependencies.
- Do not add `requirements.txt` or another dependency export unless a documented
tool needs one.
- Use `uv add` or `uv remove` to change dependencies. Do not edit `uv.lock` by
hand.

## Templates

- Templates in `src/middleware_kit/templates/` must produce working standalone
projects.
- Write template markers as `__UPPER_SNAKE_CASE__`. Add each marker to
`TemplateContext.replacements` and test its rendered value.
- Do not format, type-check, or edit generated Python protobuf and gRPC files.
- After changing a template, generate a project in a temporary directory and
run its documented checks when practical.

## Tests

- Unit tests must not contact GitHub or run `uv` or Cargo in generated projects.
Pass test doubles for downloads and command execution.
- Add regression tests when changing file safety, failure cleanup, names,
manifests, network handling, or generated files.
- Run end-to-end generation in a new temporary directory. Never generate over
an existing directory.

## Checks

Run these commands from this directory:

```sh
uv run ruff format --check .
uv run ruff check .
uv run ty check
uv run pytest
uv build
```

Report any command that could not run and why.
157 changes: 157 additions & 0 deletions projects/middleware-kit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# OpenShell Middleware Kit

`middleware-kit` creates and updates Python or Rust services for OpenShell
supervisor middleware. Each new project starts as a working pass-through gRPC
service. It includes the protocol file for one OpenShell release, tests,
dependency locks, and instructions for registering the service.

The CLI does not install or change OpenShell.

## Requirements

- Linux or macOS
- [uv](https://docs.astral.sh/uv/)
- Network access to GitHub and the selected OpenShell release
- For Rust projects: Cargo and a Rust 1.90-compatible toolchain

## Install the CLI

Install `mkit` from GitHub with `uv`:

```sh
uv tool install \
"middleware-kit @ git+https://github.com/NVIDIA/OpenShell-Research.git#subdirectory=projects/middleware-kit"
```

If you already have this repository checked out, install from its local path
instead:

```sh
uv tool install /path/to/OpenShell-Research/projects/middleware-kit
```

Both commands install `mkit` for use outside this repository.

To work on the CLI itself, use the locked project environment:

```sh
uv sync --locked
uv run mkit --help
```

## Quick start

Generate and run a Python starter with the installed command:

```sh
mkit create audit-headers \
--language python \
--openshell-version v0.0.86 \
--output /tmp/audit-headers

cd /tmp/audit-headers
uv run pytest
uv run audit-headers
```

Or generate and run a Rust starter:

```sh
mkit create audit-headers \
--language rust \
--openshell-version v0.0.86 \
--output /tmp/audit-headers-rust

cd /tmp/audit-headers-rust
cargo test --locked
cargo run --locked -- 127.0.0.1:50051
```

The output path must not exist. Pin an OpenShell tag when you need repeatable
builds. Use `--openshell-version latest` when you want the newest release.

Run `mkit --help` for all options. By default, `mkit` derives the Python package
name from the project name. Use `--package-name` to set it yourself.

## Update a project

Run this inside a generated project to use the latest OpenShell release:

```sh
mkit update
```

To choose a release or update a project in another directory:

```sh
mkit update /path/to/audit-headers \
--openshell-version v0.0.90
```

`mkit update` reads `middleware-dev-manifest.json` to find the project language
and Python package. It downloads the selected `supervisor_middleware.proto`,
regenerates Python protobuf and gRPC bindings when needed, updates `uv.lock` or
`Cargo.lock`, and writes the version and protocol checksum to the manifest.
The manifest must name `middleware-kit` as its generator.

## What you get

Each project contains:

- a pass-through implementation of `Describe`, `ValidateConfig`, and
`EvaluateHttpRequest`;
- the exact `supervisor_middleware.proto` from the selected OpenShell release;
- generated Python gRPC bindings or Rust Tonic build configuration;
- tests and lint/type-check configuration;
- `uv.lock` or `Cargo.lock`; and
- `middleware-dev-manifest.json` with the release, source URL, and protocol
checksum.

Start by implementing policy behavior in the generated `validate_config` and
`evaluate_http_request` functions. The generated README explains how to run the
service and register it with OpenShell.

## How `mkit` protects your files

`mkit create` builds and checks the project in a temporary directory next to
the output path. It moves the finished project into place only after every
check passes. If the output path already exists, including as a symlink, the
command stops without changing it.

`mkit update` works on a temporary copy of the project. It changes only the
protocol, generated bindings or Rust build files, lockfile, and manifest. It
runs the project checks before replacing those files. Your implementation files
stay unchanged. If a file replacement fails, `mkit` restores the files it
already replaced.

A lock prevents two `mkit` processes from changing the same path at once.
Normal failures remove the lock and temporary files. If an update and its
rollback both fail, `mkit` keeps the recovery files and prints their locations.

If the process is killed, it may leave a `.<output>.middleware-kit.lock`
directory and a temporary project directory. Clean them up as follows:

1. Open `metadata.json` in the lock directory.
2. On the host listed in that file, check that the listed PID is no longer an
`mkit` process.
3. For `create`, also check that the requested output path does not exist.
Never remove the project directory after an interrupted `update`.
4. Inspect the temporary directory listed in `metadata.json`, then remove only
that directory.
5. Remove `owner` and `metadata.json`. Use `rmdir` to remove the empty lock
directory. Stop if the lock directory contains any other files.

## Develop the CLI

Run these checks from this directory:

```sh
uv run ruff format --check .
uv run ruff check .
uv run ty check
uv run pytest
uv build
```

Unit tests use local protocol fixtures. They do not contact GitHub or run `uv`
or Cargo inside generated projects.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "openshell-middleware-init"
name = "middleware-kit"
version = "0.1.0"
description = "Generate version-matched OpenShell supervisor middleware projects."
description = "Create and update version-matched OpenShell middleware projects."
readme = "README.md"
requires-python = ">=3.10"
license = "Apache-2.0"
Expand All @@ -13,7 +13,7 @@ dependencies = [
]

[project.scripts]
openshell-middleware-init = "openshell_middleware_init.cli:main"
mkit = "middleware_kit.cli:main"

[project.urls]
Repository = "https://github.com/NVIDIA/OpenShell-Research"
Expand All @@ -34,7 +34,7 @@ build-backend = "uv_build"
addopts = [
"--strict-config",
"--strict-markers",
"--cov=openshell_middleware_init",
"--cov=middleware_kit",
"--cov-report=term-missing",
"--cov-fail-under=95",
]
Expand All @@ -43,14 +43,14 @@ testpaths = ["tests"]
[tool.ruff]
line-length = 100
target-version = "py310"
extend-exclude = ["src/openshell_middleware_init/templates"]
extend-exclude = ["src/middleware_kit/templates"]

[tool.ruff.lint]
select = ["B", "E", "F", "I", "RUF", "SIM", "UP"]

[tool.ty.src]
include = ["src", "tests"]
exclude = ["src/openshell_middleware_init/templates"]
exclude = ["src/middleware_kit/templates"]

[tool.uv]
required-version = ">=0.11.0"
3 changes: 3 additions & 0 deletions projects/middleware-kit/src/middleware_kit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Create and update version-matched OpenShell middleware projects."""

__version__ = "0.1.0"
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Typer command-line interface for the middleware project generator."""
"""Typer command-line interface for OpenShell middleware projects."""

from __future__ import annotations

Expand All @@ -8,7 +8,11 @@

import typer

from openshell_middleware_init.generator import InitializationError, initialize_project
from middleware_kit.generator import (
ProjectError,
create_project,
update_project,
)


class Language(str, Enum):
Expand All @@ -22,12 +26,12 @@ class Language(str, Enum):
add_completion=False,
no_args_is_help=True,
pretty_exceptions_enable=False,
help="Generate a runnable, version-matched OpenShell middleware project.",
help="Create or update a version-matched OpenShell middleware project.",
)


@app.command()
def init(
def create(
name: Annotated[
str,
typer.Argument(help="Project name, such as audit-headers."),
Expand Down Expand Up @@ -63,22 +67,56 @@ def init(
"""Create a new OpenShell supervisor middleware project."""
destination = output if output is not None else Path.cwd() / name
try:
result = initialize_project(
result = create_project(
name=name,
language=language.value,
requested_version=openshell_version,
destination=destination,
package_name=package_name,
)
except InitializationError as error:
typer.echo(f"openshell-middleware-init: error: {error}", err=True)
raise typer.Exit(code=1) from error
except ProjectError as error:
_report_error(error)

typer.echo(f"Created {result.language} middleware project at {result.destination}")
typer.echo(f"OpenShell contract: {result.openshell_version}")
typer.echo(f"Next: cd {result.destination} && {result.run_command}")


@app.command()
def update(
project: Annotated[
Path,
typer.Argument(
help="Existing generated middleware project. Defaults to the current directory."
),
] = Path("."),
openshell_version: Annotated[
str,
typer.Option(
"--openshell-version",
"--version",
help="OpenShell release tag (for example v0.0.86), or latest.",
),
] = "latest",
) -> None:
"""Update an existing middleware project's OpenShell contract and generated files."""
try:
result = update_project(
project_dir=project,
requested_version=openshell_version,
)
except ProjectError as error:
_report_error(error)

typer.echo(f"Updated {result.language} middleware project at {result.destination}")
typer.echo(f"OpenShell contract: {result.openshell_version}")


def _report_error(error: ProjectError) -> None:
typer.echo(f"mkit: error: {error}", err=True)
raise typer.Exit(code=1) from error


def main() -> None:
"""Run the command-line application."""
app()
Expand Down
Loading