From 15250f316f6b82588ad4f955171605764f40a5e5 Mon Sep 17 00:00:00 2001 From: Tejaswi Nadahalli Date: Wed, 25 Feb 2026 15:02:28 +0100 Subject: [PATCH 1/2] Add --confidential and --secret flags to workflow deploy Populate the Attributes field on UpsertWorkflow with a JSON payload when deploying a confidential workflow. --secret is repeatable and accepts KEY or KEY:namespace format. Validation enforces that --secret requires --confidential. --- cmd/workflow/deploy/deploy.go | 18 +++++++++ cmd/workflow/deploy/deploy_test.go | 25 ++++++++++++ cmd/workflow/deploy/register.go | 37 +++++++++++++++++- cmd/workflow/deploy/register_test.go | 58 ++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) diff --git a/cmd/workflow/deploy/deploy.go b/cmd/workflow/deploy/deploy.go index f8299e38..f8598b71 100644 --- a/cmd/workflow/deploy/deploy.go +++ b/cmd/workflow/deploy/deploy.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "strings" "sync" "github.com/ethereum/go-ethereum/common" @@ -42,6 +43,9 @@ type Inputs struct { OwnerLabel string `validate:"omitempty"` SkipConfirmation bool + + Confidential bool + Secrets []string } func (i *Inputs) ResolveConfigURL(fallbackURL string) string { @@ -104,6 +108,8 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddSkipConfirmation(deployCmd) deployCmd.Flags().StringP("output", "o", defaultOutputPath, "The output file for the compiled WASM binary encoded in base64") deployCmd.Flags().StringP("owner-label", "l", "", "Label for the workflow owner (used during auto-link if owner is not already linked)") + deployCmd.Flags().Bool("confidential", false, "Deploy as a confidential workflow (runs in enclave)") + deployCmd.Flags().StringSlice("secret", nil, "VaultDON secret to request (repeatable, format: KEY or KEY:namespace)") return deployCmd } @@ -171,10 +177,16 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress, OwnerLabel: v.GetString("owner-label"), SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), + Confidential: v.GetBool("confidential"), + Secrets: v.GetStringSlice("secret"), }, nil } func (h *handler) ValidateInputs() error { + if len(h.inputs.Secrets) > 0 && !h.inputs.Confidential { + return fmt.Errorf("--secret requires --confidential flag") + } + validate, err := validation.NewValidator() if err != nil { return fmt.Errorf("failed to initialize validator: %w", err) @@ -299,5 +311,11 @@ func (h *handler) displayWorkflowDetails() { ui.Title(fmt.Sprintf("Deploying Workflow: %s", h.inputs.WorkflowName)) ui.Dim(fmt.Sprintf("Target: %s", h.settings.User.TargetName)) ui.Dim(fmt.Sprintf("Owner Address: %s", h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress)) + if h.inputs.Confidential { + ui.Dim("Confidential: yes") + if len(h.inputs.Secrets) > 0 { + ui.Dim(fmt.Sprintf("Secrets: %s", strings.Join(h.inputs.Secrets, ", "))) + } + } ui.Line() } diff --git a/cmd/workflow/deploy/deploy_test.go b/cmd/workflow/deploy/deploy_test.go index 66c9b193..0595dec3 100644 --- a/cmd/workflow/deploy/deploy_test.go +++ b/cmd/workflow/deploy/deploy_test.go @@ -228,6 +228,31 @@ func TestResolveInputs_TagTruncation(t *testing.T) { } } +func TestValidateInputs_SecretRequiresConfidential(t *testing.T) { + t.Parallel() + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) + defer simulatedEnvironment.Close() + + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + h := newHandler(ctx, buf) + + h.inputs = Inputs{ + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + WorkflowPath: "testdata/basic_workflow/main.go", + DonFamily: "zone-a", + WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + WorkflowRegistryContractAddress: simulatedEnvironment.Contracts.WorkflowRegistry.Contract.Hex(), + Secrets: []string{"API_KEY"}, + Confidential: false, + } + + err := h.ValidateInputs() + require.Error(t, err) + assert.Contains(t, err.Error(), "--secret requires --confidential flag") + assert.False(t, h.validated) +} + func stringPtr(s string) *string { return &s } diff --git a/cmd/workflow/deploy/register.go b/cmd/workflow/deploy/register.go index 4042c9db..2f5deaf9 100644 --- a/cmd/workflow/deploy/register.go +++ b/cmd/workflow/deploy/register.go @@ -2,7 +2,9 @@ package deploy import ( "encoding/hex" + "encoding/json" "fmt" + "strings" "time" "github.com/ethereum/go-ethereum/common" @@ -52,7 +54,7 @@ func (h *handler) prepareUpsertParams() (client.RegisterWorkflowV2Parameters, er DonFamily: h.inputs.DonFamily, BinaryURL: binaryURL, ConfigURL: configURL, - Attributes: []byte{}, // optional + Attributes: h.buildAttributes(), KeepAlive: h.inputs.KeepAlive, }, nil } @@ -145,3 +147,36 @@ func (h *handler) handleUpsert(params client.RegisterWorkflowV2Parameters) error } return nil } + +func (h *handler) buildAttributes() []byte { + if !h.inputs.Confidential { + return []byte{} + } + + secrets := make([]secretIdentifier, 0, len(h.inputs.Secrets)) + for _, s := range h.inputs.Secrets { + key, ns, _ := strings.Cut(s, ":") + secrets = append(secrets, secretIdentifier{ + Key: key, + Namespace: ns, + }) + } + + attrs := workflowAttributes{ + Confidential: true, + VaultDonSecrets: secrets, + } + + data, _ := json.Marshal(attrs) + return data +} + +type workflowAttributes struct { + Confidential bool `json:"confidential"` + VaultDonSecrets []secretIdentifier `json:"vault_don_secrets,omitempty"` +} + +type secretIdentifier struct { + Key string `json:"key"` + Namespace string `json:"namespace,omitempty"` +} diff --git a/cmd/workflow/deploy/register_test.go b/cmd/workflow/deploy/register_test.go index b0b8a6cb..070f920e 100644 --- a/cmd/workflow/deploy/register_test.go +++ b/cmd/workflow/deploy/register_test.go @@ -1,6 +1,7 @@ package deploy import ( + "encoding/json" "path/filepath" "testing" @@ -171,3 +172,60 @@ func TestPrepareUpsertParams_StatusPreservation(t *testing.T) { assert.Equal(t, uint8(0), params.Status, "updating active workflow should preserve active status (0)") }) } + +func TestBuildAttributes(t *testing.T) { + t.Parallel() + + t.Run("non-confidential workflow returns empty attributes", func(t *testing.T) { + t.Parallel() + h := &handler{inputs: Inputs{Confidential: false}} + attrs := h.buildAttributes() + assert.Equal(t, []byte{}, attrs) + }) + + t.Run("confidential workflow with no secrets", func(t *testing.T) { + t.Parallel() + h := &handler{inputs: Inputs{Confidential: true}} + attrs := h.buildAttributes() + + var parsed workflowAttributes + require.NoError(t, json.Unmarshal(attrs, &parsed)) + assert.True(t, parsed.Confidential) + assert.Empty(t, parsed.VaultDonSecrets) + }) + + t.Run("confidential workflow with secrets", func(t *testing.T) { + t.Parallel() + h := &handler{inputs: Inputs{ + Confidential: true, + Secrets: []string{"API_KEY", "DB_PASS"}, + }} + attrs := h.buildAttributes() + + var parsed workflowAttributes + require.NoError(t, json.Unmarshal(attrs, &parsed)) + assert.True(t, parsed.Confidential) + require.Len(t, parsed.VaultDonSecrets, 2) + assert.Equal(t, "API_KEY", parsed.VaultDonSecrets[0].Key) + assert.Empty(t, parsed.VaultDonSecrets[0].Namespace) + assert.Equal(t, "DB_PASS", parsed.VaultDonSecrets[1].Key) + assert.Empty(t, parsed.VaultDonSecrets[1].Namespace) + }) + + t.Run("secret with namespace parsed correctly", func(t *testing.T) { + t.Parallel() + h := &handler{inputs: Inputs{ + Confidential: true, + Secrets: []string{"API_KEY:prod", "DB_PASS"}, + }} + attrs := h.buildAttributes() + + var parsed workflowAttributes + require.NoError(t, json.Unmarshal(attrs, &parsed)) + require.Len(t, parsed.VaultDonSecrets, 2) + assert.Equal(t, "API_KEY", parsed.VaultDonSecrets[0].Key) + assert.Equal(t, "prod", parsed.VaultDonSecrets[0].Namespace) + assert.Equal(t, "DB_PASS", parsed.VaultDonSecrets[1].Key) + assert.Empty(t, parsed.VaultDonSecrets[1].Namespace) + }) +} From bdb4e25c4735f995d663875fd56ce477e9e33c72 Mon Sep 17 00:00:00 2001 From: Tejaswi Nadahalli Date: Sat, 28 Feb 2026 22:53:03 +0100 Subject: [PATCH 2/2] Validate secret keys, propagate marshal errors, fix stale comment - Reject empty secret keys in ValidateInputs (--secret "" was silently accepted and would fail at VaultDON runtime). - Change buildAttributes to return an error instead of silently discarding json.Marshal failures. - Fix stale Attributes field comment (described Status, not Attributes). --- cmd/client/workflow_registry_v2_client.go | 2 +- cmd/workflow/deploy/deploy.go | 7 +++++++ cmd/workflow/deploy/register.go | 18 +++++++++++++----- cmd/workflow/deploy/register_test.go | 12 ++++++++---- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/cmd/client/workflow_registry_v2_client.go b/cmd/client/workflow_registry_v2_client.go index a8dd6c5f..dfcc6702 100644 --- a/cmd/client/workflow_registry_v2_client.go +++ b/cmd/client/workflow_registry_v2_client.go @@ -39,7 +39,7 @@ type RegisterWorkflowV2Parameters struct { BinaryURL string // required: URL location for the workflow binary WASM file ConfigURL string // optional: URL location for the workflow configuration file (default empty string) - Attributes []byte // optional: 1 to pause workflow after registration, 0 to activate it (default is 0) + Attributes []byte // optional: JSON-encoded workflow attributes (e.g. confidential flag, vault secrets) KeepAlive bool // optional: whether to keep the other workflows of the same name and owner active after the new deploy (default is false) } diff --git a/cmd/workflow/deploy/deploy.go b/cmd/workflow/deploy/deploy.go index f8598b71..0f9abbf5 100644 --- a/cmd/workflow/deploy/deploy.go +++ b/cmd/workflow/deploy/deploy.go @@ -187,6 +187,13 @@ func (h *handler) ValidateInputs() error { return fmt.Errorf("--secret requires --confidential flag") } + for _, s := range h.inputs.Secrets { + key, _, _ := strings.Cut(s, ":") + if strings.TrimSpace(key) == "" { + return fmt.Errorf("--secret value %q has empty key", s) + } + } + validate, err := validation.NewValidator() if err != nil { return fmt.Errorf("failed to initialize validator: %w", err) diff --git a/cmd/workflow/deploy/register.go b/cmd/workflow/deploy/register.go index 2f5deaf9..930548f1 100644 --- a/cmd/workflow/deploy/register.go +++ b/cmd/workflow/deploy/register.go @@ -45,6 +45,11 @@ func (h *handler) prepareUpsertParams() (client.RegisterWorkflowV2Parameters, er status = *h.existingWorkflowStatus } + attrs, err := h.buildAttributes() + if err != nil { + return client.RegisterWorkflowV2Parameters{}, err + } + ui.Dim(fmt.Sprintf("Preparing transaction for workflowID: %s", workflowID)) return client.RegisterWorkflowV2Parameters{ WorkflowName: workflowName, @@ -54,7 +59,7 @@ func (h *handler) prepareUpsertParams() (client.RegisterWorkflowV2Parameters, er DonFamily: h.inputs.DonFamily, BinaryURL: binaryURL, ConfigURL: configURL, - Attributes: h.buildAttributes(), + Attributes: attrs, KeepAlive: h.inputs.KeepAlive, }, nil } @@ -148,9 +153,9 @@ func (h *handler) handleUpsert(params client.RegisterWorkflowV2Parameters) error return nil } -func (h *handler) buildAttributes() []byte { +func (h *handler) buildAttributes() ([]byte, error) { if !h.inputs.Confidential { - return []byte{} + return []byte{}, nil } secrets := make([]secretIdentifier, 0, len(h.inputs.Secrets)) @@ -167,8 +172,11 @@ func (h *handler) buildAttributes() []byte { VaultDonSecrets: secrets, } - data, _ := json.Marshal(attrs) - return data + data, err := json.Marshal(attrs) + if err != nil { + return nil, fmt.Errorf("marshalling workflow attributes: %w", err) + } + return data, nil } type workflowAttributes struct { diff --git a/cmd/workflow/deploy/register_test.go b/cmd/workflow/deploy/register_test.go index 070f920e..26425c0d 100644 --- a/cmd/workflow/deploy/register_test.go +++ b/cmd/workflow/deploy/register_test.go @@ -179,14 +179,16 @@ func TestBuildAttributes(t *testing.T) { t.Run("non-confidential workflow returns empty attributes", func(t *testing.T) { t.Parallel() h := &handler{inputs: Inputs{Confidential: false}} - attrs := h.buildAttributes() + attrs, err := h.buildAttributes() + require.NoError(t, err) assert.Equal(t, []byte{}, attrs) }) t.Run("confidential workflow with no secrets", func(t *testing.T) { t.Parallel() h := &handler{inputs: Inputs{Confidential: true}} - attrs := h.buildAttributes() + attrs, err := h.buildAttributes() + require.NoError(t, err) var parsed workflowAttributes require.NoError(t, json.Unmarshal(attrs, &parsed)) @@ -200,7 +202,8 @@ func TestBuildAttributes(t *testing.T) { Confidential: true, Secrets: []string{"API_KEY", "DB_PASS"}, }} - attrs := h.buildAttributes() + attrs, err := h.buildAttributes() + require.NoError(t, err) var parsed workflowAttributes require.NoError(t, json.Unmarshal(attrs, &parsed)) @@ -218,7 +221,8 @@ func TestBuildAttributes(t *testing.T) { Confidential: true, Secrets: []string{"API_KEY:prod", "DB_PASS"}, }} - attrs := h.buildAttributes() + attrs, err := h.buildAttributes() + require.NoError(t, err) var parsed workflowAttributes require.NoError(t, json.Unmarshal(attrs, &parsed))