feat: add --chat-hook-allow-insecure to allow plain HTTP chat hook URLs (#27896)

Adds a hidden `--chat-hook-allow-insecure` /
`CODER_CHAT_HOOK_ALLOW_INSECURE` deployment option (default `false`)
that allows the chat lifecycle hook URL to use plain HTTP for any host.

The HTTPS requirement is enforced at two points, and the flag relaxes
both: `DeploymentValues.Validate()` rejects `http` hook URLs at startup,
and the hook dispatcher's `validateHookURL` allows `http` only for
loopback hosts. With the flag set, any-host `http` is accepted; the
host, fragment/userinfo, secret, and timeout checks are unchanged, and
non-http(s) schemes still fail. This removes the need for an HTTPS
reverse proxy when testing a hook consumer on a trusted network.

Following security review feedback, the flag description and docs state
that plain HTTP lets an on-path attacker forge hook responses (which
control agent execution), and `coder server` logs a startup warning
(with a redacted hook URL) when hooks run over plain HTTP.

Docs, generated API types, and the server config golden are updated
accordingly.

> Mux acted on Mike's behalf to create this PR.
This commit is contained in:
Michael Suchacz
2026-08-05 22:41:17 +02:00
committed by GitHub
parent 3e2a8bd421
commit 4b9880afa6
16 changed files with 161 additions and 41 deletions
+8 -2
View File
@@ -803,8 +803,9 @@ chat:
# opt-in settings.
# (default: false, type: bool)
debugLoggingEnabled: false
# HTTPS URL to receive chat agent lifecycle hook events. Hooks are disabled when
# unset. Requires the agent-lifecycle-hooks experiment.
# HTTPS URL to receive chat agent lifecycle hook events (plain HTTP requires
# --chat-hook-allow-insecure). Hooks are disabled when unset. Requires the
# agent-lifecycle-hooks experiment.
# (default: <unset>, type: url)
hookURL:
# Maximum time to wait for a chat agent lifecycle hook response.
@@ -814,6 +815,11 @@ chat:
# Requires the agent-lifecycle-hooks experiment.
# (default: true, type: bool)
hookEnabled: true
# Allow the chat hook URL to use plain HTTP for any host. Plain HTTP exposes
# sensitive chat data and lets an on-path attacker forge hook responses that
# control agent execution, so only enable this on a network you fully trust.
# (default: false, type: bool)
hookAllowInsecure: false
# Deprecated: AI Gateway routing is now the only routing path. Setting this value
# has no effect. This option will be removed in a future release.
# (default: true, type: bool)
+3
View File
@@ -17407,6 +17407,9 @@ const docTemplate = `{
"debug_logging_enabled": {
"type": "boolean"
},
"hook_allow_insecure": {
"type": "boolean"
},
"hook_enabled": {
"type": "boolean"
},
+3
View File
@@ -15644,6 +15644,9 @@
"debug_logging_enabled": {
"type": "boolean"
},
"hook_allow_insecure": {
"type": "boolean"
},
"hook_enabled": {
"type": "boolean"
},
+6
View File
@@ -894,10 +894,16 @@ func New(options *Options) *API {
)
}
if hooksConfigured && hooksExperimentEnabled {
if chatConfig.HookAllowInsecure.Value() && chatConfig.HookURL.Value().Scheme == "http" {
options.Logger.Warn(ctx, "chat hooks use a plain HTTP URL; hook traffic is unencrypted and hook responses controlling agent execution can be forged on the network",
slog.F("hook_url", mcpclient.RedactURL(chatConfig.HookURL.String())),
)
}
hookDispatcher = dispatch.New(
options.Logger,
nil,
chatConfig.HookURL.String(),
chatConfig.HookAllowInsecure.Value(),
chatConfig.HookSecret.Value(),
chatConfig.HookTimeout.Value(),
api.DeploymentID,
+10 -4
View File
@@ -107,9 +107,11 @@ type Dispatcher struct {
}
// validateHookURL requires HTTPS because hook traffic carries sensitive data
// and authorization tokens, and responses can control execution. Plain HTTP
// is allowed only for loopback development consumers.
func validateHookURL(raw string) error {
// and authorization tokens, and responses can control execution. Loopback HTTP
// is allowed by default; allowInsecure permits HTTP for any host.
//
//nolint:revive // allowInsecure is operator configuration, not caller control coupling.
func validateHookURL(raw string, allowInsecure bool) error {
if raw == "" {
return nil
}
@@ -137,6 +139,9 @@ func validateHookURL(raw string) error {
if host == "" {
return xerrors.New("chat hook URL must include a host")
}
if allowInsecure {
return nil
}
if host == "localhost" {
return nil
}
@@ -155,6 +160,7 @@ func New(
logger slog.Logger,
client *http.Client,
hookURL string,
allowInsecureURL bool,
secret string,
timeout time.Duration,
deploymentID string,
@@ -174,7 +180,7 @@ func New(
logger: logger.Named("chat_hook_dispatcher"),
client: client,
hookURL: hookURL,
hookURLErr: validateHookURL(hookURL),
hookURLErr: validateHookURL(hookURL, allowInsecureURL),
secret: []byte(secret),
timeout: timeout,
deploymentID: deploymentID,
@@ -78,18 +78,25 @@ func TestDispatcherRejectsCleartextURL(t *testing.T) {
_, _, err := dispatcher.Dispatch(testutil.Context(t, testutil.WaitShort), event)
require.ErrorContains(t, err, "must use HTTPS")
require.NoError(t, validateHookURL(""))
require.NoError(t, validateHookURL("https://hooks.example.com/coder"))
require.NoError(t, validateHookURL("http://localhost:8080/hooks"))
require.NoError(t, validateHookURL("http://127.0.0.1:8080/hooks"))
require.NoError(t, validateHookURL("http://[::1]:8080/hooks"))
require.Error(t, validateHookURL("http://10.0.0.5/hooks"))
require.Error(t, validateHookURL("ftp://hooks.example.com/coder"))
require.ErrorContains(t, validateHookURL("https:///coder"), "must include a host")
require.ErrorContains(t, validateHookURL("https:hooks.example.com"), "must include a host")
require.ErrorContains(t, validateHookURL("http:///hooks"), "must include a host")
require.ErrorContains(t, validateHookURL("https://hooks.example.com/coder#frag"), "must not contain a fragment")
require.ErrorContains(t, validateHookURL("https://user:pass@hooks.example.com/coder"), "must not contain userinfo")
require.NoError(t, validateHookURL("", false))
require.NoError(t, validateHookURL("https://hooks.example.com/coder", false))
require.NoError(t, validateHookURL("http://localhost:8080/hooks", false))
require.NoError(t, validateHookURL("http://127.0.0.1:8080/hooks", false))
require.NoError(t, validateHookURL("http://[::1]:8080/hooks", false))
require.Error(t, validateHookURL("http://10.0.0.5/hooks", false))
require.Error(t, validateHookURL("ftp://hooks.example.com/coder", false))
require.ErrorContains(t, validateHookURL("https:///coder", false), "must include a host")
require.ErrorContains(t, validateHookURL("https:hooks.example.com", false), "must include a host")
require.ErrorContains(t, validateHookURL("http:///hooks", false), "must include a host")
require.ErrorContains(t, validateHookURL("https://hooks.example.com/coder#frag", false), "must not contain a fragment")
require.ErrorContains(t, validateHookURL("https://user:pass@hooks.example.com/coder", false), "must not contain userinfo")
require.NoError(t, validateHookURL("http://10.0.0.5/hooks", true))
require.NoError(t, validateHookURL("http://hooks.example.com/coder", true))
require.Error(t, validateHookURL("ftp://hooks.example.com/coder", true))
require.ErrorContains(t, validateHookURL("http:///hooks", true), "must include a host")
require.ErrorContains(t, validateHookURL("http://hooks.example.com/coder#frag", true), "must not contain a fragment")
require.ErrorContains(t, validateHookURL("http://user:pass@hooks.example.com/coder", true), "must not contain userinfo")
}
func TestDispatcherDeny(t *testing.T) {
@@ -566,7 +573,7 @@ func TestDispatcherRejectedResponseIsNotObserved(t *testing.T) {
registry := prometheus.NewRegistry()
dispatcher := New(
testutil.Logger(t), server.Client(), server.URL, testSecret, time.Second,
testutil.Logger(t), server.Client(), server.URL, false, testSecret, time.Second,
testDeploymentID, testVersion, registry,
)
_, _, err := dispatcher.Dispatch(testutil.Context(t, testutil.WaitLong), event)
@@ -655,6 +662,7 @@ func newTestDispatcher(
testutil.Logger(t),
client,
hookURL,
false,
testSecret,
timeout,
testDeploymentID,
@@ -738,7 +746,7 @@ func TestDispatcherAdmissionReserve(t *testing.T) {
t.Cleanup(server.Close)
dispatcher := New(
testutil.Logger(t), server.Client(), server.URL, testSecret, testutil.WaitShort,
testutil.Logger(t), server.Client(), server.URL, false, testSecret, testutil.WaitShort,
testDeploymentID, testVersion, prometheus.NewRegistry(),
)
event := newTestEvent(t, agenthooks.EventUserPromptSubmit, agenthooks.UserPromptSubmitData{Prompt: "hi"})
@@ -797,7 +805,7 @@ func TestDispatcherAdmissionReserve(t *testing.T) {
t.Cleanup(server.Close)
dispatcher := New(
testutil.Logger(t), server.Client(), server.URL, testSecret, testutil.WaitShort,
testutil.Logger(t), server.Client(), server.URL, false, testSecret, testutil.WaitShort,
testDeploymentID, testVersion, prometheus.NewRegistry(),
)
fill(t, dispatcher.admission, maxAdmissionDispatches)
@@ -51,6 +51,7 @@ func TestSessionStartDispatchSources(t *testing.T) {
slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}),
consumer.Client(),
consumer.URL,
false,
secret,
time.Second,
"test-deployment",
@@ -102,6 +103,7 @@ func newTestTrigger(t *testing.T, handler http.Handler) *Trigger {
slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}),
consumer.Client(),
consumer.URL,
false,
"test-hook-secret-32-bytes-minimum!!",
time.Second,
"test-deployment",
+1
View File
@@ -59,6 +59,7 @@ func TestSessionStartDispatchFailureFinishesGeneration(t *testing.T) {
slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}),
consumer.Client(),
consumer.URL,
false,
"test-hook-secret-32-bytes-minimum!!",
time.Second,
"test-deployment",
+1
View File
@@ -121,6 +121,7 @@ func newHookDispatcher(t *testing.T, _ database.Store, consumer *httptest.Server
slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}),
consumer.Client(),
consumer.URL,
false,
"test-hook-secret-32-bytes-minimum!!",
time.Second,
"test-deployment",
+1
View File
@@ -298,6 +298,7 @@ func TestCreateChildSubagentChatDispatchesUserPromptSubmit(t *testing.T) {
slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}),
consumer.Client(),
consumer.URL,
false,
"test-hook-secret-32-bytes-minimum!!",
time.Second,
"test-deployment",
+25 -6
View File
@@ -4334,7 +4334,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "Chat: Hook URL",
Description: "HTTPS URL to receive chat agent lifecycle hook events. Hooks are disabled when unset. Requires the agent-lifecycle-hooks experiment.",
Description: "HTTPS URL to receive chat agent lifecycle hook events (plain HTTP requires --chat-hook-allow-insecure). Hooks are disabled when unset. Requires the agent-lifecycle-hooks experiment.",
Flag: "chat-hook-url",
Hidden: true,
Env: "CODER_CHAT_HOOK_URL",
@@ -4377,6 +4377,17 @@ Write out the current server config as YAML to stdout.`,
Group: &deploymentGroupChat,
YAML: "hookEnabled",
},
{
Name: "Chat: Hook Allow Insecure",
Description: "Allow the chat hook URL to use plain HTTP for any host. Plain HTTP exposes sensitive chat data and lets an on-path attacker forge hook responses that control agent execution, so only enable this on a network you fully trust.",
Flag: "chat-hook-allow-insecure",
Hidden: true,
Env: "CODER_CHAT_HOOK_ALLOW_INSECURE",
Value: &c.AI.Chat.HookAllowInsecure,
Default: "false",
Group: &deploymentGroupChat,
YAML: "hookAllowInsecure",
},
{
Name: "Chat: AI Gateway Routing Enabled",
Description: "Deprecated: AI Gateway routing is now the only routing path. Setting this value has no effect. This option will be removed in a future release.",
@@ -5065,6 +5076,7 @@ type ChatConfig struct {
HookSecret serpent.String `json:"hook_secret" typescript:",notnull"`
HookTimeout serpent.Duration `json:"hook_timeout" typescript:",notnull"`
HookEnabled serpent.Bool `json:"hook_enabled" typescript:",notnull"`
HookAllowInsecure serpent.Bool `json:"hook_allow_insecure" typescript:",notnull"`
// Deprecated: AI Gateway routing is now the only routing path. Setting this
// value has no effect. This option will be removed in a future release.
AIGatewayRoutingEnabled serpent.Bool `json:"ai_gateway_routing_enabled" typescript:",notnull" swaggerignore:"true"`
@@ -5119,17 +5131,24 @@ func (c *DeploymentValues) Validate() error {
if c.AI.Chat.HookEnabled.Value() {
if c.AI.Chat.HookURL.String() != "" {
hookURL := c.AI.Chat.HookURL.Value()
if hookURL.Scheme != "https" {
return xerrors.New("chat hook URL must use HTTPS; set --chat-hook-url to an HTTPS URL")
allowInsecure := c.AI.Chat.HookAllowInsecure.Value()
switch {
case hookURL.Scheme == "https":
case hookURL.Scheme == "http" && allowInsecure:
default:
return xerrors.New("chat hook URL must use HTTPS; set --chat-hook-url to an HTTPS URL, or set --chat-hook-allow-insecure to allow plain HTTP")
}
if hookURL.Host == "" {
return xerrors.New("chat hook URL must include a host; set --chat-hook-url to a complete HTTPS URL")
// Hostname() instead of Host: a URL like http://:8080/hooks has a
// non-empty Host (":8080") but no hostname, and the dispatcher
// rejects it on every dispatch.
if hookURL.Hostname() == "" {
return xerrors.New("chat hook URL must include a host; set --chat-hook-url to a complete URL")
}
// The configured string is signed verbatim as the JWT audience,
// and neither component is ever transmitted, so a consumer
// configured with the URL it actually serves would never match.
if hookURL.Fragment != "" || hookURL.RawFragment != "" || hookURL.User != nil {
return xerrors.New("chat hook URL must not contain a fragment or userinfo; set --chat-hook-url to a plain HTTPS URL")
return xerrors.New("chat hook URL must not contain a fragment or userinfo; set --chat-hook-url to a URL without a fragment or userinfo")
}
if c.AI.Chat.HookSecret.Value() == "" {
return xerrors.New("chat hook secret is required when chat hook URL is set; set --chat-hook-secret")
+61 -6
View File
@@ -791,12 +791,13 @@ func TestDeploymentValues_Validate_ChatHooks(t *testing.T) {
t.Parallel()
tests := []struct {
name string
disabled bool
url string
secret string
timeout time.Duration
wantErr string
name string
disabled bool
url string
secret string
timeout time.Duration
allowInsecure bool
wantErr string
}{
{
name: "NoURL",
@@ -821,6 +822,28 @@ func TestDeploymentValues_Validate_ChatHooks(t *testing.T) {
timeout: 1500 * time.Millisecond,
wantErr: "chat hook URL must use HTTPS",
},
{
name: "HTTPURLAllowInsecure",
url: "http://hooks.example.com/agent",
secret: "0123456789abcdef0123456789abcdef",
timeout: 1500 * time.Millisecond,
allowInsecure: true,
},
{
name: "NonHTTPSchemeAllowInsecure",
url: "ftp://hooks.example.com/agent",
secret: "0123456789abcdef0123456789abcdef",
timeout: 1500 * time.Millisecond,
allowInsecure: true,
wantErr: "chat hook URL must use HTTPS",
},
{
name: "AllowInsecureStillRequiresSecret",
url: "http://hooks.example.com/agent",
timeout: 1500 * time.Millisecond,
allowInsecure: true,
wantErr: "chat hook secret is required",
},
{
name: "HostlessURL",
url: "https:///hook",
@@ -828,6 +851,29 @@ func TestDeploymentValues_Validate_ChatHooks(t *testing.T) {
timeout: 1500 * time.Millisecond,
wantErr: "must include a host",
},
{
name: "HostlessHTTPURLAllowInsecure",
url: "http:///hook",
secret: "0123456789abcdef0123456789abcdef",
timeout: 1500 * time.Millisecond,
allowInsecure: true,
wantErr: "set --chat-hook-url to a complete URL",
},
{
name: "PortOnlyHTTPURLAllowInsecure",
url: "http://:8080/hooks",
secret: "0123456789abcdef0123456789abcdef",
timeout: 1500 * time.Millisecond,
allowInsecure: true,
wantErr: "must include a host",
},
{
name: "PortOnlyHTTPSURL",
url: "https://:8080/hooks",
secret: "0123456789abcdef0123456789abcdef",
timeout: 1500 * time.Millisecond,
wantErr: "must include a host",
},
{
name: "FragmentURL",
url: "https://hooks.example.com/agent#frag",
@@ -835,6 +881,14 @@ func TestDeploymentValues_Validate_ChatHooks(t *testing.T) {
timeout: 1500 * time.Millisecond,
wantErr: "must not contain a fragment or userinfo",
},
{
name: "FragmentHTTPURLAllowInsecure",
url: "http://hooks.example.com/agent#frag",
secret: "0123456789abcdef0123456789abcdef",
timeout: 1500 * time.Millisecond,
allowInsecure: true,
wantErr: "set --chat-hook-url to a URL without a fragment or userinfo",
},
{
name: "UserinfoURL",
url: "https://user:pass@hooks.example.com/agent",
@@ -892,6 +946,7 @@ func TestDeploymentValues_Validate_ChatHooks(t *testing.T) {
dv.AI.Chat.HookEnabled = serpent.Bool(!tt.disabled)
dv.AI.Chat.HookSecret = serpent.String(tt.secret)
dv.AI.Chat.HookTimeout = serpent.Duration(tt.timeout)
dv.AI.Chat.HookAllowInsecure = serpent.Bool(tt.allowInsecure)
if tt.url != "" {
require.NoError(t, dv.AI.Chat.HookURL.Set(tt.url))
}
+10 -8
View File
@@ -29,12 +29,13 @@ The experiment list is read at startup, so enabling or disabling it requires a `
Set the following deployment options on `coder server`.
| Environment variable | CLI flag | Default | Requirement |
|---------------------------|-----------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------|
| `CODER_CHAT_HOOK_URL` | `--chat-hook-url` | Empty | Use an `https` URL. Hooks are inactive when this value is empty. |
| `CODER_CHAT_HOOK_SECRET` | `--chat-hook-secret` | Empty | Required when the hook URL is set, at least 32 bytes of cryptographically random data. Coder uses this shared secret to sign HS256 JWTs. |
| `CODER_CHAT_HOOK_TIMEOUT` | `--chat-hook-timeout` | `1.5s` | Must be greater than `0` and no more than `5s`. The timeout applies to each request. |
| `CODER_CHAT_HOOK_ENABLED` | `--chat-hook-enabled` | `true` | Set to `false` to stop dispatching without removing the URL or secret. |
| Environment variable | CLI flag | Default | Requirement |
|----------------------------------|------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `CODER_CHAT_HOOK_URL` | `--chat-hook-url` | Empty | Use an `https` URL, or an `http` URL with `CODER_CHAT_HOOK_ALLOW_INSECURE`. Hooks are inactive when this value is empty. |
| `CODER_CHAT_HOOK_SECRET` | `--chat-hook-secret` | Empty | Required when the hook URL is set, at least 32 bytes of cryptographically random data. Coder uses this shared secret to sign HS256 JWTs. |
| `CODER_CHAT_HOOK_TIMEOUT` | `--chat-hook-timeout` | `1.5s` | Must be greater than `0` and no more than `5s`. The timeout applies to each request. |
| `CODER_CHAT_HOOK_ENABLED` | `--chat-hook-enabled` | `true` | Set to `false` to stop dispatching without removing the URL or secret. |
| `CODER_CHAT_HOOK_ALLOW_INSECURE` | `--chat-hook-allow-insecure` | `false` | Set to `true` to allow a plain `http` hook URL. Plain HTTP lets an attacker on the network forge hook responses, so only enable it on a network you fully trust. |
Treat `CODER_CHAT_HOOK_ENABLED=false` as the break-glass control.
Changing deployment options requires the normal `coder server` configuration rollout for your installation.
@@ -42,7 +43,8 @@ Changing deployment options requires the normal `coder server` configuration rol
Use a dedicated secret and rotate it through your existing secret-management process.
Rotation is a hard cutover: Coder signs with exactly one secret, so dispatches fail until the consumer accepts the new value.
Rotate during a maintenance window, or temporarily set `CODER_CHAT_HOOK_ENABLED=false` for the cutover if blocked chats are worse than unreviewed ones for your deployment.
Coder requires the configured URL to use HTTPS.
Coder requires the configured URL to use HTTPS unless `CODER_CHAT_HOOK_ALLOW_INSECURE` is set.
Plain HTTP removes more than transport privacy: hook responses are what allow or deny tool calls and can rewrite prompts and tool inputs, so anyone on the network path can forge them. Coder logs a warning at startup when hooks run over plain HTTP.
A TLS terminator can forward the request to a consumer over plain HTTP on a trusted local network.
Configure the consumer with the same `CODER_CHAT_HOOK_URL` value, because that URL is the audience Coder signs into every dispatch.
The consumer compares the `aud` claim against its configured audience and rejects a mismatch.
@@ -233,7 +235,7 @@ Agent hooks server listening on 127.0.0.1:8081 in log-only mode
```
The reference server accepts optional TLS certificate and key paths.
For local testing with plain HTTP, place an HTTPS reverse proxy in front of it because `CODER_CHAT_HOOK_URL` accepts only HTTPS URLs, and pass the proxy's URL as `--audience`.
For local testing with plain HTTP, either set `CODER_CHAT_HOOK_ALLOW_INSECURE=true` and use the `http` URL directly, or place an HTTPS reverse proxy in front of the consumer and pass the proxy's URL as `--audience`.
Run `go run ./scripts/agenthooks-server --help` for all flags and environment variable names.
## Audit dispatches
+1
View File
@@ -233,6 +233,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"chat": {
"acquire_batch_size": 0,
"debug_logging_enabled": true,
"hook_allow_insecure": true,
"hook_enabled": true,
"hook_secret": "string",
"hook_timeout": 0,
+5
View File
@@ -1082,6 +1082,7 @@
"chat": {
"acquire_batch_size": 0,
"debug_logging_enabled": true,
"hook_allow_insecure": true,
"hook_enabled": true,
"hook_secret": "string",
"hook_timeout": 0,
@@ -2455,6 +2456,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
{
"acquire_batch_size": 0,
"debug_logging_enabled": true,
"hook_allow_insecure": true,
"hook_enabled": true,
"hook_secret": "string",
"hook_timeout": 0,
@@ -2480,6 +2482,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|-------------------------|----------------------------|----------|--------------|-------------|
| `acquire_batch_size` | integer | false | | |
| `debug_logging_enabled` | boolean | false | | |
| `hook_allow_insecure` | boolean | false | | |
| `hook_enabled` | boolean | false | | |
| `hook_secret` | string | false | | |
| `hook_timeout` | integer | false | | |
@@ -5921,6 +5924,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
"chat": {
"acquire_batch_size": 0,
"debug_logging_enabled": true,
"hook_allow_insecure": true,
"hook_enabled": true,
"hook_secret": "string",
"hook_timeout": 0,
@@ -6547,6 +6551,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
"chat": {
"acquire_batch_size": 0,
"debug_logging_enabled": true,
"hook_allow_insecure": true,
"hook_enabled": true,
"hook_secret": "string",
"hook_timeout": 0,
+1
View File
@@ -2037,6 +2037,7 @@ export interface ChatConfig {
readonly hook_secret: string;
readonly hook_timeout: number;
readonly hook_enabled: boolean;
readonly hook_allow_insecure: boolean;
/**
* @deprecated AI Gateway routing is now the only routing path. Setting this
* value has no effect. This option will be removed in a future release.