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
10 changes: 7 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ read `docs/development/index.md`.
## Repository rules

- Make the smallest change that satisfies the task and preserve unrelated work.
- Use `uv` and the committed `uv.lock` within Python projects. Do not hand-edit
generated dependency exports such as `requirements.txt`.
- Use `uv` for Python dependency management, environments, locking, builds, and
command execution unless a project explicitly documents an exception. Treat
`pyproject.toml` and the committed `uv.lock` as the dependency sources of truth.
- Do not add `requirements.txt` or another generated dependency export by
default. Commit one only when a named non-uv consumer requires it and that
workflow is documented; regenerate exports with `uv`, never by hand.
- Never commit credentials or populated `.env` files. Document configuration in
`.env.example`.
- Do not hand-edit generated Dev Notes cards, bylines, or navigation; use
Expand All @@ -36,4 +40,4 @@ read `docs/development/index.md`.
- For documentation changes, run `python3 tests/test_render_dev_notes.py` and
`scripts/build-docs.sh`, then serve the generated site as described in
`docs/development/index.md`.
- Report the checks run and any checks that could not be completed.
- Report the checks run and any checks that could not be completed.
2 changes: 2 additions & 0 deletions projects/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ layout.

Current projects:

- `openshell-middleware-init`: Typer CLI that generates 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.
58 changes: 58 additions & 0 deletions projects/openshell-middleware-init/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Agent instructions

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

## Preserve these invariants

- Keep initialization non-destructive. Never merge into, follow, or replace an
existing output path, including a symlink.
- Build and validate in a hidden sibling staging directory. Publish only after
all generation and validation steps succeed.
- Preserve reservation ownership checks and atomic no-replace publication.
- Support Linux and macOS explicitly. Do not weaken publication guarantees to
add another platform implicitly.
- Keep every generated project version-matched: the OpenShell tag, downloaded
proto, bindings or build configuration, lockfile, and manifest must agree.
- Do not install, replace, or configure the user's OpenShell installation.

## Use the project toolchain

- Use `uv` for this Python project. Treat `pyproject.toml` and `uv.lock` as the
dependency sources of truth.
- Do not add `requirements.txt` or another dependency export unless a documented
non-uv consumer requires it.
- Use `uv add` or `uv remove` for dependency changes; do not hand-edit the lock.

## Change templates carefully

- Keep templates under `src/openshell_middleware_init/templates/` runnable as
standalone projects.
- Use `__UPPER_SNAKE_CASE__` for template markers. Add every marker to
`TemplateContext.replacements` and cover it with a rendering test.
- Treat generated Python protobuf and gRPC modules as generator-owned. Do not
format, type-check, or hand-edit them.
- When changing a template, generate the affected language project in isolated
scratch storage and run its documented checks when practical.

## Test behavior, not implementation details

- Keep initializer unit tests hermetic. Inject protocol downloads and project
preparation instead of contacting GitHub or invoking uv or Cargo.
- Add regression tests for changes to output safety, failure cleanup, naming,
manifests, network behavior, or rendered files.
- Use isolated temporary paths for end-to-end generation. Never generate over an
existing directory.

## Validate every change

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.
127 changes: 127 additions & 0 deletions projects/openshell-middleware-init/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# OpenShell Middleware Init

`openshell-middleware-init` creates a runnable Python or Rust starter for an
OpenShell supervisor middleware service. The starter implements the complete
gRPC service as a pass-through, pins its protocol contract to one OpenShell
release, and includes tests, dependency locks, and registration guidance.

The initializer does not install or replace 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 the command in an isolated tool environment from GitHub:

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

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

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

Both forms make `openshell-middleware-init` available outside the source tree
without running `uv sync` in this project.

Contributors working on the CLI should use the locked project environment:

```sh
uv sync --locked
uv run openshell-middleware-init --help
```

## Quick start

Generate and run a Python starter with the installed command:

```sh
openshell-middleware-init 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
openshell-middleware-init 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 already exist. Use a pinned OpenShell tag for
reproducible projects; `--openshell-version latest` is available for
experimentation.

Run `openshell-middleware-init --help` for all options. Python package names
default to a normalized project name and can be changed with `--package-name`.

## What you get

Each generated 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
SHA-256.

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.

## Safety and failure behavior

Generation is non-destructive. The initializer validates a hidden sibling
staging directory, then publishes it atomically. It refuses an existing output,
including a symlink, and uses a per-output reservation to prevent concurrent
writers. A normal failure removes the initializer's own staging and reservation
without publishing a partial project.

If the process is killed, it may leave
`.<output>.openshell-middleware-init.lock` and a hidden staging directory. Before
removing either one:

1. Read `metadata.json` in the reservation.
2. On the recorded host, confirm that the recorded PID is no longer the same
initializer process and that the final output does not exist.
3. Inspect and remove only the recorded staging directory.
4. Remove `owner` and `metadata.json`, then remove the empty reservation with
`rmdir`. Stop if it contains anything unexpected.

## Develop the CLI

Run the complete local gate from this directory:

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

Unit tests are hermetic: they use local protocol fixtures and do not contact
GitHub or invoke uv or Cargo for generated projects.
56 changes: 56 additions & 0 deletions projects/openshell-middleware-init/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
[project]
name = "openshell-middleware-init"
version = "0.1.0"
description = "Generate version-matched OpenShell supervisor middleware projects."
readme = "README.md"
requires-python = ">=3.10"
license = "Apache-2.0"
authors = [
{ name = "NVIDIA CORPORATION & AFFILIATES" },
]
dependencies = [
"typer>=0.16,<1",
]

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

[project.urls]
Repository = "https://github.com/NVIDIA/OpenShell-Research"

[dependency-groups]
dev = [
"pytest>=8,<10",
"pytest-cov>=5,<8",
"ruff>=0.12,<1",
"ty>=0.0.53",
]

[build-system]
requires = ["uv_build>=0.11.8,<0.12.0"]
build-backend = "uv_build"

[tool.pytest.ini_options]
addopts = [
"--strict-config",
"--strict-markers",
"--cov=openshell_middleware_init",
"--cov-report=term-missing",
"--cov-fail-under=95",
]
testpaths = ["tests"]

[tool.ruff]
line-length = 100
target-version = "py310"
extend-exclude = ["src/openshell_middleware_init/templates"]

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

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

[tool.uv]
required-version = ">=0.11.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Generate version-matched OpenShell middleware projects."""

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

from __future__ import annotations

from enum import Enum
from pathlib import Path
from typing import Annotated

import typer

from openshell_middleware_init.generator import InitializationError, initialize_project


class Language(str, Enum):
"""Middleware implementation languages supported by the generator."""

PYTHON = "python"
RUST = "rust"


app = typer.Typer(
add_completion=False,
no_args_is_help=True,
pretty_exceptions_enable=False,
help="Generate a runnable, version-matched OpenShell middleware project.",
)


@app.command()
def init(
name: Annotated[
str,
typer.Argument(help="Project name, such as audit-headers."),
],
language: Annotated[
Language,
typer.Option("--language", "-l", help="Implementation language."),
],
openshell_version: Annotated[
str,
typer.Option(
"--openshell-version",
"--version",
help="OpenShell release tag (for example v0.0.86), or latest.",
),
],
output: Annotated[
Path | None,
typer.Option(
"--output",
"-o",
help="Destination directory. Defaults to ./<name>.",
),
] = None,
package_name: Annotated[
str | None,
typer.Option(
"--package-name",
help="Python import package override (Python projects only).",
),
] = None,
) -> None:
"""Create a new OpenShell supervisor middleware project."""
destination = output if output is not None else Path.cwd() / name
try:
result = initialize_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

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}")


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


if __name__ == "__main__":
main()
Loading