governance://¶
Producer-only gate that consults a registered governance.Stack (kill switches → rings → policy → audit) and either lets the exchange continue or marks it failed with ErrGovernanceBlocked. Drop one into a route's steps to gate a non-agent tool call, an HTTP API path, or a Kafka write — anywhere you'd consult kill switches / rings / policy.
Use governance:// when you want policy enforcement at any seam in a workflow, not just on agent:// calls.
Producer-only — CreateConsumer returns ErrConsumerNotSupported (governance is a gate, not an event source).
URI shape¶
The <stack-name> must be registered with the component via RegisterStack(name, *governance.Stack) before any route reaches the endpoint; an unregistered name fails the producer call (deny-by-default at the boundary).
Parameters¶
| Param | Type | Default | Producer/Consumer/Both | Description |
|---|---|---|---|---|
(path) stack-name |
string | required | Producer | Stack registration key. |
action |
string | exchange FromURI() |
Producer | Action / tool name to evaluate. |
agent |
string | exchange WorkflowID() |
Producer | Agent ID for the decision. |
tenant |
string | X-Tenant-ID header |
Producer | Tenant ID exposed to Starlang predicates as ctx["tenant"]. |
agentHeader |
string | helix.governance.agent |
Producer | Per-exchange override header for agent. Header wins over URI param. |
actionHeader |
string | helix.governance.action |
Producer | Per-exchange override header for action. Header wins over URI param. |
Headers¶
Read by the producer¶
| Header | Description |
|---|---|
<agentHeader> |
Per-exchange agent override (default header: helix.governance.agent). |
<actionHeader> |
Per-exchange action override (default header: helix.governance.action). |
X-Tenant-ID |
Tenant fallback when the tenant URI param is unset. |
Set via ex.SetProperty(...)¶
| Property | Description |
|---|---|
helix.governance.decision |
allow on pass; one of deny, review, etc. on block. |
helix.governance.blocked_by |
When blocked: which rule/policy fired. |
On block, ex.SetException(...) is called with a wrapped ErrGovernanceBlocked so the standard error-handler path handles the response. Process always returns nil — the deny semantics are exchange-level, not router-level, which keeps the gate cheap.
Examples¶
Gate a database write:
workflows:
- id: customer-update
trigger: { uri: http-server://0.0.0.0:8080/update }
steps:
- target: governance://prod?action=db.write&agent=customer-update
- target: postgres://customers?command=UPDATE&dsn=${DSN}
Gate an outbound HTTP call by tenant:
- set-header:
name: helix.governance.action
starlang: 'header("X-Endpoint")'
- target: governance://prod?tenant={{ headers["X-Tenant"] }}
- target: http://payments-api/charge
Wire up the stack programmatically:
import (
govcomp "github.com/arvenza/helix/pkg/component/governance"
"github.com/arvenza/helix/pkg/governance"
)
stack := governance.NewStack(...).
WithKillSwitch(redisKillSwitch).
WithRing(productionRing).
WithPolicy(billingPolicy)
govComp := govcomp.New()
govComp.RegisterStack("prod", stack)
Behavior notes¶
- Deny by default at the boundary —
governance://unknownfails immediately withErrGovernanceBlocked(no stack registered). This is deliberate: forgetting to register a stack should block traffic, not silently pass it. - Error vs exception —
Processreturnsnileven when blocked; the block is recorded on the exchange viaSetException(...)andhelix.governance.decision = "deny". The route's standard error-handling path picks it up. Returning a hard error here would short-circuit at engine level (heavier). - Hot-swap policy —
RegisterStack(name, ...)overwrites a prior binding. Existing endpoints transparently pick up the new stack on the next call. - No goroutines or external connections — lifecycle is owned by the
governance.Stack.Start/Stopon this component are no-ops.
Source¶
pkg/component/governance/governance.go, pkg/governance/...