Skip to content

Commit 5c425c6

Browse files
authored
feat: implement HTTP allowed hosts/origins checking (#49)
1 parent e783ff1 commit 5c425c6

File tree

5 files changed

+976
-45
lines changed

5 files changed

+976
-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`. 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. Hosts must be hostnames only (no ports); the server ignores the port portion of incoming requests when authorizing.
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://`) and support wildcards (e.g., `https://*.example.com`):
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: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,17 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er
9595
}
9696
}
9797
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),
98+
srv, err := httpapi.NewServer(ctx, httpapi.ServerConfig{
99+
AgentType: agentType,
100+
Process: process,
101+
Port: port,
102+
ChatBasePath: viper.GetString(FlagChatBasePath),
103+
AllowedHosts: viper.GetStringSlice(FlagAllowedHosts),
104+
AllowedOrigins: viper.GetStringSlice(FlagAllowedOrigins),
103105
})
106+
if err != nil {
107+
return xerrors.Errorf("failed to create server: %w", err)
108+
}
104109
if printOpenAPI {
105110
fmt.Println(srv.GetOpenAPI())
106111
return nil
@@ -150,12 +155,15 @@ type flagSpec struct {
150155
}
151156

152157
const (
153-
FlagType = "type"
154-
FlagPort = "port"
155-
FlagPrintOpenAPI = "print-openapi"
156-
FlagChatBasePath = "chat-base-path"
157-
FlagTermWidth = "term-width"
158-
FlagTermHeight = "term-height"
158+
FlagType = "type"
159+
FlagPort = "port"
160+
FlagPrintOpenAPI = "print-openapi"
161+
FlagChatBasePath = "chat-base-path"
162+
FlagTermWidth = "term-width"
163+
FlagTermHeight = "term-height"
164+
FlagAllowedHosts = "allowed-hosts"
165+
FlagAllowedOrigins = "allowed-origins"
166+
FlagExit = "exit"
159167
)
160168

161169
func CreateServerCmd() *cobra.Command {
@@ -165,6 +173,10 @@ func CreateServerCmd() *cobra.Command {
165173
Long: fmt.Sprintf("Run the server with the specified agent (one of: %s)", strings.Join(agentNames, ", ")),
166174
Args: cobra.MinimumNArgs(1),
167175
Run: func(cmd *cobra.Command, args []string) {
176+
// The --exit flag is used for testing validation of flags in the test suite
177+
if viper.GetBool(FlagExit) {
178+
return
179+
}
168180
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
169181
ctx := logctx.WithLogger(context.Background(), logger)
170182
if err := runServer(ctx, logger, cmd.Flags().Args()); err != nil {
@@ -181,6 +193,10 @@ func CreateServerCmd() *cobra.Command {
181193
{FlagChatBasePath, "c", "/chat", "Base path for assets and routes used in the static files of the chat interface", "string"},
182194
{FlagTermWidth, "W", uint16(80), "Width of the emulated terminal", "uint16"},
183195
{FlagTermHeight, "H", uint16(1000), "Height of the emulated terminal", "uint16"},
196+
// localhost is the default host for the server. Port is ignored during matching.
197+
{FlagAllowedHosts, "a", []string{"localhost", "127.0.0.1", "[::1]"}, "HTTP allowed hosts (hostnames only, no ports). Use '*' for all, comma-separated list via flag, space-separated list via AGENTAPI_ALLOWED_HOSTS env var", "stringSlice"},
198+
// localhost:3284 is the default origin when you open the chat interface in your browser. localhost:3000 and 3001 are used during development.
199+
{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"},
184200
}
185201

186202
for _, spec := range flagSpecs {
@@ -193,6 +209,8 @@ func CreateServerCmd() *cobra.Command {
193209
serverCmd.Flags().BoolP(spec.name, spec.shorthand, spec.defaultValue.(bool), spec.usage)
194210
case "uint16":
195211
serverCmd.Flags().Uint16P(spec.name, spec.shorthand, spec.defaultValue.(uint16), spec.usage)
212+
case "stringSlice":
213+
serverCmd.Flags().StringSliceP(spec.name, spec.shorthand, spec.defaultValue.([]string), spec.usage)
196214
default:
197215
panic(fmt.Sprintf("unknown flag type: %s", spec.flagType))
198216
}
@@ -201,6 +219,14 @@ func CreateServerCmd() *cobra.Command {
201219
}
202220
}
203221

222+
serverCmd.Flags().Bool(FlagExit, false, "Exit immediately after parsing arguments")
223+
if err := serverCmd.Flags().MarkHidden(FlagExit); err != nil {
224+
panic(fmt.Sprintf("failed to mark flag %s as hidden: %v", FlagExit, err))
225+
}
226+
if err := viper.BindPFlag(FlagExit, serverCmd.Flags().Lookup(FlagExit)); err != nil {
227+
panic(fmt.Sprintf("failed to bind flag %s: %v", FlagExit, err))
228+
}
229+
204230
viper.SetEnvPrefix("AGENTAPI")
205231
viper.AutomaticEnv()
206232
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))

0 commit comments

Comments
 (0)