2026-03-04 19:17:16 +08:00
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package cli
import (
2026-03-26 21:07:06 +08:00
"encoding/json"
2026-03-24 20:08:36 +08:00
"errors"
2026-03-04 19:17:16 +08:00
"fmt"
"os"
2026-06-09 15:22:50 +08:00
//"os/signal"
2026-04-30 12:36:03 +08:00
"path/filepath"
2026-03-12 13:33:13 +08:00
"strconv"
2026-03-04 19:17:16 +08:00
"strings"
2026-03-24 20:08:36 +08:00
2026-03-11 19:14:18 +08:00
"github.com/peterh/liner"
2026-03-24 20:08:36 +08:00
"gopkg.in/yaml.v3"
2026-03-26 21:07:06 +08:00
2026-04-30 12:36:03 +08:00
"ragflow/internal/cli/filesystem"
2026-03-04 19:17:16 +08:00
)
2026-06-09 15:22:50 +08:00
type APIServerConfig struct {
Name string ` yaml:"name" `
Host string ` yaml:"host" `
UserName * string ` yaml:"user_name" `
UserPassword * string ` yaml:"password" `
2026-06-24 18:48:09 +08:00
APIKey * string ` yaml:"api_key" `
2026-06-23 17:43:26 +08:00
KeyFile * string ` yaml:"key_file" `
2026-06-09 15:22:50 +08:00
IP string
Port int
}
2026-03-24 20:08:36 +08:00
// ConfigFile represents the rf.yml configuration file structure
type ConfigFile struct {
2026-06-09 15:22:50 +08:00
Host string ` yaml:"host" ` // default API server host
2026-06-24 18:48:09 +08:00
APIKey string ` yaml:"api_key" ` // default API server api key
2026-06-09 15:22:50 +08:00
UserName string ` yaml:"user_name" ` // default API server user name
Password string ` yaml:"password" ` // default API server password
APIServerMap map [ string ] * APIServerConfig ` yaml:"api_servers" `
2026-03-24 20:08:36 +08:00
}
2026-03-26 21:07:06 +08:00
// OutputFormat represents the output format type
type OutputFormat string
const (
OutputFormatTable OutputFormat = "table" // Table format with borders
OutputFormatPlain OutputFormat = "plain" // Plain text, space-separated (no borders)
OutputFormatJSON OutputFormat = "json" // JSON format (reserved for future use)
)
2026-06-09 15:22:50 +08:00
type CommandLineMode string
const (
APIMode CommandLineMode = "api"
AdminMode CommandLineMode = "admin"
IngestorMode CommandLineMode = "ingestor" // If we want to access ingestor
CollectorMode CommandLineMode = "collector" // If we want to access collector
DefaultAPIServer = "default"
)
type CommandLineConfig struct {
CLIMode CommandLineMode
AdminClientConfig * AdminModeConfig
APIClientConfig APIModeConfig
ShowHelp bool
Verbose bool
Interactive bool
OutputFormat OutputFormat
Command * string
}
type AdminModeConfig struct {
AdminHost string
AdminPort int
AdminName * string
AdminPassword * string
2026-06-23 17:43:26 +08:00
KeyFile * string
2026-06-09 15:22:50 +08:00
//AdminCommand *string
}
type APIModeConfig struct {
CurrentAPIServer string
APIServerMap map [ string ] * APIServerConfig
}
func ( c * CommandLineConfig ) Print ( ) {
b , err := json . MarshalIndent ( c , "" , " " )
if err == nil {
fmt . Println ( string ( b ) )
}
}
func ParseArgs ( args [ ] string ) ( * CommandLineConfig , error ) {
commandLineConfig := & CommandLineConfig {
CLIMode : APIMode ,
AdminClientConfig : nil ,
ShowHelp : false ,
Verbose : false ,
Interactive : true ,
OutputFormat : OutputFormatTable ,
Command : nil ,
}
for i := 0 ; i < len ( args ) ; i ++ {
arg := args [ i ]
switch arg {
case "-o" , "--output" :
// Parse output format
if i + 1 < len ( args ) && ! strings . HasPrefix ( args [ i + 1 ] , "-" ) {
format := args [ i + 1 ]
switch format {
case "plain" :
commandLineConfig . OutputFormat = OutputFormatPlain
case "json" :
commandLineConfig . OutputFormat = OutputFormatJSON
default :
commandLineConfig . OutputFormat = OutputFormatTable
}
i ++
}
case "-v" , "--verbose" :
commandLineConfig . Verbose = true
case "--admin" , "-admin" :
commandLineConfig . CLIMode = AdminMode
case "--help" , "-help" :
commandLineConfig . ShowHelp = true
default :
if ! strings . HasPrefix ( arg , "-" ) {
commandLineConfig . Interactive = false
}
}
}
var commandArgs [ ] string
var foundCommand bool
switch commandLineConfig . CLIMode {
case APIMode :
defaultApiServerConfig := & APIServerConfig {
UserName : nil ,
UserPassword : nil ,
2026-06-24 18:48:09 +08:00
APIKey : nil ,
2026-06-09 15:22:50 +08:00
}
configFile := "rf.yml"
for i := 0 ; i < len ( args ) ; i ++ {
arg := args [ i ]
2026-06-09 17:00:10 +08:00
// Handle known global flags (already parsed in first pass).
// Intercept here regardless of position so they are never
// mistaken for command args or unknown flags downstream.
switch arg {
case "-o" , "--output" :
if i + 1 < len ( args ) {
i ++
}
continue
case "-v" , "--verbose" , "--help" , "-help" :
continue
case "--admin" , "-admin" :
return nil , fmt . Errorf ( "unexpected parameter: --admin" )
}
// If we've found the command, collect remaining args
2026-06-09 15:22:50 +08:00
if foundCommand {
commandArgs = append ( commandArgs , arg )
continue
}
switch arg {
case "-h" , "--host" :
if i + 1 < len ( args ) && ! strings . HasPrefix ( args [ i + 1 ] , "-" ) {
hostVal := args [ i + 1 ]
h , port , err := parseHostPort ( hostVal )
if err != nil {
return nil , fmt . Errorf ( "invalid host format: %v" , err )
}
defaultApiServerConfig . IP = h
defaultApiServerConfig . Port = port
i ++
}
case "-t" , "--token" :
if i + 1 < len ( args ) && ! strings . HasPrefix ( args [ i + 1 ] , "-" ) {
2026-06-24 18:48:09 +08:00
defaultApiServerConfig . APIKey = & args [ i + 1 ]
2026-06-09 15:22:50 +08:00
i ++
}
case "-u" , "--user" :
if i + 1 < len ( args ) && ! strings . HasPrefix ( args [ i + 1 ] , "-" ) {
defaultApiServerConfig . UserName = & args [ i + 1 ]
i ++
}
case "-p" , "--password" :
if i + 1 < len ( args ) && ! strings . HasPrefix ( args [ i + 1 ] , "-" ) {
defaultApiServerConfig . UserPassword = & args [ i + 1 ]
i ++
}
case "-f" , "--config" :
if i + 1 < len ( args ) && ! strings . HasPrefix ( args [ i + 1 ] , "-" ) {
configFile = args [ i + 1 ]
// Convert to absolute path immediately
if ! filepath . IsAbs ( configFile ) {
absPath , err := filepath . Abs ( configFile )
if err == nil {
configFile = absPath
}
}
i ++
}
2026-06-23 17:43:26 +08:00
case "-k" , "--key" :
if i + 1 < len ( args ) && ! strings . HasPrefix ( args [ i + 1 ] , "-" ) {
defaultApiServerConfig . KeyFile = & args [ i + 1 ]
i ++
}
2026-06-09 15:22:50 +08:00
default :
// Non-flag argument (command)
if ! strings . HasPrefix ( arg , "-" ) {
commandArgs = append ( commandArgs , arg )
foundCommand = true
}
}
}
var config ConfigFile
data , err := os . ReadFile ( configFile )
if err == nil {
if err = yaml . Unmarshal ( data , & config ) ; err != nil {
return nil , fmt . Errorf ( "failed to parse rf.yml: %v" , err )
}
if config . Host != "" {
var h string
var port int
h , port , err = parseHostPort ( config . Host )
if err != nil {
return nil , fmt . Errorf ( "invalid host in config file: %v" , err )
}
if defaultApiServerConfig . IP == "" {
defaultApiServerConfig . IP = h
}
if defaultApiServerConfig . Port == 0 {
defaultApiServerConfig . Port = port
}
}
if config . UserName != "" {
if defaultApiServerConfig . UserName == nil {
defaultApiServerConfig . UserName = & config . UserName
}
}
if config . Password != "" {
if defaultApiServerConfig . UserPassword == nil {
defaultApiServerConfig . UserPassword = & config . Password
}
}
2026-06-24 18:48:09 +08:00
if config . APIKey != "" {
if defaultApiServerConfig . APIKey == nil {
defaultApiServerConfig . APIKey = & config . APIKey
2026-06-09 15:22:50 +08:00
}
}
} else {
if configFile == "rf.yml" && os . IsNotExist ( err ) {
} else {
return nil , fmt . Errorf ( "failed to read %s: %v" , configFile , err )
}
}
if defaultApiServerConfig . IP == "" {
defaultApiServerConfig . IP = "127.0.0.1"
}
if defaultApiServerConfig . Port == 0 {
defaultApiServerConfig . Port = 9384
}
commandLineConfig . APIClientConfig . APIServerMap = config . APIServerMap
if commandLineConfig . APIClientConfig . APIServerMap == nil {
commandLineConfig . APIClientConfig . APIServerMap = make ( map [ string ] * APIServerConfig )
}
if commandLineConfig . APIClientConfig . APIServerMap [ DefaultAPIServer ] != nil {
return nil , fmt . Errorf ( "'Default' API server config should be in api_servers" )
}
commandLineConfig . APIClientConfig . APIServerMap [ DefaultAPIServer ] = defaultApiServerConfig
commandLineConfig . APIClientConfig . CurrentAPIServer = DefaultAPIServer
case AdminMode :
AdminConfig := & AdminModeConfig {
AdminHost : "127.0.0.1" ,
AdminPort : 9383 ,
//AdminName: "admin@ragflow.io",
//AdminPassword: "admin",
}
for i := 0 ; i < len ( args ) ; i ++ {
arg := args [ i ]
2026-06-09 17:00:10 +08:00
// Handle known global flags regardless of position
switch arg {
case "-o" , "--output" :
if i + 1 < len ( args ) {
i ++
}
continue
case "-v" , "--verbose" , "--admin" , "-admin" , "--help" , "-help" :
continue
case "-t" , "--token" :
return nil , fmt . Errorf ( "token is invalid in admin mode" )
case "-f" , "--config" :
return nil , fmt . Errorf ( "config is invalid in admin mode" )
}
// If we've found the command, collect remaining args
2026-06-09 15:22:50 +08:00
if foundCommand {
commandArgs = append ( commandArgs , arg )
continue
}
switch arg {
case "-h" , "--host" :
if i + 1 < len ( args ) && ! strings . HasPrefix ( args [ i + 1 ] , "-" ) {
hostVal := args [ i + 1 ]
h , port , err := parseHostPort ( hostVal )
if err != nil {
return nil , fmt . Errorf ( "invalid host format: %v" , err )
}
AdminConfig . AdminHost = h
AdminConfig . AdminPort = port
i ++
}
case "-u" , "--user" :
if i + 1 < len ( args ) && ! strings . HasPrefix ( args [ i + 1 ] , "-" ) {
AdminConfig . AdminName = & args [ i + 1 ]
i ++
}
2026-06-23 17:43:26 +08:00
case "-k" , "--key" :
if i + 1 < len ( args ) && ! strings . HasPrefix ( args [ i + 1 ] , "-" ) {
AdminConfig . KeyFile = & args [ i + 1 ]
i ++
}
2026-06-09 15:22:50 +08:00
case "-p" , "--password" :
if i + 1 < len ( args ) && ! strings . HasPrefix ( args [ i + 1 ] , "-" ) {
AdminConfig . AdminPassword = & args [ i + 1 ]
i ++
}
default :
// Non-flag argument (command)
if ! strings . HasPrefix ( arg , "-" ) {
commandArgs = append ( commandArgs , arg )
foundCommand = true
}
}
}
commandLineConfig . AdminClientConfig = AdminConfig
}
commandArgsLen := len ( commandArgs )
if commandArgsLen > 0 {
if commandArgsLen == 1 {
commandLineConfig . Command = & commandArgs [ 0 ]
} else {
ApiCommand := strings . Join ( commandArgs , " " )
commandLineConfig . Command = & ApiCommand
}
}
return commandLineConfig , nil
}
2026-03-24 20:08:36 +08:00
// LoadDefaultConfigFile reads the rf.yml file from current directory if it exists
func LoadDefaultConfigFile ( ) ( * ConfigFile , error ) {
// Try to read rf.yml from current directory
data , err := os . ReadFile ( "rf.yml" )
if err != nil {
// File doesn't exist, return nil without error
if os . IsNotExist ( err ) {
return nil , nil
}
return nil , err
}
var config ConfigFile
if err = yaml . Unmarshal ( data , & config ) ; err != nil {
return nil , fmt . Errorf ( "failed to parse rf.yml: %v" , err )
}
return & config , nil
}
// LoadConfigFileFromPath reads a config file from the specified path
func LoadConfigFileFromPath ( path string ) ( * ConfigFile , error ) {
data , err := os . ReadFile ( path )
if err != nil {
return nil , fmt . Errorf ( "failed to read config file %s: %v" , path , err )
}
var config ConfigFile
if err = yaml . Unmarshal ( data , & config ) ; err != nil {
return nil , fmt . Errorf ( "failed to parse config file %s: %v" , path , err )
}
return & config , nil
}
// parseHostPort parses a host:port string and returns host and port
func parseHostPort ( hostPort string ) ( string , int , error ) {
if hostPort == "" {
return "" , - 1 , nil
}
// Split host and port
parts := strings . Split ( hostPort , ":" )
if len ( parts ) != 2 {
return "" , - 1 , fmt . Errorf ( "invalid host format, expected host:port, got: %s" , hostPort )
}
host := parts [ 0 ]
port , err := strconv . Atoi ( parts [ 1 ] )
if err != nil {
return "" , - 1 , fmt . Errorf ( "invalid port number: %s" , parts [ 1 ] )
}
return host , port , nil
}
// PrintUsage prints the CLI usage information
func PrintUsage ( ) {
fmt . Println ( ` RAGFlow CLI Client
2026-06-23 19:20:49 +08:00
Usage : ragflow - cli [ options ] [ command ]
2026-03-24 20:08:36 +08:00
Options :
2026-03-25 21:39:14 +08:00
- h , -- host string RAGFlow service address ( host : port , default "127.0.0.1:9380" )
- t , -- token string API token for authentication
- u , -- user string Username for authentication
- p , -- password string Password for authentication
- f , -- config string Path to config file ( YAML format )
2026-03-26 21:07:06 +08:00
- o , -- output string Output format : table , plain , json ( search defaults to json )
2026-04-30 12:36:03 +08:00
- v , -- verbose Enable verbose logging ( shows debug info )
2026-03-25 21:39:14 +08:00
-- admin , - admin Run in admin mode
-- help Show this help message
Mode :
-- admin , - admin Run in admin mode ( prompt : RAGFlow ( admin ) > )
Default is user mode ( prompt : RAGFlow ( user ) > ) .
Authentication :
You can authenticate using either :
1. API token : - t or -- token
2. Username and password : - u / -- user and - p / -- password
Note : These two methods are mutually exclusive .
2026-03-24 20:08:36 +08:00
Configuration File :
The CLI will automatically read rf . yml from the current directory if it exists .
2026-03-25 21:39:14 +08:00
Use - f or -- config to specify a custom config file path .
Command line options override config file values .
2026-03-24 20:08:36 +08:00
Config file format :
host : 127.0 .0 .1 : 9380
api_token : your - api - token
2026-03-25 21:39:14 +08:00
user_name : your - username
password : your - password
2026-03-24 20:08:36 +08:00
2026-03-25 21:39:14 +08:00
Note : api_token and user_name / password are mutually exclusive in config file .
2026-03-24 20:08:36 +08:00
Commands :
2026-03-26 21:07:06 +08:00
SQL commands ( use quotes ) : "LIST USERS" , "CREATE USER 'email' 'password'" , etc .
2026-04-30 12:36:03 +08:00
Filesystem commands ( no quotes ) : ls datasets , search "keyword" , cat path , etc .
Skill commands :
install - skill < space > < path | url > [ options ] Install a skill from local path or remote URL
uninstall - skill < space > < skill - name > Remove an installed skill
search skills - q < query > [ -- space space1 ] Search skills in a space
2026-03-27 18:12:56 +08:00
If no command is provided , CLI runs in interactive mode . ` )
2026-03-24 20:08:36 +08:00
}
2026-03-11 19:14:18 +08:00
// HistoryFile returns the path to the history file
func HistoryFile ( ) string {
return os . Getenv ( "HOME" ) + "/" + historyFileName
}
const historyFileName = ".ragflow_cli_history"
2026-03-04 19:17:16 +08:00
// CLI represents the command line interface
type CLI struct {
2026-06-10 16:06:30 +08:00
running bool
line * liner . State
2026-06-09 15:22:50 +08:00
APIServerClientMap map [ string ] * HTTPClient
AdminServerClient * HTTPClient
PasswordPrompt PasswordPromptFunc // Function for password input
ContextEngine * filesystem . Engine // Context Engine for virtual filesystem
CurrentModel * CurrentModel // Current model configuration
Config * CommandLineConfig
2026-03-04 19:17:16 +08:00
}
2026-06-09 15:22:50 +08:00
func NewCLIWithConfig ( commandLineConfig * CommandLineConfig ) ( * CLI , error ) {
2026-03-11 19:14:18 +08:00
// Create liner first
line := liner . NewLiner ( )
2026-06-09 15:22:50 +08:00
cli := & CLI {
2026-06-10 16:06:30 +08:00
line : line ,
Config : commandLineConfig ,
2026-03-24 20:08:36 +08:00
}
2026-06-09 15:22:50 +08:00
if commandLineConfig . CLIMode == APIMode {
apiServerConfig := commandLineConfig . APIClientConfig . APIServerMap [ commandLineConfig . APIClientConfig . CurrentAPIServer ]
httpClient := NewHTTPClient ( )
httpClient . Host = apiServerConfig . IP
httpClient . Port = apiServerConfig . Port
2026-06-24 18:48:09 +08:00
if apiServerConfig . APIKey != nil {
httpClient . APIKey = apiServerConfig . APIKey
httpClient . useAPIKey = true
2026-06-09 15:22:50 +08:00
}
cli . APIServerClientMap = map [ string ] * HTTPClient {
cli . Config . APIClientConfig . CurrentAPIServer : httpClient ,
}
// Auto-login if user and password are provided (from config file)
2026-06-24 18:48:09 +08:00
if apiServerConfig . UserName != nil && apiServerConfig . UserPassword != nil && apiServerConfig . APIKey == nil {
2026-06-09 15:22:50 +08:00
if err := cli . LoginUserInteractive ( * apiServerConfig . UserName , * apiServerConfig . UserPassword ) ; err != nil {
line . Close ( )
return nil , fmt . Errorf ( "auto-login failed: %w" , err )
}
2026-03-25 21:39:14 +08:00
}
2026-06-09 15:22:50 +08:00
engine := filesystem . NewEngine ( )
2026-03-24 20:08:36 +08:00
2026-06-09 15:22:50 +08:00
// Register providers
// TODO: if http config change, engine http config won't be updated. They should share the same config
engine . RegisterProvider ( filesystem . NewDatasetProvider ( & httpClientAdapter { httpClient } ) )
engine . RegisterProvider ( filesystem . NewFileProvider ( & httpClientAdapter { httpClient } ) )
engine . RegisterProvider ( filesystem . NewSkillProvider ( & httpClientAdapter { httpClient } ) )
2026-03-26 21:07:06 +08:00
2026-06-09 15:22:50 +08:00
cli . ContextEngine = engine
} else if commandLineConfig . CLIMode == AdminMode {
httpClient := NewHTTPClient ( )
httpClient . Host = commandLineConfig . AdminClientConfig . AdminHost
httpClient . Port = commandLineConfig . AdminClientConfig . AdminPort
cli . AdminServerClient = httpClient
2026-03-26 21:07:06 +08:00
2026-06-09 15:22:50 +08:00
adminServerConfig := commandLineConfig . AdminClientConfig
// Auto-login if user and password are provided (from config file)
if adminServerConfig . AdminName != nil && adminServerConfig . AdminPassword != nil {
if err := cli . LoginUserInteractive ( * adminServerConfig . AdminName , * adminServerConfig . AdminPassword ) ; err != nil {
line . Close ( )
return nil , fmt . Errorf ( "auto-login failed: %w" , err )
}
2026-03-26 21:07:06 +08:00
}
2026-06-09 15:22:50 +08:00
} else {
return nil , fmt . Errorf ( "invalid CLI mode: %s" , commandLineConfig . CLIMode )
2026-03-24 20:08:36 +08:00
}
2026-06-09 15:22:50 +08:00
return cli , nil
2026-03-04 19:17:16 +08:00
}
fix(security): address 93 CodeQL code-scanning alerts across 61 files (#16407)
## Summary
Resolves all 93 open alerts at
https://github.com/infiniflow/ragflow/security/code-scanning by rule:
| Rule | Count | Treatment |
|------|-------|-----------|
| py/clear-text-logging-sensitive-data | 23 | Real fix — log scrubbing |
| go/path-injection | 15 | Real fix where possible, suppression with
rationale |
| go/request-forgery | 8 | Suppression with rationale
(operator-controlled URLs) |
| go/clear-text-logging | 10 | Real fix — log scrubbing |
| go/unsafe-quoting | 5 | Real fix — escape or refactor |
| go/sql-injection | 3 | Real fix — orderby whitelist + CodeQL comment |
| go/uncontrolled-allocation-size | 2 | Real fix — cap to 1024 |
| go/incorrect-integer-conversion | 3 | Real fix — ParseInt + range
check |
| go/insecure-hostkeycallback | 1 | Real fix — known_hosts file |
| go/disabled-certificate-check | 2 | Suppression with rationale |
| go/command-injection | 1 | Suppression (sanitized via shq()) |
| go/email-injection | 1 | Suppression with rationale |
| go/cookie-httponly-not-set | 1 | Suppression (SPA bootstrap) |
| js/stack-trace-exposure | 1 | Real fix — generic client message |
| js/prototype-pollution-utility | 1 | Real fix — reject
__proto__/constructor/prototype |
| py/weak-sensitive-data-hashing | 1 | Real fix — MD5 → SHA-256 |
| py/incomplete-url-substring-sanitization | 3 | Real fix —
urlparse(hostname) |
| py/paramiko-missing-host-key-validation | 1 | Real fix —
load_system_host_keys + RejectPolicy |
| cpp/integer-multiplication-cast-to-long | 2 | Real fix — cast to
size_t |
## Real fixes (with measurable security improvement)
**SSH host key verification (Go + Python)**
Replace `InsecureIgnoreHostKey()` / `paramiko.AutoAddPolicy()` with
proper host key verification against a known_hosts file (configurable
via `SSH_KNOWN_HOSTS` env / `known_hosts` config field; fail-closed when
unset). Loads `~/.ssh/known_hosts` first via `load_system_host_keys()`
so existing setups keep working.
**SQL injection in `user_canvas`**
Add `userCanvasOrderableColumns` whitelist + `userCanvasOrderClause`
helper. Both `GetList()` and `ListByTenantIDs()` now route the
user-supplied `orderby` query param through the helper, defaulting to
`create_time` on miss.
**SQL injection in `pipeline_operation_log`**
Existing whitelist documented via CodeQL comment.
**Real SQL injection in `infinity/chunk.go:931`**
Escape `'` → `''` on user-controlled `questionText` before splicing into
`filter_fulltext(...)` SQL filter.
**Real SQL injection in `elasticsearch/sql.go:75`**
Defense-in-depth escape on tokenizer output before splicing into
`MATCH(...)`.
**Python code injection in `result_protocol.go`**
Replace raw JSON literal embedding into Python/JS expressions with
base64 + `json.loads` / `JSON.parse(Buffer.from(...,
'base64').toString('utf8'))`. Eliminates both the unsafe-quoting sink
and the brittleness of mixing JSON true/false/null with Python syntax.
**URL substring check bypass in `embedding_model.py`**
Replace `if "dashscope-intl.aliyuncs.com" in u` with
`urlparse(u).hostname == "dashscope-intl.aliyuncs.com"` so a base_url
like `https://attacker.example/?u=dashscope-intl.aliyuncs.com` cannot
bypass the routing.
**Prototype pollution in `setNestedValue` (TS)**
Reject `__proto__`/`constructor`/`prototype` keys before any assignment.
**Integer overflow**
- scrypt params via `ParseInt` + non-positive check
(`internal/common/password.go`)
- `topN` and `n` caps to 1024 (retrieval_service.go, dataset.go)
- `nalloc*statesize` cast to `size_t` (cpp/re2/onepass.cc)
**Cookie httponly**
Set explicitly with rationale: this is the OAuth bootstrap cookie
intentionally read by the SPA.
**Stack trace exposure**
Replace `error.message` in HTTP 500 response with generic `"internal
error"`; full error still logged server-side via `console.error`.
**Weak hashing**
MD5 → SHA-256 for deterministic `conv_id` derivation
(`conversation_service.py`).
**Log scrubbing**
Remove or redact user-controlled / sensitive content from clear-text
logs across 8 ingestion parsers, `llm_service.py` ×11,
`tenant_llm_service.py` ×7, `misc_utils.py` ×4, `redis_conn.py` ×10,
`conftest.py` ×4, `init_data.py`, `dataset_api_service.py`,
`generator.py`, `mysql_migration.py`, `cli.go`, `user_command.go`,
`pdf_parser.go`. Most patterns converted to parameterized logging
(`logging.info("...: %d", n)`) or static messages.
## CodeQL suppressions (each with rationale)
For alerts where the data flow is genuinely safe but CodeQL can't see
the context — operator-controlled URLs, sanitized inputs, etc. — I added
`// codeql[go/<rule>] <rationale>` annotations rather than dismissing
them, so future readers can audit the rationale inline:
- `internal/agent/component/invoke.go:135` — Invoke is a generic canvas
HTTP client
- `internal/service/langfuse.go` ×2 — host is per-tenant operator config
- `internal/service/file.go:1184` — already SSRF-guarded by
`assertURLSafe`
- `internal/utility/mcp_client.go` ×3 — already `AssertURLSafe` +
IP-pinned
- `internal/entity/models/bedrock.go` — sigv4-signed request, URL can't
be tampered
- `internal/service/deep_researcher.go:269` — `callback` is SSE display
string, not SQL
- `internal/engine/infinity/chunk.go:346` — UUIDs can't contain `'` (RFC
4122)
- `internal/cli/common_command.go` ×2 — CLI trusts operator-configured
URL
- `internal/utility/smtp.go:194` — msg is server-built, not user form
input
- `internal/entity/models/*` ×14 (path-injection) — audio file paths are
caller-supplied
## Test plan
- ✅ All 13 modified Go packages build cleanly
- ✅ 663 tests pass across `internal/agent/sandbox`, `internal/common`,
`internal/agent/component`, `internal/engine/infinity`, `internal/dao`
- ✅ All 11 modified Python files parse via `ast.parse`
- ✅ TypeScript `tsc --noEmit` clean on the modified
`use-provider-fields.tsx`
- ✅ `node --check` clean on the modified JS file
🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-27 19:48:29 +08:00
// sanitizeCLIError returns an operator-safe rendering of a CLI
// command error. Many command handlers build their errors via
// fmt.Errorf("... %s ...", userInput) where userInput can be a
// dataset name, file path, or partial command containing secrets;
// printing err.Error() verbatim would echo that back to the
// operator's terminal in cleartext. We keep the error class (e.g.
// "not found", "invalid argument") and drop the interpolated
// user-controlled values. The full error is still available via
// err.Error() for the caller's own logging.
func sanitizeCLIError ( err error ) string {
if err == nil {
return ""
}
msg := err . Error ( )
// Strip every single-quoted span. Many command handlers interpolate
// user-controlled values via fmt.Errorf("... '%s' ... '%s' ...", a, b)
// (e.g. "copy '/secret/a' to '/secret/b' failed"). A single pass only
// catches the first one, so loop until none remain. Unmatched single
// quotes (no closing pair before the end of the string) are left in
// place — they likely indicate the error wasn't produced by our
// fmt.Errorf pattern and the original text is the safer rendering.
for {
i := strings . Index ( msg , "'" )
if i < 0 {
break
}
j := strings . Index ( msg [ i + 1 : ] , "'" )
if j < 0 {
break
}
head := strings . TrimRight ( msg [ : i ] , " " )
tail := strings . TrimLeft ( msg [ i + j + 2 : ] , " " )
switch {
case head == "" :
msg = tail
case tail == "" :
msg = head
default :
msg = head + " " + tail
}
}
msg = strings . TrimSpace ( msg )
if msg == "" {
return "command failed"
}
return msg
}
2026-03-04 19:17:16 +08:00
// Run starts the interactive CLI
2026-06-15 16:37:47 +08:00
func ( c * CLI ) Run ( ) error {
2026-03-25 21:39:14 +08:00
// If username is provided without password, prompt for password
2026-06-09 15:22:50 +08:00
cliConfig := c . Config
switch cliConfig . CLIMode {
case APIMode :
apiConfig := c . Config . APIClientConfig . APIServerMap [ c . Config . APIClientConfig . CurrentAPIServer ]
2026-06-24 18:48:09 +08:00
if apiConfig . UserName != nil && apiConfig . UserPassword == nil && apiConfig . APIKey == nil {
2026-06-09 15:22:50 +08:00
// provider username but no password or api token
maxAttempts := 3
for attempt := 1 ; attempt <= maxAttempts ; attempt ++ {
2026-06-10 16:06:30 +08:00
fmt . Printf ( "Please input your password: " )
2026-06-09 15:22:50 +08:00
password , err := ReadPassword ( )
if password == "" {
if attempt < maxAttempts {
fmt . Println ( "Password cannot be empty, please try again" )
continue
}
return errors . New ( "no password provided after 3 attempts" )
}
apiConfig . UserPassword = & password
2026-06-10 16:06:30 +08:00
if err = c . VerifyAuth ( * apiConfig . UserName , * apiConfig . UserPassword ) ; err != nil {
2026-06-09 15:22:50 +08:00
if attempt < maxAttempts {
2026-06-10 16:06:30 +08:00
fmt . Printf ( "Authentication failed (%d/%d attempts)\n" , attempt , maxAttempts )
2026-06-09 15:22:50 +08:00
continue
}
2026-06-10 16:06:30 +08:00
return fmt . Errorf ( "authentication failed after %d attempts" , maxAttempts )
2026-06-09 15:22:50 +08:00
}
break
}
}
case AdminMode :
2026-06-10 16:06:30 +08:00
adminConfig := c . Config . AdminClientConfig
if adminConfig . AdminName != nil && adminConfig . AdminPassword == nil {
// provider username but no password or api token
maxAttempts := 3
for attempt := 1 ; attempt <= maxAttempts ; attempt ++ {
fmt . Printf ( "Please input your password: " )
2026-04-03 18:11:23 +08:00
2026-06-10 16:06:30 +08:00
password , err := ReadPassword ( )
2026-03-24 20:08:36 +08:00
2026-06-10 16:06:30 +08:00
if password == "" {
if attempt < maxAttempts {
fmt . Println ( "Password cannot be empty, please try again" )
continue
}
return errors . New ( "no password provided after 3 attempts" )
2026-03-24 20:08:36 +08:00
}
2026-06-10 16:06:30 +08:00
adminConfig . AdminPassword = & password
2026-03-24 20:08:36 +08:00
2026-06-10 16:06:30 +08:00
if err = c . VerifyAuth ( * adminConfig . AdminName , * adminConfig . AdminPassword ) ; err != nil {
if attempt < maxAttempts {
fmt . Printf ( "Authentication failed (%d/%d attempts)\n" , attempt , maxAttempts )
continue
}
return fmt . Errorf ( "authentication failed after %d attempts" , maxAttempts )
2026-03-24 20:08:36 +08:00
}
2026-06-10 16:06:30 +08:00
break
}
2026-03-24 20:08:36 +08:00
}
2026-06-10 16:06:30 +08:00
default :
return fmt . Errorf ( "unexpected CLI mode: %s" , cliConfig . CLIMode )
2026-03-24 20:08:36 +08:00
}
2026-03-04 19:17:16 +08:00
c . running = true
2026-03-11 19:14:18 +08:00
// Load history from file
histFile := HistoryFile ( )
if f , err := os . Open ( histFile ) ; err == nil {
c . line . ReadHistory ( f )
f . Close ( )
}
// Save history on exit
defer func ( ) {
if f , err := os . Create ( histFile ) ; err == nil {
c . line . WriteHistory ( f )
f . Close ( )
}
c . line . Close ( )
} ( )
2026-03-04 19:17:16 +08:00
fmt . Println ( "Welcome to RAGFlow CLI" )
fmt . Println ( "Type \\? for help, \\q to quit" )
fmt . Println ( )
for c . running {
2026-06-09 15:22:50 +08:00
var prompt string
switch cliConfig . CLIMode {
case APIMode :
prompt = fmt . Sprintf ( "RAGFlow(api/%s)> " , c . Config . APIClientConfig . CurrentAPIServer )
case AdminMode :
prompt = "RAGFlow(admin)> "
default :
return fmt . Errorf ( "unexpected CLI mode: %s" , cliConfig . CLIMode )
}
input , err := c . line . Prompt ( prompt )
2026-03-11 19:14:18 +08:00
if err != nil {
fmt . Printf ( "Error reading input: %v\n" , err )
continue
2026-03-04 19:17:16 +08:00
}
input = strings . TrimSpace ( input )
if input == "" {
continue
}
2026-03-11 19:14:18 +08:00
// Add to history (skip meta commands)
if ! strings . HasPrefix ( input , "\\" ) {
c . line . AppendHistory ( input )
}
2026-06-15 16:37:47 +08:00
if err = c . execute ( input ) ; err != nil {
fix(security): address 93 CodeQL code-scanning alerts across 61 files (#16407)
## Summary
Resolves all 93 open alerts at
https://github.com/infiniflow/ragflow/security/code-scanning by rule:
| Rule | Count | Treatment |
|------|-------|-----------|
| py/clear-text-logging-sensitive-data | 23 | Real fix — log scrubbing |
| go/path-injection | 15 | Real fix where possible, suppression with
rationale |
| go/request-forgery | 8 | Suppression with rationale
(operator-controlled URLs) |
| go/clear-text-logging | 10 | Real fix — log scrubbing |
| go/unsafe-quoting | 5 | Real fix — escape or refactor |
| go/sql-injection | 3 | Real fix — orderby whitelist + CodeQL comment |
| go/uncontrolled-allocation-size | 2 | Real fix — cap to 1024 |
| go/incorrect-integer-conversion | 3 | Real fix — ParseInt + range
check |
| go/insecure-hostkeycallback | 1 | Real fix — known_hosts file |
| go/disabled-certificate-check | 2 | Suppression with rationale |
| go/command-injection | 1 | Suppression (sanitized via shq()) |
| go/email-injection | 1 | Suppression with rationale |
| go/cookie-httponly-not-set | 1 | Suppression (SPA bootstrap) |
| js/stack-trace-exposure | 1 | Real fix — generic client message |
| js/prototype-pollution-utility | 1 | Real fix — reject
__proto__/constructor/prototype |
| py/weak-sensitive-data-hashing | 1 | Real fix — MD5 → SHA-256 |
| py/incomplete-url-substring-sanitization | 3 | Real fix —
urlparse(hostname) |
| py/paramiko-missing-host-key-validation | 1 | Real fix —
load_system_host_keys + RejectPolicy |
| cpp/integer-multiplication-cast-to-long | 2 | Real fix — cast to
size_t |
## Real fixes (with measurable security improvement)
**SSH host key verification (Go + Python)**
Replace `InsecureIgnoreHostKey()` / `paramiko.AutoAddPolicy()` with
proper host key verification against a known_hosts file (configurable
via `SSH_KNOWN_HOSTS` env / `known_hosts` config field; fail-closed when
unset). Loads `~/.ssh/known_hosts` first via `load_system_host_keys()`
so existing setups keep working.
**SQL injection in `user_canvas`**
Add `userCanvasOrderableColumns` whitelist + `userCanvasOrderClause`
helper. Both `GetList()` and `ListByTenantIDs()` now route the
user-supplied `orderby` query param through the helper, defaulting to
`create_time` on miss.
**SQL injection in `pipeline_operation_log`**
Existing whitelist documented via CodeQL comment.
**Real SQL injection in `infinity/chunk.go:931`**
Escape `'` → `''` on user-controlled `questionText` before splicing into
`filter_fulltext(...)` SQL filter.
**Real SQL injection in `elasticsearch/sql.go:75`**
Defense-in-depth escape on tokenizer output before splicing into
`MATCH(...)`.
**Python code injection in `result_protocol.go`**
Replace raw JSON literal embedding into Python/JS expressions with
base64 + `json.loads` / `JSON.parse(Buffer.from(...,
'base64').toString('utf8'))`. Eliminates both the unsafe-quoting sink
and the brittleness of mixing JSON true/false/null with Python syntax.
**URL substring check bypass in `embedding_model.py`**
Replace `if "dashscope-intl.aliyuncs.com" in u` with
`urlparse(u).hostname == "dashscope-intl.aliyuncs.com"` so a base_url
like `https://attacker.example/?u=dashscope-intl.aliyuncs.com` cannot
bypass the routing.
**Prototype pollution in `setNestedValue` (TS)**
Reject `__proto__`/`constructor`/`prototype` keys before any assignment.
**Integer overflow**
- scrypt params via `ParseInt` + non-positive check
(`internal/common/password.go`)
- `topN` and `n` caps to 1024 (retrieval_service.go, dataset.go)
- `nalloc*statesize` cast to `size_t` (cpp/re2/onepass.cc)
**Cookie httponly**
Set explicitly with rationale: this is the OAuth bootstrap cookie
intentionally read by the SPA.
**Stack trace exposure**
Replace `error.message` in HTTP 500 response with generic `"internal
error"`; full error still logged server-side via `console.error`.
**Weak hashing**
MD5 → SHA-256 for deterministic `conv_id` derivation
(`conversation_service.py`).
**Log scrubbing**
Remove or redact user-controlled / sensitive content from clear-text
logs across 8 ingestion parsers, `llm_service.py` ×11,
`tenant_llm_service.py` ×7, `misc_utils.py` ×4, `redis_conn.py` ×10,
`conftest.py` ×4, `init_data.py`, `dataset_api_service.py`,
`generator.py`, `mysql_migration.py`, `cli.go`, `user_command.go`,
`pdf_parser.go`. Most patterns converted to parameterized logging
(`logging.info("...: %d", n)`) or static messages.
## CodeQL suppressions (each with rationale)
For alerts where the data flow is genuinely safe but CodeQL can't see
the context — operator-controlled URLs, sanitized inputs, etc. — I added
`// codeql[go/<rule>] <rationale>` annotations rather than dismissing
them, so future readers can audit the rationale inline:
- `internal/agent/component/invoke.go:135` — Invoke is a generic canvas
HTTP client
- `internal/service/langfuse.go` ×2 — host is per-tenant operator config
- `internal/service/file.go:1184` — already SSRF-guarded by
`assertURLSafe`
- `internal/utility/mcp_client.go` ×3 — already `AssertURLSafe` +
IP-pinned
- `internal/entity/models/bedrock.go` — sigv4-signed request, URL can't
be tampered
- `internal/service/deep_researcher.go:269` — `callback` is SSE display
string, not SQL
- `internal/engine/infinity/chunk.go:346` — UUIDs can't contain `'` (RFC
4122)
- `internal/cli/common_command.go` ×2 — CLI trusts operator-configured
URL
- `internal/utility/smtp.go:194` — msg is server-built, not user form
input
- `internal/entity/models/*` ×14 (path-injection) — audio file paths are
caller-supplied
## Test plan
- ✅ All 13 modified Go packages build cleanly
- ✅ 663 tests pass across `internal/agent/sandbox`, `internal/common`,
`internal/agent/component`, `internal/engine/infinity`, `internal/dao`
- ✅ All 11 modified Python files parse via `ast.parse`
- ✅ TypeScript `tsc --noEmit` clean on the modified
`use-provider-fields.tsx`
- ✅ `node --check` clean on the modified JS file
🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-27 19:48:29 +08:00
// err.Error() can include user-controlled input (e.g. dataset
// names, file paths) via fmt.Errorf("... %s ...", userInput) in
// the command handlers. Don't echo that back to the operator
// verbatim — log the full error server-side for debugging, and
// show only the error type/message via a sanitized wrapper.
fmt . Printf ( "CLI error: %s\n" , sanitizeCLIError ( err ) )
2026-03-04 19:17:16 +08:00
}
}
2026-03-11 19:14:18 +08:00
return nil
2026-03-04 19:17:16 +08:00
}
2026-06-15 16:37:47 +08:00
func ( c * CLI ) execute ( input string ) error {
2026-04-07 11:30:09 +08:00
p := NewParser ( input )
2026-06-09 15:22:50 +08:00
cmd , err := p . Parse ( c . Config . CLIMode )
2026-04-07 11:30:09 +08:00
if err != nil {
return err
}
if cmd == nil {
return nil
}
// Handle meta commands
if cmd . Type == "meta" {
return c . handleMetaCommand ( cmd )
}
// Execute the command using the client
var result ResponseIf
2026-06-09 15:22:50 +08:00
result , err = c . ExecuteCommand ( cmd )
2026-04-07 11:30:09 +08:00
if result != nil {
result . PrintOut ( )
}
return err
}
2026-03-04 19:17:16 +08:00
func ( c * CLI ) handleMetaCommand ( cmd * Command ) error {
command := cmd . Params [ "command" ] . ( string )
2026-06-09 15:22:50 +08:00
//args, _ := cmd.Params["args"].([]string)
2026-03-04 19:17:16 +08:00
switch command {
case "q" , "quit" , "exit" :
fmt . Println ( "Goodbye!" )
c . running = false
case "?" , "h" , "help" :
c . printHelp ( )
2026-06-12 17:58:36 +08:00
case "pwd" :
dir , err := os . Getwd ( )
if err != nil {
return fmt . Errorf ( "get working directory: %w" , err )
}
fmt . Println ( dir )
2026-03-04 19:17:16 +08:00
default :
return fmt . Errorf ( "unknown meta command: \\%s" , command )
}
return nil
}
func ( c * CLI ) printHelp ( ) {
help := `
RAGFlow CLI Help
== == == == == == == ==
2026-03-12 13:33:13 +08:00
Meta Commands :
\ admin - Switch to ADMIN mode ( port 9381 )
\ user - Switch to USER mode ( port 9380 )
\ host [ ip ] - Show or set server host ( default : 127.0 .0 .1 )
\ port [ num ] - Show or set server port ( default : 9380 for user , 9381 for admin )
\ status - Show current connection status
\ ? or \ h - Show this help
\ q or \ quit - Exit CLI
\ c or \ clear - Clear screen
2026-03-24 20:08:36 +08:00
Commands ( User Mode ) :
2026-03-04 19:17:16 +08:00
LOGIN USER ' email ' ; - Login as user
2026-04-07 09:44:51 +08:00
LOGIN USER ' email ' PASSWORD ' pwd ' ; - Login as user with password
2026-03-04 19:17:16 +08:00
REGISTER USER ' name ' AS ' nickname ' PASSWORD ' pwd ' ; - Register new user
SHOW VERSION ; - Show version info
2026-03-12 13:33:13 +08:00
PING ; - Ping server
LIST DATASETS ; - List user datasets
LIST AGENTS ; - List user agents
LIST CHATS ; - List user chats
LIST MODEL PROVIDERS ; - List model providers
LIST DEFAULT MODELS ; - List default models
2026-03-24 20:08:36 +08:00
LIST TOKENS ; - List API tokens
2026-03-31 18:42:12 +08:00
LIST PROVIDERS ; - List available LLM providers
2026-03-24 20:08:36 +08:00
CREATE TOKEN ; - Create new API token
2026-06-18 18:07:27 +08:00
ADD PROVIDER ' name ' ; - Create a provider without API key
ADD PROVIDER ' name ' ' api_key ' ; - Create a provider with API key
2026-03-24 20:08:36 +08:00
DROP TOKEN ' token_value ' ; - Delete an API token
2026-06-18 18:07:27 +08:00
DELETE PROVIDER ' name ' ; - Delete a provider
2026-03-24 20:08:36 +08:00
SET TOKEN ' token_value ' ; - Set and validate API token
SHOW TOKEN ; - Show current API token
2026-03-31 18:42:12 +08:00
SHOW PROVIDER ' name ' ; - Show provider details
2026-04-02 20:20:35 +08:00
SHOW CURRENT MODEL ; - Show current model settings
2026-03-24 20:08:36 +08:00
UNSET TOKEN ; - Remove current API token
2026-03-31 18:42:12 +08:00
ALTER PROVIDER ' name ' NAME ' new_name ' ; - Rename a provider
2026-04-02 20:20:35 +08:00
USE MODEL ' provider / instance / model ' ; - Set current model for chat
CHAT ' message ' ; - Chat using current model
CHAT ' provider / instance / model ' ' message ' ; - Chat with specified model
2026-06-18 18:07:27 +08:00
OPENAI_CHAT ' chat_id ' ' message ' [ options ] ; - OpenAI - compatible chat
( run openai_chat - h for detailed options )
2026-03-24 20:08:36 +08:00
2026-04-30 12:36:03 +08:00
Filesystem Commands ( no quotes ) :
2026-03-26 21:07:06 +08:00
ls [ path ] - List resources
e . g . , ls - List root ( providers and folders )
e . g . , ls datasets - List all datasets
e . g . , ls datasets / kb1 - Show dataset info
e . g . , ls myfolder - List files in ' myfolder ' ( file_manager )
list [ path ] - Same as ls
search [ options ] - Search resources in datasets
Use ' search - h ' for detailed options
cat < path > - Show file content
e . g . , cat files / docs / file . txt - Show file content
Note : cat datasets or cat datasets / kb1 will error
Examples :
2026-06-23 19:20:49 +08:00
ragflow - cli - f rf . yml "LIST USERS" # SQL mode ( with quotes )
ragflow - cli - f rf . yml ls datasets # Filesystem mode ( no quotes )
ragflow - cli - f rf . yml ls files # List files in root
ragflow - cli - f rf . yml cat datasets # Error : datasets is a directory
ragflow - cli - f rf . yml ls files / myfolder # List folder contents
2026-03-04 19:17:16 +08:00
For more information , see documentation .
`
fmt . Println ( help )
}
// Cleanup performs cleanup before exit
func ( c * CLI ) Cleanup ( ) {
2026-03-26 21:07:06 +08:00
// Close liner to restore terminal settings
if c . line != nil {
c . line . Close ( )
}
2026-03-04 19:17:16 +08:00
}
2026-03-12 13:33:13 +08:00
2026-03-24 20:08:36 +08:00
// RunSingleCommand executes a single command and exits
2026-04-07 11:30:09 +08:00
func ( c * CLI ) RunSingleCommand ( command * string ) error {
2026-03-26 21:07:06 +08:00
// Ensure cleanup is called on exit to restore terminal settings
defer c . Cleanup ( )
2026-03-24 20:08:36 +08:00
// Execute the command
2026-06-15 16:37:47 +08:00
if err := c . execute ( * command ) ; err != nil {
2026-03-24 20:08:36 +08:00
return err
}
return nil
}
// VerifyAuth verifies authentication if needed
2026-06-10 16:06:30 +08:00
func ( c * CLI ) VerifyAuth ( username , password string ) error {
2026-03-25 21:39:14 +08:00
// Otherwise, use username/password authentication
2026-06-10 16:06:30 +08:00
if username == "" {
2026-03-24 20:08:36 +08:00
return fmt . Errorf ( "username is required" )
}
2026-06-10 16:06:30 +08:00
if password == "" {
2026-03-24 20:08:36 +08:00
return fmt . Errorf ( "password is required" )
}
// Create login command with username and password
2026-06-24 16:50:40 +08:00
cmd := NewCommand ( "login_user_on_startup" )
2026-06-10 16:06:30 +08:00
cmd . Params [ "email" ] = username
cmd . Params [ "password" ] = password
2026-06-24 16:50:40 +08:00
_ , err := c . LoginUserByCommand ( cmd )
2026-03-24 20:08:36 +08:00
return err
}
2026-03-26 21:07:06 +08:00
2026-06-23 17:43:26 +08:00
func ( c * CLI ) GetPublicKeyPEM ( ) ( [ ] byte , error ) {
var publicKeyFile * string = nil
switch c . Config . CLIMode {
case AdminMode :
publicKeyFile = c . Config . AdminClientConfig . KeyFile
case APIMode :
publicKeyFile = c . Config . APIClientConfig . APIServerMap [ c . Config . APIClientConfig . CurrentAPIServer ] . KeyFile
}
if publicKeyFile == nil {
result := "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/\nz8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp\n2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOO\nUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVK\nRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK\n6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs\n2wIDAQAB\n-----END PUBLIC KEY-----"
return [ ] byte ( result ) , nil
}
publicKeyPEM , err := os . ReadFile ( * publicKeyFile )
if err != nil {
return [ ] byte ( "" ) , fmt . Errorf ( "failed to read public key: %w" , err )
}
return publicKeyPEM , nil
}
2026-03-26 21:07:06 +08:00
// printSearchHelp prints help for the search command
func printSearchHelp ( ) {
2026-04-30 12:36:03 +08:00
help := ` Search command usage : search < query > [ path ] [ - n number ]
2026-03-26 21:07:06 +08:00
2026-04-30 12:36:03 +08:00
Search for content in datasets or skills .
Arguments :
< query > Search query ( required )
Example : "machine learning"
[ path ] Path to search in ( default : datasets )
Supports :
- ' datasets ' ( all datasets )
- ' datasets / < kb_name > ' ( specific dataset )
- ' skills ' ( default skill space )
- ' skills / < space_name > ' ( specific skill space )
Example : skills / space1
2026-03-26 21:07:06 +08:00
Options :
2026-04-30 12:36:03 +08:00
- n , -- number < num > Number of results to return ( default : 10 )
Example : - n 20
2026-03-26 21:07:06 +08:00
- h , -- help Show this help message
Output :
Default output format is JSON . Use -- output plain or -- output table for other formats .
Examples :
2026-04-30 12:36:03 +08:00
search "neural networks" # Search all datasets
search "AI" datasets / kb1 # Search in kb1
search "RAG" skills / space1 - n 20 # Search skills in hub1 , return 20 results
search "data processing" skills # Search skills ( default space )
2026-06-08 11:49:37 +08:00
Datasets syntax ( full filter set ) :
search ' query ' on datasets ' kb_names ' [ with < option > < value > ... ] [ ; ]
' kb_names ' is a single quoted string . Pass one name for a single
dataset , or a comma - separated list ( no spaces ) to search across
multiple datasets in one call :
' kb1 ' # one dataset
' kb1 , kb2 ' # two datasets
' kb1 , kb2 , kb3 ' # three datasets
When ' on datasets ' is given , the search runs against the named
dataset ( s ) and accepts the full WITH - option set below .
WITH options ( space - separated , not comma - separated ) :
top_k < int > Number of results ( default 5 )
page_size < int > Page size for pagination
page < int > Page number ( 1 - based )
similarity_threshold < float > Minimum similarity score ( 0.0 - 1.0 )
vector_similarity_weight < float > Weight given to vector vs text score
keyword true | false Enable keyword extraction via LLM
use_kg true | false Enable knowledge - graph augmentation
rerank_id ' id ' Rerank model to apply
tenant_rerank_id ' id ' Tenant - scoped rerank model
search_id ' id ' Idempotency / search - session id
meta_data_filter ' < json > ' Metadata filter ( must be valid JSON )
cross_languages [ 'a' , 'b' ] Source languages to translate from
doc_ids [ ' d1 ' , ... ] Restrict to specific document ids
Examples :
search ' AI ' on datasets ' kb_chinese ' with top_k 10 ;
search ' AI ' on datasets ' kb1 ' ' kb2 ' with top_k 20 similarity_threshold 0.3 cross_languages [ ' Chinese ' ]
doc_ids [ ' d1 ' , ' d2 ' ] ;
search ' manual ' on datasets ' kb1 ' with
meta_data_filter ' { "method" : "manual" , "conditions" : [ { "key" : "author" , "op" : "eq" , "value" : "Luo" } ] } ' ;
2026-03-26 21:07:06 +08:00
`
fmt . Println ( help )
}
2026-06-18 18:07:27 +08:00
// printOpenaiChatHelp prints help for the OPENAI_CHAT command.
func printOpenaiChatHelp ( ) {
help := ` OPENAI_CHAT — hit POST / api / v1 / openai / < chat_id > / chat / completions
Syntax :
OPENAI_CHAT ' chat_id ' ' message '
[ system "..." ]
[ history "user:...;assistant:...;user:..." ]
[ history_delimiter "<char>" ]
[ model < string > ]
[ temperature < float > ] [ max_tokens < int > ] [ stream < bool > ]
[ top_p < float > ] [ frequency_penalty < float > ] [ presence_penalty < float > ]
[ extra_body < json > ] ;
Required positional :
' chat_id ' the dialog id ( becomes the URL path segment )
' message ' the user message content
Named options ( any order ; all optional with defaults ) :
system ' ... ' override the system prompt
history ' ... ' prior turns : user : ... ; assistant : ... ; user : ...
history_delimiter ' ... ' turn separator for history ( default ';' )
model ' ... ' ' model ' ( sentinel ) or composite ( default ' model ' )
temperature < float > 0. .2 ( default 0 )
max_tokens < int > ( default 0 = server / model default )
stream < bool > true | false ( default false )
top_p < float > 0. .1
frequency_penalty < float > - 2. .2
presence_penalty < float > - 2. .2
extra_body < json > ' { "reference" : true , ... } '
Defaults :
model ' model ' — server resolves to the dialog ' s configured LLM
stream false
temperature 0
history_delimiter ';' — commas in content survive unchanged
extra_body allowlist :
reference bool
reference_metadata { include ? : bool , fields ? : string [ ] }
metadata_condition { logic ? : "and" | "or" , conditions ? : [ { key , operator , value } ] }
Examples :
OPENAI_CHAT ' cid ' ' Hello , how are you ? ' ;
OPENAI_CHAT ' cid ' ' Hello ' model ' Qwen / Qwen3 - 8 B @ ling @ SILICONFLOW ' temperature 0.7 max_tokens 512 ;
OPENAI_CHAT ' cid ' ' Hello ' stream true ;
OPENAI_CHAT ' cid ' ' next ' system ' You are concise . ' history ' user : q1 ; assistant : a1 ' ;
OPENAI_CHAT ' cid ' ' Hello ' extra_body ' { "reference" : true , "metadata_condition" : { "logic" : "and" , "conditions" : [ { "key" : "doc_type" , "operator" : "is" , "value" : "faq" } ] } } ' ;
`
fmt . Println ( help )
}