script

package
v1.121.1 Latest Latest
Warning

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

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

Documentation

Overview

Package script is the managed-script domain: the live script record, its immutable version history, the typed parameter contract, and the single edit funnel every mutation surface crosses.

A managed script is agent-authored Starlark that the platform stores, versions, and governs so a solved process (a KPI report, a daily export) can be re-run without re-deriving it through a model. The package holds the model and the rules; it executes nothing and knows nothing about Starlark. The engine lives in internal/platform/scriptrun and the MCP surface in internal/platform/scriptlayer, so the domain stays free of both.

The governance shape deliberately copies pkg/prompt — live row plus immutable per-mutation snapshots, one ApplyEdit funnel, mixed-edit refusal, gate re-validation under the row lock — rather than genericizing it. The rules are domain-tuned and abstracting across two domains before both exist would fix the wrong shape; see the ApplyEdit and RequiresReview comments for where the two deliberately diverge.

Index

Constants

View Source
const (
	// OutputKindAsset is a portal asset the platform versions and serves.
	OutputKindAsset = "portal_asset"
	// OutputKindObject is an object delivered to a granted bucket, which the
	// platform wrote and does not hold.
	OutputKindObject = "object"
)

Output kinds. An output has two shapes since external delivery (#1288), and the payload names which one it is rather than leaving a caller to infer it from which fields happen to be populated.

View Source
const (
	// DestinationKindPortal versions a portal asset.
	DestinationKindPortal = "portal"

	// DestinationKindS3 writes an object to a bucket over a named platform S3
	// connection. It is the only way an output leaves the platform.
	DestinationKindS3 = "s3"
)

Destination kinds. The kind decides what the platform does with the bytes, and the set is closed for the same reason the capability set is: a review is only meaningful while a reviewer can read what kind of place they are approving.

View Source
const (
	CapabilityQuery  = "platform.query"
	CapabilityExport = "platform.export"
)

Capability names. A capability is one host binding a script may call, and the set is closed: a review is only meaningful while every capability a script can reach can be listed, which stops being true the moment the set grows a wildcard or an open-ended tool surface. The engine that implements these bindings (internal/platform/scriptrun) refers to these names rather than defining its own, so the vocabulary a grant is written in and the vocabulary the interpreter enforces are one list.

View Source
const (
	ParamTypeString = "string"
	ParamTypeInt    = "int"
	ParamTypeFloat  = "float"
	ParamTypeBool   = "bool"
	ParamTypeDate   = "date"
	ParamTypeEnum   = "enum"
)

Parameter type names. The set is deliberately closed and stricter than prompt.Argument's untyped strings: a script's parameters are bound once at run creation and then frozen into run.params, so a value that reaches the interpreter has already been checked. Anything richer than a scalar belongs in the script's own logic, not in its signature.

View Source
const (
	RunStatusPending        = "pending"
	RunStatusRunning        = "running"
	RunStatusSucceeded      = "succeeded"
	RunStatusFailed         = "failed"
	RunStatusSkippedOverlap = "skipped_overlap"
)

Run lifecycle statuses.

  • pending: enqueued, waiting for a worker to claim it.
  • running: a worker holds the lease and the interpreter is executing.
  • succeeded / failed: terminal. A failed run carries the reason in Error and, when the script itself failed, the Starlark backtrace with it.
  • skipped_overlap: terminal, and never executed. A schedule came due while the previous run of the same schedule was still open, and the overlap policy is to skip. It is recorded as a run rather than logged because a report that stopped producing is precisely what a schedule's history has to show; a skip is not an outage, but it is not a run either.
View Source
const (
	// TriggerTool marks a run requested through the run_script tool.
	TriggerTool = "tool"
	// TriggerSchedule marks a run materialized by a script's schedule. The two
	// triggers produce identical rows and execute through the same worker
	// under the same grant; what differs is that nobody is waiting on this
	// one, which is why a failed scheduled run notifies and a failed tool run
	// answers its caller.
	TriggerSchedule = "schedule"
)

Run triggers: what produced the run row.

View Source
const (
	// MaxCronSpecLength bounds a cron expression. A standard five-field spec
	// and every descriptor form fit inside it many times over; the cap keeps a
	// pathological string out of the parser and the review surface.
	MaxCronSpecLength = 200

	// MinFireInterval is the closest together two fires of one schedule may
	// be. Standard cron cannot express anything finer than a minute, so this
	// only ever bites the @every descriptor — where "@every 5s" would turn a
	// governed automation into a load generator against the query engine it is
	// approved to reach.
	MinFireInterval = time.Minute

	// DefaultTimezone is the zone a schedule is interpreted in when it names
	// none. UTC rather than the host's local zone: a schedule means the same
	// thing on every replica, and a deployment that moves regions does not
	// silently move its reports.
	DefaultTimezone = "UTC"
)

Schedule bounds.

View Source
const (
	ScopeGlobal   = "global"
	ScopePersona  = "persona"
	ScopePersonal = "personal"
)

Scope constants define script visibility levels, matching prompt scopes.

View Source
const (
	StatusDraft      = "draft"
	StatusActive     = "active"
	StatusDeprecated = "deprecated"
	StatusSuperseded = "superseded"
)

Status constants define the script lifecycle.

  • draft: authored, not yet approved for execution. Every script starts here and stays here until a version is approved.
  • active: the script has an approved version and the platform will execute it. Set by the approval action.
  • deprecated: still readable and still explains past runs, but should no longer be scheduled or called.
  • superseded: replaced by another script, named in SupersededBy.

There is exactly one approval concept in this domain and it is the VERSION (see ApprovedVersionID). Status reports the consequence; it is not a second, independent gate.

View Source
const (
	VersionStatusDraft      = "draft"
	VersionStatusApplied    = "applied"
	VersionStatusSuperseded = "superseded"
	VersionStatusRejected   = "rejected"
)

Version status constants. A version row is the immutable snapshot of a script's reviewable substance at one mutation:

  • draft: a proposed edit to a script whose approved version is being executed, awaiting approval; the live row keeps carrying the applied snapshot and the approved version keeps executing.
  • applied: the snapshot was applied to the live row.
  • superseded: a draft still pending when a different draft was approved.
  • rejected: a draft a reviewer explicitly rejected.
View Source
const DateLayout = "2006-01-02"

DateLayout is the one accepted wire form for a date parameter and the form every date-module function reads and returns. One layout, everywhere: a script that computes a report date and a schedule that binds one are then talking about the same strings.

View Source
const (
	DefaultSearchLimit = 20
)

Search result limits. DefaultSearchLimit is the top-K returned when the caller names no limit; maxSearchLimit bounds an explicit request so one ranked query cannot ask for an unbounded result set. They match the prompt library's, so a federated search cannot be skewed by one source quietly returning more candidates than another.

View Source
const DestinationPortal = "portal"

DestinationPortal is the name of the portal destination: an asset owned by the platform, versioned by the platform, and reachable only through it. It is where an output lands when a script names no destination, which keeps the common case — a scheduled report refreshing the asset people already read — the shortest thing to write.

View Source
const FireDateToken = "${fire_date}" // #nosec G101 -- a parameter token, not a credential

FireDateToken is the one token a schedule's bound parameters may carry. It expands, at materialization, to the date of the fire in the schedule's own timezone.

The vocabulary is deliberately one entry long. Its whole job is to pin time-dependence into the run record: a script that computed today's date itself would produce a different answer every time it ran, and a run nobody can reproduce is not a governed run. Everything else a date needs — a previous day, a month boundary — is arithmetic the script does on this value through the date module, where it is visible in the source a reviewer read.

View Source
const (

	// MaxSourceBytes bounds the Starlark source of one script. Scripts are glue
	// — a few hundred lines that call the platform and shape the result — and
	// heavy computation belongs in SQL, not in the interpreter. The cap keeps a
	// pathological body out of the parser, the version history, and the review
	// surface a human is expected to actually read.
	MaxSourceBytes = 256 * 1024
)

Bounds on the free-text fields of a script record, matched to the equivalent limits on prompts and assets so tag and name input is uniformly bounded.

View Source
const PrincipalPrefix = "script:"

PrincipalPrefix marks a user id belonging to a managed script rather than a person, following the apikey:<name> service-principal convention.

Variables

View Source
var (
	// ErrNoWork reports that no run was due for a claiming worker. It is the
	// normal idle outcome, not a failure.
	ErrNoWork = errors.New("no script run is due")

	// ErrLeaseLost reports that a worker tried to write to a run it no longer
	// holds, because its lease expired and another worker reclaimed the run.
	// The write is refused rather than applied: a process that lost its lease
	// is, by definition, no longer the one whose result counts.
	ErrLeaseLost = errors.New("the lease on this script run was lost")

	// ErrRunNotFound reports a lookup for a run id that does not exist.
	ErrRunNotFound = errors.New("script run not found")
)

Run queue and lifecycle errors.

View Source
var (
	// ErrScheduleNotFound reports a lookup for a schedule that does not exist.
	ErrScheduleNotFound = errors.New("this script has no schedule")

	// ErrUnknownToken marks a parameter binding that carries a token the
	// vocabulary does not define.
	ErrUnknownToken = errors.New("unknown schedule token")

	// ErrUnknownTimezone marks a zone the runtime could not load. It is a
	// separate sentinel from a bad cron expression because the two have
	// opposite causes: an expression that will not parse is a property of the
	// schedule, while a zone that will not load is a property of the BINARY —
	// the database is compiled in (time/tzdata) and a build that omits it fails
	// every named zone at once. A caller must not treat the second as a reason
	// to change the schedule.
	ErrUnknownTimezone = errors.New("unknown timezone")
)

Schedule errors.

Capabilities is the full host surface, in the order help, validate, and the review surfaces report it.

DestinationKinds is the full set of destination kinds.

View Source
var ErrInvalidGrant = errors.New("invalid grant")

ErrInvalidGrant marks an approval refused because the capability set it carries is not one the platform can bind. It is a sentinel rather than a message shape so a REST surface can answer 400 instead of 500 by asking what the error IS, not what it looks like.

View Source
var ErrNoGrants = errors.New("this script version carries no approved capability grant")

ErrNoGrants marks an execution attempt against a version carrying no grant record, which is a version that was never approved.

View Source
var ErrReviewRequiredMixedEdit = errors.New(
	"source or parameter changes to an approved script require review and cannot be combined with " +
		"scope, status, or other non-versioned changes; submit them as separate updates")

ErrReviewRequiredMixedEdit rejects an edit that combines a review-gated substance change (the source or parameter contract of a script with an approved version) with changes a draft version cannot carry (scope, personas, status, name, owner, enabled). The two must be submitted separately so the deferred draft is exactly the reviewable snapshot — a reviewer approving a code change must not also be silently approving a scope widening.

View Source
var ErrUnknownParam = errors.New("unknown parameter")

ErrUnknownParam marks a bind rejected because the caller supplied a name the script does not declare. It is a distinct error because the corrective action differs from a bad value: the caller is passing something the script will never read, which is nearly always a typo.

View Source
var ErrVersionConflict = errors.New("script version conflict")

ErrVersionConflict marks a version write rejected because the script or version state changed underneath the caller (a draft already resolved, a script retired, an edit racing an approval). REST handlers map it to 409; any other store error is an internal failure.

Functions

func BindParams

func BindParams(defs []Param, values map[string]any) (map[string]any, error)

BindParams checks caller-supplied values against the declared contract and returns the bound set: every declared parameter present exactly once, with defaults applied and each value coerced to its declared type. Undeclared names are refused rather than passed through, so a typo never reaches the script as a silently ignored argument.

The result is what becomes the frozen run.params dict, which is why binding happens here — in the domain, once, before any interpreter is involved — rather than inside the engine.

func BindScheduleParams

func BindScheduleParams(defs []Param, raw map[string]any, fire time.Time, loc *time.Location) (map[string]any, error)

BindScheduleParams expands the schedule's tokens against a fire time and binds the result to the script's parameter contract.

Expansion happens here, at materialization, and the expanded values are what the run row stores. That is what makes a scheduled run reproducible: the run records the date it was computing for, so re-running it later with the same parameters asks the same question, rather than asking about whatever day the re-run happens on.

func ParamSummary

func ParamSummary(params []Param) string

ParamSummary renders a parameter contract as a comma-separated name list, marking the required ones, so a caller learns what a script needs without reading the typed contract field by field. Empty for a script that takes no parameters.

func ParamsEqual

func ParamsEqual(a, b []Param) bool

ParamsEqual reports whether two parameter contracts are identical. Param carries an untyped Default and a value list, so it is not a comparable type and slices.Equal cannot be used on a []Param; this is the one definition of parameter-contract equality, shared by the edit funnel and the diff surface.

func RefuseNewRun

func RefuseNewRun(sc *Script, approved *Version) error

RefuseNewRun reports why a run requested right now would be refused, or nil when one would be admitted. approved is the script's approved version, nil when it has none.

It answers the question a discovery surface has to answer — "if I call run_script on this, will anything happen?" — and it answers it by asking the gate itself against the run such a request would create, rather than by re-deriving the gate's rules. A caller is therefore never told a script is runnable that run_script would then decline.

func RefuseRun

func RefuseRun(sc *Script, v *Version, run *Run) error

RefuseRun reports why the execution gate must not execute this run, or nil when it admits it.

This is the gate itself, and it lives in the domain because it is the rule the whole feature is built around: nothing the platform runs unattended runs except an approved version of an in-service script. Every path into execution answers to this one function, so a second producer of runs cannot arrive with a second, slightly different idea of what is executable.

It is checked when a run is executed, not only when it is queued, because the two happen at different times: between them a script can be disabled, retired, or approved onto a different version, and running the queued row anyway would execute code whose approval has since moved.

func RequiresReview

func RequiresReview(before, after *Script) bool

RequiresReview reports whether an edit must go through review before it can be served: a change to the script's substance (source or parameter contract) when the pre-edit script has an approved version.

This is where the script domain deliberately parts company with prompts. prompt.RequiresReview gates only APPROVED SHARED prompts, because a personal prompt has exactly one consumer — its owner — and versioning it silently costs nobody anything. A script with an approved version is executed by the platform, on a schedule, under a governed identity; letting the owner swap the code out from under that approval would make the approval meaningless at any scope. So the gate here keys on the execution pointer, not on visibility.

A script with no approved version is pure authoring: nothing executes it but its author, through run_draft, under their own authority. Those edits apply directly and snapshot an applied version, which is every edit today.

func SnapshotChanged

func SnapshotChanged(before, after *Script) bool

SnapshotChanged reports whether any versioned snapshot field (source, params, display name, description, tags) differs between the two states.

func ValidateName

func ValidateName(name string) error

ValidateName checks that a script name is well-formed.

func ValidateObjectKey

func ValidateObjectKey(key string) error

ValidateObjectKey checks a relative object key: the granted prefix of a destination, or the key a script writes beneath it.

The rules refuse rather than rewrite. A key is part of the contract between a script and whatever consumes its output, so silently rewriting one would mean the object landing somewhere the source does not say — and a traversal that was cleaned away rather than reported is a refusal nobody was told about.

func ValidateParams

func ValidateParams(params []Param) error

ValidateParams checks a parameter list is a well-formed contract: known types, unique identifier-shaped names, enums that enumerate something, and defaults that satisfy their own declared type.

func ValidateScope

func ValidateScope(scope string) error

ValidateScope checks that a scope value is allowed.

func ValidateSource

func ValidateSource(src string) error

ValidateSource checks that Starlark source is present and within bounds. It is a size and presence check only; parsing is the engine's job.

func ValidateStatus

func ValidateStatus(status string) error

ValidateStatus checks that a status value is recognized.

func ValidateStatusTransition

func ValidateStatusTransition(from, to string) error

ValidateStatusTransition checks whether a status transition is allowed.

func ValidateTags

func ValidateTags(tags []string) error

ValidateTags checks that a script's tag list is within bounds.

Types

type ApprovalStore

type ApprovalStore interface {
	// ApproveVersion stamps the named version as approved by approver, binds
	// grants to it, and points the script's execution gate at it.
	//
	// The grant's Roles are ignored: the implementation copies them from the
	// version's own author, so an approval can never widen authority beyond
	// what the author held. It returns the approved version as stored.
	//
	// Approving a draft applies its snapshot to the live row, so the served
	// script and the executed version are the same code. Any other pending
	// draft is superseded. It returns ErrVersionConflict when the version was
	// already resolved or the script moved underneath the reviewer.
	ApproveVersion(ctx context.Context, scriptID string, version int, approver string, grants Grants) (*Version, error)
}

ApprovalStore is the execution gate's write side: the one operation that makes a script executable by the platform.

It is separate from VersionStore because it is a different kind of act. Every VersionStore method records what an author did; this one records what a reviewer decided, and it is the only path that may write scripts.approved_version_id.

type Author

type Author struct {
	// Email identifies the person recorded on the version.
	Email string
	// Roles is the authority they held when they wrote it.
	Roles []string
}

Author is who produced a version and the authority they held while doing it.

The roles half is what makes the execution gate honest. The middleware resolves a caller's persona from their roles, and an approved script runs with nobody present — no token, no session, no live identity to resolve — so the authority it presents has to have been captured at some earlier moment. Capturing it from the AUTHOR, at the moment they wrote the version, means an approved run can only ever do what the person who wrote that code could already do. An approver cannot widen it, because approval copies these roles rather than accepting them from the request.

type Contract

type Contract struct {
	ID          string   `json:"id" example:"script_a1b2c3d4"`
	Name        string   `json:"name" example:"daily-sales-report"`
	DisplayName string   `json:"display_name,omitempty" example:"Daily Sales Report"`
	Description string   `json:"description,omitempty" example:"Summarize yesterday's sales by region"`
	OwnerEmail  string   `json:"owner_email,omitempty" example:"jane@example.com"`
	Scope       string   `json:"scope" example:"personal"`
	Personas    []string `json:"personas,omitempty" example:"analyst"`
	Tags        []string `json:"tags,omitempty" example:"sales,reporting"`
	Status      string   `json:"status" example:"active"`
	Enabled     bool     `json:"enabled" example:"true"`

	// Params is the typed parameter contract a run binds against: the approved
	// version's when one is approved, the live record's otherwise.
	Params []Param `json:"params"`

	Approval ContractApproval `json:"approval"`

	// Schedule is the cadence this script fires on, nil when it has none.
	Schedule *ContractSchedule `json:"schedule,omitempty"`

	// LastRun is the most recent successful run, nil when the script has never
	// completed one. It answers "what does this produce" with evidence rather
	// than with a promise.
	LastRun *ContractRun `json:"last_successful_run,omitempty"`
}

Contract is what a reference to a script resolves to: everything a caller needs to answer "what is this, may I use it, and what does it produce" without reading a line of Starlark.

It exists as one type because a script is reachable from more than one surface — fetch on an mcp:script:<id> reference, and a prompt that attaches one (#1302, #1289) — and each surface answering the question in its own shape would be two contracts to keep in step. The source is deliberately not part of it: reading the code is what manage_script's get is for, and what a reviewer does.

Two fields deserve their reasoning stated. Params are the APPROVED version's whenever there is one, because run_script executes that version and binds against its contract; the live record's parameters would describe an edit nothing will run. Approval carries both halves of the execution gate: whether a version is approved, and whether a run requested right now would be admitted at all, which a disabled or deprecated script fails even with an approved version behind it.

func BuildContract

func BuildContract(sc *Script, approved *Version, sched *Schedule, lastRun *Run) Contract

BuildContract renders the contract for one script from the records that define it: the live row, the approved version (nil when none is approved), the schedule (nil when it has none), and the last successful run (nil when it has never had one).

It is a pure function over records the caller has already read, so every surface that resolves a script reference produces the identical document and none of them needs its own composition rule.

func (Contract) Text

func (c Contract) Text() string

Text renders the contract as prose: what the script is, what it takes, whether anything will execute it, on what cadence, and what it last produced.

It lives here rather than in a consumer because every surface that resolves a script reference shows the same document — fetch returns it as a document body, and a prompt that references a script serves it inline — and two renderers would be two answers to one question, drifting apart the first time either is edited.

func (Contract) Title

func (c Contract) Title() string

Title is the script's human label: its display name, falling back to the name an agent would call it by.

func (Contract) VisibleToAny

func (c Contract) VisibleToAny(email string, personas []string) bool

VisibleToAny reports whether a caller who belongs to any of personas may see the script this contract describes. It answers through the same rule the record and the store predicate answer through, so a surface holding only the contract — the fetch path, which composes it in one read — enforces the identical visibility without a second read of the script row.

type ContractApproval

type ContractApproval struct {
	// Approved reports whether a version is approved for execution.
	Approved bool `json:"approved" example:"true"`
	// Version is the approved version number, zero when none is approved.
	Version int `json:"version,omitempty" example:"3"`
	// ApprovedBy and ApprovedAt stamp who admitted that version and when.
	ApprovedBy string     `json:"approved_by,omitempty" example:"admin@example.com"`
	ApprovedAt *time.Time `json:"approved_at,omitempty"`

	// Refusal states why a run requested now would be refused, and is empty when
	// one would be admitted. It is the gate's own answer (RefuseNewRun), not a
	// second reading of it, so a caller is never told a script is runnable that
	// run_script would then decline.
	Refusal string `json:"refusal,omitempty" example:"the script has no approved version, so nothing may execute it"`
}

ContractApproval is the execution gate as a caller sees it.

type ContractOutput

type ContractOutput struct {
	Name string `json:"name" example:"sales_by_region"`
	Kind string `json:"kind" example:"portal_asset"`
	// Destination is the granted destination the output went to; "portal" for an
	// output that named none.
	Destination string `json:"destination" example:"portal"`
	Format      string `json:"format,omitempty" example:"csv"`
	RowCount    int    `json:"row_count,omitempty" example:"1420"`
	Bytes       int    `json:"bytes,omitempty" example:"98304"`

	// AssetID and AssetVersion locate an OutputKindAsset output.
	AssetID      string `json:"asset_id,omitempty" example:"asset_a1b2c3d4"`
	AssetVersion int    `json:"asset_version,omitempty" example:"7"`

	// Bucket and Key locate an OutputKindObject output.
	Bucket string `json:"bucket,omitempty" example:"acme-exports"`
	Key    string `json:"key,omitempty" example:"weekly/2026/08/sales.csv"`
}

ContractOutput is one thing a run produced. Kind decides which locator is meaningful: an asset carries the id and version a caller can fetch, an object carries the bucket and key it was written to, which is all the platform knows about it — the bytes left the platform and nothing here will serve them back.

type ContractRun

type ContractRun struct {
	RunID      string           `json:"run_id" example:"run_a1b2c3d4"`
	Version    int              `json:"version" example:"3"`
	FinishedAt *time.Time       `json:"finished_at,omitempty"`
	Outputs    []ContractOutput `json:"outputs,omitempty"`
}

ContractRun summarizes one successful run and what it wrote.

type ContractSchedule

type ContractSchedule struct {
	CronSpec string `json:"cron_spec" example:"0 7 * * 1-5"`
	Timezone string `json:"timezone" example:"America/Los_Angeles"`
	Enabled  bool   `json:"enabled" example:"true"`
	// NextRunAt is the next due fire, nil when the schedule is disabled or its
	// expression has no further fire.
	NextRunAt *time.Time `json:"next_run_at,omitempty"`
}

ContractSchedule is a script's cadence, reported rather than offered: a caller learns the script refreshes itself and when it next will, which is what decides whether to run it again or read what it already produced.

type Cron

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

Cron is a parsed cron expression bound to its timezone.

Parsing is all this domain takes from the cron library: the schedule's timing is computed from the row on demand, and no goroutine anywhere waits on a cron ticker. What fires a run is the run queue's own due predicate, so there is one scheduler in the platform and it is the queue.

func ParseCron

func ParseCron(spec, timezone string) (Cron, error)

ParseCron parses a cron expression in a timezone. An empty timezone is UTC.

func (Cron) Location

func (c Cron) Location() *time.Location

Location returns the zone the expression is read in.

func (Cron) Next

func (c Cron) Next(t time.Time) time.Time

Next returns the first fire strictly after t.

type Destination

type Destination struct {
	// Name is what a script writes as destination="...", unique within a grant.
	Name string `json:"name" example:"acme-drop"`

	// Kind is one of DestinationKinds.
	Kind string `json:"kind" example:"s3"`

	// Connection is the named platform S3 connection the object is written
	// over, empty for the portal. It is also the name the authorization
	// middleware checks independently when the write is issued, so a
	// destination whose connection the script's persona cannot reach is
	// refused a second time, by the authority of record.
	Connection string `json:"connection,omitempty" example:"acme-s3"`

	// Bucket is the bucket objects land in, empty for the portal.
	Bucket string `json:"bucket,omitempty" example:"acme-exports"`

	// Prefix is the key prefix every object written here sits under, empty for
	// the portal and optional for a bucket. It is the boundary of the grant:
	// the script chooses a key beneath it and can never write outside it.
	Prefix string `json:"prefix,omitempty" example:"weekly"`
}

Destination is one place an approved version may write: named by the script, resolved by the platform.

It carries the ADDRESS rather than only a label, because the address is what a reviewer is agreeing to. A grant naming just "acme-drop" would leave the meaning of that name in configuration the reviewer cannot see at approval time and an operator could repoint afterwards without anyone approving anything. Pinning the connection, the bucket, and the prefix onto the version makes an approval say what it did, and makes repointing it a re-approval.

A script supplies no endpoint, no credential, and no bucket. It names a destination and everything below comes from what was approved, which is why there is no arbitrary egress to have: the only network a script reaches is the operator-configured connection set.

func PortalDestination

func PortalDestination() Destination

PortalDestination returns the canonical portal destination.

func (Destination) IsPortal

func (d Destination) IsPortal() bool

IsPortal reports whether the destination is the platform's own asset store.

func (Destination) Label

func (d Destination) Label() string

Label renders a destination for an error message or a log line: the name a script writes, and the address it resolves to.

func (Destination) Normalized

func (d Destination) Normalized() Destination

Normalized returns the destination with its fields trimmed and its prefix in one canonical form, so two approvals that meant the same place read as the same place in a diff rather than as a widening.

func (*Destination) UnmarshalJSON

func (d *Destination) UnmarshalJSON(data []byte) error

UnmarshalJSON reads a destination, accepting the bare name a grant recorded before a destination had an address.

The portal was the only destination that existed then, so the older form is unambiguous rather than merely tolerable: "portal" meant exactly what PortalDestination means now. Accepting it is what lets a replica running this code read a grant a replica running the previous code approved, which is the direction a rolling upgrade actually produces — the older code cannot read this addressed form at all, so a version approved mid-upgrade is unreadable on the replicas that have not moved yet, and stays that way until they do.

func (Destination) Validate

func (d Destination) Validate() error

Validate checks that one destination names a place the platform can write.

type EditOutcome

type EditOutcome struct {
	// Applied is true when the live row was updated.
	Applied bool `json:"applied"`
	// PendingVersion is the draft version number when the edit was deferred for
	// review; zero when Applied.
	PendingVersion int `json:"pending_version,omitempty"`
}

EditOutcome reports how ApplyEdit landed an edit: applied to the live script row, or deferred as a pending draft version awaiting approval.

func ApplyEdit

func ApplyEdit(ctx context.Context, store Store, before, after *Script, author Author) (EditOutcome, error)

ApplyEdit lands a script edit through the one shared gate every mutation surface crosses (the manage_script tool today; admin REST and the portal later). before must be the persisted pre-edit state and after the fully mutated copy; author is the actor recorded on any version produced, together with the authority they held, which becomes the ceiling on what approving that version can grant (see Author).

A review-gated edit (RequiresReview) becomes a pending draft version and leaves the live row untouched, so the approved version keeps executing until the draft is approved. Every other edit is applied via UpdateWithVersion, which snapshots a new applied version when a versioned field changed. The versioning capability is asserted from the store itself; a store without it degrades to a plain unversioned update.

type Grants

type Grants struct {
	// Roles is the authority the run presents to the authorization middleware,
	// copied from the approved version's author. The middleware resolves them
	// to a persona exactly as it does for a human caller, and that persona —
	// not this struct — is the authority of record.
	Roles []string `json:"roles"`

	// Connections is the set of named connections the script may query. A
	// query that names no connection is refused rather than defaulted: the
	// grant cannot verify a connection the call did not name.
	Connections []string `json:"connections"`

	// Capabilities is the set of host bindings the script may call.
	Capabilities []string `json:"capabilities"`

	// Destinations is the set of places the script may write output, each one a
	// resolved address rather than a label. An empty list means the script may
	// compute but not persist.
	Destinations []Destination `json:"destinations"`
}

Grants is the capability set bound to one approved version: the authority the run presents, the connections it may reach, the host bindings it may call, and where its outputs may land.

Grants are NOT persona-shaped. A persona is an org role that drifts as the organization changes; a script's needs are static properties of reviewed code, so they are recorded per version and re-approved when they change. There are no wildcards anywhere in this type on purpose: a reviewer must be able to read the grant and know exactly what was approved.

Roles are not caller input. The approval action copies them from the version's author, so approving cannot hand a script authority its author did not hold; see Version.AuthorRoles.

func (Grants) AllowsCapability

func (g Grants) AllowsCapability(name string) bool

AllowsCapability reports whether the grant permits one host binding.

func (Grants) AllowsConnection

func (g Grants) AllowsConnection(name string) bool

AllowsConnection reports whether the grant permits one named connection. An empty name is never allowed: the platform would resolve it to a default the approval never named.

func (Grants) AllowsDestination

func (g Grants) AllowsDestination(name string) bool

AllowsDestination reports whether the grant permits writing to one named destination.

func (Grants) Destination

func (g Grants) Destination(name string) (Destination, bool)

Destination resolves one granted destination by the name a script writes. The resolved record — not the name — is what the write is issued against, so a script can only ever reach the address its approval pinned.

func (Grants) DestinationNames

func (g Grants) DestinationNames() []string

DestinationNames lists the granted destinations by name, for the messages that tell an author what this script was approved to write to.

func (Grants) IsZero

func (g Grants) IsZero() bool

IsZero reports whether the grant is entirely empty, which distinguishes an unapproved version from one deliberately approved with nothing granted.

func (Grants) MissingFor

func (g Grants) MissingFor(capabilities, connections, destinations []string) Missing

MissingFor reports what a script's source references that the grant does not cover, given the capability, connection, and destination names a static validation found. It is the referenced-versus-granted diff a reviewer reads before approving, and the same diff the approval action refuses on: approving a script whose code reaches for something it was not granted approves a run that fails.

func (Grants) Validate

func (g Grants) Validate() error

Validate checks a grant about to be bound to a version at approval: every capability and destination must be one the platform implements, no entry may be blank, and the authority must be non-empty.

The roles check is not a formality. Roles resolve to a persona, and a caller presenting none resolves to the deny-all default persona, so a version approved without them would be approved into a script that fails on its first tool call. Refusing here reports that at approval, where a human can act on it, rather than at 3am on the first scheduled fire.

type ListFilter

type ListFilter struct {
	Scope      string   // "global", "persona", "personal", or "" for all
	Personas   []string // filter by persona membership (OR match)
	OwnerEmail string   // filter by owner
	Enabled    *bool    // filter by enabled state
	Status     string   // filter by lifecycle status; "" for all
	Search     string   // free-text search on name, display_name, description
	Limit      int      // cap the number of rows returned; 0 means the store default

	// VisibleTo and VisiblePersona apply the scope rules of Script.VisibleTo as
	// a query predicate: global scripts, the persona-scoped scripts of
	// VisiblePersona, and the personal scripts of VisibleTo. They exist as a
	// pair because filtering by owner alone would hide the shared scripts a
	// caller is entitled to see, and filtering by nothing would list the
	// persona-scoped scripts of personas they do not hold. Empty VisibleTo
	// disables the predicate, which is the admin case.
	VisibleTo      string
	VisiblePersona string
}

ListFilter controls which scripts are returned by List.

type Materialization

type Materialization string

Materialization is what one attempt to create a scheduled run produced.

const (
	// MaterializedRun means this caller inserted the run.
	MaterializedRun Materialization = "run"
	// MaterializedSkippedOverlap means the previous run of this schedule was
	// still open, so a skipped_overlap row was recorded instead. The skip is a
	// row rather than a log line because a report that quietly stopped
	// producing is exactly the failure a schedule is supposed to make visible.
	MaterializedSkippedOverlap Materialization = "skipped_overlap"
	// MaterializedDuplicate means another replica materialized this fire
	// first. It is the normal outcome of racing materializers, not a fault.
	MaterializedDuplicate Materialization = "duplicate"
)

Materialization outcomes.

type Missing

type Missing struct {
	Capabilities []string
	Connections  []string
	Destinations []string
}

Missing is what a script's source references that its grant does not cover, on each axis a static validation can read.

func (Missing) Any

func (m Missing) Any() bool

Any reports whether the code reaches for anything it was not granted.

type Param

type Param struct {
	Name        string `json:"name" example:"report_date"`
	Type        string `json:"type" example:"date"`
	Description string `json:"description,omitempty" example:"The business date to report on"`
	Required    bool   `json:"required" example:"true"`
	// Default supplies the value when the caller omits an optional parameter.
	// It is bound through exactly the same coercion and checking as a
	// caller-supplied value, so a default cannot smuggle in a type the
	// parameter does not accept.
	Default any `json:"default,omitempty"`
	// Values enumerates the allowed values of an enum parameter, and is
	// meaningless (and refused) on every other type.
	Values []string `json:"values,omitempty" example:"daily,weekly"`
}

Param is one typed parameter of a script: the contract a caller, a schedule, or a draft run binds values against.

type PendingReview

type PendingReview struct {
	ScriptID    string `json:"script_id" example:"script_a1b2c3d4"`
	ScriptName  string `json:"script_name" example:"daily-sales-report"`
	DisplayName string `json:"display_name" example:"Daily Sales Report"`
	Description string `json:"description" example:"Summarize yesterday's sales by region"`
	OwnerEmail  string `json:"owner_email" example:"jane@example.com"`
	Scope       string `json:"scope" example:"global"`
	// Version is the number a reviewer would approve, and VersionStatus is that
	// version's row status — "draft" for a proposed change, "applied" for the
	// snapshot a never-approved script is already serving.
	Version       int    `json:"version" example:"3"`
	VersionID     string `json:"version_id" example:"sver_a1b2c3d4"`
	VersionStatus string `json:"version_status" example:"draft"`
	// Author is who wrote the version and AuthorRoles is the authority
	// approving it would bind, which approval copies rather than accepts.
	Author      string   `json:"author" example:"jane@example.com"`
	AuthorRoles []string `json:"author_roles" example:"analyst"`
	// FirstApproval marks a script that has never had an approved version, so
	// nothing of it executes today. The distinction matters to a reviewer:
	// approving a change alters what already runs unattended, while approving a
	// first version starts something running.
	FirstApproval bool `json:"first_approval" example:"true"`
	// CreatedAt is when the version was authored, which is how long the
	// decision has been outstanding.
	CreatedAt time.Time `json:"created_at" example:"2026-08-14T09:00:00Z"`
}

PendingReview is one version waiting for a reviewer's decision, with the script it belongs to, flattened into the shape a queue lists.

It deliberately carries no source. A queue is read to decide what to open next, and a script's source is the largest field in the record; the review surface fetches the version itself once a reviewer picks a row.

func (PendingReview) AgeDays

func (p PendingReview) AgeDays(now time.Time) int

AgeDays returns how many whole days the review has been waiting as of now.

type RejectionStore

type RejectionStore interface {
	// RejectVersion marks a pending draft rejected, leaving the live script and
	// its approved version untouched.
	//
	// Only a draft can be rejected. The live version of a never-approved script
	// is also awaiting review, but rejecting it would mark the code the script
	// is serving as rejected while it kept being served; declining that version
	// means leaving it unapproved, which is already what it is. Returns
	// ErrVersionConflict when the version is not a pending draft.
	RejectVersion(ctx context.Context, scriptID string, version int) error
}

RejectionStore is the review decision that is not an approval.

It sits beside ApprovalStore rather than in it because the two decisions have different reach: approving resolves the whole queue for a script (it applies a snapshot, supersedes competing drafts, and moves the execution gate), while rejecting takes one proposal out of consideration and changes nothing about what runs.

type ReviewStore

type ReviewStore interface {
	// ListPendingReviews returns every version awaiting approval across all
	// scripts, oldest first, which is the order a queue is worked.
	//
	// Two states qualify, and both mean "the platform is not executing this
	// version": a pending draft, and the live version of a script that has no
	// approved version at all. A script the execution gate would refuse anyway
	// — disabled, deprecated, or superseded — is excluded, because approving it
	// would change nothing and nothing could clear it from the queue.
	ListPendingReviews(ctx context.Context) ([]PendingReview, error)
}

ReviewStore is the read side of the review queue: what is waiting for a human. It is separate from VersionStore because it answers a question about every script at once rather than about one script's history, and separate from ApprovalStore because reading the queue decides nothing.

type Run

type Run struct {
	ID string `json:"id" example:"run_a1b2c3d4"`

	ScriptID  string `json:"script_id"`
	VersionID string `json:"version_id"`
	// Version is the version NUMBER executed, carried alongside the id so a
	// run reads as "daily-sales v3" without a second lookup.
	Version int `json:"version" example:"3"`

	Trigger string `json:"trigger" example:"tool"`
	Status  string `json:"status" example:"succeeded"`

	// ScheduleID names the schedule that materialized this run, and is empty
	// for every other trigger. It is what the single-fire guarantee is written
	// against: the run's (schedule, fire time) pair is unique, so however many
	// replicas notice the same fire, exactly one run exists for it.
	ScheduleID string `json:"schedule_id,omitempty"`

	// Params are the bound, type-checked parameter values the run executes
	// with. They are bound once, when the run is created, so a re-read of the
	// row explains the run exactly.
	Params map[string]any `json:"params,omitempty"`

	// RequestedBy is the email of whoever asked for this run.
	RequestedBy string `json:"requested_by,omitempty" example:"jane@example.com"`

	// FireTime is the instant the run computes against, handed to the script as
	// run.fire_time. It is pinned when the run is created and never moves,
	// which is what ScheduledFor cannot promise: an infrastructure retry pushes
	// the due time out, and a run delayed that way must still produce the report
	// it was asked for rather than one shifted by the delay.
	FireTime     time.Time  `json:"fire_time"`
	ScheduledFor time.Time  `json:"scheduled_for"`
	StartedAt    *time.Time `json:"started_at,omitempty"`
	FinishedAt   *time.Time `json:"finished_at,omitempty"`

	// Attempt counts claims of this run, and LockedUntil / LockedBy carry the
	// current worker's lease. Together they are the fencing token: a write
	// from a worker whose lease expired and was reclaimed matches no row.
	Attempt     int        `json:"attempt"`
	LockedUntil *time.Time `json:"locked_until,omitempty"`
	LockedBy    string     `json:"locked_by,omitempty"`

	Error        string      `json:"error,omitempty"`
	Log          string      `json:"log,omitempty"`
	LogTruncated bool        `json:"log_truncated,omitempty"`
	Metrics      RunMetrics  `json:"metrics"`
	Outputs      []RunOutput `json:"outputs,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Run is one execution of one approved script version: a queue row while it is pending or running, and the durable history of that execution afterwards.

The two roles are deliberately one table. A run's history IS its queue record — what was executed, with which parameters, by whose request, how long it took, what it wrote — and splitting them would mean copying every field to keep the history readable.

func (*Run) Lease

func (r *Run) Lease() RunLease

Lease is one worker's claim on one run, and the fencing token every write against that run carries.

func (*Run) Output

func (r *Run) Output(name, destination string) *RunOutput

Output returns the recorded output this run wrote under one name to one destination, or nil. A worker consults it before writing: an output this run already persisted must not be written twice when the run is reclaimed after a crash.

The lookup is by the pair because the name alone is not the identity of a write. One result may be both versioned as a portal asset and delivered to a bucket, and matching on the name would report the second write as already done and silently skip it.

func (*Run) Terminal

func (r *Run) Terminal() bool

Terminal reports whether the run has finished, however it ended. A skipped overlap is terminal on arrival: it names a fire that will not be executed, so nothing is ever going to move it.

type RunFilter

type RunFilter struct {
	// ScriptID scopes the listing to one script; empty lists across scripts.
	ScriptID string
	// Status scopes the listing to one lifecycle status.
	Status string
	// Limit caps the rows returned; zero means the store default.
	Limit int
}

RunFilter selects runs for a history listing.

type RunLease

type RunLease struct {
	RunID   string
	Worker  string
	Attempt int
}

RunLease identifies the holder of a claim. Every mutating store call takes one, and a call whose lease no longer matches the row is refused with ErrLeaseLost rather than overwriting the work of the worker that took over.

type RunMetrics

type RunMetrics struct {
	Steps      uint64 `json:"steps"`
	DurationMS int64  `json:"duration_ms"`
	Queries    int    `json:"queries"`
	Exports    int    `json:"exports"`
}

RunMetrics is what one execution cost, recorded on the run for capacity review and for sizing an approved script's limits against what it actually uses.

type RunOutput

type RunOutput struct {
	Name string `json:"name"`
	// Destination is the granted destination's name. It is empty on rows
	// written before destinations existed, which Destination() reads as the
	// portal.
	Destination string `json:"destination,omitempty"`

	// AssetID and AssetVersion identify a portal output.
	AssetID      string `json:"asset_id,omitempty"`
	AssetVersion int    `json:"asset_version,omitempty"`

	// Bucket and Key locate a delivered object.
	Bucket string `json:"bucket,omitempty"`
	Key    string `json:"key,omitempty"`

	Format   string `json:"format"`
	RowCount int    `json:"row_count"`
	Bytes    int    `json:"bytes"`
}

RunOutput is one persisted output of a run: where it went, and what landed there. An output written to the portal names the stable asset the output name maps to and the version this run created of it; one delivered to a bucket names the object it wrote.

One run may write the same output name to more than one destination — the dashboard keeps its versioned asset while an external system receives its file — so a recorded output is identified by the PAIR, never by the name alone.

type RunResult

type RunResult struct {
	// Status is RunStatusSucceeded or RunStatusFailed.
	Status string
	// Error is the failure message, carrying the Starlark backtrace when the
	// script itself failed.
	Error        string
	Log          string
	LogTruncated bool
	Metrics      RunMetrics
}

RunResult is the terminal outcome of one attempt, as the worker reports it.

type RunStore

type RunStore interface {
	// Enqueue inserts a pending run, assigning ID when empty.
	Enqueue(ctx context.Context, r *Run) error

	// GetRun returns one run by id, or ErrRunNotFound.
	GetRun(ctx context.Context, id string) (*Run, error)

	// ListRuns returns runs matching the filter, newest first.
	ListRuns(ctx context.Context, filter RunFilter) ([]Run, error)

	// Claim takes the next due run for worker, holding it for lease. It
	// returns ErrNoWork when nothing is due.
	Claim(ctx context.Context, worker string, lease time.Duration) (*Run, error)

	// RecordOutput appends one persisted output to the run. It is written as
	// soon as the output exists, not at the end of the run, so a reclaimed run
	// can tell what it already wrote.
	RecordOutput(ctx context.Context, lease RunLease, out RunOutput) error

	// Finish records a terminal result for the claimed run.
	Finish(ctx context.Context, lease RunLease, res RunResult) error

	// Retry returns the claimed run to pending, due after backoff, recording
	// the cause. Reserved for infrastructure failures: a script error is
	// deterministic and retrying it changes nothing.
	Retry(ctx context.Context, lease RunLease, cause string, backoff time.Duration) error

	// PurgeRuns deletes terminal runs older than retention, returning the
	// number removed.
	PurgeRuns(ctx context.Context, retention time.Duration) (int64, error)
}

RunStore is the queue and the history of script runs.

The queue half follows the shape the platform's other durable queues use: a claim that folds crashed-worker recovery into its own predicate (a lease that expired makes the row claimable again), so there is no reaper process and no leader election, and any number of replicas can run a worker.

type Schedule

type Schedule struct {
	ID       string `json:"id"`
	ScriptID string `json:"script_id"`

	// CronSpec is a standard five-field cron expression or one of the
	// descriptors (@daily, @hourly, @every 30m).
	CronSpec string `json:"cron_spec" example:"0 7 * * 1-5"`
	// Timezone is the IANA zone the spec is read in.
	Timezone string `json:"timezone" example:"America/Los_Angeles"`

	// Params are the values every fire binds, with tokens unexpanded. They are
	// stored as written so the schedule reads as what it means ("report on the
	// day it fires") rather than as whatever date happened to be current when
	// somebody set it.
	Params map[string]any `json:"params,omitempty"`

	Enabled bool `json:"enabled"`

	// NextRunAt is when the next fire is due, and zero when the expression has
	// no further fire at all. It is the materializer's efficiency index, not
	// its correctness guarantee: two replicas may read the same due schedule,
	// and what stops them producing two runs is the unique index on the run
	// they insert.
	NextRunAt time.Time `json:"next_run_at,omitzero"`
	// LastFireAt is the fire time of the most recent run this schedule
	// produced, empty until it has produced one.
	LastFireAt *time.Time `json:"last_fire_at,omitempty"`
	// MissedFires counts fires that came due while the platform was not
	// materializing them, cumulatively. Catching up on them is deliberately
	// not attempted (see NextFire), so the count is the visible record of the
	// gap.
	MissedFires int `json:"missed_fires"`

	CreatedBy string    `json:"created_by,omitempty" example:"jane@example.com"`
	UpdatedBy string    `json:"updated_by,omitempty" example:"jane@example.com"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Schedule is the cadence one script runs on: the cron expression, the zone it is read in, and the parameter values every fire binds.

A schedule is not an authority. It names when an already-approved version runs and with which parameters, and nothing else; the version it executes, the roles it presents, and the connections it may reach all come from the approval, which a schedule cannot touch. That is why setting one is an owner-or-admin action rather than a reviewer's.

func BuildSchedule

func BuildSchedule(sc *Script, approved *Version, prev *Schedule, req ScheduleRequest, now time.Time) (*Schedule, error)

BuildSchedule turns a request into the schedule to store, validated against the parameter contract its fires will bind against and with its first fire computed.

The contract is the APPROVED version's when there is one, and the live record's otherwise. Both cases are right for the same reason: a schedule binds against whatever will actually execute, and until a version is approved nothing will, so the live record is the only contract there is to check against — which lets an author prepare a schedule before review without pretending it will fire.

prev, when non-nil, is the schedule being replaced: its identity and its creator survive an edit of the cadence, because the runs that point at it point at the same automation.

func (*Schedule) NextFire

func (s *Schedule) NextFire(c Cron, now time.Time) ScheduleFire

NextFire returns the fire a due schedule should materialize now, the number of fires that were missed before it, and the next due time to store.

The misfire policy is fire-once-latest. After a gap — a stopped worker, a restored database — the schedule materializes ONE run, for the most recent fire that has come due, and counts the rest as missed. Catching up would mean a burst of identical reports against the warehouse the moment the platform came back, each computing a date nobody is waiting on any more, which is a worse failure than a visible gap. A backfill somebody actually wants is a run_script call with the parameters they want.

Due is false when nothing is due yet, and when the schedule is further behind than one pass will walk.

func (*Schedule) Validate

func (s *Schedule) Validate(params []Param) error

Validate checks that a schedule is well-formed against the parameter contract of the version it will execute.

The parameter check is done here, when the schedule is set, rather than at the fire — a schedule whose bindings do not satisfy the contract would otherwise fail silently every night, with nobody watching.

type ScheduleAdvance

type ScheduleAdvance struct {
	// ID names the schedule and From is where the caller found NextRunAt. The
	// update applies only if the row still carries From, which is what makes
	// the step idempotent across replicas.
	ID   string
	From time.Time
	// Next is the fire time to store — zero when the expression has no further
	// fire, which parks the schedule rather than leaving it perpetually due —
	// and Fired, when non-zero, is the fire this pass materialized.
	Next  time.Time
	Fired time.Time
	// Missed is added to the schedule's cumulative missed-fire count.
	Missed int
}

ScheduleAdvance is one conditional step forward of a schedule.

type ScheduleFilter

type ScheduleFilter struct {
	// ScriptID scopes the listing to one script.
	ScriptID string
	// ScriptIDs scopes the listing to a set of scripts, which is how a caller
	// lists the schedules it may see without reading the rest and discarding
	// them. An EMPTY, non-nil slice means "no scripts" and matches nothing —
	// the distinction matters, because a caller who can see nothing must not
	// fall through to an unfiltered listing.
	ScriptIDs []string
	// Enabled, when set, scopes the listing to enabled or disabled schedules.
	Enabled *bool
	// Limit caps the rows returned; zero means the store default.
	Limit int
}

ScheduleFilter selects schedules for a listing.

type ScheduleFire

type ScheduleFire struct {
	// At is the fire to materialize, meaningful only when Due is true.
	At time.Time
	// Missed counts the fires stepped over without materializing.
	Missed int
	// Next is the due time to store, zero when the expression has no further
	// fire.
	Next time.Time
	// Due reports whether At should be materialized at all.
	Due bool
}

ScheduleFire is what one materialization pass concluded about a schedule: which fire to materialize, how many were stepped over to reach it, and where the schedule moves to.

type ScheduleRequest

type ScheduleRequest struct {
	CronSpec string
	Timezone string
	// Params are the bindings, with tokens as written.
	Params map[string]any
	// Enabled is a pointer so a request that does not mention it leaves an
	// existing schedule's state alone; a new schedule with nothing said is on,
	// because setting a schedule is asking for it to run.
	Enabled *bool
	// Actor is who is making the change, recorded on the row.
	Actor string
}

ScheduleRequest is a surface's request to set a script's schedule.

type ScheduleStore

type ScheduleStore interface {
	// SetSchedule creates or replaces the schedule of one script, assigning ID
	// when empty. A script has at most one schedule: a second cadence is a
	// second script, which keeps the run history of "the 07:00 report" from
	// interleaving with another cadence's.
	SetSchedule(ctx context.Context, s *Schedule) error

	// GetSchedule returns one script's schedule, or ErrScheduleNotFound.
	GetSchedule(ctx context.Context, scriptID string) (*Schedule, error)

	// ListSchedules returns schedules matching the filter.
	ListSchedules(ctx context.Context, filter ScheduleFilter) ([]Schedule, error)

	// SetScheduleEnabled turns one script's schedule on or off, recording who
	// did it. Disabling is how a schedule is retired: the row stays, because a
	// schedule that produced runs is part of the explanation of those runs.
	SetScheduleEnabled(ctx context.Context, scriptID string, enabled bool, actor string) error

	// DueSchedules returns enabled schedules whose next fire has arrived.
	DueSchedules(ctx context.Context, now time.Time, limit int) ([]Schedule, error)

	// MaterializeRun inserts one scheduled run, reporting what happened: the
	// run was created, it was skipped because the previous one is still open,
	// or another replica got there first.
	MaterializeRun(ctx context.Context, r *Run) (Materialization, error)

	// AdvanceSchedule moves a schedule forward after a materialization pass.
	// It is conditional on the schedule still being where the caller found it,
	// so two replicas that walked the same fire do not double-count the misses
	// or move the schedule twice. It reports whether the row moved.
	AdvanceSchedule(ctx context.Context, adv ScheduleAdvance) (bool, error)
}

ScheduleStore persists schedules and materializes the runs they produce.

Materialization lives here rather than on RunStore because its correctness is a property of the schedule, not of the queue: what makes a fire happen once across every replica is the unique index this store's insert conflicts against.

type ScoredScript

type ScoredScript struct {
	Script Script  `json:"script"`
	Score  float64 `json:"score"`
}

ScoredScript pairs a script with its relevance score in [0,1].

type Script

type Script struct {
	ID          string   `json:"id" example:"script_a1b2c3d4"`
	Name        string   `json:"name" example:"daily-sales-report"`
	DisplayName string   `json:"display_name" example:"Daily Sales Report"`
	Description string   `json:"description" example:"Summarize yesterday's sales by region"`
	Source      string   `json:"source" example:"rows = platform.query(connection='primary', sql='SELECT 1')"`
	Params      []Param  `json:"params"`
	Scope       string   `json:"scope" example:"personal"`
	Personas    []string `json:"personas" example:"analyst"`
	OwnerEmail  string   `json:"owner_email" example:"jane@example.com"`
	Tags        []string `json:"tags" example:"sales,reporting"`
	Enabled     bool     `json:"enabled" example:"true"`

	// Lifecycle.
	Status       string     `json:"status" example:"draft"`
	SupersededBy string     `json:"superseded_by,omitempty" example:"daily-sales-report-v2"`
	DeprecatedAt *time.Time `json:"deprecated_at,omitempty"`

	// Version is the number of the snapshot the live row currently carries.
	// Pending draft versions above this number exist in the history and are not
	// served.
	Version int `json:"version" example:"3"`

	// ApprovedVersionID is THE execution gate: the id of the one version the
	// platform may execute. Empty means the script has no approved version and
	// nothing will run it — which is every script today, because the approval
	// action and the runner that reads this pointer arrive with the execution
	// gate. Draft execution (manage_script run_draft) deliberately never reads
	// it: a draft runs under its author's own identity and authority, so it
	// needs no approval, and it also cannot stand in for one.
	ApprovedVersionID string `json:"approved_version_id,omitempty"`

	CreatedAt time.Time `json:"created_at" example:"2026-08-13T14:30:00Z"`
	UpdatedAt time.Time `json:"updated_at" example:"2026-08-13T14:30:00Z"`
}

Script is the live record of one managed script: its identity, its currently served source and parameter contract, and the pointer to the version the platform is allowed to execute.

func (*Script) ApplyStatusTransition

func (s *Script) ApplyStatusTransition(newStatus, supersededBy string, now time.Time) error

ApplyStatusTransition validates and applies a status change, stamping the lifecycle metadata. A no-op when newStatus is empty or unchanged. now is passed in for testability. Returns an error on an invalid transition.

Moving to active is refused for a script with no approved version: active asserts that the platform will execute this script, and with no ApprovedVersionID there is nothing it is allowed to execute. That keeps status a report of the execution gate rather than a way around it.

func (*Script) Executable

func (s *Script) Executable() bool

Executable reports whether the platform may execute this script on its own — on a schedule or through a run tool. It is false until a version is approved.

func (*Script) Principal

func (s *Script) Principal() string

Principal is the identity an approved run of this script authenticates as.

A script gets its own principal rather than borrowing its owner's so every gate, rate limiter, and audit row can tell the two apart: a row reading script:daily-sales is a governed automation, and one reading the owner's address is that person at a keyboard. The owner stays accountable through the script's owner_email, which the run also carries.

func (*Script) Validate

func (s *Script) Validate() error

Validate checks the whole record: name, scope, source, parameter contract, tags, and status. Mutation surfaces call it on the FINAL state rather than field by field as arguments arrive, so a record that was valid before an edit and invalid after it is refused — which is the case a per-argument check misses, since the offending combination may involve a field the caller never sent.

func (*Script) VisibleTo

func (s *Script) VisibleTo(email, persona string) bool

VisibleTo reports whether a caller identified by email and holding persona may see this script. It is the one definition of script visibility, shared by the read path and by the list predicate, so a script can never be listable but unreadable or the reverse. Admin authority is applied by the caller, not here: this answers only what the scope rules say.

func (*Script) VisibleToAny

func (s *Script) VisibleToAny(email string, personas []string) bool

VisibleToAny reports whether a caller who BELONGS TO any of personas may see this script. It is the discovery arity of the same rule: a listing scopes on the single persona a request resolved to act as, while search and fetch scope on the caller's whole membership set, which is an entitlement they hold rather than a property of one request. An empty set means "belongs to no persona", so persona-scoped scripts are invisible — the fail-closed answer.

type SearchQuery

type SearchQuery struct {
	// QueryText is the raw intent text the lexical ranking matches.
	QueryText string
	// OwnerEmail is the caller identity, for personal-scope visibility. Empty
	// leaves the caller seeing no personal scripts at all, including their own.
	OwnerEmail string
	// Personas is every persona the caller belongs to, for persona-scope
	// visibility.
	Personas []string
	// Limit caps the candidates returned; see EffectiveLimit.
	Limit int
}

SearchQuery describes a relevance ranking request over the script library.

Visibility is applied before ranking, as a predicate rather than a filter over the answer: a script the caller cannot see must cost neither a row nor a decision. The rule is Script.VisibleTo expressed in SQL — global scripts, persona-scoped scripts of a persona the caller belongs to, and the caller's own personal scripts — with one deliberate difference from the manage_script listing, which scopes on the single persona a request resolved to. Discovery scopes on the whole membership set for the same reason the managed-resources provider does: membership is an entitlement the caller holds, while the acting persona is a property of one request. An empty set therefore means "belongs to no persona", which is the fail-closed answer and also what a deployment that never wires a persona resolver gets.

func (SearchQuery) EffectiveLimit

func (q SearchQuery) EffectiveLimit() int

EffectiveLimit clamps the requested limit into [1, maxSearchLimit], defaulting an unset or out-of-range value to DefaultSearchLimit.

type Searcher

type Searcher interface {
	Search(ctx context.Context, q SearchQuery) ([]ScoredScript, error)

	// Contract composes the contract document for one script: the script's own
	// record, the approved version's parameter contract and approval stamp, its
	// cadence when it has one, and its last successful run. It applies no
	// visibility rule of its own — the caller has already established that this
	// script may be seen.
	Contract(ctx context.Context, id string) (*Contract, error)
}

Searcher ranks scripts by relevance within the caller's visibility, and resolves one script's whole contract by id. The two halves are the two halves of discovery: search says a script exists and what it takes, and the contract read says everything a caller needs to decide whether to use it.

It is a capability separate from Store, so only a backing store that can rank (the PostgreSQL one) implements it and the feature degrades to absent rather than forcing every Store to carry a ranking query.

type Store

type Store interface {
	// Create persists a new script and its first version, assigning ID when
	// empty. The author is recorded on that version along with the authority
	// they held, which is the ceiling on what approving it can grant.
	Create(ctx context.Context, s *Script, author Author) error

	// Get retrieves a shared (global or persona) script by its globally unique
	// name. Returns nil, nil if not found.
	Get(ctx context.Context, name string) (*Script, error)

	// GetPersonal retrieves a personal script by owner and name. Returns
	// nil, nil if not found.
	GetPersonal(ctx context.Context, ownerEmail, name string) (*Script, error)

	// GetByID retrieves a script by ID. Returns nil, nil if not found.
	GetByID(ctx context.Context, id string) (*Script, error)

	// Update modifies an existing script.
	Update(ctx context.Context, s *Script) error

	// Delete removes a script by ID.
	Delete(ctx context.Context, id string) error

	// List returns scripts matching the filter, newest first.
	List(ctx context.Context, filter ListFilter) ([]Script, error)
}

Store defines the interface for script persistence. It mirrors the prompt store's resolution contract: shared names are globally unique and resolve with Get, personal names are unique only within an owner and need GetPersonal.

type Version

type Version struct {
	ID          string   `json:"id" example:"sver_a1b2c3d4"`
	ScriptID    string   `json:"script_id" example:"script_a1b2c3d4"`
	Version     int      `json:"version" example:"3"`
	DisplayName string   `json:"display_name" example:"Daily Sales Report"`
	Description string   `json:"description" example:"Summarize yesterday's sales by region"`
	Source      string   `json:"source"`
	Params      []Param  `json:"params"`
	Tags        []string `json:"tags" example:"sales,reporting"`
	Author      string   `json:"author" example:"jane@example.com"`
	// AuthorRoles is the authority the author held when this snapshot was
	// written — the ceiling on what approving it can grant. See Author.
	AuthorRoles []string   `json:"author_roles,omitempty" example:"analyst"`
	Status      string     `json:"status" example:"applied"`
	ApprovedBy  string     `json:"approved_by,omitempty" example:"admin@example.com"`
	ApprovedAt  *time.Time `json:"approved_at,omitempty"`
	// Grants is the capability set bound to this version at approval: what the
	// approver approved this code to be able to do. It is empty on every
	// version that was never approved, and the approval action is the only
	// writer — changing what a script may reach means approving it again, which
	// re-stamps the approval alongside the new grant.
	Grants    Grants    `json:"grants"`
	CreatedAt time.Time `json:"created_at" example:"2026-08-13T14:30:00Z"`
}

Version is one immutable snapshot of a script's versioned fields (source, params, display name, description, tags), with the author who produced it and the approval stamp bound to this specific version. Stamps never change once set: approving v5 does not alter what was recorded for v4.

The snapshot is what makes a run explainable months later — a run record names the version it executed, and the code of that version is still here.

func (*Version) Approved

func (v *Version) Approved() bool

Approved reports whether this version carries an approval stamp.

type VersionStore

type VersionStore interface {
	// UpdateWithVersion persists s like Store.Update and, when any versioned
	// snapshot field differs from the stored row, records a new applied version
	// authored by author and advances s.Version to it.
	UpdateWithVersion(ctx context.Context, s *Script, author Author) error

	// CreateDraftVersion snapshots proposed's versioned fields as a new draft
	// version without touching the live row, returning the new version number.
	// The approved version keeps executing until the draft is approved.
	CreateDraftVersion(ctx context.Context, scriptID string, proposed *Script, author Author) (int, error)

	// ListVersions returns every version of the script, newest first.
	ListVersions(ctx context.Context, scriptID string) ([]Version, error)

	// GetVersion returns one version with its full source, or nil, nil when the
	// script has no such version.
	GetVersion(ctx context.Context, scriptID string, version int) (*Version, error)

	// GetVersionByID returns one version by its id, or nil, nil when no such
	// version exists. It is how a run loads the code it is allowed to execute:
	// the execution gate is an id, so the runner resolves that id and never a
	// version number, which could be renumbered or point at a later draft.
	GetVersionByID(ctx context.Context, id string) (*Version, error)
}

VersionStore is the versioning capability of a script store. The PostgreSQL store implements it; a store without it (no-database deployments, plain test stores) degrades to unversioned updates through ApplyEdit's fallback. Every write method is transactional with the scripts row it touches.

Jump to

Keyboard shortcuts

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