Compare commits

..

No commits in common. "main" and "v0.3.2" have entirely different histories.
main ... v0.3.2

7 changed files with 45 additions and 324 deletions

View file

@ -30,7 +30,7 @@ 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
# The fai-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.

View file

@ -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 `<wasm_url>.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}"

View file

@ -7,17 +7,17 @@
name = "orchestrator_llm"
version = "0.3.2"
edition = "2024"
authors = ["Dr. Stefan Flemming <chain@flemming.ai>"]
authors = ["Dr. Stefan Flemming <platform@flemming.ai>"]
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"

View file

@ -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.

Binary file not shown.

View file

@ -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"

View file

@ -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<Outputs, ModuleError> {