scriptlayer

package
v1.123.0 Latest Latest
Warning

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

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

Documentation

Overview

Package scriptlayer is the MCP surface of the managed-script feature: the manage_script tool and everything it needs to resolve, authorize, edit, validate, and dry-run a script.

It owns the assembly (the Postgres-backed script store) and the tool, and it depends on pkg/script for the domain rules and internal/platform/scriptrun for the engine. Nothing here decides what Starlark means and nothing here re-implements the edit gate; both live one layer down, so the tool is a translation from MCP arguments into domain calls.

The MCP server is captured at RegisterTool rather than at construction: the store must exist early, while the server exists only once the platform has assembled it, and run_draft needs that server to open its in-memory session.

Index

Constants

View Source
const (
	// DefaultWaitSeconds is how long run_script waits for a run to finish when
	// the caller names no window.
	DefaultWaitSeconds = 120

	// MaxWaitSeconds caps the wait. Past it the tool answers with the run id and
	// a pending status rather than holding a request open for the ten minutes a
	// run is allowed to take.
	MaxWaitSeconds = 300
)

Waiting policy for run_script.

View Source
const DialectContract = `Managed scripts are written in Starlark: Python-shaped syntax, deliberately smaller.

WHAT IS AVAILABLE
  platform.query(sql, connection=..., params={})  Run read-only SQL. Returns
      {"columns": [...], "rows": [...], "row_count": n}; rows are dicts keyed by
      column name. A script cannot write: a statement that modifies state,
      such as INSERT, UPDATE, DELETE, CREATE or DROP, is refused. Compute the
      result with SELECT and write it with platform.export.
      Use :name placeholders and pass the values in params; the platform
      quotes them by type. Never build SQL by string concatenation.
      A date binds as a quoted string, so compare it against a DATE column as
      "DATE :day", which renders the standard date literal DATE '2026-08-12'.
      A query whose result is truncated by the row cap FAILS rather than
      returning a partial answer: aggregate in SQL, or narrow the query.
      A SQL DECIMAL column arrives in the rows as a STRING, not a number, so
      pass it through float() before arithmetic:
      sum([float(r["total"]) for r in rows]).
  platform.export(name, rows, format="csv", destination="portal", key=None)
      Declare an output. rows is a list of dicts serialized in the declared
      format, or a string body written verbatim so a script can compose a
      document: an HTML or JSX dashboard, a prose report, a hand-assembled
      markdown page. Formats: csv, json, markdown, text, html, jsx. csv and
      json require rows, so a data feed stays well-formed by construction;
      html and jsx take only a string body; markdown and text accept either.
      name is the output's identity across runs: the same name from the same
      script is one portal asset, and every run adds a version of it, so a
      dashboard keeps its identity instead of a new asset appearing every
      morning.
      destination says where the output goes. The default, "portal", is that
      versioned asset. A destination the deployment configures for a bucket
      delivers the same bytes to an external system instead; the script names
      only the destination, and the connection, bucket and prefix come from
      the configuration. Exporting one result to both is two calls with one
      name.
      key is the object key beneath a bucket destination's configured prefix
      ("2026/08/sales.csv"); it defaults to the output name plus the format's
      extension, and the portal takes no key because it stores its own objects.
      destination and key must be passed BY NAME. Only name, rows and format may
      be positional, because where a script writes has to be readable from its
      source.
      In a draft run this writes nothing, wherever it was addressed, and
      reports the shape and size the output would have: the content is
      serialized in the declared format to measure it, so the size is the one
      a real run writes.
  platform.publish_data(name, data)
      Refresh the data region of an existing dashboard without touching its
      markup. name is the same output identity platform.export uses, and must
      already be an html, jsx, or markdown document of this script's; data is
      a dict or list, serialized as JSON and spliced into the interior of the
      ONE element matching ` + script.DataRegionSelector + ` — conventionally
      <script type="application/json" id="data">...</script>, whose content
      the dashboard's own code reads and renders (a markdown document carries
      the island as a raw-HTML block). The write is a new version of the
      asset, so every refresh is a self-contained as-of snapshot; a document
      without the marked region fails the run rather than being written
      anywhere else. Publish the presentation once with
      platform.export(name, body, format="html") (or "jsx" or "markdown"),
      then let the schedule refresh only the numbers: the layout can be
      edited in the asset like any document, with no script change needed.
      Zero rows is your decision, as with any export: publish the empty
      structure or fail().
      In a draft run this writes nothing and reports the payload size it
      would splice.
  print(...)  Goes to the run log (capped; anything larger is an export).
  run.run_id, run.fire_time, run.params["name"]  The frozen run record.
      A parameter is typed string, int, float, bool, date, enum or connection.
      Declare a connection parameter for a connection the caller chooses rather
      than a string: the surfaces that ask for one offer the connections this
      script may reach, and a name outside them is refused where it was
      entered instead of failing the run.
  json.encode / json.decode / json.indent
  date.of, date.parse, date.format, date.add_days, date.add_months,
      date.diff_days, date.start_of_month, date.weekday  All dates are
      YYYY-MM-DD strings. date.format uses YYYY, MM and DD tokens.
  The Starlark built-ins: len, range, sorted, min, max, sum, enumerate, zip,
      str, int, float, dict, list, set, any, all, fail, and the string, list and
      dict methods (including "{}".format(x) and "%d" % x).

WHAT IS NOT, AND WHAT TO WRITE INSTEAD
  import              There is no module system. json and date are already here.
  try / except        Errors fail the run by design, so the failure is recorded
                      rather than swallowed. Check first, or call fail("why").
  while               Unbounded loops are off so a script's cost is readable
                      from its source. Loop over a list, or do it in SQL.
  recursion           Off, for the same reason. Flatten it into a loop.
  f"..."              Use "{}".format(x) or "%s" % x.
  class               Use dicts for structured values and functions for behavior.
  datetime / now()    There is no clock. Reading one would make the run
                      unreproducible; the fire time is pinned on run.fire_time.
  random              There is no randomness, for the same reason.
  open / requests     There is no filesystem and no network. The platform is the
                      only outside world a script has.
  credentials         Never in the source. Name a connection; the platform holds
                      its credentials and authorizes the call.

WHAT DETERMINISTIC MEANS HERE
  Same script version + same parameters + same underlying data produce the same
  output. The warehouse still changes between runs, and that is the point of
  re-running: the promise is that the SCRIPT contributes no variation of its own.

THE LOOP
  create -> validate -> run_draft -> patch -> validate -> run_draft. validate
  parses and reports the capabilities, connections and destinations the script
  reaches; run_draft executes it under your own identity with nothing persisted.`

DialectContract is the help command's body: what a script is, what is predeclared, and what a Python instinct will reach for and not find. It is exported for the built-in knowledge pages (#1390): the authoring page derives its dialect section from this constant, so the two cannot drift.

View Source
const ToolNameManageScript = "manage_script"

ToolNameManageScript is the MCP tool name of the script-management tool, exported for composition roots that bind UI apps to it.

View Source
const ToolNameRunScript = "run_script"

ToolNameRunScript is the MCP tool name of the platform-execution tool, exported for composition roots that bind UI apps to it.

View Source
const ToolNameShowScripts = "show_scripts"

ToolNameShowScripts is the MCP tool name of the presentation-only trigger that opens the portal's script pages for the human.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// DB backs the script store; nil leaves the store nil and manage_script
	// unregistered (there is nowhere to keep a script).
	DB *sql.DB
	// Store, when non-nil, is used directly instead of building a Postgres
	// store from DB. Production passes DB and leaves this nil.
	Store script.Store
	// Runs is the run queue run_script enqueues onto and the run history the
	// run commands read. nil leaves run_script unregistered, which is the
	// correct shape for a deployment that cannot execute scripts at all.
	Runs script.RunStore
	// AdminPersona is the persona name that grants authority over every
	// script; matched against the caller's persona in each command.
	AdminPersona string
	// PortalURL is the deployment's public portal address, used by show_scripts
	// to name where the script pages are. Empty leaves the tool registered and
	// linkless: a deployment that has not been told its own address cannot be
	// given one by guessing.
	PortalURL string
	// Destinations is the deployment's configured bucket destinations, which a
	// draft run resolves platform.export names against exactly as a platform
	// run does.
	Destinations []script.Destination
}

Config carries the resolved values the owner needs to assemble the script layer. The caller translates its own config into this shape so this package stays free of the platform's config types.

type Handle

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

Handle owns the assembled script layer. All accessors are nil-safe, so a deployment without a database holds a Handle that registers nothing.

func New

func New(cfg Config) *Handle

New assembles the script layer.

func (*Handle) IndexProducer added in v1.122.0

func (h *Handle) IndexProducer() *indexjobs.Producer

IndexProducer returns the write-path index-job producer behind the managed- script store, or nil on a deployment with no database. The composition root hands it to the index queue, which binds it once the scripts consumer is registered; until then, and forever where no worker runs, NotifyWrite is a no-op and the reconciler is the only route to the index.

func (*Handle) RegisterTool

func (h *Handle) RegisterTool(server *mcp.Server)

RegisterTool registers manage_script; where the deployment can execute saved scripts, run_script; and the presentation-only show_scripts. It also captures the server the two run paths open their in-memory sessions against. No-op on a nil Handle or a no-database deployment (there is nowhere to keep a script).

Jump to

Keyboard shortcuts

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