Skip to content
Open
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
5 changes: 5 additions & 0 deletions .claude/skills/forge.md
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,11 @@ guardrails_path: guardrails.json

## 15. How to create an agent

For a first taste, `forge try` scaffolds a keyless demo agent and drops you into
a chat with the tool/egress loop rendered inline (ephemeral by default;
`forge try --keep` writes it to `./forge-quickstart`). The pipeline below is for
building your own from scratch.

```bash
# 1. Scaffold
forge init my-agent --model-provider anthropic --channels slack --non-interactive
Expand Down
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,31 @@ Forge is the open-source runtime for Anthropic's Agent Skills standard — built

## Quick Start

Talk to a working agent, and watch it use a tool, in under 60 seconds:

```bash
# Install (pick one)
brew install initializ/tap/forge
curl -sSL https://raw.githubusercontent.com/initializ/forge/main/install.sh | bash
brew install initializ/tap/forge && forge try
```

`forge try` scaffolds a keyless demo agent, finds whatever model credential you
already have, and drops you into a chat whose every tool call and egress check
renders inline:

# Create and run an agent
forge init my-agent && cd my-agent && forge run
```
you › what's the weather in Tokyo, should I pack an umbrella?

▸ tool weather_current(location=Tokyo)
▸ egress wttr.in ✓ allowed
◂ 18C, light rain this evening

# Connect to Slack
forge run --with slack
agent › 18C in Tokyo with light rain tonight. Yes, take the umbrella.
```

See [Quick Start](docs/getting-started/quick-start.md) for the full walkthrough, or [Installation](docs/getting-started/installation.md) for all methods.
Then build your own: `forge try --keep` graduates the demo to `./forge-quickstart`.

See [Quick Start](docs/getting-started/quick-start.md) for the walkthrough,
[Ship to Production](docs/getting-started/ship-to-production.md) for the full
pipeline, or [Installation](docs/getting-started/installation.md) for all methods.

## How It Works

Expand Down Expand Up @@ -73,7 +85,8 @@ You write a `SKILL.md`. Forge compiles it into a secure, runnable agent with egr

| Document | Description |
|----------|-------------|
| [Quick Start](docs/getting-started/quick-start.md) | Get an agent running in 60 seconds |
| [Quick Start](docs/getting-started/quick-start.md) | Talk to an agent in 60 seconds with `forge try` |
| [Ship to Production](docs/getting-started/ship-to-production.md) | The full init, build, package, deploy pipeline |
| [Installation](docs/getting-started/installation.md) | Homebrew, binary, and Windows install |
| [Architecture](docs/core-concepts/how-forge-works.md) | System design, module layout, and data flows |

Expand Down
92 changes: 62 additions & 30 deletions docs/getting-started/quick-start.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,83 @@
---
title: "Quick Start"
description: "Get a Forge agent running in under 60 seconds."
description: "Talk to a working agent in under 60 seconds with forge try."
order: 2
---

Get a Forge agent running in under 60 seconds.
Talk to a working agent, and watch it use a tool, in under 60 seconds. One command:

## Why Forge?
```bash
brew install initializ/tap/forge && forge try
```

**Instant Agent From a Single Command**
No build, no cluster, no config. `forge try` scaffolds a keyless demo agent into
a throwaway workspace, finds whatever model credential you already have (an env
key, an OpenAI sign-in, or a local Ollama), and drops you into a chat whose every
tool call and egress check renders inline.

Write a SKILL.md. Run `forge init`. Your agent is live.
## What it looks like

The wizard configures your model provider, validates your API key,
connects Slack or Telegram, picks skills, and starts your agent.
Zero to running in under 60 seconds.
```
$ forge try

**Secure by Default**
forge try: talking to a live agent in your terminal.
No build, no cluster. Ctrl-D or /exit to quit.

Forge is designed for safe execution:
Using OpenAI (signed in).
Agent: quickstart · skills: weather · tools: http_request, datetime_now, math_calculate

* Does NOT create public tunnels
* Does NOT expose webhooks automatically
* Uses outbound-only connections (Slack Socket Mode, Telegram polling)
* Enforces outbound domain allowlists at both build-time and runtime, including subprocess HTTP via a local egress proxy
* Encrypts secrets at rest (AES-256-GCM) with per-agent isolation
* Signs build artifacts (Ed25519) for supply chain integrity
* Supports restricted network profiles with audit logging
Try: what's the weather in Tokyo?
what's 17% of 4,200?
what time is it in UTC?

No accidental exposure. No hidden listeners.
you › what's the weather in Tokyo, should I pack an umbrella?

## Get Started in 60 Seconds
▸ tool weather_current(location=Tokyo)
▸ egress wttr.in ✓ allowed
◂ 18C, light rain this evening

```bash
# Install
curl -sSL https://raw.githubusercontent.com/initializ/forge/main/install.sh | bash
agent › 18C in Tokyo with light rain expected tonight. Yes, take the umbrella.

# Initialize a new agent (interactive wizard)
forge init my-agent
audit {"tools":["weather_current"],"egress":["wttr.in:allow"]}

# Run locally
cd my-agent && forge run
you › ^D

# Run with Telegram
forge run --with telegram
You just ran an agent whose every tool call and egress you can see and audit.
Want to keep it and make it yours? -> forge try --keep (writes ./forge-quickstart)
Then edit skills/, run it as a service with forge serve, or deploy with forge package.
```

The `forge init` wizard walks you through model provider, API key, fallback providers, tools, skills, and channel setup. Use `--non-interactive` with flags for scripted setups.
The inline `▸ tool` / `▸ egress` lines are the agent's own audit stream, the same
signal the enterprise story rests on, surfaced as the first-run view. Add `--audit`
to see the full NDJSON, or `--quiet` to hide the loop.

## One-shot mode

For a non-interactive taste (CI, docs, a quick check):

```bash
forge try --once "what's 2 + 2?"
```

## No credential yet?

`forge try` resolves, in order: an explicit `--provider`/`--model`, then an env
key (`ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY`), then a saved
OpenAI sign-in, then a local [Ollama](https://ollama.com) daemon. With none of
those, it offers a one-time picker (sign in with OpenAI, paste a key, or use
Ollama). Nothing is written to disk unless you pass `--keep`.

## The ladder

`forge try` is rung one. Each step adds one capability:

1. **60s** — `forge try`: talk to an agent, watch it use a tool.
2. **5 min** — `forge try --keep`, then edit `skills/<name>/SKILL.md` and add a skill. See [Your First Skill](your-first-skill.md).
3. **15 min** — `forge serve`, tail the audit log, add a guardrail and watch it block. See [Ship to Production](ship-to-production.md).
4. **Ship** — `forge package`: an egress-enforced container into your own cluster. See [Ship to Production](ship-to-production.md).

## Next steps

See [Installation](installation.md) for all installation methods.
- [Your First Skill](your-first-skill.md) — teach the agent something new.
- [Ship to Production](ship-to-production.md) — the full init -> build -> package -> deploy pipeline.
- [Installation](installation.md) — every install method.
95 changes: 95 additions & 0 deletions docs/getting-started/ship-to-production.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
title: "Ship to Production"
description: "The full pipeline: init, skills, secrets, validate, build, package, deploy."
order: 3
---

Once [`forge try`](quick-start.md) has shown you a working agent, this is the
path to a deployable one you own: scaffold it, give it skills and secrets,
validate, build, and package it into an egress-enforced container for your own
cluster. Each step is independently runnable.

## 1. Scaffold

```bash
forge init my-agent
cd my-agent
```

The interactive wizard configures the model provider, validates the API key,
optionally connects a channel (Slack / Telegram), picks skills, and sets the
egress allowlist. For scripted setups, use flags with `--non-interactive`:

```bash
forge init my-agent --model-provider anthropic --non-interactive
```

`forge try --keep` writes the same layout to `./forge-quickstart`, so you can
graduate the demo agent instead of starting from scratch.

## 2. Add skills

Skills are the agent's capabilities. Install from the registry or write your own:

```bash
forge skills add tavily-research # registry skill
# or author skills/<name>/SKILL.md by hand
```

See [Your First Skill](your-first-skill.md) for the SKILL.md format.

## 3. Configure secrets

Secrets are encrypted at rest (AES-256-GCM), per-agent:

```bash
forge secret set ANTHROPIC_API_KEY sk-...
forge secret set SLACK_BOT_TOKEN xoxb-...
```

## 4. Validate

Catch config, egress, and policy problems before building:

```bash
forge validate
```

## 5. Run locally

```bash
forge run # A2A server on :8080
forge serve # long-running service
forge run --with slack # attach a channel
```

Tail the audit log while it runs to see tool calls, egress decisions, and
guardrail blocks, the same stream `forge try` renders inline.

## 6. Build

Compile the agent and its dependencies into a runnable artifact. Build-time
egress allowlisting and Ed25519 artifact signing happen here:

```bash
forge build
```

## 7. Package and deploy

Produce an egress-enforced container image and the deployment manifests for your
cluster:

```bash
forge package
```

The generated image enforces the outbound domain allowlist at runtime (including
subprocess HTTP via the local egress proxy), so the deployed agent has the same
network posture you validated locally. Deploy it with your own pipeline.

## Related

- [Quick Start](quick-start.md) — the 60-second `forge try` on-ramp.
- [Your First Skill](your-first-skill.md) — author a SKILL.md.
- [Installation](installation.md) — every install method.
7 changes: 7 additions & 0 deletions docs/reference/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,10 +532,17 @@ forge auth secret-yaml --name custom-secret-name
# Common one-liner: populate the Secret a `forge package` deploy
# expects from the local runtime.token.
forge auth secret-yaml | kubectl apply -f -

# Remove a stored LLM OAuth credential (default: openai) so the next
# `forge init` / `forge try` prompts you to sign in again.
forge auth logout
forge auth logout openai
```

The `forge.agent.id` label on the generated Secret is always sourced from `forge.yaml`'s `agent_id` (or the `"forge-agent"` fallback), never from the `--name` override — so operators using `--name` to match an existing cluster convention still see telemetry and label-selectors keyed on the real agent ID.

`forge auth logout` is an operator/laptop command: it deletes the OAuth credential from `~/.forge/credentials` and the encrypted store, and **refuses to run inside an agent runtime** — a container, or when `FORGE_PLATFORM_TOKEN` is set. A deployed agent authenticates with an injected API key or platform token, not the OAuth credential store, so there is nothing there for the runtime to log out of; the refusal is defense-in-depth so Forge is never the tool an agent shells out to in order to wipe an operator's credential.

---

## `forge key`
Expand Down
1 change: 1 addition & 0 deletions forge-cli/cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,5 @@ func init() {
authCmd.AddCommand(authShowTokenCmd)
authCmd.AddCommand(authMintTokenCmd)
authCmd.AddCommand(authSecretYAMLCmd)
authCmd.AddCommand(authLogoutCmd)
}
90 changes: 90 additions & 0 deletions forge-cli/cmd/auth_logout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package cmd

import (
"fmt"
"os"
"strings"

"github.com/spf13/cobra"

"github.com/initializ/forge/forge-core/llm/oauth"
"github.com/initializ/forge/forge-core/security"
)

// authLogoutCmd removes a stored LLM OAuth credential so the next sign-in
// prompts again (e.g. to re-show the `forge try` credential picker).
//
// It is deliberately an operator/laptop command and REFUSES to run inside an
// agent runtime (a container, or when FORGE_PLATFORM_TOKEN is injected). A
// deployed agent authenticates with an injected API key or platform token, not
// this OAuth credential store, so there is nothing here for the runtime to log
// out of — and Forge should not be the tool an agent shells out to in order to
// wipe an operator's credential from inside a sandbox. This is defense in
// depth, not the primary control: an agent that can run arbitrary commands
// could `rm` the file directly, so the real mitigation is denying the agent
// write access to the operator's ~/.forge. See PR #353 discussion.
var authLogoutCmd = &cobra.Command{
Use: "logout [provider]",
Short: "Remove a stored LLM OAuth credential (laptop/dev only)",
Long: `Delete the stored OAuth credential for an LLM provider (default: openai)
from ~/.forge/credentials and the encrypted store, so the next
'forge init' / 'forge try' prompts you to sign in again.

This is an operator/laptop command. It refuses to run inside an agent
runtime (a container, or when FORGE_PLATFORM_TOKEN is set): a deployed
agent authenticates with an injected API key or platform token, not
this OAuth file, so there is nothing there to log out of.`,
Args: cobra.MaximumNArgs(1),
RunE: runAuthLogout,
SilenceUsage: true,
}

func runAuthLogout(cmd *cobra.Command, args []string) error {
provider := "openai"
if len(args) == 1 && strings.TrimSpace(args[0]) != "" {
provider = strings.ToLower(strings.TrimSpace(args[0]))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low (path traversal via unvalidated provider). provider is taken raw from args[0] and passed to oauth.LoadCredentials (L55) and oauth.DeleteCredentials (L59), both of which map it to filepath.Join(dir, provider+".json") with no sanitization (store.go:311, removePlaintextFile is always reached). So:

forge auth logout '../../../../tmp/x'
→ os.Remove(~/.forge/credentials/../../../../tmp/x.json) → deletes /tmp/x.json

Severity is Low — laptop/dev-only (the deniedInAgentRuntime guard blocks the container/platform case, so this isn't agent-reachable), operator-invoked, only .json-suffixed files the user already owns, no priv-esc. But it's a real validation gap and inconsistent with the paste-key path this same PR just hardened with providerFromInput. Close it at this entry point:

if _, ok := providerKeyEnv[provider]; !ok {
    return fmt.Errorf("unknown provider %q (use openai, anthropic, or gemini)", provider)
}

Defense-in-depth, the store.go provider→path helpers should also reject path separators (they're the pre-existing traversal-capable code shared by save/load/remove), but validating here closes the newly-reachable CLI vector. A TestAuthLogout_RejectsTraversalProvider case would pin it.

}

// Validate against the known providers BEFORE the value reaches the
// credential store, whose provider->path mapping is
// filepath.Join(dir, provider+".json") with no sanitization — so an
// unchecked arg like "../../../../tmp/x" would delete /tmp/x.json. Same
// whitelist the paste-key path uses.
if _, ok := providerKeyEnv[provider]; !ok {
return fmt.Errorf("unknown provider %q (use openai, anthropic, or gemini)", provider)
}

if reason, denied := deniedInAgentRuntime(); denied {
return fmt.Errorf("refusing to log out %s: %s. "+
"`auth logout` is a laptop/dev command; a deployed agent uses an injected "+
"key or platform token, not the OAuth credential store", provider, reason)
}

out := cmd.OutOrStdout()
// Only report "nothing to do" when the store is DEFINITIVELY empty. On a
// read error (e.g. a corrupt token file) fall through to delete so logout
// still clears it, rather than silently leaving it in place.
if tok, err := oauth.LoadCredentials(provider); err == nil && tok == nil {
_, _ = fmt.Fprintf(out, "No %s credential stored; nothing to do.\n", provider)
return nil
}
if err := oauth.DeleteCredentials(provider); err != nil {
return fmt.Errorf("removing %s credentials: %w", provider, err)
}
_, _ = fmt.Fprintf(out, "Logged out of %s. The next sign-in will prompt again.\n", provider)
return nil
}

// deniedInAgentRuntime reports whether the process looks like a deployed agent
// runtime (a container, or a managed deployment with a platform token) rather
// than an operator's laptop, along with a human reason. Sensitive operator-only
// auth commands refuse there.
func deniedInAgentRuntime() (reason string, denied bool) {
if os.Getenv("FORGE_PLATFORM_TOKEN") != "" {
return "FORGE_PLATFORM_TOKEN is set (managed deployment)", true
}
if security.InContainer() {
return "running inside a container", true
}
return "", false
}
Loading
Loading