From c8cd0e68f9231ed0b49d3175eac2c32d421af962 Mon Sep 17 00:00:00 2001
From: Dmitry Ng <19asdek91@gmail.com>
Date: Sun, 29 Mar 2026 15:53:30 +0300
Subject: [PATCH] feat: add Docker host network mode support and improve agent
terminal execution
- Add host network mode support in Docker client (DOCKER_NETWORK=host)
- Update documentation for network modes (bridge vs host)
- Enhance OOB port allocation guidance with mandatory directives
- Improve terminal command execution descriptions (detach, timeout)
- Fix MSF workflow issues: add process isolation rules and RPC daemon patterns
- Add terminal execution mechanics to adviser prompts for better monitoring
- Update installer locale with host network mode explanation
Fixes agent issues with msfconsole hanging, port conflicts, and process isolation.
---
backend/cmd/installer/wizard/locale/locale.go | 26 ++++-
backend/docs/config.md | 44 +++++---
backend/docs/docker.md | 45 +++++---
backend/pkg/docker/client.go | 103 ++++++++++--------
backend/pkg/graph/schema.resolvers.go | 2 +-
backend/pkg/providers/helpers.go | 66 ++++++++---
backend/pkg/providers/provider.go | 7 +-
backend/pkg/providers/providers.go | 6 +
backend/pkg/templates/prompts/adviser.tmpl | 39 +++++++
backend/pkg/templates/prompts/pentester.tmpl | 37 ++++++-
.../prompts/question_execution_monitor.tmpl | 8 ++
.../prompts/question_task_planner.tmpl | 6 +
backend/pkg/tools/args.go | 18 +--
backend/pkg/tools/browser.go | 15 +--
backend/pkg/tools/terminal.go | 84 +++++++-------
backend/pkg/tools/terminal_test.go | 12 +-
backend/pkg/tools/tools.go | 8 +-
build/.gitkeep | 0
18 files changed, 364 insertions(+), 162 deletions(-)
create mode 100644 build/.gitkeep
diff --git a/backend/cmd/installer/wizard/locale/locale.go b/backend/cmd/installer/wizard/locale/locale.go
index ec71cbc6..b7c7f360 100644
--- a/backend/cmd/installer/wizard/locale/locale.go
+++ b/backend/cmd/installer/wizard/locale/locale.go
@@ -1505,7 +1505,7 @@ Configuration combines based on scenario: enable both capabilities for full pent
ToolsDockerSocket = "Docker Socket"
ToolsDockerSocketDesc = "Path to Docker socket on host filesystem"
ToolsDockerNetwork = "Docker Network"
- ToolsDockerNetworkDesc = "Custom network name for worker containers"
+ ToolsDockerNetworkDesc = "Custom network name for worker containers, or 'host' for direct host network access"
ToolsDockerPublicIP = "Public IP Address"
ToolsDockerPublicIPDesc = "Public IP for reverse connections in OOB attacks"
@@ -1549,13 +1549,27 @@ When using DinD, use the path to the Docker socket file of the DinD container wh
Example: /var/run/docker.sock`
- ToolsDockerNetworkHelp = `Custom Docker Network provides isolation for worker containers. Allows fine-grained firewall rules and network policies.
+ ToolsDockerNetworkHelp = `Docker Network controls network isolation mode for worker containers:
-Useful for:
-• Isolating worker traffic
-• Custom network configurations
+Bridge Mode (custom network name):
+• Isolated communication between containers
+• Port forwarding from container to host
• Enhanced security boundaries
-• Network-based monitoring`
+• Network-based monitoring and filtering
+• Recommended for most use cases
+
+Host Mode (value: 'host'):
+• Direct access to host network interfaces
+• No port forwarding - ports bind directly to host
+• Required for raw packet manipulation
+• Advanced network testing capabilities
+• Lower isolation - use with caution
+
+Examples:
+• 'pentagi-network' - creates isolated bridge network
+• 'host' - enables direct host network access
+
+Security Note: Host network mode reduces container isolation. Only use when necessary for advanced penetration testing tasks requiring direct network stack access.`
ToolsDockerPublicIPHelp = `Public IP Address enables out-of-band (OOB) attack techniques by providing workers with a reachable address for reverse connections.
diff --git a/backend/docs/config.md b/backend/docs/config.md
index 949884e2..26b2c342 100644
--- a/backend/docs/config.md
+++ b/backend/docs/config.md
@@ -59,7 +59,7 @@ This document serves as a comprehensive guide to the configuration system in Pen
- [Perplexity Search](#perplexity-search)
- [Searxng Search](#searxng-search)
- [Usage Details](#usage-details-10)
- - [Proxy Settings](#proxy-settings)
+ - [Network and Proxy Settings](#network-and-proxy-settings)
- [Usage Details](#usage-details-11)
- [Graphiti Knowledge Graph Settings](#graphiti-knowledge-graph-settings)
- [Usage Details](#usage-details-12)
@@ -199,16 +199,16 @@ if cfg.LicenseKey != "" {
These settings control how PentAGI interacts with Docker, which is used for terminal isolation and executing commands in a controlled environment. They're crucial for the security and functionality of tool execution.
-| Option | Environment Variable | Default Value | Description |
-| ---------------------------- | ---------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------ |
-| DockerInside | `DOCKER_INSIDE` | `false` | Set to `true` if PentAGI runs inside Docker and needs to access the host Docker daemon. |
+| Option | Environment Variable | Default Value | Description |
+| ---------------------------- | ---------------------------------- | ---------------------- | ----------- |
+| DockerInside | `DOCKER_INSIDE` | `false` | Set to `true` if PentAGI runs inside Docker and needs to access the host Docker daemon. |
| DockerNetAdmin | `DOCKER_NET_ADMIN` | `false` | Set to `true` to grant the primary container NET_ADMIN capability for advanced networking. |
-| DockerSocket | `DOCKER_SOCKET` | *(none)* | Path to Docker socket for container management |
-| DockerNetwork | `DOCKER_NETWORK` | *(none)* | Docker network name for container communication |
-| DockerPublicIP | `DOCKER_PUBLIC_IP` | `0.0.0.0` | Public IP address for Docker containers' port bindings |
-| 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 |
+| DockerSocket | `DOCKER_SOCKET` | *(none)* | Path to Docker socket for container management |
+| DockerNetwork | `DOCKER_NETWORK` | *(none)* | Docker network name for bridge mode, or `host` for host network mode. See network modes below. |
+| DockerPublicIP | `DOCKER_PUBLIC_IP` | `0.0.0.0` | Public IP address for Docker containers' port bindings (bridge mode only) |
+| 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 |
### Usage Details
@@ -227,17 +227,35 @@ The Docker settings are primarily used in `pkg/docker/client.go` which implement
}
```
-- **DockerNetwork**: Sets the network that containers should join, enabling container-to-container communication:
+- **DockerNetwork**: Controls the network isolation mode for containers. Supports two modes:
+
+ **Bridge Mode** (custom network name, e.g., `pentagi-network`):
+ - Containers run in an isolated bridge network
+ - Port forwarding maps container ports to host ports
+ - Enhanced security through network isolation
+ - Recommended for most deployments
+
+ **Host Mode** (special value: `host`):
+ - Containers share the host's network stack directly
+ - No port forwarding - services bind directly to host interfaces
+ - Required for advanced network testing (raw packets, custom protocols)
+ - Reduced isolation - use with caution
+
```go
network := cfg.DockerNetwork
- // Used when creating network configuration
- if dc.network != "" {
+ // Host network mode
+ if dc.network == "host" {
+ hostConfig.NetworkMode = container.NetworkMode("host")
+ // No port bindings needed
+ } else if dc.network != "" {
+ // Bridge mode with custom network
networkingConfig = &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
dc.network: {},
},
}
+ // Port bindings are configured
}
```
diff --git a/backend/docs/docker.md b/backend/docs/docker.md
index 1036a265..d1ee49b9 100644
--- a/backend/docs/docker.md
+++ b/backend/docs/docker.md
@@ -92,8 +92,8 @@ The Docker client is configured through several environment variables defined in
| `DOCKER_INSIDE` | `false` | Whether PentAGI communicates with host Docker daemon from containers |
| `DOCKER_NET_ADMIN` | `false` | Whether PentAGI grants the primary container NET_ADMIN capability for advanced networking. |
| `DOCKER_SOCKET` | `/var/run/docker.sock` | Path to Docker socket on host |
-| `DOCKER_NETWORK` | | Docker network for container communication |
-| `DOCKER_PUBLIC_IP` | `0.0.0.0` | Public IP for port binding |
+| `DOCKER_NETWORK` | | Docker network for container communication (bridge mode) or `host` for host network mode |
+| `DOCKER_PUBLIC_IP` | `0.0.0.0` | Public IP for port binding (bridge mode only) |
| `DOCKER_WORK_DIR` | | Custom work directory path on host |
| `DOCKER_DEFAULT_IMAGE` | `debian:latest` | Fallback image if AI-selected image fails |
| `DOCKER_DEFAULT_IMAGE_FOR_PENTEST` | `vxcontrol/kali-linux` | Default Docker image for penetration testing tasks |
@@ -173,10 +173,25 @@ PentAGI supports running inside Docker containers while still managing other con
### Network Configuration
-When `DOCKER_NETWORK` is specified, all containers are automatically connected to this network, enabling:
-- Isolated communication between PentAGI components
-- Controlled access to external networks
-- Service discovery within the PentAGI ecosystem
+PentAGI supports two network modes for container isolation:
+
+#### Bridge Network Mode (Default)
+
+When `DOCKER_NETWORK` is set to a custom network name (e.g., `pentagi-network`), containers are connected to an isolated bridge network:
+- **Isolated Communication**: Containers communicate only within the defined network
+- **Port Mapping**: Container ports are mapped to host ports for external access
+- **Service Discovery**: Enables internal DNS-based service discovery
+- **Enhanced Security**: Network-level isolation from other containers
+
+#### Host Network Mode
+
+When `DOCKER_NETWORK` is set to the special value `host`, containers use the host's network stack directly:
+- **Direct Network Access**: Container shares the host's network interfaces
+- **No Port Mapping**: Ports are directly accessible on host interfaces (no NAT)
+- **Performance**: Eliminates network virtualization overhead
+- **Use Cases**: Advanced network testing, raw packet manipulation, network monitoring
+
+**Security Consideration**: Host network mode reduces isolation. Use only when necessary for penetration testing tasks requiring direct host network access.
## Core Interfaces
@@ -187,10 +202,10 @@ The main interface defines all Docker operations available to PentAGI components
```go
type DockerClient interface {
// Container lifecycle management
- SpawnContainer(ctx context.Context, containerName string, containerType database.ContainerType,
+ RunContainer(ctx context.Context, containerName string, containerType database.ContainerType,
flowID int64, config *container.Config, hostConfig *container.HostConfig) (database.Container, error)
StopContainer(ctx context.Context, containerID string, dbID int64) error
- DeleteContainer(ctx context.Context, containerID string, dbID int64) error
+ RemoveContainer(ctx context.Context, containerID string, dbID int64) error
IsContainerRunning(ctx context.Context, containerID string) (bool, error)
// Command execution
@@ -229,7 +244,7 @@ type dockerClient struct {
### Container Creation Process
-The `SpawnContainer` method handles the complete container creation workflow:
+The `RunContainer` method handles the complete container creation workflow:
1. **Preparation**:
- Creates flow-specific work directory
@@ -253,9 +268,9 @@ The `SpawnContainer` method handles the complete container creation workflow:
- Optionally mounts Docker socket for Docker-in-Docker
5. **Network and Ports**:
- - Assigns flow-specific ports using deterministic algorithm
- - Connects to specified Docker network if configured
- - Binds ports to public IP
+ - **Bridge Mode**: Assigns flow-specific ports using deterministic algorithm, binds to public IP
+ - **Host Mode** (`DOCKER_NETWORK=host`): Uses host network stack, skips port bindings
+ - Connects to specified Docker network (unless host mode)
6. **Container Startup**:
- Creates container with all configurations
@@ -451,7 +466,7 @@ if err != nil {
// Create container for a flow
containerName := docker.PrimaryTerminalName(flowID)
-container, err := dockerClient.SpawnContainer(
+container, err := dockerClient.RunContainer(
ctx,
containerName,
database.ContainerTypePrimary,
@@ -513,7 +528,7 @@ isRunning, err := dockerClient.IsContainerRunning(ctx, containerID)
err = dockerClient.StopContainer(ctx, containerID, dbID)
// Remove container and volumes
-err = dockerClient.DeleteContainer(ctx, containerID, dbID)
+err = dockerClient.RemoveContainer(ctx, containerID, dbID)
// Global cleanup (usually called on startup)
err = dockerClient.Cleanup(ctx)
@@ -523,7 +538,7 @@ err = dockerClient.Cleanup(ctx)
```go
// The client implements comprehensive error handling
-container, err := dockerClient.SpawnContainer(ctx, name, containerType, flowID, config, hostConfig)
+container, err := dockerClient.RunContainer(ctx, name, containerType, flowID, config, hostConfig)
if err != nil {
// Errors include:
// - Image pull failures (handled with fallback)
diff --git a/backend/pkg/docker/client.go b/backend/pkg/docker/client.go
index 668289cf..e95d306e 100644
--- a/backend/pkg/docker/client.go
+++ b/backend/pkg/docker/client.go
@@ -53,17 +53,17 @@ type dockerClient struct {
}
type DockerClient interface {
- LaunchContainer(ctx context.Context, containerName string, containerType database.ContainerType,
+ RunContainer(ctx context.Context, containerName string, containerType database.ContainerType,
flowID int64, config *container.Config, hostConfig *container.HostConfig) (database.Container, error)
- HaltContainer(ctx context.Context, containerID string, dbID int64) error
- PurgeContainer(ctx context.Context, containerID string, dbID int64) error
- VerifyContainerRuntime(ctx context.Context, containerID string) (bool, error)
+ StopContainer(ctx context.Context, containerID string, dbID int64) error
+ RemoveContainer(ctx context.Context, containerID string, dbID int64) error
+ IsContainerRunning(ctx context.Context, containerID string) (bool, error)
ContainerExecCreate(ctx context.Context, container string, config container.ExecOptions) (container.ExecCreateResponse, error)
ContainerExecAttach(ctx context.Context, execID string, config container.ExecAttachOptions) (types.HijackedResponse, error)
ContainerExecInspect(ctx context.Context, execID string) (container.ExecInspect, error)
CopyToContainer(ctx context.Context, containerID string, dstPath string, content io.Reader, options container.CopyToContainerOptions) error
CopyFromContainer(ctx context.Context, containerID string, srcPath string) (io.ReadCloser, container.PathStat, error)
- CleanupAllResources(ctx context.Context) error
+ Cleanup(ctx context.Context) error
GetDefaultImage() string
}
@@ -151,7 +151,7 @@ func NewDockerClient(ctx context.Context, db database.Querier, cfg *config.Confi
}, nil
}
-func (dc *dockerClient) LaunchContainer(
+func (dc *dockerClient) RunContainer(
ctx context.Context,
containerName string,
containerType database.ContainerType,
@@ -181,7 +181,7 @@ func (dc *dockerClient) LaunchContainer(
"work_dir": workDir,
"host_dir": hostDir,
})
- logger.Info("spawning container")
+ logger.Info("running container")
dbContainer, err := dc.db.CreateContainer(ctx, database.CreateContainerParams{
Type: containerType,
@@ -275,29 +275,38 @@ func (dc *dockerClient) LaunchContainer(
},
}
- if hostConfig.PortBindings == nil {
- hostConfig.PortBindings = nat.PortMap{}
- }
- if config.ExposedPorts == nil {
- config.ExposedPorts = nat.PortSet{}
- }
- for _, port := range GetPrimaryContainerPorts(flowID) {
- natPort := nat.Port(fmt.Sprintf("%d/tcp", port))
- hostConfig.PortBindings[natPort] = []nat.PortBinding{
- {
- HostIP: dc.publicIP,
- HostPort: fmt.Sprintf("%d", port),
- },
- }
- config.ExposedPorts[natPort] = struct{}{}
- }
-
+ // Configure network mode and port bindings
var networkingConfig *network.NetworkingConfig
- if dc.network != "" {
- networkingConfig = &network.NetworkingConfig{
- EndpointsConfig: map[string]*network.EndpointSettings{
- dc.network: {},
- },
+ if dc.network == "host" {
+ // Host network mode: container uses host network stack directly
+ // No port bindings needed as container has direct access to host interfaces
+ hostConfig.NetworkMode = container.NetworkMode("host")
+ logger.Debug("using host network mode - container will have direct access to host network interfaces")
+ } else {
+ // Bridge network mode: configure port bindings and custom network
+ if hostConfig.PortBindings == nil {
+ hostConfig.PortBindings = nat.PortMap{}
+ }
+ if config.ExposedPorts == nil {
+ config.ExposedPorts = nat.PortSet{}
+ }
+ for _, port := range GetPrimaryContainerPorts(flowID) {
+ natPort := nat.Port(fmt.Sprintf("%d/tcp", port))
+ hostConfig.PortBindings[natPort] = []nat.PortBinding{
+ {
+ HostIP: dc.publicIP,
+ HostPort: fmt.Sprintf("%d", port),
+ },
+ }
+ config.ExposedPorts[natPort] = struct{}{}
+ }
+
+ if dc.network != "" {
+ networkingConfig = &network.NetworkingConfig{
+ EndpointsConfig: map[string]*network.EndpointSettings{
+ dc.network: {},
+ },
+ }
}
}
@@ -356,7 +365,7 @@ func (dc *dockerClient) LaunchContainer(
return dbContainer, nil
}
-func (dc *dockerClient) HaltContainer(ctx context.Context, containerID string, dbID int64) error {
+func (dc *dockerClient) StopContainer(ctx context.Context, containerID string, dbID int64) error {
logger := dc.logger.WithContext(ctx).WithField("local_id", containerID)
logger.Info("initiating container shutdown sequence")
@@ -369,23 +378,25 @@ func (dc *dockerClient) HaltContainer(ctx context.Context, containerID string, d
}
}
- if _, err := dc.db.UpdateContainerStatus(ctx, database.UpdateContainerStatusParams{
+ _, err := dc.db.UpdateContainerStatus(ctx, database.UpdateContainerStatusParams{
Status: database.ContainerStatusStopped,
ID: dbID,
- }); err != nil {
+ })
+ if err != nil {
return fmt.Errorf("database status update failed during container stop: %w", err)
}
logger.Info("container shutdown completed successfully")
+
return nil
}
-func (dc *dockerClient) PurgeContainer(ctx context.Context, containerID string, dbID int64) error {
+func (dc *dockerClient) RemoveContainer(ctx context.Context, containerID string, dbID int64) error {
logger := dc.logger.WithContext(ctx).WithField("local_id", containerID)
- logger.Info("purging container and associated resources")
+ logger.Info("removing container and associated resources")
- if err := dc.HaltContainer(ctx, containerID, dbID); err != nil {
- return fmt.Errorf("failed to halt container before purge: %w", err)
+ if err := dc.StopContainer(ctx, containerID, dbID); err != nil {
+ return fmt.Errorf("failed to stop container: %w", err)
}
options := container.RemoveOptions{
@@ -413,7 +424,7 @@ func (dc *dockerClient) PurgeContainer(ctx context.Context, containerID string,
return nil
}
-func (dc *dockerClient) CleanupAllResources(ctx context.Context) error {
+func (dc *dockerClient) Cleanup(ctx context.Context) error {
logger := dc.logger.WithContext(ctx).WithField("docker", "cleanup")
logger.Info("cleaning up containers and making all flows finished...")
@@ -437,12 +448,12 @@ func (dc *dockerClient) CleanupAllResources(ctx context.Context) error {
}
var wg sync.WaitGroup
- deleteContainer := func(containerID string, dbID int64) {
+ removeContainer := func(containerID string, dbID int64) {
defer wg.Done()
logger := logger.WithField("local_id", containerID)
- if err := dc.PurgeContainer(ctx, containerID, dbID); err != nil {
- logger.WithError(err).Errorf("failed to delete container")
+ if err := dc.RemoveContainer(ctx, containerID, dbID); err != nil {
+ logger.WithError(err).Errorf("failed to remove container")
}
_, err := dc.db.UpdateContainerStatus(ctx, database.UpdateContainerStatusParams{
@@ -492,7 +503,7 @@ func (dc *dockerClient) CleanupAllResources(ctx context.Context) error {
switch container.Status {
case database.ContainerStatusStarting, database.ContainerStatusRunning:
wg.Add(1)
- go deleteContainer(container.LocalID.String, container.ID)
+ go removeContainer(container.LocalID.String, container.ID)
}
}
}
@@ -504,7 +515,7 @@ func (dc *dockerClient) CleanupAllResources(ctx context.Context) error {
return nil
}
-func (dc *dockerClient) VerifyContainerRuntime(ctx context.Context, containerID string) (bool, error) {
+func (dc *dockerClient) IsContainerRunning(ctx context.Context, containerID string) (bool, error) {
inspection, err := dc.client.ContainerInspect(ctx, containerID)
if err != nil {
return false, fmt.Errorf("container inspection failed: %w", err)
@@ -582,7 +593,7 @@ func (dc *dockerClient) pullImage(ctx context.Context, imageName string) error {
pullStream, err := dc.client.ImagePull(ctx, imageName, image.PullOptions{})
if err != nil {
- return fmt.Errorf("image pull request failed: %w", err)
+ return fmt.Errorf("failed to pull image: %w", err)
}
defer pullStream.Close()
@@ -591,7 +602,8 @@ func (dc *dockerClient) pullImage(ctx context.Context, imageName string) error {
return fmt.Errorf("image download stream processing failed: %w", err)
}
- dc.logger.WithContext(ctx).WithField("image", imageName).Debug("image download completed")
+ dc.logger.WithContext(ctx).WithField("image", imageName).Debug("image pull completed")
+
return nil
}
@@ -705,8 +717,9 @@ func getHostDataDir(ctx context.Context, cli *client.Client, dataDir, workDir st
// ensureDockerNetwork verifies that a docker network with the given name exists;
// if it does not, it attempts to create it.
+// Special case: "host" network mode is built-in and doesn't need creation.
func ensureDockerNetwork(ctx context.Context, cli *client.Client, name string) error {
- if name == "" {
+ if name == "" || name == "host" {
return nil
}
diff --git a/backend/pkg/graph/schema.resolvers.go b/backend/pkg/graph/schema.resolvers.go
index a80980e5..340b1208 100644
--- a/backend/pkg/graph/schema.resolvers.go
+++ b/backend/pkg/graph/schema.resolvers.go
@@ -42,7 +42,7 @@ func (r *mutationResolver) CreateFlow(ctx context.Context, modelProvider string,
r.Logger.WithFields(logrus.Fields{
"uid": uid,
"provider": modelProvider,
- "input": input,
+ "input": input[:min(len(input), 1000)],
}).Debug("create flow")
if modelProvider == "" {
diff --git a/backend/pkg/providers/helpers.go b/backend/pkg/providers/helpers.go
index 3253db14..19c48ed2 100644
--- a/backend/pkg/providers/helpers.go
+++ b/backend/pkg/providers/helpers.go
@@ -786,24 +786,62 @@ func (fp *flowProvider) getContainerPortsDescription() string {
var buffer strings.Builder
buffer.WriteString("**OOB Attack Infrastructure:**\n\n")
- buffer.WriteString("This container has TCP ports bound for receiving out-of-band (OOB) callbacks:\n\n")
- for _, port := range ports {
- buffer.WriteString(fmt.Sprintf("- Port %d/tcp (container) → %s:%d (external)\n", port, fp.publicIP, port))
- }
+ // Host network mode: container has direct access to host network interfaces
+ if fp.dockerNetwork == "host" {
+ buffer.WriteString("This container uses **host network mode** with direct access to host network interfaces.\n\n")
+ buffer.WriteString("**MANDATORY PORTS - YOU MUST USE ONLY THESE:**\n\n")
- buffer.WriteString("\n**Usage for OOB Attacks:**\n")
+ for _, port := range ports {
+ buffer.WriteString(fmt.Sprintf("- Port %d/tcp (REQUIRED)\n", port))
+ }
- if fp.publicIP == "0.0.0.0" {
- buffer.WriteString("The bind IP is 0.0.0.0 (all interfaces). To receive external callbacks:\n")
- buffer.WriteString("1. Discover your public IP: `curl -s https://api.ipify.org` or `curl -s ipinfo.io/ip`\n")
- buffer.WriteString("2. Use discovered IP in exploit payloads for callbacks\n")
- buffer.WriteString("3. Listen on container ports (shown above) to receive connections\n\n")
- buffer.WriteString("**Important:** Check Task.Input - user may have specified the public IP to use.\n")
+ buffer.WriteString("\n**Network Access:**\n")
+ buffer.WriteString("- Direct access to all host network interfaces\n")
+ buffer.WriteString("- No port forwarding - services bind directly to host ports\n")
+ buffer.WriteString("- **CRITICAL**: DO NOT use any other ports - this may interfere with other running applications on the host system\n")
+ buffer.WriteString("- Using non-allocated ports could cause conflicts with system services and impact host stability\n")
+ buffer.WriteString("- All host network interfaces are accessible for binding\n\n")
+
+ buffer.WriteString("**Usage for OOB Attacks:**\n")
+ if fp.publicIP == "0.0.0.0" {
+ buffer.WriteString("To determine the public IP for callbacks:\n")
+ buffer.WriteString("1. Discover your public IP: `curl -s https://api.ipify.org` or `curl -s ipinfo.io/ip`\n")
+ buffer.WriteString("2. Use discovered IP in exploit payloads for callbacks\n")
+ buffer.WriteString("3. Listen on allocated ports (shown above) to receive connections\n\n")
+ buffer.WriteString("**Important:** Check Task.Input - user may have specified the public IP to use.\n")
+ } else {
+ buffer.WriteString(fmt.Sprintf("Your external IP is: **%s**\n\n", fp.publicIP))
+ buffer.WriteString("Use this IP in exploit payloads requiring callbacks (DNS exfiltration, reverse shells, XXE OOB, SSRF verification, etc.)\n")
+ buffer.WriteString("Listen on the allocated ports above to receive incoming connections.\n")
+ }
} else {
- buffer.WriteString(fmt.Sprintf("Your external IP is: %s\n", fp.publicIP))
- buffer.WriteString("Use this IP in exploit payloads requiring callbacks (DNS exfiltration, reverse shells, XXE OOB, SSRF verification, etc.)\n")
- buffer.WriteString("Listen on the container ports above to receive incoming connections.\n")
+ // Bridge network mode: traditional port forwarding
+ buffer.WriteString("**MANDATORY FORWARDED PORTS - YOU MUST USE ONLY THESE:**\n\n")
+
+ for _, port := range ports {
+ buffer.WriteString(fmt.Sprintf("- Port %d/tcp (container) → %s:%d (external)\n", port, fp.publicIP, port))
+ }
+
+ buffer.WriteString("\n**Port Usage Rules:**\n")
+ buffer.WriteString("- **YOU MUST use ONLY the ports listed above** for all listeners and reverse connections\n")
+ buffer.WriteString("- **CRITICAL**: Any reverse connections (shells, callbacks) will FAIL on other ports - only allocated ports are forwarded\n")
+ buffer.WriteString("- Standard ports like 4444, 8080, 9001 are NOT forwarded and will NOT work\n")
+ buffer.WriteString(fmt.Sprintf("- Example: For Metasploit reverse shell, use LPORT=%d (not 4444)\n\n", ports[0]))
+
+ buffer.WriteString("**Usage for OOB Attacks:**\n")
+
+ if fp.publicIP == "0.0.0.0" {
+ buffer.WriteString("The bind IP is 0.0.0.0 (all interfaces). To receive external callbacks:\n")
+ buffer.WriteString("1. Discover your public IP: `curl -s https://api.ipify.org` or `curl -s ipinfo.io/ip`\n")
+ buffer.WriteString("2. Use discovered IP in exploit payloads for callbacks\n")
+ buffer.WriteString("3. Listen on container ports (shown above) to receive connections\n\n")
+ buffer.WriteString("**Important:** Check Task.Input - user may have specified the public IP to use.\n")
+ } else {
+ buffer.WriteString(fmt.Sprintf("Your external IP is: %s\n", fp.publicIP))
+ buffer.WriteString("Use this IP in exploit payloads requiring callbacks (DNS exfiltration, reverse shells, XXE OOB, SSRF verification, etc.)\n")
+ buffer.WriteString("Listen on the container ports above to receive incoming connections.\n")
+ }
}
return buffer.String()
diff --git a/backend/pkg/providers/provider.go b/backend/pkg/providers/provider.go
index 61d9f907..83c8a6e8 100644
--- a/backend/pkg/providers/provider.go
+++ b/backend/pkg/providers/provider.go
@@ -26,7 +26,7 @@ import (
"github.com/vxcontrol/langchaingo/llms/streaming"
)
-const ToolPlaceholder = "Always use your function calling functionality, instead of returning a text result."
+const ToolPlaceholder = "Execute operations via function invocation - textual responses are not acceptable for task completion."
const TasksNumberLimit = 15
@@ -129,8 +129,9 @@ type flowProvider struct {
embedder embeddings.Embedder
graphitiClient *graphiti.Client
- flowID int64
- publicIP string
+ flowID int64
+ publicIP string
+ dockerNetwork string
callCounter *atomic.Int64
diff --git a/backend/pkg/providers/providers.go b/backend/pkg/providers/providers.go
index 4d05358e..62ac438c 100644
--- a/backend/pkg/providers/providers.go
+++ b/backend/pkg/providers/providers.go
@@ -135,6 +135,7 @@ type providerController struct {
cfg *config.Config
docker docker.DockerClient
publicIP string
+ dockerNetwork string
embedder embeddings.Embedder
graphitiClient *graphiti.Client
@@ -357,6 +358,7 @@ func NewProviderController(
cfg: cfg,
docker: docker,
publicIP: cfg.DockerPublicIP,
+ dockerNetwork: cfg.DockerNetwork,
embedder: embedder,
graphitiClient: graphitiClient,
@@ -446,6 +448,7 @@ func (pc *providerController) NewFlowProvider(
graphitiClient: pc.graphitiClient,
flowID: flowID,
publicIP: pc.publicIP,
+ dockerNetwork: pc.dockerNetwork,
callCounter: newAtomicInt64(pc.startCallNumber.Add(deltaCallCounter)),
image: image,
title: title,
@@ -495,6 +498,7 @@ func (pc *providerController) LoadFlowProvider(
graphitiClient: pc.graphitiClient,
flowID: flowID,
publicIP: pc.publicIP,
+ dockerNetwork: pc.dockerNetwork,
callCounter: newAtomicInt64(pc.startCallNumber.Add(deltaCallCounter)),
image: image,
title: title,
@@ -589,6 +593,7 @@ func (pc *providerController) NewAssistantProvider(
graphitiClient: pc.graphitiClient,
flowID: flowID,
publicIP: pc.publicIP,
+ dockerNetwork: pc.dockerNetwork,
callCounter: newAtomicInt64(pc.startCallNumber.Add(deltaCallCounter)),
image: image,
title: title,
@@ -641,6 +646,7 @@ func (pc *providerController) LoadAssistantProvider(
graphitiClient: pc.graphitiClient,
flowID: flowID,
publicIP: pc.publicIP,
+ dockerNetwork: pc.dockerNetwork,
callCounter: newAtomicInt64(pc.startCallNumber.Add(deltaCallCounter)),
image: image,
title: title,
diff --git a/backend/pkg/templates/prompts/adviser.tmpl b/backend/pkg/templates/prompts/adviser.tmpl
index c132d303..37ae0c13 100644
--- a/backend/pkg/templates/prompts/adviser.tmpl
+++ b/backend/pkg/templates/prompts/adviser.tmpl
@@ -69,8 +69,47 @@ Note: Only planned (not yet started) Subtasks can be modified.
- User may specify public IP in task description - extract and use it when advising on OOB techniques
- If IP unknown, recommend discovering via: `curl -s https://api.ipify.org` or `curl -s ipinfo.io/ip`
- Always consider OOB port availability when recommending callback-based attacks
+- **CRITICAL:** Agents MUST use only allocated ports - other ports are not forwarded (bridge mode) or may conflict with host services (host network mode)
+## BACKEND TERMINAL EXECUTION MECHANICS
+
+
+**Command Execution:** Each terminal command executes independently in isolated Docker exec session.
+
+**Detach Modes:**
+- **detach=true:** Process survives timeout, runs independently. Returns "started in background" after 500ms. Use for long-running daemons (msfrpcd, nc -l, HTTP servers).
+- **detach=false:** Waits for completion, returns output. Command fails if timeout exceeded. Agent must predict timeout accurately.
+
+**Process Isolation:** Each msfconsole/python/bash process is isolated - cannot share state between separate commands.
+
+**Common Agent Mistakes to Identify:**
+1. **Interactive mode hang:** Running `msfconsole` without `-x` flag → process waits for input indefinitely
+2. **Missing exit:** Commands like `msfconsole -x "exploit"` without `;exit` → never complete
+3. **Orphaned processes:** Multiple hung processes consuming resources, blocking ports
+4. **Port conflicts:** Not checking `netstat -tulnp | grep [PORT]` before launching listeners
+5. **Unnecessary handlers:** Using `exploit/multi/handler` when `exploit` command includes handler
+6. **Session isolation:** Trying to check sessions via new msfconsole instance (won't see them)
+
+**Correct MSF Patterns (recommend when you see mistakes above):**
+
+**Standalone (simple):** `msfconsole -q -x "use exploit/...; set LPORT [allocated]; exploit; sleep 20; sessions -l; exit"`
+All in one command (detach=false, timeout=120+).
+
+**RPC Daemon (complex workflows):**
+1. `msfrpcd -P pass -U user -a 127.0.0.1 -p 55553` (detach=true, check port first)
+2. `msfconsole -q -x "connect 127.0.0.1:55553 user pass; exploit; exit"` (detach=false)
+3. `msfconsole -q -x "connect ...; sessions -l; exit"` (connects to same daemon)
+
+**Diagnostic Commands:**
+- Check orphans: `ps aux | grep msfconsole` (look for multiple ruby processes)
+- Check ports: `netstat -tulnp | grep [PORT]`
+- Kill orphans: `pkill -f msfconsole`
+
+**Output Minimization:** Always recommend `-q` flags to reduce token usage.
+**Host Network Mode:** Shared localhost - check port availability before any daemon.
+
+
## INPUT DATA STRUCTURE
diff --git a/backend/pkg/templates/prompts/pentester.tmpl b/backend/pkg/templates/prompts/pentester.tmpl
index 59b31a16..925700e9 100644
--- a/backend/pkg/templates/prompts/pentester.tmpl
+++ b/backend/pkg/templates/prompts/pentester.tmpl
@@ -143,7 +143,19 @@ Use both:
Specify appropriate timeouts and redirect output for long-running processes
Maximum 3 attempts of identical tool calls
Auto-approve commands with flags like `-y` when possible
-Use `detach` for all commands except the final one in a sequence
+
+LONG-RUNNING processes (daemons, servers, monitors) → detach=true, timeout=600-1200
+Purpose: Process survives timeout, runs independently
+Examples: msfrpcd, nc -l, python -m http.server, tcpdump
+Behavior: Returns "started in background" after 500ms, process continues until killed
+
+BATCH commands (scanners, exploits, clients) → detach=false, predict timeout for completion
+Purpose: Get command output upon completion
+Examples: nmap, msfconsole -x "...; exit", gobuster, curl
+Behavior: Waits for completion, returns output; command fails if timeout too low
+
+Output minimization: Use `-q` flags where available (msfconsole -q, nmap --open, etc.)
+
Create dedicated working directories for file operations
@@ -252,7 +264,16 @@ hydra, john, hashcat, crunch, medusa, patator, hashid, hash-identifier, *2john (
-msfconsole, msfvenom, msfdb, msfrpc, msfupdate, msf-pattern_*, msf-find_badchars, msf-egghunter, msf-makeiplist
+msfconsole, msfvenom, msfdb, msfrpcd, msfupdate, msf-pattern_*, msf-find_badchars, msf-egghunter, msf-makeiplist
+
+CRITICAL msfconsole rules:
+- NEVER run `msfconsole` without `-x` flag (enters interactive mode and hangs)
+- ALWAYS use: `msfconsole -q -x "commands; exit"`
+- ALWAYS end command chain with `;exit` to prevent hanging processes
+- `exploit` command automatically starts handler - do NOT use `exploit/multi/handler` separately
+- Each msfconsole process is isolated - combine all operations in ONE command: `exploit; sleep 20; sessions -l; exit`
+- Check port availability before launch: `netstat -tulnp | grep [PORT]`
+- Kill orphaned processes: `pkill -f msfconsole`
@@ -283,6 +304,18 @@ Check tool availability with 'which [tool]' before use. Install missing tools if
{{end}}
+
+Standalone (recommended): All operations in one command
+`msfconsole -q -x "use exploit/...; set LPORT [allocated]; exploit; sleep 20; sessions -l; sessions -i 1 -c 'sysinfo'; exit"`
+Timeout=120+ (predict total time). All output captured.
+
+RPC Daemon (complex workflows):
+Check port → `msfrpcd -p 55553` (detach=true) → `msfconsole -q -x "connect 127.0.0.1:55553...; exit"` (detach=false) → cleanup
+
+Recovery from mistakes:
+If you see hanging or port conflicts: `pkill -f msfconsole`, verify with `ps aux | grep msfconsole`, check ports with `netstat -tulnp`
+
+
- Verify tool availability with 'which [toolname]' before attempting installation
diff --git a/backend/pkg/templates/prompts/question_execution_monitor.tmpl b/backend/pkg/templates/prompts/question_execution_monitor.tmpl
index 22303472..b4938e3f 100644
--- a/backend/pkg/templates/prompts/question_execution_monitor.tmpl
+++ b/backend/pkg/templates/prompts/question_execution_monitor.tmpl
@@ -47,4 +47,12 @@ Based on my execution history above, I need your expert analysis on the followin
5. Is this task impossible to complete as currently defined? Should I report what I've accomplished and terminate, or request assistance from the user?
6. What are the most critical and actionable next steps I should take right now to move forward effectively?
+When analyzing terminal commands, check for these common mistakes:
+- Running msfconsole without `-x` flag (hangs in interactive mode)
+- Missing `;exit` at end of command chain (process never completes, creates orphans)
+- Using `exploit/multi/handler` separately (unnecessary - `exploit` includes handler)
+- Trying to check sessions in new msfconsole process (process isolation - won't see them)
+- Not checking port availability before launching listeners (causes bind failures)
+- Multiple orphaned processes consuming resources (visible in `ps aux` output)
+
Please provide specific, concrete recommendations based on what you see in my execution history. I need clear guidance on whether to continue with my current approach, pivot to a different strategy, or conclude my work.
\ No newline at end of file
diff --git a/backend/pkg/templates/prompts/question_task_planner.tmpl b/backend/pkg/templates/prompts/question_task_planner.tmpl
index a97d462b..cb72c4f6 100644
--- a/backend/pkg/templates/prompts/question_task_planner.tmpl
+++ b/backend/pkg/templates/prompts/question_task_planner.tmpl
@@ -14,6 +14,12 @@ The plan should:
- Help me avoid redundant work by leveraging available context
- Guide me toward efficient task completion without unnecessary actions
+Important context for planning:
+- Terminal commands execute independently (no persistent state between calls)
+- msfconsole processes are isolated - plan all MSF operations in single commands or via RPC daemon
+- Check port availability before launching any listeners/daemons
+- Minimize output with -q flags to reduce token usage
+
Please format your response as a numbered checklist like this:
1. [First critical action/verification step]
2. [Second step with specific details]
diff --git a/backend/pkg/tools/args.go b/backend/pkg/tools/args.go
index fb750927..f6cab856 100644
--- a/backend/pkg/tools/args.go
+++ b/backend/pkg/tools/args.go
@@ -6,18 +6,18 @@ import (
"strings"
)
-type CodeAction string
+type FileOp string
const (
- ReadFile CodeAction = "read_file"
- UpdateFile CodeAction = "update_file"
+ ReadFile FileOp = "read_file"
+ UpdateFile FileOp = "update_file"
)
type FileAction struct {
- Action CodeAction `json:"action" jsonschema:"required,enum=read_file,enum=update_file" jsonschema_description:"Action to perform with the code. 'read_file' - Returns the content of the file. 'update_file' - Updates the content of the file"`
- Content string `json:"content" jsonschema_description:"Content to write to the file"`
- Path string `json:"path" jsonschema:"required" jsonschema_description:"Path to the file to read or update"`
- Message string `json:"message" jsonschema:"required,title=File action message" jsonschema_description:"Not so long message which explain what do you want to read or to write to the file and explain written content to send to the user in user's language only"`
+ Action FileOp `json:"action" jsonschema:"required,enum=read_file,enum=update_file" jsonschema_description:"Action to perform with the code. 'read_file' - Returns the content of the file. 'update_file' - Updates the content of the file"`
+ Content string `json:"content" jsonschema_description:"Content to write to the file"`
+ Path string `json:"path" jsonschema:"required" jsonschema_description:"Path to the file to read or update"`
+ Message string `json:"message" jsonschema:"required,title=File action message" jsonschema_description:"Not so long message which explain what do you want to read or to write to the file and explain written content to send to the user in user's language only"`
}
type BrowserAction string
@@ -93,8 +93,8 @@ type Done struct {
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:"True if the command should be executed in the background, use timeout argument to limit of the execution time and you can not get output from the command if you use detach"`
- Timeout Int64 `json:"timeout" jsonschema:"required,type=integer" jsonschema_description:"Limit in seconds for command execution in terminal to prevent blocking of the agent and it depends on the specific command (minimum 10; maximum 1200; default 60)"`
+ 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"`
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/browser.go b/backend/pkg/tools/browser.go
index 728d7821..593d1271 100644
--- a/backend/pkg/tools/browser.go
+++ b/backend/pkg/tools/browser.go
@@ -313,19 +313,20 @@ func (b *browser) resolveUrl(targetURL string) (*url.URL, error) {
return url.Parse(scraperURL)
}
-func (b *browser) saveScreenshotData(imageData []byte) (string, error) {
+func (b *browser) saveScreenshotData(screenshot []byte) (string, error) {
flowDirName := fmt.Sprintf("flow-%d", b.flowID)
- targetDir := filepath.Join(b.dataDir, "screenshots", flowDirName)
+ screenshotDir := filepath.Join(b.dataDir, "screenshots", flowDirName)
- if err := os.MkdirAll(targetDir, 0755); err != nil {
+ err := os.MkdirAll(screenshotDir, os.ModePerm)
+ if err != nil {
return "", fmt.Errorf("failed to prepare screenshot directory: %w", err)
}
- timestamp := time.Now().Unix()
- screenshotName := fmt.Sprintf("screenshot-%d.png", timestamp)
- fullPath := filepath.Join(targetDir, screenshotName)
+ screenshotName := fmt.Sprintf("screenshot-%d.png", time.Now().Unix())
+ path := filepath.Join(screenshotDir, screenshotName)
- if err := os.WriteFile(fullPath, imageData, 0644); err != nil {
+ err = os.WriteFile(path, screenshot, 0644)
+ if err != nil {
return "", fmt.Errorf("screenshot write operation failed: %w", err)
}
diff --git a/backend/pkg/tools/terminal.go b/backend/pkg/tools/terminal.go
index 2b836aa7..767c4e7d 100644
--- a/backend/pkg/tools/terminal.go
+++ b/backend/pkg/tools/terminal.go
@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"path/filepath"
+ "strconv"
"strings"
"time"
@@ -26,10 +27,10 @@ const (
defaultQuickCheckTimeout = 500 * time.Millisecond
// ANSI terminal color codes (aligned with PentAGI UI palette)
- ansiColorInputCmd = "\033[96m" // Bright Cyan - matches UI blue accents
- ansiColorSystemMsg = "\033[92m" // Bright Green - universal success/info
- ansiColorReset = "\033[0m" // Reset to default
- ansiLineTerminator = "\r\n" // CRLF for terminal compatibility
+ ansiColorInputCmd = "\033[96m" // Bright Cyan - matches UI blue accents
+ ansiColorSystemMsg = "\033[92m" // Bright Green - universal success/info
+ ansiColorReset = "\033[0m" // Reset to default
+ ansiLineTerminator = "\r\n" // CRLF for terminal compatibility
)
type execResult struct {
@@ -152,7 +153,7 @@ func (t *terminal) ExecCommand(
}
// verify container runtime status
- isRunning, err := t.dockerClient.VerifyContainerRuntime(ctx, t.containerLID)
+ isRunning, err := t.dockerClient.IsContainerRunning(ctx, t.containerLID)
if err != nil {
return "", fmt.Errorf("runtime verification failed: %w", err)
}
@@ -245,8 +246,15 @@ func (t *terminal) getExecResult(ctx context.Context, id string, timeout time.Du
// Wait for the copy goroutine to finish
<-errChan
- result := fmt.Sprintf("temporary output: %s", dst.String())
- return "", fmt.Errorf("timeout value is too low, use greater value if you need so: %w: %s", ctx.Err(), result)
+ suggestedTimeout := max(int(timeout.Seconds())-10, 10)
+ return "", fmt.Errorf(
+ "command execution timeout (%v). Partial output: %s. "+
+ "HINT: If this is an interactive command (shell/REPL/listener), use detach=true. "+
+ "For long batch commands, wrap with shell timeout utility: 'timeout %d ' to ensure clean completion",
+ ctx.Err(),
+ truncateString(dst.String(), 500),
+ suggestedTimeout,
+ )
}
// wait for the exec process to finish
@@ -264,7 +272,7 @@ func (t *terminal) getExecResult(ctx context.Context, id string, timeout time.Du
}
if results == "" {
- results = "Process terminated with status 0. No console output generated"
+ results = "Command completed successfully with exit code 0. No output produced (silent success)"
}
return results, nil
@@ -273,7 +281,7 @@ func (t *terminal) getExecResult(ctx context.Context, id string, timeout time.Du
func (t *terminal) ReadFile(ctx context.Context, flowID int64, path string) (string, error) {
containerName := PrimaryTerminalName(flowID)
- isRunning, err := t.dockerClient.VerifyContainerRuntime(ctx, t.containerLID)
+ isRunning, err := t.dockerClient.IsContainerRunning(ctx, t.containerLID)
if err != nil {
return "", fmt.Errorf("runtime verification failed: %w", err)
}
@@ -355,7 +363,7 @@ func (t *terminal) ReadFile(ctx context.Context, flowID int64, path string) (str
func (t *terminal) WriteFile(ctx context.Context, flowID int64, content string, path string) (string, error) {
containerName := PrimaryTerminalName(flowID)
- isRunning, err := t.dockerClient.VerifyContainerRuntime(ctx, t.containerLID)
+ isRunning, err := t.dockerClient.IsContainerRunning(ctx, t.containerLID)
if err != nil {
return "", fmt.Errorf("container runtime check failed: %w", err)
}
@@ -364,50 +372,45 @@ func (t *terminal) WriteFile(ctx context.Context, flowID int64, content string,
}
// Docker SDK requires TAR format for file transfer
- tarBuffer := new(bytes.Buffer)
+ tarBuffer := &bytes.Buffer{}
archiveWriter := tar.NewWriter(tarBuffer)
+ defer archiveWriter.Close()
- baseFilename := filepath.Base(path)
+ filename := filepath.Base(path)
fileDescriptor := &tar.Header{
- Name: baseFilename,
- Mode: 0644,
+ Name: filename,
+ Mode: 0600,
Size: int64(len(content)),
}
-
- if err := archiveWriter.WriteHeader(fileDescriptor); err != nil {
- archiveWriter.Close()
+ err = archiveWriter.WriteHeader(fileDescriptor)
+ if err != nil {
return "", fmt.Errorf("tar archive header generation failed: %w", err)
}
- bytesWritten, err := archiveWriter.Write([]byte(content))
+ _, err = archiveWriter.Write([]byte(content))
if err != nil {
- archiveWriter.Close()
return "", fmt.Errorf("tar archive content serialization failed: %w", err)
}
- if bytesWritten != len(content) {
- archiveWriter.Close()
- return "", fmt.Errorf("incomplete tar write: expected %d bytes, wrote %d", len(content), bytesWritten)
+
+ err = archiveWriter.Close()
+ if err != nil {
+ return "", fmt.Errorf("failed to close tar writer: %w", err)
}
- if err := archiveWriter.Close(); err != nil {
- return "", fmt.Errorf("tar archive finalization failed: %w", err)
- }
-
- targetDirectory := filepath.Dir(path)
- copyOptions := container.CopyToContainerOptions{
+ dir := filepath.Dir(path)
+ err = t.dockerClient.CopyToContainer(ctx, containerName, dir, tarBuffer, container.CopyToContainerOptions{
AllowOverwriteDirWithFile: true,
- CopyUIDGID: false,
- }
-
- if err := t.dockerClient.CopyToContainer(ctx, containerName, targetDirectory, tarBuffer, copyOptions); err != nil {
- return "", fmt.Errorf("container file transfer failed for '%s': %w", path, err)
+ })
+ if err != nil {
+ return "", fmt.Errorf("container file transfer failed: %w", err)
}
// Format success message with styling
- successText := fmt.Sprintf("File successfully saved to %s", path)
- styledSuccess := fmt.Sprintf("%s%s%s%s", ansiColorSystemMsg, successText, ansiColorReset, ansiLineTerminator)
- if _, err := t.tlp.PutMsg(ctx, database.TermlogTypeStdin, styledSuccess, t.containerID, t.taskID, t.subtaskID); err != nil {
- return "", fmt.Errorf("terminal log recording failed: %w", err)
+ successMsg := fmt.Sprintf("File successfully saved to %s", path)
+ styledMsg := fmt.Sprintf("%s%s%s%s", ansiColorSystemMsg, successMsg, ansiColorReset, ansiLineTerminator)
+ _, err = t.tlp.PutMsg(ctx, database.TermlogTypeStdin, styledMsg, t.containerID, t.taskID, t.subtaskID)
+ if err != nil {
+ return "", fmt.Errorf("failed to put terminal log (write file cmd): %w", err)
}
return fmt.Sprintf("Successfully wrote %d bytes to %s", len(content), path), nil
@@ -420,3 +423,10 @@ func PrimaryTerminalName(flowID int64) string {
func (t *terminal) IsAvailable() bool {
return t.dockerClient != nil
}
+
+func truncateString(s string, maxLen int) string {
+ if len(s) <= maxLen {
+ return s
+ }
+ return s[:maxLen] + "... [truncated full size is " + strconv.Itoa(len(s)) + " bytes]"
+}
diff --git a/backend/pkg/tools/terminal_test.go b/backend/pkg/tools/terminal_test.go
index 4b060b27..defe0eb8 100644
--- a/backend/pkg/tools/terminal_test.go
+++ b/backend/pkg/tools/terminal_test.go
@@ -40,17 +40,17 @@ type contextAwareMockDockerClient struct {
ctxWasCanceled bool
}
-func (m *contextAwareMockDockerClient) LaunchContainer(_ context.Context, _ string, _ database.ContainerType,
+func (m *contextAwareMockDockerClient) RunContainer(_ context.Context, _ string, _ database.ContainerType,
_ int64, _ *container.Config, _ *container.HostConfig) (database.Container, error) {
return database.Container{}, nil
}
-func (m *contextAwareMockDockerClient) HaltContainer(_ context.Context, _ string, _ int64) error {
+func (m *contextAwareMockDockerClient) StopContainer(_ context.Context, _ string, _ int64) error {
return nil
}
-func (m *contextAwareMockDockerClient) PurgeContainer(_ context.Context, _ string, _ int64) error {
+func (m *contextAwareMockDockerClient) RemoveContainer(_ context.Context, _ string, _ int64) error {
return nil
}
-func (m *contextAwareMockDockerClient) VerifyContainerRuntime(_ context.Context, _ string) (bool, error) {
+func (m *contextAwareMockDockerClient) IsContainerRunning(_ context.Context, _ string) (bool, error) {
return m.isRunning, nil
}
func (m *contextAwareMockDockerClient) ContainerExecCreate(_ context.Context, _ string, _ container.ExecOptions) (container.ExecCreateResponse, error) {
@@ -97,8 +97,8 @@ func (m *contextAwareMockDockerClient) CopyToContainer(_ context.Context, _ stri
func (m *contextAwareMockDockerClient) CopyFromContainer(_ context.Context, _ string, _ string) (io.ReadCloser, container.PathStat, error) {
return io.NopCloser(nil), container.PathStat{}, nil
}
-func (m *contextAwareMockDockerClient) CleanupAllResources(_ context.Context) error { return nil }
-func (m *contextAwareMockDockerClient) GetDefaultImage() string { return "test-image" }
+func (m *contextAwareMockDockerClient) Cleanup(_ context.Context) error { return nil }
+func (m *contextAwareMockDockerClient) GetDefaultImage() string { return "test-image" }
var _ docker.DockerClient = (*contextAwareMockDockerClient)(nil)
diff --git a/backend/pkg/tools/tools.go b/backend/pkg/tools/tools.go
index 1a476363..531e296c 100644
--- a/backend/pkg/tools/tools.go
+++ b/backend/pkg/tools/tools.go
@@ -402,7 +402,7 @@ func (fte *flowToolsExecutor) Prepare(ctx context.Context) error {
fte.primaryLID = cnt.LocalID.String
return nil
default:
- fte.docker.PurgeContainer(ctx, cnt.LocalID.String, cnt.ID)
+ fte.docker.RemoveContainer(ctx, cnt.LocalID.String, cnt.ID)
}
}
@@ -412,7 +412,7 @@ func (fte *flowToolsExecutor) Prepare(ctx context.Context) error {
}
containerName := PrimaryTerminalName(fte.flowID)
- cnt, err := fte.docker.LaunchContainer(
+ cnt, err := fte.docker.RunContainer(
ctx,
containerName,
database.ContainerTypePrimary,
@@ -426,7 +426,7 @@ func (fte *flowToolsExecutor) Prepare(ctx context.Context) error {
},
)
if err != nil {
- return fmt.Errorf("failed to spawn container '%s': %w", containerName, err)
+ return fmt.Errorf("failed to launch container '%s': %w", containerName, err)
}
fte.primaryID = cnt.ID
@@ -441,7 +441,7 @@ func (fte *flowToolsExecutor) Release(ctx context.Context) error {
}
// TODO: here better to get flow containers list and purge all of them
- if err := fte.docker.PurgeContainer(ctx, fte.primaryLID, fte.primaryID); err != nil {
+ if err := fte.docker.RemoveContainer(ctx, fte.primaryLID, fte.primaryID); err != nil {
containerName := PrimaryTerminalName(fte.flowID)
return fmt.Errorf("failed to purge container '%s': %w", containerName, err)
}
diff --git a/build/.gitkeep b/build/.gitkeep
new file mode 100644
index 00000000..e69de29b