From d2a73b75ba9d6f5b03a6f50f7ba1357db095b62a Mon Sep 17 00:00:00 2001 From: Zachary Blasczyk Date: Fri, 1 May 2026 16:19:43 -0500 Subject: [PATCH 1/2] fix: merge runner config into workflow-triggered jobs Workflow runs were dropping serverUrl/apiKey from the JobAgent runner row, so argo-workflow dispatch failed even though the same credentials work for the deployment path (which already merges in jobeligibility). --- .../http/server/openapi/workflows/setters.go | 19 +++++++- .../openapi/workflows/workflows_test.go | 48 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/apps/workspace-engine/svc/http/server/openapi/workflows/setters.go b/apps/workspace-engine/svc/http/server/openapi/workflows/setters.go index 522425fb5..b7912086f 100644 --- a/apps/workspace-engine/svc/http/server/openapi/workflows/setters.go +++ b/apps/workspace-engine/svc/http/server/openapi/workflows/setters.go @@ -95,6 +95,18 @@ func (s *PostgresSetter) CreateWorkflowRun( return tx.Commit(ctx) } +// mergeWorkflowJobAgentConfig builds the JobAgentConfig that ends up on a +// workflow-triggered job. The runner row holds shared credentials (e.g. +// serverUrl, apiKey); the per-job WorkflowJobAgent.Config holds the +// per-invocation payload (e.g. template, name). Per-job values win on +// conflict, mirroring the deployment flow's runner < deployment < version +// precedence in jobeligibility. +func mergeWorkflowJobAgentConfig( + runnerConfig, perJobConfig map[string]any, +) oapi.JobAgentConfig { + return oapi.DeepMergeConfigs(runnerConfig, perJobConfig) +} + func (s *PostgresSetter) dispatchJobForAgent( ctx context.Context, queries *db.Queries, @@ -107,7 +119,12 @@ func (s *PostgresSetter) dispatchJobForAgent( if err != nil { return fmt.Errorf("parse job agent id: %w", err) } - jobAgentConfig, err := json.Marshal(jobAgent.Config) + runner, err := queries.GetJobAgentByID(ctx, jobAgentIDUUID) + if err != nil { + return fmt.Errorf("get job agent runner: %w", err) + } + mergedConfig := mergeWorkflowJobAgentConfig(runner.Config, jobAgent.Config) + jobAgentConfig, err := json.Marshal(mergedConfig) if err != nil { return fmt.Errorf("marshal job agent config: %w", err) } diff --git a/apps/workspace-engine/svc/http/server/openapi/workflows/workflows_test.go b/apps/workspace-engine/svc/http/server/openapi/workflows/workflows_test.go index d1bcf32bb..09454bc54 100644 --- a/apps/workspace-engine/svc/http/server/openapi/workflows/workflows_test.go +++ b/apps/workspace-engine/svc/http/server/openapi/workflows/workflows_test.go @@ -155,6 +155,54 @@ func TestResolveInputs_EmptyWorkflowInputs(t *testing.T) { assert.Equal(t, "value", resolved["extra"]) } +func TestMergeWorkflowJobAgentConfig_RunnerCredentialsPreserved(t *testing.T) { + runner := map[string]any{ + "serverUrl": "https://argo.example", + "apiKey": "secret", + } + perJob := map[string]any{ + "template": "apiVersion: argoproj.io/v1alpha1", + "name": "deploy", + } + + merged := mergeWorkflowJobAgentConfig(runner, perJob) + + assert.Equal(t, "https://argo.example", merged["serverUrl"]) + assert.Equal(t, "secret", merged["apiKey"]) + assert.Equal(t, "apiVersion: argoproj.io/v1alpha1", merged["template"]) + assert.Equal(t, "deploy", merged["name"]) +} + +func TestMergeWorkflowJobAgentConfig_PerJobOverridesRunner(t *testing.T) { + runner := map[string]any{ + "serverUrl": "https://shared.example", + "apiKey": "secret", + } + perJob := map[string]any{ + "serverUrl": "https://override.example", + "template": "spec", + } + + merged := mergeWorkflowJobAgentConfig(runner, perJob) + + assert.Equal(t, "https://override.example", merged["serverUrl"]) + assert.Equal(t, "secret", merged["apiKey"]) + assert.Equal(t, "spec", merged["template"]) +} + +func TestMergeWorkflowJobAgentConfig_NilInputs(t *testing.T) { + merged := mergeWorkflowJobAgentConfig(nil, nil) + assert.Empty(t, merged) + + runner := map[string]any{"serverUrl": "https://argo.example"} + merged = mergeWorkflowJobAgentConfig(runner, nil) + assert.Equal(t, "https://argo.example", merged["serverUrl"]) + + perJob := map[string]any{"template": "spec"} + merged = mergeWorkflowJobAgentConfig(nil, perJob) + assert.Equal(t, "spec", merged["template"]) +} + func TestResolveInputs_ExtraProvidedInputsPassThrough(t *testing.T) { workflow := &oapi.Workflow{ Inputs: []oapi.WorkflowInput{ From 159395c78e0679fcb08aa069f099bd7a40495037 Mon Sep 17 00:00:00 2001 From: Zachary Blasczyk Date: Fri, 1 May 2026 16:58:11 -0500 Subject: [PATCH 2/2] fix: scope workflow job agent lookup to workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reject dispatch when the runner row's workspace doesn't match the workflow's — without it, a workflow could reference a JobAgent UUID from another workspace and pull its credentials into a job. Also tightens the merge helper to take oapi.JobAgentConfig directly. --- .../svc/http/server/openapi/workflows/setters.go | 14 ++++++++++++-- .../server/openapi/workflows/workflows_test.go | 12 ++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/workspace-engine/svc/http/server/openapi/workflows/setters.go b/apps/workspace-engine/svc/http/server/openapi/workflows/setters.go index b7912086f..fdd92e0cb 100644 --- a/apps/workspace-engine/svc/http/server/openapi/workflows/setters.go +++ b/apps/workspace-engine/svc/http/server/openapi/workflows/setters.go @@ -102,7 +102,7 @@ func (s *PostgresSetter) CreateWorkflowRun( // conflict, mirroring the deployment flow's runner < deployment < version // precedence in jobeligibility. func mergeWorkflowJobAgentConfig( - runnerConfig, perJobConfig map[string]any, + runnerConfig, perJobConfig oapi.JobAgentConfig, ) oapi.JobAgentConfig { return oapi.DeepMergeConfigs(runnerConfig, perJobConfig) } @@ -119,9 +119,19 @@ func (s *PostgresSetter) dispatchJobForAgent( if err != nil { return fmt.Errorf("parse job agent id: %w", err) } + workspaceIDUUID, err := uuid.Parse(workspaceID) + if err != nil { + return fmt.Errorf("parse workspace id: %w", err) + } runner, err := queries.GetJobAgentByID(ctx, jobAgentIDUUID) if err != nil { - return fmt.Errorf("get job agent runner: %w", err) + return fmt.Errorf("get job agent: %w", err) + } + if runner.WorkspaceID != workspaceIDUUID { + return fmt.Errorf( + "job agent %s does not belong to workspace %s", + jobAgentIDUUID, workspaceIDUUID, + ) } mergedConfig := mergeWorkflowJobAgentConfig(runner.Config, jobAgent.Config) jobAgentConfig, err := json.Marshal(mergedConfig) diff --git a/apps/workspace-engine/svc/http/server/openapi/workflows/workflows_test.go b/apps/workspace-engine/svc/http/server/openapi/workflows/workflows_test.go index 09454bc54..23b9a8ee0 100644 --- a/apps/workspace-engine/svc/http/server/openapi/workflows/workflows_test.go +++ b/apps/workspace-engine/svc/http/server/openapi/workflows/workflows_test.go @@ -156,11 +156,11 @@ func TestResolveInputs_EmptyWorkflowInputs(t *testing.T) { } func TestMergeWorkflowJobAgentConfig_RunnerCredentialsPreserved(t *testing.T) { - runner := map[string]any{ + runner := oapi.JobAgentConfig{ "serverUrl": "https://argo.example", "apiKey": "secret", } - perJob := map[string]any{ + perJob := oapi.JobAgentConfig{ "template": "apiVersion: argoproj.io/v1alpha1", "name": "deploy", } @@ -174,11 +174,11 @@ func TestMergeWorkflowJobAgentConfig_RunnerCredentialsPreserved(t *testing.T) { } func TestMergeWorkflowJobAgentConfig_PerJobOverridesRunner(t *testing.T) { - runner := map[string]any{ + runner := oapi.JobAgentConfig{ "serverUrl": "https://shared.example", "apiKey": "secret", } - perJob := map[string]any{ + perJob := oapi.JobAgentConfig{ "serverUrl": "https://override.example", "template": "spec", } @@ -194,11 +194,11 @@ func TestMergeWorkflowJobAgentConfig_NilInputs(t *testing.T) { merged := mergeWorkflowJobAgentConfig(nil, nil) assert.Empty(t, merged) - runner := map[string]any{"serverUrl": "https://argo.example"} + runner := oapi.JobAgentConfig{"serverUrl": "https://argo.example"} merged = mergeWorkflowJobAgentConfig(runner, nil) assert.Equal(t, "https://argo.example", merged["serverUrl"]) - perJob := map[string]any{"template": "spec"} + perJob := oapi.JobAgentConfig{"template": "spec"} merged = mergeWorkflowJobAgentConfig(nil, perJob) assert.Equal(t, "spec", merged["template"]) }