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
- Variables
- func AutoApprovable(sc *Script, author Author) bool
- func BindParams(defs []Param, values map[string]any) (map[string]any, error)
- func BindScheduleParams(defs []Param, raw map[string]any, fire time.Time, loc *time.Location) (map[string]any, error)
- func CheckConnectionParams(defs []Param, bound map[string]any, g Grants) error
- func DescriptionNotice(desc string) string
- func ExecutionNote(s *Script) string
- func IndexText(s *Script) string
- func ParamSummary(params []Param) string
- func ParamsEqual(a, b []Param) bool
- func RefuseDraftRun(sc *Script) error
- func RefuseNewRun(sc *Script, approved *Version) error
- func RefuseRun(sc *Script, v *Version, run *Run) error
- func RefuseUnreachable(g Grants, reachable []string) string
- func RequiresReview(before, after *Script) bool
- func SnapshotChanged(before, after *Script) bool
- func SourceDigest(source string) []byte
- func Title(s *Script) string
- func ValidateName(name string) error
- func ValidateObjectKey(key string) error
- func ValidateParams(params []Param) error
- func ValidateScope(scope string) error
- func ValidateSource(src string) error
- func ValidateStatus(status string) error
- func ValidateStatusTransition(from, to string) error
- func ValidateTags(tags []string) error
- func WithdrawsAutoApproval(before, after *Script) bool
- type ApprovalStore
- type Author
- type AutoApprovalStore
- type AutoApprover
- type AutoDecision
- type AutoOutcome
- type Contract
- type ContractApproval
- type ContractOutput
- type ContractRun
- type ContractSchedule
- type Cron
- type Destination
- type DryRun
- type DryRunOutput
- type DryRunStore
- type Edit
- type EditOutcome
- type Grants
- func (g Grants) AllowsCapability(name string) bool
- func (g Grants) AllowsConnection(name string) bool
- func (g Grants) AllowsDestination(name string) bool
- func (g Grants) Destination(name string) (Destination, bool)
- func (g Grants) DestinationNames() []string
- func (g Grants) IsZero() bool
- func (g Grants) MissingFor(capabilities, connections, destinations []string) Missing
- func (g Grants) Validate() error
- type ListFilter
- type Materialization
- type Missing
- type Param
- type PendingReview
- type Referenced
- type RejectionStore
- type ReviewStore
- type Run
- type RunFilter
- type RunLease
- type RunMetrics
- type RunOutput
- type RunResult
- type RunStore
- type Schedule
- type ScheduleAdvance
- type ScheduleFilter
- type ScheduleFire
- type ScheduleRequest
- type ScheduleStore
- type ScoredScript
- type Script
- func (s *Script) ApplyStatusTransition(newStatus, supersededBy string, now time.Time) error
- func (s *Script) Executable() bool
- func (s *Script) OwnedPersonally(email string) bool
- func (s *Script) Principal() string
- func (s *Script) Validate() error
- func (s *Script) VisibleTo(email, persona string) bool
- func (s *Script) VisibleToAny(email string, personas []string) bool
- type SearchQuery
- type Searcher
- type Store
- type Version
- type VersionStore
Constants ¶
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.
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.
const ( // MaxDisplayNameLen bounds a script's display name, in runes, at the limit // its siblings use (resource.MaxDisplayNameLen). Unlike the description // there is no reading on which a label wants to be long: it is what the // listing, the page header and a search result print, and every one of them // truncates. An empty display name is allowed and falls back to the name an // agent calls the script by (Title). MaxDisplayNameLen = 200 // MaxDescriptionBytes is the ceiling a description is refused above, and it // is a structural limit rather than an editorial one: it exists to protect // the row, the version history, and the search vector. // // The vector is the binding constraint. script_fts is composed from the // description together with the title, the category, the tags and the // parameter contract, and it is built into a GIN index expression // (migration 000102), so to_tsvector runs on every write. PostgreSQL // refuses a tsvector input over 1 MiB, and an index expression that raises // makes the row unwritable rather than merely unfindable — the description // would take the script down with it. 64 KiB leaves the composed document // an order of magnitude inside that limit even with every other indexed // field at its own maximum, and it is roughly twenty pages of markdown, // which is far past any honest documentation of one script. MaxDescriptionBytes = 64 * 1024 )
The rules for the three fields that explain a script to a person: its display name, its description, and the category it is filed under (#1369).
A managed script is complex logic that outlives the conversation that produced it, so its description is a DOCUMENT rather than a caption: markdown an author writes at whatever length the automation actually needs explaining at. That is why the bounds here are not the ones an asset or a resource puts on its own description. Those are caption bounds — 2000 characters, about half a page — and capping a script description there would cap the thing this documentation exists to hold.
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.
const ( ParamTypeString = "string" ParamTypeInt = "int" ParamTypeFloat = "float" ParamTypeBool = "bool" ParamTypeDate = "date" ParamTypeEnum = "enum" // ParamTypeConnection is a connection name (#1361). It binds as a string, // and it is a type of its own because the platform knows the whole set of // values it can take and an approved version's grant names the subset this // script may use. That is the difference from a string: a surface asking // for one can OFFER the set instead of asking somebody to remember the // spelling, and a value outside it is refused where it was entered rather // than at the run it would have failed. ParamTypeConnection = "connection" )
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.
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.
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" // TriggerPortal marks a run an owner asked for on the script's own page // (#1363). It executes exactly as the other two do — same worker, same // grant, same audit — and it is a distinct label because the run history is // read by the person who clicked it, and recording their own click as an // agent's tool call is a false statement about who did what. TriggerPortal = "portal" )
Run triggers: what produced the run row.
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.
const ( ScopeGlobal = "global" ScopePersona = "persona" ScopePersonal = "personal" )
Scope constants define script visibility levels, matching prompt scopes.
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.
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.
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.
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.
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.
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.
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.
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 ¶
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.
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.
var Capabilities = []string{CapabilityQuery, CapabilityExport}
Capabilities is the full host surface, in the order help, validate, and the review surfaces report it.
var DestinationKinds = []string{DestinationKindPortal, DestinationKindS3}
DestinationKinds is the full set of destination kinds.
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.
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.
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.
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.
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 AutoApprovable ¶ added in v1.122.0
AutoApprovable reports whether a version of this script, written by this author, is a candidate for automatic approval at all.
The three conditions are the whole of the authority argument. The script is PERSONAL, so its only caller is the person it belongs to; the author IS that person, so the roles the version captured are their own and approving binds nothing they did not already hold; and the script has not been replaced, which is the one lifecycle state that must never become executable again.
An edit written by anybody else — an administrator fixing somebody's script is the author of what they wrote, and their roles are what the version would capture — is not a candidate and goes to review.
func BindParams ¶
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 CheckConnectionParams ¶ added in v1.122.0
CheckConnectionParams refuses a bound value for a connection-typed parameter that the grant does not permit (#1361).
The run would refuse it anyway: a connection reaches platform.query as an argument, and the host checks every one against this same list. What this buys is WHERE the refusal lands. Without it a mistyped connection name is accepted by every surface that takes it, queued, executed, and reported as a failed run to somebody who is no longer looking; with it the surface that asked for the value answers, naming what this script was approved to reach.
It applies only to a run confined by a grant. A draft executes under its author's own identity with no grant layer, so there is nothing here to check it against and the author's persona is the boundary.
func DescriptionNotice ¶ added in v1.122.0
DescriptionNotice is the non-blocking signal that a description has outgrown the script it documents, and empty when it has not.
It is advisory by construction, which is the whole point of it. The platform wants long descriptions — that is what makes a stored automation understandable months later by somebody who did not write it — so the only hard refusal is the structural one (MaxDescriptionBytes), and everything short of it succeeds. This says what the author might do instead; it never decides for them.
The signal is size alone, unlike the knowledge page's, which also counts headings. A page with twelve top-level sections is covering several topics and wants splitting; a script description with twelve sections is still about one script, and refusing to distinguish the two would put a nudge in front of an author who is documenting thoroughly and correctly.
func ExecutionNote ¶ added in v1.122.0
ExecutionNote states a script's execution state in one sentence.
func IndexText ¶ added in v1.122.0
IndexText composes the text a script is embedded on and shown as in a search result: its title (display name, falling back to the name an agent calls it by), its description, the names of the parameters a run binds, the category it is filed under and its tags, and one line stating whether anything will execute it. Empty parts are skipped so a sparse script does not pad the text with blank lines.
The execution note is part of the document rather than decoration: it changes what the script IS FOR. An approved script is something to run; an unapproved one is something to ask a reviewer about, and reading a result should not leave that ambiguous.
The source code is deliberately absent. docs/scripts/security.md admits the contract to anyone the scope rules admit and the source only to the owner and to administrators; one vector per row cannot be split along that line, so a vector built partly from source would let code a caller may not read decide how their results rank. The source also churns on every code edit while a description changes rarely, so indexing it would re-embed the corpus for changes that do not alter what the script is for.
The indexjobs scripts consumer and the discovery source MUST agree on this composition — a stored embedding has to live in the same space as the text a caller is shown — so it is defined once here for both.
func ParamSummary ¶
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 ¶
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 RefuseDraftRun ¶ added in v1.122.0
RefuseDraftRun reports why a draft run of this script would be refused, or nil when one would be admitted.
A draft run is the only execution path an unapproved script has, so without this check "disabled" and "superseded" would disable and supersede nothing. It is deliberately NOT the approved-run gate: a draft executes as its author with no grant, so approval has nothing to say about it.
func RefuseNewRun ¶
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 ¶
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 RefuseUnreachable ¶ added in v1.122.0
RefuseUnreachable reports the granted connections the author's own persona cannot reach, or an empty string when every one of them is reachable.
An approved run presents the author's roles, so a script granted a connection its author cannot reach is a script that fails on its first query — the middleware refuses the call whatever the grant says. Answering it here means the owner is told while they are still looking at the script, rather than by a failed run at three in the morning.
An empty reachable set is not read as "reaches nothing": a deployment that cannot enumerate its connections would otherwise refuse every script, and this check is a courtesy in front of a boundary the middleware enforces regardless.
func RequiresReview ¶
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 ¶
SnapshotChanged reports whether any versioned snapshot field (source, params, display name, description, category, tags) differs between the two states.
The four documentation fields are versioned together and none of them is gated by RequiresReview, which is what lets one form edit all of them at once (#1369): an edit that only documents a script applies to the live row immediately, is captured as a version like every other edit, and does not send the version that is executing back to a reviewer.
func SourceDigest ¶ added in v1.122.0
SourceDigest is the digest a dry-run account is keyed by. One definition, used by the writer that records a run and by the reader that matches a version against it, so the two cannot disagree about what "the same source" means.
func Title ¶ added in v1.122.0
Title renders a script's human label: its display name, falling back to the name an agent would call it by.
func ValidateName ¶
ValidateName checks that a script name is well-formed.
func ValidateObjectKey ¶
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 ¶
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 ¶
ValidateScope checks that a scope value is allowed.
func ValidateSource ¶
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 ¶
ValidateStatus checks that a status value is recognized.
func ValidateStatusTransition ¶
ValidateStatusTransition checks whether a status transition is allowed.
func ValidateTags ¶
ValidateTags checks that a script's tag list is within bounds.
func WithdrawsAutoApproval ¶ added in v1.122.0
WithdrawsAutoApproval reports whether an edit takes a script out of the scope its automatic approval was granted under.
Widening scope off personal is the one edit that changes who the approval was reasoned about. The version was approved because its only caller was its author; a persona-scoped or global script has an audience that never agreed to anything, so the approval nobody made must not follow the script into it. The gate is cleared and the version returns to the review queue.
It says nothing about whether the approval WAS automatic — that is a fact about the version, which the store reads under the same lock — because an approval a person made survives the scope change: they decided, and widening the audience does not un-decide it.
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 AutoApprovalStore ¶ added in v1.122.0
type AutoApprovalStore interface {
// AutoApproveVersion stamps the named version approved on the owner's own
// authorship, binds grants to it, marks it as an approval nobody reviewed,
// and points the script's execution gate at it.
//
// The grant's Roles are ignored here exactly as they are in ApproveVersion:
// the implementation copies them from the version's author, who for an
// automatic approval is the owner. It returns ErrVersionConflict when the
// version was already resolved or the script moved underneath the write.
AutoApproveVersion(ctx context.Context, scriptID string, version int, owner string, grants Grants) (*Version, error)
}
AutoApprovalStore is the execution gate's other write: the approval the platform makes for a personal script on behalf of its owner (#1367).
It is separate from ApprovalStore for the same reason that one is separate from VersionStore — it records a different act. Everything else about it is identical, deliberately: it binds a grant, applies the snapshot, and moves the execution pointer through the same transaction body, so an automatically approved version is not a second kind of approved version.
type AutoApprover ¶ added in v1.122.0
type AutoApprover interface {
// Consider decides whether this script's next version can be approved with
// no reviewer, and what it would be approved to reach. It writes nothing.
Consider(ctx context.Context, sc *Script, author Author) AutoDecision
// Approve binds a decision Consider admitted to the named version, and
// advances sc to what the write left behind. A decision that was not
// approvable is answered with its own reason and nothing is written.
Approve(ctx context.Context, sc *Script, version int, decision AutoDecision) AutoOutcome
}
AutoApprover mints and binds the grant an owner-authored personal version executes under. A nil one is a deployment with no automatic approval, where every version waits for a reviewer exactly as before.
Neither method reports an error. A version that cannot be approved automatically is a version awaiting review, which is a state the surface has to describe either way, and failing the SAVE over the approval that followed it would tell an owner their work was lost when it was stored. Every refusal, including one caused by a store failure, comes back as a Reason.
type AutoDecision ¶ added in v1.122.0
type AutoDecision struct {
// Approvable reports that the grant below can be bound with no reviewer.
Approvable bool
// Reason states why it cannot, in the owner's terms; empty when Approvable
// and empty when the script was never a candidate.
Reason string
// Grants is what the version would be approved to reach.
Grants Grants
// Approved is the version the script executes today, nil when it has none.
// It is carried so binding can tell an approval that would change nothing
// from one that would, without reading the row again.
Approved *Version
}
AutoDecision is what automatic approval WOULD do, decided without writing anything.
The decision is separate from the act because the edit funnel has to know the answer before it chooses where to put the edit. An edit that will be approved is applied to the live row; one that will not becomes a draft awaiting review. Deciding afterwards would leave a declined edit applied to a script whose execution pointer still names the older version — running code nobody can find in the review queue.
type AutoOutcome ¶ added in v1.122.0
type AutoOutcome struct {
// Approved reports that the version is now the script's approved version,
// bound to a grant the platform minted.
Approved bool
// Reason states why it was not, in the owner's terms, so the answer to
// "why is nothing running this" is on the response that saved it. It is
// empty when the version was approved, and equally empty when the script was
// never a candidate — a shared script and an edit somebody else wrote are
// not refusals, they are the ordinary review path.
Reason string
}
AutoOutcome is what automatic approval did with a version.
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"`
Category string `json:"category,omitempty" example:"reporting"`
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 ¶
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 ¶
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 ¶
Title is the script's human label: its display name, falling back to the name an agent would call it by.
func (Contract) VisibleToAny ¶
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"`
// Automatic reports that the platform approved this version itself, because
// the script is personal and its owner wrote it (#1367). Nobody reviewed it,
// and a reader of the contract is told so rather than reading ApprovedBy as
// a decision somebody made.
Automatic bool `json:"automatic,omitempty" example:"false"`
// 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.
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 DryRun ¶ added in v1.122.0
type DryRun struct {
// ID is the run id the draft executed under, which is also its session id,
// so the audit rows the run produced are reachable from this account.
ID string `json:"id" example:"run_a1b2c3d4"`
ScriptID string `json:"script_id"`
// SourceSHA256 is the digest of the source that executed. It is what links
// this account to a version, and it is a raw digest rather than its hex
// spelling because that is what the column stores.
SourceSHA256 []byte `json:"-"`
RequestedBy string `json:"requested_by,omitempty" example:"jane@example.com"`
// Status is RunStatusSucceeded or RunStatusFailed. A draft run has no
// pending state: it is executed inline, and the caller waits for it.
Status string `json:"status" example:"succeeded"`
// Error is why a failed draft failed, including the interpreter traceback.
Error string `json:"error,omitempty"`
// Log is what the run printed, bounded when it was captured.
Log string `json:"log,omitempty"`
LogTruncated bool `json:"log_truncated,omitempty"`
Metrics RunMetrics `json:"metrics"`
Outputs []DryRunOutput `json:"outputs,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
DryRun is one recorded draft execution.
type DryRunOutput ¶ added in v1.122.0
type DryRunOutput struct {
Name string `json:"name" example:"daily_sales"`
Destination string `json:"destination,omitempty" example:"portal"`
Format string `json:"format" example:"csv"`
RowCount int `json:"row_count" example:"1200"`
// Bytes is the serialized length in the declared format. A preview
// serializes to measure rather than estimating, so it is the size a real
// run of the same rows would write.
Bytes int `json:"bytes" example:"48213"`
}
DryRunOutput is one output a draft run would have written. It carries the shape and nothing else: a preview has no asset id and no object key, because it wrote neither.
type DryRunStore ¶ added in v1.122.0
type DryRunStore interface {
// RecordDryRun stores one account, trimming the author's older accounts of
// the same script so the table is bounded by the authoring loop's working
// set rather than by how many times somebody pressed the button.
RecordDryRun(ctx context.Context, d *DryRun) error
// LatestDryRun returns the newest account of one script's exact source, or
// nil when nobody has run it. Nil is the ordinary answer — most versions
// were never dry-run — so it is not an error.
LatestDryRun(ctx context.Context, scriptID string, sourceSHA256 []byte) (*DryRun, error)
}
DryRunStore records and resolves accounts of draft executions.
type Edit ¶ added in v1.122.0
type Edit struct {
// Before is the persisted pre-edit state and After the fully mutated copy.
Before *Script
After *Script
// Author is recorded on any version produced, together with the authority
// they held, which becomes the ceiling on what approving that version can
// grant (see Author).
Author Author
// Auto mints the approval a personal script's own version carries (#1367).
// Nil is a deployment with no automatic approval, where every version waits
// for a reviewer.
Auto AutoApprover
}
Edit is one edit crossing the funnel: the persisted pre-edit state, the fully mutated copy, who wrote it and the authority they held, and the automatic approval an owner-authored personal version may carry.
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"`
// Auto is what automatic approval did with an applied version (#1367). It is
// the zero value on a deferred edit, which is a version waiting for a
// reviewer by definition.
Auto AutoOutcome `json:"-"`
}
EditOutcome reports how ApplyEdit landed an edit: applied to the live script row, or deferred as a pending draft version awaiting approval.
func ApplyEdit ¶
ApplyEdit lands a script edit through the one shared gate every mutation surface crosses: the manage_script tool and the portal editor.
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.
An applied version is then offered to automatic approval (#1367), which is the one place that happens: a personal script whose owner wrote the edit becomes executable here, and every other script leaves this function exactly as it did before, waiting for a reviewer.
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 DeriveGrants ¶ added in v1.122.0
func DeriveGrants(ref Referenced, roles []string, pinned []Destination) (Grants, error)
DeriveGrants mints the capability grant a version runs under from what a static read says its code reaches, or reports why it cannot be minted.
pinned is the destination set the script's currently-approved version already carries, and it is the ONLY source of an address for a destination outside the platform. A destination names a connection, a bucket and a prefix that a person decided on, so there is nothing in the source to resolve one from: the first delivery to a bucket is reviewed, a reviewer pins the address, and the owner's later edits are approved against the address that was pinned. The portal needs no pinning, because the platform owns where its own assets live.
The error text is the owner's answer, not a diagnostic: it is put in front of the person who just pressed save.
func (Grants) AllowsCapability ¶
AllowsCapability reports whether the grant permits one host binding.
func (Grants) AllowsConnection ¶
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 ¶
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 ¶
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 ¶
IsZero reports whether the grant is entirely empty, which distinguishes an unapproved version from one deliberately approved with nothing granted.
func (Grants) MissingFor ¶
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 ¶
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
// Category narrows to one category slug; "" for all. Tags narrows to the
// scripts carrying ANY of the named tags, which is the same OR match the
// persona axis uses: a reader filtering by two tags is asking for the union
// of two shelves, not for the scripts on both.
Category string
Tags []string
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 ¶
Missing is what a script's source references that its grant does not cover, on each axis a static validation can read.
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.
type Referenced ¶ added in v1.122.0
type Referenced struct {
Capabilities []string
Connections []string
Destinations []string
// Incomplete is true when a call computes its connection or its destination
// instead of naming one, so the lists above are known to be short. A grant
// derived from a short list is a grant the run then refuses itself on, so it
// is a refusal to derive rather than a partial answer.
Incomplete bool
}
Referenced is what a static read of a script's source says the code plainly reaches: the host bindings it calls, the connections it names, and the destinations it writes to. It is the domain's view of a report the engine produces, so the rules here can be stated without the domain knowing what Starlark is.
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 ¶
Lease is one worker's claim on one run, and the fencing token every write against that run carries.
func (*Run) Output ¶
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.
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 ¶
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.
//
// While the schedule is disabled the field still holds the fire it was
// paused on, because resuming picks up from there. What a READER is told is
// DueAt, which is empty for a paused schedule; see MarshalJSON.
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) DueAt ¶ added in v1.122.0
DueAt reports when this schedule's next fire is due, and the zero time when no fire is due at all.
A disabled schedule has no next fire. Its stored NextRunAt is the fire it was paused on, kept so that resuming picks up where the pause began, and stating it to a reader announces a fire that will not happen: the materializer's own predicate requires the schedule to be enabled. Every surface that tells somebody when a schedule fires next asks this rather than reading the field.
func (Schedule) MarshalJSON ¶ added in v1.122.0
MarshalJSON renders the schedule as a reader may act on it, which differs from the stored row in exactly one field: next_run_at is DueAt.
It lives here rather than in each of the surfaces that serve a Schedule because the rule is a property of the schedule, not of any one payload — the admin API, the portal API, and the manage_script response all serve this struct, and a rule applied in three places is a rule that drifts in one.
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 ¶
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 ¶
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"`
// Category files the script under one lowercase slug, the axis a listing
// filters on and a reader scans. It is the same axis a resource and an
// insight carry, written the same way (#1369).
Category string `json:"category,omitempty" example:"reporting"`
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 ¶
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 ¶
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) OwnedPersonally ¶ added in v1.122.0
OwnedPersonally reports whether the named caller is this script's entire audience: it is personal, and it is theirs.
It is the one definition of that question, which two rules turn on (#1367). Automatic approval is available on exactly this shape, because there is no second person for a reviewer to be protecting; and the same shape is what makes a script its owner's to delete outright, since nothing anybody else can see or run disappears with it.
Both sides must be identified, for the reason scopeVisible gives: a script whose owner could not be established would otherwise belong to every caller the platform cannot name either.
func (*Script) Principal ¶
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 ¶
Validate checks the whole record: name, scope, source, parameter contract, tags, the fields that document the script (display name, description, category), 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 ¶
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 ¶
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 {
// Embedding is the query vector. A nil Embedding selects lexical-only
// ranking, which is exactly the behavior a deployment with no embedding
// provider has always had; a non-nil one selects hybrid ranking over the
// vectors the indexjobs scripts consumer writes.
Embedding []float32
// 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"`
Category string `json:"category,omitempty" example:"reporting"`
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"`
// AutoApproved marks an approval the PLATFORM made rather than a person
// (#1367): a personal script's own owner wrote this version, so the grant
// was minted from what its code reaches instead of being asked of a
// reviewer. ApprovedBy still names the owner, because they are accountable
// for it; this is what separates their authorship from somebody's decision,
// so an operator reading the history can tell which scripts nobody reviewed.
AutoApproved bool `json:"auto_approved,omitempty" example:"false"`
// 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, category, 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.
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.
//
// ungated carries the edit funnel's verdict that this edit needs no review,
// which the store cannot re-derive: an edit to a script with an approved
// version reaches the live row ONLY when automatic approval decided it
// covers it (#1367), and that decision needs a static read of the source
// this layer has no business doing. Everything else is re-validated against
// the row as locked and refused as a conflict, which is what stops an edit
// racing an approval from swapping code out from under it.
UpdateWithVersion(ctx context.Context, s *Script, author Author, ungated bool) 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.