scriptexec

package
v1.123.1 Latest Latest
Warning

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

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

Documentation

Overview

Package scriptexec executes 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 run gate is re-read on every run rather than trusted from the queue row that got here:

  • code comes from the version the run was queued against, the latest saved version at the moment of the request, so a run always executes an immutable snapshot a person saved;
  • authority is the roles that version's AUTHOR held when they saved it, 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

	// Destinations is the deployment's configured bucket destinations, which a
	// run resolves platform.export names against at run time.
	Destinations []script.Destination

	// 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)
	GetVersion(ctx context.Context, scriptID string, version int) (*script.Version, error)
}

VersionReader is the version lookup the execution side needs. The worker reads by id because a run is queued against one — only an id names one immutable snapshot for the life of a script — and the scheduler reads the script's current version by number to build the run it materializes.

Jump to

Keyboard shortcuts

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