notify

package
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 37 Imported by: 0

Documentation

Overview

Package notify delivers approval notifications: a transactional outbox written beside every new approval, and a worker on every replica that sends each row at least once to an operator-named channel. The config is read once at boot from WARDYN_APPROVAL_NOTIFY.

Index

Constants

View Source
const (
	TypeWebhook = "webhook"
	TypeTeams   = "teams"
	TypeSlack   = "slack"
	TypeSMTP    = "smtp"
)

The channel types this build implements. A new type is added here and in Channel.render; any other value is refused at boot.

View Source
const (
	TargetRunOwner       = "run_owner"
	TargetProfileContact = "profile_contact"
)

Notify targets a tier may name.

View Source
const BudgetLockSQL = `SELECT pg_advisory_xact_lock(hashtext('approval_notify:' || $1::text))`

BudgetLockSQL takes the run's budget lock, parameter $1 the run id as text, held to the end of the transaction. Both insertion seams run it first, before the approval's INSERT and before any row lock, when notifications are on: under Read Committed two raises that each read RunBudget-1 rows would both enqueue, so the count and the insert are serialised per run. Lock order: this lock, then rows. The broker's mint takes its grant row FOR UPDATE and never this lock, so no cycle can form.

View Source
const MaxTiers = 5

MaxTiers is the most escalation tiers a route may have: the outbox column admits tiers 0 to 4.

View Source
const ProfileSQL = `SELECT governance_profile_id FROM agent_runs WHERE id = $1`

ProfileSQL reads the run's leaf governance profile inside the approval's own transaction, so a concurrent profile change cannot route this approval by stale data.

View Source
const RunBudget = 25

RunBudget is how many tier-0 outbox rows one run may create per hour. The sandbox agent decides how many distinct approvals its run raises, so without a bound a flood of chat cards could bury the one real approval. Counted in rows, so a deployment with N channels admits 25/N raises. The console still lists every approval.

View Source
const SchemaV1 = "wardyn.approval.v1"

SchemaV1 names the webhook body. The body is an ALLOWLIST: every field below is set from an admin-authored or server-minted value. requested_scope, the reason, the run title and any credential are written or influenced by the sandbox agent and are never sent, because a chat message under the deployment's name carrying agent-chosen text is a phishing channel into the approving team.

Variables

This section is empty.

Functions

func Channels

func Channels() []string

Channels lists the configured channel ids, so a scrape can emit a zero for a channel that has not failed yet.

func Enabled

func Enabled() bool

Enabled reports whether a config is installed.

func SetActive

func SetActive(c *Config, r audit.Recorder)

SetActive installs the boot config and the audit recorder the worker and the budget use. A nil config turns notifications off: Plan returns nothing and no row is ever written.

func Sign

func Sign(secret string, t int64, body []byte) string

Sign returns the X-Wardyn-Signature value: t=<unix seconds>,v1=hex(HMAC-SHA256(secret, t+"."+body)). The timestamp lets a receiver bound replay; one more than five minutes old should be refused.

Types

type Channel

type Channel struct {
	ID          string `json:"id"`
	Type        string `json:"type"`
	URL         string `json:"url"`
	HMACSecret  string `json:"hmac_secret"`
	BearerToken string `json:"bearer_token"`
	// RedactRequester drops the run owner's principal and email from this channel's messages.
	RedactRequester bool `json:"redact_requester"`

	// The smtp type's settings. It has no url: Host and Port name the relay, which must offer
	// STARTTLS. To is a static recipient list; the tier's notify targets are added to it.
	Host     string   `json:"host"`
	Port     int      `json:"port"`
	From     string   `json:"from"`
	To       []string `json:"to"`
	Username string   `json:"username"`
	Password string   `json:"password"`
}

Channel is one named destination. The id, never the URL, is what the outbox stores, so rotating a URL under the same id keeps pending rows deliverable.

type ChannelInfo

type ChannelInfo struct {
	ID, Type, Host string
}

ChannelInfo is what the console may show about a configured channel: its operator-chosen id, its type and the destination HOST. Never the URL: a chat webhook carries its credential in the path or query, and a URL may carry userinfo.

func ChannelInfos

func ChannelInfos() []ChannelInfo

ChannelInfos lists the configured channels in config order, or nil when notifications are off.

type Config

type Config struct {
	ConsoleURL string    `json:"console_url"`
	Channels   []Channel `json:"channels"`
	// Routes choose channels by approval kind and leaf profile. Absent means every approval goes to
	// every channel at tier 0.
	Routes []Route `json:"routes"`
}

Config is the parsed WARDYN_APPROVAL_NOTIFY value.

func Parse

func Parse(raw string) (*Config, error)

Parse validates raw and returns the config. An empty value means notifications are off and returns (nil, nil).

Every error names the channel id and the rule it broke and nothing else: never the URL, a secret, a token, console_url or any other field value, because boot errors reach logs and process supervisors. The decoder's own errors are not wrapped for the same reason: they can quote input.

func (*Config) ExpiryWarnings

func (c *Config) ExpiryWarnings(expiry time.Duration) []string

ExpiryWarnings lists the tiers whose after is at or beyond the approval expiry: the approval will have expired first, so that tier never sends. A non-positive expiry means approvals never expire.

type Counts

type Counts struct {
	Failed, Suppressed map[string]int64
}

Counts is a snapshot of both series.

func Snapshot

func Snapshot() Counts

Snapshot copies the counters.

type Deps

type Deps struct {
	Pool   *pgxpool.Pool
	Config *Config
	Masks  *secretmask.Registry
	Client *http.Client
}

Deps are the worker's collaborators. Client is optional (tests inject one that trusts their server).

type Enqueue

type Enqueue struct {
	// contains filtered or unexported fields
}

Enqueue is the notification half of one approval insert. The zero value is "notifications off".

func NewEnqueue

func NewEnqueue(kind types.ApprovalKind, profileID *uuid.UUID) Enqueue

NewEnqueue plans the rows for a new approval of this kind under this profile.

func (Enqueue) Args

func (e Enqueue) Args() []any

Args are the three array parameters Tail refers to.

func (Enqueue) Done

func (e Enqueue) Done(ctx context.Context, approvalID, runID uuid.UUID, raised, queued int64)

Done is called after the approval's transaction COMMITTED, with the counts the Tail statement returned. A raise that inserted an approval but no outbox row was suppressed by the run budget.

func (Enqueue) On

func (e Enqueue) On() bool

On reports whether the statement should carry the outbox insert.

func (Enqueue) Tail

func (e Enqueue) Tail(firstArg int) string

Tail returns the text to append after a `WITH ins AS (...)` clause, numbering its three parameters from firstArg; Args returns their values. The statement then yields (approvals inserted, outbox rows inserted).

type Planned

type Planned struct {
	Tier    int16
	Channel string
	After   time.Duration
}

Planned is one outbox row a new approval will get.

func Plan

func Plan(kind types.ApprovalKind, profileID *uuid.UUID) []Planned

Plan returns the rows to enqueue for a new approval, a pure function of the boot config. It is the one planner both insertion seams call, so they cannot drift. With no routes every channel is planned at tier 0; otherwise the first route matching kind and the run's leaf profileID plans one row per channel per tier, due After the approval was raised, and no match plans nothing.

type Route

type Route struct {
	Kinds    []types.ApprovalKind `json:"kinds"`
	Profiles []string             `json:"profiles"` // governance profile ids, never names
	Tiers    []Tier               `json:"tiers"`
}

Route is one routing rule. An absent Kinds or Profiles matches anything; the first matching route wins.

type Tier

type Tier struct {
	After    string   `json:"after"`
	Channels []string `json:"channels"`
	Notify   []string `json:"notify"`
	// contains filtered or unexported fields
}

Tier is one escalation step: its channels are sent After the approval was raised, while it is still pending.

type Worker

type Worker struct {
	// contains filtered or unexported fields
}

Worker delivers due outbox rows. Every replica runs one: SKIP LOCKED keeps replicas off each other's rows and the lease is what recovers a row whose replica died mid-send.

func NewWorker

func NewWorker(d Deps) *Worker

NewWorker builds a worker from a validated config. Call it after boot has applied the trusted CA, so the cloned transport carries it.

func (*Worker) Run

func (w *Worker) Run(ctx context.Context)

Run ticks until ctx ends.

func (*Worker) Tick

func (w *Worker) Tick(ctx context.Context) (int, error)

Tick claims and delivers one batch, then purges old terminal rows. It returns how many rows it claimed.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL