Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions commands/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
validate_edge_key,
)
from templates.compose import (
AGENT_FIREBIRD_SNIPPET,
AGENT_MARIADB_SNIPPET,
AGENT_MONGODB_AUTH_SNIPPET,
AGENT_MONGODB_SNIPPET,
Expand Down Expand Up @@ -77,6 +78,22 @@ def agent(

raw_template = fetch_template("agent.yml")

if "{{EXTRA_SERVICES}}" not in raw_template:
if "\nnetworks:" in raw_template:
raw_template = raw_template.replace(
"\nnetworks:", "\n\n{{EXTRA_SERVICES}}\n\nnetworks:"
)
else:
raw_template += "\n\n{{EXTRA_SERVICES}}\n"

if "{{EXTRA_VOLUMES}}" not in raw_template:
if "\nnetworks:" in raw_template:
raw_template = raw_template.replace(
"\nnetworks:", "\n\n{{EXTRA_VOLUMES}}\n\nnetworks:"
)
else:
raw_template += "\n\n{{EXTRA_VOLUMES}}\n"

env_vars = {
"EDGE_KEY": key,
"PROJECT_NAME": project_name,
Expand Down Expand Up @@ -114,7 +131,7 @@ def agent(
if category == "SQL":
db_type = Prompt.ask(
"Type",
choices=["postgresql", "mysql", "mariadb", "sqlite"],
choices=["postgresql", "mysql", "mariadb", "sqlite", "firebird"],
default="postgresql",
)
else:
Expand Down Expand Up @@ -150,7 +167,11 @@ def agent(
"Port",
default=5432
if db_type == "postgresql"
else (3306 if db_type in ["mysql", "mariadb"] else 27017),
else (
3050
if db_type == "firebird"
else (3306 if db_type in ["mysql", "mariadb"] else 27017)
),
)
user = Prompt.ask("Username")
password = Prompt.ask("Password", password=True)
Expand All @@ -177,7 +198,7 @@ def agent(
if category == "SQL":
db_engine = Prompt.ask(
"Engine",
choices=["postgresql", "mysql", "mariadb", "sqlite"],
choices=["postgresql", "mysql", "mariadb", "sqlite", "firebird"],
default="postgresql",
)
db_variant = "standard"
Expand Down Expand Up @@ -372,6 +393,48 @@ def agent(
f"[success]✔ Added MongoDB container (Port {mongo_port})[/success]"
)

elif db_engine == "firebird":
fb_port = get_free_port()
db_user = "alice"
db_pass = secrets.token_hex(8)
db_name = "mirror.fdb"
service_name = f"db-firebird-{secrets.token_hex(2)}"

var_prefix = service_name.upper().replace("-", "_")
env_vars[f"{var_prefix}_PORT"] = str(fb_port)
env_vars[f"{var_prefix}_DB"] = db_name
env_vars[f"{var_prefix}_USER"] = db_user
env_vars[f"{var_prefix}_PASS"] = db_pass

snippet = (
AGENT_FIREBIRD_SNIPPET.replace("${SERVICE_NAME}", service_name)
.replace("${PORT}", f"${{{var_prefix}_PORT}}")
.replace("${VOL_NAME}", f"{service_name}-data")
.replace("${DB_NAME}", f"${{{var_prefix}_DB}}")
.replace("${USER}", f"${{{var_prefix}_USER}}")
.replace("${PASSWORD}", f"${{{var_prefix}_PASS}}")
)

extra_services += snippet
volumes_list.append(f"{service_name}-data")

add_db_to_json(
path,
{
"name": db_name,
"database": db_name,
"type": "firebird",
"username": db_user,
"password": db_pass,
"port": fb_port,
"host": "localhost",
"generated_id": str(uuid.uuid4()),
},
)
Comment on lines +396 to +433
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check the current code structure around lines 396-436
sed -n '390,440p' commands/agent.py | head -60

Repository: Portabase/cli

Length of output: 2123


🏁 Script executed:

# Find the AGENT_FIREBIRD_SNIPPET definition
rg "AGENT_FIREBIRD_SNIPPET" -B 5 -A 30

Repository: Portabase/cli

Length of output: 8829


🏁 Script executed:

# Search for how the database config (from add_db_to_json) is used
rg "add_db_to_json" -B 2 -A 10

Repository: Portabase/cli

Length of output: 9545


🏁 Script executed:

# Check if there are any other Firebird-related constants or templates
rg -i "firebird" --type py -l

Repository: Portabase/cli

Length of output: 110


🌐 Web query:

Firebird Docker official database location and path requirements

💡 Result:

The official Firebird Docker image (firebirdsql/firebird) uses /var/lib/firebird/data as the default location for database files. This directory is declared as a VOLUME in the image, making it the recommended mount point for persistent data storage. Key details: - Databases created via the FIREBIRD_DATABASE environment variable are stored by default in /var/lib/firebird/data (e.g., /var/lib/firebird/data/mirror.fdb). - Absolute paths outside this directory are allowed for databases. - Usage example: docker run -v ./host/data:/var/lib/firebird/data firebirdsql/firebird - To persist data, always mount a host directory or Docker volume to /var/lib/firebird/data to avoid data loss on container restart/removal. - Firebird configuration files like databases.conf can be mounted separately at /opt/firebird/databases.conf if needed for aliases. This is confirmed across official sources including the GitHub repo, Docker Hub page, and FirebirdSQL.org announcement recognizing these as official images (updated as recently as 3 months ago).

Citations:


🌐 Web query:

Firebird TCP/IP connection string database path requirements

💡 Result:

For Firebird TCP/IP connections, the connection string format is: [hostname/IP]:[port]:database_identifier, where database_identifier is either a database alias (defined in aliases.conf on the server) or the absolute path to the database file on the server machine. Key requirements for the database path: - Must be the absolute path as it exists on the server's filesystem. - Use forward slashes (/) for paths on both Windows and Linux servers; backslashes () are supported on Windows but forward slashes are preferred for cross-platform consistency. Backslashes are invalid on POSIX (Linux) servers. - Examples: - Linux server: serverhost:/opt/firebird/databases/employee.fdb or serverhost:employee (alias) - Windows server: serverhost:c:/databases/mydatabase.fdb or serverhost:MyAlias - Optional port: serverhost/3051:/path/to/db.fdb (default port 3050) - URL-style (Firebird 3+): inet://serverhost:3051/path/to/db.fdb - Local loopback: localhost:/path/to/db.fdb or inet://localhost/path/to/db.fdb The path is interpreted by the server, so use server-native format. Aliases are recommended over full paths for security and portability. This applies to current versions including Firebird 5 (latest stable as of 2024 docs). No Firebird 6 released yet.

Citations:


Use a unique filename and store the full server-side path for Firebird database connectivity.

The current code hardcodes db_name = "mirror.fdb" and stores it as "database": db_name in the database config. This causes two problems:

  1. TCP connections fail: The "database" field stores only the filename, but Firebird TCP/IP connections require the server-side absolute path (e.g., /var/lib/firebird/data/mirror.fdb), not just a filename. Clients connecting to the container over the network need this full path.

  2. Duplicate names: All generated Firebird instances share the identical name mirror.fdb, making them indistinguishable in database listings and removal operations.

The container's volume is already correctly mounted at /var/lib/firebird/data in the Docker template, but the stored connection metadata doesn't reflect this.

🐛 Proposed fix
             elif db_engine == "firebird":
                 fb_port = get_free_port()
                 db_user = "alice"
                 db_pass = secrets.token_hex(8)
-                db_name = "mirror.fdb"
+                db_file = f"fb_{secrets.token_hex(4)}.fdb"
+                db_path = f"/var/lib/firebird/data/{db_file}"
                 service_name = f"db-firebird-{secrets.token_hex(2)}"

                 var_prefix = service_name.upper().replace("-", "_")
                 env_vars[f"{var_prefix}_PORT"] = str(fb_port)
-                env_vars[f"{var_prefix}_DB"] = db_name
+                env_vars[f"{var_prefix}_DB"] = db_file
                 env_vars[f"{var_prefix}_USER"] = db_user
                 env_vars[f"{var_prefix}_PASS"] = db_pass
@@
                     path,
                     {
-                        "name": db_name,
-                        "database": db_name,
+                        "name": db_file,
+                        "database": db_path,
                         "type": "firebird",
                         "username": db_user,
                         "password": db_pass,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commands/agent.py` around lines 396 - 433, The Firebird branch currently
hardcodes db_name="mirror.fdb" causing duplicate names and storing only a
filename; change it to generate a unique filename (e.g., use service_name or a
uuid: "{service_name}.fdb" or similar) and set the stored "database" value to
the full container path where the volume is mounted (e.g.,
"/var/lib/firebird/data/{unique_filename}"). Update the variables used in
AGENT_FIREBIRD_SNIPPET replacements and env_vars so the snippet still gets the
filename/DB env var if needed, and ensure add_db_to_json receives "name"
(display name) and "database" (absolute server-side path) accordingly; keep
volumes_list.append(f"{service_name}-data") unchanged. Ensure references:
db_name, service_name, AGENT_FIREBIRD_SNIPPET, env_vars, volumes_list, and
add_db_to_json are updated consistently.

console.print(
f"[success]✔ Added Firebird container (Port {fb_port})[/success]"
)

if volumes_list:
for vol in volumes_list:
extra_volumes += f" {vol}:\n"
Expand Down
51 changes: 44 additions & 7 deletions commands/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from core.config import add_db_to_json, load_db_config, save_db_config, write_env_file
from core.utils import console, get_free_port, validate_work_dir
from templates.compose import (
AGENT_FIREBIRD_SNIPPET,
AGENT_MARIADB_SNIPPET,
AGENT_MONGODB_AUTH_SNIPPET,
AGENT_MONGODB_SNIPPET,
Expand Down Expand Up @@ -75,7 +76,7 @@ def add_db(name: str = typer.Argument(..., help="Name of the agent")):
if category == "SQL":
db_type = Prompt.ask(
"Type",
choices=["postgresql", "mysql", "mariadb", "sqlite"],
choices=["postgresql", "mysql", "mariadb", "sqlite", "firebird"],
default="postgresql",
)
else:
Expand All @@ -98,7 +99,11 @@ def add_db(name: str = typer.Argument(..., help="Name of the agent")):
"Port",
default=5432
if db_type == "postgresql"
else (3306 if db_type in ["mysql", "mariadb"] else 27017),
else (
3050
if db_type == "firebird"
else (3306 if db_type in ["mysql", "mariadb"] else 27017)
),
)
user = Prompt.ask("Username")
password = Prompt.ask("Password", password=True)
Expand All @@ -119,7 +124,7 @@ def add_db(name: str = typer.Argument(..., help="Name of the agent")):
if category == "SQL":
db_engine = Prompt.ask(
"Engine",
choices=["postgresql", "mysql", "mariadb", "sqlite"],
choices=["postgresql", "mysql", "mariadb", "sqlite", "firebird"],
default="postgresql",
)
else:
Expand Down Expand Up @@ -246,23 +251,55 @@ def add_db(name: str = typer.Argument(..., help="Name of the agent")):
.replace("${DB_NAME}", f"${{{var_prefix}_DB}}")
)

elif db_engine == "firebird":
db_port = get_free_port()
db_user = "alice"
db_pass = secrets.token_hex(8)
db_name = "mirror.fdb"
service_name = f"db-firebird-{secrets.token_hex(2)}"
var_prefix = service_name.upper().replace("-", "_")
env_vars[f"{var_prefix}_PORT"] = str(db_port)
env_vars[f"{var_prefix}_DB"] = db_name
env_vars[f"{var_prefix}_USER"] = db_user
env_vars[f"{var_prefix}_PASS"] = db_pass
snippet = (
AGENT_FIREBIRD_SNIPPET.replace("${SERVICE_NAME}", service_name)
.replace("${PORT}", f"${{{var_prefix}_PORT}}")
.replace("${VOL_NAME}", f"{service_name}-data")
.replace("${DB_NAME}", f"${{{var_prefix}_DB}}")
.replace("${USER}", f"${{{var_prefix}_USER}}")
.replace("${PASSWORD}", f"${{{var_prefix}_PASS}}")
)

compose_path = path / "docker-compose.yml"
if compose_path.exists():
with open(compose_path, "r") as f:
content = f.read()

insert_pos = content.find("networks:")
if insert_pos == -1:
if "\nnetworks:" in content:
insert_pos = content.find("\nnetworks:") + 1
elif content.startswith("networks:"):
insert_pos = 0
else:
insert_pos = len(content)

new_content = content[:insert_pos] + snippet + "\n" + content[insert_pos:]

vol_snippet = f" {service_name}-data:\n"
vol_pos = new_content.find("volumes:")

vol_pos = -1
if "\nvolumes:" in new_content:
vol_pos = new_content.find("\nvolumes:") + 1
elif new_content.startswith("volumes:"):
vol_pos = 0

if vol_pos != -1:
end_of_volumes = new_content.find("networks:", vol_pos)
end_of_volumes = new_content.find("\nnetworks:", vol_pos)
if end_of_volumes == -1:
end_of_volumes = len(new_content)
else:
end_of_volumes += 1

new_content = (
new_content[:end_of_volumes]
+ vol_snippet
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ dependencies = [
"rich>=14.2.0",
"typer>=0.20.0",
"pyinstaller>=6.17.0",
"requests>=2.32.5"
"requests>=2.32.5",
"pyyaml >=6.0.0",
]
20 changes: 20 additions & 0 deletions templates/compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,25 @@
- ${VOL_NAME}:/data/db
"""

AGENT_FIREBIRD_SNIPPET = """
${SERVICE_NAME}:
container_name: ${PROJECT_NAME}-${SERVICE_NAME}
image: firebirdsql/firebird
restart: always
networks:
- portabase
- default
ports:
- "${PORT}:3050"
volumes:
- ${VOL_NAME}:/var/lib/firebird/data
environment:
- FIREBIRD_DATABASE=${DB_NAME}
- FIREBIRD_USER=${USER}
- FIREBIRD_PASSWORD=${PASSWORD}
- FIREBIRD_ROOT_PASSWORD=${PASSWORD}
Comment on lines +73 to +76
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine templates/compose.py
find . -name "compose.py" -type f | head -20

Repository: Portabase/cli

Length of output: 79


🏁 Script executed:

# Search for files containing FIREBIRD configuration
rg -l "FIREBIRD" --type py | head -20

Repository: Portabase/cli

Length of output: 110


🏁 Script executed:

# Read templates/compose.py to verify the code and context
cat -n templates/compose.py | head -100

Repository: Portabase/cli

Length of output: 2535


🏁 Script executed:

# Check for FIREBIRD_ROOT_PASSWORD usage in the codebase
rg "FIREBIRD_ROOT_PASSWORD" --type py -B 2 -A 2

Repository: Portabase/cli

Length of output: 323


🏁 Script executed:

# Check for ROOT_PASSWORD usage more broadly
rg "ROOT_PASSWORD" --type py

Repository: Portabase/cli

Length of output: 248


🏁 Script executed:

# Check for PASSWORD variable handling and rendering
rg "PASSWORD" templates/compose.py -B 2 -A 2

Repository: Portabase/cli

Length of output: 726


🏁 Script executed:

# Check how PASSWORD is generated and used in the renderer/agent code
rg "PASSWORD" commands/ --type py -B 3 -A 3

Repository: Portabase/cli

Length of output: 4679


🏁 Script executed:

# Look for where these variables are rendered/substituted
rg "\$\{PASSWORD\}" --type py -B 2 -A 2

Repository: Portabase/cli

Length of output: 39


🏁 Script executed:

# Check if there's any code that parses or handles the PASSWORD variable
rg "render\|substitute\|PASSWORD" commands/ --type py | head -30

Repository: Portabase/cli

Length of output: 39


🏁 Script executed:

# Check if ROOT_PASSWORD is already supported in the rendering logic
rg "ROOT_PASSWORD\|root_pass\|ROOT_PASS" commands/db.py commands/agent.py --type py -B 2 -A 2

Repository: Portabase/cli

Length of output: 39


🏁 Script executed:

# Check for any environment variable generation or secret handling
rg "secrets\|generate\|token" commands/db.py --type py | head -20

Repository: Portabase/cli

Length of output: 39


🏁 Script executed:

# Look at the full context of the replace logic in commands/db.py for Firebird
sed -n '1,150p' commands/db.py | tail -100

Repository: Portabase/cli

Length of output: 3279


🏁 Script executed:

# Search for the Firebird-specific handling in db.py
rg "firebird" commands/db.py -i -B 5 -A 10

Repository: Portabase/cli

Length of output: 3457


🏁 Script executed:

# Look at commands/db.py to see the full Firebird handling
cat -n commands/db.py | grep -A 30 "firebird"

Repository: Portabase/cli

Length of output: 5051


🏁 Script executed:

# Let me check if there's any mention of separate root password handling elsewhere in the codebase
rg "root_pass\|root-pass\|ROOT_PASS" --type py -i

Repository: Portabase/cli

Length of output: 39


🏁 Script executed:

# Check the agent.py for similar Firebird handling
rg "firebird" commands/agent.py -i -B 5 -A 10

Repository: Portabase/cli

Length of output: 4529


Use a separate Firebird root password.

${PASSWORD} is used for both the app user and FIREBIRD_ROOT_PASSWORD, so any app credential leak also grants SYSDBA admin access. Add a distinct root-password placeholder and have the renderers generate a separate secret.

This requires two changes:

  1. Update the template in templates/compose.py line 76 to use ${ROOT_PASSWORD} instead of ${PASSWORD}
  2. Update the Firebird rendering logic in commands/db.py (lines 254–272) and commands/agent.py to generate a separate root password and add .replace("${ROOT_PASSWORD}", f"${{{var_prefix}_ROOT_PASS}}")
Template change needed
       - FIREBIRD_DATABASE=${DB_NAME}
       - FIREBIRD_USER=${USER}
       - FIREBIRD_PASSWORD=${PASSWORD}
-      - FIREBIRD_ROOT_PASSWORD=${PASSWORD}
+      - FIREBIRD_ROOT_PASSWORD=${ROOT_PASSWORD}
       - FIREBIRD_DATABASE_DEFAULT_CHARSET=UTF8
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@templates/compose.py` around lines 73 - 76, Change the FIREBIRD root password
placeholder in the compose template from ${PASSWORD} to ${ROOT_PASSWORD} (update
templates/compose.py where FIREBIRD_ROOT_PASSWORD is set) and update the
rendering logic that builds environment substitutions in commands/db.py and
commands/agent.py to generate a distinct root secret (e.g., var_prefix +
"_ROOT_PASS") and apply it by adding a replacement for "${ROOT_PASSWORD}" using
.replace("${ROOT_PASSWORD}", f"${{{var_prefix}_ROOT_PASS}}"); ensure the app
user password generation/placeholder for ${PASSWORD} remains unchanged.

- FIREBIRD_DATABASE_DEFAULT_CHARSET=UTF8
"""