From 9b61afb04297e5b15ce3974e9f9c14cb80176e2a Mon Sep 17 00:00:00 2001 From: Kairos Date: Wed, 15 Apr 2026 10:46:55 +0800 Subject: [PATCH 001/275] fix(docs): resolve mermaid syntax errors Signed-off-by: Kairos --- backend/docs/flow_execution.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/docs/flow_execution.md b/backend/docs/flow_execution.md index d4303a99d..ee9309ec3 100644 --- a/backend/docs/flow_execution.md +++ b/backend/docs/flow_execution.md @@ -188,7 +188,7 @@ graph TD AssistantDirect[Assistant Agent
UseAgents=false] --> DirectTools[Direct Tools Only] - Note over AssistantUA,AssistantDirect: Operates independently
from Task/Subtask hierarchy + AssistantNote[Operates independently
from Task/Subtask hierarchy] end subgraph "Specialist Agent Tools" @@ -239,7 +239,7 @@ graph TD Enricher --> Memorist Enricher --> Searcher - Note over Adviser: Also used for:
- Mentor (execution monitoring)
- Planner (task planning) + AdviserNote[Also used for:
- Mentor execution monitoring
- Planner task planning] end subgraph "Error Correction" From 7214abeefb2b8a38f05961a90dd74151fa43ea98 Mon Sep 17 00:00:00 2001 From: Kairos Date: Wed, 15 Apr 2026 11:06:02 +0800 Subject: [PATCH 002/275] fix(docs): resolve mermaid syntax errors - added the previously stripped parentheses Signed-off-by: Kairos --- backend/docs/flow_execution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/docs/flow_execution.md b/backend/docs/flow_execution.md index ee9309ec3..39c518cc0 100644 --- a/backend/docs/flow_execution.md +++ b/backend/docs/flow_execution.md @@ -239,7 +239,7 @@ graph TD Enricher --> Memorist Enricher --> Searcher - AdviserNote[Also used for:
- Mentor execution monitoring
- Planner task planning] + AdviserNote["Also used for:
- Mentor (execution monitoring)
- Planner (task planning)"] end subgraph "Error Correction" From 72a5671562bf0f32d8f85929ec1422d994c264e2 Mon Sep 17 00:00:00 2001 From: "Mason Kim(ZINUS US_SALES)" Date: Wed, 15 Apr 2026 11:02:56 -0400 Subject: [PATCH 003/275] docs: clarify OAuth callback configuration Signed-off-by: Mason Kim(ZINUS US_SALES) --- README.md | 33 +++++++++++++++++++++++++++++++-- backend/docs/config.md | 27 ++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7ddc15baa..390cead84 100644 --- a/README.md +++ b/README.md @@ -2331,9 +2331,38 @@ OAuth integration with GitHub and Google allows users to authenticate using thei - Access to user profile information from GitHub/Google accounts - Seamless integration with existing development workflows -For using GitHub OAuth you need to create a new OAuth application in your GitHub account and set the `OAUTH_GITHUB_CLIENT_ID` and `OAUTH_GITHUB_CLIENT_SECRET` in `.env` file. +PentAGI uses `PUBLIC_URL` as the public base URL for OAuth redirects. In the default deployment, both GitHub and Google callbacks are handled by: -For using Google OAuth you need to create a new OAuth application in your Google account and set the `OAUTH_GOOGLE_CLIENT_ID` and `OAUTH_GOOGLE_CLIENT_SECRET` in `.env` file. +```text +${PUBLIC_URL}/auth/login-callback +``` + +For GitHub OAuth: + +1. Create a new OAuth App in your GitHub account. +2. Set **Homepage URL** to your `PUBLIC_URL`. +3. Set **Authorization callback URL** to `${PUBLIC_URL}/auth/login-callback`. +4. Add the client credentials to your `.env` file: + +```bash +PUBLIC_URL=https://pentagi.example.com +OAUTH_GITHUB_CLIENT_ID=your_github_client_id +OAUTH_GITHUB_CLIENT_SECRET=your_github_client_secret +``` + +For Google OAuth: + +1. Create OAuth credentials in your Google Cloud project. +2. Use the same callback endpoint: `${PUBLIC_URL}/auth/login-callback`. +3. Add the client credentials to your `.env` file: + +```bash +PUBLIC_URL=https://pentagi.example.com +OAUTH_GOOGLE_CLIENT_ID=your_google_client_id +OAUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret +``` + +Make sure `PUBLIC_URL` matches the externally accessible HTTPS address of your PentAGI instance. If the URL configured in the OAuth provider does not exactly match the callback generated by PentAGI, the provider will reject the login attempt with a redirect URI mismatch error. ### Docker Image Configuration diff --git a/backend/docs/config.md b/backend/docs/config.md index 26b2c342e..c2a65e91e 100644 --- a/backend/docs/config.md +++ b/backend/docs/config.md @@ -382,7 +382,7 @@ These settings control authentication mechanisms, including cookie-based session | Option | Environment Variable | Default Value | Description | | ----------------------- | ---------------------------- | ------------- | ------------------------------------------------------ | | CookieSigningSalt | `COOKIE_SIGNING_SALT` | *(none)* | Salt for signing and securing cookies used in sessions | -| PublicURL | `PUBLIC_URL` | *(none)* | Public URL for auth callbacks from OAuth providers | +| PublicURL | `PUBLIC_URL` | *(none)* | Public base URL used to build OAuth callback URLs such as `/auth/login-callback` | | OAuthGoogleClientID | `OAUTH_GOOGLE_CLIENT_ID` | *(none)* | Google OAuth client ID for authentication | | OAuthGoogleClientSecret | `OAUTH_GOOGLE_CLIENT_SECRET` | *(none)* | Google OAuth client secret | | OAuthGithubClientID | `OAUTH_GITHUB_CLIENT_ID` | *(none)* | GitHub OAuth client ID for authentication | @@ -407,6 +407,29 @@ The authentication settings are used in `pkg/server/router.go` to set up authent publicURL, err := url.Parse(cfg.PublicURL) ``` + The router builds the login callback path by appending `/auth/login-callback` to this public URL: + ```go + oauthLoginCallbackURL := "/auth/login-callback" + + publicURL, err := url.Parse(cfg.PublicURL) + if err == nil { + publicURL.Path = path.Join(baseURL, oauthLoginCallbackURL) + } + ``` + + In the default deployment, configure your OAuth providers with: + - **Homepage URL**: `PUBLIC_URL` + - **Authorization callback URL / Redirect URI**: `${PUBLIC_URL}/auth/login-callback` + + Example: + ```bash + PUBLIC_URL=https://pentagi.example.com + OAUTH_GITHUB_CLIENT_ID=your_github_client_id + OAUTH_GITHUB_CLIENT_SECRET=your_github_client_secret + OAUTH_GOOGLE_CLIENT_ID=your_google_client_id + OAUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret + ``` + - **OAuth Provider Settings**: Used to configure authentication with Google and GitHub: ```go // Google OAuth setup @@ -430,6 +453,8 @@ The authentication settings are used in `pkg/server/router.go` to set up authent } ``` + Google and GitHub both use the same PentAGI login callback endpoint. If the URL configured in the provider console does not exactly match the generated callback URL, authentication will fail with a redirect URI mismatch error. + These settings are essential for: - Secure user authentication and session management - Supporting social login through OAuth providers From 86d7666c8681ec513d68f4568f332d7e7dc891e7 Mon Sep 17 00:00:00 2001 From: "Mason Kim(ZINUS US_SALES)" Date: Wed, 15 Apr 2026 12:34:03 -0400 Subject: [PATCH 004/275] feat: add configurable terminal tool timeout Signed-off-by: Mason Kim(ZINUS US_SALES) --- .env.example | 4 + README.md | 1 + backend/cmd/ftester/mocks/tools.go | 2 +- backend/cmd/ftester/worker/executor.go | 3 + .../installer/wizard/controller/controller.go | 6 ++ backend/cmd/installer/wizard/locale/locale.go | 16 +++- .../wizard/models/server_settings_form.go | 26 +++++++ backend/docs/config.md | 17 +++++ backend/pkg/config/config.go | 1 + backend/pkg/config/config_test.go | 27 ++++++- backend/pkg/tools/args.go | 2 +- backend/pkg/tools/registry.go | 5 +- backend/pkg/tools/terminal.go | 73 +++++++++++++------ backend/pkg/tools/terminal_test.go | 45 ++++++++++++ backend/pkg/tools/tools.go | 8 ++ docker-compose.yml | 1 + 16 files changed, 207 insertions(+), 30 deletions(-) diff --git a/.env.example b/.env.example index 4d2795d6d..cb7033a1e 100644 --- a/.env.example +++ b/.env.example @@ -117,6 +117,10 @@ EXTERNAL_SSL_INSECURE= ## Default: 600 (10 minutes). Set to 0 to use the default. HTTP_CLIENT_TIMEOUT= +## Default terminal tool timeout in seconds when a command uses timeout=0 +## Default: 600 (10 minutes). Set to 0 to disable the server-side default timeout. +TERMINAL_TOOL_TIMEOUT= + ## Scraper URLs and settings ## For Docker (default): SCRAPER_PUBLIC_URL= diff --git a/README.md b/README.md index 7ddc15baa..88e5829eb 100644 --- a/README.md +++ b/README.md @@ -2871,6 +2871,7 @@ EMBEDDING_STRIP_NEW_LINES=true # Whether to remove new lines from text before e # Advanced settings PROXY_URL= # Optional proxy for all API calls HTTP_CLIENT_TIMEOUT=600 # Timeout in seconds for external API calls (default: 600, 0 = no timeout) +TERMINAL_TOOL_TIMEOUT=600 # Default timeout in seconds for terminal tool commands when timeout=0 (0 = no default timeout) # SSL/TLS Certificate Configuration (for external communication with LLM backends and tool servers) EXTERNAL_SSL_CA_PATH= # Path to custom CA certificate file (PEM format) inside the container diff --git a/backend/cmd/ftester/mocks/tools.go b/backend/cmd/ftester/mocks/tools.go index 949d5e05c..dbd462295 100644 --- a/backend/cmd/ftester/mocks/tools.go +++ b/backend/cmd/ftester/mocks/tools.go @@ -369,7 +369,7 @@ func MockResponse(funcName string, args json.RawMessage) (string, error) { builder.WriteString(fmt.Sprintf("# Subtask ID %d\n\n", searchMemoryArgs.SubtaskID.Int64())) } builder.WriteString("# Tool Name 'terminal'\n\n") - builder.WriteString("# Tool Description\n\nCalls a terminal command in blocking mode with hard limit timeout 1200 seconds and optimum timeout 60 seconds\n\n") + builder.WriteString("# Tool Description\n\nCalls a terminal command in blocking mode. Use timeout=0 to apply the server default from TERMINAL_TOOL_TIMEOUT (default 600 seconds), or provide an explicit timeout up to 1200 seconds\n\n") builder.WriteString("# Chunk\n\n") builder.WriteString(fmt.Sprintf("This is a memory chunk related to your questions '%s'. It contains information about previous commands, outputs, and relevant context that was stored in the vector database.\n\n", questionsText)) builder.WriteString("---------------------------\n") diff --git a/backend/cmd/ftester/worker/executor.go b/backend/cmd/ftester/worker/executor.go index bbd8e9975..70e992761 100644 --- a/backend/cmd/ftester/worker/executor.go +++ b/backend/cmd/ftester/worker/executor.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "time" "pentagi/cmd/ftester/mocks" "pentagi/pkg/config" @@ -135,6 +136,7 @@ func (te *toolExecutor) GetTool(ctx context.Context, funcName string) (tools.Too containerLID, te.dockerClient, te.proxies.GetTermLogProvider(), + time.Duration(te.cfg.TerminalToolTimeout)*time.Second, ), nil case tools.FileToolName: @@ -147,6 +149,7 @@ func (te *toolExecutor) GetTool(ctx context.Context, funcName string) (tools.Too containerLID, te.dockerClient, te.proxies.GetTermLogProvider(), + time.Duration(te.cfg.TerminalToolTimeout)*time.Second, ), nil case tools.BrowserToolName: diff --git a/backend/cmd/installer/wizard/controller/controller.go b/backend/cmd/installer/wizard/controller/controller.go index 6e0fe4cf4..3080a6323 100644 --- a/backend/cmd/installer/wizard/controller/controller.go +++ b/backend/cmd/installer/wizard/controller/controller.go @@ -1913,6 +1913,7 @@ type ServerSettingsConfig struct { CookieSigningSalt loader.EnvVar // COOKIE_SIGNING_SALT ProxyURL loader.EnvVar // PROXY_URL HTTPClientTimeout loader.EnvVar // HTTP_CLIENT_TIMEOUT + TerminalToolTimeout loader.EnvVar // TERMINAL_TOOL_TIMEOUT ExternalSSLCAPath loader.EnvVar // EXTERNAL_SSL_CA_PATH ExternalSSLInsecure loader.EnvVar // EXTERNAL_SSL_INSECURE SSLDir loader.EnvVar // PENTAGI_SSL_DIR @@ -1934,6 +1935,7 @@ func (c *controller) GetServerSettingsConfig() *ServerSettingsConfig { "COOKIE_SIGNING_SALT", "PROXY_URL", "HTTP_CLIENT_TIMEOUT", + "TERMINAL_TOOL_TIMEOUT", "EXTERNAL_SSL_CA_PATH", "EXTERNAL_SSL_INSECURE", "PENTAGI_SSL_DIR", @@ -1949,6 +1951,7 @@ func (c *controller) GetServerSettingsConfig() *ServerSettingsConfig { "PENTAGI_DATA_DIR": "pentagi-data", "PENTAGI_SSL_DIR": "pentagi-ssl", "HTTP_CLIENT_TIMEOUT": "600", + "TERMINAL_TOOL_TIMEOUT": "600", "EXTERNAL_SSL_INSECURE": "false", } @@ -1968,6 +1971,7 @@ func (c *controller) GetServerSettingsConfig() *ServerSettingsConfig { CookieSigningSalt: vars["COOKIE_SIGNING_SALT"], ProxyURL: vars["PROXY_URL"], HTTPClientTimeout: vars["HTTP_CLIENT_TIMEOUT"], + TerminalToolTimeout: vars["TERMINAL_TOOL_TIMEOUT"], ExternalSSLCAPath: vars["EXTERNAL_SSL_CA_PATH"], ExternalSSLInsecure: vars["EXTERNAL_SSL_INSECURE"], SSLDir: vars["PENTAGI_SSL_DIR"], @@ -2007,6 +2011,7 @@ func (c *controller) UpdateServerSettingsConfig(config *ServerSettingsConfig) er "COOKIE_SIGNING_SALT": config.CookieSigningSalt.Value, "PROXY_URL": proxyURL, "HTTP_CLIENT_TIMEOUT": config.HTTPClientTimeout.Value, + "TERMINAL_TOOL_TIMEOUT": config.TerminalToolTimeout.Value, "EXTERNAL_SSL_CA_PATH": config.ExternalSSLCAPath.Value, "EXTERNAL_SSL_INSECURE": config.ExternalSSLInsecure.Value, "PENTAGI_SSL_DIR": config.SSLDir.Value, @@ -2031,6 +2036,7 @@ func (c *controller) ResetServerSettingsConfig() *ServerSettingsConfig { "COOKIE_SIGNING_SALT", "PROXY_URL", "HTTP_CLIENT_TIMEOUT", + "TERMINAL_TOOL_TIMEOUT", "EXTERNAL_SSL_CA_PATH", "EXTERNAL_SSL_INSECURE", "PENTAGI_SSL_DIR", diff --git a/backend/cmd/installer/wizard/locale/locale.go b/backend/cmd/installer/wizard/locale/locale.go index b7c7f3602..9f7289982 100644 --- a/backend/cmd/installer/wizard/locale/locale.go +++ b/backend/cmd/installer/wizard/locale/locale.go @@ -1199,8 +1199,10 @@ const ( ServerSettingsProxyPassword = "Proxy Password" ServerSettingsProxyPasswordDesc = "Password for proxy authentication (optional)" - ServerSettingsHTTPClientTimeout = "HTTP Client Timeout" - ServerSettingsHTTPClientTimeoutDesc = "Timeout in seconds for external API calls (LLM providers, search engines, etc.)" + ServerSettingsHTTPClientTimeout = "HTTP Client Timeout" + ServerSettingsHTTPClientTimeoutDesc = "Timeout in seconds for external API calls (LLM providers, search engines, etc.)" + ServerSettingsTerminalToolTimeout = "Terminal Tool Timeout" + ServerSettingsTerminalToolTimeoutDesc = "Default timeout in seconds for terminal tool commands when timeout=0" ServerSettingsExternalSSLCAPath = "Custom CA Certificate Path" ServerSettingsExternalSSLCAPathDesc = "Path inside container to custom root CA cert (e.g., /opt/pentagi/ssl/ca-bundle.pem)" @@ -1227,6 +1229,7 @@ const ( ServerSettingsProxyUsernameHint = "Proxy Username" ServerSettingsProxyPasswordHint = "Proxy Password" ServerSettingsHTTPClientTimeoutHint = "HTTP Timeout" + ServerSettingsTerminalToolTimeoutHint = "Terminal Timeout" ServerSettingsExternalSSLCAPathHint = "Custom CA Path" ServerSettingsExternalSSLInsecureHint = "Skip SSL Verification" ServerSettingsSSLDirHint = "SSL Directory" @@ -1270,6 +1273,14 @@ Default: 600 seconds (10 minutes) Setting to 0 disables timeout (not recommended in production) Too low values may cause legitimate long-running requests to fail.` + ServerSettingsTerminalToolTimeoutHelp = `Default timeout in seconds for terminal tool commands when the tool call uses timeout=0. + +This affects commands executed through the isolated terminal container, including scanners and CLI-based utilities. + +Default: 600 seconds (10 minutes) +Setting to 0 disables the server-side default timeout +Explicit timeout values provided by the tool call still take precedence when they are within the normal range.` + ServerSettingsExternalSSLCAPathHelp = `Path to custom CA certificate file (PEM format) inside the container. Must point to /opt/pentagi/ssl/ directory, which is mounted from pentagi-ssl volume on the host. @@ -2277,6 +2288,7 @@ const ( EnvDesc_COOKIE_SIGNING_SALT = "PentAGI Cookie Signing Salt" EnvDesc_PROXY_URL = "HTTP/HTTPS Proxy URL" EnvDesc_HTTP_CLIENT_TIMEOUT = "HTTP Client Timeout (seconds)" + EnvDesc_TERMINAL_TOOL_TIMEOUT = "Terminal Tool Timeout (seconds)" EnvDesc_EXTERNAL_SSL_CA_PATH = "Custom CA Certificate Path" EnvDesc_EXTERNAL_SSL_INSECURE = "Skip SSL Verification" EnvDesc_PENTAGI_SSL_DIR = "PentAGI SSL Directory" diff --git a/backend/cmd/installer/wizard/models/server_settings_form.go b/backend/cmd/installer/wizard/models/server_settings_form.go index 817e420f4..d23728d53 100644 --- a/backend/cmd/installer/wizard/models/server_settings_form.go +++ b/backend/cmd/installer/wizard/models/server_settings_form.go @@ -101,6 +101,12 @@ func (m *ServerSettingsFormModel) BuildForm() tea.Cmd { config.HTTPClientTimeout, false, )) + fields = append(fields, m.createTextField("terminal_tool_timeout", + locale.ServerSettingsTerminalToolTimeout, + locale.ServerSettingsTerminalToolTimeoutDesc, + config.TerminalToolTimeout, + false, + )) // external ssl settings fields = append(fields, m.createTextField("external_ssl_ca_path", @@ -281,6 +287,14 @@ func (m *ServerSettingsFormModel) GetCurrentConfiguration() string { sections = append(sections, fmt.Sprintf("• %s: %s", locale.ServerSettingsHTTPClientTimeoutHint, httpTimeout)) } + if terminalTimeout := cfg.TerminalToolTimeout.Value; terminalTimeout != "" { + terminalTimeout = m.GetStyles().Info.Render(terminalTimeout + "s") + sections = append(sections, fmt.Sprintf("• %s: %s", locale.ServerSettingsTerminalToolTimeoutHint, terminalTimeout)) + } else if terminalTimeout := cfg.TerminalToolTimeout.Default; terminalTimeout != "" { + terminalTimeout = m.GetStyles().Muted.Render(terminalTimeout + "s") + sections = append(sections, fmt.Sprintf("• %s: %s", locale.ServerSettingsTerminalToolTimeoutHint, terminalTimeout)) + } + if externalSSLCAPath := cfg.ExternalSSLCAPath.Value; externalSSLCAPath != "" { externalSSLCAPath = m.GetStyles().Info.Render(externalSSLCAPath) sections = append(sections, fmt.Sprintf("• %s: %s", locale.ServerSettingsExternalSSLCAPathHint, externalSSLCAPath)) @@ -352,6 +366,8 @@ func (m *ServerSettingsFormModel) GetHelpContent() string { sections = append(sections, locale.ServerSettingsProxyURLHelp) case "http_client_timeout": sections = append(sections, locale.ServerSettingsHTTPClientTimeoutHelp) + case "terminal_tool_timeout": + sections = append(sections, locale.ServerSettingsTerminalToolTimeoutHelp) case "external_ssl_ca_path": sections = append(sections, locale.ServerSettingsExternalSSLCAPathHelp) case "external_ssl_insecure": @@ -382,6 +398,7 @@ func (m *ServerSettingsFormModel) HandleSave() error { CookieSigningSalt: cfg.CookieSigningSalt, ProxyURL: cfg.ProxyURL, HTTPClientTimeout: cfg.HTTPClientTimeout, + TerminalToolTimeout: cfg.TerminalToolTimeout, ExternalSSLCAPath: cfg.ExternalSSLCAPath, ExternalSSLInsecure: cfg.ExternalSSLInsecure, SSLDir: cfg.SSLDir, @@ -430,6 +447,15 @@ func (m *ServerSettingsFormModel) HandleSave() error { } } newCfg.HTTPClientTimeout.Value = value + case "terminal_tool_timeout": + if value != "" { + if timeout, err := strconv.Atoi(value); err != nil { + return fmt.Errorf("invalid terminal tool timeout: must be a number") + } else if timeout < 0 { + return fmt.Errorf("invalid terminal tool timeout: must be >= 0") + } + } + newCfg.TerminalToolTimeout.Value = value case "external_ssl_ca_path": newCfg.ExternalSSLCAPath.Value = value case "external_ssl_insecure": diff --git a/backend/docs/config.md b/backend/docs/config.md index 26b2c342e..438d668c4 100644 --- a/backend/docs/config.md +++ b/backend/docs/config.md @@ -209,6 +209,7 @@ These settings control how PentAGI interacts with Docker, which is used for term | DockerWorkDir | `DOCKER_WORK_DIR` | *(none)* | Custom working directory inside Docker containers | | DockerDefaultImage | `DOCKER_DEFAULT_IMAGE` | `debian:latest` | Default Docker image for containers when specific images fail | | DockerDefaultImageForPentest | `DOCKER_DEFAULT_IMAGE_FOR_PENTEST` | `vxcontrol/kali-linux` | Default Docker image for penetration testing tasks | +| TerminalToolTimeout | `TERMINAL_TOOL_TIMEOUT` | `600` | Default timeout in seconds for terminal tool commands when timeout is set to `0` (0 = no default timeout) | ### Usage Details @@ -227,6 +228,22 @@ The Docker settings are primarily used in `pkg/docker/client.go` which implement } ``` +- **TerminalToolTimeout**: Sets the default execution timeout for terminal tool commands when the tool call uses `timeout=0`: + ```go + term := NewTerminalTool( + flowID, + taskID, + subtaskID, + containerID, + containerLID, + dockerClient, + termLogProvider, + time.Duration(cfg.TerminalToolTimeout)*time.Second, + ) + ``` + + Use `0` to disable the server-side default timeout. Explicit timeout values provided by the tool call still take precedence when they are within the normal range. + - **DockerNetwork**: Controls the network isolation mode for containers. Supports two modes: **Bridge Mode** (custom network name, e.g., `pentagi-network`): diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index 0f88c97c4..b4d649c45 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -35,6 +35,7 @@ type Config struct { DockerWorkDir string `env:"DOCKER_WORK_DIR"` DockerDefaultImage string `env:"DOCKER_DEFAULT_IMAGE" envDefault:"debian:latest"` DockerDefaultImageForPentest string `env:"DOCKER_DEFAULT_IMAGE_FOR_PENTEST" envDefault:"vxcontrol/kali-linux"` + TerminalToolTimeout int `env:"TERMINAL_TOOL_TIMEOUT" envDefault:"600"` // === API Server Configuration === ServerPort int `env:"SERVER_PORT" envDefault:"8080"` diff --git a/backend/pkg/config/config_test.go b/backend/pkg/config/config_test.go index 10f7c7878..9927a599c 100644 --- a/backend/pkg/config/config_test.go +++ b/backend/pkg/config/config_test.go @@ -271,7 +271,7 @@ func clearConfigEnv(t *testing.T) { envVars := []string{ "DATABASE_URL", "DEBUG", "DATA_DIR", "ASK_USER", "INSTALLATION_ID", "LICENSE_KEY", "DOCKER_INSIDE", "DOCKER_NET_ADMIN", "DOCKER_SOCKET", "DOCKER_NETWORK", - "DOCKER_PUBLIC_IP", "DOCKER_WORK_DIR", "DOCKER_DEFAULT_IMAGE", "DOCKER_DEFAULT_IMAGE_FOR_PENTEST", + "DOCKER_PUBLIC_IP", "DOCKER_WORK_DIR", "DOCKER_DEFAULT_IMAGE", "DOCKER_DEFAULT_IMAGE_FOR_PENTEST", "TERMINAL_TOOL_TIMEOUT", "SERVER_PORT", "SERVER_HOST", "SERVER_USE_SSL", "SERVER_SSL_KEY", "SERVER_SSL_CRT", "STATIC_URL", "STATIC_DIR", "CORS_ORIGINS", "COOKIE_SIGNING_SALT", "SCRAPER_PUBLIC_URL", "SCRAPER_PRIVATE_URL", @@ -548,6 +548,31 @@ func TestNewConfig_HTTPClientTimeout(t *testing.T) { }) } +func TestNewConfig_TerminalToolTimeout(t *testing.T) { + clearConfigEnv(t) + t.Chdir(t.TempDir()) + + t.Run("default timeout", func(t *testing.T) { + config, err := NewConfig() + require.NoError(t, err) + assert.Equal(t, 600, config.TerminalToolTimeout) + }) + + t.Run("custom timeout", func(t *testing.T) { + t.Setenv("TERMINAL_TOOL_TIMEOUT", "900") + config, err := NewConfig() + require.NoError(t, err) + assert.Equal(t, 900, config.TerminalToolTimeout) + }) + + t.Run("zero timeout", func(t *testing.T) { + t.Setenv("TERMINAL_TOOL_TIMEOUT", "0") + config, err := NewConfig() + require.NoError(t, err) + assert.Equal(t, 0, config.TerminalToolTimeout) + }) +} + func TestNewConfig_AgentSupervisionDefaults(t *testing.T) { clearConfigEnv(t) t.Chdir(t.TempDir()) diff --git a/backend/pkg/tools/args.go b/backend/pkg/tools/args.go index f6cab856e..62334c133 100644 --- a/backend/pkg/tools/args.go +++ b/backend/pkg/tools/args.go @@ -94,7 +94,7 @@ type TerminalAction struct { Input string `json:"input" jsonschema:"required" jsonschema_description:"Command to be run in the docker container terminal according to rules to execute commands"` Cwd string `json:"cwd" jsonschema:"required" jsonschema_description:"Custom current working directory to execute commands in or default directory otherwise if it's not specified"` Detach Bool `json:"detach" jsonschema:"required,type=boolean" jsonschema_description:"Set to true for INTERACTIVE or LONG-RUNNING commands: shells (msfconsole, bash, python), listeners (nc -lvnp, socat TCP-LISTEN), servers (python -m http.server, php -S), monitors (tcpdump, tail -f). These commands expect user input or run indefinitely. When true: command runs in background, you get immediate confirmation, no stdout/stderr captured. When false: command must complete within timeout and return output. For quick batch commands (nmap, curl, ls) use false"` - Timeout Int64 `json:"timeout" jsonschema:"required,type=integer" jsonschema_description:"Execution time limit in seconds (minimum 10; maximum 1200; default 60). For batch commands that may run long, use the 'timeout' shell utility INSIDE your command to ensure clean completion with full output: 'timeout 55 nmap -sV target' (set 5-10 seconds less than this parameter). For interactive/long-running commands, use detach=true instead of relying solely on timeout"` + Timeout Int64 `json:"timeout" jsonschema:"required,type=integer" jsonschema_description:"Execution time limit in seconds. Use 0 to apply the server default from TERMINAL_TOOL_TIMEOUT (default 600, 0 = no timeout). Explicit values should normally stay within 10-1200 seconds. For batch commands that may run long, use the 'timeout' shell utility INSIDE your command to ensure clean completion with full output: 'timeout 55 nmap -sV target' (set 5-10 seconds less than this parameter). For interactive/long-running commands, use detach=true instead of relying solely on timeout"` Message string `json:"message" jsonschema:"required,title=Terminal command message" jsonschema_description:"Not so long message which explain what do you want to achieve and to execute in terminal to send to the user in user's language only"` } diff --git a/backend/pkg/tools/registry.go b/backend/pkg/tools/registry.go index 4bcf2b5d0..a7efa2870 100644 --- a/backend/pkg/tools/registry.go +++ b/backend/pkg/tools/registry.go @@ -156,8 +156,9 @@ var allowedStoringInMemoryTools = []string{ var registryDefinitions = map[string]llms.FunctionDefinition{ TerminalToolName: { Name: TerminalToolName, - Description: "Calls a terminal command in blocking mode with hard limit timeout 1200 seconds and " + - "optimum timeout 60 seconds, only one command can be executed at a time", + Description: "Calls a terminal command in blocking mode. Use timeout=0 to apply the server default " + + "from TERMINAL_TOOL_TIMEOUT (default 600 seconds), or provide an explicit timeout up to 1200 seconds. " + + "Only one command can be executed at a time", Parameters: reflector.Reflect(&TerminalAction{}), }, FileToolName: { diff --git a/backend/pkg/tools/terminal.go b/backend/pkg/tools/terminal.go index 767c4e7d5..cfac289b1 100644 --- a/backend/pkg/tools/terminal.go +++ b/backend/pkg/tools/terminal.go @@ -22,9 +22,10 @@ import ( ) const ( - defaultExecCommandTimeout = 5 * time.Minute - defaultExtraExecTimeout = 5 * time.Second - defaultQuickCheckTimeout = 500 * time.Millisecond + maxExplicitExecCommandTimeout = 20 * time.Minute + defaultExtraExecTimeout = 5 * time.Second + maxRuntimeExecCommandTimeout = maxExplicitExecCommandTimeout + defaultExtraExecTimeout + defaultQuickCheckTimeout = 500 * time.Millisecond // ANSI terminal color codes (aligned with PentAGI UI palette) ansiColorInputCmd = "\033[96m" // Bright Cyan - matches UI blue accents @@ -39,13 +40,14 @@ type execResult struct { } type terminal struct { - flowID int64 - taskID *int64 - subtaskID *int64 - containerID int64 - containerLID string - dockerClient docker.DockerClient - tlp TermLogProvider + flowID int64 + taskID *int64 + subtaskID *int64 + containerID int64 + containerLID string + dockerClient docker.DockerClient + tlp TermLogProvider + defaultExecTimeout time.Duration } func NewTerminalTool( @@ -54,15 +56,36 @@ func NewTerminalTool( containerID int64, containerLID string, dockerClient docker.DockerClient, tlp TermLogProvider, + defaultExecTimeout time.Duration, ) Tool { return &terminal{ - flowID: flowID, - taskID: taskID, - subtaskID: subtaskID, - containerID: containerID, - containerLID: containerLID, - dockerClient: dockerClient, - tlp: tlp, + flowID: flowID, + taskID: taskID, + subtaskID: subtaskID, + containerID: containerID, + containerLID: containerLID, + dockerClient: dockerClient, + tlp: tlp, + defaultExecTimeout: defaultExecTimeout, + } +} + +func (t *terminal) configuredExecTimeout() time.Duration { + if t.defaultExecTimeout < 0 { + return 0 + } + + return t.defaultExecTimeout +} + +func (t *terminal) normalizeExecTimeout(timeout time.Duration) time.Duration { + switch { + case timeout > 0 && timeout <= maxRuntimeExecCommandTimeout: + return timeout + case timeout > maxRuntimeExecCommandTimeout: + return t.configuredExecTimeout() + default: + return t.configuredExecTimeout() } } @@ -106,7 +129,10 @@ func (t *terminal) Handle(ctx context.Context, name string, args json.RawMessage logger.WithError(err).Error("failed to unmarshal terminal action") return "", fmt.Errorf("failed to unmarshal terminal action: %w", err) } - timeout := time.Duration(action.Timeout)*time.Second + defaultExtraExecTimeout + timeout := t.normalizeExecTimeout(time.Duration(action.Timeout) * time.Second) + if timeout > 0 { + timeout += defaultExtraExecTimeout + } result, err := t.ExecCommand(ctx, action.Cwd, action.Input, action.Detach.Bool(), timeout) return t.wrapCommandResult(ctx, args, name, result, err) case FileToolName: @@ -172,9 +198,7 @@ func (t *terminal) ExecCommand( return "", fmt.Errorf("failed to put terminal log (stdin): %w", err) } - if timeout <= 0 || timeout > 20*time.Minute { - timeout = defaultExecCommandTimeout - } + timeout = t.normalizeExecTimeout(timeout) createResp, err := t.dockerClient.ContainerExecCreate(ctx, containerName, container.ExecOptions{ Cmd: cmd, @@ -214,8 +238,11 @@ func (t *terminal) ExecCommand( } func (t *terminal) getExecResult(ctx context.Context, id string, timeout time.Duration) (string, error) { - ctx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() + if timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + } // attach to the exec process resp, err := t.dockerClient.ContainerExecAttach(ctx, id, container.ExecAttachOptions{ diff --git a/backend/pkg/tools/terminal_test.go b/backend/pkg/tools/terminal_test.go index defe0eb81..ff26a77b2 100644 --- a/backend/pkg/tools/terminal_test.go +++ b/backend/pkg/tools/terminal_test.go @@ -208,3 +208,48 @@ func TestPrimaryTerminalName(t *testing.T) { }) } } + +func TestNormalizeExecTimeout(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + configured time.Duration + requested time.Duration + want time.Duration + }{ + { + name: "explicit timeout is preserved", + configured: 10 * time.Minute, + requested: 45 * time.Second, + want: 45 * time.Second, + }, + { + name: "zero requested timeout uses configured default", + configured: 10 * time.Minute, + requested: 0, + want: 10 * time.Minute, + }, + { + name: "too large explicit timeout falls back to configured default", + configured: 10 * time.Minute, + requested: maxRuntimeExecCommandTimeout + time.Second, + want: 10 * time.Minute, + }, + { + name: "configured zero keeps timeout disabled", + configured: 0, + requested: 0, + want: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + term := &terminal{defaultExecTimeout: tt.configured} + assert.Equal(t, tt.want, term.normalizeExecTimeout(tt.requested)) + }) + } +} diff --git a/backend/pkg/tools/tools.go b/backend/pkg/tools/tools.go index 531e296c4..a900aa236 100644 --- a/backend/pkg/tools/tools.go +++ b/backend/pkg/tools/tools.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "time" "pentagi/pkg/config" "pentagi/pkg/database" @@ -528,6 +529,7 @@ func (fte *flowToolsExecutor) GetAssistantExecutor(cfg AssistantExecutorConfig) container.LocalID.String, fte.docker, fte.tlp, + time.Duration(fte.cfg.TerminalToolTimeout)*time.Second, ) definitions := []llms.FunctionDefinition{ @@ -799,6 +801,7 @@ func (fte *flowToolsExecutor) GetInstallerExecutor(cfg InstallerExecutorConfig) container.LocalID.String, fte.docker, fte.tlp, + time.Duration(fte.cfg.TerminalToolTimeout)*time.Second, ) ce := &customExecutor{ @@ -993,6 +996,7 @@ func (fte *flowToolsExecutor) GetPentesterExecutor(cfg PentesterExecutorConfig) container.LocalID.String, fte.docker, fte.tlp, + time.Duration(fte.cfg.TerminalToolTimeout)*time.Second, ) ce := &customExecutor{ @@ -1256,6 +1260,7 @@ func (fte *flowToolsExecutor) GetGeneratorExecutor(cfg GeneratorExecutorConfig) container.LocalID.String, fte.docker, fte.tlp, + time.Duration(fte.cfg.TerminalToolTimeout)*time.Second, ) ce := &customExecutor{ @@ -1321,6 +1326,7 @@ func (fte *flowToolsExecutor) GetRefinerExecutor(cfg RefinerExecutorConfig) (Con container.LocalID.String, fte.docker, fte.tlp, + time.Duration(fte.cfg.TerminalToolTimeout)*time.Second, ) ce := &customExecutor{ @@ -1382,6 +1388,7 @@ func (fte *flowToolsExecutor) GetMemoristExecutor(cfg MemoristExecutorConfig) (C container.LocalID.String, fte.docker, fte.tlp, + time.Duration(fte.cfg.TerminalToolTimeout)*time.Second, ) ce := &customExecutor{ @@ -1450,6 +1457,7 @@ func (fte *flowToolsExecutor) GetEnricherExecutor(cfg EnricherExecutorConfig) (C container.LocalID.String, fte.docker, fte.tlp, + time.Duration(fte.cfg.TerminalToolTimeout)*time.Second, ) ce := &customExecutor{ diff --git a/docker-compose.yml b/docker-compose.yml index 6a78a7efc..f706b1661 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -112,6 +112,7 @@ services: - EXTERNAL_SSL_CA_PATH=${EXTERNAL_SSL_CA_PATH:-} - EXTERNAL_SSL_INSECURE=${EXTERNAL_SSL_INSECURE:-} - HTTP_CLIENT_TIMEOUT=${HTTP_CLIENT_TIMEOUT:-} + - TERMINAL_TOOL_TIMEOUT=${TERMINAL_TOOL_TIMEOUT:-} - SCRAPER_PUBLIC_URL=${SCRAPER_PUBLIC_URL:-} - SCRAPER_PRIVATE_URL=${SCRAPER_PRIVATE_URL:-} - GRAPHITI_ENABLED=${GRAPHITI_ENABLED:-} From ef195de84a520f85705de542c5a8cec0943e3e7d Mon Sep 17 00:00:00 2001 From: "Mason Kim(ZINUS US_SALES)" Date: Wed, 15 Apr 2026 13:14:06 -0400 Subject: [PATCH 005/275] docs: fix OAuth callback URL docs --- README.md | 10 +++++----- backend/docs/config.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 390cead84..1ff9872d2 100644 --- a/README.md +++ b/README.md @@ -2331,17 +2331,17 @@ OAuth integration with GitHub and Google allows users to authenticate using thei - Access to user profile information from GitHub/Google accounts - Seamless integration with existing development workflows -PentAGI uses `PUBLIC_URL` as the public base URL for OAuth redirects. In the default deployment, both GitHub and Google callbacks are handled by: +PentAGI uses `PUBLIC_URL` as the public origin/base URL for OAuth redirects. In the default deployment, both GitHub and Google callbacks are handled by: ```text -${PUBLIC_URL}/auth/login-callback +${PUBLIC_URL}/api/v1/auth/login-callback ``` For GitHub OAuth: 1. Create a new OAuth App in your GitHub account. 2. Set **Homepage URL** to your `PUBLIC_URL`. -3. Set **Authorization callback URL** to `${PUBLIC_URL}/auth/login-callback`. +3. Set **Authorization callback URL** to `${PUBLIC_URL}/api/v1/auth/login-callback`. 4. Add the client credentials to your `.env` file: ```bash @@ -2353,7 +2353,7 @@ OAUTH_GITHUB_CLIENT_SECRET=your_github_client_secret For Google OAuth: 1. Create OAuth credentials in your Google Cloud project. -2. Use the same callback endpoint: `${PUBLIC_URL}/auth/login-callback`. +2. Use the same callback endpoint: `${PUBLIC_URL}/api/v1/auth/login-callback`. 3. Add the client credentials to your `.env` file: ```bash @@ -2362,7 +2362,7 @@ OAUTH_GOOGLE_CLIENT_ID=your_google_client_id OAUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret ``` -Make sure `PUBLIC_URL` matches the externally accessible HTTPS address of your PentAGI instance. If the URL configured in the OAuth provider does not exactly match the callback generated by PentAGI, the provider will reject the login attempt with a redirect URI mismatch error. +Make sure `PUBLIC_URL` matches the externally accessible HTTPS address of your PentAGI instance and does not include the callback path itself. If the URL configured in the OAuth provider does not exactly match the callback generated by PentAGI, the provider will reject the login attempt with a redirect URI mismatch error. ### Docker Image Configuration diff --git a/backend/docs/config.md b/backend/docs/config.md index c2a65e91e..1bbc1455c 100644 --- a/backend/docs/config.md +++ b/backend/docs/config.md @@ -382,7 +382,7 @@ These settings control authentication mechanisms, including cookie-based session | Option | Environment Variable | Default Value | Description | | ----------------------- | ---------------------------- | ------------- | ------------------------------------------------------ | | CookieSigningSalt | `COOKIE_SIGNING_SALT` | *(none)* | Salt for signing and securing cookies used in sessions | -| PublicURL | `PUBLIC_URL` | *(none)* | Public base URL used to build OAuth callback URLs such as `/auth/login-callback` | +| PublicURL | `PUBLIC_URL` | *(none)* | Public origin/base URL used to build OAuth callback URLs such as `/api/v1/auth/login-callback` | | OAuthGoogleClientID | `OAUTH_GOOGLE_CLIENT_ID` | *(none)* | Google OAuth client ID for authentication | | OAuthGoogleClientSecret | `OAUTH_GOOGLE_CLIENT_SECRET` | *(none)* | Google OAuth client secret | | OAuthGithubClientID | `OAUTH_GITHUB_CLIENT_ID` | *(none)* | GitHub OAuth client ID for authentication | @@ -402,12 +402,12 @@ The authentication settings are used in `pkg/server/router.go` to set up authent router.Use(sessions.Sessions("auth", cookieStore)) ``` -- **PublicURL**: The base URL for OAuth callback endpoints, crucial for redirects after authentication: +- **PublicURL**: The public origin/base URL for OAuth callback endpoints, crucial for redirects after authentication: ```go publicURL, err := url.Parse(cfg.PublicURL) ``` - The router builds the login callback path by appending `/auth/login-callback` to this public URL: + The router builds the login callback path under the API base URL: ```go oauthLoginCallbackURL := "/auth/login-callback" @@ -419,7 +419,7 @@ The authentication settings are used in `pkg/server/router.go` to set up authent In the default deployment, configure your OAuth providers with: - **Homepage URL**: `PUBLIC_URL` - - **Authorization callback URL / Redirect URI**: `${PUBLIC_URL}/auth/login-callback` + - **Authorization callback URL / Redirect URI**: `${PUBLIC_URL}/api/v1/auth/login-callback` Example: ```bash @@ -453,7 +453,7 @@ The authentication settings are used in `pkg/server/router.go` to set up authent } ``` - Google and GitHub both use the same PentAGI login callback endpoint. If the URL configured in the provider console does not exactly match the generated callback URL, authentication will fail with a redirect URI mismatch error. + Google and GitHub both use the same PentAGI login callback endpoint. `PUBLIC_URL` should be the externally reachable base URL only, without an extra path suffix. If the URL configured in the provider console does not exactly match the generated callback URL, authentication will fail with a redirect URI mismatch error. These settings are essential for: - Secure user authentication and session management From 1d6da349d41a066fbfdb9cc6368cfb5d281d4d33 Mon Sep 17 00:00:00 2001 From: "Mason Kim(ZINUS US_SALES)" Date: Wed, 15 Apr 2026 13:14:16 -0400 Subject: [PATCH 006/275] fix: clamp oversized terminal timeout --- .../installer/wizard/models/server_settings_form.go | 4 ++-- backend/pkg/tools/terminal.go | 2 +- backend/pkg/tools/terminal_test.go | 10 ++++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/cmd/installer/wizard/models/server_settings_form.go b/backend/cmd/installer/wizard/models/server_settings_form.go index d23728d53..38abb0988 100644 --- a/backend/cmd/installer/wizard/models/server_settings_form.go +++ b/backend/cmd/installer/wizard/models/server_settings_form.go @@ -289,10 +289,10 @@ func (m *ServerSettingsFormModel) GetCurrentConfiguration() string { if terminalTimeout := cfg.TerminalToolTimeout.Value; terminalTimeout != "" { terminalTimeout = m.GetStyles().Info.Render(terminalTimeout + "s") - sections = append(sections, fmt.Sprintf("• %s: %s", locale.ServerSettingsTerminalToolTimeoutHint, terminalTimeout)) + sections = append(sections, fmt.Sprintf("• %s: %s", locale.ServerSettingsTerminalToolTimeoutHint, terminalTimeout)) } else if terminalTimeout := cfg.TerminalToolTimeout.Default; terminalTimeout != "" { terminalTimeout = m.GetStyles().Muted.Render(terminalTimeout + "s") - sections = append(sections, fmt.Sprintf("• %s: %s", locale.ServerSettingsTerminalToolTimeoutHint, terminalTimeout)) + sections = append(sections, fmt.Sprintf("• %s: %s", locale.ServerSettingsTerminalToolTimeoutHint, terminalTimeout)) } if externalSSLCAPath := cfg.ExternalSSLCAPath.Value; externalSSLCAPath != "" { diff --git a/backend/pkg/tools/terminal.go b/backend/pkg/tools/terminal.go index cfac289b1..8ad995194 100644 --- a/backend/pkg/tools/terminal.go +++ b/backend/pkg/tools/terminal.go @@ -83,7 +83,7 @@ func (t *terminal) normalizeExecTimeout(timeout time.Duration) time.Duration { case timeout > 0 && timeout <= maxRuntimeExecCommandTimeout: return timeout case timeout > maxRuntimeExecCommandTimeout: - return t.configuredExecTimeout() + return maxRuntimeExecCommandTimeout default: return t.configuredExecTimeout() } diff --git a/backend/pkg/tools/terminal_test.go b/backend/pkg/tools/terminal_test.go index ff26a77b2..de18809fb 100644 --- a/backend/pkg/tools/terminal_test.go +++ b/backend/pkg/tools/terminal_test.go @@ -231,10 +231,10 @@ func TestNormalizeExecTimeout(t *testing.T) { want: 10 * time.Minute, }, { - name: "too large explicit timeout falls back to configured default", + name: "too large explicit timeout clamps to max runtime timeout", configured: 10 * time.Minute, requested: maxRuntimeExecCommandTimeout + time.Second, - want: 10 * time.Minute, + want: maxRuntimeExecCommandTimeout, }, { name: "configured zero keeps timeout disabled", @@ -242,6 +242,12 @@ func TestNormalizeExecTimeout(t *testing.T) { requested: 0, want: 0, }, + { + name: "configured zero does not disable oversized explicit timeout limit", + configured: 0, + requested: maxRuntimeExecCommandTimeout + time.Second, + want: maxRuntimeExecCommandTimeout, + }, } for _, tt := range tests { From f984c7865e0846fd4797d49c84158a1dad656036 Mon Sep 17 00:00:00 2001 From: "Mason Kim(ZINUS US_SALES)" Date: Wed, 15 Apr 2026 13:45:45 -0400 Subject: [PATCH 007/275] docs: add Docker mirror guidance for restricted networks Signed-off-by: Mason Kim(ZINUS US_SALES) --- README.md | 31 ++++++++++++++++++++++++++++++- backend/docs/installer/checker.md | 15 +++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ddc15baa..9a14f8ef0 100644 --- a/README.md +++ b/README.md @@ -2341,10 +2341,13 @@ PentAGI allows you to configure Docker image selection for executing various tas | Variable | Default | Description | | ---------------------------------- | ---------------------- | ----------------------------------------------------------- | +| `PENTAGI_IMAGE` | `vxcontrol/pentagi:latest` | Docker image used for the main PentAGI application service | | `DOCKER_DEFAULT_IMAGE` | `debian:latest` | Default Docker image for general tasks and ambiguous cases | | `DOCKER_DEFAULT_IMAGE_FOR_PENTEST` | `vxcontrol/kali-linux` | Default Docker image for security/penetration testing tasks | -When these environment variables are set, AI agents will be limited to the image choices you specify. This is particularly useful for: +`PENTAGI_IMAGE` changes the image used by the main `pentagi` service in `docker-compose.yml`. The `DOCKER_DEFAULT_IMAGE` and `DOCKER_DEFAULT_IMAGE_FOR_PENTEST` variables only affect automatic worker image selection for task execution inside PentAGI. They do not rewrite the rest of the Compose stack, so services such as `pgvector`, `scraper`, and the optional `graphiti` stack still use the image references defined in the compose files. + +When `DOCKER_DEFAULT_IMAGE` and `DOCKER_DEFAULT_IMAGE_FOR_PENTEST` are set, AI agents will be limited to the image choices you specify. This is particularly useful for: - **Security Enforcement**: Restricting usage to only verified and trusted images - **Environment Standardization**: Using corporate or customized images across all operations @@ -2353,6 +2356,9 @@ When these environment variables are set, AI agents will be limited to the image Configuration examples: ```bash +# Using a custom PentAGI application image +PENTAGI_IMAGE=registry.example.com/security/pentagi:latest + # Using a custom image for general tasks DOCKER_DEFAULT_IMAGE=mycompany/custom-debian:latest @@ -2363,6 +2369,29 @@ DOCKER_DEFAULT_IMAGE_FOR_PENTEST=mycompany/pentest-tools:v2.0 > [!NOTE] > If a user explicitly specifies a particular Docker image in their task, the system will try to use that exact image, ignoring these settings. These variables only affect the system's automatic image selection process. +#### Restricted Networks, Docker Mirrors, and Proxies + +If your environment cannot reach Docker Hub (`docker.io`) directly, changing PentAGI environment variables is usually not enough to fix image download failures. PentAGI still relies on Docker's own registry access for Compose-managed services, and the installer network checks also validate Docker Hub reachability. + +For restricted networks: + +1. Confirm that the host can resolve and reach `docker.io`. +2. If your environment requires an outbound proxy, configure it for both PentAGI's outbound HTTP requests and the Docker daemon / Docker Desktop. +3. If Docker Hub is blocked or heavily rate-limited, configure an organization-approved registry mirror or registry proxy before running the installer or `docker compose up`. +4. Restart Docker after changing the daemon configuration, then rerun the installer checks or Compose startup. + +Example Docker daemon mirror configuration: + +```json +{ + "registry-mirrors": ["https://mirror.example.com"] +} +``` + +On Linux, this is typically configured in `/etc/docker/daemon.json`. On Docker Desktop, use the equivalent Docker Engine or proxy settings. A Docker Hub mirror can help with Docker Hub-hosted images such as `vxcontrol/*`, but optional stacks may still pull from other registries such as `quay.io` or `gcr.io`, so those registries still need direct access or an approved proxy path. + +See the official Docker documentation for [registry mirrors](https://docs.docker.com/docker-hub/image-library/mirror/) and [daemon proxy configuration](https://docs.docker.com/engine/daemon/proxy/). + ## Development ### Development Requirements diff --git a/backend/docs/installer/checker.md b/backend/docs/installer/checker.md index ac0c7b2ce..c2f02f2ae 100644 --- a/backend/docs/installer/checker.md +++ b/backend/docs/installer/checker.md @@ -77,6 +77,21 @@ Three-tier verification process: 2. **HTTP Connectivity**: Verifies HTTPS access (proxy-aware) 3. **Docker Pull Test**: Attempts to pull a small test image +#### Restricted Network Troubleshooting + +The current checker validates Docker Hub reachability by resolving `docker.io`, making an HTTPS connectivity check, and attempting a Docker pull with the default test image. This means the installer can fail network validation even when the host has general internet access but Docker itself is not configured for the target network. + +Recommended remediation order: + +1. Confirm general internet access and DNS resolution for `docker.io` +2. If your environment requires an outbound proxy, configure it for the installer/update path and for Docker itself +3. If Docker Hub is blocked or rate-limited, configure an organization-approved Docker registry mirror or registry proxy at the Docker daemon / Docker Desktop level +4. Restart Docker and rerun the installer checks + +PentAGI variables such as `PENTAGI_IMAGE`, `DOCKER_DEFAULT_IMAGE`, and `DOCKER_DEFAULT_IMAGE_FOR_PENTEST` do not replace Docker daemon registry configuration. They only influence the PentAGI application image or worker image selection after Docker is already able to pull the required images. Optional stacks may also use registries outside Docker Hub, so mirror coverage depends on your Docker environment. + +See Docker's official documentation for [registry mirrors](https://docs.docker.com/docker-hub/image-library/mirror/) and [daemon proxy configuration](https://docs.docker.com/engine/daemon/proxy/). + ### 5. Update Availability Checks - Communicates with update server to check latest versions - Sends current component versions and configuration From af1bf049aff68037e84b34d3b2c12d05d35720af Mon Sep 17 00:00:00 2001 From: "Mason Kim(ZINUS US_SALES)" Date: Wed, 15 Apr 2026 16:15:21 -0400 Subject: [PATCH 008/275] docs: add first-use guide after login Signed-off-by: Mason Kim(ZINUS US_SALES) --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/README.md b/README.md index 7ddc15baa..426964cac 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ - [Architecture](#architecture) - [Agent Supervision](#advanced-agent-supervision) - [Quick Start](#quick-start) +- [How to Use PentAGI After Login](#how-to-use-pentagi-after-login) - [API Access](#api-access) - [LLM Provider Configuration](#custom-llm-provider-configuration) - [Ollama](#ollama-provider-configuration) @@ -937,6 +938,60 @@ The `ASSISTANT_USE_AGENTS` setting affects the initial state of the "Use Agents" Note that users can always override this setting by toggling the "Use Agents" button in the UI when creating or editing an assistant. This environment variable only controls the initial default state. +## How to Use PentAGI After Login + +Once the stack is running and you can sign in to the web UI, the fastest way to start is through the Flows workflow. + +### 1. Create your first flow + +1. Open **Flows** in the sidebar. +2. Click **New Flow**. +3. Choose the mode that fits your goal: + - **Automation**: fully autonomous execution for a testing goal you want PentAGI to carry out end-to-end + - **Assistant**: interactive back-and-forth help when you want to steer the investigation step by step +4. Select the LLM provider you want to use for this flow. +5. Describe the target and the objective in natural language in the message box. + +Good first prompts usually include: + +- the target system or URL +- the type of assessment you want +- any scope limitations or rules of engagement +- the result you expect, such as a vulnerability report or validation of a hypothesis + +Example: + +```text +Assess https://target.example for common web application vulnerabilities. Focus on authentication, file handling, and injection issues. Stay within the provided target only and summarize confirmed findings with reproduction steps. +``` + +### 2. Use templates for repeatable workflows + +The new flow form includes a template picker, which can prefill the message box with a saved flow template. This is useful when you run similar assessments repeatedly. + +- Use an existing template if you already have one saved in **Templates** +- Start from the example prompt in [`examples/prompts/base_web_pentest.md`](examples/prompts/base_web_pentest.md) if you need a practical baseline for web testing +- Adjust the target, scope, and constraints before starting the flow + +Templates are starting points. You do not need special syntax to use PentAGI: plain natural-language instructions work well as long as the target and goal are clear. + +### 3. Monitor execution and review output + +After submitting the flow, PentAGI opens the flow page automatically. + +- Use the main flow view to follow messages, agent activity, and task progress +- Inspect tool activity and terminal output as the flow runs +- Review generated tasks and subtasks to understand what PentAGI is doing + +Once the flow has enough results, use the **Report** menu on the flow page to: + +- open the report in a web view +- copy the generated report to the clipboard +- download the report as Markdown +- download the report as PDF + +For early testing, start with a narrow target and a single clear objective. This makes the output easier to review and helps you refine your prompts before running larger assessments. + ## API Access PentAGI provides comprehensive programmatic access through both REST and GraphQL APIs, allowing you to integrate penetration testing workflows into your automation pipelines, CI/CD processes, and custom applications. From eea0a86c85d6e2a6a614f2cb7072a28ac99cd172 Mon Sep 17 00:00:00 2001 From: "Mason Kim(ZINUS US_SALES)" Date: Wed, 15 Apr 2026 16:18:36 -0400 Subject: [PATCH 009/275] fix: align compose checks with docker compose Signed-off-by: Mason Kim(ZINUS US_SALES) --- backend/cmd/installer/checker/helpers.go | 15 +++--- backend/cmd/installer/checker/helpers_test.go | 46 +++++++++++++++++++ backend/cmd/installer/wizard/locale/locale.go | 20 ++++---- 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/backend/cmd/installer/checker/helpers.go b/backend/cmd/installer/checker/helpers.go index 4a7814bc7..612364be9 100644 --- a/backend/cmd/installer/checker/helpers.go +++ b/backend/cmd/installer/checker/helpers.go @@ -236,14 +236,15 @@ func checkDockerCliVersion() DockerVersion { } func checkDockerComposeVersion() DockerVersion { - cmd := exec.Command("docker", "compose", "version") - output, err := cmd.Output() + return checkDockerComposeVersionWithRunner(func(name string, args ...string) ([]byte, error) { + return exec.Command(name, args...).Output() + }) +} + +func checkDockerComposeVersionWithRunner(run func(name string, args ...string) ([]byte, error)) DockerVersion { + output, err := run("docker", "compose", "version") if err != nil { - cmd = exec.Command("docker-compose", "--version") - output, err = cmd.Output() - if err != nil { - return DockerVersion{Version: "", Valid: false} - } + return DockerVersion{Version: "", Valid: false} } versionStr := extractVersionFromOutput(string(output)) diff --git a/backend/cmd/installer/checker/helpers_test.go b/backend/cmd/installer/checker/helpers_test.go index 481d86cdd..e2b65d935 100644 --- a/backend/cmd/installer/checker/helpers_test.go +++ b/backend/cmd/installer/checker/helpers_test.go @@ -2,6 +2,7 @@ package checker import ( "context" + "errors" "fmt" "net/http" "net/http/httptest" @@ -157,6 +158,51 @@ func TestExtractVersionFromOutput(t *testing.T) { } } +func TestCheckDockerComposeVersionWithRunner(t *testing.T) { + t.Run("uses docker compose v2 output", func(t *testing.T) { + calls := 0 + result := checkDockerComposeVersionWithRunner(func(name string, args ...string) ([]byte, error) { + calls++ + if name != "docker" { + t.Fatalf("unexpected command %q", name) + } + if len(args) != 2 || args[0] != "compose" || args[1] != "version" { + t.Fatalf("unexpected args: %v", args) + } + + return []byte("Docker Compose version v2.12.2"), nil + }) + + if calls != 1 { + t.Fatalf("expected 1 command invocation, got %d", calls) + } + if result.Version != "2.12.2" { + t.Fatalf("expected version 2.12.2, got %q", result.Version) + } + if !result.Valid { + t.Fatal("expected docker compose version to be valid") + } + }) + + t.Run("fails when docker compose is unavailable", func(t *testing.T) { + calls := 0 + result := checkDockerComposeVersionWithRunner(func(name string, args ...string) ([]byte, error) { + calls++ + return nil, errors.New("executable file not found") + }) + + if calls != 1 { + t.Fatalf("expected 1 command invocation, got %d", calls) + } + if result.Version != "" { + t.Fatalf("expected empty version, got %q", result.Version) + } + if result.Valid { + t.Fatal("expected docker compose check to be invalid") + } + }) +} + func TestCheckVersionCompatibility(t *testing.T) { tests := []struct { version string diff --git a/backend/cmd/installer/wizard/locale/locale.go b/backend/cmd/installer/wizard/locale/locale.go index b7c7f3602..3fb0a3f47 100644 --- a/backend/cmd/installer/wizard/locale/locale.go +++ b/backend/cmd/installer/wizard/locale/locale.go @@ -155,23 +155,27 @@ Required: 20.0.0+` // Docker Compose issues TroubleshootComposeTitle = "Docker Compose Not Found" - TroubleshootComposeDesc = "Docker Compose is required but not installed or not in PATH." + TroubleshootComposeDesc = "The Docker Compose v2 plugin is required, but `docker compose` is not available." TroubleshootComposeFix = `To fix: -1. Install Docker Desktop (includes Compose) or -2. Install standalone: https://docs.docker.com/compose/install/ +1. Install or update Docker Desktop, or install the Docker Compose v2 plugin for Docker Engine +2. Verify the plugin is available: docker compose version +3. If only legacy docker-compose is installed, remove it or install the v2 plugin as well -Verify installation: docker compose version` +PentAGI executes "docker compose", so legacy "docker-compose" alone is not sufficient. +Documentation: https://docs.docker.com/compose/install/` // Docker Compose version issues TroubleshootComposeVersionTitle = "Docker Compose Version Too Old" - TroubleshootComposeVersionDesc = "Your Docker Compose version is incompatible. PentAGI requires Docker Compose 1.25.0 or newer." + TroubleshootComposeVersionDesc = "Your `docker compose` version is incompatible. PentAGI requires Docker Compose 1.25.0 or newer." TroubleshootComposeVersionFix = `Current version: %s Required: 1.25.0+ To fix: -1. Update Docker Desktop to latest version -2. Or install newer Docker Compose: - https://docs.docker.com/compose/install/` +1. Update Docker Desktop or the Docker Compose v2 plugin to a newer version +2. Verify the result with: docker compose version +3. If only legacy docker-compose is installed, install the v2 plugin as well + +Documentation: https://docs.docker.com/compose/install/` // Worker environment issues TroubleshootWorkerTitle = "Worker Docker Environment Not Accessible" From 92232f57553c6fb14fb61346bc6980794f6e181a Mon Sep 17 00:00:00 2001 From: "Mason Kim(ZINUS US_SALES)" Date: Wed, 15 Apr 2026 17:51:28 -0400 Subject: [PATCH 010/275] fix: accept compose version output on error Signed-off-by: Mason Kim(ZINUS US_SALES) --- backend/cmd/installer/checker/helpers.go | 2 +- backend/cmd/installer/checker/helpers_test.go | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/backend/cmd/installer/checker/helpers.go b/backend/cmd/installer/checker/helpers.go index 612364be9..b00cd11d7 100644 --- a/backend/cmd/installer/checker/helpers.go +++ b/backend/cmd/installer/checker/helpers.go @@ -243,7 +243,7 @@ func checkDockerComposeVersion() DockerVersion { func checkDockerComposeVersionWithRunner(run func(name string, args ...string) ([]byte, error)) DockerVersion { output, err := run("docker", "compose", "version") - if err != nil { + if err != nil && len(output) == 0 { return DockerVersion{Version: "", Valid: false} } diff --git a/backend/cmd/installer/checker/helpers_test.go b/backend/cmd/installer/checker/helpers_test.go index e2b65d935..5e7a74a6f 100644 --- a/backend/cmd/installer/checker/helpers_test.go +++ b/backend/cmd/installer/checker/helpers_test.go @@ -184,6 +184,24 @@ func TestCheckDockerComposeVersionWithRunner(t *testing.T) { } }) + t.Run("parses version from stdout even when error is returned", func(t *testing.T) { + calls := 0 + result := checkDockerComposeVersionWithRunner(func(name string, args ...string) ([]byte, error) { + calls++ + return []byte("Docker Compose version v2.12.2"), errors.New("exit status 1") + }) + + if calls != 1 { + t.Fatalf("expected 1 command invocation, got %d", calls) + } + if result.Version != "2.12.2" { + t.Fatalf("expected version 2.12.2, got %q", result.Version) + } + if !result.Valid { + t.Fatal("expected docker compose version to remain valid when stdout is parseable") + } + }) + t.Run("fails when docker compose is unavailable", func(t *testing.T) { calls := 0 result := checkDockerComposeVersionWithRunner(func(name string, args ...string) ([]byte, error) { From a07c28c2aea11870e58ecafa3e35bc78d016621a Mon Sep 17 00:00:00 2001 From: "Mason Kim(ZINUS US_SALES)" Date: Wed, 15 Apr 2026 17:52:16 -0400 Subject: [PATCH 011/275] docs: add authorized-use reminder Signed-off-by: Mason Kim(ZINUS US_SALES) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 426964cac..dc94d72ea 100644 --- a/README.md +++ b/README.md @@ -965,6 +965,8 @@ Example: Assess https://target.example for common web application vulnerabilities. Focus on authentication, file handling, and injection issues. Stay within the provided target only and summarize confirmed findings with reproduction steps. ``` +Only test systems you own or are explicitly authorized to assess. See [EULA.md](EULA.md) for the acceptable use requirements. + ### 2. Use templates for repeatable workflows The new flow form includes a template picker, which can prefill the message box with a saved flow template. This is useful when you run similar assessments repeatedly. From a9a0b76226d3f8e0b41fe44c1d1333df3df6c152 Mon Sep 17 00:00:00 2001 From: "Mason Kim(ZINUS US_SALES)" Date: Wed, 15 Apr 2026 18:04:50 -0400 Subject: [PATCH 012/275] docs: add pentesting prompt methodology guide Signed-off-by: Mason Kim(ZINUS US_SALES) --- README.md | 13 +++++++++++++ backend/docs/prompt_engineering_pentagi.md | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/README.md b/README.md index 7ddc15baa..8d6842d92 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ - [OAuth Integration](#github-and-google-oauth-integration) - [Docker Image Configuration](#docker-image-configuration) - [Development](#development) + - [Pentesting Prompt Methodology](#pentesting-prompt-methodology) - [Testing LLM Agents](#testing-llm-agents) - [Embedding Configuration and Testing](#embedding-configuration-and-testing) - [Function Testing with ftester](#function-testing-with-ftester) @@ -3192,6 +3193,18 @@ When developing new prompt templates or agent behaviors: 3. Observe responses and adjust prompts accordingly 4. Check Langfuse for detailed traces of all function calls +### Pentesting Prompt Methodology + +When refining prompts for offensive security work, give the agent a clear methodology instead of a flat list of payloads: + +1. Start with explicit scope, authorization, and success criteria +2. Map the application first: roles, routes, parameters, uploads, integrations, and trust boundaries +3. Prioritize attack surfaces systematically instead of testing everything at once +4. Validate findings with reproducible evidence before escalating to deeper exploitation +5. Finish with report-ready notes that capture impact, prerequisites, and next steps + +For PentAGI-specific prompt guidance, see [`backend/docs/prompt_engineering_pentagi.md`](backend/docs/prompt_engineering_pentagi.md). For a practical starting point, reuse and adapt [`examples/prompts/base_web_pentest.md`](examples/prompts/base_web_pentest.md) to match the target application, technology stack, and engagement scope. + ### Verifying Docker Container Setup Ensure containers are properly configured: diff --git a/backend/docs/prompt_engineering_pentagi.md b/backend/docs/prompt_engineering_pentagi.md index 5a0e37c09..1ef1005e0 100644 --- a/backend/docs/prompt_engineering_pentagi.md +++ b/backend/docs/prompt_engineering_pentagi.md @@ -367,6 +367,20 @@ A comprehensive framework for designing high-performance prompts within the Pent - **Key Sections**: `KNOWLEDGE MANAGEMENT` (Memory Protocol), `OPERATIONAL ENVIRONMENT` (Container Constraints), `COMMAND EXECUTION RULES` (Terminal Protocol), `PENETRATION TESTING TOOLS` (list available), `TEAM COLLABORATION`, `DELEGATION PROTOCOL`, `SUMMARIZATION AWARENESS PROTOCOL`, `COMPLETION REQUIREMENTS` (using `{{.HackResultToolName}}`). - **Critical Instructions**: Check memory first, strictly adhere to terminal rules & container constraints, use only listed available tools, delegate appropriately (e.g., exploit development to Coder), provide detailed, evidence-backed exploitation reports using `{{.HackResultToolName}}`. +#### Pentesting Methodology Checklist for Prompt Authors +- Encode authorization boundaries explicitly. Prompts should remind the agent to test only approved targets, respect engagement scope, and avoid destructive actions unless the task requires them. +- Start with coverage before exploitation. Instruct the agent to map routes, roles, inputs, file handling, integrations, and trust boundaries before choosing attack paths. +- Organize testing by attack surface. Good prompts group checks around authentication, access control, injection, cross-site scripting, server-side request forgery, file processing, and business logic instead of presenting a random payload dump. +- Prefer low-risk validation first. Reflection markers, controlled payloads, timing checks, and out-of-band verification should be used deliberately to confirm hypotheses before deeper exploitation. +- Require evidence at every stage. Prompts should ask for captured requests, responses, tool output, prerequisites, and impact notes so confirmed findings can move directly into a report. +- Use memory and iteration intentionally. The agent should record confirmed dead ends, revisit promising leads with new context, and avoid repeating the same failed checks. +- End with actionable reporting. A strong pentesting prompt tells the agent to summarize what was confirmed, what remains unverified, how the issue can be reproduced, and which follow-up actions are justified. + +#### Recommended Reference Material +- Use public methodology resources such as [HackTricks](https://book.hacktricks.wiki/en/index.html) and [Pentest Book](https://pentestbook.six2dez.com/) as inspiration for attack-surface coverage and testing depth. +- Translate those references into concise phases, priorities, and verification rules for the agent instead of copying long checklists into the system prompt verbatim. +- Keep prompt examples aligned with live PentAGI assets such as `backend/pkg/templates/prompts/pentester.tmpl` and `../../examples/prompts/base_web_pentest.md`. + ### Searcher Agent - **Focus**: Highly efficient information retrieval (internal memory & external sources), source evaluation and prioritization, synthesis of findings. - **Key Sections**: `CORE CAPABILITIES` (Action Economy, Search Optimization), `SEARCH TOOL DEPLOYMENT MATRIX`, `OPERATIONAL PROTOCOLS` (Search Efficiency, Query Engineering), `SUMMARIZATION AWARENESS PROTOCOL`, `SEARCH RESULT DELIVERY` (using `{{.SearchResultToolName}}`). From 4e2ece89450cd177d30631c4d14ad2d86d23ccce Mon Sep 17 00:00:00 2001 From: "Mason Kim(ZINUS US_SALES)" Date: Wed, 15 Apr 2026 19:35:08 -0400 Subject: [PATCH 013/275] docs: clarify memory lifecycle across flows Signed-off-by: Mason Kim(ZINUS US_SALES) --- README.md | 13 +++++++++++++ backend/docs/flow_execution.md | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/README.md b/README.md index 7ddc15baa..885f68fa1 100644 --- a/README.md +++ b/README.md @@ -3001,6 +3001,19 @@ Available search parameters: - `-limit NUMBER`: Maximum number of results (default: 3) - `-threshold NUMBER`: Similarity threshold (0.0-1.0, default: 0.7) +### Memory Lifecycle Across Flows + +PentAGI stores several kinds of vector documents, and they serve different purposes: + +- `memory` captures flow-specific execution history such as tool results and agent observations +- `guide`, `answer`, and `code` are intended for reusable knowledge that can help future runs + +If you want to inspect what happened in one engagement, search the vector store with the related `flow_id`. If you want knowledge to survive beyond a single run, store the durable result explicitly as a `guide`, `answer`, or `code` document instead of relying on execution memory alone. + +For example, if a target has recurring setup notes, authentication quirks, or target-specific testing methodology, instruct the agent to save that information as a `guide` and search for it at the beginning of the next engagement. This is the safest current workflow when you want a new flow to start with reusable context. + +Flow deletion removes the flow from normal queries through PentAGI's soft-delete mechanism, so reusable knowledge should be treated as a separate concern from per-flow execution history. If you need broader episodic context across operations, enable the optional Graphiti knowledge graph described earlier in this README. + ### Common Troubleshooting Scenarios 1. **After changing embedding provider**: Always run `flush` or `reindex` to ensure consistency diff --git a/backend/docs/flow_execution.md b/backend/docs/flow_execution.md index d4303a99d..688acb3f7 100644 --- a/backend/docs/flow_execution.md +++ b/backend/docs/flow_execution.md @@ -706,6 +706,12 @@ The system maintains multiple types of persistent knowledge with PostgreSQL + pg - **Answer Storage** (`doc_type: answer`) - Q&A pairs for common scenarios - **Code Storage** (`doc_type: code`) - Programming language-specific code samples +**Lifecycle Guidance**: +- Treat `memory` as flow-scoped execution history. It is most useful for understanding what happened in a specific engagement and is commonly inspected with a `flow_id` filter. +- Treat `guide`, `answer`, and `code` as reusable knowledge. These document types exist to preserve durable procedures, reusable target notes, Q&A material, and code snippets across future runs. +- If you want a later flow to begin with known context, store the confirmed result intentionally through `store_guide`, `store_answer`, or `store_code` instead of assuming execution history alone will provide the right reusable context. +- Current prompt templates already distinguish these roles: reusable guides/code live in vector documents, while Graphiti is intended for episodic memory about what actually happened during execution. + **Technical Parameters**: - **Similarity Threshold**: 0.2 for all vector searches - **Result Limits**: 3 documents maximum per search From ea8aa205bcd5fb9ce0b84aef1cd3d7490329b1ff Mon Sep 17 00:00:00 2001 From: mason5052 Date: Thu, 16 Apr 2026 12:27:11 -0400 Subject: [PATCH 014/275] docs: clarify current PentAGI capability boundaries --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 7ddc15baa..9332d71e3 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,13 @@ You can watch the video **PentAGI overview**: - API Token Authentication. Secure Bearer token system for programmatic access to REST and GraphQL APIs. - Quick Deployment. Easy setup through [Docker Compose](https://docs.docker.com/compose/) with comprehensive environment configuration. +### Current Capability Boundaries + +- PentAGI today is an autonomous and assistant-guided penetration testing platform, not a CALDERA-style Breach and Attack Simulation (BAS) or adversary emulation product with predefined campaigns or attack plans. +- BAS-like agent-authored attack scripts should be treated as conceptual or future work, not as a feature that is implemented today. +- The current flow report UI supports web view, copy to clipboard, Markdown download, and PDF download. JSON flow-report export is not documented as a supported output format today. +- Provider flexibility is available today through built-in providers and custom/OpenAI-compatible endpoints. See [Custom LLM Provider Configuration](#custom-llm-provider-configuration) and the [vLLM + Qwen3.5-27B-FP8 guide](examples/guides/vllm-qwen35-27b-fp8.md). + ## Architecture ### System Context From e86052b041b0b3b8d81cf386ff66360258c24f3f Mon Sep 17 00:00:00 2001 From: mason5052 Date: Thu, 16 Apr 2026 12:38:51 -0400 Subject: [PATCH 015/275] docs: clarify current web settings coverage --- README.md | 18 ++++++++++++++++++ backend/docs/config.md | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/README.md b/README.md index 7ddc15baa..c8c8ba061 100644 --- a/README.md +++ b/README.md @@ -625,6 +625,24 @@ The installer will: 5. **Security Hardening**: Generate secure credentials and configure SSL certificates 6. **Deployment**: Start PentAGI with docker-compose +### Current Web Settings Coverage + +The PentAGI web console already manages several settings areas after the server is up and running: + +- **Settings -> Providers**: Create, edit, delete, and test user-defined provider profiles for supported provider types. These profiles control per-agent model selection, runtime parameters, reasoning options, and pricing metadata. +- **Settings -> Prompts**: Manage system, human, and tool prompt templates. +- **Settings -> API Tokens**: Create and manage PentAGI Bearer tokens for REST and GraphQL access. +- **Other UI-managed preferences**: Favorite flows are stored as user preferences, and theme selection is handled from the main sidebar/profile controls rather than the Settings pages. + +### Still Server-Managed + +The following configuration areas still need to be set on the server through environment variables, compose files, or mounted config files: + +- **LLM credentials and connection details**: API keys, endpoints, auth modes, and config-path settings such as `OPEN_AI_KEY`, `ANTHROPIC_API_KEY`, `BEDROCK_*`, `OLLAMA_SERVER_*`, and `LLM_SERVER_*`. +- **Search provider credentials and options**: Settings such as `DUCKDUCKGO_*`, `GOOGLE_*`, `TAVILY_API_KEY`, `TRAVERSAAL_API_KEY`, `PERPLEXITY_*`, `SEARXNG_*`, and `SPLOITUS_ENABLED`. +- **Third-party integrations**: Langfuse, Graphiti, and similar external services remain server-side configuration. +- **MCP server management**: MCP settings pages are not currently exposed as a live web-console feature. + **For Production & Enhanced Security:** For production deployments or security-sensitive environments, we **strongly recommend** using a distributed two-node architecture where worker operations are isolated on a separate server. This prevents untrusted code execution and network access issues on your main system. diff --git a/backend/docs/config.md b/backend/docs/config.md index 26b2c342e..8ca418abc 100644 --- a/backend/docs/config.md +++ b/backend/docs/config.md @@ -7,6 +7,8 @@ This document serves as a comprehensive guide to the configuration system in Pen - [PentAGI Configuration Guide](#pentagi-configuration-guide) - [Table of Contents](#table-of-contents) - [Configuration Basics](#configuration-basics) + - [Current Web Settings Coverage](#current-web-settings-coverage) + - [Still Server-Managed](#still-server-managed) - [General Settings](#general-settings) - [Usage Details](#usage-details) - [Docker Settings](#docker-settings) @@ -101,6 +103,26 @@ func NewConfig() (*Config, error) { This function automatically loads environment variables from a `.env` file if present, then parses them into the `Config` struct using the `env` package from `github.com/caarlos0/env/v10`. +### Current Web Settings Coverage + +The running PentAGI instance already exposes several settings areas in the web UI: + +- **Settings -> Providers**: Manage user-defined provider profiles, per-agent model and runtime options, and provider test actions for provider types supported by the running server. +- **Settings -> Prompts**: Manage system, human, and tool prompt templates. +- **Settings -> API Tokens**: Create, revoke, and delete PentAGI API tokens. +- **Other UI-managed preferences**: Favorite flows are stored as user preferences, and theme selection is handled client-side from the main sidebar/profile controls. + +These web-console features do not replace the environment variables in this guide for provider credentials, endpoints, or external integrations. + +### Still Server-Managed + +The environment variables documented below remain the source of truth for configuration that is not currently editable from the web console: + +- **LLM credentials and connection settings**: API keys, base URLs, auth modes, and config-path values for OpenAI, Anthropic, Bedrock, Ollama, custom providers, and similar backends. +- **Search provider credentials and options**: DuckDuckGo, Google, Tavily, Traversaal, Perplexity, Searxng, Sploitus, and related search configuration. +- **Third-party integrations**: Langfuse, Graphiti, and other external observability or knowledge services. +- **MCP server management**: MCP settings are not currently exposed as a live web-console feature. + ## General Settings These settings control basic application behavior and are foundational for the system's operation. From 4d7c3678a970f9f52ca065fbdd5aadf0ae08d58f Mon Sep 17 00:00:00 2001 From: mason5052 Date: Sun, 19 Apr 2026 14:27:06 -0400 Subject: [PATCH 016/275] feat: add flow-scoped file uploads --- backend/pkg/server/response/errors.go | 6 + backend/pkg/server/router.go | 11 + backend/pkg/server/services/flow_files.go | 333 ++++++++++++++++++ .../pkg/server/services/flow_files_test.go | 95 +++++ .../src/features/flows/files/flow-files.tsx | 289 +++++++++++++++ frontend/src/features/flows/flow-tabs.tsx | 9 + 6 files changed, 743 insertions(+) create mode 100644 backend/pkg/server/services/flow_files.go create mode 100644 backend/pkg/server/services/flow_files_test.go create mode 100644 frontend/src/features/flows/files/flow-files.tsx diff --git a/backend/pkg/server/response/errors.go b/backend/pkg/server/response/errors.go index b3161a1fc..f294ddc33 100644 --- a/backend/pkg/server/response/errors.go +++ b/backend/pkg/server/response/errors.go @@ -109,6 +109,12 @@ var ErrFlowsInvalidRequest = NewHttpError(400, "Flows.InvalidRequest", "invalid var ErrFlowsNotFound = NewHttpError(404, "Flows.NotFound", "flow not found") var ErrFlowsInvalidData = NewHttpError(500, "Flows.InvalidData", "invalid flow data") +// flow files + +var ErrFlowFilesInvalidRequest = NewHttpError(400, "FlowFiles.InvalidRequest", "invalid flow file request data") +var ErrFlowFilesNotFound = NewHttpError(404, "FlowFiles.NotFound", "flow file not found") +var ErrFlowFilesInvalidData = NewHttpError(500, "FlowFiles.InvalidData", "invalid flow file data") + // tasks var ErrTasksInvalidRequest = NewHttpError(400, "Tasks.InvalidRequest", "invalid task request data") diff --git a/backend/pkg/server/router.go b/backend/pkg/server/router.go index 4e5404c38..45bb5a53f 100644 --- a/backend/pkg/server/router.go +++ b/backend/pkg/server/router.go @@ -130,6 +130,7 @@ func NewRouter( roleService := services.NewRoleService(orm) providerService := services.NewProviderService(providers) flowService := services.NewFlowService(orm, providers, controller, subscriptions) + flowFileService := services.NewFlowFileService(orm, cfg.DataDir) taskService := services.NewTaskService(orm) subtaskService := services.NewSubtaskService(orm) containerService := services.NewContainerService(orm) @@ -223,6 +224,7 @@ func NewRouter( setProvidersGroup(privateGroup, providerService) setFlowsGroup(privateGroup, flowService) + setFlowFilesGroup(privateGroup, flowFileService) setTasksGroup(privateGroup, taskService) setSubtasksGroup(privateGroup, subtaskService) setContainersGroup(privateGroup, containerService) @@ -367,6 +369,15 @@ func setFlowsGroup(parent *gin.RouterGroup, svc *services.FlowService) { } } +func setFlowFilesGroup(parent *gin.RouterGroup, svc *services.FlowFileService) { + flowFilesGroup := parent.Group("/flows/:flowID/files") + { + flowFilesGroup.GET("/", svc.GetFlowFiles) + flowFilesGroup.POST("/", svc.UploadFlowFiles) + flowFilesGroup.GET("/:fileName", svc.DownloadFlowFile) + } +} + func setContainersGroup(parent *gin.RouterGroup, svc *services.ContainerService) { containersViewGroup := parent.Group("/containers") { diff --git a/backend/pkg/server/services/flow_files.go b/backend/pkg/server/services/flow_files.go new file mode 100644 index 000000000..b74617792 --- /dev/null +++ b/backend/pkg/server/services/flow_files.go @@ -0,0 +1,333 @@ +package services + +import ( + "errors" + "fmt" + "net/http" + "os" + "path" + "path/filepath" + "slices" + "sort" + "strconv" + "strings" + "time" + + "pentagi/pkg/server/logger" + "pentagi/pkg/server/models" + "pentagi/pkg/server/response" + + "github.com/gin-gonic/gin" + "github.com/jinzhu/gorm" +) + +const flowUploadsDirName = "uploads" + +type flowFile struct { + Name string `json:"name"` + Path string `json:"path"` + Size int64 `json:"size"` + ModifiedAt time.Time `json:"modifiedAt"` +} + +type flowFiles struct { + Files []flowFile `json:"files"` + Total uint64 `json:"total"` +} + +type FlowFileService struct { + dataDir string + db *gorm.DB +} + +func NewFlowFileService(db *gorm.DB, dataDir string) *FlowFileService { + return &FlowFileService{ + dataDir: dataDir, + db: db, + } +} + +func (s *FlowFileService) GetFlowFiles(c *gin.Context) { + flowID, err := parseFlowIDParam(c) + if err != nil { + logger.FromContext(c).WithError(err).Error("error parsing flow id") + response.Error(c, response.ErrFlowFilesInvalidRequest, err) + return + } + + if _, err := s.getFlow(c, flowID, false); err != nil { + s.handleFlowLookupError(c, flowID, err) + return + } + + files, err := s.listFlowFiles(flowID) + if err != nil { + logger.FromContext(c).WithError(err).WithField("flow_id", flowID).Error("error listing flow files") + response.Error(c, response.ErrInternal, err) + return + } + + response.Success(c, http.StatusOK, flowFiles{ + Files: files, + Total: uint64(len(files)), + }) +} + +func (s *FlowFileService) UploadFlowFiles(c *gin.Context) { + flowID, err := parseFlowIDParam(c) + if err != nil { + logger.FromContext(c).WithError(err).Error("error parsing flow id") + response.Error(c, response.ErrFlowFilesInvalidRequest, err) + return + } + + if _, err := s.getFlow(c, flowID, true); err != nil { + s.handleFlowLookupError(c, flowID, err) + return + } + + multipartForm, err := c.MultipartForm() + if err != nil { + logger.FromContext(c).WithError(err).WithField("flow_id", flowID).Error("error reading multipart form") + response.Error(c, response.ErrFlowFilesInvalidRequest, err) + return + } + + fileHeaders := multipartForm.File["files"] + if len(fileHeaders) == 0 { + fileHeader, formErr := c.FormFile("file") + if formErr == nil && fileHeader != nil { + fileHeaders = append(fileHeaders, fileHeader) + } + } + if len(fileHeaders) == 0 { + err = errors.New("at least one uploaded file is required") + logger.FromContext(c).WithError(err).WithField("flow_id", flowID).Error("missing uploaded files") + response.Error(c, response.ErrFlowFilesInvalidRequest, err) + return + } + + uploadDir := s.flowUploadsDir(flowID) + if err := os.MkdirAll(uploadDir, 0755); err != nil { + logger.FromContext(c).WithError(err).WithField("flow_id", flowID).Error("error creating upload directory") + response.Error(c, response.ErrInternal, err) + return + } + + savedFiles := make([]flowFile, 0, len(fileHeaders)) + for _, fileHeader := range fileHeaders { + fileName, err := sanitizeFlowFileName(fileHeader.Filename) + if err != nil { + logger.FromContext(c).WithError(err).WithField("flow_id", flowID).Error("invalid uploaded file name") + response.Error(c, response.ErrFlowFilesInvalidData, err) + return + } + + dstPath := filepath.Join(uploadDir, fileName) + if err := c.SaveUploadedFile(fileHeader, dstPath); err != nil { + logger.FromContext(c).WithError(err).WithFields(map[string]any{ + "flow_id": flowID, + "file_name": fileName, + }).Error("error saving uploaded file") + response.Error(c, response.ErrInternal, err) + return + } + + info, err := os.Stat(dstPath) + if err != nil { + logger.FromContext(c).WithError(err).WithFields(map[string]any{ + "flow_id": flowID, + "file_name": fileName, + }).Error("error stating uploaded file") + response.Error(c, response.ErrInternal, err) + return + } + + savedFiles = append(savedFiles, newFlowFile(info)) + } + + sortFlowFiles(savedFiles) + response.Success(c, http.StatusOK, flowFiles{ + Files: savedFiles, + Total: uint64(len(savedFiles)), + }) +} + +func (s *FlowFileService) DownloadFlowFile(c *gin.Context) { + flowID, err := parseFlowIDParam(c) + if err != nil { + logger.FromContext(c).WithError(err).Error("error parsing flow id") + response.Error(c, response.ErrFlowFilesInvalidRequest, err) + return + } + + if _, err := s.getFlow(c, flowID, false); err != nil { + s.handleFlowLookupError(c, flowID, err) + return + } + + fileName, err := sanitizeFlowFileName(c.Param("fileName")) + if err != nil { + logger.FromContext(c).WithError(err).WithField("flow_id", flowID).Error("invalid download file name") + response.Error(c, response.ErrFlowFilesInvalidRequest, err) + return + } + + filePath := filepath.Join(s.flowUploadsDir(flowID), fileName) + info, err := os.Stat(filePath) + if err != nil { + logger.FromContext(c).WithError(err).WithFields(map[string]any{ + "flow_id": flowID, + "file_name": fileName, + }).Error("error reading flow file") + if errors.Is(err, os.ErrNotExist) { + response.Error(c, response.ErrFlowFilesNotFound, err) + } else { + response.Error(c, response.ErrInternal, err) + } + return + } + if info.IsDir() { + err = fmt.Errorf("file '%s' is a directory", fileName) + logger.FromContext(c).WithError(err).WithFields(map[string]any{ + "flow_id": flowID, + "file_name": fileName, + }).Error("invalid flow file type") + response.Error(c, response.ErrFlowFilesNotFound, err) + return + } + + c.FileAttachment(filePath, fileName) +} + +func (s *FlowFileService) getFlow(c *gin.Context, flowID uint64, writeAccess bool) (models.Flow, error) { + var flow models.Flow + + uid := c.GetUint64("uid") + privs := c.GetStringSlice("prm") + scope := flowScopeForFiles(privs, uid, flowID, writeAccess) + if scope == nil { + return flow, response.ErrNotPermitted + } + + if err := s.db.Model(&flow).Scopes(scope).Take(&flow).Error; err != nil { + if gorm.IsRecordNotFoundError(err) { + return flow, response.ErrFlowsNotFound + } + return flow, err + } + + return flow, nil +} + +func (s *FlowFileService) listFlowFiles(flowID uint64) ([]flowFile, error) { + entries, err := os.ReadDir(s.flowUploadsDir(flowID)) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return []flowFile{}, nil + } + return nil, err + } + + files := make([]flowFile, 0, len(entries)) + for _, entry := range entries { + if entry.IsDir() { + continue + } + + info, err := entry.Info() + if err != nil { + return nil, err + } + + files = append(files, newFlowFile(info)) + } + + sortFlowFiles(files) + return files, nil +} + +func (s *FlowFileService) handleFlowLookupError(c *gin.Context, flowID uint64, err error) { + fields := map[string]any{"flow_id": flowID} + + switch err { + case response.ErrNotPermitted: + logger.FromContext(c).WithFields(fields).Error("error filtering user role permissions: permission not found") + response.Error(c, response.ErrNotPermitted, nil) + case response.ErrFlowsNotFound: + logger.FromContext(c).WithFields(fields).Error("error finding flow for flow files") + response.Error(c, response.ErrFlowsNotFound, err) + default: + logger.FromContext(c).WithError(err).WithFields(fields).Error("error loading flow for flow files") + response.Error(c, response.ErrInternal, err) + } +} + +func (s *FlowFileService) flowUploadsDir(flowID uint64) string { + return filepath.Join(s.dataDir, fmt.Sprintf("flow-%d", flowID), flowUploadsDirName) +} + +func flowScopeForFiles( + privs []string, + uid uint64, + flowID uint64, + writeAccess bool, +) func(db *gorm.DB) *gorm.DB { + if slices.Contains(privs, "flows.admin") { + return func(db *gorm.DB) *gorm.DB { + return db.Where("id = ?", flowID) + } + } + + if writeAccess && slices.Contains(privs, "flows.edit") { + return func(db *gorm.DB) *gorm.DB { + return db.Where("id = ? AND user_id = ?", flowID, uid) + } + } + + if !writeAccess && slices.Contains(privs, "flows.view") { + return func(db *gorm.DB) *gorm.DB { + return db.Where("id = ? AND user_id = ?", flowID, uid) + } + } + + return nil +} + +func parseFlowIDParam(c *gin.Context) (uint64, error) { + return strconv.ParseUint(c.Param("flowID"), 10, 64) +} + +func sanitizeFlowFileName(fileName string) (string, error) { + trimmedName := strings.TrimSpace(fileName) + if trimmedName == "" { + return "", fmt.Errorf("file name is required") + } + + normalizedName := strings.ReplaceAll(trimmedName, "\\", "/") + cleanName := path.Base(path.Clean("/" + normalizedName)) + if cleanName == "." || cleanName == "/" || cleanName == "" { + return "", fmt.Errorf("invalid file name") + } + + return cleanName, nil +} + +func newFlowFile(info os.FileInfo) flowFile { + return flowFile{ + Name: info.Name(), + Path: path.Join("/work", flowUploadsDirName, info.Name()), + Size: info.Size(), + ModifiedAt: info.ModTime(), + } +} + +func sortFlowFiles(files []flowFile) { + sort.Slice(files, func(i, j int) bool { + if files[i].ModifiedAt.Equal(files[j].ModifiedAt) { + return files[i].Name < files[j].Name + } + + return files[i].ModifiedAt.After(files[j].ModifiedAt) + }) +} diff --git a/backend/pkg/server/services/flow_files_test.go b/backend/pkg/server/services/flow_files_test.go new file mode 100644 index 000000000..b40cb4404 --- /dev/null +++ b/backend/pkg/server/services/flow_files_test.go @@ -0,0 +1,95 @@ +package services + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSanitizeFlowFileName(t *testing.T) { + testCases := []struct { + expected string + fileName string + name string + wantErr bool + }{ + { + name: "keeps plain file name", + fileName: "report.txt", + expected: "report.txt", + }, + { + name: "collapses parent traversal", + fileName: "../report.txt", + expected: "report.txt", + }, + { + name: "normalizes windows separators", + fileName: `nested\brief.md`, + expected: "brief.md", + }, + { + name: "rejects empty names", + fileName: " ", + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := sanitizeFlowFileName(tc.fileName) + + if tc.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tc.expected, actual) + }) + } +} + +func TestFlowFileService_ListFlowFiles(t *testing.T) { + dataDir := t.TempDir() + service := NewFlowFileService(nil, dataDir) + uploadDir := filepath.Join(dataDir, "flow-7", "uploads") + + require.NoError(t, os.MkdirAll(uploadDir, 0755)) + require.NoError(t, os.Mkdir(filepath.Join(uploadDir, "nested"), 0755)) + + oldPath := filepath.Join(uploadDir, "old.txt") + newPath := filepath.Join(uploadDir, "new.txt") + + require.NoError(t, os.WriteFile(oldPath, []byte("old"), 0644)) + require.NoError(t, os.WriteFile(newPath, []byte("newer content"), 0644)) + + oldTime := time.Now().Add(-2 * time.Hour) + newTime := time.Now().Add(-1 * time.Hour) + require.NoError(t, os.Chtimes(oldPath, oldTime, oldTime)) + require.NoError(t, os.Chtimes(newPath, newTime, newTime)) + + files, err := service.listFlowFiles(7) + require.NoError(t, err) + require.Len(t, files, 2) + + assert.Equal(t, "new.txt", files[0].Name) + assert.Equal(t, "/work/uploads/new.txt", files[0].Path) + assert.Equal(t, int64(len("newer content")), files[0].Size) + + assert.Equal(t, "old.txt", files[1].Name) + assert.Equal(t, "/work/uploads/old.txt", files[1].Path) + assert.Equal(t, int64(len("old")), files[1].Size) +} + +func TestFlowFileService_ListFlowFiles_MissingDirectory(t *testing.T) { + service := NewFlowFileService(nil, t.TempDir()) + + files, err := service.listFlowFiles(999) + require.NoError(t, err) + assert.Empty(t, files) +} diff --git a/frontend/src/features/flows/files/flow-files.tsx b/frontend/src/features/flows/files/flow-files.tsx new file mode 100644 index 000000000..9c2b05d3c --- /dev/null +++ b/frontend/src/features/flows/files/flow-files.tsx @@ -0,0 +1,289 @@ +import { AlertCircle, Copy, Download, FileUp, FolderUp, Loader2, RefreshCw } from 'lucide-react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { toast } from 'sonner'; + +import { Button, buttonVariants } from '@/components/ui/button'; +import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'; +import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/ui/input-group'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { axios } from '@/lib/axios'; +import { copyToClipboard } from '@/lib/report'; +import { cn } from '@/lib/utils'; +import { formatDate } from '@/lib/utils/format'; +import { baseUrl } from '@/models/api'; +import { useFlow } from '@/providers/flow-provider'; + +interface FlowFile { + modifiedAt: string; + name: string; + path: string; + size: number; +} + +interface FlowFilesResponse { + files: Array; + total: number; +} + +const formatFileSize = (size: number) => { + if (size < 1024) { + return `${size} B`; + } + + const units = ['KB', 'MB', 'GB', 'TB']; + let unitIndex = -1; + let value = size; + + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024; + unitIndex += 1; + } + + return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`; +}; + +const FlowFiles = () => { + const { flowId } = useFlow(); + const inputRef = useRef(null); + const [files, setFiles] = useState>([]); + const [isLoading, setIsLoading] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [searchValue, setSearchValue] = useState(''); + + const loadFiles = useCallback(async () => { + if (!flowId) { + setFiles([]); + + return; + } + + setIsLoading(true); + + try { + const data = await axios.get(`/flows/${flowId}/files`); + setFiles(data.files ?? []); + } catch (error) { + const description = error instanceof Error ? error.message : 'An error occurred while loading flow files'; + toast.error('Failed to load files', { + description, + }); + } finally { + setIsLoading(false); + } + }, [flowId]); + + useEffect(() => { + void loadFiles(); + }, [loadFiles]); + + const handleUploadButtonClick = useCallback(() => { + inputRef.current?.click(); + }, []); + + const handleCopyPath = useCallback(async (filePath: string) => { + const success = await copyToClipboard(filePath); + + if (success) { + toast.success('Path copied to clipboard'); + + return; + } + + toast.error('Failed to copy path'); + }, []); + + const handleFileSelection = useCallback( + async (event: React.ChangeEvent) => { + if (!flowId) { + return; + } + + const selectedFiles = Array.from(event.target.files ?? []); + + if (selectedFiles.length === 0) { + return; + } + + const formData = new FormData(); + + for (const file of selectedFiles) { + formData.append('files', file); + } + + setIsUploading(true); + + try { + const data = await axios.post(`/flows/${flowId}/files`, formData); + const uploadedCount = data.files?.length ?? selectedFiles.length; + + toast.success(uploadedCount === 1 ? 'File uploaded successfully' : 'Files uploaded successfully', { + description: + uploadedCount === 1 + ? `${data.files?.[0]?.path ?? '/work/uploads'}` + : `${uploadedCount} files are now available under /work/uploads`, + }); + + await loadFiles(); + } catch (error) { + const description = error instanceof Error ? error.message : 'An error occurred while uploading files'; + toast.error('Failed to upload files', { + description, + }); + } finally { + setIsUploading(false); + event.target.value = ''; + } + }, + [flowId, loadFiles], + ); + + const filteredFiles = useMemo(() => { + const normalizedSearch = searchValue.toLowerCase().trim(); + + if (!normalizedSearch) { + return files; + } + + return files.filter((file) => { + return ( + file.name.toLowerCase().includes(normalizedSearch) || file.path.toLowerCase().includes(normalizedSearch) + ); + }); + }, [files, searchValue]); + + return ( +
+ + +
+
+
+ +
+

Uploaded files are shared with the whole flow.

+

+ PentAGI stores user uploads under /work/uploads, so automation, assistants, + and container commands can access the same files immediately. +

+
+
+
+ +
+ + + + + setSearchValue(event.target.value)} + placeholder="Filter uploaded files..." + type="text" + value={searchValue} + /> + {searchValue && ( + + setSearchValue('')} + type="button" + > + Clear + + + )} + + +
+ + +
+
+
+ + {filteredFiles.length > 0 ? ( +
+ {filteredFiles.map((file) => ( +
+
+
+
+ +

{file.name}

+
+
+

{formatFileSize(file.size)}

+
+ {file.path} + + + + + Copy container path + +
+

{formatDate(new Date(file.modifiedAt))}

+
+
+ + + + Download + +
+
+ ))} +
+ ) : ( + + + + + + No uploaded files + + Upload files into /work/uploads when you want this flow, its assistants, or + container commands to use them. + + + + )} +
+ ); +}; + +export default FlowFiles; diff --git a/frontend/src/features/flows/flow-tabs.tsx b/frontend/src/features/flows/flow-tabs.tsx index a86b74ba2..6d911b0a9 100644 --- a/frontend/src/features/flows/flow-tabs.tsx +++ b/frontend/src/features/flows/flow-tabs.tsx @@ -4,6 +4,7 @@ import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import FlowAgents from '@/features/flows/agents/flow-agents'; import FlowDashboard from '@/features/flows/dashboard/flow-dashboard'; +import FlowFiles from '@/features/flows/files/flow-files'; import FlowAssistantMessages from '@/features/flows/messages/flow-assistant-messages'; import FlowAutomationMessages from '@/features/flows/messages/flow-automation-messages'; import FlowScreenshots from '@/features/flows/screenshots/flow-screenshots'; @@ -49,6 +50,7 @@ const FlowTabs = ({ activeTab, onTabChange }: FlowTabsProps) => { Agents Searches Vector Store + Files Screenshots @@ -117,6 +119,13 @@ const FlowTabs = ({ activeTab, onTabChange }: FlowTabsProps) => { + + + + Date: Tue, 14 Apr 2026 21:05:05 +0700 Subject: [PATCH 017/275] fix: lint (cherry picked from commit 2aac8ff0563483d01b16bb5ee8c191fafa8aff14) --- frontend/src/components/shared/markdown.tsx | 27 +++-- .../src/components/shared/monaco-terminal.tsx | 6 +- .../components/shared/terminal/use-xterm.ts | 5 +- frontend/src/components/ui/input-group.tsx | 2 +- frontend/src/components/ui/input.tsx | 2 +- frontend/src/components/ui/kbd.tsx | 46 ++++---- frontend/src/components/ui/textarea.tsx | 8 +- .../src/features/flows/agents/flow-agent.tsx | 15 ++- .../features/flows/messages/flow-message.tsx | 22 ++-- .../src/features/flows/tasks/flow-subtask.tsx | 21 ++-- .../src/features/flows/tasks/flow-task.tsx | 15 ++- .../src/features/flows/tools/flow-tool.tsx | 14 ++- .../flows/vector-stores/flow-vector-store.tsx | 14 ++- .../hooks/use-adaptive-column-visibility.ts | 4 +- frontend/src/lib/report-pdf.tsx | 4 +- frontend/src/lib/table-storage.ts | 20 ++-- "frontend/src/lib/\321\201lipboard.ts" | 10 +- frontend/src/pages/flows/flow-report.tsx | 106 ++++++++++-------- frontend/src/pages/login.tsx | 5 +- .../pages/settings/settings-mcp-servers.tsx | 1 - .../src/pages/settings/settings-prompt.tsx | 2 - .../src/pages/settings/settings-prompts.tsx | 1 - .../src/pages/settings/settings-provider.tsx | 6 +- .../src/pages/settings/settings-providers.tsx | 1 - frontend/src/providers/theme-provider.tsx | 1 + frontend/src/styles/index.css | 12 +- 26 files changed, 194 insertions(+), 176 deletions(-) diff --git a/frontend/src/components/shared/markdown.tsx b/frontend/src/components/shared/markdown.tsx index a3d32b673..8d37a41f6 100644 --- a/frontend/src/components/shared/markdown.tsx +++ b/frontend/src/components/shared/markdown.tsx @@ -147,8 +147,8 @@ const Markdown = ({ children, className, searchValue }: MarkdownProps) => { ); // Optimized helper function to process text nodes recursively - const processTextNode = useCallback( - (nodeChildren: any): any => { + const processTextNode = useMemo(() => { + const fn = (nodeChildren: any): any => { if (!processedSearch) { return nodeChildren; } @@ -163,15 +163,13 @@ const Markdown = ({ children, className, searchValue }: MarkdownProps) => { return createHighlightedText(child); } - // Avoid deep cloning React elements to prevent memory leaks - // Only process if it's a simple object with props if (child && typeof child === 'object' && child.props && child.props.children !== undefined) { return { ...child, key: child.key || `processed-${index}`, props: { ...child.props, - children: processTextNode(child.props.children), + children: fn(child.props.children), }, }; } @@ -180,7 +178,6 @@ const Markdown = ({ children, className, searchValue }: MarkdownProps) => { }); } - // Handle React elements safely if ( nodeChildren && typeof nodeChildren === 'object' && @@ -191,25 +188,31 @@ const Markdown = ({ children, className, searchValue }: MarkdownProps) => { ...nodeChildren, props: { ...nodeChildren.props, - children: processTextNode(nodeChildren.props.children), + children: fn(nodeChildren.props.children), }, }; } return nodeChildren; - }, - [processedSearch, createHighlightedText], - ); + }; + + return fn; + }, [processedSearch, createHighlightedText]); // Create a simple component renderer factory to avoid recreating functions const createComponentRenderer = useCallback( (ComponentName: string) => { - return ({ children: nodeChildren, ...props }: any) => { + const Component = ComponentName as React.ElementType; + + const Renderer = ({ children: nodeChildren, ...props }: Record) => { const processedChildren = processTextNode(nodeChildren); - const Component = ComponentName as any; return {processedChildren}; }; + + Renderer.displayName = `Highlighted(${ComponentName})`; + + return Renderer; }, [processTextNode], ); diff --git a/frontend/src/components/shared/monaco-terminal.tsx b/frontend/src/components/shared/monaco-terminal.tsx index 52b5e09c7..43c0a3fe4 100644 --- a/frontend/src/components/shared/monaco-terminal.tsx +++ b/frontend/src/components/shared/monaco-terminal.tsx @@ -67,7 +67,11 @@ const injectedColorClasses = new Set(); const rgbStringToHex = (rgb: string): string => rgb .split(',') - .map((part) => Math.min(255, Math.max(0, parseInt(part.trim(), 10))).toString(16).padStart(2, '0')) + .map((part) => + Math.min(255, Math.max(0, parseInt(part.trim(), 10))) + .toString(16) + .padStart(2, '0'), + ) .join(''); /** diff --git a/frontend/src/components/shared/terminal/use-xterm.ts b/frontend/src/components/shared/terminal/use-xterm.ts index 5153a97fd..0ebd1f2bc 100644 --- a/frontend/src/components/shared/terminal/use-xterm.ts +++ b/frontend/src/components/shared/terminal/use-xterm.ts @@ -102,10 +102,7 @@ export function useXterm({ theme }: { theme: 'dark' | 'light' | 'system' }): Use const openLink = (event: MouseEvent, uri: string) => { const uriLower = uri.toLowerCase(); - if ( - (mac ? event.metaKey : event.ctrlKey) && - SAFE_PROTOCOLS.some((p) => uriLower.startsWith(p)) - ) { + if ((mac ? event.metaKey : event.ctrlKey) && SAFE_PROTOCOLS.some((p) => uriLower.startsWith(p))) { window.open(uri, '_blank', 'noopener,noreferrer'); } }; diff --git a/frontend/src/components/ui/input-group.tsx b/frontend/src/components/ui/input-group.tsx index 31d91d22a..6b4a10443 100644 --- a/frontend/src/components/ui/input-group.tsx +++ b/frontend/src/components/ui/input-group.tsx @@ -121,7 +121,7 @@ function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) { ); } -function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) { +function InputGroupTextarea({ className, ...props }: React.ComponentProps) { return (