actors

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrUnknownKind   = errors.New("no such connection kind")
	ErrUnknownAction = errors.New("no such action")
	ErrNeedApproval  = errors.New("this action leaves the organisation and needs a human approval")
	ErrNeedIdem      = errors.New("a live send needs an idempotency key")
	ErrBadPrincipal  = errors.New("the acting principal is not one that can be audited")
	ErrNotConfigured = errors.New("this connection is not configured")
	ErrBlockedSF001  = errors.New("connections cannot read their credential yet — see SF-001 in docs/security-findings.md")
)

Errors the runner and the API distinguish between.

View Source
var ErrAlreadySent = errors.New("this idempotency key has already been used; not sending again")

ErrAlreadySent is returned when an idempotency key has been used before. Not an error condition so much as the guarantee working.

Functions

func Kinds

func Kinds() []string

Kinds lists every registered actor kind, sorted so the UI is stable.

func Register

func Register(a Actor)

Register adds an actor kind. Called from init() in each provider file.

func RetryAfterOr

func RetryAfterOr(out ActOutput, def time.Duration) time.Duration

RetryAfterOr returns the provider's requested backoff, or a default. The CALLER waits; an actor never sleeps.

Types

type ActInput

type ActInput struct {
	Action string          `json:"action"`
	Params json.RawMessage `json:"params"`
	Actor  Principal       `json:"actor"`

	// Idem is mandatory unless DryRun. UNIQUE per effect.
	Idem       string `json:"idem"`
	RunID      string `json:"runId"`
	ApprovalID string `json:"approvalId"`

	// DryRun MUST NOT touch the network. Fill Preview and return.
	DryRun bool `json:"dryRun"`
}

type ActOutput

type ActOutput struct {
	ProviderRef string          `json:"providerRef"`
	Preview     string          `json:"preview"`
	Result      json.RawMessage `json:"result"`
	Retryable   bool            `json:"retryable"`
	// RetryAfter is honoured by the CALLER. An actor never sleeps inside Act —
	// a provider that says "wait 30s" must not hold a request goroutine for 30s.
	RetryAfter time.Duration `json:"retryAfter"`
}

type ActionSpec

type ActionSpec struct {
	Name        string          `json:"name"`
	Title       Text            `json:"title"`
	Description Text            `json:"description"`
	InputSchema json.RawMessage `json:"inputSchema"`

	// External means it leaves the organisation's boundary. Rule 41: an agent
	// may draft it, only a human may send it. Forces approval — see Run.
	External bool `json:"external"`

	// Idempotent means the same Idem key may be retried safely. Default false,
	// and nothing else is ever auto-retried (Rule 39): "did the first call
	// land?" must be answerable before anything sends twice.
	Idempotent  bool `json:"idempotent"`
	Destructive bool `json:"destructive"`

	// Grant is an exact permission string. togo's Can() is an exact match, so
	// "*" grants nothing — see Rule 16.
	Grant      string `json:"grant"`
	RatePerMin int    `json:"ratePerMin"`
}

ActionSpec describes one thing an actor can do.

One schema, three consumers: the create form renders it, the admin panel turns it into a tool definition, and the runner validates against it. Three copies of the same shape is how they drift.

func SpecFor

func SpecFor(kind, action string) (ActionSpec, bool)

SpecFor finds one action's spec.

func Validate

func Validate(kind string, in ActInput) (ActionSpec, error)

Validate checks an ActInput against the spec BEFORE anything is written or sent. Every failure here is one the caller can fix.

type Actor

type Actor interface {
	// Kind is the registry key: "slack", "discord", "telegram", "email", "webhook".
	Kind() string
	// Actions is the full set this actor supports.
	Actions() []ActionSpec
	// Act performs one. It must honour DryRun by rendering Preview and
	// returning without touching the network.
	Act(ctx context.Context, in ActInput, cfg json.RawMessage, sec Secrets) (ActOutput, error)
}

Actor is a connection that sends.

func Get

func Get(kind string) (Actor, bool)

Get returns the actor for a kind.

type Blocked

type Blocked struct {
	// Kind and Name name the connection, so the error says which one.
	Kind string
	Name string
}

Blocked is a Secrets that refuses, with the reason.

It is the default wiring, so an operator who configures a connection before SF-001 is resolved gets a precise message instead of a puzzling auth failure from Slack.

func (Blocked) Reveal

func (b Blocked) Reveal(_ context.Context, name string) (string, error)

type OutboxRow

type OutboxRow struct {
	ConnectionID string
	Kind         string
	Action       string
	Actor        Principal
	Idem         string
	Preview      string
	Params       json.RawMessage
	ApprovalID   string
}

type Principal

type Principal struct {
	Kind string `json:"kind"` // user | agent | connection | system
	Slug string `json:"slug"`
	ID   string `json:"id"`
}

Principal is WHO asked. A connector never picks its own.

func (Principal) Valid

func (p Principal) Valid() bool

Valid reports whether this principal can be stored and audited.

The slug shape is the grants table's, deliberately: a principal that cannot be written to an audit row is a principal that cannot be held responsible, and SF-001 is what happens when those two drift apart.

type Runner

type Runner struct {
	Store Store
	Log   *slog.Logger
	// Secrets resolves a connection's credential. Blocked{} until SF-001 is
	// decided — see secrets.go.
	Secrets func(kind, name string) Secrets
}

Runner performs actions. One per process.

func (*Runner) Run

func (r *Runner) Run(ctx context.Context, connID, kind string, cfg json.RawMessage, in ActInput) (ActOutput, error)

Run validates, records, and performs one action.

type SQLStore

type SQLStore struct{ DB *sql.DB }

func (SQLStore) Claim

func (s SQLStore) Claim(ctx context.Context, row OutboxRow) (string, bool, error)

func (SQLStore) Finish

func (s SQLStore) Finish(ctx context.Context, id, status string, out ActOutput, sendErr error) error

type Secrets

type Secrets interface {
	Reveal(ctx context.Context, name string) (string, error)
}

Secrets is how an actor reads its credential. One method, so the blocked seam is one function (secrets.go) rather than a vault import in five files.

type Static

type Static map[string]string

Static is a Secrets backed by an in-memory map.

For TESTS and for DryRun paths only. It is exported because the actor tests need to render a payload without a vault, and giving them a fake here is better than each one inventing its own — but it must never be wired into a live send: a credential in a Go map is a credential in a core dump, a heap snapshot, and every backup of the process that held it (Rule 34).

func (Static) Reveal

func (s Static) Reveal(_ context.Context, name string) (string, error)

type Store

type Store interface {
	// Claim inserts the outbox row, or returns the existing one for this
	// idempotency key. `fresh` is false when the key was already present.
	Claim(ctx context.Context, row OutboxRow) (id string, fresh bool, err error)
	// Finish records the terminal state.
	Finish(ctx context.Context, id string, status string, out ActOutput, sendErr error) error
}

Store is the persistence an actor run needs. An interface so the tests can exercise the ordering without a database.

type Text

type Text struct {
	EN string `json:"en"`
	AR string `json:"ar"`
}

Text is a localized label. Mirrors the plan's `Text`.

Jump to

Keyboard shortcuts

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