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
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,44 @@

## Unreleased

### Added

- **Workspace-level skill-builder LLM config (issue #92).** The `forge ui`
skill builder now reads its LLM configuration from
`<workspace>/.forge/ui.yaml` (or `~/.forge/ui.yaml` as a machine-wide
fallback) instead of borrowing credentials from whichever agent the
operator picked. The skill-builder LLM is decoupled from any agent's
runtime LLM, so the same configuration works across every agent in
the workspace and is usable before any agent has been scaffolded.
- New `GET` / `PUT` endpoints at `/api/settings/skill-builder` plus a
Settings modal in the skill-builder UI.
- New `GET /api/skill-builder/provider` (path-less) for first-run
detection in an empty workspace.
- Status banner surfaces the resolution source (`workspace` / `user` /
`agent_fallback` / `unset`) and a deprecation warning when the
agent-fallback compat shim resolves.
- The Settings modal accepts the API key value inline (password field)
and persists it to `<workspace>/.forge/.env` with mode 0600. An
auto-generated `<workspace>/.forge/.gitignore` protects the file
from accidental commits. The key value never appears in `ui.yaml`
and is never echoed back by the GET endpoint.
- See `docs/ui/skill-builder-llm.md` for the configuration reference.

### Changed

- **`SkillBuilderCodegenModel` no longer overrides the operator's model
(issue #92).** The function previously forced `gpt-4.1` for openai and
`claude-opus-4-6` for anthropic regardless of what the agent (or
workspace) had configured. The override is removed; the operator's
chosen model is used verbatim. This unblocks agents pointed at custom
OpenAI-compatible endpoints (OpenRouter, vLLM, litellm, self-hosted
Kimi/Llama) where the hardcoded "stronger" model isn't hosted.
- **Skill-builder handlers no longer call `os.Setenv` (issue #92).** The
pre-#92 handlers leaked the picked agent's `.env` into the `forge ui`
process's environment via `os.Setenv` calls, which caused cross-agent
credential stomping when switching agents in the UI. Credentials are
now threaded as request-scoped values.

### Fixed

- **`forge init` Custom provider now produces a runnable agent (issue #83).**
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img src="assets/banner.png" alt="Forge — open-source AI agent framework" width="100%">
</p>

# Forge — OpenClaw for Enterprise: A Secure, Portable AI Agent Runtime
# Forge — An Open, Secure, Portable AI Agent Runtime for the Enterprise

<p align="center">
<a href="https://useforge.ai/docs/"><img src="https://img.shields.io/badge/Docs-useforge.ai-FFD700?style=for-the-badge" alt="Documentation"></a>
Expand All @@ -11,8 +11,7 @@
<a href="https://initializ.ai"><img src="https://img.shields.io/badge/Built%20by-initializ-blueviolet?style=for-the-badge" alt="Built by initializ"></a>
</p>

Build, run, and deploy AI agents from a single `SKILL.md` file.
Secure by default. Runs anywhere — local, container, cloud, air-gapped.
Forge is the open-source runtime for Anthropic's Agent Skills standard — built for the agent that runs next to a service, in your environment, on infrastructure you already operate. Write a `SKILL.md`. Compile to a portable, hardened agent. Deploy it anywhere containers run: Kubernetes, on-prem, air-gapped, embedded in CI, or as an A2A endpoint.

## Why Forge?

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ forge init [name] [flags]
| `--name` | `-n` | | Agent name |
| `--framework` | `-f` | | Framework: `crewai`, `langchain`, or `custom` |
| `--language` | `-l` | | Language: `python`, `typescript`, or `go` |
| `--model-provider` | `-m` | | Model provider: `openai`, `anthropic`, `ollama`, or `custom` |
| `--model-provider` | `-m` | | Model provider: `openai`, `anthropic`, `gemini`, `ollama`, or `custom` (alias for an OpenAI-compatible endpoint — scaffolds `provider: openai` + `OPENAI_BASE_URL` / `OPENAI_API_KEY`) |
| `--channels` | | | Channel adapters (e.g., `slack,telegram`) |
| `--tools` | | | Builtin tools to enable (e.g., `web_search,http_request`) |
| `--skills` | | | Registry skills to include (e.g., `github,weather`) |
Expand Down
6 changes: 5 additions & 1 deletion docs/reference/forge-yaml-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ registry: "ghcr.io/org" # Container registry
entrypoint: "agent.py" # Required for crewai/langchain, omit for forge

model:
provider: "openai" # openai, anthropic, gemini, ollama, custom
provider: "openai" # openai, anthropic, gemini, ollama
name: "gpt-4o" # Model name
organization_id: "org-xxx" # OpenAI Organization ID (enterprise, optional)
fallbacks: # Fallback providers (optional)
- provider: "anthropic"
name: "claude-sonnet-4-20250514"
organization_id: "" # Per-fallback org ID override (optional)
# For OpenAI-compatible endpoints (OpenRouter, vLLM, litellm, self-hosted
# Kimi/Llama): use provider: "openai" + set OPENAI_BASE_URL in .env.
# The forge init wizard's "Custom" option normalizes to this shape — the
# generated forge.yaml never carries provider: "custom".

tools:
- name: "web_search"
Expand Down
21 changes: 18 additions & 3 deletions docs/reference/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,16 @@ An AI-powered conversational tool for creating custom skills. Access it via the

### How It Works

The Skill Builder uses the agent's own LLM provider to power a chat conversation that generates valid SKILL.md files and optional helper scripts. It automatically selects a stronger code-generation model when available (e.g. `gpt-4.1` for OpenAI, `claude-opus-4-6` for Anthropic). API key detection loads the agent's `.env` file and encrypted secrets (if unlocked) in addition to system environment variables.
The Skill Builder uses a **workspace-level LLM** that's independent of any specific agent's runtime LLM. The same configuration applies across every agent in the workspace and works before any agent has been scaffolded.

Configuration is persisted under `<workspace>/.forge/`:

- `ui.yaml` holds the non-secret fields (`provider`, `model`, optional `base_url`, optional `api_key_env`).
- `.env` holds the API key value (mode `0600`, auto-protected by a sibling `.gitignore`).

The status banner above the chat panel surfaces the resolution source — `workspace`, `user` (machine-wide fallback), `agent_fallback` (a deprecated compat shim used when no workspace/user config exists), or `unset`. When `unset`, the banner shows a **Configure** button that opens the settings modal.

See [Skill Builder LLM](../ui/skill-builder-llm.md) for the full configuration reference, three-tier resolution precedence, and trust-boundary rationale.

### Features

Expand Down Expand Up @@ -182,11 +191,14 @@ The validator enforces the [SKILL.md format](../core-concepts/skill-md-format.md

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/agents/{id}/skill-builder/provider` | Returns the agent's LLM provider, codegen model, and API key status |
| `GET` | `/api/skill-builder/provider` | Returns the resolved workspace-level LLM (provider, model, `source`, `has_key`, deprecation `warning`). Use this for first-run detection before any agent is picked. |
| `GET` | `/api/agents/{id}/skill-builder/provider` | Same shape as above, but consults the agent-fallback tier when no workspace/user config exists. |
| `GET` | `/api/agents/{id}/skill-builder/context` | Returns the system prompt used for skill generation |
| `POST` | `/api/agents/{id}/skill-builder/chat` | Streams an LLM conversation via SSE (accepts `messages` array) |
| `POST` | `/api/agents/{id}/skill-builder/chat` | Streams an LLM conversation via SSE (accepts `messages` array). Returns `400` when the workspace LLM is `unset` or has no resolvable API key. |
| `POST` | `/api/agents/{id}/skill-builder/validate` | Validates a SKILL.md and optional scripts |
| `POST` | `/api/agents/{id}/skill-builder/save` | Saves a validated skill to `skills/{name}/`, merges egress domains, writes env vars; returns `SkillSaveResult` with `path`, `egress_added`, `env_configured`, `env_missing` |
| `GET` | `/api/settings/skill-builder` | Returns the workspace-level config + metadata for the Settings modal (`source`, `has_key`, `providers` list). The API key value is never echoed back. |
| `PUT` | `/api/settings/skill-builder` | Persists `provider`, `model`, optional `base_url`, optional `api_key_env` to `<workspace>/.forge/ui.yaml`. When an optional `api_key` field is present in the body, writes it to `<workspace>/.forge/.env` (mode `0600`) under `api_key_env` (or the provider default). The key value never leaks to `ui.yaml`. Submit an empty / omitted `api_key` to leave the saved value untouched. |

## Architecture

Expand All @@ -199,6 +211,9 @@ forge-ui/
handlers.go Dashboard API (agents, start/stop, chat, sessions)
handlers_create.go Wizard API (create, config, skills, tools, OAuth)
handlers_skill_builder.go Skill Builder API (chat, validate, save, provider)
handlers_settings.go Workspace-level settings API (skill-builder LLM)
uiconfig/ Workspace ui.yaml + .env loader; SkillBuilderLLM
resolution + atomic .env writer with auto-.gitignore
skill_builder_context.go System prompt for the Skill Designer AI
skill_validator.go SKILL.md validation and artifact extraction
process.go Process manager (exec forge serve start/stop)
Expand Down
2 changes: 1 addition & 1 deletion docs/skills/skills-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Tooling can match on the substrings `(via policy)` or `(acknowledged by policy)`

## Skill Builder (Web UI)

The [Web Dashboard](../reference/web-dashboard.md#skill-builder) includes an AI-powered Skill Builder that generates valid SKILL.md files and helper scripts through a conversational interface. It uses the agent's own LLM provider and includes server-side validation before saving to the agent's `skills/` directory. On save, the builder automatically parses the skill's requirements and:
The [Web Dashboard](../reference/web-dashboard.md#skill-builder) includes an AI-powered Skill Builder that generates valid SKILL.md files and helper scripts through a conversational interface. It uses a [workspace-level LLM](../ui/skill-builder-llm.md) (independent of any specific agent's runtime LLM) and includes server-side validation before saving to the agent's `skills/` directory. On save, the builder automatically parses the skill's requirements and:

- **Merges egress domains** into `forge.yaml` `egress.allowed_domains` (deduplicated)
- **Writes user-provided env vars** to `.env` (skipping keys already present)
Expand Down
173 changes: 173 additions & 0 deletions docs/ui/skill-builder-llm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Skill Builder LLM (workspace-level)

The `forge ui` skill builder generates SKILL.md files via an LLM. That
LLM is configured at the **workspace level** — independent of any
specific agent's runtime LLM — so the same configuration works across
every agent in the workspace and is usable before any agent has been
scaffolded.

## Why workspace-level

Pre-#92 the skill builder borrowed credentials from whichever agent
the operator clicked into: it read the agent's `forge.yaml` and `.env`,
applied a hardcoded model upgrade (`gpt-4.1` for openai, `claude-opus-4-6`
for anthropic), and called the LLM from inside the `forge ui` process.
That model conflated two distinct concerns ("agent runtime LLM" vs.
"build-time codegen LLM"), broke against any agent pointed at a custom
OpenAI-compatible endpoint (the upgrade requested a model the endpoint
didn't host), caused cross-agent env-var stomping when switching agents,
and had no answer for empty workspaces.

The workspace-level model fixes all four:

- One LLM for the operator's skill-building work, used across every
agent in the workspace.
- Operator picks the model — no hardcoded upgrade.
- Credentials threaded as request-scoped data; the UI process's
environment is never mutated by handler calls.
- Works in an empty workspace before any agent is scaffolded.

## Configuration file

`<workspace>/.forge/ui.yaml`:

```yaml
skill_builder:
provider: openai # openai | anthropic | gemini | ollama
model: gpt-4.1 # operator-chosen; no hardcoded upgrade
base_url: https://... # optional, for OpenAI-compatible endpoints
api_key_env: OPENAI_API_KEY # which env var holds the key (default per provider)
```

- `provider` (required) — one of `openai`, `anthropic`, `gemini`, `ollama`.
- `model` (required) — operator picks. The skill builder uses this
verbatim; there is no `SkillBuilderCodegenModel` hardcoded upgrade.
- `base_url` (optional, openai only) — set this for OpenAI-compatible
endpoints (OpenRouter, vLLM, litellm, etc.).
- `api_key_env` (optional) — name of the environment variable the
`forge ui` process reads for the API key. Defaults per provider
(`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GEMINI_API_KEY`). Set this
if you keep the skill-builder credentials under a different name
(e.g. `WORKSPACE_LLM_KEY`) to avoid collisions with per-agent
runtime credentials.

The API key itself is **never** stored in `ui.yaml` — only the env var
name is. Set the env var in your shell before launching `forge ui`.

## Resolution precedence

The loader resolves the skill-builder LLM through three tiers, in order:

1. `<workspace>/.forge/ui.yaml` — primary, per-workspace.
2. `~/.forge/ui.yaml` — fallback, operator's machine-wide default.
3. The picked agent's `forge.yaml` + `.env` — **deprecated** fallback.
When this tier resolves, the UI banner shows a deprecation warning
prompting the operator to configure workspace settings. This
compatibility shim will be removed in a future release.

If none of the tiers resolves and the skill builder is invoked, the
chat handler returns a 400 with a message pointing to Settings.

## Setting it up

### Via the UI (recommended)

1. Open `forge ui --dir <your-workspace>` and click any agent's
Skill Builder.
2. If no workspace-level config exists, the banner reads
**"Workspace skill-builder LLM is not configured"** — click
**Configure**.
3. Fill the form (provider, model, optional base URL, optional API key
env override) and **paste your API key** in the password field.
4. Save. The key is written to `<workspace>/.forge/.env` (mode 0600)
under the env var name shown in the form. An auto-generated
`.forge/.gitignore` protects the file from being committed.

The key value is never sent back by the GET endpoint and never
appears in `ui.yaml` — only `<workspace>/.forge/.env` ever holds it.
To rotate, open Settings and paste a new value; submitting an empty
key leaves the saved value untouched.

### Via the file

```
mkdir -p <workspace>/.forge
cat > <workspace>/.forge/ui.yaml <<'YAML'
skill_builder:
provider: openai
model: gpt-4.1
YAML
echo 'OPENAI_API_KEY=sk-...' > <workspace>/.forge/.env
chmod 600 <workspace>/.forge/.env
echo '.env' > <workspace>/.forge/.gitignore
```

Then launch `forge ui --dir <workspace>`. The `forge ui` process
consults `<workspace>/.forge/.env` for any env var named in `ui.yaml`,
with the OS environment as a fallback.

### Via the API

```sh
# Persist config + key in one PUT (api_key is optional; omit to leave
# the saved key unchanged).
curl -X PUT http://localhost:4200/api/settings/skill-builder \
-H 'Content-Type: application/json' \
-d '{"provider":"openai","model":"gpt-4.1","api_key":"sk-..."}'

curl http://localhost:4200/api/settings/skill-builder
```

### Via the API

```sh
curl -X PUT http://localhost:4200/api/settings/skill-builder \
-H 'Content-Type: application/json' \
-d '{"provider":"openai","model":"gpt-4.1"}'

curl http://localhost:4200/api/settings/skill-builder
```

## Status banner semantics

The skill builder header shows the resolved configuration plus a hint
about where it came from:

| Banner says | What it means |
|---|---|
| `openai/gpt-4.1` (clean) | Workspace config resolved successfully; API key found. |
| `openai/gpt-4.1` + **using agent fallback (deprecated)** | No workspace/user config exists; the picked agent's `forge.yaml` is being used. Configure workspace settings to migrate. |
| `openai/gpt-4.1` + **API key not configured (env: OPENAI_API_KEY)** | Config resolved but the named env var is empty in the `forge ui` process. Set it and reload. |
| **Workspace skill-builder LLM is not configured** | First-run state. Click Configure. |

## Why split `ui.yaml` and `.env`?

Same trust-boundary reasoning as `forge init`'s `forge.yaml` / `.env`
split:

- `ui.yaml` is non-secret (provider name, model name, base URL, env
var name). Operators may want to check this into their workspace
repo so the team shares the same skill-builder configuration.
- `.env` is the secret material (API key value). It lives under
`.forge/` with mode 0600 and an auto-generated `.gitignore` that
protects it from being committed.

If you want to keep skill-builder credentials separate from per-agent
runtime credentials (recommended, especially when agents point at
OpenAI-compatible endpoints other than openai.com), set
`api_key_env: WORKSPACE_LLM_KEY` (or similar). The key value lands at
`<workspace>/.forge/.env` under that name; per-agent runtime
credentials in each `<agent>/.env` stay untouched.

## Decoupling rules the implementation enforces

These are pinned by regression tests:

- The skill builder LLM is independent of any agent's runtime LLM.
Agent A can ship with `provider: anthropic` and Claude while you
use GPT-4.1 to build skills.
- The skill builder **never** calls `os.Setenv` on the `forge ui`
process. Credentials are passed via request-scoped values.
- The previous `SkillBuilderCodegenModel` mapping (which forced
`gpt-4.1` / `claude-opus-4-6` regardless of the agent's configured
model) is removed. The operator's chosen model is used verbatim.
Loading
Loading