mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-11 01:13:02 +03:00
Compare commits
1 Commits
fix/12734-
...
fix/12568-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b87c92b3d7 |
24
.env.example
24
.env.example
@@ -124,6 +124,21 @@ DISABLE_SQLITE_AUTO_BACKUP=false
|
||||
# Host port for the compose Redis sidecar. Default: 6379.
|
||||
# REDIS_PORT=6379
|
||||
|
||||
# Host interface docker-compose publishes the app's own ports (dashboard,
|
||||
# API, live-WS) on for the base/web/cli/host profiles and docker-compose.prod.yml.
|
||||
# Default: 127.0.0.1 (loopback only). Combined with REQUIRE_API_KEY=false
|
||||
# (the default below), an unqualified publish spec would expose the anonymous
|
||||
# /v1 LLM proxy to your whole LAN/WAN. Only set this to 0.0.0.0 once you've
|
||||
# confirmed REQUIRE_API_KEY=true, or that a reverse proxy in front of this
|
||||
# instance already enforces its own authentication. (#12568)
|
||||
# APP_BIND_HOST=127.0.0.1
|
||||
# Host interface docker-compose publishes the Qdrant memory sidecar on.
|
||||
# Default: 127.0.0.1 (loopback only). Same LAN-exposure reasoning as Redis.
|
||||
# QDRANT_BIND_HOST=127.0.0.1
|
||||
# Host interface docker-compose publishes the Bifrost router sidecar on.
|
||||
# Default: 127.0.0.1 (loopback only). Same LAN-exposure reasoning as Redis.
|
||||
# BIFROST_BIND_HOST=127.0.0.1
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 3. NETWORK & PORTS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -373,6 +388,8 @@ AUTH_COOKIE_SECURE=false
|
||||
# Require an API key for all /v1/* proxy endpoints.
|
||||
# Used by: API middleware — rejects unauthenticated requests to the proxy API.
|
||||
# Default: false | Set true for multi-user/public deployments.
|
||||
# Leaving this false is only safe when the app is reachable on loopback only
|
||||
# (see APP_BIND_HOST above) or sits behind a reverse proxy doing its own auth.
|
||||
REQUIRE_API_KEY=false
|
||||
|
||||
# Allow revealing full API key values in the Dashboard UI.
|
||||
@@ -2077,6 +2094,13 @@ APP_LOG_TO_FILE=true
|
||||
# Management key for an externally managed instance. Embedded instances use
|
||||
# OmniRoute's encrypted service key.
|
||||
# CLIPROXYAPI_MANAGEMENT_KEY=
|
||||
# Host interface docker-compose publishes the cliproxyapi sidecar on (the
|
||||
# --profile cliproxyapi Docker service, port 8317). Default: 127.0.0.1
|
||||
# (loopback only) — its data volume holds provider OAuth/API credentials, and
|
||||
# the pinned image has no env-based data-plane api-keys override (only a
|
||||
# mounted config.yaml), so an unqualified publish spec would put a
|
||||
# credential-bearing service on your whole LAN. (#12578)
|
||||
# CLIPROXY_BIND_HOST=127.0.0.1
|
||||
|
||||
# ── Mux embedded service ──
|
||||
# Override the port where the embedded Mux (coder/mux) agent-orchestration
|
||||
|
||||
212
@omniroute/opencode-plugin-v2/package-lock.json
generated
212
@omniroute/opencode-plugin-v2/package-lock.json
generated
@@ -22,7 +22,7 @@
|
||||
"node": ">=22.22.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opencode-ai/plugin": ">=1.18.29 <2"
|
||||
"@opencode-ai/plugin": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/provider": {
|
||||
@@ -39,9 +39,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -56,9 +56,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -73,9 +73,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -90,9 +90,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -107,9 +107,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -124,9 +124,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -141,9 +141,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -158,9 +158,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -175,9 +175,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -192,9 +192,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -209,9 +209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -226,9 +226,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
|
||||
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -243,9 +243,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
|
||||
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -260,9 +260,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -277,9 +277,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
|
||||
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -294,9 +294,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
|
||||
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -311,9 +311,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
|
||||
"version": "0.28.1",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -328,9 +326,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -345,9 +343,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -362,9 +360,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -379,9 +377,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -396,9 +394,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -413,9 +411,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -430,9 +428,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -447,9 +445,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -464,9 +462,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1182,9 +1180,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
|
||||
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
|
||||
"version": "0.28.1",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -1195,32 +1191,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.2",
|
||||
"@esbuild/android-arm": "0.28.2",
|
||||
"@esbuild/android-arm64": "0.28.2",
|
||||
"@esbuild/android-x64": "0.28.2",
|
||||
"@esbuild/darwin-arm64": "0.28.2",
|
||||
"@esbuild/darwin-x64": "0.28.2",
|
||||
"@esbuild/freebsd-arm64": "0.28.2",
|
||||
"@esbuild/freebsd-x64": "0.28.2",
|
||||
"@esbuild/linux-arm": "0.28.2",
|
||||
"@esbuild/linux-arm64": "0.28.2",
|
||||
"@esbuild/linux-ia32": "0.28.2",
|
||||
"@esbuild/linux-loong64": "0.28.2",
|
||||
"@esbuild/linux-mips64el": "0.28.2",
|
||||
"@esbuild/linux-ppc64": "0.28.2",
|
||||
"@esbuild/linux-riscv64": "0.28.2",
|
||||
"@esbuild/linux-s390x": "0.28.2",
|
||||
"@esbuild/linux-x64": "0.28.2",
|
||||
"@esbuild/netbsd-arm64": "0.28.2",
|
||||
"@esbuild/netbsd-x64": "0.28.2",
|
||||
"@esbuild/openbsd-arm64": "0.28.2",
|
||||
"@esbuild/openbsd-x64": "0.28.2",
|
||||
"@esbuild/openharmony-arm64": "0.28.2",
|
||||
"@esbuild/sunos-x64": "0.28.2",
|
||||
"@esbuild/win32-arm64": "0.28.2",
|
||||
"@esbuild/win32-ia32": "0.28.2",
|
||||
"@esbuild/win32-x64": "0.28.2"
|
||||
"@esbuild/aix-ppc64": "0.28.1",
|
||||
"@esbuild/android-arm": "0.28.1",
|
||||
"@esbuild/android-arm64": "0.28.1",
|
||||
"@esbuild/android-x64": "0.28.1",
|
||||
"@esbuild/darwin-arm64": "0.28.1",
|
||||
"@esbuild/darwin-x64": "0.28.1",
|
||||
"@esbuild/freebsd-arm64": "0.28.1",
|
||||
"@esbuild/freebsd-x64": "0.28.1",
|
||||
"@esbuild/linux-arm": "0.28.1",
|
||||
"@esbuild/linux-arm64": "0.28.1",
|
||||
"@esbuild/linux-ia32": "0.28.1",
|
||||
"@esbuild/linux-loong64": "0.28.1",
|
||||
"@esbuild/linux-mips64el": "0.28.1",
|
||||
"@esbuild/linux-ppc64": "0.28.1",
|
||||
"@esbuild/linux-riscv64": "0.28.1",
|
||||
"@esbuild/linux-s390x": "0.28.1",
|
||||
"@esbuild/linux-x64": "0.28.1",
|
||||
"@esbuild/netbsd-arm64": "0.28.1",
|
||||
"@esbuild/netbsd-x64": "0.28.1",
|
||||
"@esbuild/openbsd-arm64": "0.28.1",
|
||||
"@esbuild/openbsd-x64": "0.28.1",
|
||||
"@esbuild/openharmony-arm64": "0.28.1",
|
||||
"@esbuild/sunos-x64": "0.28.1",
|
||||
"@esbuild/win32-arm64": "0.28.1",
|
||||
"@esbuild/win32-ia32": "0.28.1",
|
||||
"@esbuild/win32-x64": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
|
||||
6
@omniroute/opencode-plugin/package-lock.json
generated
6
@omniroute/opencode-plugin/package-lock.json
generated
@@ -1745,9 +1745,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/toml": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/toml/-/toml-4.3.0.tgz",
|
||||
"integrity": "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz",
|
||||
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
1
changelog.d/fixes/12568-compose-loopback-bind.md
Normal file
1
changelog.d/fixes/12568-compose-loopback-bind.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(docker): default docker-compose app ports (dashboard/API/live-WS) to loopback instead of `0.0.0.0`, closing the anonymous `/v1` LAN/WAN exposure gap left open by `REQUIRE_API_KEY=false` (#12568)
|
||||
1
changelog.d/fixes/12578-cliproxyapi-loopback-bind.md
Normal file
1
changelog.d/fixes/12578-cliproxyapi-loopback-bind.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(docker): scope the cliproxyapi/qdrant/bifrost sidecars to loopback by default and forward `CLIPROXYAPI_MANAGEMENT_KEY` into the cliproxyapi container so its management API is not left both unauthenticated and LAN-published (#12578)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(cache): fold tool_choice/tools/response_format into the semantic cache signature so a cached tool_calls response can no longer be replayed for a request whose tool policy forbids it (#12734)
|
||||
@@ -63,17 +63,22 @@ services:
|
||||
- DASHBOARD_PORT=${DASHBOARD_PORT:-${PORT:-20128}}
|
||||
- API_PORT=${API_PORT:-20129}
|
||||
- LIVE_WS_PORT=${LIVE_WS_PORT:-20132}
|
||||
- LIVE_WS_HOST=${LIVE_WS_HOST:-0.0.0.0}
|
||||
- LIVE_WS_HOST=${LIVE_WS_HOST:-127.0.0.1}
|
||||
- LIVE_WS_ALLOWED_ORIGINS=${LIVE_WS_ALLOWED_ORIGINS:-http://localhost:${PROD_DASHBOARD_PORT:-20130},http://127.0.0.1:${PROD_DASHBOARD_PORT:-20130}}
|
||||
- API_HOST=${API_HOST:-0.0.0.0}
|
||||
- HOSTNAME=0.0.0.0
|
||||
- API_HOST=${API_HOST:-127.0.0.1}
|
||||
# HOSTNAME intentionally not hardcoded to 0.0.0.0 (#12568) — let the
|
||||
# app's own loopback-first default apply unless the operator sets it.
|
||||
- DATA_DIR=/app/data
|
||||
- OMNIROUTE_BASE_PATH=${OMNIROUTE_BASE_PATH:-}
|
||||
- CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
|
||||
ports:
|
||||
- "${PROD_DASHBOARD_PORT:-20130}:${DASHBOARD_PORT:-${PORT:-20128}}"
|
||||
- "${PROD_API_PORT:-20131}:${API_PORT:-20129}"
|
||||
- "${PROD_LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
||||
# Loopback-only by default (#12568) — see docker-compose.yml's
|
||||
# APP_BIND_HOST comment for the rationale. Override for a LAN/WAN prod
|
||||
# deployment only once REQUIRE_API_KEY=true or a reverse proxy in front
|
||||
# of this instance is confirmed to enforce its own auth.
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${PROD_DASHBOARD_PORT:-20130}:${DASHBOARD_PORT:-${PORT:-20128}}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${PROD_API_PORT:-20131}:${API_PORT:-20129}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${PROD_LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
||||
volumes:
|
||||
- omniroute-prod-data:/app/data
|
||||
healthcheck:
|
||||
|
||||
@@ -37,9 +37,9 @@ x-common: &common
|
||||
- PORT=${PORT:-20128}
|
||||
- DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
|
||||
- API_PORT=${API_PORT:-20129}
|
||||
- API_HOST=${API_HOST:-0.0.0.0}
|
||||
- API_HOST=${API_HOST:-127.0.0.1}
|
||||
- LIVE_WS_PORT=${LIVE_WS_PORT:-20132}
|
||||
- LIVE_WS_HOST=${LIVE_WS_HOST:-0.0.0.0}
|
||||
- LIVE_WS_HOST=${LIVE_WS_HOST:-127.0.0.1}
|
||||
- LIVE_WS_ALLOWED_ORIGINS=${LIVE_WS_ALLOWED_ORIGINS:-http://localhost:20128,http://127.0.0.1:20128}
|
||||
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
|
||||
- NODE_OPTIONS=--max-old-space-size=2048
|
||||
@@ -99,9 +99,14 @@ services:
|
||||
OMNIROUTE_BASE_PATH: ${OMNIROUTE_BASE_PATH:-}
|
||||
image: omniroute:base
|
||||
ports:
|
||||
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
- "${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
||||
# Loopback-only by default (#12568): with REQUIRE_API_KEY=false shipping
|
||||
# as the .env.example default, an unqualified publish spec here binds
|
||||
# 0.0.0.0 and exposes the anonymous /v1 LLM proxy on every LAN/WAN
|
||||
# interface. Set APP_BIND_HOST=0.0.0.0 only once you've confirmed
|
||||
# REQUIRE_API_KEY=true or an upstream reverse proxy enforces its own auth.
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
||||
profiles:
|
||||
- base
|
||||
|
||||
@@ -126,17 +131,17 @@ services:
|
||||
- PORT=${PORT:-20128}
|
||||
- DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
|
||||
- API_PORT=${API_PORT:-20129}
|
||||
- API_HOST=${API_HOST:-0.0.0.0}
|
||||
- API_HOST=${API_HOST:-127.0.0.1}
|
||||
- LIVE_WS_PORT=${LIVE_WS_PORT:-20132}
|
||||
- LIVE_WS_HOST=${LIVE_WS_HOST:-0.0.0.0}
|
||||
- LIVE_WS_HOST=${LIVE_WS_HOST:-127.0.0.1}
|
||||
- LIVE_WS_ALLOWED_ORIGINS=${LIVE_WS_ALLOWED_ORIGINS:-http://localhost:20128,http://127.0.0.1:20128}
|
||||
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
|
||||
- OMNIROUTE_BASE_PATH=${OMNIROUTE_BASE_PATH:-}
|
||||
- CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
|
||||
ports:
|
||||
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
- "${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
||||
profiles:
|
||||
- web
|
||||
|
||||
@@ -165,9 +170,9 @@ services:
|
||||
OMNIROUTE_BASE_PATH: ${OMNIROUTE_BASE_PATH:-}
|
||||
image: omniroute:cli
|
||||
ports:
|
||||
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
- "${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
# SECURITY: mounting the host Docker socket gives this container full
|
||||
@@ -194,17 +199,17 @@ services:
|
||||
OMNIROUTE_BASE_PATH: ${OMNIROUTE_BASE_PATH:-}
|
||||
image: omniroute:base
|
||||
ports:
|
||||
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
- "${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
- "${APP_BIND_HOST:-127.0.0.1}:${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
||||
environment:
|
||||
- DATA_DIR=/app/data
|
||||
- PORT=${PORT:-20128}
|
||||
- DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
|
||||
- API_PORT=${API_PORT:-20129}
|
||||
- API_HOST=${API_HOST:-0.0.0.0}
|
||||
- API_HOST=${API_HOST:-127.0.0.1}
|
||||
- LIVE_WS_PORT=${LIVE_WS_PORT:-20132}
|
||||
- LIVE_WS_HOST=${LIVE_WS_HOST:-0.0.0.0}
|
||||
- LIVE_WS_HOST=${LIVE_WS_HOST:-127.0.0.1}
|
||||
- LIVE_WS_ALLOWED_ORIGINS=${LIVE_WS_ALLOWED_ORIGINS:-http://localhost:20128,http://127.0.0.1:20128}
|
||||
- CLI_MODE=host
|
||||
- CLI_EXTRA_PATHS=/host-local/bin:/host-node/bin
|
||||
@@ -243,8 +248,8 @@ services:
|
||||
container_name: omniroute-qdrant
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${QDRANT_PORT:-6333}:6333"
|
||||
- "${QDRANT_GRPC_PORT:-6334}:6334"
|
||||
- "${QDRANT_BIND_HOST:-127.0.0.1}:${QDRANT_PORT:-6333}:6333"
|
||||
- "${QDRANT_BIND_HOST:-127.0.0.1}:${QDRANT_GRPC_PORT:-6334}:6334"
|
||||
volumes:
|
||||
- qdrant-data:/qdrant/storage
|
||||
environment:
|
||||
@@ -271,7 +276,7 @@ services:
|
||||
container_name: omniroute-bifrost
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${BIFROST_PORT:-8080}:8080"
|
||||
- "${BIFROST_BIND_HOST:-127.0.0.1}:${BIFROST_PORT:-8080}:8080"
|
||||
volumes:
|
||||
- bifrost-data:/data
|
||||
environment:
|
||||
@@ -294,12 +299,22 @@ services:
|
||||
image: docker.io/eceasy/cli-proxy-api:v6.9.7
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${CLIPROXYAPI_PORT:-8317}:${CLIPROXYAPI_PORT:-8317}"
|
||||
# Loopback-only by default: this sidecar's data volume
|
||||
# (cliproxiapi-data:/root/.cli-proxy-api) holds provider OAuth/API
|
||||
# credentials, and the pinned image only reads api-keys from a mounted
|
||||
# config.yaml (not env vars), so an unqualified "8317:8317" publish spec
|
||||
# would put a credential-bearing service with no compose-configured
|
||||
# data-plane auth on every LAN interface. Same reasoning as Redis above.
|
||||
- "${CLIPROXY_BIND_HOST:-127.0.0.1}:${CLIPROXYAPI_PORT:-8317}:${CLIPROXYAPI_PORT:-8317}"
|
||||
volumes:
|
||||
- cliproxyapi-data:/root/.cli-proxy-api
|
||||
environment:
|
||||
- PORT=${CLIPROXYAPI_PORT:-8317}
|
||||
- HOST=0.0.0.0
|
||||
# Forwards to the one auth-related env var the pinned binary actually
|
||||
# reads (MANAGEMENT_PASSWORD) — secures the management API only; the
|
||||
# data-plane completions endpoints have no env-based override upstream.
|
||||
- MANAGEMENT_PASSWORD=${CLIPROXYAPI_MANAGEMENT_KEY:-}
|
||||
healthcheck:
|
||||
test:
|
||||
["CMD", "wget", "--spider", "-q", "http://127.0.0.1:${CLIPROXYAPI_PORT:-8317}/v1/models"]
|
||||
|
||||
@@ -338,6 +338,8 @@ Beyond the defaults documented in [ENVIRONMENT.md](../reference/ENVIRONMENT.md),
|
||||
| `AUTO_UPDATE_HOST_REPO_DIR` | Host path mounted into `cli` profile at `/workspace/omniroute` for self-update workflows | `.` (current directory) |
|
||||
| `OMNIROUTE_MEMORY_MB` | Runtime Node heap ceiling for the Docker standalone server; overrides the image default above. Coding agents: `8192`+ (see [runtime RAM](#runtime-ram-for-coding-agents)). | `1024` |
|
||||
| `DASHBOARD_PORT` / `API_PORT` | Override exposed ports for dashboard (20128) and API (20129) | `20128` / `20129` |
|
||||
| `APP_BIND_HOST` | Host interface docker-compose publishes the dashboard/API/live-WS ports on. With `REQUIRE_API_KEY=false` (the default), `0.0.0.0` exposes the anonymous `/v1` proxy to the LAN — only widen with `REQUIRE_API_KEY=true` or a reverse proxy in front. | `127.0.0.1` |
|
||||
| `CLIPROXY_BIND_HOST` | Host interface docker-compose publishes the `cliproxyapi` sidecar on — its data volume holds provider credentials. | `127.0.0.1` |
|
||||
| `OMNIROUTE_PLUGINS_DIR` | Directory the runtime plugin scanner reads and installs into. Set it when plugins are bind-mounted: the default follows `HOME`, which an image need not export. | `~/.omniroute/plugins` |
|
||||
| `OMNIROUTE_BASE_PATH` | URL subpath when the app is published behind a reverse proxy (e.g. `/omniroute`) | _(empty = root)_ |
|
||||
| `NEXT_PUBLIC_BASE_URL` | Public browser origin including the subpath (e.g. `https://host/omniroute`) | unset |
|
||||
|
||||
@@ -1072,6 +1072,7 @@ desktop install.
|
||||
| `CLIPROXYAPI_API_KEY` | _(empty)_ | `open-sse/handlers/chatCore/cliproxyapiCredentials.ts` | Data-plane key fallback when the `cliproxyapi_api_key` setting is absent. |
|
||||
| `CLIPROXYAPI_MANAGEMENT_KEY` | _(empty)_ | `src/lib/services/cliproxyAccountHealth.ts` | Management key for account-health reads from an externally managed CLIProxyAPI instance. |
|
||||
| `CLIPROXYAPI_CONFIG_DIR` | `~/.cli-proxy-api` | `src/lib/versionManager/processManager.ts` | CLIProxyAPI config directory. |
|
||||
| `CLIPROXY_BIND_HOST` | `127.0.0.1` | `docker-compose.yml` | Host interface docker-compose publishes the `cliproxyapi` sidecar on (#12578). Its data volume holds provider OAuth/API credentials and the pinned image has no env-based data-plane `api-keys` override (only a mounted `config.yaml`), so `0.0.0.0` exposes a credential-bearing service to the whole LAN. |
|
||||
| `MUX_SERVICE_PORT` | `8322` | `src/lib/services/bootstrap.ts` | Override the port where the embedded Mux (coder/mux) agent-orchestration daemon listens (always 127.0.0.1). |
|
||||
| `DARIO_HOST` | `127.0.0.1` | `open-sse/executors/dario.ts` | Dario embedded-service bind/connect host (loopback only by default). |
|
||||
| `DARIO_PORT` | `3456` | `open-sse/executors/dario.ts` | Dario embedded-service port. |
|
||||
@@ -1381,6 +1382,9 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
|
||||
| `OMNIROUTE_REDIS_BIND_HOST` | `127.0.0.1` | `bin/cli/commands/redis.mjs` | Host interface the 1-click Redis launcher publishes on. The launcher starts Redis WITHOUT a password, so binding `0.0.0.0` hands every host on your LAN an unauthenticated Redis — only widen this if you also set a password on the instance yourself. |
|
||||
| `REDIS_BIND_HOST` | `127.0.0.1` | `docker-compose.yml` | Host interface docker-compose publishes the Redis sidecar on (#9286). The compose Redis runs without `requirepass`; app containers reach it over the compose network (`redis:6379`) — the published port exists only for host-side tooling. `0.0.0.0` exposes an unauthenticated Redis to the whole LAN. |
|
||||
| `REDIS_PORT` | `6379` | `docker-compose.yml` | Host port for the compose Redis sidecar. |
|
||||
| `APP_BIND_HOST` | `127.0.0.1` | `docker-compose.yml`, `docker-compose.prod.yml` | Host interface docker-compose publishes the app's own dashboard/API/live-WS ports on (#12568). With `REQUIRE_API_KEY=false` shipping as the `.env.example` default, `0.0.0.0` exposes the anonymous `/v1` LLM proxy to the whole LAN/WAN — only widen once `REQUIRE_API_KEY=true` or a reverse proxy in front enforces its own auth. |
|
||||
| `QDRANT_BIND_HOST` | `127.0.0.1` | `docker-compose.yml` | Host interface docker-compose publishes the Qdrant memory sidecar on (#12578). Same LAN-exposure reasoning as `REDIS_BIND_HOST`. |
|
||||
| `BIFROST_BIND_HOST` | `127.0.0.1` | `docker-compose.yml` | Host interface docker-compose publishes the Bifrost router sidecar on (#12578). Same LAN-exposure reasoning as `REDIS_BIND_HOST`. |
|
||||
| `REDIS_KEY_PREFIX` | `omniroute:` | `src/shared/utils/rateLimiter.ts` | Namespace prefix applied to every OmniRoute Redis key (rate limiter, auth cache, quota store). Prevents key collisions when the Redis instance is shared with other apps (#11042). |
|
||||
| `OMNIROUTE_INTERNAL_SERVICE_TOKEN` | _(unset — mechanism disabled)_ | `src/lib/api/internalServiceAuth.ts` | Shared secret for identity-preserving internal REST hops (#9260): OmniRoute components calling other local OmniRoute routes send it as `x-omniroute-internal-service-token` so the original caller identity is preserved. Compared with `timingSafeEqual`. |
|
||||
| `OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE` | _(unset)_ | `src/lib/api/internalServiceAuth.ts` | Secret-file variant of the internal service token: path to a file whose trimmed content is the token. Only consulted when the inline var is unset. |
|
||||
|
||||
6
electron/package-lock.json
generated
6
electron/package-lock.json
generated
@@ -2113,9 +2113,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
||||
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
||||
@@ -29,13 +29,7 @@ export async function checkSemanticCache({
|
||||
semanticCacheEnabled: boolean;
|
||||
// Only the fields this read path actually touches are named; everything else
|
||||
// on the request body stays `unknown` via the index signature.
|
||||
body: Record<string, unknown> & {
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
tool_choice?: unknown;
|
||||
tools?: unknown;
|
||||
response_format?: unknown;
|
||||
};
|
||||
body: Record<string, unknown> & { temperature?: number; top_p?: number };
|
||||
clientRawRequest: { headers?: unknown } | null;
|
||||
model: string;
|
||||
provider: string;
|
||||
@@ -57,8 +51,7 @@ export async function checkSemanticCache({
|
||||
body.messages ?? body.input,
|
||||
body.temperature,
|
||||
body.top_p,
|
||||
apiKeyId ?? undefined,
|
||||
{ toolChoice: body.tool_choice, tools: body.tools, responseFormat: body.response_format }
|
||||
apiKeyId ?? undefined
|
||||
);
|
||||
const cached = getCachedResponse(signature);
|
||||
if (cached) {
|
||||
|
||||
@@ -22,9 +22,6 @@ type CacheBody = {
|
||||
input?: unknown;
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
tool_choice?: unknown;
|
||||
tools?: unknown;
|
||||
response_format?: unknown;
|
||||
};
|
||||
|
||||
type UsageLike = { prompt_tokens?: number; completion_tokens?: number } | null | undefined;
|
||||
@@ -68,12 +65,7 @@ export function storeSemanticCacheResponse(
|
||||
args.body.messages ?? args.body.input,
|
||||
args.body.temperature,
|
||||
args.body.top_p,
|
||||
args.apiKeyId ?? undefined,
|
||||
{
|
||||
toolChoice: args.body.tool_choice,
|
||||
tools: args.body.tools,
|
||||
responseFormat: args.body.response_format,
|
||||
}
|
||||
args.apiKeyId ?? undefined
|
||||
);
|
||||
const tokensSaved = args.usage?.prompt_tokens + args.usage?.completion_tokens || 0;
|
||||
deps.setCachedResponse(signature, args.model, args.translatedResponse, tokensSaved);
|
||||
|
||||
@@ -23,9 +23,6 @@ type CacheBody = {
|
||||
input?: unknown;
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
tool_choice?: unknown;
|
||||
tools?: unknown;
|
||||
response_format?: unknown;
|
||||
};
|
||||
|
||||
export interface StreamingSemanticCacheStoreDeps {
|
||||
@@ -72,12 +69,7 @@ function writeStreamingCacheEntry(
|
||||
args.body.messages ?? args.body.input,
|
||||
args.body.temperature,
|
||||
args.body.top_p,
|
||||
args.apiKeyId ?? undefined,
|
||||
{
|
||||
toolChoice: args.body.tool_choice,
|
||||
tools: args.body.tools,
|
||||
responseFormat: args.body.response_format,
|
||||
}
|
||||
args.apiKeyId ?? undefined
|
||||
);
|
||||
const tokensSaved = streamTokensSaved(args.streamUsage);
|
||||
deps.setCachedResponse(sig, args.model, cleanBody, tokensSaved);
|
||||
|
||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -8687,6 +8687,20 @@
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@openai/codex-security/node_modules/smol-toml": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz",
|
||||
"integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/cyyynthia"
|
||||
}
|
||||
},
|
||||
"node_modules/@openai/codex-security/node_modules/type-fest": {
|
||||
"version": "5.9.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.9.0.tgz",
|
||||
@@ -15203,9 +15217,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@yarnpkg/parsers/node_modules/js-yaml": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
||||
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -18521,9 +18535,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csv-parse": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-7.0.2.tgz",
|
||||
"integrity": "sha512-uKZghv9UmPkMVLYy//KZ9HFAIJsl7wkhoEdIL0+rhuSY9pZQlhaeGEDPIe+/w7eh81MOql8Q/9+inAGWG6ZHYA==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-7.0.1.tgz",
|
||||
"integrity": "sha512-+2z7Ar0APQ7Uu6fX4cn+pitRmxjZ1WPBcGmZFKmA74FCyi7Et/XZx8cjNQ5CjbZ4HCOxXCOpRBYvYH08Qa003A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -23589,9 +23603,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.13.7",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz",
|
||||
"integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==",
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.0.tgz",
|
||||
"integrity": "sha512-jhunvfHWxd7J5EFfSgH4xsYJzSe/lfqbUCxiyyeaQasUsXeEHXtzVid+7EOGByc5JnFa23SSFL3Y2RV/z1T+eQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -26102,9 +26116,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/joi": {
|
||||
"version": "18.2.8",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-18.2.8.tgz",
|
||||
"integrity": "sha512-G2TX62h58ZHuwqetJgP2F4ualakqAmZtBYe3jWen7gxQRw5xApX6crnFtuB91WC0c3ESBnva+kGSnb3+6pIQDQ==",
|
||||
"version": "18.2.3",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-18.2.3.tgz",
|
||||
"integrity": "sha512-N5A3KTWQpPWT4ExxxPlUx7WmykGXRzhNidWhV41d6Abu9YfI2NyWCJuxdPnslJCPWtbRpSVOWSnSS6GakLM/Rg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -27932,9 +27946,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lockfile-lint/node_modules/js-yaml": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
||||
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -39606,9 +39620,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder2/node_modules/js-yaml": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
||||
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
@@ -486,10 +486,7 @@
|
||||
"fast-uri": "^3.1.7",
|
||||
"body-parser": "^2.3.0",
|
||||
"@yarnpkg/parsers": {
|
||||
"js-yaml": "^4.3.2"
|
||||
},
|
||||
"@openai/codex-security": {
|
||||
"smol-toml": "^1.8.0"
|
||||
"js-yaml": "^4.3.1"
|
||||
},
|
||||
"jsdom": {
|
||||
"undici": "^7.29.0"
|
||||
|
||||
@@ -19,10 +19,7 @@ export async function DELETE(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
// Scope the sweep to the caller. Only the operator's own dashboard (session
|
||||
// auth) may clear the whole instance; an API key clears only its own
|
||||
// completed batches (GHSA-wvxc-jp3v-5mg5).
|
||||
const result = deleteCompletedBatches(scope.isSessionAuth ? undefined : scope.apiKeyId);
|
||||
const result = deleteCompletedBatches();
|
||||
|
||||
return NextResponse.json(
|
||||
{ deleted: true, deletedBatches: result.deletedBatches, deletedFiles: result.deletedFiles },
|
||||
|
||||
@@ -2,6 +2,7 @@ import http from "http";
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import net from "net";
|
||||
import { getRuntimePorts } from "@/lib/runtime/ports";
|
||||
import { warnIfNonLoopbackWithoutApiKey } from "@/lib/startup/nonLoopbackApiKeyGuard";
|
||||
import { getApiBridgeTimeoutConfig } from "@/shared/utils/runtimeTimeouts";
|
||||
import {
|
||||
attachRequestStreamGuards,
|
||||
@@ -184,6 +185,7 @@ export function initApiBridgeServer(): void {
|
||||
if (apiPort === dashboardPort) return;
|
||||
|
||||
const host = process.env.API_HOST || "127.0.0.1";
|
||||
warnIfNonLoopbackWithoutApiKey("API bridge", host);
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// Absorb client-abort errors (browser closes the socket during navigation/
|
||||
|
||||
@@ -411,37 +411,15 @@ export function deleteBatch(id: string): boolean {
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk-delete completed batches and the files they reference.
|
||||
*
|
||||
* `apiKeyId` scopes EVERY statement to that owner. Omitting it keeps the
|
||||
* instance-wide sweep, which is legitimate for the operator's own dashboard
|
||||
* (session auth) and for nothing else: without the predicate, an ordinary
|
||||
* inference key could wipe every tenant's completed batches and null out their
|
||||
* file contents (GHSA-wvxc-jp3v-5mg5). Same ownership shape as `listBatches`
|
||||
* and `countBatches` above.
|
||||
*/
|
||||
export function deleteCompletedBatches(apiKeyId?: string | null): {
|
||||
deletedBatches: number;
|
||||
deletedFiles: number;
|
||||
} {
|
||||
export function deleteCompletedBatches(): { deletedBatches: number; deletedFiles: number } {
|
||||
const db = getDbInstance();
|
||||
const scoped = typeof apiKeyId === "string" && apiKeyId.length > 0;
|
||||
|
||||
// Collect unique file IDs from the completed batches in scope
|
||||
const rows = (
|
||||
scoped
|
||||
? db
|
||||
.prepare(
|
||||
"SELECT input_file_id, output_file_id, error_file_id FROM batches WHERE status = 'completed' AND api_key_id = ?"
|
||||
)
|
||||
.all(apiKeyId)
|
||||
: db
|
||||
.prepare(
|
||||
"SELECT input_file_id, output_file_id, error_file_id FROM batches WHERE status = 'completed'"
|
||||
)
|
||||
.all()
|
||||
) as Array<{
|
||||
// Collect unique file IDs from all completed batches
|
||||
const rows = db
|
||||
.prepare(
|
||||
"SELECT input_file_id, output_file_id, error_file_id FROM batches WHERE status = 'completed'"
|
||||
)
|
||||
.all() as Array<{
|
||||
input_file_id: string | null;
|
||||
output_file_id: string | null;
|
||||
error_file_id: string | null;
|
||||
@@ -463,16 +441,6 @@ export function deleteCompletedBatches(apiKeyId?: string | null): {
|
||||
}
|
||||
}
|
||||
|
||||
if (scoped) {
|
||||
db.prepare(
|
||||
"DELETE FROM batch_item_checkpoints WHERE batch_id IN (SELECT id FROM batches WHERE status = 'completed' AND api_key_id = ?)"
|
||||
).run(apiKeyId);
|
||||
const result = db
|
||||
.prepare("DELETE FROM batches WHERE status = 'completed' AND api_key_id = ?")
|
||||
.run(apiKeyId);
|
||||
return { deletedBatches: result.changes, deletedFiles };
|
||||
}
|
||||
|
||||
db.prepare(
|
||||
"DELETE FROM batch_item_checkpoints WHERE batch_id IN (SELECT id FROM batches WHERE status = 'completed')"
|
||||
).run();
|
||||
|
||||
@@ -137,43 +137,6 @@ export function clearMemoryCache(): void {
|
||||
|
||||
// ─── Signature Generation ─────────────────
|
||||
|
||||
/**
|
||||
* Behavior-changing generation constraints that MUST be folded into the cache signature
|
||||
* (#12734). Without these, a cached response produced under one `tool_choice`/`tools`/
|
||||
* `response_format` could be replayed for a later request that forbids or changes that
|
||||
* behavior (e.g. a cached `tool_calls` response served to a `tool_choice: "none"` request).
|
||||
*/
|
||||
export interface SignatureConstraints {
|
||||
toolChoice?: unknown;
|
||||
tools?: unknown;
|
||||
responseFormat?: unknown;
|
||||
}
|
||||
|
||||
/** Normalize a single tool definition, keeping only the fields that define its policy. */
|
||||
function normalizeTool(tool: unknown): unknown {
|
||||
const record = asRecord(tool);
|
||||
const fn = asRecord(record.function);
|
||||
if (Object.keys(fn).length === 0 && Object.keys(record).length === 0) return tool;
|
||||
return {
|
||||
type: typeof record.type === "string" ? record.type : "function",
|
||||
function: {
|
||||
name: fn.name,
|
||||
description: fn.description,
|
||||
parameters: fn.parameters,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize `tools` for consistent hashing (mirrors `normalizeConversation` for messages):
|
||||
* strips volatile/irrelevant fields while keeping name/description/parameters, which are
|
||||
* what actually define the tool policy a cached response was generated under.
|
||||
*/
|
||||
function normalizeTools(tools: unknown): unknown {
|
||||
if (!Array.isArray(tools) || tools.length === 0) return undefined;
|
||||
return tools.map(normalizeTool);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate deterministic cache signature from request params.
|
||||
* @param {string} model
|
||||
@@ -181,8 +144,6 @@ function normalizeTools(tools: unknown): unknown {
|
||||
* @param {number} temperature
|
||||
* @param {number} topP
|
||||
* @param {string} [apiKeyId] - API key ID for per-key isolation (prevents cross-user cache hits)
|
||||
* @param {SignatureConstraints} [constraints] - tool_choice/tools/response_format (#12734):
|
||||
* these change model behavior and must not collide with a signature computed without them.
|
||||
* @returns {string} hex signature
|
||||
*/
|
||||
export function generateSignature(
|
||||
@@ -190,17 +151,13 @@ export function generateSignature(
|
||||
conversation,
|
||||
temperature = 0,
|
||||
topP = 1,
|
||||
apiKeyId?: string,
|
||||
constraints?: SignatureConstraints
|
||||
apiKeyId?: string
|
||||
) {
|
||||
const payload = JSON.stringify({
|
||||
model,
|
||||
messages: normalizeConversation(conversation),
|
||||
temperature,
|
||||
top_p: topP,
|
||||
tool_choice: constraints?.toolChoice,
|
||||
tools: normalizeTools(constraints?.tools),
|
||||
response_format: constraints?.responseFormat,
|
||||
});
|
||||
const digest = crypto.createHash("sha256").update(payload).digest("hex");
|
||||
// Per-key cache isolation (#3740) namespaces the signature with the apiKeyId as a
|
||||
|
||||
36
src/lib/startup/nonLoopbackApiKeyGuard.ts
Normal file
36
src/lib/startup/nonLoopbackApiKeyGuard.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// Boot-time guard for issue #12568: docker-compose can be told to bind the
|
||||
// dashboard/API/live-WS ports to a non-loopback interface (APP_BIND_HOST,
|
||||
// API_HOST, LIVE_WS_HOST) while REQUIRE_API_KEY still defaults to `false`.
|
||||
// That combination puts the anonymous /v1 LLM proxy on the LAN/WAN with no
|
||||
// key required. This never hard-fails the boot (a reverse proxy in front of
|
||||
// OmniRoute may already be doing its own auth) — it only logs a loud warning
|
||||
// so the operator notices the exposure instead of discovering it from traffic.
|
||||
|
||||
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1", "localhost", "::ffff:127.0.0.1"]);
|
||||
|
||||
function isLoopbackHost(host: string): boolean {
|
||||
return LOOPBACK_HOSTS.has(host.trim().toLowerCase());
|
||||
}
|
||||
|
||||
function isRequireApiKeyDisabled(): boolean {
|
||||
const raw = (process.env.REQUIRE_API_KEY || "").trim().toLowerCase();
|
||||
// Matches the feature-flag default: unset/empty falls back to "false".
|
||||
return raw !== "true" && raw !== "1" && raw !== "yes";
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning when `host` resolves to a non-loopback interface while
|
||||
* REQUIRE_API_KEY is disabled. Never throws and never blocks startup.
|
||||
*/
|
||||
export function warnIfNonLoopbackWithoutApiKey(serverLabel: string, host: string): void {
|
||||
if (isLoopbackHost(host)) return;
|
||||
if (!isRequireApiKeyDisabled()) return;
|
||||
|
||||
console.warn(
|
||||
`[startup] ${serverLabel} is bound to non-loopback host "${host}" while ` +
|
||||
"REQUIRE_API_KEY is disabled — this exposes the anonymous /v1 proxy to " +
|
||||
"every reachable network interface. Set REQUIRE_API_KEY=true, or bind " +
|
||||
"back to 127.0.0.1, unless a reverse proxy in front of this instance " +
|
||||
"already enforces its own authentication."
|
||||
);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import type { DashboardEventName, DashboardEventMap, DashboardChannel } from "@/
|
||||
|
||||
import { CHANNEL_EVENTS, getChannelForEvent } from "@/lib/events/types";
|
||||
import { isAutomatedTestProcess, isBuildProcess } from "@/shared/utils/testProcess";
|
||||
import { warnIfNonLoopbackWithoutApiKey } from "@/lib/startup/nonLoopbackApiKeyGuard";
|
||||
|
||||
import {
|
||||
attachRequestStreamGuards,
|
||||
@@ -650,6 +651,7 @@ export function isLiveWsEnabled(): boolean {
|
||||
if (!isBuildOrTest() && isLiveWsEnabled()) {
|
||||
const port = parseInt(process.env.LIVE_WS_PORT || String(DEFAULT_PORT), 10);
|
||||
const host = process.env.LIVE_WS_HOST || DEFAULT_HOST;
|
||||
warnIfNonLoopbackWithoutApiKey("Live dashboard WebSocket", host);
|
||||
startLiveDashboardServer(port, host).catch((err) => {
|
||||
console.error("[LiveWS] Failed to start: %s", err instanceof Error ? err.message : String(err));
|
||||
});
|
||||
|
||||
@@ -58,8 +58,6 @@
|
||||
"tests/unit/account-fallback-retry-after-json.test.ts",
|
||||
"tests/unit/account-fallback-route-restriction-403.test.ts",
|
||||
"tests/unit/account-fallback-service.test.ts",
|
||||
"tests/unit/combo-pin-implicit-allowlist.test.ts",
|
||||
"tests/unit/combo/execute-target-gates.test.ts",
|
||||
"tests/unit/moonshot-quota-writeback.test.ts",
|
||||
"tests/unit/accountfallback-ratelimit-400-4976.test.ts",
|
||||
"tests/unit/adaptive-admission-route-matrix.test.ts",
|
||||
@@ -77,8 +75,6 @@
|
||||
"tests/unit/api-key-policy-noauth-allowed-connections.test.ts",
|
||||
"tests/unit/api-key-rotator-health.test.ts",
|
||||
"tests/unit/chat-routing-synced-inventory-11089.test.ts",
|
||||
"tests/unit/openai-compatible-per-upstream-402-health.test.ts",
|
||||
"tests/unit/quota-window-label.test.ts",
|
||||
"tests/unit/repro-combo-persisted-cooldown-preskip.test.ts",
|
||||
"tests/unit/repro-glm-iso-reset-24h-cap.test.ts",
|
||||
"tests/unit/security-route-guard-tiers.test.ts",
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* GHSA-wvxc-jp3v-5mg5 — `DELETE /api/v1/batches/delete-completed` deleted the
|
||||
* completed batches of EVERY api key on the instance, and nulled the contents of
|
||||
* every file those batches referenced.
|
||||
*
|
||||
* Two defects in one endpoint:
|
||||
* 1. `deleteCompletedBatches()` carried no `api_key_id` predicate — the file
|
||||
* SELECT, the checkpoint DELETE and the batch DELETE were all instance-wide.
|
||||
* 2. The route only checked that SOME key was present (`!scope.apiKeyId` →
|
||||
* 401), never that the caller owned anything. A key with `scopes: []` —
|
||||
* an ordinary inference key — could wipe another tenant's batches.
|
||||
*
|
||||
* The ownership pattern this restores is not new: `listBatches(apiKeyId?)` and
|
||||
* `countBatches(apiKeyId?)` in the same module already scope by `api_key_id`,
|
||||
* and `batches/[id]/route.ts` already gates per-record access with `scopeCheck`
|
||||
* (session auth sees everything, a key sees only its own). This helper was the
|
||||
* one that never got it.
|
||||
*
|
||||
* Run with:
|
||||
* node --import tsx/esm --test tests/unit/batch-delete-completed-ownership-wvxc.test.ts
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createFile, getFile } from "@/lib/db/files";
|
||||
import { createBatch, getBatch, deleteCompletedBatches } from "@/lib/db/batches";
|
||||
|
||||
const KEY_A = "key-wvxc-aaaa";
|
||||
const KEY_B = "key-wvxc-bbbb";
|
||||
|
||||
function seedCompletedBatch(apiKeyId: string | null, tag: string) {
|
||||
const file = createFile({
|
||||
bytes: 10,
|
||||
filename: `wvxc-${tag}.jsonl`,
|
||||
purpose: "batch",
|
||||
content: Buffer.from("{}"),
|
||||
});
|
||||
const batch = createBatch({
|
||||
endpoint: "/v1/chat/completions",
|
||||
completionWindow: "24h",
|
||||
inputFileId: file.id,
|
||||
status: "completed",
|
||||
apiKeyId,
|
||||
});
|
||||
return { file, batch };
|
||||
}
|
||||
|
||||
describe("deleteCompletedBatches — ownership scoping (GHSA-wvxc-jp3v-5mg5)", () => {
|
||||
it("scoped to one key deletes ONLY that key's completed batches", () => {
|
||||
const a = seedCompletedBatch(KEY_A, "a1");
|
||||
const b = seedCompletedBatch(KEY_B, "b1");
|
||||
|
||||
const result = deleteCompletedBatches(KEY_A);
|
||||
|
||||
assert.equal(getBatch(a.batch.id), null, "the caller's own batch should be gone");
|
||||
assert.ok(getBatch(b.batch.id), "another key's batch must survive");
|
||||
assert.equal(result.deletedBatches, 1, "must report only what it actually deleted");
|
||||
});
|
||||
|
||||
it("scoped deletion does not touch another key's file contents", () => {
|
||||
const a = seedCompletedBatch(KEY_A, "a2");
|
||||
const b = seedCompletedBatch(KEY_B, "b2");
|
||||
|
||||
deleteCompletedBatches(KEY_A);
|
||||
|
||||
assert.equal(getFile(a.file.id), null, "the caller's own file should be gone");
|
||||
assert.ok(getFile(b.file.id), "another key's file must survive with its content intact");
|
||||
});
|
||||
|
||||
it("a key with no completed batches deletes nothing at all", () => {
|
||||
const b = seedCompletedBatch(KEY_B, "b3");
|
||||
|
||||
const result = deleteCompletedBatches("key-wvxc-with-nothing");
|
||||
|
||||
assert.equal(result.deletedBatches, 0);
|
||||
assert.equal(result.deletedFiles, 0);
|
||||
assert.ok(getBatch(b.batch.id), "an unrelated key's batch must survive");
|
||||
});
|
||||
|
||||
it("unscoped (dashboard session) still clears the whole instance", () => {
|
||||
// The operator's own dashboard legitimately cleans up everything; that is
|
||||
// the ONLY caller allowed to omit the key. Preserved deliberately.
|
||||
seedCompletedBatch(KEY_A, "a4");
|
||||
seedCompletedBatch(KEY_B, "b4");
|
||||
|
||||
const result = deleteCompletedBatches();
|
||||
|
||||
assert.ok(
|
||||
result.deletedBatches >= 2,
|
||||
`expected an instance-wide sweep, got ${result.deletedBatches}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the route passes the caller's key through", () => {
|
||||
it("delete-completed scopes by api key instead of calling the helper bare", async () => {
|
||||
const { readFileSync } = await import("node:fs");
|
||||
const { fileURLToPath } = await import("node:url");
|
||||
const src = readFileSync(
|
||||
fileURLToPath(
|
||||
new URL("../../src/app/api/v1/batches/delete-completed/route.ts", import.meta.url)
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
assert.ok(
|
||||
!/deleteCompletedBatches\(\s*\)/.test(src),
|
||||
"the route still calls deleteCompletedBatches() with no owner — every tenant's batches go"
|
||||
);
|
||||
assert.ok(
|
||||
/deleteCompletedBatches\(\s*scope\./.test(src),
|
||||
"the route must pass the caller's scope into the helper"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -135,38 +135,3 @@ test("missing usage → tokensSaved coerces to 0 (NaN || 0)", () => {
|
||||
storeSemanticCacheResponse(baseArgs({ usage: undefined }), deps);
|
||||
assert.equal(stored[0].tokens, 0);
|
||||
});
|
||||
|
||||
// #12734: tool_choice/tools/response_format must reach generateSignature so a cached
|
||||
// tool_calls response cannot be replayed under a stricter tool policy.
|
||||
test("signature is called with tool_choice/tools/response_format from body (#12734)", () => {
|
||||
let captured: unknown[] = [];
|
||||
const { deps } = makeDeps({
|
||||
generateSignature: (...a: unknown[]) => {
|
||||
captured = a;
|
||||
return "sig";
|
||||
},
|
||||
});
|
||||
const tools = [{ type: "function", function: { name: "get_weather" } }];
|
||||
storeSemanticCacheResponse(
|
||||
baseArgs({
|
||||
body: {
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
temperature: 0,
|
||||
top_p: 1,
|
||||
tool_choice: "none",
|
||||
tools,
|
||||
response_format: { type: "json_object" },
|
||||
},
|
||||
}),
|
||||
deps
|
||||
);
|
||||
// args: (model, messages ?? input, temperature, top_p, apiKeyId, constraints)
|
||||
const constraints = captured[5] as {
|
||||
toolChoice: unknown;
|
||||
tools: unknown;
|
||||
responseFormat: unknown;
|
||||
};
|
||||
assert.equal(constraints.toolChoice, "none");
|
||||
assert.deepEqual(constraints.tools, tools);
|
||||
assert.deepEqual(constraints.responseFormat, { type: "json_object" });
|
||||
});
|
||||
|
||||
@@ -180,14 +180,12 @@ function makeHitArgs(overrides: Record<string, unknown> = {}) {
|
||||
|
||||
// Seed the cache under the EXACT signature checkSemanticCache rebuilds for `args`.
|
||||
function seedHit(args: ReturnType<typeof makeHitArgs>["args"], response: unknown) {
|
||||
const body = args.body as Record<string, unknown>;
|
||||
const signature = generateSignature(
|
||||
args.model,
|
||||
body.messages ?? body.input,
|
||||
args.body.messages ?? (args.body as Record<string, unknown>).input,
|
||||
args.body.temperature,
|
||||
body.top_p,
|
||||
args.apiKeyId ?? undefined,
|
||||
{ toolChoice: body.tool_choice, tools: body.tools, responseFormat: body.response_format }
|
||||
(args.body as Record<string, unknown>).top_p,
|
||||
args.apiKeyId ?? undefined
|
||||
);
|
||||
setCachedResponse(signature, args.model, response);
|
||||
return signature;
|
||||
@@ -494,76 +492,3 @@ test("checkSemanticCache HIT includes X-OmniRoute-Cache-Latency: synthetic heade
|
||||
"HIT response carries X-OmniRoute-Cache-Latency: synthetic marker"
|
||||
);
|
||||
});
|
||||
|
||||
// ─── tool_choice / tools / response_format must be part of the signature (#12734) ────────────
|
||||
|
||||
test("#12734: cached tool_calls response must NOT be replayed for tool_choice: 'none'", async () => {
|
||||
clearCache();
|
||||
const messages = [{ role: "user", content: "what is 2+2?" }];
|
||||
const toolCallResponse = {
|
||||
id: "chatcmpl-tool-calls",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
finish_reason: "tool_calls",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{ id: "call_1", type: "function", function: { name: "memory_search", arguments: "{}" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
||||
};
|
||||
// Stored under a body with NO tool_choice (mirrors the real pipeline: the cache check
|
||||
// runs before memory/skill tool injection, so the signature it stores under never saw
|
||||
// tool_choice at all).
|
||||
const { args: storeArgs } = makeHitArgs({ body: { model: "gpt-4o", messages, temperature: 0 } });
|
||||
seedHit(storeArgs, toolCallResponse);
|
||||
|
||||
const { args: forbidArgs } = makeHitArgs({
|
||||
body: { model: "gpt-4o", messages, temperature: 0, tool_choice: "none" },
|
||||
});
|
||||
const result = await checkSemanticCache(forbidArgs as Parameters<typeof checkSemanticCache>[0]);
|
||||
|
||||
assert.equal(
|
||||
result,
|
||||
null,
|
||||
"a tool_choice:'none' request must be a cache MISS against a tool_calls response cached without tool_choice"
|
||||
);
|
||||
});
|
||||
|
||||
test("#12734: identical tool_choice/tools/response_format across requests still HITs", async () => {
|
||||
clearCache();
|
||||
const messages = [{ role: "user", content: "what is the weather?" }];
|
||||
const tools = [
|
||||
{
|
||||
type: "function",
|
||||
function: { name: "get_weather", description: "Get the weather", parameters: { type: "object" } },
|
||||
},
|
||||
];
|
||||
const cached = {
|
||||
id: "chatcmpl-tool-config-hit",
|
||||
choices: [
|
||||
{ index: 0, message: { role: "assistant", content: "sunny" }, finish_reason: "stop" },
|
||||
],
|
||||
usage: { prompt_tokens: 8, completion_tokens: 2, total_tokens: 10 },
|
||||
};
|
||||
const body = {
|
||||
model: "gpt-4o",
|
||||
messages,
|
||||
temperature: 0,
|
||||
tool_choice: "auto",
|
||||
tools,
|
||||
response_format: { type: "json_object" },
|
||||
};
|
||||
const { args: storeArgs } = makeHitArgs({ body });
|
||||
seedHit(storeArgs, cached);
|
||||
|
||||
const { args: readArgs } = makeHitArgs({ body: { ...body } });
|
||||
const result = await checkSemanticCache(readArgs as Parameters<typeof checkSemanticCache>[0]);
|
||||
|
||||
assert.ok(result, "identical tool_choice/tools/response_format must still HIT");
|
||||
});
|
||||
|
||||
@@ -127,38 +127,3 @@ test("a throwing dep is swallowed (fail-open, non-critical)", () => {
|
||||
});
|
||||
assert.doesNotThrow(() => storeStreamingSemanticCacheResponse(baseArgs(), deps));
|
||||
});
|
||||
|
||||
// #12734: tool_choice/tools/response_format must reach generateSignature so a cached
|
||||
// tool_calls streaming response cannot be replayed under a stricter tool policy.
|
||||
test("signature is called with tool_choice/tools/response_format from body (#12734)", () => {
|
||||
let captured: unknown[] = [];
|
||||
const { deps } = makeDeps({
|
||||
generateSignature: (...a: unknown[]) => {
|
||||
captured = a;
|
||||
return "sig";
|
||||
},
|
||||
});
|
||||
const tools = [{ type: "function", function: { name: "get_weather" } }];
|
||||
storeStreamingSemanticCacheResponse(
|
||||
baseArgs({
|
||||
body: {
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
temperature: 0,
|
||||
top_p: 1,
|
||||
tool_choice: "none",
|
||||
tools,
|
||||
response_format: { type: "json_object" },
|
||||
},
|
||||
}),
|
||||
deps
|
||||
);
|
||||
// args: (model, messages ?? input, temperature, top_p, apiKeyId, constraints)
|
||||
const constraints = captured[5] as {
|
||||
toolChoice: unknown;
|
||||
tools: unknown;
|
||||
responseFormat: unknown;
|
||||
};
|
||||
assert.equal(constraints.toolChoice, "none");
|
||||
assert.deepEqual(constraints.tools, tools);
|
||||
assert.deepEqual(constraints.responseFormat, { type: "json_object" });
|
||||
});
|
||||
|
||||
@@ -13,11 +13,6 @@ type Check = {
|
||||
validate?: (content: string, claim?: string) => { ok: boolean; detail: string };
|
||||
};
|
||||
|
||||
/** Escape a value that is interpolated into a RegExp source. */
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
describe("ToS caution heading count", () => {
|
||||
it("the Caution heading carries the count the gate checks", () => {
|
||||
const txt = readFileSync(join(process.cwd(), "docs/reference/FREE_TIERS.md"), "utf8");
|
||||
@@ -26,24 +21,7 @@ describe("ToS caution heading count", () => {
|
||||
const tos = (buildChecks() as Check[]).find((c) =>
|
||||
String(c.docKey ?? "").includes("ToS caution")
|
||||
);
|
||||
// The backslashes must survive the TEMPLATE LITERAL to reach the regex.
|
||||
// Written as `\(\s*…` they did not: JS resolves `\(` to "(" and `\s` to the
|
||||
// LETTER "s" before RegExp ever sees them, so the pattern compiled to
|
||||
// `(s*16s*)` — a capture group around optional "s" characters. That matched
|
||||
// any heading merely containing the number, with no literal parentheses
|
||||
// required at all, so this guard passed on headings it was written to reject
|
||||
// (CodeQL js/useless-regexp-character-escape #994-#997).
|
||||
const count = escapeRegExp(String(tos?.actual));
|
||||
assert.match(heading, new RegExp(`\\(\\s*${count}\\s*\\)`));
|
||||
});
|
||||
|
||||
it("the heading guard actually requires the parentheses", () => {
|
||||
// Pins the defect above: the pattern this test builds must REJECT a heading
|
||||
// that carries the count without parentheses. Before the fix it accepted it.
|
||||
const pattern = new RegExp(`\\(\\s*${escapeRegExp("16")}\\s*\\)`);
|
||||
assert.equal(pattern.test("### Caution — clauses worth checking 16"), false);
|
||||
assert.equal(pattern.test("### Caution — clauses worth checking (16)"), true);
|
||||
assert.equal(pattern.test("### Caution — clauses worth checking ( 16 )"), true);
|
||||
assert.match(heading, new RegExp(`\(\s*${String(tos?.actual)}\s*\)`));
|
||||
});
|
||||
it("buildChecks exposes a soft ToS entry on FREE_TIERS.md with requireClaim", () => {
|
||||
const checks = buildChecks() as Check[];
|
||||
|
||||
71
tests/unit/compose-app-ports-loopback-bind.test.ts
Normal file
71
tests/unit/compose-app-ports-loopback-bind.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const REPO_ROOT = path.resolve(import.meta.dirname, "../..");
|
||||
|
||||
// docker-compose.yml (base/web/cli/host profiles) and docker-compose.prod.yml
|
||||
// default API_HOST/LIVE_WS_HOST/HOSTNAME to 0.0.0.0 and publish the dashboard/
|
||||
// API/live-WS ports with a bare, unscoped spec — Docker expands that to every
|
||||
// interface. Combined with .env.example shipping REQUIRE_API_KEY=false by
|
||||
// default, this puts the anonymous /v1 LLM proxy on the LAN/WAN. Mirrors the
|
||||
// existing Redis precedent (tests/unit/compose-redis-loopback-bind.test.ts).
|
||||
// Issue #12568.
|
||||
|
||||
function readCompose(file: string): string {
|
||||
return fs.readFileSync(path.join(REPO_ROOT, file), "utf8");
|
||||
}
|
||||
|
||||
test("docker-compose.yml publishes the dashboard/API/live-WS ports on loopback by default", () => {
|
||||
const compose = readCompose("docker-compose.yml");
|
||||
const barePublishSpecs = [
|
||||
/- "\$\{DASHBOARD_PORT:-20128\}:\$\{DASHBOARD_PORT:-20128\}"/,
|
||||
/- "\$\{API_PORT:-20129\}:\$\{API_PORT:-20129\}"/,
|
||||
/- "\$\{LIVE_WS_PORT:-20132\}:\$\{LIVE_WS_PORT:-20132\}"/,
|
||||
];
|
||||
for (const re of barePublishSpecs) {
|
||||
assert.doesNotMatch(compose, re, `unqualified publish spec ${re} binds 0.0.0.0`);
|
||||
}
|
||||
assert.match(
|
||||
compose,
|
||||
/- "\$\{APP_BIND_HOST:-127\.0\.0\.1\}:\$\{DASHBOARD_PORT:-20128\}:\$\{DASHBOARD_PORT:-20128\}"/
|
||||
);
|
||||
assert.doesNotMatch(compose, /API_HOST=\$\{API_HOST:-0\.0\.0\.0\}/);
|
||||
assert.doesNotMatch(compose, /LIVE_WS_HOST=\$\{LIVE_WS_HOST:-0\.0\.0\.0\}/);
|
||||
});
|
||||
|
||||
test("docker-compose.prod.yml publishes the app's ports on loopback by default", () => {
|
||||
const compose = readCompose("docker-compose.prod.yml");
|
||||
assert.doesNotMatch(compose, /API_HOST=\$\{API_HOST:-0\.0\.0\.0\}/);
|
||||
assert.doesNotMatch(compose, /LIVE_WS_HOST=\$\{LIVE_WS_HOST:-0\.0\.0\.0\}/);
|
||||
assert.doesNotMatch(compose, /HOSTNAME=0\.0\.0\.0/);
|
||||
assert.match(compose, /\$\{APP_BIND_HOST:-127\.0\.0\.1\}:\$\{PROD_DASHBOARD_PORT/);
|
||||
});
|
||||
|
||||
test(".env.example does not ship REQUIRE_API_KEY=false without a boot-time non-loopback guard", () => {
|
||||
const env = fs.readFileSync(path.join(REPO_ROOT, ".env.example"), "utf8");
|
||||
const requireApiKeyFalse = /^REQUIRE_API_KEY=false\s*$/m.test(env);
|
||||
if (requireApiKeyFalse) {
|
||||
const guardHits = ["src/server", "src/lib", "open-sse"].some((dir) => {
|
||||
try {
|
||||
const files = fs.readdirSync(path.join(REPO_ROOT, dir), { recursive: true }) as string[];
|
||||
return files.some((f) => {
|
||||
if (!f.endsWith(".ts")) return false;
|
||||
const full = path.join(REPO_ROOT, dir, f);
|
||||
if (!fs.statSync(full).isFile()) return false;
|
||||
const content = fs.readFileSync(full, "utf8");
|
||||
return content.includes("non-loopback") && content.includes("REQUIRE_API_KEY");
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
assert.ok(guardHits, "REQUIRE_API_KEY=false ships with no boot-time non-loopback guard");
|
||||
}
|
||||
});
|
||||
|
||||
test(".env.example documents APP_BIND_HOST and its default", () => {
|
||||
const env = fs.readFileSync(path.join(REPO_ROOT, ".env.example"), "utf8");
|
||||
assert.match(env, /# APP_BIND_HOST=127\.0\.0\.1/);
|
||||
});
|
||||
59
tests/unit/compose-cliproxyapi-loopback-bind.test.ts
Normal file
59
tests/unit/compose-cliproxyapi-loopback-bind.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const REPO_ROOT = path.resolve(import.meta.dirname, "../..");
|
||||
|
||||
// The optional `cliproxyapi` sidecar (profile `cliproxyapi`) proxies provider
|
||||
// credentials (its data volume is `cliproxyapi-data:/root/.cli-proxy-api`) and
|
||||
// carried no auth-related environment variable in its `environment:` block.
|
||||
// Docker/Podman expand an unqualified "8317:8317" publish spec to 0.0.0.0,
|
||||
// which puts this credential-bearing sidecar on every LAN interface the same
|
||||
// way a bare "6379:6379" would for Redis (see
|
||||
// tests/unit/compose-redis-loopback-bind.test.ts, the precedent this repo
|
||||
// already applied). Issue #12578.
|
||||
|
||||
function readCompose(file: string): string {
|
||||
return fs.readFileSync(path.join(REPO_ROOT, file), "utf8");
|
||||
}
|
||||
|
||||
test("docker-compose publishes cliproxyapi on loopback by default", () => {
|
||||
const compose = readCompose("docker-compose.yml");
|
||||
assert.match(
|
||||
compose,
|
||||
/- "\$\{CLIPROXY_BIND_HOST:-127\.0\.0\.1\}:\$\{CLIPROXYAPI_PORT:-8317\}:\$\{CLIPROXYAPI_PORT:-8317\}"/,
|
||||
"cliproxyapi publish spec must default to 127.0.0.1 (matching the Redis precedent)"
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
compose,
|
||||
/- "\$\{CLIPROXYAPI_PORT:-8317\}:\$\{CLIPROXYAPI_PORT:-8317\}"/,
|
||||
"unqualified cliproxyapi publish spec binds 0.0.0.0"
|
||||
);
|
||||
});
|
||||
|
||||
test("cliproxyapi service forwards a management/auth key into its environment", () => {
|
||||
const compose = readCompose("docker-compose.yml");
|
||||
const serviceMatch = compose.match(/ {2}cliproxyapi:\n(?:.*\n)*?(?=\n {2}\S|$)/);
|
||||
assert.ok(serviceMatch, "cliproxyapi service block must exist in docker-compose.yml");
|
||||
assert.match(
|
||||
serviceMatch![0],
|
||||
/CLIPROXYAPI_MANAGEMENT_KEY/,
|
||||
"cliproxyapi environment block must forward CLIPROXYAPI_MANAGEMENT_KEY (already documented in docs/reference/ENVIRONMENT.md) instead of leaving auth entirely to the upstream image's undocumented default"
|
||||
);
|
||||
});
|
||||
|
||||
test("qdrant and bifrost sidecars also publish on loopback by default", () => {
|
||||
const compose = readCompose("docker-compose.yml");
|
||||
assert.match(compose, /- "\$\{QDRANT_BIND_HOST:-127\.0\.0\.1\}:\$\{QDRANT_PORT:-6333\}:6333"/);
|
||||
assert.match(
|
||||
compose,
|
||||
/- "\$\{QDRANT_BIND_HOST:-127\.0\.0\.1\}:\$\{QDRANT_GRPC_PORT:-6334\}:6334"/
|
||||
);
|
||||
assert.match(compose, /- "\$\{BIFROST_BIND_HOST:-127\.0\.0\.1\}:\$\{BIFROST_PORT:-8080\}:8080"/);
|
||||
});
|
||||
|
||||
test(".env.example documents CLIPROXY_BIND_HOST and its default", () => {
|
||||
const env = fs.readFileSync(path.join(REPO_ROOT, ".env.example"), "utf8");
|
||||
assert.match(env, /# CLIPROXY_BIND_HOST=127\.0\.0\.1/);
|
||||
});
|
||||
76
tests/unit/non-loopback-api-key-guard.test.ts
Normal file
76
tests/unit/non-loopback-api-key-guard.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { warnIfNonLoopbackWithoutApiKey } from "@/lib/startup/nonLoopbackApiKeyGuard";
|
||||
|
||||
// #12568: docker-compose can bind the app's ports to a non-loopback interface
|
||||
// while REQUIRE_API_KEY still defaults to false, exposing the anonymous /v1
|
||||
// proxy to the LAN/WAN. This guard warns (never blocks) when that combination
|
||||
// is detected at server startup.
|
||||
|
||||
function withEnv<T>(vars: Record<string, string | undefined>, fn: () => T): T {
|
||||
const prev: Record<string, string | undefined> = {};
|
||||
for (const key of Object.keys(vars)) {
|
||||
prev[key] = process.env[key];
|
||||
const value = vars[key];
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
for (const key of Object.keys(prev)) {
|
||||
const value = prev[key];
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function captureWarn(fn: () => void): string[] {
|
||||
const messages: string[] = [];
|
||||
const original = console.warn;
|
||||
console.warn = (...args: unknown[]) => {
|
||||
messages.push(args.map(String).join(" "));
|
||||
};
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
console.warn = original;
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
test("warns when bound to 0.0.0.0 with REQUIRE_API_KEY unset (default false)", () => {
|
||||
withEnv({ REQUIRE_API_KEY: undefined }, () => {
|
||||
const messages = captureWarn(() => warnIfNonLoopbackWithoutApiKey("Test server", "0.0.0.0"));
|
||||
assert.equal(messages.length, 1);
|
||||
assert.match(messages[0], /non-loopback host "0\.0\.0\.0"/);
|
||||
assert.match(messages[0], /REQUIRE_API_KEY/);
|
||||
});
|
||||
});
|
||||
|
||||
test("warns when bound to a LAN IP with REQUIRE_API_KEY=false", () => {
|
||||
withEnv({ REQUIRE_API_KEY: "false" }, () => {
|
||||
const messages = captureWarn(() => warnIfNonLoopbackWithoutApiKey("Test server", "192.168.1.5"));
|
||||
assert.equal(messages.length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
test("stays silent when bound to loopback regardless of REQUIRE_API_KEY", () => {
|
||||
withEnv({ REQUIRE_API_KEY: "false" }, () => {
|
||||
const messages = captureWarn(() => warnIfNonLoopbackWithoutApiKey("Test server", "127.0.0.1"));
|
||||
assert.equal(messages.length, 0);
|
||||
});
|
||||
withEnv({ REQUIRE_API_KEY: "false" }, () => {
|
||||
const messages = captureWarn(() => warnIfNonLoopbackWithoutApiKey("Test server", "::1"));
|
||||
assert.equal(messages.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test("stays silent when bound to 0.0.0.0 with REQUIRE_API_KEY=true", () => {
|
||||
withEnv({ REQUIRE_API_KEY: "true" }, () => {
|
||||
const messages = captureWarn(() => warnIfNonLoopbackWithoutApiKey("Test server", "0.0.0.0"));
|
||||
assert.equal(messages.length, 0);
|
||||
});
|
||||
});
|
||||
@@ -107,81 +107,6 @@ describe("Semantic Cache", () => {
|
||||
const sigKeyless = generateSignature("gpt-4o", messages, 0, 1, undefined);
|
||||
assert.notEqual(sigKeyed, sigKeyless);
|
||||
});
|
||||
|
||||
// #12734: tool_choice/tools/response_format change model behavior and must not be
|
||||
// ignored by the signature — otherwise a cached tool_calls response can be replayed
|
||||
// for a request whose tool policy forbids it.
|
||||
describe("tool_choice / tools / response_format (#12734)", () => {
|
||||
const messages = [{ role: "user", content: "what is 2+2?" }];
|
||||
|
||||
it("generates different signatures for different tool_choice ('auto' vs 'none')", () => {
|
||||
const sig1 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
|
||||
toolChoice: "auto",
|
||||
});
|
||||
const sig2 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
|
||||
toolChoice: "none",
|
||||
});
|
||||
assert.notEqual(sig1, sig2);
|
||||
});
|
||||
|
||||
it("generates different signatures for a forced-function tool_choice", () => {
|
||||
const sig1 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
|
||||
toolChoice: "auto",
|
||||
});
|
||||
const sig2 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
|
||||
toolChoice: { type: "function", function: { name: "get_weather" } },
|
||||
});
|
||||
assert.notEqual(sig1, sig2);
|
||||
});
|
||||
|
||||
it("generates different signatures for no tool_choice vs an explicit one (the #12734 collision)", () => {
|
||||
const sigNoToolChoice = generateSignature("gpt-4o", messages, 0, 1);
|
||||
const sigWithToolChoice = generateSignature("gpt-4o", messages, 0, 1, undefined, {
|
||||
toolChoice: "none",
|
||||
});
|
||||
assert.notEqual(sigNoToolChoice, sigWithToolChoice);
|
||||
});
|
||||
|
||||
it("generates different signatures for different tools arrays", () => {
|
||||
const tools1 = [{ type: "function", function: { name: "get_weather", parameters: {} } }];
|
||||
const tools2 = [{ type: "function", function: { name: "get_stock_price", parameters: {} } }];
|
||||
const sig1 = generateSignature("gpt-4o", messages, 0, 1, undefined, { tools: tools1 });
|
||||
const sig2 = generateSignature("gpt-4o", messages, 0, 1, undefined, { tools: tools2 });
|
||||
assert.notEqual(sig1, sig2);
|
||||
});
|
||||
|
||||
it("generates different signatures for different response_format", () => {
|
||||
const sig1 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
|
||||
responseFormat: { type: "text" },
|
||||
});
|
||||
const sig2 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
|
||||
responseFormat: { type: "json_object" },
|
||||
});
|
||||
assert.notEqual(sig1, sig2);
|
||||
});
|
||||
|
||||
it("generates identical signatures when constraints are identical (no hit-rate regression)", () => {
|
||||
const tools = [{ type: "function", function: { name: "get_weather", parameters: {} } }];
|
||||
const constraints = {
|
||||
toolChoice: "auto",
|
||||
tools,
|
||||
responseFormat: { type: "json_object" },
|
||||
};
|
||||
const sig1 = generateSignature("gpt-4o", messages, 0, 1, undefined, constraints);
|
||||
const sig2 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
|
||||
toolChoice: "auto",
|
||||
tools: [{ type: "function", function: { name: "get_weather", parameters: {} } }],
|
||||
responseFormat: { type: "json_object" },
|
||||
});
|
||||
assert.equal(sig1, sig2);
|
||||
});
|
||||
|
||||
it("generates identical signatures for omitted constraints vs an explicitly empty constraints object", () => {
|
||||
const sig1 = generateSignature("gpt-4o", messages, 0, 1, undefined);
|
||||
const sig2 = generateSignature("gpt-4o", messages, 0, 1, undefined, {});
|
||||
assert.equal(sig1, sig2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCacheableForRead", () => {
|
||||
|
||||
Reference in New Issue
Block a user