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 ¶
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.
const ( TargetRunOwner = "run_owner" TargetProfileContact = "profile_contact" )
Notify targets a tier may name.
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.
const MaxTiers = 5
MaxTiers is the most escalation tiers a route may have: the outbox column admits tiers 0 to 4.
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.
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.
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.
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 ¶
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.
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) Done ¶
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.
type Planned ¶
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.