Skip to content

Commit e2c72e6

Browse files
committed
feat: allowed hosts
1 parent e783ff1 commit e2c72e6

File tree

7 files changed

+835
-45
lines changed

7 files changed

+835
-45
lines changed

README.md

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ Control [Claude Code](https://github.com/anthropics/claude-code), [Goose](https:
44

55
![agentapi-chat](https://github.com/user-attachments/assets/57032c9f-4146-4b66-b219-09e38ab7690d)
66

7-
87
You can use AgentAPI:
98

109
- to build a unified chat interface for coding agents
@@ -54,9 +53,6 @@ You can use AgentAPI:
5453

5554
Run an HTTP server that lets you control an agent. If you'd like to start an agent with additional arguments, pass the full agent command after the `--` flag.
5655

57-
> [!NOTE]
58-
> When using Codex, always specify the agent type explicitly (`agentapi server --type=codex -- codex`), or message formatting may break.
59-
6056
```bash
6157
agentapi server -- claude --allowedTools "Bash(git*) Edit Replace"
6258
```
@@ -68,6 +64,9 @@ agentapi server -- aider --model sonnet --api-key anthropic=sk-ant-apio3-XXX
6864
agentapi server -- goose
6965
```
7066

67+
> [!NOTE]
68+
> When using Codex, always specify the agent type explicitly (`agentapi server --type=codex -- codex`), or message formatting may break.
69+
7170
An OpenAPI schema is available in [openapi.json](openapi.json).
7271

7372
By default, the server runs on port 3284. Additionally, the server exposes the same OpenAPI schema at http://localhost:3284/openapi.json and the available endpoints in a documentation UI at http://localhost:3284/docs.
@@ -79,6 +78,54 @@ There are 4 endpoints:
7978
- GET `/status` - returns the current status of the agent, either "stable" or "running"
8079
- GET `/events` - an SSE stream of events from the agent: message and status updates
8180

81+
#### Allowed hosts
82+
83+
By default, the server only allows requests with the host header set to localhost:3284. If you'd like to host AgentAPI elsewhere, you can change this by using the `AGENTAPI_ALLOWED_HOSTS` environment variable or the `--allowed-hosts` flag.
84+
85+
To allow requests from any host, use `*` as the allowed host.
86+
87+
```bash
88+
agentapi server --allowed-hosts '*' -- claude
89+
```
90+
91+
To allow a specific host, use:
92+
93+
```bash
94+
agentapi server --allowed-hosts 'example.com' -- claude
95+
```
96+
97+
To specify multiple hosts, use a comma-separated list when using the `--allowed-hosts` flag, or a space-separated list when using the `AGENTAPI_ALLOWED_HOSTS` environment variable.
98+
99+
```bash
100+
agentapi server --allowed-hosts 'example.com,example.org' -- claude
101+
# or
102+
AGENTAPI_ALLOWED_HOSTS='example.com example.org' agentapi server -- claude
103+
```
104+
105+
#### Allowed origins
106+
107+
By default, the server allows CORS requests from `http://localhost:3284`, `http://localhost:3000`, and `http://localhost:3001`. If you'd like to change which origins can make cross-origin requests to AgentAPI, you can change this by using the `AGENTAPI_ALLOWED_ORIGINS` environment variable or the `--allowed-origins` flag.
108+
109+
To allow requests from any origin, use `*` as the allowed origin:
110+
111+
```bash
112+
agentapi server --allowed-origins '*' -- claude
113+
```
114+
115+
To allow a specific origin, use:
116+
117+
```bash
118+
agentapi server --allowed-origins 'https://example.com' -- claude
119+
```
120+
121+
To specify multiple origins, use a comma-separated list when using the `--allowed-origins` flag, or a space-separated list when using the `AGENTAPI_ALLOWED_ORIGINS` environment variable. Origins must include the protocol (`http://` or `https://`):
122+
123+
```bash
124+
agentapi server --allowed-origins 'https://example.com,http://localhost:3000' -- claude
125+
# or
126+
AGENTAPI_ALLOWED_ORIGINS='https://example.com http://localhost:3000' agentapi server -- claude
127+
```
128+
82129
### `agentapi attach`
83130

84131
Attach to a running agent's terminal session.

cmd/server/server.go

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os"
1010
"sort"
1111
"strings"
12+
"unicode"
1213

1314
"github.com/spf13/cobra"
1415
"github.com/spf13/viper"
@@ -58,6 +59,26 @@ func parseAgentType(firstArg string, agentTypeVar string) (AgentType, error) {
5859
return AgentTypeCustom, nil
5960
}
6061

62+
// Validate allowed hosts or origins don't contain whitespace or commas.
63+
// Viper/Cobra use different separators (space for env vars, comma for flags),
64+
// so these characters in origins likely indicate user error.
65+
func validateAllowedHostsOrOrigins(input []string) error {
66+
if len(input) == 0 {
67+
return fmt.Errorf("the list must not be empty")
68+
}
69+
for _, item := range input {
70+
for _, r := range item {
71+
if unicode.IsSpace(r) {
72+
return fmt.Errorf("'%s' contains whitespace characters, which are not allowed", item)
73+
}
74+
}
75+
if strings.Contains(item, ",") {
76+
return fmt.Errorf("'%s' contains comma characters, which are not allowed", item)
77+
}
78+
}
79+
return nil
80+
}
81+
6182
func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) error {
6283
agent := argsToPass[0]
6384
agentTypeValue := viper.GetString(FlagType)
@@ -95,12 +116,17 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er
95116
}
96117
}
97118
port := viper.GetInt(FlagPort)
98-
srv := httpapi.NewServer(ctx, httpapi.ServerConfig{
99-
AgentType: agentType,
100-
Process: process,
101-
Port: port,
102-
ChatBasePath: viper.GetString(FlagChatBasePath),
119+
srv, err := httpapi.NewServer(ctx, httpapi.ServerConfig{
120+
AgentType: agentType,
121+
Process: process,
122+
Port: port,
123+
ChatBasePath: viper.GetString(FlagChatBasePath),
124+
AllowedHosts: viper.GetStringSlice(FlagAllowedHosts),
125+
AllowedOrigins: viper.GetStringSlice(FlagAllowedOrigins),
103126
})
127+
if err != nil {
128+
return xerrors.Errorf("failed to create server: %w", err)
129+
}
104130
if printOpenAPI {
105131
fmt.Println(srv.GetOpenAPI())
106132
return nil
@@ -150,12 +176,15 @@ type flagSpec struct {
150176
}
151177

152178
const (
153-
FlagType = "type"
154-
FlagPort = "port"
155-
FlagPrintOpenAPI = "print-openapi"
156-
FlagChatBasePath = "chat-base-path"
157-
FlagTermWidth = "term-width"
158-
FlagTermHeight = "term-height"
179+
FlagType = "type"
180+
FlagPort = "port"
181+
FlagPrintOpenAPI = "print-openapi"
182+
FlagChatBasePath = "chat-base-path"
183+
FlagTermWidth = "term-width"
184+
FlagTermHeight = "term-height"
185+
FlagAllowedHosts = "allowed-hosts"
186+
FlagAllowedOrigins = "allowed-origins"
187+
FlagExit = "exit"
159188
)
160189

161190
func CreateServerCmd() *cobra.Command {
@@ -164,7 +193,22 @@ func CreateServerCmd() *cobra.Command {
164193
Short: "Run the server",
165194
Long: fmt.Sprintf("Run the server with the specified agent (one of: %s)", strings.Join(agentNames, ", ")),
166195
Args: cobra.MinimumNArgs(1),
196+
PreRunE: func(cmd *cobra.Command, args []string) error {
197+
allowedHosts := viper.GetStringSlice(FlagAllowedHosts)
198+
if err := validateAllowedHostsOrOrigins(allowedHosts); err != nil {
199+
return xerrors.Errorf("failed to validate allowed hosts: %w", err)
200+
}
201+
allowedOrigins := viper.GetStringSlice(FlagAllowedOrigins)
202+
if err := validateAllowedHostsOrOrigins(allowedOrigins); err != nil {
203+
return xerrors.Errorf("failed to validate allowed origins: %w", err)
204+
}
205+
return nil
206+
},
167207
Run: func(cmd *cobra.Command, args []string) {
208+
// The --exit flag is used for testing validation of flags in the test suite
209+
if viper.GetBool(FlagExit) {
210+
return
211+
}
168212
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
169213
ctx := logctx.WithLogger(context.Background(), logger)
170214
if err := runServer(ctx, logger, cmd.Flags().Args()); err != nil {
@@ -181,6 +225,10 @@ func CreateServerCmd() *cobra.Command {
181225
{FlagChatBasePath, "c", "/chat", "Base path for assets and routes used in the static files of the chat interface", "string"},
182226
{FlagTermWidth, "W", uint16(80), "Width of the emulated terminal", "uint16"},
183227
{FlagTermHeight, "H", uint16(1000), "Height of the emulated terminal", "uint16"},
228+
// localhost:3284 is the default host for the server
229+
{FlagAllowedHosts, "a", []string{"localhost:3284"}, "HTTP allowed hosts. Use '*' for all, comma-separated list via flag, space-separated list via AGENTAPI_ALLOWED_HOSTS env var", "stringSlice"},
230+
// localhost:3284 is the default origin when you open the chat interface in your browser. localhost:3000 and 3001 are used during development.
231+
{FlagAllowedOrigins, "o", []string{"http://localhost:3284", "http://localhost:3000", "http://localhost:3001"}, "HTTP allowed origins. Use '*' for all, comma-separated list via flag, space-separated list via AGENTAPI_ALLOWED_ORIGINS env var", "stringSlice"},
184232
}
185233

186234
for _, spec := range flagSpecs {
@@ -193,6 +241,8 @@ func CreateServerCmd() *cobra.Command {
193241
serverCmd.Flags().BoolP(spec.name, spec.shorthand, spec.defaultValue.(bool), spec.usage)
194242
case "uint16":
195243
serverCmd.Flags().Uint16P(spec.name, spec.shorthand, spec.defaultValue.(uint16), spec.usage)
244+
case "stringSlice":
245+
serverCmd.Flags().StringSliceP(spec.name, spec.shorthand, spec.defaultValue.([]string), spec.usage)
196246
default:
197247
panic(fmt.Sprintf("unknown flag type: %s", spec.flagType))
198248
}
@@ -201,6 +251,14 @@ func CreateServerCmd() *cobra.Command {
201251
}
202252
}
203253

254+
serverCmd.Flags().Bool(FlagExit, false, "Exit immediately after parsing arguments")
255+
if err := serverCmd.Flags().MarkHidden(FlagExit); err != nil {
256+
panic(fmt.Sprintf("failed to mark flag %s as hidden: %v", FlagExit, err))
257+
}
258+
if err := viper.BindPFlag(FlagExit, serverCmd.Flags().Lookup(FlagExit)); err != nil {
259+
panic(fmt.Sprintf("failed to bind flag %s: %v", FlagExit, err))
260+
}
261+
204262
viper.SetEnvPrefix("AGENTAPI")
205263
viper.AutomaticEnv()
206264
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))

0 commit comments

Comments
 (0)