From e02074e9c3882bc42cfb079336ab685cd6eef861 Mon Sep 17 00:00:00 2001 From: vrtornisiello Date: Wed, 4 Feb 2026 08:58:37 -0300 Subject: [PATCH 1/7] ci: sync staging deployment workflow [skip ci] The staging deployment workflow uses the `workflow_run` trigger, which executes the version of `deploy-staging.yaml` from the `main` branch. To ensure the staging deployment runs the latest logic, the workflow must be updated on `main`. --- .github/workflows/deploy-staging.yaml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index 725b186..c13348a 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -69,14 +69,9 @@ jobs: limits: cpu: 500m memory: 2Gi + envConfigMap: chatbot-api-staging-config envSecret: chatbot-api-staging-secrets - sharedEnvSecret: api-staging-secrets - sharedDatabase: - host: cloud-sql-proxy - port: 5432 - name: api_staging - user: api_staging - passwordSecret: api-staging-database-password + envSharedSecret: api-staging-secrets EOF - name: Deploy using Helm From 1114d9fcbfef8d3332456ae3b587a62e1ad95b09 Mon Sep 17 00:00:00 2001 From: vrtornisiello Date: Wed, 4 Feb 2026 11:15:50 -0300 Subject: [PATCH 2/7] ci: sync staging deployment workflow [skip ci] The staging deployment workflow uses the workflow_run trigger, which executes the version of deploy-staging.yaml from the main branch. To ensure the staging deployment runs the latest logic, the workflow must be updated on main. --- .github/workflows/deploy-staging.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index c13348a..0549bcd 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -74,6 +74,11 @@ jobs: envSharedSecret: api-staging-secrets EOF + # NOTE: --force-conflicts is required because Helm v4 uses Server-Side Apply (SSA) + # which tracks field ownership. Without it, fields cause conflicts when their + # ownership differs from previous deploys. + # Caveat: This means Helm will override any external changes made to these fields. + # Ensure Helm is the single source of truth for this deployment. - name: Deploy using Helm run: | helm upgrade \ @@ -82,4 +87,5 @@ jobs: --timeout 10m \ --namespace website \ --values values.yaml \ + --force-conflicts \ chatbot-api-staging charts/basedosdados-chatbot From c357cc28bb1400edaaa0fe2254b658b27c91ce9d Mon Sep 17 00:00:00 2001 From: vrtornisiello Date: Wed, 4 Feb 2026 11:57:55 -0300 Subject: [PATCH 3/7] ci: sync staging deployment workflow [skip ci] The staging deployment workflow uses the workflow_run trigger, which executes the version of deploy-staging.yaml from the main branch. To ensure the staging deployment runs the latest logic, the workflow must be updated on main. --- .github/workflows/deploy-staging.yaml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index 0549bcd..a34040b 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -74,11 +74,10 @@ jobs: envSharedSecret: api-staging-secrets EOF - # NOTE: --force-conflicts is required because Helm v4 uses Server-Side Apply (SSA) - # which tracks field ownership. Without it, fields cause conflicts when their - # ownership differs from previous deploys. - # Caveat: This means Helm will override any external changes made to these fields. - # Ensure Helm is the single source of truth for this deployment. + # NOTE: --server-side=false disables Helm v4's Server-Side Apply (SSA) and uses + # Client-Side Apply (CSA) instead, which mirrors Helm v3 behavior. This is simpler + # for single-manager setups: fields removed from the template are deleted from the + # cluster, and there are no field ownership conflicts. - name: Deploy using Helm run: | helm upgrade \ @@ -87,5 +86,5 @@ jobs: --timeout 10m \ --namespace website \ --values values.yaml \ - --force-conflicts \ + --server-side false \ chatbot-api-staging charts/basedosdados-chatbot From db0f67d3c87ae03f4b3d4f65a30b675ec955cc22 Mon Sep 17 00:00:00 2001 From: vrtornisiello Date: Thu, 12 Mar 2026 17:19:06 -0300 Subject: [PATCH 4/7] fix: use parameterized queries to prevent SQL injection --- app/agent/tools.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/agent/tools.py b/app/agent/tools.py index e329807..9717618 100644 --- a/app/agent/tools.py +++ b/app/agent/tools.py @@ -556,17 +556,23 @@ def decode_table_values( client = get_bigquery_client() - dataset_id = f"{project_name}.{dataset_name}" - dict_table_id = f"{dataset_id}.dicionario" + dict_table_id = f"`{project_name}.{dataset_name}.dicionario`" search_query = f""" SELECT nome_coluna, chave, valor FROM {dict_table_id} - WHERE id_tabela = '{table_name}' + WHERE id_tabela = @table_name """ + query_params = [ + bq.ScalarQueryParameter("table_name", "STRING", table_name), + ] + if column_name is not None: - search_query += f"AND nome_coluna = '{column_name}'" + search_query += "AND nome_coluna = @column_name\n" + query_params.append( + bq.ScalarQueryParameter("column_name", "STRING", column_name), + ) search_query += "ORDER BY nome_coluna, chave" @@ -576,7 +582,7 @@ def decode_table_values( "tool_name": inspect.currentframe().f_code.co_name, } - job_config = bq.QueryJobConfig(labels=labels) + job_config = bq.QueryJobConfig(query_parameters=query_params, labels=labels) query_job = client.query(search_query, job_config=job_config) rows = query_job.result() From 1de1507fef297f86857bdfbc823d4c5abe989b46 Mon Sep 17 00:00:00 2001 From: vrtornisiello Date: Fri, 13 Mar 2026 09:06:19 -0300 Subject: [PATCH 5/7] test: update `decode_table_values` tool tests --- tests/app/agent/test_tools.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/app/agent/test_tools.py b/tests/app/agent/test_tools.py index 0e07b76..fc669a1 100644 --- a/tests/app/agent/test_tools.py +++ b/tests/app/agent/test_tools.py @@ -634,6 +634,15 @@ def test_decode_all_columns(self, mocker: MockerFixture, mock_config: dict): assert len(output.results) == 2 assert output.error_details is None + # Verify parameterized table filter was added to query + call_args = mock_bigquery_client.query.call_args[0][0] + assert "id_tabela = @table_name" in call_args + + # Verify query parameters include table_name + job_config = mock_bigquery_client.query.call_args[1]["job_config"] + param_names = {p.name for p in job_config.query_parameters} + assert "table_name" in param_names + def test_decode_specific_column(self, mocker: MockerFixture, mock_config: dict): """Test decoding a specific column.""" mock_query_job = MagicMock() @@ -660,9 +669,15 @@ def test_decode_specific_column(self, mocker: MockerFixture, mock_config: dict): output = ToolOutput.model_validate(json.loads(result)) assert output.status == "success" - # Verify column filter was added to query + + # Verify parameterized column filter was added to query call_args = mock_bigquery_client.query.call_args[0][0] - assert "nome_coluna = 'col1'" in call_args + assert "nome_coluna = @column_name" in call_args + + # Verify query parameters include column_name + job_config = mock_bigquery_client.query.call_args[1]["job_config"] + param_names = {p.name for p in job_config.query_parameters} + assert "column_name" in param_names def test_dictionary_not_found(self, mocker: MockerFixture, mock_config: dict): """Test error when dictionary table doesn't exist.""" From a837aacc078f3987b6c1a6259ec7da88c8fc249f Mon Sep 17 00:00:00 2001 From: vrtornisiello Date: Fri, 13 Mar 2026 09:41:01 -0300 Subject: [PATCH 6/7] fix: use parameterized queries to prevent SQL injection --- app/agent/tools/bigquery.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/agent/tools/bigquery.py b/app/agent/tools/bigquery.py index 82b9b45..ae04ccb 100644 --- a/app/agent/tools/bigquery.py +++ b/app/agent/tools/bigquery.py @@ -111,11 +111,18 @@ def decode_table_values( search_query = f""" SELECT nome_coluna, chave, valor FROM {dict_table_id} - WHERE id_tabela = '{table_name}' + WHERE id_tabela = @table_name """ + query_params = [ + bq.ScalarQueryParameter("table_name", "STRING", table_name), + ] + if column_name is not None: - search_query += f"AND nome_coluna = '{column_name}'" + search_query += "AND nome_coluna = @column_name\n" + query_params.append( + bq.ScalarQueryParameter("column_name", "STRING", column_name), + ) search_query += "ORDER BY nome_coluna, chave" @@ -127,7 +134,10 @@ def decode_table_values( try: client = _get_client() - job = client.query(search_query, job_config=bq.QueryJobConfig(labels=labels)) + job = client.query( + search_query, + job_config=bq.QueryJobConfig(query_parameters=query_params, labels=labels), + ) results = [dict(row) for row in job.result()] except GoogleAPICallError as e: reason = e.errors[0].get("reason") if getattr(e, "errors", None) else None From 80f4f02d573e9fd83cf51c3f340799a218f4a2d8 Mon Sep 17 00:00:00 2001 From: vrtornisiello Date: Fri, 13 Mar 2026 09:41:34 -0300 Subject: [PATCH 7/7] test: update `decode_table_values` tool tests --- tests/app/agent/tools/test_bigquery.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/app/agent/tools/test_bigquery.py b/tests/app/agent/tools/test_bigquery.py index 1d71563..f64e7b2 100644 --- a/tests/app/agent/tools/test_bigquery.py +++ b/tests/app/agent/tools/test_bigquery.py @@ -157,6 +157,15 @@ def test_decode_all_columns(self, mocker: MockerFixture, mock_config: dict): assert len(output) == 2 + # Verify parameterized table filter was added to query + call_args = mock_bigquery_client.query.call_args[0][0] + assert "id_tabela = @table_name" in call_args + + # Verify query parameters include table_name + job_config = mock_bigquery_client.query.call_args[1]["job_config"] + param_names = {p.name for p in job_config.query_parameters} + assert "table_name" in param_names + def test_decode_specific_column(self, mocker: MockerFixture, mock_config: dict): """Test decoding a specific column.""" mock_query_job = MagicMock() @@ -183,9 +192,15 @@ def test_decode_specific_column(self, mocker: MockerFixture, mock_config: dict): output = json.loads(result) assert len(output) == 2 - # Verify column filter was added to query + + # Verify parameterized column filter was added to query call_args = mock_bigquery_client.query.call_args[0][0] - assert "nome_coluna = 'col1'" in call_args + assert "nome_coluna = @column_name" in call_args + + # Verify query parameters include column_name + job_config = mock_bigquery_client.query.call_args[1]["job_config"] + param_names = {p.name for p in job_config.query_parameters} + assert "column_name" in param_names def test_dictionary_not_found(self, mocker: MockerFixture, mock_config: dict): """Test error when dictionary table doesn't exist."""