scriptexec

package
v1.122.0 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: Apache-2.0 Imports: 31 Imported by: 0

Documentation

Overview

Package scriptexec executes approved managed scripts: the run worker that claims due runs off the queue, the per-run script principal it executes them under, and the writer that turns a script's output into a portal asset.

It is the half of the managed-script feature that runs with nobody present. The authoring half (internal/platform/scriptlayer) is interactive and runs as whoever is typing; everything here runs later, unattended, which is why the execution gate exists and why this package reads it on every run rather than trusting the queue row that got here:

  • code comes from scripts.approved_version_id and nothing else, so a run enqueued against a version that is no longer the approved one fails instead of executing code that lost its approval;
  • authority comes from the grant bound to that version at approval, which carries the roles the version's AUTHOR held, so an unattended run can never exceed what the person who wrote the code could do;
  • every capability call goes through the assembled MCP server over an in-memory session, so persona and connection authorization, rate limiting, and audit apply to a script exactly as they apply to an agent.

The package must not import pkg/platform. The composition root passes in the stores, the assembled server, and the portal dependencies it already holds.

Index

Constants

View Source
const DefaultRunRetention = 365 * 24 * time.Hour

DefaultRunRetention is how long a terminal run row is kept when the deployment names no retention.

It is a year, an order of magnitude beyond the notification queue's thirty days, because these rows are not queue bookkeeping. A scheduled script's run history is its refresh history — the record of what a dashboard was showing and when it last succeeded — which is product surface a person reads, not residue a sweep should be eager to remove.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// DB backs the run, script, and version stores. A nil DB with no stores
	// supplied disables the feature: New returns nil and every method on the
	// nil handle is a no-op.
	DB *sql.DB

	// Runs, Scripts, Versions and Schedules, when non-nil, are used directly
	// instead of building PostgreSQL stores from DB. Production passes DB and
	// leaves them nil; they exist so the execution side can be assembled over
	// in-memory stores, which is the only way to exercise the worker, the
	// engine, and the middleware chain together without a database. The same
	// shape as scriptlayer.Config.Store.
	Runs      script.RunStore
	Scripts   ScriptReader
	Versions  VersionReader
	Schedules script.ScheduleStore

	// DSN is the raw database DSN for the LISTEN connection that wakes the
	// worker the moment a run is enqueued. Empty degrades to poll-only.
	DSN string

	// Server is the assembled MCP server a run drives over an in-memory
	// session. A nil server leaves the worker running but every run fails with
	// "script execution is unavailable", which is the honest report.
	Server *mcp.Server

	// Export carries the portal dependencies platform.export writes through. An
	// incomplete set leaves scripts able to query and unable to persist, which
	// each affected run reports.
	Export ExportDeps

	// Metrics records what the run queue is doing: runs by script, trigger and
	// status, their duration, how many are executing on this replica, and the
	// fires the misfire policy stepped over (#1307). Optional — every method on
	// it is nil-safe — but without it the automations are invisible to an
	// operator watching the platform rather than reading the run table.
	Metrics *observability.Metrics

	// Audit records the script_run lifecycle event. Optional.
	Audit middleware.AuditLogger

	// Notifier queues the alert a failed SCHEDULED run raises. When nil and DB
	// is set, one is built over the notification queue unless
	// NotificationsDisabled says the deployment turned notifications off.
	Notifier Notifier

	// NotificationsDisabled mirrors notifications.enabled: false in YAML.
	NotificationsDisabled bool

	// DigestHourUTC is the hour daily digests are scheduled for, for a
	// recipient whose delivery mode is daily.
	DigestHourUTC int

	// RunRetention overrides DefaultRunRetention.
	RunRetention time.Duration

	// WorkerDisabled leaves this replica serving without ever claiming from the
	// run queue. run_script still enqueues and still waits on the result, which
	// a separate deployment of the same binary with the worker on produces, so
	// script execution scales apart from serving and a pathological script
	// reaches no pod an agent is talking to. The zero value runs the worker,
	// which is the single-binary default.
	WorkerDisabled bool
}

Config carries everything the execution side needs. No platform types.

type ExportDeps

type ExportDeps struct {
	Assets   portal.AssetStore
	Versions portal.VersionStore
	S3       portal.S3Client
	Bucket   string
	Prefix   string
}

ExportDeps is what turning a script's rows into a portal asset needs.

type Handle

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

Handle owns the running execution side.

A handle with no worker is the split deployment's serving half: it still owns the queue, so run_script enqueues and waits exactly as it does on a single-binary replica, and nothing here ever claims. The listener goes with the worker, because waking a replica that will not claim buys nothing but a database connection.

func New

func New(cfg Config) *Handle

New composes the execution side. It returns nil when there is nowhere to keep runs, because a run queue with no storage is not a degraded feature, it is no feature; every method here is nil-safe so the caller wires it unconditionally.

func (*Handle) Notify

func (h *Handle) Notify()

Notify wakes the run worker without waiting for its poll tick. The queue's LISTEN adapter calls it on every pg_notify the store fires, which is how an enqueue on any replica reaches a worker on any other; a producer holding this handle can call it directly and skip the round trip. Nil-safe.

func (*Handle) Runs

func (h *Handle) Runs() script.RunStore

Runs exposes the queue so the tool surface can enqueue and follow runs without building a second store over the same table. Nil-safe.

func (*Handle) Start

func (h *Handle) Start(ctx context.Context) error

Start launches the run worker and, when configured, the LISTEN adapter that wakes it on enqueue. A LISTEN failure degrades to the worker's poll interval rather than failing startup: the wakeup is a latency optimization, and the poll is what makes the queue correct. Nil-safe.

func (*Handle) Stop

func (h *Handle) Stop(ctx context.Context) error

Stop closes the listener and the materializer, then drains the worker inside whatever budget ctx carries: the run in flight finishes if it can and is released if it cannot.

Both producers stop before the consumer. The listener goes first so nothing wakes a worker that is already draining, and the materializer goes with it so the shutdown budget is not spent executing a fire that arrived during it — the fire is not lost, because the schedule is not advanced until its run exists, so the replica that takes over materializes it. Nil-safe.

type Notifier

type Notifier interface {
	Notify(ctx context.Context, recipient, category string, p notification.Payload) (bool, error)
}

Notifier queues one notification. It is the enqueue half of the email substrate, narrowed to the one call this package makes.

type ScriptReader

type ScriptReader interface {
	GetByID(ctx context.Context, id string) (*script.Script, error)
}

ScriptReader is the script lookup the execution side needs, narrowed to the one method so nothing here depends on the whole store contract.

type VersionReader

type VersionReader interface {
	GetVersionByID(ctx context.Context, id string) (*script.Version, error)
}

VersionReader is the version lookup the execution side needs. It reads by id because the execution gate stores an id: only an id names one immutable snapshot for the life of a script.

Jump to

Keyboard shortcuts

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