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 ¶
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
// FollowTables, when set, is called after every version an output writes,
// so the tables registered over the output's file follow it (#1536). It
// returns what happened to each table, which the run records on the
// output and prints into its log. Nil on a deployment that cannot
// register tables.
FollowTables func(ctx context.Context, assetID string, version int) []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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.