From b445d8a1179d2d88551a08c2a741fa7b84238200 Mon Sep 17 00:00:00 2001 From: Jacob Mathison Date: Tue, 21 Jul 2026 11:10:20 -0500 Subject: [PATCH] feat: scaffold rust llm module controller --- .env.example | 11 + .gitea/workflows/ci.yml | 32 + .githooks/pre-commit | 4 + .gitignore | 9 + .woodpecker.yml | 11 + AGENTS.md | 69 ++ Cargo.lock | 1714 +++++++++++++++++++++++++++++++++++ Cargo.toml | 20 + README.md | 75 ++ llm.txt | 41 + manifest.llm.json | 124 +++ scripts/codex-run-logger.sh | 9 + scripts/codex-setup.sh | 20 + scripts/precommit-check.sh | 13 + src/config.rs | 26 + src/dictionary.rs | 145 +++ src/dispatcher.rs | 95 ++ src/llm.rs | 167 ++++ src/main.rs | 125 +++ src/registry.rs | 50 + src/scripts/logger.rs | 73 ++ src/scripts/mod.rs | 1 + src/server.rs | 54 ++ 23 files changed, 2888 insertions(+) create mode 100644 .env.example create mode 100644 .gitea/workflows/ci.yml create mode 100755 .githooks/pre-commit create mode 100644 .gitignore create mode 100644 .woodpecker.yml create mode 100644 AGENTS.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 llm.txt create mode 100644 manifest.llm.json create mode 100755 scripts/codex-run-logger.sh create mode 100755 scripts/codex-setup.sh create mode 100755 scripts/precommit-check.sh create mode 100644 src/config.rs create mode 100644 src/dictionary.rs create mode 100644 src/dispatcher.rs create mode 100644 src/llm.rs create mode 100644 src/main.rs create mode 100644 src/registry.rs create mode 100644 src/scripts/logger.rs create mode 100644 src/scripts/mod.rs create mode 100644 src/server.rs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..52cb18d --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Required: path to a local GGUF model for llama.cpp. +LLAMA_MODEL_PATH=/absolute/path/to/model.gguf + +# Optional llama.cpp server settings. +LLAMA_HOST=127.0.0.1 +LLAMA_PORT=8080 +LLAMA_CONTEXT_SIZE= +LLAMA_EXTRA_ARGS= + +# Optional controller logging. +CONTROLLER_LOG_LEVEL=info diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..db43ceb --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust if needed + shell: bash + run: | + if ! command -v cargo >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + fi + + - name: Show Rust versions + shell: bash + run: | + rustc --version + cargo --version + + - name: Run precommit checks + shell: bash + run: scripts/precommit-check.sh diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..3b01a42 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +exec scripts/precommit-check.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1791b5a --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/target/ +/.env +/.env.* +!/.env.example +/logs/ +/runtime/ +/dictionary/actions.jsonl +/dictionary/actions.index.json +*.log diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..a5f6909 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,11 @@ +when: + - event: push + - event: pull_request + +steps: + validate: + image: rust:1.95 + commands: + - rustc --version + - cargo --version + - scripts/precommit-check.sh diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e5f8f4e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,69 @@ +# AGENTS.md + +## Project + +`mp-ai-module-controller` is a Rust CLI scaffold for local llama.cpp-driven action dispatch. It starts a local `llama-server`, generates an action dictionary for small-model training and runtime lookup, queries the server through its OpenAI-compatible chat endpoint, and dispatches exact-match Rust scripts. + +## Commands + +- Build: `cargo build` +- Test: `cargo test` +- Generate dictionary: `cargo run -- dictionary generate` +- Start llama.cpp: `cargo run -- serve` +- Query local model: `cargo run -- query "log hello from codex"` +- Direct proof-of-concept dispatch: `cargo run -- run logger --payload '{"message":"hello from codex"}'` +- Codex setup: `scripts/codex-setup.sh` +- Codex logger smoke test: `scripts/codex-run-logger.sh` +- Precommit/CI check: `scripts/precommit-check.sh` + +## Environment + +Configuration is read from environment variables: + +- `LLAMA_MODEL_PATH`: required GGUF model path for `cargo run -- serve`. +- `LLAMA_HOST`: llama.cpp host. Default: `127.0.0.1`. +- `LLAMA_PORT`: llama.cpp port. Default: `8080`. +- `LLAMA_CONTEXT_SIZE`: optional value passed as `--ctx-size`. +- `LLAMA_EXTRA_ARGS`: optional shell-split arguments appended to `llama-server`. +- `CONTROLLER_LOG_LEVEL`: tracing log level. Default: `info`. + +Use `.env` for local values and keep it out of git. Update `.env.example`, `README.md`, `manifest.llm.json`, and `llm.txt` when environment variables or commands change. + +## Dictionary Rules + +- `src/registry.rs` is the source of truth for action definitions. +- `dictionary/actions.jsonl` is generated canonical training data. +- `dictionary/actions.index.json` is the generated runtime lookup file. +- Regenerate dictionary files with `cargo run -- dictionary generate` after changing registered actions. +- Do not manually edit generated dictionary files. + +## Dispatch Rules + +- Dispatch only exact `action_id` values present in `dictionary/actions.index.json`. +- Aliases and intent examples are training hints only; they are not executable ids. +- Unknown, malformed, ambiguous, missing, or empty action ids must be rejected without running scripts. +- Script processes are detached. The controller spawns them and does not monitor completion. +- Keep script payload validation strict and local to the script module. + +## Generated Paths + +- `target/` +- `logs/` +- `runtime/` +- `dictionary/actions.jsonl` +- `dictionary/actions.index.json` + +## CI And Hooks + +- Gitea Actions workflow lives at `.gitea/workflows/ci.yml`. +- Woodpecker workflow lives at `.woodpecker.yml`. +- The committed pre-commit hook lives at `.githooks/pre-commit`. +- Configure local hooks with `git config core.hooksPath .githooks`. +- All CI and pre-commit checks should call `scripts/precommit-check.sh` so dictionary generation stays validated consistently. + +## Coding Guidelines + +- Keep the CLI Rust-first and small. +- Prefer adding scripts under `src/scripts/` and registering them in `src/registry.rs`. +- Add focused tests for parsing, dictionary generation, action lookup, and payload validation. +- Run `cargo test` before handing off code changes. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..181ec7b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1714 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fb99565819980999fb7b4a1796046a5c949e6d4ff132cf5fadf5a641e20d776" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f2392eae7f16557a3d727ef3a12e57b2b2ca6f98566a5f4fb41ffe305df077" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22053b6a34f84abc97f9129e61334f40174659a1b9bd18c970b83db6a9a6348b" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mp-ai-module-controller" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "dotenvy", + "reqwest", + "serde", + "serde_json", + "shell-words", + "tempfile", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f36b58b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "mp-ai-module-controller" +version = "0.1.0" +edition = "2024" +description = "Rust controller for llama.cpp-driven local module dispatch." + +[dependencies] +anyhow = "1.0" +chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } +clap = { version = "4.5", features = ["derive"] } +dotenvy = "0.15" +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +shell-words = "1.1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[dev-dependencies] +tempfile = "3.10" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b911b61 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# mp-ai-module-controller + +Rust controller for starting a local `llama.cpp` server, asking it to map natural-language requests to known local actions, and dispatching exact-match Rust scripts. + +## Setup + +```sh +cp .env.example .env +# edit LLAMA_MODEL_PATH in .env +cargo build +cargo run -- dictionary generate +git config core.hooksPath .githooks +``` + +The controller expects `llama-server` from `llama.cpp` to be installed. On this machine the planned binary is `/opt/homebrew/bin/llama-server`; if that path is not present, the controller falls back to `llama-server` on `PATH`. + +## Commands + +```sh +cargo run -- dictionary generate +cargo run -- serve +cargo run -- query "log hello from codex" +cargo run -- run logger --payload '{"message":"hello from direct dispatch"}' +scripts/precommit-check.sh +``` + +`serve` starts: + +```sh +llama-server --model "$LLAMA_MODEL_PATH" --host "$LLAMA_HOST" --port "$LLAMA_PORT" +``` + +Optional environment variables: + +- `LLAMA_CONTEXT_SIZE`: appended as `--ctx-size`. +- `LLAMA_EXTRA_ARGS`: shell-split and appended to `llama-server`. +- `CONTROLLER_LOG_LEVEL`: tracing log level, default `info`. + +## Dictionary + +Run `cargo run -- dictionary generate` to create: + +- `dictionary/actions.jsonl`: canonical training dictionary, one action record per line. +- `dictionary/actions.index.json`: compact runtime lookup index. + +The runtime only dispatches exact `action_id` matches from `actions.index.json`. Aliases and examples are training hints, not executable ids. + +## LLM Response Contract + +The local model must return compact JSON: + +```json +{"action_id":"logger","payload":{"message":"hello"}} +``` + +Malformed JSON, missing `action_id`, empty `action_id`, aliases, and unknown action ids are rejected without running anything. + +## Initial Script + +The first registered script is `logger`. It writes JSONL records to `logs/controller-actions.jsonl`. Dispatch is detached: after the controller spawns the script process, it does not monitor completion. + +## CI And Hooks + +The project includes: + +- `.gitea/workflows/ci.yml` for Gitea Actions. +- `.woodpecker.yml` for Woodpecker. +- `.githooks/pre-commit` for local pre-commit checks. +- `scripts/precommit-check.sh` as the shared validation entrypoint. + +Enable the hook after cloning: + +```sh +git config core.hooksPath .githooks +``` diff --git a/llm.txt b/llm.txt new file mode 100644 index 0000000..f3ce65d --- /dev/null +++ b/llm.txt @@ -0,0 +1,41 @@ +# mp-ai-module-controller + +Rust CLI for local llama.cpp action dispatch. + +Important files: + +- `src/main.rs`: CLI entrypoint. +- `src/registry.rs`: source-of-truth action registry. +- `src/dictionary.rs`: generates `dictionary/actions.jsonl` and `dictionary/actions.index.json`. +- `src/llm.rs`: sends OpenAI-compatible chat requests to the local llama.cpp server and parses action JSON. +- `src/dispatcher.rs`: exact-match runtime dispatch and detached script spawning. +- `src/scripts/logger.rs`: proof-of-concept logger script. +- `AGENTS.md`: agent instructions. +- `manifest.llm.json`: machine-readable project metadata. +- `.env.example`: environment variable template. + +Commands: + +- `cargo build` +- `cargo test` +- `cargo run -- dictionary generate` +- `cargo run -- serve` +- `cargo run -- query "log hello from codex"` +- `cargo run -- run logger --payload '{"message":"hello from codex"}'` +- `scripts/codex-setup.sh` +- `scripts/codex-run-logger.sh` +- `scripts/precommit-check.sh` +- `git config core.hooksPath .githooks` + +Environment: + +- `LLAMA_MODEL_PATH`: required for `serve`; local GGUF model path. +- `LLAMA_HOST`: default `127.0.0.1`. +- `LLAMA_PORT`: default `8080`. +- `LLAMA_CONTEXT_SIZE`: optional `--ctx-size`. +- `LLAMA_EXTRA_ARGS`: optional extra llama-server args. +- `CONTROLLER_LOG_LEVEL`: default `info`. + +The LLM must return compact JSON like `{"action_id":"logger","payload":{"message":"hello"}}`. The runtime only executes exact `action_id` matches from `dictionary/actions.index.json`; aliases are not executable. Invalid or unknown outputs are rejected without running anything. + +CI and hooks: `.gitea/workflows/ci.yml`, `.woodpecker.yml`, and `.githooks/pre-commit` all use `scripts/precommit-check.sh` to verify formatting, tests, and dictionary generation. diff --git a/manifest.llm.json b/manifest.llm.json new file mode 100644 index 0000000..6419428 --- /dev/null +++ b/manifest.llm.json @@ -0,0 +1,124 @@ +{ + "schema_version": "1.0", + "name": "mp-ai-module-controller", + "description": "Rust CLI controller for llama.cpp-backed local action dispatch.", + "project_type": "cli", + "language": "rust", + "package_manager": "cargo", + "entrypoints": { + "main": "src/main.rs", + "action_registry": "src/registry.rs", + "dictionary_generator": "src/dictionary.rs", + "llm_client": "src/llm.rs", + "dispatcher": "src/dispatcher.rs", + "logger_script": "src/scripts/logger.rs" + }, + "commands": { + "build": "cargo build", + "test": "cargo test", + "generate_dictionary": "cargo run -- dictionary generate", + "serve": "cargo run -- serve", + "query": "cargo run -- query \"log hello from codex\"", + "run_logger": "cargo run -- run logger --payload '{\"message\":\"hello from codex\"}'", + "codex_setup": "scripts/codex-setup.sh", + "codex_logger_smoke": "scripts/codex-run-logger.sh", + "precommit_check": "scripts/precommit-check.sh", + "install_hooks": "git config core.hooksPath .githooks" + }, + "environment": { + "example_file": ".env.example", + "variables": [ + { + "name": "LLAMA_MODEL_PATH", + "required": true, + "secret": false, + "description": "Absolute path to a local GGUF model used by llama-server." + }, + { + "name": "LLAMA_HOST", + "required": false, + "secret": false, + "default": "127.0.0.1", + "description": "Host for the local llama.cpp server." + }, + { + "name": "LLAMA_PORT", + "required": false, + "secret": false, + "default": "8080", + "description": "Port for the local llama.cpp server." + }, + { + "name": "LLAMA_CONTEXT_SIZE", + "required": false, + "secret": false, + "description": "Optional context size passed to llama-server as --ctx-size." + }, + { + "name": "LLAMA_EXTRA_ARGS", + "required": false, + "secret": false, + "description": "Optional shell-split extra arguments appended to llama-server." + }, + { + "name": "CONTROLLER_LOG_LEVEL", + "required": false, + "secret": false, + "default": "info", + "description": "Tracing log level for the controller." + } + ] + }, + "llama_cpp": { + "server_binary": "/opt/homebrew/bin/llama-server", + "fallback_binary": "llama-server", + "api": "OpenAI-compatible /v1/chat/completions", + "response_contract": { + "format": "compact JSON", + "example": { + "action_id": "logger", + "payload": { + "message": "hello" + } + } + } + }, + "dictionary": { + "source_of_truth": "src/registry.rs", + "generated_jsonl": "dictionary/actions.jsonl", + "generated_index": "dictionary/actions.index.json", + "runtime_policy": "Only exact action_id matches from the generated index are executable." + }, + "actions": [ + { + "action_id": "logger", + "script": "src/scripts/logger.rs", + "description": "Proof-of-concept script that writes structured JSONL log records.", + "payload_example": { + "message": "hello from codex" + }, + "output": "logs/controller-actions.jsonl" + } + ], + "generated_paths": [ + "target/", + "logs/", + "runtime/", + "dictionary/actions.jsonl", + "dictionary/actions.index.json" + ], + "ci": { + "gitea_actions": ".gitea/workflows/ci.yml", + "woodpecker": ".woodpecker.yml", + "precommit_hook": ".githooks/pre-commit", + "shared_check": "scripts/precommit-check.sh", + "checks": [ + "cargo fmt --check", + "cargo test", + "cargo run -- dictionary generate", + "test -s dictionary/actions.jsonl", + "test -s dictionary/actions.index.json" + ] + }, + "agent_guidance": "See AGENTS.md. Keep src/registry.rs as the source of truth, regenerate dictionary files after action changes, and reject non-exact LLM action outputs." +} diff --git a/scripts/codex-run-logger.sh b/scripts/codex-run-logger.sh new file mode 100755 index 0000000..371bebe --- /dev/null +++ b/scripts/codex-run-logger.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +cargo run -- dictionary generate +cargo run -- run logger --payload '{"message":"hello from codex"}' + +echo "logger dispatched; see logs/controller-actions.jsonl" diff --git a/scripts/codex-setup.sh b/scripts/codex-setup.sh new file mode 100755 index 0000000..744d94e --- /dev/null +++ b/scripts/codex-setup.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +command -v rustc >/dev/null +command -v cargo >/dev/null + +if [[ -x /opt/homebrew/bin/llama-server ]]; then + echo "llama-server: /opt/homebrew/bin/llama-server" +else + command -v llama-server >/dev/null + echo "llama-server: $(command -v llama-server)" +fi + +mkdir -p dictionary logs runtime +cargo build +cargo run -- dictionary generate + +echo "codex setup complete" diff --git a/scripts/precommit-check.sh b/scripts/precommit-check.sh new file mode 100755 index 0000000..84125ac --- /dev/null +++ b/scripts/precommit-check.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +cargo fmt --check +cargo test +cargo run -- dictionary generate + +test -s dictionary/actions.jsonl +test -s dictionary/actions.index.json + +echo "precommit check complete" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..762748a --- /dev/null +++ b/src/config.rs @@ -0,0 +1,26 @@ +use std::env; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ControllerConfig { + pub llama_host: String, + pub llama_port: u16, +} + +impl ControllerConfig { + pub fn from_env() -> Self { + let llama_host = env::var("LLAMA_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); + let llama_port = env::var("LLAMA_PORT") + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(8080); + + Self { + llama_host, + llama_port, + } + } + + pub fn base_url(&self) -> String { + format!("http://{}:{}", self.llama_host, self.llama_port) + } +} diff --git a/src/dictionary.rs b/src/dictionary.rs new file mode 100644 index 0000000..214abeb --- /dev/null +++ b/src/dictionary.rs @@ -0,0 +1,145 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::fs::{self, File}; +use std::io::Write; +#[cfg(test)] +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; + +use crate::registry::ActionDefinition; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ActionDictionaryRecord { + pub action_id: String, + pub aliases: Vec, + pub description: String, + pub intent_examples: Vec, + pub payload_schema: serde_json::Value, + pub executable: crate::registry::ExecutableDefinition, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ActionIndex { + pub schema_version: String, + pub actions: BTreeMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DictionarySummary { + pub action_count: usize, + pub jsonl_path: PathBuf, + pub index_path: PathBuf, +} + +pub fn default_dictionary_dir() -> PathBuf { + PathBuf::from("dictionary") +} + +pub fn generate_dictionary_files( + actions: &[ActionDefinition], + out_dir: &Path, +) -> Result { + fs::create_dir_all(out_dir) + .with_context(|| format!("creating dictionary directory {}", out_dir.display()))?; + + let jsonl_path = out_dir.join("actions.jsonl"); + let index_path = out_dir.join("actions.index.json"); + let records = actions + .iter() + .cloned() + .map(ActionDictionaryRecord::from) + .collect::>(); + + let mut jsonl = + File::create(&jsonl_path).with_context(|| format!("creating {}", jsonl_path.display()))?; + for record in &records { + serde_json::to_writer(&mut jsonl, record)?; + jsonl.write_all(b"\n")?; + } + + let index = ActionIndex { + schema_version: "1.0".to_string(), + actions: records + .into_iter() + .map(|record| (record.action_id.clone(), record)) + .collect(), + }; + + let pretty = serde_json::to_string_pretty(&index)?; + fs::write(&index_path, format!("{pretty}\n")) + .with_context(|| format!("writing {}", index_path.display()))?; + + Ok(DictionarySummary { + action_count: index.actions.len(), + jsonl_path, + index_path, + }) +} + +pub fn load_action_index(dictionary_dir: &Path) -> Result { + let path = dictionary_dir.join("actions.index.json"); + let raw = fs::read_to_string(&path).with_context(|| { + format!( + "reading {}; run `cargo run -- dictionary generate` first", + path.display() + ) + })?; + let index = serde_json::from_str::(&raw) + .with_context(|| format!("parsing {}", path.display()))?; + Ok(index) +} + +#[cfg(test)] +fn read_jsonl_records(path: &Path) -> Result> { + let file = File::open(path).with_context(|| format!("opening {}", path.display()))?; + let reader = BufReader::new(file); + let mut records = Vec::new(); + + for line in reader.lines() { + let line = line?; + if line.trim().is_empty() { + continue; + } + records.push(serde_json::from_str::(&line)?); + } + + Ok(records) +} + +impl From for ActionDictionaryRecord { + fn from(action: ActionDefinition) -> Self { + Self { + action_id: action.action_id, + aliases: action.aliases, + description: action.description, + intent_examples: action.intent_examples, + payload_schema: action.payload_schema, + executable: action.executable, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::registry::registered_actions; + + #[test] + fn generates_jsonl_and_index() { + let temp = tempfile::tempdir().expect("temp dir"); + let summary = generate_dictionary_files(®istered_actions(), temp.path()).unwrap(); + + assert_eq!(summary.action_count, 1); + assert!(summary.jsonl_path.exists()); + assert!(summary.index_path.exists()); + + let records = read_jsonl_records(&summary.jsonl_path).unwrap(); + assert_eq!(records.len(), 1); + assert_eq!(records[0].action_id, "logger"); + + let index = load_action_index(temp.path()).unwrap(); + assert!(index.actions.contains_key("logger")); + assert!(!index.actions.contains_key("log")); + } +} diff --git a/src/dispatcher.rs b/src/dispatcher.rs new file mode 100644 index 0000000..bd0f717 --- /dev/null +++ b/src/dispatcher.rs @@ -0,0 +1,95 @@ +use anyhow::{Context, Result, anyhow, bail}; +use serde_json::Value; +use std::fs::{self, OpenOptions}; +use std::process::{Command, Stdio}; + +use crate::dictionary::ActionIndex; +use crate::scripts::logger::{LoggerPayload, run_logger}; + +pub fn dispatch_action(index: &ActionIndex, action_id: &str, payload: Value) -> Result<()> { + let action = index + .actions + .get(action_id) + .ok_or_else(|| anyhow!("unknown action_id `{action_id}`; refusing to dispatch"))?; + + if action.executable.kind != "internal-rust-script" { + bail!( + "unsupported executable kind `{}` for action `{}`", + action.executable.kind, + action.action_id + ); + } + + let payload = serde_json::to_string(&payload)?; + let current_exe = std::env::current_exe().context("resolving current executable")?; + let log_file = open_child_log()?; + + Command::new(current_exe) + .arg("__script") + .arg(&action.executable.script) + .arg("--payload") + .arg(payload) + .stdin(Stdio::null()) + .stdout(Stdio::from(log_file.try_clone()?)) + .stderr(Stdio::from(log_file)) + .spawn() + .with_context(|| format!("spawning action `{action_id}`"))?; + + tracing::info!("dispatched action `{}`", action_id); + println!("dispatched action `{action_id}`"); + Ok(()) +} + +pub fn run_internal_script(action_id: &str, payload: Value) -> Result<()> { + match action_id { + "logger" => { + let payload = serde_json::from_value::(payload)?; + run_logger(payload) + } + other => bail!("unknown internal script `{other}`"), + } +} + +fn open_child_log() -> Result { + fs::create_dir_all("logs").context("creating logs directory")?; + let file = OpenOptions::new() + .create(true) + .append(true) + .open("logs/controller-child.log") + .context("opening logs/controller-child.log")?; + Ok(file) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dictionary::ActionIndex; + use crate::registry::registered_actions; + use std::collections::BTreeMap; + + fn test_index() -> ActionIndex { + let actions = registered_actions() + .into_iter() + .map(crate::dictionary::ActionDictionaryRecord::from) + .map(|record| (record.action_id.clone(), record)) + .collect::>(); + + ActionIndex { + schema_version: "1.0".to_string(), + actions, + } + } + + #[test] + fn exact_action_lookup_accepts_registered_id() { + let index = test_index(); + assert!(index.actions.contains_key("logger")); + } + + #[test] + fn exact_action_lookup_rejects_alias() { + let index = test_index(); + let result = dispatch_action(&index, "log", serde_json::json!({ "message": "test" })); + assert!(result.is_err()); + } +} diff --git a/src/llm.rs b/src/llm.rs new file mode 100644 index 0000000..c494917 --- /dev/null +++ b/src/llm.rs @@ -0,0 +1,167 @@ +use anyhow::{Context, Result, anyhow, bail}; +use reqwest::blocking::Client; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::time::Duration; + +use crate::config::ControllerConfig; +use crate::dictionary::ActionIndex; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ActionRequest { + pub action_id: String, + #[serde(default)] + pub payload: Value, +} + +#[derive(Debug, Serialize)] +struct ChatRequest { + model: String, + messages: Vec, + temperature: f32, + stream: bool, +} + +#[derive(Debug, Serialize)] +struct ChatMessage { + role: String, + content: String, +} + +#[derive(Debug, Deserialize)] +struct ChatResponse { + choices: Vec, +} + +#[derive(Debug, Deserialize)] +struct ChatChoice { + message: ChatChoiceMessage, +} + +#[derive(Debug, Deserialize)] +struct ChatChoiceMessage { + content: String, +} + +pub fn query_llama(config: &ControllerConfig, index: &ActionIndex, text: &str) -> Result { + let actions = serde_json::to_string(&index.actions)?; + let system = format!( + "You map user requests to exact local action ids. Return only compact JSON with shape {{\"action_id\":\"...\",\"payload\":{{...}}}}. Valid actions index: {actions}" + ); + let request = ChatRequest { + model: "local".to_string(), + messages: vec![ + ChatMessage { + role: "system".to_string(), + content: system, + }, + ChatMessage { + role: "user".to_string(), + content: text.to_string(), + }, + ], + temperature: 0.0, + stream: false, + }; + + let url = format!("{}/v1/chat/completions", config.base_url()); + let client = Client::builder() + .timeout(Duration::from_secs(120)) + .build() + .context("building HTTP client")?; + let response = client + .post(&url) + .json(&request) + .send() + .with_context(|| format!("posting to {url}"))? + .error_for_status() + .with_context(|| format!("llama.cpp server returned an error for {url}"))? + .json::() + .context("parsing OpenAI-compatible chat completion response")?; + + response + .choices + .into_iter() + .next() + .map(|choice| choice.message.content) + .ok_or_else(|| anyhow!("llama.cpp response had no choices")) +} + +pub fn parse_action_response(raw: &str) -> Result { + let trimmed = raw.trim(); + let json_text = if trimmed.starts_with('{') { + trimmed + } else { + extract_first_json_object(trimmed) + .ok_or_else(|| anyhow!("LLM response did not contain a JSON object"))? + }; + + let request = serde_json::from_str::(json_text) + .with_context(|| format!("parsing LLM action JSON: {json_text}"))?; + + if request.action_id.trim().is_empty() { + bail!("LLM action JSON had an empty action_id"); + } + + Ok(request) +} + +fn extract_first_json_object(text: &str) -> Option<&str> { + let start = text.find('{')?; + let mut depth = 0_i32; + let mut in_string = false; + let mut escaped = false; + + for (offset, ch) in text[start..].char_indices() { + if escaped { + escaped = false; + continue; + } + + match ch { + '\\' if in_string => escaped = true, + '"' => in_string = !in_string, + '{' if !in_string => depth += 1, + '}' if !in_string => { + depth -= 1; + if depth == 0 { + let end = start + offset + ch.len_utf8(); + return Some(&text[start..end]); + } + } + _ => {} + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_valid_action_response() { + let parsed = + parse_action_response(r#"{"action_id":"logger","payload":{"message":"hello"}}"#) + .unwrap(); + assert_eq!(parsed.action_id, "logger"); + assert_eq!(parsed.payload["message"], "hello"); + } + + #[test] + fn parses_embedded_json_response() { + let parsed = parse_action_response( + r#"Here is the action: {"action_id":"logger","payload":{"message":"hello"}}"#, + ) + .unwrap(); + assert_eq!(parsed.action_id, "logger"); + } + + #[test] + fn rejects_malformed_response() { + assert!(parse_action_response("run logger please").is_err()); + assert!(parse_action_response(r#"{"payload":{"message":"missing id"}}"#).is_err()); + assert!(parse_action_response(r#"{"action_id":"","payload":{}}"#).is_err()); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1c60795 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,125 @@ +mod config; +mod dictionary; +mod dispatcher; +mod llm; +mod registry; +mod scripts; +mod server; + +use anyhow::Result; +use clap::{Parser, Subcommand}; +use serde_json::Value; +use std::path::PathBuf; + +use crate::config::ControllerConfig; +use crate::dictionary::{default_dictionary_dir, generate_dictionary_files, load_action_index}; +use crate::dispatcher::{dispatch_action, run_internal_script}; +use crate::llm::{parse_action_response, query_llama}; +use crate::registry::registered_actions; +use crate::server::start_llama_server; + +#[derive(Debug, Parser)] +#[command(name = "mp-ai-module-controller")] +#[command(about = "Control local Rust scripts through a llama.cpp action dictionary.")] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Start a llama.cpp server using LLAMA_MODEL_PATH. + Serve, + /// Generate or inspect the LLM action dictionary. + Dictionary { + #[command(subcommand)] + command: DictionaryCommand, + }, + /// Query the local LLM and dispatch the exact action id it returns. + Query { + /// Natural-language request to send to the local model. + text: String, + }, + /// Dispatch a known action id directly. + Run { + /// Exact action id from dictionary/actions.index.json. + action_id: String, + /// JSON payload for the action. Defaults to the logger proof-of-concept payload. + #[arg(long)] + payload: Option, + }, + /// Internal detached script runner. Not part of the public CLI. + #[command(name = "__script", hide = true)] + Script { + action_id: String, + #[arg(long)] + payload: String, + }, +} + +#[derive(Debug, Subcommand)] +enum DictionaryCommand { + /// Generate dictionary/actions.jsonl and dictionary/actions.index.json. + Generate { + /// Output directory for generated dictionary files. + #[arg(long)] + out_dir: Option, + }, +} + +fn main() -> Result<()> { + let _ = dotenvy::dotenv(); + init_tracing(); + + let cli = Cli::parse(); + match cli.command { + Command::Serve => { + let config = ControllerConfig::from_env(); + start_llama_server(&config) + } + Command::Dictionary { + command: DictionaryCommand::Generate { out_dir }, + } => { + let out_dir = out_dir.unwrap_or_else(default_dictionary_dir); + let summary = generate_dictionary_files(®istered_actions(), &out_dir)?; + println!( + "generated {} actions: {} and {}", + summary.action_count, + summary.jsonl_path.display(), + summary.index_path.display() + ); + Ok(()) + } + Command::Query { text } => { + let config = ControllerConfig::from_env(); + let index = load_action_index(&default_dictionary_dir())?; + let response = query_llama(&config, &index, &text)?; + let request = parse_action_response(&response)?; + dispatch_action(&index, &request.action_id, request.payload) + } + Command::Run { action_id, payload } => { + let index = load_action_index(&default_dictionary_dir())?; + let payload = match payload { + Some(raw) => serde_json::from_str::(&raw)?, + None => serde_json::json!({ "message": "logger proof of concept" }), + }; + dispatch_action(&index, &action_id, payload) + } + Command::Script { action_id, payload } => { + let payload = serde_json::from_str::(&payload)?; + run_internal_script(&action_id, payload) + } + } +} + +fn init_tracing() { + let level = std::env::var("CONTROLLER_LOG_LEVEL").unwrap_or_else(|_| "info".to_string()); + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| tracing_subscriber::EnvFilter::try_new(level)) + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); + + let _ = tracing_subscriber::fmt() + .with_env_filter(filter) + .with_target(false) + .try_init(); +} diff --git a/src/registry.rs b/src/registry.rs new file mode 100644 index 0000000..5bb6b3d --- /dev/null +++ b/src/registry.rs @@ -0,0 +1,50 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ActionDefinition { + pub action_id: String, + pub aliases: Vec, + pub description: String, + pub intent_examples: Vec, + pub payload_schema: Value, + pub executable: ExecutableDefinition, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ExecutableDefinition { + pub kind: String, + pub script: String, +} + +pub fn registered_actions() -> Vec { + vec![ActionDefinition { + action_id: "logger".to_string(), + aliases: vec![ + "log".to_string(), + "write_log".to_string(), + "record_message".to_string(), + ], + description: "Write a structured proof-of-concept log message.".to_string(), + intent_examples: vec![ + "log hello from codex".to_string(), + "record this message".to_string(), + "write a logger proof of concept entry".to_string(), + ], + payload_schema: serde_json::json!({ + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string", + "description": "Message text to write into logs/controller-actions.jsonl." + } + }, + "additionalProperties": false + }), + executable: ExecutableDefinition { + kind: "internal-rust-script".to_string(), + script: "logger".to_string(), + }, + }] +} diff --git a/src/scripts/logger.rs b/src/scripts/logger.rs new file mode 100644 index 0000000..0d70213 --- /dev/null +++ b/src/scripts/logger.rs @@ -0,0 +1,73 @@ +use anyhow::{Context, Result}; +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use std::fs::{self, OpenOptions}; +use std::io::Write; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct LoggerPayload { + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct LoggerRecord { + pub timestamp: String, + pub action_id: String, + pub message: String, +} + +pub fn run_logger(payload: LoggerPayload) -> Result<()> { + let record = build_logger_record(payload)?; + fs::create_dir_all("logs").context("creating logs directory")?; + + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open("logs/controller-actions.jsonl") + .context("opening logs/controller-actions.jsonl")?; + + serde_json::to_writer(&mut file, &record)?; + file.write_all(b"\n")?; + println!("logger wrote: {}", record.message); + Ok(()) +} + +pub fn build_logger_record(payload: LoggerPayload) -> Result { + let message = payload.message.trim(); + if message.is_empty() { + anyhow::bail!("logger payload message must not be empty"); + } + + Ok(LoggerRecord { + timestamp: Utc::now().to_rfc3339(), + action_id: "logger".to_string(), + message: message.to_string(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validates_and_serializes_payload() { + let payload = LoggerPayload { + message: " hello ".to_string(), + }; + let record = build_logger_record(payload).unwrap(); + assert_eq!(record.action_id, "logger"); + assert_eq!(record.message, "hello"); + + let json = serde_json::to_string(&record).unwrap(); + assert!(json.contains(r#""action_id":"logger""#)); + assert!(json.contains(r#""message":"hello""#)); + } + + #[test] + fn rejects_empty_message() { + let result = build_logger_record(LoggerPayload { + message: " ".to_string(), + }); + assert!(result.is_err()); + } +} diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs new file mode 100644 index 0000000..d991728 --- /dev/null +++ b/src/scripts/mod.rs @@ -0,0 +1 @@ +pub mod logger; diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..7b2e181 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,54 @@ +use anyhow::{Context, Result, anyhow}; +use std::path::Path; +use std::process::Command; + +use crate::config::ControllerConfig; + +pub fn start_llama_server(config: &ControllerConfig) -> Result<()> { + let model_path = std::env::var("LLAMA_MODEL_PATH") + .map_err(|_| anyhow!("LLAMA_MODEL_PATH is required and must point to a GGUF model"))?; + + let binary = llama_server_binary(); + let mut command = Command::new(&binary); + command + .arg("--model") + .arg(model_path) + .arg("--host") + .arg(&config.llama_host) + .arg("--port") + .arg(config.llama_port.to_string()); + + if let Ok(context_size) = std::env::var("LLAMA_CONTEXT_SIZE") { + if !context_size.trim().is_empty() { + command.arg("--ctx-size").arg(context_size); + } + } + + if let Ok(extra_args) = std::env::var("LLAMA_EXTRA_ARGS") { + if !extra_args.trim().is_empty() { + for arg in shell_words::split(&extra_args).context("parsing LLAMA_EXTRA_ARGS")? { + command.arg(arg); + } + } + } + + tracing::info!("starting llama.cpp server on {}", config.base_url()); + let status = command + .status() + .with_context(|| format!("starting {}", binary.display()))?; + + if status.success() { + Ok(()) + } else { + Err(anyhow!("llama-server exited with status {status}")) + } +} + +fn llama_server_binary() -> std::path::PathBuf { + let homebrew_path = Path::new("/opt/homebrew/bin/llama-server"); + if homebrew_path.exists() { + homebrew_path.to_path_buf() + } else { + "llama-server".into() + } +}