diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 56ea4e9..2eee8b6 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -10,16 +10,16 @@ env: CARGO_TERM_COLOR: always RUSTFLAGS: "-D warnings" RUST_TOOLCHAIN: "1.86" - CARGO_NET_GIT_FETCH_WITH_CLI: "true" jobs: ci: name: Linux x86_64 (Forgejo) runs-on: ubuntu-latest steps: - # Manual external-URL checkout — see fai/platform CI for - # background. Lays this repo at $GITHUB_WORKSPACE. - - name: Checkout text-extract via external URL + # See fai/platform docs/architecture/ci.md for why we + # check out via the external URL instead of using + # actions/checkout. + - name: Checkout via external URL run: | set -eu mkdir -p "$GITHUB_WORKSPACE" @@ -30,23 +30,6 @@ jobs: git fetch --depth=1 origin "$GITHUB_SHA" git checkout -q FETCH_HEAD - # The chain-module-sdk dependency is a git dep against a repo - # that lives in another Forgejo org. The repo-scoped - # GITHUB_TOKEN cannot read it, and the Forgejo instance has - # REQUIRE_SIGNIN_VIEW=true so anonymous read is also denied. - # MODULE_SDK_PAT is a stored secret with read access to - # fai/module-sdk; we inject it into any outgoing - # https://git.flemming.ws/ URL via insteadOf so cargo's git - # fetch (driven by CARGO_NET_GIT_FETCH_WITH_CLI) authenticates - # transparently. - - name: Configure git URL rewrite for SDK fetch - env: - SDK_PAT: ${{ secrets.MODULE_SDK_PAT }} - run: | - git config --global \ - "url.https://x-access-token:${SDK_PAT}@git.flemming.ws/.insteadOf" \ - "https://git.flemming.ws/" - - name: Install system dependencies run: | apt-get update -qq @@ -73,13 +56,13 @@ jobs: run: cargo fmt --all -- --check - name: Cargo clippy - run: cargo clippy --all-targets -- -D warnings + run: cargo clippy --workspace --all-targets -- -D warnings - name: Cargo build (host) - run: cargo build --all-targets + run: cargo build --workspace --all-targets - name: Cargo test (host) - run: cargo test --all-targets + run: cargo test --workspace --all-targets - name: Cargo build (wasm32-wasip2) run: cargo build --release --target wasm32-wasip2 diff --git a/.forgejo/workflows/sign.yml b/.forgejo/workflows/sign.yml deleted file mode 100644 index 25b768e..0000000 --- a/.forgejo/workflows/sign.yml +++ /dev/null @@ -1,223 +0,0 @@ -# F∆I module bundle signing workflow (cosign key-pinning model). -# -# Drop-in template for any module repo under chain-modules/. -# Triggers on tag push (vX.Y.Z) — builds the module, packs the -# .fai bundle, signs the bundle bytes with the Flemming.AI -# signing key (ECDSA P-256), and attaches bundle + .sig sidecar -# to the matching Forgejo Release. -# -# The hub-side verifier ships in 0.12.0+: when an operator sets -# `require_signatures: true`, install downloads the .sig sidecar -# from `.sig` and verifies it against the pinned store -# public key (built-in for `bundled` / `official`, per-store -# config for everything else). -# -# Adopt by copying this file to `.forgejo/workflows/sign.yml` in -# the module repo and replacing the MODULE_NAME placeholder. -# Add the signing key to Forgejo secrets as FAI_SIGNING_KEY (PEM, -# unencrypted, ECDSA P-256). Rotation is a 5-minute key-roll + -# binary re-release; see infra/cosign/README.md. - -name: sign-bundle - -on: - push: - tags: - - 'v*.*.*' - -env: - CARGO_TERM_COLOR: always - RUST_TOOLCHAIN: "1.86" - CARGO_NET_GIT_FETCH_WITH_CLI: "true" - -jobs: - sign: - runs-on: ubuntu-latest - permissions: - contents: write # attach signature to release - - env: - # The canonical module name as written in module.yaml. - # Used in the bundle filename + wasm path. - MODULE_NAME: orchestrator-llm - - steps: - # See fai/platform CI for background on the manual external - # checkout: the DinD runner cannot resolve forgejo:3000, so - # we clone via the external URL with $GITHUB_TOKEN. - - name: Checkout via external URL - run: | - set -eu - mkdir -p "$GITHUB_WORKSPACE" - cd "$GITHUB_WORKSPACE" - git init -q - git remote add origin \ - "https://x-access-token:${GITHUB_TOKEN}@git.flemming.ai/${GITHUB_REPOSITORY}.git" - git fetch --depth=1 origin "$GITHUB_SHA" - git checkout -q FETCH_HEAD - - # The chain-module-sdk dependency lives in another Forgejo org - # behind REQUIRE_SIGNIN_VIEW. MODULE_SDK_PAT (org-level - # secret, read-only on fai/module-sdk) authenticates cargo's - # git fetch via insteadOf. - - name: Configure git URL rewrite for SDK fetch - env: - SDK_PAT: ${{ secrets.MODULE_SDK_PAT }} - run: | - git config --global \ - "url.https://x-access-token:${SDK_PAT}@git.flemming.ai/.insteadOf" \ - "https://git.flemming.ai/" - - - name: Install system dependencies - run: | - apt-get update -qq - apt-get install -y --no-install-recommends \ - curl \ - ca-certificates \ - build-essential \ - pkg-config \ - libssl-dev \ - git \ - jq \ - openssl - - - name: Install Rust toolchain - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y \ - --profile minimal \ - --default-toolchain "$RUST_TOOLCHAIN" \ - --target wasm32-wasip2 - echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - - - name: Build module (wasm32-wasip2) - run: cargo build --release --target wasm32-wasip2 - - - name: Install fai CLI - # Pinned to the production channel; signing requires the - # `fai pack` command. Override FAI_VERSION when bundle - # format compatibility matters. - run: | - curl -fsSL https://get.chain.flemming.ai | sh -s -- --no-bootstrap - # The installer drops fai at $HOME/.local/bin - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - export PATH="$HOME/.local/bin:$PATH" - fai --version - - - name: Stage bundle inputs - # The wasm artefact is named after the Cargo package - # (underscored), which may differ from the module name - # in module.yaml (dashed). Resolve it from Cargo.toml so - # the template stays agnostic to the project's naming - # convention. - run: | - set -eu - CARGO_NAME=$(grep -E '^name *= *"' Cargo.toml \ - | head -1 | sed 's/.*"\(.*\)".*/\1/') - mkdir -p staging - cp module.yaml staging/ - cp "target/wasm32-wasip2/release/${CARGO_NAME}.wasm" staging/module.wasm - if [ -f sbom.cdx.json ]; then cp sbom.cdx.json staging/; fi - if [ -f MODULE.md ]; then cp MODULE.md staging/; fi - if [ -f MODULE.de.md ]; then cp MODULE.de.md staging/; fi - - - name: Pack bundle - id: pack - run: | - export PATH="$HOME/.local/bin:$PATH" - BUNDLE="${{ env.MODULE_NAME }}-${{ github.ref_name }}.fai" - fai pack staging --output "$BUNDLE" - echo "bundle=$BUNDLE" >> "$GITHUB_OUTPUT" - ls -la "$BUNDLE" - - - name: Sign bundle (Flemming.AI signing key) - env: - FAI_SIGNING_KEY_PEM: ${{ secrets.FAI_SIGNING_KEY }} - run: | - if [ -z "${FAI_SIGNING_KEY_PEM:-}" ]; then - cat >&2 <<'EOF' - - FAI_SIGNING_KEY secret is empty or undefined. - - This workflow signs module bundles with the Flemming.AI - module-signing key (ECDSA P-256, PKCS#8 PEM, unencrypted). - The matching public key is pinned in the fai-hub binary at - infra/cosign/official.pub — hubs that require_signatures - will reject anything not signed with this key. - - To configure: - 1. Forgejo -> chain-modules org -> Settings -> Secrets - 2. Add secret FAI_SIGNING_KEY (org-level recommended) - 3. Paste the unencrypted PEM (BEGIN PRIVATE KEY ...) - The encrypted source-of-truth lives on Stefan's Mac at - ~/.fai-secrets/flemming-ai-signing.key (passphrase in - mSecure under "Ch∆In — Module Signing Key (Private)"). - - See fai/platform infra/cosign/OPERATOR-HANDOFF.md for the - full setup runbook and rotation process. - EOF - exit 1 - fi - umask 077 - echo "$FAI_SIGNING_KEY_PEM" > /tmp/signing-key.pem - openssl dgst -sha256 \ - -sign /tmp/signing-key.pem \ - -out "${{ steps.pack.outputs.bundle }}.sig.raw" \ - "${{ steps.pack.outputs.bundle }}" - base64 < "${{ steps.pack.outputs.bundle }}.sig.raw" \ - > "${{ steps.pack.outputs.bundle }}.sig" - rm "${{ steps.pack.outputs.bundle }}.sig.raw" - shred -u /tmp/signing-key.pem || rm -f /tmp/signing-key.pem - ls -la "${{ steps.pack.outputs.bundle }}.sig" - - - name: Round-trip verify - # Sanity-check: the freshly-signed bundle must verify - # against the well-known public key before we publish it. - run: | - curl -fsSL https://git.flemming.ai/fai/platform/raw/branch/main/infra/cosign/official.pub \ - -o /tmp/official.pub - openssl dgst -sha256 \ - -verify /tmp/official.pub \ - -signature <(base64 -d < "${{ steps.pack.outputs.bundle }}.sig") \ - "${{ steps.pack.outputs.bundle }}" - - - name: Attach bundle + signature to Forgejo Release - run: | - set -eu - BUNDLE="${{ steps.pack.outputs.bundle }}" - SIG="$BUNDLE.sig" - REL_JSON=$(curl -sS \ - -H "Authorization: token ${GITHUB_TOKEN}" \ - "https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}") - REL_ID=$(echo "$REL_JSON" | jq -r '.id // empty') - if [ -z "$REL_ID" ] || [ "$REL_ID" = "null" ]; then - REL_ID=$(curl -sS -X POST \ - -H "Authorization: token ${GITHUB_TOKEN}" \ - -H "Content-Type: application/json" \ - "https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases" \ - -d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\",\"draft\":false,\"prerelease\":false,\"body\":\"Auto-generated by sign-bundle workflow\"}" \ - | jq -r '.id') - fi - upload_one() { - local asset_path="$1" asset - asset=$(basename "$asset_path") - EXISTING=$(curl -sS \ - -H "Authorization: token ${GITHUB_TOKEN}" \ - "https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases/${REL_ID}/assets" \ - | jq -r ".[] | select(.name==\"${asset}\") | .id") - if [ -n "$EXISTING" ]; then - curl -sS -X DELETE \ - -H "Authorization: token ${GITHUB_TOKEN}" \ - "https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases/${REL_ID}/assets/${EXISTING}" \ - || true - fi - curl -sS -X POST \ - -H "Authorization: token ${GITHUB_TOKEN}" \ - -F "attachment=@${asset_path}" \ - "https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases/${REL_ID}/assets?name=${asset}" \ - > /dev/null - echo "uploaded $asset" - } - upload_one "$BUNDLE" - upload_one "$SIG" - echo "release ${GITHUB_REF_NAME} ready: https://git.flemming.ai/${GITHUB_REPOSITORY}/releases/tag/${GITHUB_REF_NAME}" diff --git a/Cargo.toml b/Cargo.toml index 5f7f6c0..6f0614c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,19 +5,19 @@ [package] name = "orchestrator_llm" -version = "0.3.2" +version = "0.3.1" edition = "2024" -authors = ["Dr. Stefan Flemming "] +authors = ["Dr. Stefan Flemming "] license = "Apache-2.0" publish = false description = "F∆I orchestrator module — turns natural-language goals into structured execution plans" -repository = "https://git.flemming.ws/chain-modules/orchestrator-llm" +repository = "https://git.flemming.ws/fai-modules/orchestrator-llm" [lib] crate-type = ["cdylib", "rlib"] [dependencies] -chain-module-sdk = { git = "https://git.flemming.ai/fai/chain-module-sdk-rust.git", branch = "main" } +fai-module-sdk = { git = "https://git.flemming.ws/fai/module-sdk.git", branch = "main" } serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" diff --git a/LICENSE b/LICENSE index cdde5b2..9536d06 100644 --- a/LICENSE +++ b/LICENSE @@ -58,7 +58,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2026 Flemming.AI +Copyright 2026 fai-modules Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/module.wasm b/module.wasm deleted file mode 100644 index 84fafc4..0000000 Binary files a/module.wasm and /dev/null differ diff --git a/module.yaml b/module.yaml index c176aa8..f1fff80 100644 --- a/module.yaml +++ b/module.yaml @@ -1,111 +1,55 @@ -schema_version: 3 -provider: chain +schema_version: 1 name: orchestrator-llm version: 0.3.1 +# Capability provided by this module. provides: - capability: orchestrator.plan version: 0.3.0 -# Capabilities the plans this module emits typically reference. -# Advisory only — Studio surfaces "you'll also want X" when the -# operator installs orchestrator-llm; `fai admin doctor` flags -# unmet entries; the hub does NOT block install. -consumes: - - capability: llm.chat - version: "^0" - why: "orchestrator-emitted plans dispatch llm.chat steps for downstream LLM-driven work; a flow that runs the plan unmodified needs an llm.chat provider installed." - +# Inputs the invoke function accepts. inputs: - goal: - type: text - description: - en: Operator-stated goal in natural language — what the resulting flow should accomplish. - de: Ziel in natürlicher Sprache — was der erzeugte Flow erreichen soll. - llm_endpoint: - type: text - description: - en: | - LLM HTTP endpoint, e.g. "http://localhost:11434/api/chat" - for Ollama, "https://api.openai.com/v1/chat/completions", - or "https://api.anthropic.com/v1/messages". - de: | - LLM-HTTP-Endpunkt, z. B. "http://localhost:11434/api/chat" - für Ollama, "https://api.openai.com/v1/chat/completions" - oder "https://api.anthropic.com/v1/messages". - llm_model: - type: text - description: - en: Model identifier, e.g. "qwen2.5:14b" for Ollama or "gpt-4o-mini". - de: Modell-Identifier, z. B. "qwen2.5:14b" (Ollama) oder "gpt-4o-mini". - llm_api_key: - type: text - description: - en: Optional bearer token for cloud providers. Empty = no Authorization header. - de: Optionaler Bearer-Token für Cloud-Provider. Leer = kein Authorization-Header. - available_capabilities: - type: text - description: - en: | - Optional JSON-encoded list of installed capabilities. - Omit or pass an empty string to plan without capability - context. - de: | - Optional JSON-codierte Liste der installierten - Capabilities. Leer = ohne Capability-Kontext planen. - store_capabilities: - type: text - description: - en: | - Optional JSON-encoded list of all known store - capabilities (installed + planned + alpha). Used when - the orchestrator needs to suggest installations. - de: | - Optional JSON-codierte Liste aller bekannten Store- - Capabilities (installiert + geplant + alpha). Nötig, - wenn der Orchestrator Installationen vorschlagen soll. + goal: text + # LLM HTTP endpoint, e.g. "http://localhost:11434/api/chat" + # for Ollama, "https://api.openai.com/v1/chat/completions", + # or "https://api.anthropic.com/v1/messages". + llm_endpoint: text + # Model identifier, e.g. "qwen2.5:14b" for Ollama or "gpt-4o-mini". + llm_model: text + # Optional bearer token for cloud providers. Empty string means + # no Authorization header is sent. + llm_api_key: text + # Optional JSON-encoded list of installed capabilities. If + # omitted or empty, the module plans without capability context. + available_capabilities: text + # Optional JSON-encoded list of all known store capabilities + # (installed + planned + alpha). Used when the orchestrator + # needs to suggest installations. + store_capabilities: text +# Outputs produced. outputs: - plan: - type: json - description: - en: JSON-encoded Plan compatible with `fai_hub::plan::Plan`. - de: JSON-codierter Plan, kompatibel mit `fai_hub::plan::Plan`. - model_endpoint: - type: text - description: - en: | - The endpoint the plan was generated against. Empty when - the deterministic stub path (no llm_endpoint) ran. - de: | - Endpunkt, gegen den der Plan erzeugt wurde. Leer beim - deterministischen Stub-Pfad (ohne llm_endpoint). - model_name: - type: text - description: - en: The model name as supplied to the LLM API. - de: An die LLM-API übergebener Modellname. - model_digest: - type: text - description: - en: | - SHA-256 digest of the served Ollama model, when - reachable. Empty for cloud providers (OpenAI / Anthropic - do not expose a digest API) and for the deterministic - stub path. Together with model_endpoint and model_name - this answers the audit question "which exact model - produced this plan?" - de: | - SHA-256-Digest des bedienten Ollama-Modells, wenn - erreichbar. Leer für Cloud-Provider (OpenAI / Anthropic - bieten keine Digest-API) und für den deterministischen - Stub-Pfad. Zusammen mit model_endpoint + model_name - beantwortet das die Audit-Frage "welches genaue Modell - hat diesen Plan erzeugt?" + # JSON-encoded Plan compatible with `fai_hub::plan::Plan`. + plan: json + # The endpoint the plan was generated against. Empty when the + # deterministic stub path (no llm_endpoint) ran. + model_endpoint: text + # The model name as supplied to the LLM API. + model_name: text + # SHA-256 digest of the served Ollama model, when reachable. + # Empty for cloud providers (OpenAI / Anthropic do not expose + # a digest API) and for the deterministic stub path. Together + # with model_endpoint and model_name this answers the audit + # question "which exact model produced this plan?" + model_digest: text # Permissions required. # # The module makes outbound HTTP to the configured LLM endpoint. +# Default declarations cover the common providers (cloud OpenAI, +# Anthropic, local Ollama on loopback). Operators with different +# LLM endpoints add a host-level entry via operator config +# (Phase 1+) — for Phase 0.5 they fork module.yaml. permissions: - "net: api.openai.com" - "net: api.anthropic.com" diff --git a/src/lib.rs b/src/lib.rs index adc4e2a..900660c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ //! orchestrator-llm — turns natural-language goals into Plan JSON. //! //! v0.3.0 migrates from a hand-written wit_bindgen scaffold to the -//! `#[fai_module]` macro from `chain-module-sdk`. The behavior is +//! `#[fai_module]` macro from `fai-module-sdk`. The behavior is //! unchanged from v0.2.1: a configured Ollama-compatible endpoint //! drives a real plan when `llm_endpoint` is set, otherwise the //! deterministic stub from v0.1.0 takes over. @@ -19,7 +19,7 @@ mod plan; pub use plan::{Plan, PlanStep, build_stub_plan, parse_and_validate, validate}; -use chain_module_sdk::prelude::*; +use fai_module_sdk::prelude::*; #[fai_module] pub fn invoke(_ctx: Context, inputs: Inputs) -> Result {