Skip to content

[CRE] Add ConfidentialModule for confidential workflow execution#21298

Closed
nadahalli wants to merge 12 commits intodevelopfrom
tejaswi/cw-phase4
Closed

[CRE] Add ConfidentialModule for confidential workflow execution#21298
nadahalli wants to merge 12 commits intodevelopfrom
tejaswi/cw-phase4

Conversation

@nadahalli
Copy link
Copy Markdown
Contributor

@nadahalli nadahalli commented Feb 25, 2026

Confidential CRE Workflows (implementation plan)

Summary

When a workflow is deployed with "confidential": true in its Attributes, the engine uses a ConfidentialModule (a new host.ModuleV2 impl) instead of the WASM host module. The ConfidentialModule delegates both subscribe and trigger execution to the confidential-workflows@1.0.0-alpha capability, which runs as a LOOP plugin.

Changes

  • DB migration 0291: add attributes bytea column to workflow_specs_v2
  • WorkflowSpec / ORM / Handler: plumb Attributes from WorkflowRegisteredEvent through to the database and engine creation
  • ConfidentialModule: host.ModuleV2 impl that serializes the ExecuteRequest, builds a ConfidentialWorkflowRequest, and calls the capability via CapabilitiesRegistry
  • Handler routing: tryEngineCreate() checks IsConfidential(spec.Attributes) and branches to tryConfidentialEngineCreate(), which builds a V2 engine with the ConfidentialModule instead of a WASM module

What this does NOT change

  • Engine internals (engine.go): the engine calls Module.Execute() the same way regardless of module type
  • EngineConfig: no new fields; the Module field already accepts any host.ModuleV2
  • ExecutionHelper / SecretsFetcher: ConfidentialModule ignores both; the enclave has its own

Attributes JSON format

Set by the CLI at deploy time:

{
  "confidential": true,
  "vault_don_secrets": [
    {"key": "API_KEY"},
    {"key": "SIGNING_KEY", "namespace": "custom-ns"}
  ]
}

Design decision: Binary hash

SHA-256 is computed from the binary in WorkflowSpec, not from Attributes. The enclave re-verifies against the fetched binary, so this is defense-in-depth.

Links

Copilot AI review requested due to automatic review settings February 25, 2026 00:10
@nadahalli nadahalli requested review from a team as code owners February 25, 2026 00:10
@github-actions
Copy link
Copy Markdown
Contributor

👋 nadahalli, thanks for creating this pull request!

To help reviewers, please consider creating future PRs as drafts first. This allows you to self-review and make any final changes before notifying the team.

Once you're ready, you can mark it as "Ready for review" to request feedback. Thanks!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 25, 2026

✅ No conflicts with other open PRs targeting develop

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds support for confidential workflow execution by introducing a new ConfidentialModule that delegates WASM execution to a Trusted Execution Environment (TEE) via the confidential-workflows@1.0.0-alpha capability. The implementation extends the existing workflow infrastructure to support workflows marked as confidential in their attributes.

Changes:

  • Added database support for workflow attributes via new attributes bytea column
  • Implemented ConfidentialModule to delegate execution to confidential compute capability
  • Added routing logic to direct confidential workflows to TEE execution path
  • Registered confidential-workflows LOOP plugin for confidential compute integration

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
core/store/migrate/migrations/0291_add_workflow_attributes_column.sql Database migration adding attributes bytea column to workflow_specs_v2 table
core/services/job/models.go Added Attributes []byte field to WorkflowSpec struct
core/services/workflows/artifacts/v2/orm.go Updated ORM to persist and retrieve the new attributes column
core/services/workflows/v2/confidential_module.go New module implementing host.ModuleV2 for confidential workflow execution via capability delegation
core/services/workflows/syncer/v2/handler.go Added routing logic and tryConfidentialEngineCreate to handle confidential workflows
plugins/plugins.private.yaml Registered confidential-workflows LOOP plugin
go.mod Updated chainlink-protos/cre/go dependency and added local replace directive for chainlink-common
go.sum Updated checksums for dependency changes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +48 to +55
// IsConfidential returns true if the Attributes JSON has "confidential": true.
func IsConfidential(data []byte) bool {
attrs, err := ParseWorkflowAttributes(data)
if err != nil {
return false
}
return attrs.Confidential
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The IsConfidential function silently returns false when ParseWorkflowAttributes fails. This could hide configuration errors and cause workflows intended to be confidential to execute non-confidentially. Consider logging the parsing error or exposing it to callers so that malformed attributes are caught early rather than silently ignored.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. IsConfidential now returns (bool, error) so callers fail loudly on malformed attributes instead of silently falling through to non-confidential execution.

@@ -0,0 +1,5 @@
-- +goose Up
ALTER TABLE workflow_specs_v2 ADD COLUMN attributes bytea DEFAULT '';
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The default value for the attributes column is set to an empty string (''). For a bytea column, this creates a zero-length byte array rather than NULL. Consider whether NULL would be more appropriate for workflows without attributes, which would be consistent with how empty/missing data is typically represented in SQL. If an empty byte array is intentional, this is fine, but ensure that ParseWorkflowAttributes handles both nil and empty byte arrays correctly (which it does on line 39).

Suggested change
ALTER TABLE workflow_specs_v2 ADD COLUMN attributes bytea DEFAULT '';
ALTER TABLE workflow_specs_v2 ADD COLUMN attributes bytea;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leaving as-is. ParseWorkflowAttributes handles both nil and empty byte arrays identically via len(data) == 0, so the behavior is correct either way. Using an empty default keeps existing rows consistent without NULL semantics.


protoSecrets := make([]*confworkflowtypes.SecretIdentifier, len(m.vaultDonSecrets))
for i, s := range m.vaultDonSecrets {
ns := s.Namespace
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The code defaults an empty namespace to "main" on line 116. This default appears to be a business logic decision, but it's not documented. Consider adding a comment explaining why "main" is the default namespace and whether this aligns with the VaultDON's expected behavior. This will help future maintainers understand the implicit contract.

Suggested change
ns := s.Namespace
ns := s.Namespace
// Default to the "main" namespace when none is provided. VaultDON and the
// confidential workflows capability treat "main" as the canonical default
// namespace for secrets, so leaving this empty would not match the expected
// behavior of downstream components.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added comment: "VaultDON treats 'main' as the default namespace for secrets."

Comment on lines +776 to +879
// tryConfidentialEngineCreate creates a V2 engine backed by a ConfidentialModule
// instead of a local WASM module. The ConfidentialModule delegates execution to
// the confidential-workflows capability which runs the WASM inside a TEE.
func (h *eventHandler) tryConfidentialEngineCreate(
ctx context.Context,
spec *job.WorkflowSpec,
wid types.WorkflowID,
workflowName types.WorkflowName,
decodedBinary []byte,
source string,
) error {
attrs, err := v2.ParseWorkflowAttributes(spec.Attributes)
if err != nil {
return fmt.Errorf("failed to parse workflow attributes: %w", err)
}

binaryHash := v2.ComputeBinaryHash(decodedBinary)

lggr := logger.Named(h.lggr, "WorkflowEngine.ConfidentialModule")
lggr = logger.With(lggr, "workflowID", spec.WorkflowID, "workflowName", spec.WorkflowName, "workflowOwner", spec.WorkflowOwner)

module := v2.NewConfidentialModule(
h.capRegistry,
spec.BinaryURL,
binaryHash,
spec.WorkflowID,
spec.WorkflowOwner,
workflowName.String(),
spec.WorkflowTag,
attrs.VaultDonSecrets,
lggr,
)

initDone := make(chan error, 1)

cfg := &v2.EngineConfig{
Lggr: h.lggr,
Module: module,
WorkflowConfig: []byte(spec.Config),
CapRegistry: h.capRegistry,
DonSubscriber: h.workflowDonSubscriber,
UseLocalTimeProvider: h.useLocalTimeProvider,
DonTimeStore: h.donTimeStore,
ExecutionsStore: h.workflowStore,
WorkflowID: spec.WorkflowID,
WorkflowOwner: spec.WorkflowOwner,
WorkflowName: workflowName,
WorkflowTag: spec.WorkflowTag,
WorkflowEncryptionKey: h.workflowEncryptionKey,

LocalLimits: v2.EngineLimits{},
LocalLimiters: h.engineLimiters,
GlobalExecutionConcurrencyLimiter: h.workflowLimits,

BeholderEmitter: h.emitter,
BillingClient: h.billingClient,

WorkflowRegistryAddress: h.workflowRegistryAddress,
WorkflowRegistryChainSelector: h.workflowRegistryChainSelector,
OrgResolver: h.orgResolver,
SecretsFetcher: h.secretsFetcher,
}

existingHook := cfg.Hooks.OnInitialized
cfg.Hooks.OnInitialized = func(err error) {
initDone <- err
if existingHook != nil {
existingHook(err)
}
}

engine, err := v2.NewEngine(cfg)
if err != nil {
return fmt.Errorf("failed to create confidential workflow engine: %w", err)
}

if err = engine.Start(ctx); err != nil {
return fmt.Errorf("failed to start confidential workflow engine: %w", err)
}

select {
case <-ctx.Done():
if closeErr := engine.Close(); closeErr != nil {
h.lggr.Errorw("failed to close engine after context cancellation", "error", closeErr, "workflowID", spec.WorkflowID)
}
return fmt.Errorf("context cancelled while waiting for engine initialization: %w", ctx.Err())
case initErr := <-initDone:
if initErr != nil {
if closeErr := engine.Close(); closeErr != nil {
h.lggr.Errorw("failed to close engine after initialization failure", "error", closeErr, "workflowID", spec.WorkflowID)
}
return fmt.Errorf("engine initialization failed: %w", initErr)
}
}

if err := h.engineRegistry.Add(wid, source, engine); err != nil {
if closeErr := engine.Close(); closeErr != nil {
return fmt.Errorf("failed to close workflow engine: %w during invariant violation: %w", closeErr, err)
}
return fmt.Errorf("invariant violation: %w", err)
}

return nil
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The tryConfidentialEngineCreate function lacks test coverage. The codebase shows comprehensive testing for tryEngineCreate and other handler flows. Consider adding tests that verify: (1) confidential engine creation when IsConfidential returns true, (2) proper initialization and lifecycle hooks, (3) error handling during engine creation and startup, and (4) integration with the engine registry.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added three handler-level tests in Test_workflowRegisteredHandler_confidentialRouting: (1) confidential attributes bypass the engine factory and route to the confidential path, (2) non-confidential attributes use the engine factory, (3) malformed attributes return a parse error.

Comment on lines +1 to +182
package v2

import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"

"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"

"github.com/smartcontractkit/chainlink-common/pkg/capabilities"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/types/core"
"github.com/smartcontractkit/chainlink-common/pkg/workflows/wasm/host"

confworkflowtypes "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialworkflow"
sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk"
)

const confidentialWorkflowsCapabilityID = "confidential-workflows@1.0.0-alpha"

// WorkflowAttributes is the JSON structure stored in WorkflowSpec.Attributes.
type WorkflowAttributes struct {
Confidential bool `json:"confidential"`
VaultDonSecrets []SecretIdentifier `json:"vault_don_secrets"`
}

// SecretIdentifier identifies a secret in VaultDON.
type SecretIdentifier struct {
Key string `json:"key"`
Namespace string `json:"namespace,omitempty"`
}

// ParseWorkflowAttributes parses the Attributes JSON from a WorkflowSpec.
// Returns a zero-value struct if data is nil or empty.
func ParseWorkflowAttributes(data []byte) (WorkflowAttributes, error) {
var attrs WorkflowAttributes
if len(data) == 0 {
return attrs, nil
}
if err := json.Unmarshal(data, &attrs); err != nil {
return attrs, fmt.Errorf("failed to parse workflow attributes: %w", err)
}
return attrs, nil
}

// IsConfidential returns true if the Attributes JSON has "confidential": true.
func IsConfidential(data []byte) bool {
attrs, err := ParseWorkflowAttributes(data)
if err != nil {
return false
}
return attrs.Confidential
}

// ConfidentialModule implements host.ModuleV2 for confidential workflows.
// Instead of running WASM locally, it delegates execution to the
// confidential-workflows capability via the CapabilitiesRegistry.
type ConfidentialModule struct {
capRegistry core.CapabilitiesRegistry
binaryURL string
binaryHash []byte
workflowID string
workflowOwner string
workflowName string
workflowTag string
vaultDonSecrets []SecretIdentifier
lggr logger.Logger
}

var _ host.ModuleV2 = (*ConfidentialModule)(nil)

func NewConfidentialModule(
capRegistry core.CapabilitiesRegistry,
binaryURL string,
binaryHash []byte,
workflowID string,
workflowOwner string,
workflowName string,
workflowTag string,
vaultDonSecrets []SecretIdentifier,
lggr logger.Logger,
) *ConfidentialModule {
return &ConfidentialModule{
capRegistry: capRegistry,
binaryURL: binaryURL,
binaryHash: binaryHash,
workflowID: workflowID,
workflowOwner: workflowOwner,
workflowName: workflowName,
workflowTag: workflowTag,
vaultDonSecrets: vaultDonSecrets,
lggr: lggr,
}
}

func (m *ConfidentialModule) Start() {}
func (m *ConfidentialModule) Close() {}
func (m *ConfidentialModule) IsLegacyDAG() bool { return false }

func (m *ConfidentialModule) Execute(
ctx context.Context,
request *sdkpb.ExecuteRequest,
_ host.ExecutionHelper,
) (*sdkpb.ExecutionResult, error) {
execReqBytes, err := proto.Marshal(request)
if err != nil {
return nil, fmt.Errorf("failed to marshal ExecuteRequest: %w", err)
}

protoSecrets := make([]*confworkflowtypes.SecretIdentifier, len(m.vaultDonSecrets))
for i, s := range m.vaultDonSecrets {
ns := s.Namespace
if ns == "" {
ns = "main"
}
protoSecrets[i] = &confworkflowtypes.SecretIdentifier{
Key: s.Key,
Namespace: proto.String(ns),
}
}

capInput := &confworkflowtypes.ConfidentialWorkflowRequest{
VaultDonSecrets: protoSecrets,
Execution: &confworkflowtypes.WorkflowExecution{
WorkflowId: m.workflowID,
BinaryUrl: m.binaryURL,
BinaryHash: m.binaryHash,
ExecuteRequest: execReqBytes,
},
}

payload, err := anypb.New(capInput)
if err != nil {
return nil, fmt.Errorf("failed to marshal capability payload: %w", err)
}

cap, err := m.capRegistry.GetExecutable(ctx, confidentialWorkflowsCapabilityID)
if err != nil {
return nil, fmt.Errorf("failed to get confidential-workflows capability: %w", err)
}

capReq := capabilities.CapabilityRequest{
Payload: payload,
Method: "Execute",
CapabilityId: confidentialWorkflowsCapabilityID,
Metadata: capabilities.RequestMetadata{
WorkflowID: m.workflowID,
WorkflowOwner: m.workflowOwner,
WorkflowName: m.workflowName,
WorkflowTag: m.workflowTag,
},
}

capResp, err := cap.Execute(ctx, capReq)
if err != nil {
return nil, fmt.Errorf("confidential-workflows capability execution failed: %w", err)
}

if capResp.Payload == nil {
return nil, fmt.Errorf("confidential-workflows capability returned nil payload")
}

var confResp confworkflowtypes.ConfidentialWorkflowResponse
if err := capResp.Payload.UnmarshalTo(&confResp); err != nil {
return nil, fmt.Errorf("failed to unmarshal capability response: %w", err)
}

var result sdkpb.ExecutionResult
if err := proto.Unmarshal(confResp.ExecutionResult, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal ExecutionResult: %w", err)
}

return &result, nil
}

// ComputeBinaryHash returns the SHA-256 hash of the given binary.
func ComputeBinaryHash(binary []byte) []byte {
h := sha256.Sum256(binary)
return h[:]
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The new ConfidentialModule and tryConfidentialEngineCreate function lack test coverage. The codebase shows comprehensive testing for other modules and engine creation flows. Consider adding tests that verify: (1) confidential module creation and configuration, (2) routing logic for workflows with confidential attributes, (3) error handling when the capability is not available, and (4) proper passing of vault secrets and binary hash.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added confidential_module_test.go with tests for ParseWorkflowAttributes, IsConfidential, ComputeBinaryHash, and ConfidentialModule.Execute (success, error paths, namespace defaulting, request field forwarding, nil payload).

return fmt.Errorf("invalid workflow name: %w", err)
}

if v2.IsConfidential(spec.Attributes) {
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

Consider adding a log statement when routing to the confidential workflow path (after line 701). This would help with debugging and observability by making it clear when a workflow is being executed confidentially. For example: h.lggr.Infow("routing workflow to confidential execution", "workflowID", spec.WorkflowID)

Suggested change
if v2.IsConfidential(spec.Attributes) {
if v2.IsConfidential(spec.Attributes) {
h.lggr.Infow("routing workflow to confidential execution", "workflowID", spec.WorkflowID)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added: h.lggr.Infow("routing workflow to confidential execution", "workflowID", spec.WorkflowID)

@nadahalli nadahalli marked this pull request as draft February 25, 2026 00:36
@trunk-io
Copy link
Copy Markdown

trunk-io bot commented Feb 25, 2026

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@nadahalli
Copy link
Copy Markdown
Contributor Author

Implementation plan: Confidential CRE Workflows (full PR chain and cross-repo dependencies)

@nadahalli nadahalli changed the title Add ConfidentialModule for confidential workflow execution [CRE] Add ConfidentialModule for confidential workflow execution Mar 2, 2026
@nadahalli nadahalli marked this pull request as ready for review March 3, 2026 13:57
nadahalli added a commit that referenced this pull request Mar 5, 2026
Both #21298 and #21375 independently claimed migration version 291.
Renumber add_workflow_attributes_column to 292 to fix the conflict.
nadahalli added a commit that referenced this pull request Mar 10, 2026
Both #21298 and #21375 independently claimed migration version 291.
Renumber add_workflow_attributes_column to 292 to fix the conflict.
…lows

- DB migration 0291: add attributes bytea column to workflow_specs_v2
- WorkflowSpec: add Attributes field, persist through ORM
- Handler: store payload.Attributes, route confidential workflows to
  dedicated engine creation path (tryConfidentialEngineCreate)
- ConfidentialModule: host.ModuleV2 impl that delegates execution to
  the confidential-workflows@1.0.0-alpha capability via CapabilitiesRegistry
- Plugin registration for confidential-workflows in plugins.private.yaml
- IsConfidential now returns (bool, error) instead of silently swallowing
  malformed attributes JSON
- Add info log when routing workflow to confidential execution
- Add unit tests for ParseWorkflowAttributes, IsConfidential,
  ComputeBinaryHash, ConfidentialModule.Execute (success, error paths,
  namespace defaulting, request field forwarding)
- Add handler-level tests verifying confidential routing: confidential
  attributes bypass engine factory, non-confidential uses it, malformed
  attributes return error
- Add comment documenting "main" as VaultDON default namespace
Replace the local ../chainlink-common replace directive with a proper
module reference to commit 177ddc60abbe on the
tejaswi/confidential-workflows-codegen branch. Also fix Namespace
field assignment (string, not *string) to match the published proto.
The tryConfidentialEngineCreate path was missing the WorkflowID
collision diagnostic that tryEngineCreate has. Also remove the #wip
changeset tag before merge.
Will add back when confidential workflows is ready for release.
- Bump chainlink-common to v0.10.1-0.20260303010151-2879e49d71bd (PR #1851)
- Fix Namespace field: proto optional field requires *string, not string
- Fix NewEventHandler call in handler_test.go: insert nil featureFlags arg to match updated signature
0291_change_chain_selector_and_block_height_to_numeric.sql landed on
develop via #21303, so our workflow_attributes migration must be 0292.
Points at ed10df3 which has replace directives removed, so
loopinstall can build from the module cache on a clean checkout.
0292_soft_drop_evm_heads_numeric_id.sql landed on develop, so bump
our migration from 292 to 293.
@cl-sonarqube-production
Copy link
Copy Markdown

Quality Gate failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)
6 New Major Issues (required ≤ 5)

See analysis details on SonarQube

Catch issues before they fail your Quality Gate with our IDE extension SonarQube IDE SonarQube IDE

Attributes []byte `db:"attributes"`
sdkWorkflow *sdk.WorkflowSpec
rawSpec []byte
config []byte
Copy link
Copy Markdown
Contributor

@vreff vreff Mar 12, 2026

Choose a reason for hiding this comment

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

what goes in this config or Config fields? Could we put the isPrivate attribute there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Config is runtime input passed to the WASM binary. Attributes is deployment metadata from the on-chain registry that controls how the node handles the workflow (confidential routing, secret declarations). Different concerns, so they live in separate fields.

Comment on lines +902 to +983
initDone := make(chan error, 1)

cfg := &v2.EngineConfig{
Lggr: h.lggr,
Module: module,
WorkflowConfig: []byte(spec.Config),
CapRegistry: h.capRegistry,
DonSubscriber: h.workflowDonSubscriber,
UseLocalTimeProvider: h.useLocalTimeProvider,
DonTimeStore: h.donTimeStore,
ExecutionsStore: h.workflowStore,
WorkflowID: spec.WorkflowID,
WorkflowOwner: spec.WorkflowOwner,
WorkflowName: workflowName,
WorkflowTag: spec.WorkflowTag,
WorkflowEncryptionKey: h.workflowEncryptionKey,

LocalLimits: v2.EngineLimits{},
LocalLimiters: h.engineLimiters,
GlobalExecutionConcurrencyLimiter: h.workflowLimits,

BeholderEmitter: h.emitter,
BillingClient: h.billingClient,

WorkflowRegistryAddress: h.workflowRegistryAddress,
WorkflowRegistryChainSelector: h.workflowRegistryChainSelector,
OrgResolver: h.orgResolver,
SecretsFetcher: h.secretsFetcher,
}

existingHook := cfg.Hooks.OnInitialized
cfg.Hooks.OnInitialized = func(err error) {
initDone <- err
if existingHook != nil {
existingHook(err)
}
}

engine, err := v2.NewEngine(cfg)
if err != nil {
return fmt.Errorf("failed to create confidential workflow engine: %w", err)
}

if err = engine.Start(ctx); err != nil {
return fmt.Errorf("failed to start confidential workflow engine: %w", err)
}

select {
case <-ctx.Done():
if closeErr := engine.Close(); closeErr != nil {
h.lggr.Errorw("failed to close engine after context cancellation", "error", closeErr, "workflowID", spec.WorkflowID)
}
return fmt.Errorf("context cancelled while waiting for engine initialization: %w", ctx.Err())
case initErr := <-initDone:
if initErr != nil {
if closeErr := engine.Close(); closeErr != nil {
h.lggr.Errorw("failed to close engine after initialization failure", "error", closeErr, "workflowID", spec.WorkflowID)
}
return fmt.Errorf("engine initialization failed: %w", initErr)
}
}

if err := h.engineRegistry.Add(wid, source, engine); err != nil {
if closeErr := engine.Close(); closeErr != nil {
return fmt.Errorf("failed to close workflow engine: %w during invariant violation: %w", closeErr, err)
}

if errors.Is(err, ErrAlreadyExists) {
existingEntry, found := h.engineRegistry.Get(wid)
if found {
h.lggr.Warnw("WorkflowID collision detected: workflow already exists from different source",
"workflowID", wid.Hex(),
"attemptedSource", source,
"existingSource", existingEntry.Source,
"hint", "Each workflow ID should only be registered from a single source. Check your workflow configurations for duplicates.")
}
}

return fmt.Errorf("invariant violation: %w", err)
}

return nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this function seems to be copying a lot from the original createEngine function. I would consider extracting all the createEngine logic we're duplicating into its own function then reusing as much as possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Extracted the start/wait/register lifecycle into startAndRegisterEngine. Both the normal and confidential paths call it now. Net -34 lines.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in #21444. The shared validation (wid, workflowName, decodedBinary) is extracted into tryEngineCreate; tryConfidentialEngineCreate only handles the module creation and engine config delta.

spec.WorkflowOwner,
workflowName.String(),
spec.WorkflowTag,
attrs.VaultDonSecrets,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is wrong. We will never specify the allowed secrets in some fixed config. Rather, it will be a hook that the workflow provides. We should add a TODO with a ticket around this field to deprecate it in favor of the workflow secrets hook.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is going away. We're switching to runtime secret fetching from the enclave. No upfront secret declaration; vault_don_secrets will be an error.

return nil, fmt.Errorf("failed to marshal capability payload: %w", err)
}

cap, err := m.capRegistry.GetExecutable(ctx, confidentialWorkflowsCapabilityID)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we will need CRE approval for this. AFAIK this is putting the capability in the registry which would technically make it discoverable (and possibly executable?) by workflows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Although I massively favor doing it anyway due to how clean this is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as the answer on CC #253. LOOP is the transport. The chainlink side wires it as a ModuleV2 implementation. It registers in the capability registry only so the LOOP server can start, but it's not discoverable or executable by workflows.

confidential-http:
- moduleURI: "github.com/smartcontractkit/confidential-compute/enclave/apps/confidential-http/capability"
gitRef: "854d537b54e50906801d53b0c3c92777dde1d1fb"
gitRef: "ed10df3862dc8c70d85ee46f123138a87e7c7ed4"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adds the confidential-workflows LOOP plugin entry so loopinstall can build it. The gitRef bumps are from rebasing all capabilities to a single commit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The gitRef bumps are for release-time alignment. Will revert to current develop refs and update when we're closer to shipping.

@nadahalli
Copy link
Copy Markdown
Contributor Author

Superseded by #21603

@nadahalli nadahalli closed this Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants