From a51e79a82d341b99ea1f3806b19faf7a5eec2da9 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 7 May 2026 10:10:19 -0700 Subject: [PATCH] Stop fetching docs at Cloudflare build time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cloudflare Pages builds run on shared IPs without GITHUB_TOKEN, hitting the 60/hr unauthenticated rate limit. The added sync:docs step made two extra api.github.com calls before fetch:skills and fetch:contributors, tipping the quota and causing both to silently fall back to defaults (only 4 skills shown on /hub, "—" star count in nav). Move the docs sync out of the build chain — the GitHub Actions workflow already runs it on docs/** changes. Un-gitignore the synced docs so the workflow's commits land in the repo and Cloudflare ships them as-is. --- .gitignore | 10 - package.json | 2 +- src/content/docs/core-concepts/channels.md | 234 ++++++++ src/content/docs/core-concepts/hooks.md | 149 +++++ .../docs/core-concepts/how-forge-works.md | 366 +++++++++++++ .../docs/core-concepts/memory-system.md | 129 +++++ .../docs/core-concepts/runtime-engine.md | 261 +++++++++ src/content/docs/core-concepts/scheduling.md | 60 ++ .../docs/core-concepts/skill-md-format.md | 168 ++++++ .../docs/core-concepts/tools-and-builtins.md | 255 +++++++++ src/content/docs/deployment/docker.md | 62 +++ src/content/docs/deployment/kubernetes.md | 39 ++ src/content/docs/deployment/monitoring.md | 62 +++ .../docs/deployment/production-checklist.md | 31 ++ src/content/docs/faq.md | 105 ++++ .../docs/getting-started/configuration.md | 45 ++ .../docs/getting-started/contributing.md | 166 ++++++ .../docs/getting-started/installation.md | 34 ++ .../docs/getting-started/quick-start.md | 54 ++ .../docs/getting-started/your-first-skill.md | 73 +++ .../reference/agent-skills-compatibility.md | 44 ++ src/content/docs/reference/cli-reference.md | 511 ++++++++++++++++++ .../docs/reference/command-integration.md | 201 +++++++ .../docs/reference/environment-variables.md | 35 ++ .../docs/reference/forge-yaml-schema.md | 94 ++++ .../docs/reference/framework-plugins.md | 148 +++++ src/content/docs/reference/web-dashboard.md | 192 +++++++ src/content/docs/security/audit-logging.md | 36 ++ src/content/docs/security/build-signing.md | 51 ++ src/content/docs/security/egress-control.md | 310 +++++++++++ src/content/docs/security/guardrails.md | 310 +++++++++++ src/content/docs/security/overview.md | 280 ++++++++++ .../docs/security/secret-management.md | 88 +++ src/content/docs/security/trust-model.md | 57 ++ .../docs/skills/contributing-a-skill.md | 70 +++ src/content/docs/skills/embedded-skills.md | 266 +++++++++ src/content/docs/skills/skills-cli.md | 26 + .../docs/skills/writing-custom-skills.md | 146 +++++ src/data/docs-manifest.json | 376 +++++++++++++ 39 files changed, 5535 insertions(+), 11 deletions(-) create mode 100644 src/content/docs/core-concepts/channels.md create mode 100644 src/content/docs/core-concepts/hooks.md create mode 100644 src/content/docs/core-concepts/how-forge-works.md create mode 100644 src/content/docs/core-concepts/memory-system.md create mode 100644 src/content/docs/core-concepts/runtime-engine.md create mode 100644 src/content/docs/core-concepts/scheduling.md create mode 100644 src/content/docs/core-concepts/skill-md-format.md create mode 100644 src/content/docs/core-concepts/tools-and-builtins.md create mode 100644 src/content/docs/deployment/docker.md create mode 100644 src/content/docs/deployment/kubernetes.md create mode 100644 src/content/docs/deployment/monitoring.md create mode 100644 src/content/docs/deployment/production-checklist.md create mode 100644 src/content/docs/faq.md create mode 100644 src/content/docs/getting-started/configuration.md create mode 100644 src/content/docs/getting-started/contributing.md create mode 100644 src/content/docs/getting-started/installation.md create mode 100644 src/content/docs/getting-started/quick-start.md create mode 100644 src/content/docs/getting-started/your-first-skill.md create mode 100644 src/content/docs/reference/agent-skills-compatibility.md create mode 100644 src/content/docs/reference/cli-reference.md create mode 100644 src/content/docs/reference/command-integration.md create mode 100644 src/content/docs/reference/environment-variables.md create mode 100644 src/content/docs/reference/forge-yaml-schema.md create mode 100644 src/content/docs/reference/framework-plugins.md create mode 100644 src/content/docs/reference/web-dashboard.md create mode 100644 src/content/docs/security/audit-logging.md create mode 100644 src/content/docs/security/build-signing.md create mode 100644 src/content/docs/security/egress-control.md create mode 100644 src/content/docs/security/guardrails.md create mode 100644 src/content/docs/security/overview.md create mode 100644 src/content/docs/security/secret-management.md create mode 100644 src/content/docs/security/trust-model.md create mode 100644 src/content/docs/skills/contributing-a-skill.md create mode 100644 src/content/docs/skills/embedded-skills.md create mode 100644 src/content/docs/skills/skills-cli.md create mode 100644 src/content/docs/skills/writing-custom-skills.md create mode 100644 src/data/docs-manifest.json diff --git a/.gitignore b/.gitignore index f521a6f..51eaf13 100644 --- a/.gitignore +++ b/.gitignore @@ -17,16 +17,6 @@ src/content/skills/*.md src/content/changelog/*.md !src/content/changelog/.gitkeep -# Synced docs (generated at build time from forge repo) -src/content/docs/getting-started/ -src/content/docs/core-concepts/ -src/content/docs/security/ -src/content/docs/skills/ -src/content/docs/deployment/ -src/content/docs/reference/ -src/content/docs/faq.md -src/data/docs-manifest.json - # wrangler files .wrangler .dev.vars* diff --git a/package.json b/package.json index 47b34e6..67489bf 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "generate:og": "npx tsx scripts/generate-og.ts", "register:links": "npx tsx scripts/register-links.ts", "build": "npm run fetch && npm run register:links && npm run generate:og && astro build", - "fetch": "npm run sync:docs && npm run fetch:skills && npm run fetch:contributors && npm run fetch:releases && npm run fetch:trust", + "fetch": "npm run fetch:skills && npm run fetch:contributors && npm run fetch:releases && npm run fetch:trust", "sync:docs": "npx tsx scripts/sync-docs.ts", "sync:docs:stub": "npx tsx scripts/sync-docs-stub.ts", "fetch:skills": "npx tsx scripts/fetch-skills.ts", diff --git a/src/content/docs/core-concepts/channels.md b/src/content/docs/core-concepts/channels.md new file mode 100644 index 0000000..1bd0359 --- /dev/null +++ b/src/content/docs/core-concepts/channels.md @@ -0,0 +1,234 @@ +--- +title: "Channels" +description: "Bridge messaging platforms like Slack and Telegram to your AI agent." +order: 4 +editUrl: "https://github.com/initializ/forge/edit/main/docs/core-concepts/channels.md" +--- + + + +Channel adapters bridge messaging platforms (Slack, Telegram) to your A2A-compliant agent. Each adapter normalizes platform-specific events into a common `ChannelEvent` format, forwards them to the agent's A2A server, and delivers responses back to the originating platform. + +``` + Slack/Telegram ──→ Channel Plugin ──→ Router ──→ A2A Server + ↑ │ + └──────────────── SendResponse ←────────────────────────┘ +``` + +Both channels use **outbound-only connections** — no public URLs, no ngrok, no inbound webhooks. Telegram webhook mode binds to `127.0.0.1` only with secret token verification. + +## Supported Channels + +| Channel | Adapter | Mode | Default Port | +|---------|---------|------|-------------| +| Slack | `slack.Plugin` | Socket Mode | 3000 | +| Telegram | `telegram.Plugin` | Polling or Webhook | 3001 | + +> **Note:** Slack uses Socket Mode — an outbound WebSocket connection from the agent to Slack's servers. No public URL or ngrok is needed for local development. + +## Adding a Channel + +```bash +# Add Slack adapter to your project +forge channel add slack + +# Add Telegram adapter +forge channel add telegram +``` + +This command: +1. Generates `{adapter}-config.yaml` with placeholder settings +2. Updates `.env` with required environment variables +3. Adds the channel to `forge.yaml`'s `channels` list +4. Prints setup instructions + +## Running with Channels + +### Alongside the Agent + +```bash +# Start agent with Slack and Telegram adapters +forge run --with slack,telegram +``` + +This starts the A2A dev server and all specified channel adapters in the same process. + +### Standalone Mode + +```bash +# Run adapter separately (requires AGENT_URL) +export AGENT_URL=http://localhost:8080 +forge channel serve slack +``` + +Standalone mode is useful for running adapters as separate services in production. Each adapter connects to the agent's A2A server via HTTP. + +## Slack App Setup + +Before running the Slack adapter, create and configure a Slack App: + +1. **Create a Slack App** at https://api.slack.com/apps -> "Create New App" -> "From scratch" +2. **Enable Socket Mode** — Settings -> Socket Mode -> toggle **On** +3. **Generate an App-Level Token** — Basic Information -> "App-Level Tokens" -> "Generate Token and Scopes" -> add the `connections:write` scope -> copy the `xapp-...` token +4. **Enable Event Subscriptions** — Features -> Event Subscriptions -> toggle **On** -> Subscribe to bot events: + - `message.channels` — messages in public channels + - `message.im` — direct messages + - `app_mention` — @mentions of your bot +5. **Set Bot Token Scopes** — Features -> OAuth & Permissions -> Bot Token Scopes -> add: + - `app_mentions:read` + - `chat:write` + - `channels:history` + - `im:history` + - `files:write` (for large response file uploads) + - `reactions:write` (for processing indicators) +6. **Install the App** — Settings -> Install App -> "Install to Workspace" -> copy the `xoxb-...` Bot Token +7. **Add tokens to `.env`**: + ``` + SLACK_APP_TOKEN=xapp-1-... + SLACK_BOT_TOKEN=xoxb-... + ``` +8. **Invite the bot** to any channel where you want it active: `/invite @YourBot` + +### Mention-Aware Filtering + +The Slack adapter resolves the bot's own user ID at startup via `auth.test` and uses it for intelligent message filtering: + +- **Channel messages** — the bot only responds when explicitly @mentioned (e.g. `@ForgeBot what's the status?`) +- **Thread replies** — the bot responds to all messages in a thread it's participating in, unless the message @mentions a different user +- **Direct messages** — all DMs are processed +- Bot mentions are stripped from the message text before passing to the LLM, so it sees clean input + +### Processing Indicators + +When the Slack adapter receives a message: + +1. An :eyes: reaction is added immediately to acknowledge receipt +2. If the handler takes longer than 15 seconds, an interim message is posted: _"Researching, I'll post the result shortly..."_ +3. The :eyes: reaction is removed when the response is ready + +This gives users visual feedback that their message is being processed, especially for long-running research queries. + +### Telegram Processing Indicators + +The Telegram adapter mirrors Slack's processing feedback: + +1. A typing indicator ("typing...") is sent immediately and refreshed every 4 seconds +2. If the handler takes longer than 15 seconds, an interim message is posted: _"Working on it — I'll send the result when ready."_ +3. The typing indicator stops when the response is ready + +**Context isolation:** Each handler goroutine runs with an independent context (10-minute timeout), detached from the polling loop. This prevents in-flight tasks from being cancelled if the polling context is interrupted during server restarts or errors. + +## Configuration + +### Slack (`slack-config.yaml`) + +```yaml +adapter: slack +settings: + app_token_env: SLACK_APP_TOKEN + bot_token_env: SLACK_BOT_TOKEN +``` + +Environment variables: +- `SLACK_APP_TOKEN` — Socket Mode app-level token (`xapp-...`) +- `SLACK_BOT_TOKEN` — Bot user OAuth token (`xoxb-...`) + +### Telegram (`telegram-config.yaml`) + +```yaml +adapter: telegram +webhook_port: 3001 +webhook_path: /telegram/webhook +settings: + bot_token: TELEGRAM_BOT_TOKEN + mode: polling +``` + +Environment variables: +- `TELEGRAM_BOT_TOKEN` — Bot token from @BotFather + +Mode options: +- `polling` (default) — Long-polling via `getUpdates` +- `webhook` — Receives updates via HTTP webhook (loopback-only binding with secret token verification) + +### Telegram Webhook Security + +When running in webhook mode, the Telegram adapter applies multiple security controls: + +| Control | Detail | +|---------|--------| +| **Loopback binding** | Webhook server binds to `127.0.0.1:` instead of `0.0.0.0`, preventing direct internet exposure | +| **Secret token verification** | A 32-byte random secret is generated at startup and registered with Telegram's `setWebhook` API. Incoming requests must include the matching `X-Telegram-Bot-Api-Secret-Token` header; mismatches return 401 | +| **Content-Type enforcement** | Only `application/json` requests are accepted; others return 415 | +| **Request body limit** | Bodies are limited to 1 MiB via `http.MaxBytesReader`; oversized payloads return 413 | + +### Slack Event Deduplication + +The Slack adapter deduplicates events by envelope ID to prevent processing the same message multiple times (common during reconnections or network retries): + +- Each envelope ID is recorded in an in-memory cache on first receipt +- Subsequent envelopes with the same ID are silently skipped after acknowledgment +- Cache entries older than 5 minutes are evicted automatically every 60 seconds +- Empty envelope IDs are never considered duplicates + +## Large Response Handling + +When an agent response exceeds 4096 characters (common with research reports), channel adapters automatically split it into a **summary message** and a **file attachment**: + +1. A brief summary (first paragraph, up to 600 characters) is sent as a regular message +2. The full report is uploaded as a downloadable Markdown file (`research-report.md`) + +This works on both Slack (via `files.getUploadURLExternal`) and Telegram (via `sendDocument`). If file upload fails, adapters fall back to chunked messages. Markdown is converted to platform-native formatting (Slack mrkdwn or Telegram HTML). + +Additionally, the runtime tracks large tool outputs (>8000 characters) and attaches them as file parts in the A2A response. This ensures channel adapters receive the complete, untruncated tool output even when the LLM's text summary is truncated by output token limits. JSON tool outputs (e.g. Tavily Research/Search results) are automatically unwrapped into readable markdown before delivery. + +## Container Deployment + +When channels are configured in `forge.yaml`, the build pipeline automatically: + +1. **Includes channel config files** — `slack-config.yaml`, `telegram-config.yaml`, etc. are copied into the Docker build context alongside `forge.yaml` +2. **Adds `--with` to the entrypoint** — The container entrypoint becomes `["forge", "run", "--host", "0.0.0.0", "--with", "slack,telegram"]` +3. **Handles auth loopback** — When [external auth](/docs/core-concepts/runtime#external-authentication) is configured, channel adapters authenticate to the A2A server using an internal token, bypassing the external auth provider + +Pass channel secrets via environment variables: + +```bash +docker run \ + -e SLACK_APP_TOKEN=xapp-... \ + -e SLACK_BOT_TOKEN=xoxb-... \ + -e FORGE_AUTH_URL=https://auth.example.com/verify \ + my-agent +``` + +## Docker Compose Integration + +```bash +# Package agent with channel adapter sidecars +forge package --with-channels +``` + +This generates a `docker-compose.yaml` with: +- An `agent` service running the A2A server +- Adapter services (e.g., `slack-adapter`, `telegram-adapter`) connecting to the agent + +## Writing a Custom Channel Adapter + +Implement the `channels.ChannelPlugin` interface: + +```go +type ChannelPlugin interface { + Name() string + Init(cfg ChannelConfig) error + Start(ctx context.Context, handler EventHandler) error + Stop() error + NormalizeEvent(raw []byte) (*ChannelEvent, error) + SendResponse(event *ChannelEvent, response *a2a.Message) error +} +``` + +### Steps + +1. Create a new package under `forge-plugins/channels/yourplatform/`. +2. Implement `ChannelPlugin`. +3. Register the plugin in the channel registry. +4. Add config generation in `generateChannelConfig()` and env vars in `generateEnvVars()`. diff --git a/src/content/docs/core-concepts/hooks.md b/src/content/docs/core-concepts/hooks.md new file mode 100644 index 0000000..2291a9f --- /dev/null +++ b/src/content/docs/core-concepts/hooks.md @@ -0,0 +1,149 @@ +--- +title: "Hooks" +description: "Hook into the agent loop for logging, enforcement, and auditing." +order: 7 +editUrl: "https://github.com/initializ/forge/edit/main/docs/core-concepts/hooks.md" +--- + + + +The hook system allows custom logic to run at key points in the LLM agent loop. Hooks can observe, modify context, or block execution. + +## Overview + +Hooks fire synchronously during the agent loop and can: + +- **Log** interactions for debugging or auditing +- **Block** execution by returning an error +- **Inspect** messages, responses, and tool activity + +## Hook Points + +| Hook Point | When It Fires | Available Data | +|-----------|---------------|------------------| +| `BeforeLLMCall` | Before each LLM API call | `Messages`, `TaskID`, `CorrelationID` | +| `AfterLLMCall` | After each LLM API call | `Messages`, `Response`, `TaskID`, `CorrelationID` | +| `BeforeToolExec` | Before each tool execution | `ToolName`, `ToolInput`, `TaskID`, `CorrelationID` | +| `AfterToolExec` | After each tool execution | `ToolName`, `ToolInput`, `ToolOutput` (mutable), `Error`, `TaskID`, `CorrelationID` | +| `OnError` | When an LLM call fails | `Error`, `TaskID`, `CorrelationID` | +| `OnProgress` | During tool execution | `Phase`, `ToolName`, `StatusMessage` | + +## HookContext + +The `HookContext` struct carries data available at each hook point: + +```go +type HookContext struct { + Messages []llm.ChatMessage // Current conversation messages + Response *llm.ChatResponse // LLM response (AfterLLMCall only) + ToolName string // Tool being executed + ToolInput string // Tool input arguments (JSON) + ToolOutput string // Tool result (AfterToolExec only) + Error error // Error that occurred +} +``` + +## Writing Hooks + +Hooks implement the `Hook` function signature: + +```go +type Hook func(ctx context.Context, hctx *HookContext) error +``` + +### Logging Hook Example + +```go +hooks := engine.NewHookRegistry() + +hooks.Register(engine.BeforeLLMCall, func(ctx context.Context, hctx *engine.HookContext) error { + log.Printf("LLM call with %d messages", len(hctx.Messages)) + return nil +}) + +hooks.Register(engine.AfterToolExec, func(ctx context.Context, hctx *engine.HookContext) error { + log.Printf("Tool %s returned: %s", hctx.ToolName, hctx.ToolOutput) + return nil +}) +``` + +### Enforcement Hook Example + +```go +hooks.Register(engine.BeforeToolExec, func(ctx context.Context, hctx *engine.HookContext) error { + if hctx.ToolName == "dangerous_tool" { + return fmt.Errorf("tool %q is blocked by policy", hctx.ToolName) + } + return nil +}) +``` + +## Output Redaction + +`AfterToolExec` hooks can modify `hctx.ToolOutput` to redact sensitive content before it enters the LLM context. The agent loop reads back `ToolOutput` from the `HookContext` after all hooks fire. + +The runner registers a guardrail hook that scans tool output for secrets and PII patterns. The hook passes `hctx.ToolName` to the guardrail engine, enabling per-tool exemptions via `allow_tools` config. See [Tool Output Scanning](/docs/core-concepts/security/guardrails#tool-output-scanning) for details. + +```go +hooks.Register(engine.AfterToolExec, func(ctx context.Context, hctx *engine.HookContext) error { + hctx.ToolOutput = strings.ReplaceAll(hctx.ToolOutput, secret, "[REDACTED]") + return nil +}) +``` + +## Skill Guardrail Hooks + +When skills declare guardrails in their `SKILL.md` frontmatter, the runner registers four hooks that enforce skill-specific security policies across the entire agent loop: + +| Hook Point | Guardrail Type | Behavior | +|------------|---------------|----------| +| `BeforeLLMCall` | `deny_prompts` | Blocks user messages that probe agent capabilities (e.g., "what tools can you run") | +| `AfterLLMCall` | `deny_responses` | Replaces LLM responses that enumerate internal binary names | +| `BeforeToolExec` | `deny_commands` | Blocks `cli_execute` commands matching deny patterns (e.g., `kubectl get secrets`) | +| `AfterToolExec` | `deny_output` | Blocks or redacts `cli_execute` output matching deny patterns (e.g., Secret manifests) | + +These hooks complement the global guardrail hooks (secrets/PII scanning) and fire in addition to them. Skill guardrails are loaded from build artifacts or parsed at runtime from `SKILL.md` — no `forge build` step is required. + +For pattern syntax and configuration, see [Skill Guardrails](/docs/core-concepts/security/guardrails#skill-guardrails). + +## Audit Logging + +The runner registers `AfterLLMCall` hooks that emit structured audit events for each LLM interaction. Audit fields include: + +| Field | Description | +|-------|-------------| +| `provider` | LLM provider name | +| `model` | Model identifier | +| `input_tokens` | Prompt token count | +| `output_tokens` | Completion token count | +| `organization_id` | OpenAI Organization ID (when set) | + +These events are logged via `slog` at Info level and can be consumed by external log aggregators for cost tracking and compliance. + +## Progress Tracking + +The runner automatically registers progress hooks that emit real-time status updates during tool execution. Progress events include the tool name, phase (`tool_start` / `tool_end`), and a human-readable status message. These events are streamed to clients via SSE when using the A2A HTTP server, enabling live progress indicators in web and chat UIs. + +## Error Handling + +- Hooks fire **in registration order** for each hook point +- If a hook returns an **error**, execution stops immediately +- The error propagates up to the `Execute` caller +- For `BeforeToolExec`, returning an error prevents the tool from running +- For `OnError`, the error from the LLM call is available in `hctx.Error` + +## Registration + +```go +hooks := engine.NewHookRegistry() +hooks.Register(engine.BeforeLLMCall, myHook) +hooks.Register(engine.AfterToolExec, myOtherHook) + +exec := engine.NewLLMExecutor(engine.LLMExecutorConfig{ + Client: client, + Tools: tools, + Hooks: hooks, +}) +``` + +If no `HookRegistry` is provided, an empty one is created automatically. diff --git a/src/content/docs/core-concepts/how-forge-works.md b/src/content/docs/core-concepts/how-forge-works.md new file mode 100644 index 0000000..bd3ba89 --- /dev/null +++ b/src/content/docs/core-concepts/how-forge-works.md @@ -0,0 +1,366 @@ +--- +title: "How Forge Works" +description: "Understand Forge's architecture, module system, and data flows." +order: 1 +editUrl: "https://github.com/initializ/forge/edit/main/docs/core-concepts/how-forge-works.md" +--- + + + +Forge is a portable runtime for building and running secure AI agents from simple skill definitions. + +## At a Glance + +``` +SKILL.md --> Parse --> Discover tools/requirements --> Compile AgentSpec + | + v + Apply security policy + | + v + Run LLM agent loop + (tool calling + memory + cron) +``` + +1. You write a `SKILL.md` that describes what the agent can do +2. Forge parses the skill definitions and optional YAML frontmatter (binary deps, env vars) +3. The build pipeline discovers tools, resolves egress domains, and compiles an `AgentSpec` +4. Security policies (egress allowlists, capability bundles) are applied +5. Build artifacts are checksummed and optionally signed (Ed25519) +6. At runtime, encrypted secrets are decrypted and the LLM-powered tool-calling loop executes with session persistence, memory, and a cron scheduler for recurring tasks + +## Module Architecture + +Forge is organized as a Go workspace with five modules: + +``` +go.work +├── forge-core/ Embeddable library +├── forge-cli/ CLI frontend +├── forge-plugins/ Channel plugin implementations +├── forge-ui/ Local web dashboard +└── forge-skills/ Skill system (registry, parser, compiler) +``` + +### forge-core — Library + +Pure Go library with no CLI dependencies. Provides the compiler, validator, runtime engine, LLM providers, tool/plugin/channel interfaces, A2A protocol types, and security subsystem. External consumers access the library through the `forgecore` package. + +### forge-cli — CLI Frontend + +Command-line application built on top of forge-core. Includes Cobra commands, build pipeline stages, container builders, framework plugins (CrewAI, LangChain, custom), A2A dev server, and init templates. + +### forge-plugins — Channel Plugins + +Messaging platform integrations that implement the `channels.ChannelPlugin` interface from forge-core. Ships Slack, Telegram, and markdown formatting plugins. + +### forge-ui — Web Dashboard + +Local web dashboard for managing agents from the browser. Single Go module embedded into the `forge` binary. See [Dashboard](/docs/core-concepts/dashboard) for details. + +### forge-skills — Skill System + +Skill system including the embedded and local skill registries, SKILL.md parser, skill compiler, requirement aggregation, security analyzer, binary/env resolver, and skill signing/verification. + +## Package Map + +### forge-core + +| Package | Responsibility | Key Types | +|---------|---------------|-----------| +| `forgecore` | Public API entry point | `Compile`, `ValidateConfig`, `ValidateAgentSpec`, `NewRuntime` | +| `a2a` | A2A protocol types | `Task`, `Message`, `TaskStatus`, `Part` | +| `agentspec` | AgentSpec definitions and schema validation | `AgentSpec` | +| `channels` | Channel adapter plugin interface | `ChannelPlugin`, `ChannelConfig`, `ChannelEvent`, `EventHandler` | +| `compiler` | AgentSpec compilation and plugin config merging | `CompileRequest`, `CompileResult` | +| `export` | Agent export functionality | — | +| `llm` | LLM client interface and message types | `Client`, `ChatRequest`, `ChatResponse`, `StreamDelta` | +| `llm/providers` | LLM provider implementations | OpenAI, Anthropic, Ollama | +| `pipeline` | Build pipeline context and orchestration | `Pipeline`, `Stage`, `BuildContext` | +| `plugins` | Plugin and framework plugin interfaces | `Plugin`, `FrameworkPlugin`, `AgentConfig`, `FrameworkRegistry` | +| `registry` | Embedded skill registry | — | +| `runtime` | LLM agent loop, executor, hooks, memory, guardrail interface | `AgentExecutor`, `LLMExecutor`, `ToolExecutor`, `GuardrailChecker` | +| `schemas` | Embedded JSON schemas | `agentspec.v1.0.schema.json` | +| `security` | Egress allowlist, security policies, network policies | `EgressConfig`, `Resolve`, `GenerateAllowlistJSON` | +| `skills` | Skill parsing, compilation, requirements resolution | `CompiledSkills`, `Compile`, `WriteArtifacts` | +| `tools` | Tool plugin system and executor | `Tool`, `Registry`, `CommandExecutor` | +| `tools/adapters` | Tool adapters | Webhook, MCP, OpenAPI | +| `tools/builtins` | Built-in tools | `http_request`, `json_parse`, `csv_parse`, `datetime_now`, `uuid_generate`, `math_calculate`, `web_search` | +| `types` | ForgeConfig type definitions | `ForgeConfig`, `ModelRef`, `ToolRef` | +| `util` | Utility functions | Slug generation | +| `validate` | Config and schema validation | `ValidationResult`, `ValidateForgeConfig`, `ImportSimResult` | + +### forge-cli + +| Package | Responsibility | Key Types | +|---------|---------------|-----------| +| `cmd/forge` | Main entry point | — | +| `cmd` | CLI command implementations | `init`, `build`, `run`, `validate`, `package`, `export`, `tool`, `channel`, `skills`, `serve`, `schedule`, `secret`, `key`, `ui` | +| `config` | ForgeConfig loading and YAML parsing | — | +| `build` | Build pipeline stage implementations | `FrameworkAdapterStage`, `AgentSpecStage`, `ToolsStage`, `SkillsStage`, `EgressStage`, etc. | +| `container` | Container image builders | `DockerBuilder`, `PodmanBuilder`, `BuildahBuilder` | +| `plugins` | Framework plugin registry | — | +| `plugins/crewai` | CrewAI framework adapter | — | +| `plugins/langchain` | LangChain framework adapter | — | +| `plugins/custom` | Custom framework plugin | — | +| `runtime` | CLI-specific runtime (subprocess, guardrail engine, watchers, stubs, mocks) | `LibraryGuardrailEngine` | +| `server` | A2A HTTP server implementation | — | +| `channels` | Channel configuration and routing | — | +| `skills` | Skill file loading and writing | — | +| `tools` | Tool discovery and execution | — | +| `tools/devtools` | Dev-only tools | `local_shell`, `local_file_browser` | +| `templates` | Embedded templates for init wizard | — | + +### forge-plugins + +| Package | Responsibility | +|---------|---------------| +| `channels` | Channel plugin package root | +| `channels/slack` | Slack channel adapter (Socket Mode) | +| `channels/telegram` | Telegram channel adapter (polling) | +| `channels/markdown` | Markdown formatting helper | + +### forge-skills + +| Package | Responsibility | +|---------|---------------| +| `contract` | Skill types, registry interface, filtering | +| `local` | Embedded + local skill registries | +| `parser` | SKILL.md parser (frontmatter + body extraction) | +| `compiler` | Skill compiler (prompt generation) | +| `requirements` | Requirement aggregation and derivation | +| `analyzer` | Security audit for skills | +| `resolver` | Binary and env var resolution | +| `trust` | Skill signing and verification | + +## Key Interfaces + +### `forgecore` Public API + +The `forgecore` package exposes the top-level library surface: + +```go +func Compile(req CompileRequest) (*CompileResult, error) +func ValidateConfig(cfg *types.ForgeConfig) *validate.ValidationResult +func ValidateAgentSpec(jsonData []byte) ([]string, error) +func ValidateCommandCompat(spec *agentspec.AgentSpec) *validate.ValidationResult +func SimulateImport(spec *agentspec.AgentSpec) *validate.ImportSimResult +func NewRuntime(cfg RuntimeConfig) *runtime.LLMExecutor +``` + +### `runtime.AgentExecutor` + +Core execution interface for running agents. Implemented by `LLMExecutor` in forge-core. + +```go +type AgentExecutor interface { + Execute(ctx context.Context, task *a2a.Task, msg *a2a.Message) (*a2a.Message, error) + ExecuteStream(ctx context.Context, task *a2a.Task, msg *a2a.Message) (<-chan *a2a.Message, error) + Close() error +} +``` + +### `llm.Client` + +Provider-agnostic LLM client. Implementations: OpenAI, Anthropic, Ollama (in `llm/providers`). + +```go +type Client interface { + Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) + ChatStream(ctx context.Context, req *ChatRequest) (<-chan StreamDelta, error) + ModelID() string +} +``` + +### `tools.Tool` + +Agent tool with name, schema, and execution. Categories: builtin, adapter, dev, custom. + +```go +type Tool interface { + Name() string + Description() string + Category() Category + InputSchema() json.RawMessage + Execute(ctx context.Context, args json.RawMessage) (string, error) +} +``` + +### `runtime.ToolExecutor` + +Bridge between the LLM agent loop and the tool registry. + +```go +type ToolExecutor interface { + Execute(ctx context.Context, name string, arguments json.RawMessage) (string, error) + ToolDefinitions() []llm.ToolDefinition +} +``` + +### `channels.ChannelPlugin` + +Channel adapter for messaging platforms. Implementations: Slack, Telegram (in `forge-plugins/channels`). + +```go +type ChannelPlugin interface { + Name() string + Init(cfg ChannelConfig) error + Start(ctx context.Context, handler EventHandler) error + Stop() error + NormalizeEvent(raw []byte) (*ChannelEvent, error) + SendResponse(event *ChannelEvent, response *a2a.Message) error +} +``` + +### `pipeline.Stage` + +Single unit of work in the build pipeline. Receives a `BuildContext` carrying all state. + +```go +type Stage interface { + Name() string + Execute(ctx context.Context, bc *BuildContext) error +} +``` + +### `plugins.FrameworkPlugin` + +Framework adapter for the build pipeline. Implementations: CrewAI, LangChain, custom (in `forge-cli/plugins`). + +```go +type FrameworkPlugin interface { + Name() string + DetectProject(dir string) (bool, error) + ExtractAgentConfig(dir string) (*AgentConfig, error) + GenerateWrapper(config *AgentConfig) ([]byte, error) + RuntimeDependencies() []string +} +``` + +### `container.Builder` + +Container image builder. Implementations: `DockerBuilder`, `PodmanBuilder`, `BuildahBuilder` (in `forge-cli/container`). + +## Data Flows + +### Compilation Flow + +``` +forge.yaml + → config.Load() [forge-cli/config] + → types.ForgeConfig [forge-core/types] + → validate.ValidateForgeConfig() [forge-core/validate] + → skills.Compile() [forge-core/skills] + → compiler.Compile() [forge-core/compiler] + → agentspec.AgentSpec + SecurityConfig [forge-core/agentspec, forge-core/security] +``` + +Or via the public API: + +``` +forgecore.Compile(CompileRequest) → CompileResult +``` + +### Build Pipeline Flow + +The build pipeline executes stages sequentially. Each stage lives in `forge-cli/build/` and implements `pipeline.Stage` from forge-core. + +| # | Stage | Produces | +|---|-------|----------| +| 1 | **FrameworkAdapterStage** | Detects framework (crewai/langchain/custom), extracts agent config, generates A2A wrapper | +| 2 | **AgentSpecStage** | `agent.json` — canonical AgentSpec from ForgeConfig | +| 3 | **ToolsStage** | Tool schema files from discovered and configured tools | +| 4 | **PolicyStage** | `policy-scaffold.json` — guardrail configuration | +| 5 | **DockerfileStage** | `Dockerfile` — container image definition | +| 6 | **K8sStage** | `deployment.yaml`, `service.yaml`, `network-policy.yaml` | +| 7 | **ValidateStage** | Validates all generated artifacts against schemas | +| 8 | **ManifestStage** | `build-manifest.json` — build metadata and file inventory | +| — | **SkillsStage** | `compiled/skills/skills.json` + `compiled/prompt.txt` — compiled skills | +| — | **EgressStage** | `compiled/egress_allowlist.json` — egress domain allowlist | +| — | **ToolFilterStage** | Annotated + filtered tool list (dev tools removed in prod) | + +### Runtime Flow + +``` +AgentSpec + Tools + → forgecore.NewRuntime(RuntimeConfig) [forge-core/forgecore] + → runtime.LLMExecutor [forge-core/runtime] + → llm.Client (provider selection) [forge-core/llm/providers] + → Agent loop: prompt → LLM → tool calls → results → LLM → response + → a2a.Message [forge-core/a2a] +``` + +The CLI orchestrates the full runtime stack: + +``` +forge run + → config.Load() [forge-cli/config] + → tools.Discover() + tools.Registry [forge-cli/tools, forge-core/tools] + → runtime.LLMExecutor [forge-core/runtime] + → server.A2AServer [forge-cli/server] + → channels.Router (optional) [forge-cli/channels] +``` + +## Module Directory Tree + +``` +forge/ + forge-core/ Core library + a2a/ A2A protocol types + llm/ LLM client, fallback chains, OAuth + memory/ Long-term memory (vector + keyword search) + runtime/ Agent loop, hooks, compactor, audit logger + scheduler/ Cron scheduler (parser, tick loop, overlap prevention) + secrets/ Encrypted secret storage (AES-256-GCM + Argon2id) + security/ Egress resolver, enforcer, proxy, K8s NetworkPolicy + tools/ Tool registry, builtins, adapters, skill_tool + types/ Config types + forge-cli/ CLI application + cmd/ CLI commands (init, build, run, serve, schedule, etc.) + runtime/ Runner, skill registration, scheduler store, subprocess executor + internal/tui/ Interactive init wizard (Bubbletea) + tools/ CLI-specific tools (cli_execute, skill executor) + forge-plugins/ Channel plugins + telegram/ Telegram adapter (polling, document upload) + slack/ Slack adapter (Socket Mode, file upload) + markdown/ Markdown converter, message splitting + forge-ui/ Local web dashboard + server.go HTTP server, routing, CORS + handlers*.go REST API (agents, config, wizard, skills) + process.go Agent process manager + discovery.go Workspace scanner + sse.go Real-time event broker + chat.go A2A streaming chat proxy + static/dist/ Embedded SPA (Preact + HTM + Monaco) + forge-skills/ Skill system + contract/ Skill types, registry interface, filtering + local/ Embedded + local skill registries + parser/ SKILL.md parser (frontmatter + body extraction) + compiler/ Skill compiler (prompt generation) + requirements/ Requirement aggregation and derivation + analyzer/ Security audit for skills + resolver/ Binary and env var resolution + trust/ Skill signing and verification +``` + +## Schema Validation + +AgentSpec JSON is validated against `schemas/agentspec.v1.0.schema.json` (JSON Schema draft-07) using the `gojsonschema` library. The schema is embedded in the binary via `go:embed` in `forge-core/schemas/`. + +## Egress Security + +Egress controls operate at both build time and runtime. Build-time controls generate allowlist artifacts and Kubernetes NetworkPolicy manifests. Runtime controls include: + +- **IP Validation** — Rejects non-standard IP formats (octal, hex, packed decimal) and IPv6 transition addresses embedding private IPs +- **SafeDialer** — Validates resolved IPs post-DNS against blocked CIDR ranges before connecting (prevents DNS rebinding) +- **EgressEnforcer** — In-process `http.RoundTripper` backed by `SafeTransport` for domain allowlist enforcement +- **EgressProxy** — Local HTTP/HTTPS forward proxy for subprocess traffic, also backed by `SafeDialer` +- **Redirect credential stripping** — `http_request` and `webhook_call` strip `Authorization`/`Cookie` headers on cross-origin redirects + +The A2A server adds: +- **CORS restriction** — Origin allowlist (localhost by default), configurable via flag/env/YAML +- **Security headers** — `X-Content-Type-Options`, `Referrer-Policy`, `X-Frame-Options`, `Content-Security-Policy` +- **Rate limiting** — Per-IP token bucket middleware (read: 60 req/min burst 10, write: 10 req/min burst 3) with 429 responses and `Retry-After` headers; stale visitors evicted automatically +- **Request size limits** — `MaxHeaderBytes` (1 MiB) and `http.MaxBytesReader` (2 MiB) on request bodies; returns 413 on excess + +See [Egress Security](/docs/core-concepts/security/egress) for details. diff --git a/src/content/docs/core-concepts/memory-system.md b/src/content/docs/core-concepts/memory-system.md new file mode 100644 index 0000000..c36be65 --- /dev/null +++ b/src/content/docs/core-concepts/memory-system.md @@ -0,0 +1,129 @@ +--- +title: "Memory System" +description: "Session persistence, context management, and long-term memory." +order: 5 +editUrl: "https://github.com/initializ/forge/edit/main/docs/core-concepts/memory-system.md" +--- + + + +Forge provides two layers of memory management: session persistence for multi-turn conversations and long-term memory for cross-session knowledge. + +## Session Persistence + +Sessions are automatically persisted to disk across requests, enabling multi-turn conversations: + +```yaml +memory: + persistence: true # default: true + sessions_dir: ".forge/sessions" +``` + +- Sessions are saved as JSON files with atomic writes (temp file + fsync + rename) +- Orphaned tool calls (assistant tool_calls without matching tool results) are stripped on both save and recovery, preventing API rejection errors +- Automatic cleanup of sessions older than 7 days at startup +- Session recovery on subsequent requests (disk snapshot supersedes task history) +- **Session max age** (default 30 minutes): stale sessions are discarded on recovery to prevent poisoned error context from blocking tool retries. When an LLM accumulates repeated tool failures in a session, it may stop retrying altogether. The max age ensures these poisoned sessions expire, giving the agent a fresh start. + +Configure via `forge.yaml` or environment variable: + +```yaml +memory: + session_max_age: "30m" # default; use "1h", "15m", etc. +``` + +```bash +export FORGE_SESSION_MAX_AGE=1h +``` + +## Context Window Management + +Forge automatically manages context window usage based on model capabilities: + +| Model | Context Window | Character Budget | +|-------|---------------|-----------------| +| `gpt-4o` / `gpt-5` | 128K tokens | ~435K chars | +| `claude-sonnet` / `claude-opus` | 200K tokens | ~680K chars | +| `gemini-2.5` | 1M tokens | ~3.4M chars | +| `llama3` | 8K tokens | ~27K chars | +| `llama3.1` | 128K tokens | ~435K chars | + +When context grows too large, the **Compactor** automatically: +1. Takes the oldest 50% of messages +2. Flushes tool results and decisions to long-term memory (if enabled) +3. Summarizes via LLM (with extractive fallback) +4. Replaces old messages with the summary + +Research tool results receive special handling during compaction: they are preserved with a higher extraction limit (5000 vs 2000 characters) and tagged distinctly in long-term memory logs (e.g., `[research][tool:tavily_research]`) so research insights persist across sessions. + +```yaml +memory: + char_budget: 200000 # override auto-detection + trigger_ratio: 0.6 # compact at 60% of budget (default) +``` + +## Long-Term Memory + +Enable cross-session knowledge persistence with hybrid vector + keyword search: + +```yaml +memory: + long_term: true + memory_dir: ".forge/memory" + vector_weight: 0.7 + keyword_weight: 0.3 + decay_half_life_days: 7 +``` + +Or via environment variable: + +```bash +export FORGE_MEMORY_LONG_TERM=true +``` + +When enabled, Forge: +- Creates a `.forge/memory/` directory with a `MEMORY.md` template for curated facts +- Indexes all `.md` files into a hybrid search index (vector similarity + keyword overlap + temporal decay) +- Registers `memory_search` and `memory_get` tools for the agent to use +- Automatically flushes compacted conversation context to daily log files (`YYYY-MM-DD.md`) + +## Embedding Providers + +Embedding providers power the vector search component of long-term memory: + +| Provider | Default Model | Notes | +|----------|--------------|-------| +| `openai` | `text-embedding-3-small` | Standard OpenAI embeddings API | +| `gemini` | `text-embedding-3-small` | OpenAI-compatible endpoint | +| `ollama` | `nomic-embed-text` | Local embeddings | + +Falls back to keyword-only search if no embedding provider is available (e.g., when using Anthropic as the primary provider without a fallback). + +## Configuration + +Full memory configuration in `forge.yaml`: + +```yaml +memory: + persistence: true + sessions_dir: ".forge/sessions" + session_max_age: "30m" # discard sessions idle longer than this + char_budget: 200000 + trigger_ratio: 0.6 + long_term: false + memory_dir: ".forge/memory" + embedding_provider: "" # Auto-detect from LLM provider + embedding_model: "" # Provider default + vector_weight: 0.7 + keyword_weight: 0.3 + decay_half_life_days: 7 +``` + +Environment variables: + +| Variable | Description | +|----------|-------------| +| `FORGE_MEMORY_PERSISTENCE` | Set `false` to disable session persistence | +| `FORGE_SESSION_MAX_AGE` | Session idle timeout, e.g. `30m`, `1h` (default: `30m`) | +| `FORGE_MEMORY_LONG_TERM` | Set `true` to enable long-term memory | +| `FORGE_EMBEDDING_PROVIDER` | Override embedding provider | diff --git a/src/content/docs/core-concepts/runtime-engine.md b/src/content/docs/core-concepts/runtime-engine.md new file mode 100644 index 0000000..b65f4a4 --- /dev/null +++ b/src/content/docs/core-concepts/runtime-engine.md @@ -0,0 +1,261 @@ +--- +title: "Runtime Engine" +description: "The LLM runtime engine powering tool calling, memory, and hooks." +order: 6 +editUrl: "https://github.com/initializ/forge/edit/main/docs/core-concepts/runtime-engine.md" +--- + + + +The runtime engine powers `forge run` — executing agent tasks via LLM providers with tool calling, conversation memory, and lifecycle hooks. + +## Agent Loop + +The core agent loop follows a simple pattern: + +1. **Initialize memory** with the system prompt and task history +2. **Append** the user message +3. **Call the LLM** with the conversation and available tool definitions +4. If the LLM returns **tool calls**: execute each tool, append results, go to step 3 +5. If the LLM returns a **text response**: return it as the final answer +6. If **max iterations** are exceeded: return an error + +``` +User message → Memory → LLM → tool_calls? → Execute tools → LLM → ... → text → Done +``` + +The loop terminates when `len(ToolCalls) == 0`. `FinishReason` is intentionally ignored — some providers return `"stop"` even when tool calls are present. Only the tool call list determines whether execution continues. + +### Session Recovery Deduplication + +When a session is recovered from disk (e.g., after a premature loop exit), the executor checks whether the recovered conversation already ends with an identical user message. If so, the duplicate is skipped to prevent the same message from appearing twice in the context window. This handles the common case where a user retries the same prompt after a crash or timeout. + +### Q&A Nudge Suppression + +When the agent finishes with `stop` and no workflow phases are configured, the loop checks whether edit or git tools were used. If only explore-phase tools were invoked (e.g., `web_search`, `file_read`), the conversation is classified as informational/Q&A — the agent's text response is the final answer and no continuation nudge ("You stopped…") is sent. This prevents the agent from re-summarizing answers to simple questions. + +## LLM Providers + +Forge supports multiple LLM providers with automatic fallback: + +| Provider | Default Model | Auth | +|----------|--------------|------| +| `openai` | `gpt-5.2-2025-12-11` | API key or OAuth; optional Organization ID | +| `anthropic` | `claude-sonnet-4-20250514` | API key | +| `gemini` | `gemini-2.5-flash` | API key | +| `ollama` | `llama3` | None (local) | +| Custom | Configurable | API key | + +### Configuration + +```yaml +model: + provider: openai + name: gpt-4o +``` + +Or override with environment variables: + +```bash +export FORGE_MODEL_PROVIDER=anthropic +export ANTHROPIC_API_KEY=sk-ant-... +forge run +``` + +Provider is auto-detected from available API keys if not explicitly set. Provider configuration is resolved via `ResolveModelConfig()` in priority order: + +1. **CLI flag** `--provider` (highest priority) +2. **Environment variables**: `FORGE_MODEL_PROVIDER`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` +3. **forge.yaml** `model` section (lowest priority) + +### OpenAI OAuth + +For OpenAI, Forge supports browser-based OAuth login (matching the Codex CLI flow) as an alternative to API keys: + +```bash +forge init my-agent +# Select "OpenAI" -> "Login with browser (OAuth)" +# Browser opens for authentication +``` + +OAuth tokens are stored in `~/.forge/credentials/openai.json` and automatically refreshed. + +#### OAuth for Skill Scripts + +When `OPENAI_API_KEY` is set to the sentinel value `__oauth__`, the `SkillCommandExecutor` resolves OAuth credentials at execution time and injects: +- The real access token as `OPENAI_API_KEY` +- The Codex base URL as `OPENAI_BASE_URL` +- The configured model name as `REVIEW_MODEL` + +Skill scripts (e.g., `code-review-diff.sh`) detect `OPENAI_BASE_URL` and automatically use the OpenAI Responses API with streaming instead of the standard Chat Completions API. + +### Secret Reuse Detection + +At startup, the runtime validates that secret values are not reused across different purpose categories. Sharing the same token between unrelated services (e.g., using an OpenAI API key as a Telegram bot token) is blocked with an error. + +Categories: `llm` (OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, etc.), `search` (TAVILY_API_KEY, PERPLEXITY_API_KEY), `telegram` (TELEGRAM_BOT_TOKEN), `slack` (SLACK_APP_TOKEN, SLACK_BOT_TOKEN). Same-category reuse (e.g., two LLM keys with the same value) is allowed. + +### Organization ID (OpenAI Enterprise) + +Enterprise OpenAI accounts can set an Organization ID to route API requests to the correct org: + +```yaml +model: + provider: openai + name: gpt-4o + organization_id: "org-xxxxxxxxxxxxxxxxxxxxxxxx" +``` + +Or via environment variable (overrides YAML): + +```bash +export OPENAI_ORG_ID=org-xxxxxxxxxxxxxxxxxxxxxxxx +``` + +The `OpenAI-Organization` header is sent on all OpenAI API requests (chat, embeddings, responses). Fallback providers inherit the primary org ID unless overridden per-fallback. The org ID is also injected into skill subprocess environments as `OPENAI_ORG_ID`. + +### Fallback Chains + +Configure fallback providers for automatic failover when the primary provider is unavailable: + +```yaml +model: + provider: openai + name: gpt-4o + fallbacks: + - provider: anthropic + name: claude-sonnet-4-20250514 + - provider: gemini +``` + +Or via environment variable: + +```bash +export FORGE_MODEL_FALLBACKS="anthropic:claude-sonnet-4-20250514,gemini:gemini-2.5-flash" +``` + +Fallback behavior: +- **Retriable errors** (rate limits, overloaded, timeouts) try the next provider +- **Non-retriable errors** (auth, billing, bad format) abort immediately +- Per-provider exponential backoff cooldowns prevent thundering herd +- Fallbacks are also auto-detected from available API keys when not explicitly configured + +## Executor Types + +The runtime supports multiple executor implementations: + +| Executor | Use Case | +|----------|----------| +| `LLMExecutor` | Custom agents with LLM-powered tool calling | +| `SubprocessExecutor` | Framework agents (CrewAI, LangChain) running as subprocesses | +| `StubExecutor` | Returns canned responses for testing | + +Executor selection happens in `runner.go` based on framework type and configuration. + +## Running Modes + +### `forge run` — Foreground Server + +Run the agent as a foreground HTTP server. Used for development and container deployments. + +```bash +# Development (all interfaces, immediate shutdown) +forge run --with slack --port 8080 + +# Container deployment +forge run --host 0.0.0.0 --shutdown-timeout 30s +``` + +| Flag | Default | Description | +|------|---------|-------------| +| `--port` | `8080` | HTTP server port | +| `--host` | `""` (all interfaces) | Bind address | +| `--shutdown-timeout` | `0` (immediate) | Graceful shutdown timeout | +| `--with` | — | Channel adapters (e.g. `slack,telegram`) | +| `--mock-tools` | `false` | Use mock executor for testing | +| `--model` | — | Override model name | +| `--provider` | — | Override LLM provider | +| `--env` | `.env` | Path to env file | +| `--enforce-guardrails` | `true` | Enforce guardrail violations as errors | +| `--no-guardrails` | `false` | Disable all guardrail enforcement | +| `--auth-url` | — | External auth provider URL for token validation | + +### `forge serve` — Background Daemon + +Manage the agent as a background daemon process with PID/log management. + +```bash +# Start daemon (secure defaults: 127.0.0.1, 30s shutdown timeout) +forge serve + +# Start on custom port +forge serve start --port 9090 --host 0.0.0.0 + +# Stop the daemon +forge serve stop + +# Check status (PID, uptime, health) +forge serve status + +# View recent logs (last 100 lines) +forge serve logs +``` + +| Subcommand | Description | +|------------|-------------| +| `start` (default) | Start the daemon in background | +| `stop` | Send SIGTERM (10s timeout, SIGKILL fallback) | +| `status` | Show PID, listen address, health check | +| `logs` | Tail `.forge/serve.log` | + +The daemon forks `forge run` in the background with `setsid`, writes state to `.forge/serve.json`, and redirects output to `.forge/serve.log`. Passphrase prompting for encrypted secrets happens in the parent process (which has TTY access) before forking. + +## External Authentication + +When `--auth-url` is set (or `FORGE_AUTH_URL` env var), the runtime delegates token validation to an external auth provider. On each request, the bearer token is forwarded to the external URL for verification. + +```bash +# Via CLI flag +forge run --auth-url https://auth.example.com/verify + +# Via environment variable (useful in containers) +docker run -e FORGE_AUTH_URL=https://auth.example.com/verify my-agent +``` + +The middleware checks tokens in two layers: an internal token is accepted first (used by channel adapter loopback calls), then the external auth provider is consulted. This ensures channel adapters (Slack, Telegram) can reach the A2A server without needing a valid external token. + +## KUBECONFIG Materialization + +The runtime supports passing kubeconfig content directly via the `KUBECONFIG` environment variable. If `KUBECONFIG` contains inline YAML (detected by newlines or `apiVersion:` markers), the runtime automatically writes it to a file and updates `KUBECONFIG` to point to that file. This is useful for container deployments where mounting files is inconvenient: + +```bash +docker run -e KUBECONFIG="$(cat ~/.kube/config)" my-agent +``` + +## File Output Directory + +The runtime configures a `FilesDir` for tool-generated files (e.g., from `file_create`). This directory defaults to `/.forge/files/` and is injected into the execution context so tools can write files that other tools can reference by path. + +``` +/ + .forge/ + files/ ← file_create output (patches.yaml, reports, etc.) + sessions/ ← conversation persistence + memory/ ← long-term memory +``` + +The `FilesDir` is set via `LLMExecutorConfig.FilesDir` and made available to tools through `runtime.FilesDirFromContext(ctx)`. See [Tools — File Create](/docs/core-concepts/tools#file-create) for details. + +## Conversation Memory + +For details on session persistence, context window management, compaction, and long-term memory, see [Memory](/docs/core-concepts/memory). + +## Hooks + +The engine fires hooks at key points in the loop. See [Hooks](/docs/core-concepts/hooks) for details. + +The runner registers five hook groups: logging, audit, progress, global guardrail hooks, and skill guardrail hooks. Global guardrails use the `GuardrailChecker` interface backed by the `github.com/initializ/guardrails` library — the `AfterToolExec` hook scans tool output for secrets and PII, redacting or blocking before results enter the LLM context. Guardrail config is loaded from `guardrails.json` (file mode) or MongoDB (DB mode). Skill guardrail hooks enforce domain-specific rules declared in `SKILL.md` — blocking commands, redacting output, intercepting capability enumeration probes, and replacing binary-enumerating responses. Skill guardrails are loaded from build artifacts or parsed directly from `SKILL.md` at runtime (no `forge build` required). See [Guardrails](/docs/core-concepts/security/guardrails) for full details. + +## Streaming + +The LLM tool-calling loop runs non-streaming internally. `ExecuteStream` calls `Execute` and emits the final response on a channel. However, the **UI chat proxy** (`forge-ui/chat.go`) streams A2A SSE events to the browser in real-time — `status` events carry incremental text, `progress` events carry tool execution updates, and `result` events carry the final response. The frontend renders text and tool progress as each event arrives. diff --git a/src/content/docs/core-concepts/scheduling.md b/src/content/docs/core-concepts/scheduling.md new file mode 100644 index 0000000..b6539c9 --- /dev/null +++ b/src/content/docs/core-concepts/scheduling.md @@ -0,0 +1,60 @@ +--- +title: "Scheduling" +description: "Built-in cron scheduler for recurring agent tasks." +order: 8 +editUrl: "https://github.com/initializ/forge/edit/main/docs/core-concepts/scheduling.md" +--- + + + +Forge includes a built-in cron scheduler for recurring tasks, configurable in `forge.yaml` or created dynamically by the agent at runtime. + +## Configuration + +```yaml +schedules: + - id: daily-report + cron: "@daily" + task: "Generate and send the daily status report" + skill: "tavily-research" # optional: invoke a specific skill + channel: telegram # optional: deliver results to a channel + channel_target: "-100123456" # optional: destination chat/channel ID +``` + +## Cron Expressions + +| Format | Example | Description | +|--------|---------|-------------| +| 5-field standard | `*/15 * * * *` | Every 15 minutes | +| Aliases | `@hourly`, `@daily`, `@weekly`, `@monthly` | Common intervals | +| Intervals | `@every 5m`, `@every 1h30m` | Duration-based (minimum 1 minute) | + +## Schedule Tools + +The agent has four built-in tools for managing schedules at runtime: + +| Tool | Description | +|------|-------------| +| `schedule_set` | Create or update a recurring schedule | +| `schedule_list` | List all active and inactive schedules | +| `schedule_delete` | Remove a schedule (LLM-created only; YAML-defined cannot be deleted) | +| `schedule_history` | View execution history for scheduled tasks | + +Schedules can also be managed via the CLI: + +```bash +# List all schedules +forge schedule list +``` + +## Channel Delivery + +When a schedule includes `channel` and `channel_target`, the agent's response is automatically delivered to the specified channel after each execution. When schedules are created from channel conversations (Slack, Telegram), the channel context is automatically available so the agent can capture the delivery target. + +## Execution Details + +- **Tick interval**: 30 seconds +- **Overlap prevention**: A schedule won't fire again if its previous run is still in progress +- **Persistence**: Schedules are stored in `.forge/memory/SCHEDULES.md` and survive restarts +- **History**: The last 50 executions are recorded with status, duration, and correlation IDs +- **Audit events**: `schedule_fire`, `schedule_complete`, `schedule_skip`, `schedule_modify` diff --git a/src/content/docs/core-concepts/skill-md-format.md b/src/content/docs/core-concepts/skill-md-format.md new file mode 100644 index 0000000..aedf6ee --- /dev/null +++ b/src/content/docs/core-concepts/skill-md-format.md @@ -0,0 +1,168 @@ +--- +title: "SKILL.md Format" +description: "Define agent skills using the SKILL.md format with YAML frontmatter and tool definitions." +order: 2 +editUrl: "https://github.com/initializ/forge/edit/main/docs/core-concepts/skill-md-format.md" +--- + + + +## SKILL.md Format + +Skills are defined in Markdown files inside `skills//SKILL.md`. Each file supports optional YAML frontmatter and two body formats. + +```markdown +--- +name: weather +icon: 🌤️ +category: utilities +tags: + - weather + - forecast + - api +description: Weather data skill +metadata: + forge: + requires: + bins: + - curl + env: + required: [] + one_of: [] + optional: [] +--- +## Tool: weather_current + +Get current weather for a location. + +**Input:** location (string) - City name or coordinates +**Output:** Current temperature, conditions, humidity, and wind speed + +## Tool: weather_forecast + +Get weather forecast for a location. + +**Input:** location (string), days (integer: 1-7) +**Output:** Daily forecast with high/low temperatures and conditions +``` + +Each `## Tool:` heading defines a tool the agent can call. The frontmatter declares binary dependencies and environment variable requirements. Skills compile into JSON artifacts and prompt text during `forge build`. + +### YAML Frontmatter + +Top-level fields: + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | yes | Skill identifier (kebab-case) | +| `icon` | yes | Emoji displayed in the TUI skill picker | +| `category` | yes | Grouping for `forge skills list --category` (e.g., `sre`, `developer`, `research`, `utilities`) | +| `tags` | yes | Discovery keywords for `forge skills list --tags` (kebab-case) | +| `description` | yes | One-line summary | + +The `metadata.forge.requires` block declares runtime dependencies: + +- **`bins`** — Binary dependencies that must be in `$PATH` at runtime +- **`env.required`** — Environment variables that must be set +- **`env.one_of`** — At least one of these environment variables must be set +- **`env.optional`** — Optional environment variables for extended functionality + +Frontmatter is parsed by `ParseWithMetadata()` in `forge-skills/parser/parser.go` and feeds into the compilation pipeline. + +### Legacy List Format + +```markdown +# Agent Skills + +- translate +- summarize +- classify +``` + +Single-word list items (no spaces, max 64 characters) create name-only skill entries. This format is simpler but provides less metadata. + +## Skill Registry + +Forge ships with a built-in skill registry. Add skills to your project with a single command: + +```bash +# Add a skill from the registry +forge skills add tavily-research + +# Validate skill requirements +forge skills validate + +# Audit skill security +forge skills audit --embedded +``` + +`forge skills add` copies the skill's SKILL.md and any associated scripts into your project's `skills/` directory. It validates binary and environment requirements, checks for existing values in your environment, `.env` file, and encrypted secrets, and prompts only for truly missing values with a suggestion to use `forge secrets set` for sensitive keys. If the skill declares `egress_domains`, they are automatically merged into the `forge.yaml` `egress.allowed_domains` list (deduplicated and sorted). + +## Skills as First-Class Tools + +Script-backed skills are automatically registered as **first-class LLM tools** at runtime. When a skill has scripts in `skills/scripts/`, Forge: + +1. Parses the skill's SKILL.md for tool definitions, descriptions, and input schemas +2. Creates a named tool for each `## Tool:` entry (e.g., `tavily_research` becomes a tool the LLM can call directly) +3. Executes the skill's shell script with JSON input when the LLM invokes it + +This means the LLM sees skill tools alongside builtins like `web_search` and `http_request` — no generic `cli_execute` indirection needed. + +For skills **without** scripts (binary-backed skills like `k8s-incident-triage`), Forge injects the full skill instructions into the system prompt. The complete SKILL.md body — including triage steps, detection heuristics, output structure, and safety constraints — is included inline so the LLM follows the skill protocol without needing an extra tool call. Skills are invoked via `cli_execute` with the declared binary dependencies. + +``` +┌─────────────────────────────────────────────────┐ +│ LLM Tool Registry │ +├─────────────────┬───────────────────────────────┤ +│ Builtins │ web_search, http_request │ +│ Skill Tools │ tavily_research, codegen_* │ ← auto-registered from scripts +│ read_skill │ load any SKILL.md on demand │ +│ cli_execute │ run approved binaries │ +├─────────────────┴───────────────────────────────┤ +│ System Prompt: full skill instructions inline │ ← binary-backed skills +└─────────────────────────────────────────────────┘ +``` + +## Skill Execution Security + +Skill scripts run in a restricted environment via `SkillCommandExecutor`: + +- **Isolated environment**: Only `PATH`, `HOME`, and explicitly declared env vars are passed through +- **OAuth token resolution**: When `OPENAI_API_KEY` is set to `__oauth__`, the executor resolves OAuth credentials and injects the access token, `OPENAI_BASE_URL`, and the configured model as `REVIEW_MODEL` +- **Configurable timeout**: Each skill declares a `timeout_hint` in its YAML frontmatter (e.g., 300s for research) +- **No shell execution**: Scripts run via `bash