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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The format is intentionally lightweight and human-readable. Group entries by rel
- Added starter example files for OpenClaw, n8n, and CLI clients under `docs/examples/` so onboarding can begin from copy/pasteable templates
- Added starter provider snippets for cloud, local-worker, and image-provider setups under `docs/examples/`
- Added matching provider `.env` starter files for cloud, local-worker, and image-provider onboarding flows
- Added provider env placeholder checks to `foundrygate-doctor` so missing `.env` values are surfaced before rollout

## v0.7.0 - 2026-03-12

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ $EDITOR .env
```

The onboarding report now includes concrete OpenClaw, n8n, and CLI quickstart hints, a staged provider-rollout view, and a client matrix, so you can move from a generic health check to a real client and provider rollout path without leaving the terminal.
`foundrygate-doctor` now also flags provider env placeholders from `config.yaml` that are still missing in `.env`.

If you prefer the Linux service path instead of a manual Python run, jump to [Helper Scripts](#helper-scripts) and use `./scripts/foundrygate-install`.

Expand Down
2 changes: 2 additions & 0 deletions docs/ONBOARDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ $EDITOR .env
./scripts/foundrygate-onboarding-report
```

`foundrygate-doctor` now also checks whether provider env placeholders referenced in `config.yaml` are actually present in `.env`.

`foundrygate-onboarding-report` now includes concrete OpenClaw, n8n, and CLI quickstart hints plus a staged provider-rollout view. Use it after every provider or client change to keep the deployment understandable for the next operator as well.

It also prints a client matrix:
Expand Down
33 changes: 33 additions & 0 deletions foundrygate/onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path
from typing import Any

import yaml
from dotenv import dotenv_values, load_dotenv

from .config import load_config
Expand All @@ -23,6 +24,33 @@ def _is_unresolved_env(value: str) -> bool:
return stripped.startswith("${") and stripped.endswith("}")


def collect_provider_env_requirements(
*,
config_path: str | Path | None = None,
env_file: str | Path | None = None,
) -> dict[str, list[str]]:
"""Return which provider env variables are configured and which are missing."""
resolved_config = Path(config_path) if config_path else Path.cwd() / "config.yaml"
resolved_env = _env_path(env_file)
env_values = dotenv_values(resolved_env) if resolved_env.exists() else {}

with resolved_config.open(encoding="utf-8") as handle:
raw = yaml.safe_load(handle) or {}

required: set[str] = set()
for provider in (raw.get("providers") or {}).values():
if not isinstance(provider, dict):
continue
for field in ("api_key", "base_url"):
value = provider.get(field, "")
if isinstance(value, str) and _is_unresolved_env(value):
required.add(value.strip()[2:-1].split(":-", 1)[0])

present = sorted(name for name in required if env_values.get(name))
missing = sorted(name for name in required if not env_values.get(name))
return {"required": sorted(required), "present": present, "missing": missing}


def _provider_ready(provider: dict[str, Any]) -> tuple[bool, str]:
"""Return whether one provider looks ready for onboarding."""
contract = provider.get("contract", "generic")
Expand Down Expand Up @@ -254,6 +282,10 @@ def build_onboarding_report(
suggestions.append("Keep auto_update disabled until the provider and client set is stable.")
provider_rollout = _build_provider_rollout(providers, list(config.fallback_chain))
client_matrix = _build_client_matrix(client_profiles)
env_requirements = collect_provider_env_requirements(
config_path=config_path,
env_file=resolved_env,
)

enabled_presets = set(client_profiles.get("presets", []))
profile_names = set(client_profiles.get("profiles", {}).keys())
Expand Down Expand Up @@ -308,6 +340,7 @@ def build_onboarding_report(
"env": {
"exists": resolved_env.exists(),
"provider_keys_present": sorted(key for key, value in env_values.items() if value),
"provider_requirements": env_requirements,
},
"providers": {
"total": len(providers),
Expand Down
19 changes: 19 additions & 0 deletions scripts/foundrygate-doctor
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ env_file="${FOUNDRYGATE_ENV_FILE:-$repo_root/.env}"
config_file="${FOUNDRYGATE_CONFIG_FILE:-$repo_root/config.yaml}"
status=0

export FOUNDRYGATE_ENV_FILE="$env_file"
export FOUNDRYGATE_CONFIG_FILE="$config_file"

ok() {
printf '[ok] %s\n' "$1"
}
Expand Down Expand Up @@ -54,6 +57,22 @@ else
warn "no provider API key detected in $env_file"
fi

python3 - <<'PY'
import os

from foundrygate.onboarding import collect_provider_env_requirements

requirements = collect_provider_env_requirements(
config_path=os.environ.get("FOUNDRYGATE_CONFIG_FILE"),
env_file=os.environ.get("FOUNDRYGATE_ENV_FILE"),
)

for name in requirements["present"]:
print(f"[ok] provider env present: {name}")
for name in requirements["missing"]:
print(f"[warn] provider env missing: {name}")
PY

if command -v curl >/dev/null 2>&1; then
if curl -fsS -m 2 http://127.0.0.1:8090/health >/dev/null 2>&1; then
ok "local /health endpoint is reachable"
Expand Down
36 changes: 36 additions & 0 deletions tests/test_onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from foundrygate.onboarding import (
build_onboarding_report,
build_onboarding_validation,
collect_provider_env_requirements,
render_onboarding_report,
render_onboarding_validation,
)
Expand Down Expand Up @@ -53,6 +54,7 @@ def test_onboarding_report_marks_missing_api_keys_and_presets(tmp_path: Path):
assert report["providers"]["total"] == 1
assert report["providers"]["ready"] == 0
assert report["providers"]["missing_api_keys"] == ["deepseek-chat"]
assert report["env"]["provider_requirements"]["missing"] == ["DEEPSEEK_API_KEY"]
assert report["clients"]["presets"] == ["openclaw"]
assert report["integrations"]["openclaw"]["recommended"] is True
assert report["integrations"]["n8n"]["recommended"] is False
Expand Down Expand Up @@ -262,6 +264,40 @@ def test_onboarding_report_marks_all_builtin_integrations_ready(tmp_path: Path):
assert report["clients"]["matrix"][0]["has_rule"] is True


def test_collect_provider_env_requirements_tracks_present_and_missing(tmp_path: Path):
env_file = tmp_path / ".env"
env_file.write_text("DEEPSEEK_API_KEY=sk-demo\n", encoding="utf-8")

config_file = tmp_path / "config.yaml"
config_file.write_text(
"""
providers:
deepseek-chat:
backend: openai-compat
base_url: "https://api.deepseek.com/v1"
api_key: "${DEEPSEEK_API_KEY}"
model: "deepseek-chat"
image-worker:
contract: image-provider
backend: openai-compat
base_url: "${IMAGE_PROVIDER_BASE_URL}"
api_key: "${IMAGE_PROVIDER_API_KEY}"
model: "image-model"
""".strip(),
encoding="utf-8",
)

requirements = collect_provider_env_requirements(config_path=config_file, env_file=env_file)

assert requirements["required"] == [
"DEEPSEEK_API_KEY",
"IMAGE_PROVIDER_API_KEY",
"IMAGE_PROVIDER_BASE_URL",
]
assert requirements["present"] == ["DEEPSEEK_API_KEY"]
assert requirements["missing"] == ["IMAGE_PROVIDER_API_KEY", "IMAGE_PROVIDER_BASE_URL"]


def test_onboarding_report_includes_client_matrix_and_unmatched_profile_warning(
tmp_path: Path,
):
Expand Down
Loading