runtime

package
v0.40.1 Latest Latest
Warning

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

Go to latest
Published: Nov 13, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Index

Constants

View Source
const (
	RuntimeLibraryName        = "scriptling.runtime"
	RuntimeSyncLibraryName    = "scriptling.runtime.sync"
	RuntimeSandboxLibraryName = "scriptling.runtime.sandbox"
	RuntimeJSONRPCLibraryName = "scriptling.runtime.jsonrpc"
	RuntimePluginLibraryName  = "scriptling.runtime.plugin"
)

Library names for the runtime core and the sub-libraries in this package. scriptling.runtime.kv and scriptling.runtime.http live in the extlibs/runtime/kv and extlibs/runtime/http packages (kv.LibraryName, http.LibraryName).

View Source
const (
	RuntimeMCPLibraryName = "scriptling.runtime.mcp"

	// MCPRegistryVar is the environment variable name where the mcp.tool()
	// decorator records tool registrations. The folder scanner reads this after
	// evaluating a .py file to discover decorated tools.
	MCPRegistryVar = "__mcp_registry"
)

Variables

View Source
var JSONRPCErrorClass = &object.Class{
	Name: "JSONRPCError",
}

JSONRPCErrorClass is the class for error objects produced by runtime.jsonrpc.error(). The stdio server recognises instances of this class and emits a JSON-RPC error response instead of a result.

View Source
var JSONRPCSubLibrary = func() *object.Library {
	functions := map[string]*object.Builtin{
		"method": {
			Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
				if err := errors.MinArgs(args, 1); err != nil {
					return err
				}

				name, err := args[0].AsString()
				if err != nil {
					return err
				}

				if len(args) == 1 {
					return makeJSONRPCDecorator(ctx, func(ref string) {
						registerJSONRPCMethod(name, ref)
					})
				}

				handler, err := args[1].AsString()
				if err != nil {
					return err
				}

				RuntimeState.Lock()
				if RuntimeState.ServerStarted {
					fmt.Fprintf(os.Stderr, "warning: runtime.jsonrpc.method %q registered after start_server() — method will not be served\n", name)
				}
				RuntimeState.JSONRPCMethods[name] = handler
				RuntimeState.Unlock()

				return &object.Null{}
			},
			HelpText: `method(name, handler=None) - Register a JSON-RPC method handler, or use as decorator

Decorator form:
  import scriptling.jsonrpc as jsonrpc

  @jsonrpc.method("echo")
  def echo(params):
      return params

Imperative form:
  runtime.jsonrpc.method("echo", "handlers.echo")

The handler receives the decoded JSON-RPC params as its single argument and
returns a JSON-compatible result. Return runtime.jsonrpc.error(...) to produce
an error response.`,
		},

		"notification": {
			Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
				if err := errors.MinArgs(args, 1); err != nil {
					return err
				}

				name, err := args[0].AsString()
				if err != nil {
					return err
				}

				if len(args) == 1 {
					return makeJSONRPCDecorator(ctx, func(ref string) {
						registerJSONRPCNotification(name, ref)
					})
				}

				handler, err := args[1].AsString()
				if err != nil {
					return err
				}

				RuntimeState.Lock()
				if RuntimeState.ServerStarted {
					fmt.Fprintf(os.Stderr, "warning: runtime.jsonrpc.notification %q registered after start_server() — notification will not be served\n", name)
				}
				RuntimeState.JSONRPCNotifications[name] = handler
				RuntimeState.Unlock()

				return &object.Null{}
			},
			HelpText: `notification(name, handler=None) - Register a JSON-RPC notification handler, or use as decorator

Decorator form:
  @jsonrpc.notification("updated")
  def on_updated(params):
      ...

Imperative form:
  runtime.jsonrpc.notification("updated", "handlers.on_updated")

Notifications are JSON-RPC requests without an id. The handler receives the
decoded params and no response is written.`,
		},

		"error": {
			Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
				if err := errors.MinArgs(args, 2); err != nil {
					return err
				}

				code, err := args[0].AsInt()
				if err != nil {
					return err
				}

				message, err := args[1].AsString()
				if err != nil {
					return err
				}

				var data object.Object
				if len(args) >= 3 {
					data = args[2]
				}

				return CreateJSONRPCErrorInstance(code, message, data)
			},
			HelpText: `error(code, message, data=None) - Build a JSON-RPC error response

Parameters:
  code (int): JSON-RPC error code (e.g. -32602 for invalid params)
  message (str): Human-readable error message
  data (any, optional): Optional structured data attached to the error

Return this from a method handler to emit a JSON-RPC error response with a
custom code. If omitted the response uses the given code and message.

Example:
  def divide(params):
      if params["b"] == 0:
          return runtime.jsonrpc.error(-32602, "division by zero")
      return params["a"] / params["b"]`,
		},
	}

	for name, builtin := range reqctx.RequestContextBuiltins() {
		functions[name] = builtin
	}
	functions["transport"] = reqctx.NewTransportBuiltin(`transport() - How the JSON-RPC server is being served: "http", "stdio" or None

Lets one setup script work in every mode: over stdio the middleware never
runs, so anything middleware would gate per user must be handled differently
there.

  import scriptling.runtime as runtime

  if runtime.jsonrpc.transport() == "stdio":
      # No middleware over stdio: treat every caller alike.
      ...

Returns "http" when serving at POST /json-rpc (also from method handlers
mid-request), "stdio" for the --json-rpc stdio server, and None when the
script is not being served at all.`)

	return object.NewLibrary(RuntimeJSONRPCLibraryName, functions, map[string]object.Object{
		"JSONRPCError": JSONRPCErrorClass,
	}, "stdio JSON-RPC 2.0 server method and notification registration")
}()

JSONRPCSubLibrary exposes runtime.jsonrpc for registering stdio JSON-RPC 2.0 method and notification handlers. Handlers are referenced by string ("library.function") and run on a fresh evaluator per request, matching the runtime.http / MCP / WebSocket concurrency model. When served over HTTP the originating request is available to handlers via get_request() / request_context().

View Source
var MCPSubLibrary = func() *object.Library {
	functions := map[string]*object.Builtin{
		"tool": {
			Fn: mcpToolDecorator,
			HelpText: `tool(description, params=None, keywords=None, discoverable=False, ui=None, icons=None, title=None, read_only=None, destructive=None, idempotent=None, open_world=None) - Decorator for MCP tools

Decorates a function to register it as an MCP tool. The function's parameters
become the tool's input schema; the return value becomes the tool response.

Parameters:
  description (str): Tool description shown to the AI
  params (dict, optional): Parameter metadata keyed by name. Each value is either
    a string (the description; type inferred from default or defaults to "string")
    or a dict with keys "type", "description", and optional "required".
  keywords (list, optional): Keywords for tool search/discovery
  discoverable (bool, optional): If True, tool is hidden from tools/list and
    only available via search (default: False)
  ui (dict, optional): Links this tool to a companion UI resource per the MCP
    Apps extension (https://github.com/modelcontextprotocol/ext-apps). A dict
    with an optional "resourceUri" (str, the ui:// resource — omit it for an
    "app"-only action tool with no view of its own, such as a form submission
    that's only ever called by a view that's already open) and optional
    "visibility" (list of "model" and/or "app"; defaults to both). At least
    one of "resourceUri" or "visibility" is required if "ui" is given at all.
  icons (list, optional): Visual identifiers for this tool's tools/list
    descriptor. Each element is a dict with a required "src" (str, an
    https:// URL or data: URI) and optional "mimeType", "sizes" (list of
    strings like "48x48"), and "theme" ("light" or "dark").
  title (str, optional): Human-readable display name for the tool, shown by
    clients in place of the function name
  read_only (bool, optional): True if the tool does not modify its environment
  destructive (bool, optional): True if the tool may perform destructive
    updates (deleting or overwriting); False if it only adds
  idempotent (bool, optional): True if calling it again with the same
    arguments has no further effect
  open_world (bool, optional): True if the tool talks to external systems
    (the web, third-party APIs); False if it stays within a closed domain
  The four hints become the tool's annotations in tools/list. Leave one out
  (or pass None) and clients assume the spec default: not read-only, possibly
  destructive, not idempotent, open world. They tell clients how careful to
  be (e.g. confirm before a destructive call); they are not access control.

Returns:
  A decorator function that registers the tool and returns the original function.

Example:
  import scriptling.runtime.mcp as mcp

  @mcp.tool(
      description="Double a number",
      params={"n": {"type": "int", "description": "Number to double"}},
  )
  def calc(n):
      return f"{n} * 2 = {n * 2}"

  @mcp.tool(description="Greet someone", params={
      "name": "Name of the person",
      "times": {"type": "int", "description": "Number of greetings"},
  })
  def greet(name, times=1):
      return "\n".join(f"Hello, {name}!" for _ in range(times))

  @mcp.tool(description="Delete a note", title="Delete Note",
            destructive=True, idempotent=True, open_world=False)
  def delete_note(id):
      ...

  @mcp.tool(description="Get the sales report",
            ui={"resourceUri": "ui://sales-dashboard/dashboard.html"},
            icons=[{"src": "https://example.com/sales.png", "mimeType": "image/png"}])
  def sales_report():
      return {"records": [...]}

  @mcp.tool(description="Add a sale record (called by the dashboard's own form, not the model)",
            ui={"visibility": ["app"]})
  def add_sale(date, product, amount):
      return {"records": [...]}`,
		},
		"resource": {
			Fn: mcpResourceDecorator,
			HelpText: `resource(uri, name="", description="", mime_type="", template=False, complete=None) - Decorator for MCP resources

Decorates a function to register it as an MCP resource (or, with template=True,
a URI template like "user://docs/{path}"). For a static resource the function
takes no parameters; for a template its parameters are the URI's {var}
variables. A string return is the content; a dict/list return is JSON encoded.
The function runs on every resources/read, so content can change between reads.

Parameters:
  uri (str): Resource URI, or the URI template when template=True
  name (str, optional): Human-readable resource name (defaults to the URI)
  description (str, optional): Resource description
  mime_type (str, optional): Content type (default "text/plain", or
    "application/json" for dict/list results). Ignored for a "ui://" uri — the
    MCP Apps extension MUSTs that exact mimeType, so it's always set for you
  template (bool, optional): Treat uri as a {var} URI template (default: False)
  complete (dict, optional): Completers for completion/complete, keyed by
    template variable: {"path": complete_path}. Each value is a function in
    this file (or its name), or a "module.function" string. A completer is
    called as fn(argument, value, context) — the variable's name, the partial
    value typed so far, and a dict of the arguments already filled in — and
    returns a list of suggestion strings (the client gets at most 100).
    Templates only.

Returns:
  A decorator function that registers the resource and returns the original function.

Example:
  import scriptling.runtime.mcp as mcp

  @mcp.resource("config://app", name="App config", mime_type="application/json")
  def app_config():
      return {"version": "1.0", "debug": False}

  def complete_path(argument, value, context):
      return [p for p in ["intro", "install", "usage"] if p.startswith(value)]

  @mcp.resource("user://docs/{path}", template=True, mime_type="text/markdown",
                complete={"path": complete_path})
  def user_doc(path):
      return "# doc " + path`,
		},
		"prompt": {
			Fn: mcpPromptDecorator,
			HelpText: `prompt(description="", arguments=None, complete=None) - Decorator for MCP prompts

Decorates a function to register it as an MCP prompt under the function's own
name. The function's parameters become the prompt's arguments and are passed
on every prompts/get. A string return is a single user message; a dict with a
"messages" list of {"role": "user"|"assistant", "content": "..."} builds a
multi-message prompt.

Parameters:
  description (str, optional): Prompt description
  arguments (list, optional): Argument metadata dicts with "name",
    "description" and "required". Inferred from the function signature when
    omitted.
  complete (dict, optional): Completers for completion/complete, keyed by
    argument name: {"language": complete_language}. Each value is a function
    in this file (or its name), or a "module.function" string. A completer
    is called as fn(argument, value, context) — the argument's name, the
    partial value typed so far, and a dict of the arguments already filled
    in — and returns a list of suggestion strings (the client gets at most
    100). An exception becomes an error for that completion request.

Returns:
  A decorator function that registers the prompt and returns the original function.

Example:
  import scriptling.runtime.mcp as mcp

  @mcp.prompt(description="Summarise a document")
  def summarise(text):
      return "Summarise the following:\n\n" + text

  def complete_language(argument, value, context):
      return [l for l in ["go", "python", "rust"] if l.startswith(value)]

  @mcp.prompt(description="Review code", complete={"language": complete_language})
  def review(language, code):
      return {"messages": [
          {"role": "user", "content": "Review this " + language + " code:"},
          {"role": "assistant", "content": code},
      ]}`,
		},
		"skill": {
			Fn: mcpSkillDecorator,
			HelpText: `skill(files=None) - Decorator for MCP skills

Decorates a function to register it as an MCP skill (the Agent Skills format)
under the function's own name. The function takes no parameters and returns
the SKILL.md content, including its YAML frontmatter; the frontmatter's name
must match the function name and its description seeds the skill's listing,
exactly as for a SKILL.md file in the skills folder. The function runs once
when the server starts (skills are static content; they do not reload).

Parameters:
  files (dict, optional): Supporting files mapping file name to content
    string, served alongside SKILL.md as skill://<name>/<file>

Returns:
  A decorator function that registers the skill and returns the original function.

Example:
  import scriptling.runtime.mcp as mcp

  @mcp.skill(files={"regions.md": "eu-west: Europe\n"})
  def region-guide():
      return """---
  name: region-guide
  description: How to identify a region and set it
  ---

  # Region guide

  Read regions.md for the list of regions.
  """`,
		},
	}

	for name, builtin := range requestRegistrationBuiltins() {
		functions[name] = builtin
	}
	functions["notify_resource_updated"] = notifyResourceUpdatedBuiltin()
	functions["transport"] = reqctx.NewTransportBuiltin(`transport() - How the MCP server is being served: "http", "stdio" or None

Lets one setup script work in every mode: over stdio the middleware never
runs, so registrations that middleware would gate per user must be made
unconditionally instead.

  import scriptling.runtime.mcp as mcp

  if mcp.transport() == "stdio":
      # No middleware over stdio: expose the extra tools to everyone.
      ...

Returns "http" when serving over HTTP (also from middleware and tool handlers
mid-request), "stdio" for the MCP stdio server, and None when the script is
not being served at all.`)

	return object.NewLibrary(RuntimeMCPLibraryName, functions, nil, "MCP tool, resource, and prompt registration via decorators")
}()

MCPSubLibrary is the scriptling.runtime.mcp sub-library. It provides decorator functions for defining MCP tools (and in future, resources and prompts) from script code, plus the register_request_* functions middleware uses to expose entries for the life of a single request. Decorator registrations are recorded per-interpreter in __mcp_registry; request registrations in the per-request accumulator carried on the context.

View Source
var PluginSubLibrary = object.NewLibrary(RuntimePluginLibraryName, map[string]*object.Builtin{

	"serve": {
		Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
			if err := errors.MinArgs(args, 1); err != nil {
				return err
			}

			name, err := args[0].AsString()
			if err != nil {
				return err
			}

			version := ""
			if len(args) >= 2 {
				v, e := args[1].AsString()
				if e != nil {
					return e
				}
				version = v
			}
			description := ""
			if len(args) >= 3 {
				d, e := args[2].AsString()
				if e != nil {
					return e
				}
				description = d
			}

			// Optional opaque host-defined manifest data: metadata={...}. A
			// host reads it verbatim from the plugin handshake to learn
			// plugin-specific declarations without running plugin code.
			var metadata map[string]any
			if mObj := kwargs.Get("metadata"); mObj != nil {
				dict, ok := mObj.(*object.Dict)
				if !ok {
					return errors.NewError("serve: metadata must be a dict")
				}
				metadata = make(map[string]any, len(dict.Pairs))
				for _, pair := range dict.Pairs {
					key, e := pair.Key.AsString()
					if e != nil {
						return errors.NewError("serve: metadata keys must be strings")
					}
					metadata[key] = pluginMetadataValue(pair.Value)
				}
			}

			RuntimeState.Lock()
			if RuntimeState.ServerStarted {
				fmt.Fprintf(os.Stderr, "warning: runtime.plugin.serve() called after start_server() — plugin identity will not be used\n")
			}
			RuntimeState.PluginName = name
			RuntimeState.PluginVersion = version
			RuntimeState.PluginDescription = description
			RuntimeState.PluginMetadata = metadata
			RuntimeState.Unlock()

			return &object.Null{}
		},
		HelpText: `serve(name, version="", description="") - Declare this script as a plugin server

When runtime.start_server() is called in stdio mode the server serves the full
Scriptling plugin protocol (scriptling.handshake, function.call, etc.) instead
of the plain JSON-RPC loop. Clients can then load the script as a plugin peer
with scriptling=True and get auto-generated proxy libraries.

Parameters:
  name (str):        Library name (e.g. "myservice"). Clients import it as plugin.<name>.
  version (str):     Optional version string (e.g. "1.0.0").
  description (str): Optional human-readable description.
  metadata (dict):   Optional opaque host-defined manifest data, carried verbatim
                     in the plugin handshake. Scriptling never interprets it; a
                     host reads it to learn plugin-specific declarations.

Example:
  import scriptling.runtime.plugin as plugin_srv

  plugin_srv.serve("calculator", "1.0", "Basic arithmetic operations")
  plugin_srv.register_function("add", "handlers.add")
  import scriptling.runtime as runtime
  runtime.start_server()`,
	},

	"register_function": {
		Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
			if err := errors.MinArgs(args, 1); err != nil {
				return err
			}

			if fn, ok := args[0].(*object.Function); ok {
				env := evaluator.GetEnvFromContext(ctx)
				ref := libutil.ModuleRef(env, fn.Name)
				if ref == "" {
					return errors.NewError("cannot determine module name for @plugin.register_function decorator")
				}
				RuntimeState.Lock()
				RuntimeState.PluginFunctions[fn.Name] = ref
				RuntimeState.Unlock()
				return fn
			}

			name, err := args[0].AsString()
			if err != nil {
				return err
			}

			if len(args) == 1 {
				env := evaluator.GetEnvFromContext(ctx)
				return &object.Builtin{
					Fn: func(_ context.Context, _ object.Kwargs, dArgs ...object.Object) object.Object {
						if len(dArgs) == 0 {
							return errors.NewError("decorator requires a function")
						}
						fn, ok := dArgs[0].(*object.Function)
						if !ok {
							return errors.NewError("decorated value must be a function, got %s", dArgs[0].Type())
						}
						ref := libutil.ModuleRef(env, fn.Name)
						if ref == "" {
							return errors.NewError("cannot determine module name for decorator")
						}
						RuntimeState.Lock()
						RuntimeState.PluginFunctions[name] = ref
						RuntimeState.Unlock()
						return fn
					},
				}
			}

			handler, err := args[1].AsString()
			if err != nil {
				return err
			}

			RuntimeState.Lock()
			if RuntimeState.ServerStarted {
				fmt.Fprintf(os.Stderr, "warning: runtime.plugin.register_function %q registered after start_server() — function will not be served\n", name)
			}
			RuntimeState.PluginFunctions[name] = handler
			RuntimeState.Unlock()

			return &object.Null{}
		},
		HelpText: `register_function(name, handler=None) - Register a function for the plugin server, or use as decorator

Decorator form:
  import scriptling.runtime.plugin as plugin

  @plugin.register_function("add")
  def add(a, b):
      return a + b

  # Bare form uses the function name as the plugin name
  @plugin.register_function
  def greet(name):
      return "hello " + name

Imperative form:
  plugin.register_function("add", "handlers.add")`,
	},

	"register_fetcher": {
		Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
			if err := errors.MinArgs(args, 2); err != nil {
				return err
			}

			scheme, err := args[0].AsString()
			if err != nil {
				return err
			}
			read, err := args[1].AsString()
			if err != nil {
				return err
			}
			glob := ""
			if len(args) >= 3 {
				glob, err = args[2].AsString()
				if err != nil {
					return err
				}
			}

			if scheme == "" || scheme == "http" || scheme == "https" || scheme == "file" {
				return errors.NewError("register_fetcher: scheme must not be empty or a built-in (http, https, file)")
			}
			for i, r := range scheme {
				switch {
				case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z':
				case i > 0 && (r >= '0' && r <= '9' || r == '+' || r == '-' || r == '.'):
				default:
					return errors.NewError("register_fetcher: invalid scheme %q", scheme)
				}
			}

			RuntimeState.Lock()
			if RuntimeState.ServerStarted {
				fmt.Fprintf(os.Stderr, "warning: runtime.plugin.register_fetcher called after start_server() — fetcher will not be served\n")
			}
			RuntimeState.PluginFetchScheme = scheme
			RuntimeState.PluginFetchRead = read
			RuntimeState.PluginFetchGlob = glob
			RuntimeState.Unlock()

			return &object.Null{}
		},
		HelpText: `register_fetcher(scheme, read_handler, glob_handler=None) - Serve sources from this plugin server

Registers a fetcher so the host can ask this peer for files on demand — how a
script peer serves a host's declared assets (an icon, a logo) from strings or
bytes inside the script itself, the scriptling equivalent of a Go peer's
embedded assets. The host calls the handlers with the full source string and
a slash path relative to it.

Parameters:
  scheme (str):        The source scheme to serve, e.g. "notes" (the host asks
                       for notes://<path>). Must not be http, https or file.
  read_handler (str):  A "library.function" ref called as fn(source, path).
                       Return the file's contents (string or bytes); return
                       None for a miss (not found); any other error fails the
                       read.
  glob_handler (str):  Optional "library.function" ref called as fn(source,
                       pattern); return a list of {name, is_dir} dicts.
                       Without it the fetcher reports no glob matches.

Example:
  import scriptling.runtime.plugin as plugin_srv

  plugin_srv.register_fetcher("notes", "impl.fetch_read")`,
	},

	"register_constant": {
		Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
			if err := errors.MinArgs(args, 2); err != nil {
				return err
			}

			name, err := args[0].AsString()
			if err != nil {
				return err
			}

			RuntimeState.Lock()
			if RuntimeState.ServerStarted {
				fmt.Fprintf(os.Stderr, "warning: runtime.plugin.register_constant %q registered after start_server() — constant will not be served\n", name)
			}
			RuntimeState.PluginConstants[name] = args[1]
			RuntimeState.Unlock()

			return &object.Null{}
		},
		HelpText: `register_constant(name, value) - Register a constant exported by the plugin server

Parameters:
  name (str):  Constant name exposed to plugin clients.
  value (any): Value — any type that the plugin transport can encode (bool, int,
               float, string, list, dict, None).

Constants are included in the handshake schema so clients can read them
directly as attributes of the plugin library.

Example:
  import scriptling.runtime.plugin as plugin_srv

  plugin_srv.register_constant("VERSION", "1.0.0")
  plugin_srv.register_constant("MAX_RETRIES", 5)`,
	},

	"register_class": {
		Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
			if err := errors.MinArgs(args, 1); err != nil {
				return err
			}

			if cls, ok := args[0].(*object.Class); ok {
				env := evaluator.GetEnvFromContext(ctx)
				ref := libutil.ModuleRef(env, cls.Name)
				if ref == "" {
					return errors.NewError("cannot determine module name for @plugin.register_class decorator")
				}
				RuntimeState.Lock()
				RuntimeState.PluginClasses[cls.Name] = ref
				RuntimeState.Unlock()
				return cls
			}

			handler, err := args[0].AsString()
			if err != nil {
				return err
			}

			name := handler
			for i := len(handler) - 1; i >= 0; i-- {
				if handler[i] == '.' {
					name = handler[i+1:]
					break
				}
			}

			RuntimeState.Lock()
			if RuntimeState.ServerStarted {
				fmt.Fprintf(os.Stderr, "warning: runtime.plugin.register_class %q registered after start_server() — class will not be served\n", name)
			}
			RuntimeState.PluginClasses[name] = handler
			RuntimeState.Unlock()

			return &object.Null{}
		},
		HelpText: `register_class(handler) - Register a class exported by the plugin server, or use as bare decorator

Decorator form:
  import scriptling.runtime.plugin as plugin

  @plugin.register_class
  class Config:
      def __init__(self):
          self.version = "1.0"

Imperative form:
  plugin.register_class("handlers.Config")

The exposed class name is derived from the class name (decorator) or the last
segment of the handler ref (imperative).`,
	},
}, nil, "Scriptling plugin server — declare this script as a plugin peer with full handshake support")

PluginSubLibrary exposes runtime.plugin for declaring a script as a Scriptling plugin server. When start_server() is called the CLI replaces the plain JSON-RPC loop with a full plugin.Server that handles the plugin handshake, function.call, and object lifecycle. Available in the agent variant of scriptling only.

View Source
var RuntimeLibraryFunctions = map[string]*object.Builtin{
	"background": {
		Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
			if err := errors.MinArgs(args, 2); err != nil {
				return err
			}

			name, err := args[0].AsString()
			if err != nil {
				return err
			}

			handler, err := args[1].AsString()
			if err != nil {
				return err
			}

			env := libutil.EnvFromContext(ctx)
			eval := evaliface.FromContext(ctx)

			shared := false
			if v := kwargs.Get("shared"); v != nil {
				if b, e := v.AsBool(); e == nil {
					shared = b
				}
			}

			daemon := false
			if v := kwargs.Get("daemon"); v != nil {
				if b, e := v.AsBool(); e == nil {
					daemon = b
				}
			}

			handlerKwargs := make(map[string]object.Object, len(kwargs.Kwargs))
			for k, v := range kwargs.Kwargs {
				if k == "shared" || k == "daemon" {
					continue
				}
				handlerKwargs[k] = v
			}
			kwargs = object.NewKwargs(handlerKwargs)

			promise, claimed := claimTaskName(name)
			if !claimed {
				return promiseObject(promise)
			}

			handedOff := false
			defer func() {
				if !handedOff {
					releaseTaskName(promise)
				}
			}()

			end, taskErr := env.ResourceGuard().StartTask()
			if taskErr != nil {
				if goerrors.Is(taskErr, object.ErrMemoryLimitExceeded) {
					return errors.NewMemoryLimitStopError()
				}
				return &object.Error{Message: taskErr.Error(), ExceptionType: object.ExceptionTypeRuntimeError}
			}
			promise.endTask = end

			if shared {
				handedOff = true

				liveArgs := make([]object.Object, len(args)-2)
				copy(liveArgs, args[2:])
				return startSharedTask(ctx, handler, liveArgs, kwargs.Kwargs, env, eval, daemon, promise)
			}

			for i, a := range args[2:] {
				if err := object.ValidateTransferable(a); err != nil {
					return errors.NewError("background arg %d: %s", i, err)
				}
			}
			for k, v := range kwargs.Kwargs {
				if err := object.ValidateTransferable(v); err != nil {
					return errors.NewError("background kwarg '%s': %s", k, err)
				}
			}

			fnArgs := make([]object.Object, len(args[2:]))
			for i, a := range args[2:] {
				fnArgs[i] = object.CloneObject(a)
			}
			fnKwargs := make(map[string]object.Object, len(kwargs.Kwargs))
			for k, v := range kwargs.Kwargs {
				fnKwargs[k] = object.CloneObject(v)
			}

			RuntimeState.Lock()
			backgroundReady := RuntimeState.BackgroundReady
			factory := RuntimeState.BackgroundFactory
			if !backgroundReady && factory != nil {
				RuntimeState.Backgrounds[name] = handler
				RuntimeState.BackgroundArgs[name] = fnArgs
				RuntimeState.BackgroundKwargs[name] = fnKwargs
				RuntimeState.BackgroundEnvs[name] = env
				RuntimeState.BackgroundEvals[name] = eval
				RuntimeState.BackgroundCtxs[name] = ctx
				RuntimeState.BackgroundDaemons[name] = daemon
				RuntimeState.BackgroundPromises[name] = promise
			}
			RuntimeState.Unlock()

			if backgroundReady || factory == nil {
				handedOff = true
				return startBackgroundTask(promise, handler, fnArgs, fnKwargs, env, eval, factory, ctx, daemon)
			}

			handedOff = true
			return promiseObject(promise)
		},
		HelpText: `background(name, handler, *args, **kwargs) - Start a fire-and-forget background task

Starts a background task in a goroutine and returns immediately.
Returns a promise (call .get() or .wait() to await the result), or
an error if the handler is not found. In server mode the task is
queued until the server starts; the promise resolves once it runs.

  Both handler patterns run in isolated environments with no
  access to the calling script's data. Sibling functions,
  imported modules, and module-level constants (int, float,
  bool, str) are copied; other module-level variables (lists,
  dicts, objects, logger instances) are NOT visible — pass
  them as arguments or via runtime.sync. A task that fails is
  reported to stderr with its name.

  The name identifies the task: calling background() with the
  name of a task that is already queued or running starts
  nothing and returns the running task's promise. This keeps a
  module that registers both a request handler and a task from
  spawning another copy each time it is re-imported to serve a
  request. Once a task ends its name is free to reuse.

Parameters:
  name (string): Unique name for the background task
  handler (string): Function name or "library.function"
    "func_name" - runs in isolated env with import support and sibling functions
    "lib.func" - loads library.function in a new Scriptling instance
  *args: Positional arguments to pass to the function
  **kwargs: Keyword arguments to pass to the function
  shared (bool): Run in the caller's environment with live shared state
    instead of an isolated copy (default False)
  daemon (bool): Do not wait for this task at script exit
    (default False)

Arguments must be transferable types — only simple values and
recursively transferable containers are allowed:
  - Scalars: None, bool, int, float, str
  - Containers: list, dict, set, tuple (elements must also be
    transferable)
  - Not allowed: instances, classes, functions, builtins, or any
    other runtime-backed objects
Arguments are deep-copied before the task starts so the caller and
task cannot race on shared state.

Returns:
  Promise: wait() blocks until the task finishes and get() returns its
  result; starting a task under a name that is still live returns that
  task's promise. Raises RuntimeError at --max-background-tasks, and an
  error if handler validation fails.

  Background tasks are fire-and-forget. For coordination between
  tasks use runtime.sync primitives (Shared, Atomic, Queue, WaitGroup).
  Access panels via console.Console().panel("name") from background tasks.

  The scriptling CLI waits for outstanding background tasks before
  exiting, so a finishing script does not kill them mid-flight and
  their output and logging are not lost. Pass daemon=True for
  long-running tasks that must not hold the process open.

`,
	},

	"start_server": {
		Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
			wait := true
			if v := kwargs.Get("wait"); v != nil {
				if b, e := v.AsBool(); e == nil {
					wait = b
				}
			}

			RuntimeState.Lock()
			if staleServerContext(ctx) {

				RuntimeState.Unlock()
				return &object.Null{}
			}
			if !RuntimeState.ServerStarted && RuntimeState.ServerStartCh != nil {
				RuntimeState.ServerStarted = true
				if RuntimeState.ServerCollect != nil {
					RuntimeState.ServerCollect()
				}
				close(RuntimeState.ServerStartCh)
			}
			RuntimeState.Unlock()

			if wait {
				object.RunBlocking(ctx, func() {
					RuntimeState.RLock()
					ch := RuntimeState.ServerRunningCh
					RuntimeState.RUnlock()
					if ch != nil {
						<-ch
					}
				})
			}
			return &object.Null{}
		},
		HelpText: `start_server(wait=True) - Signal the server to start accepting requests

Signals the server to collect registered routes/methods and begin
listening for requests. Call this after all routes are registered.

Parameters:
  wait (bool, default True): If True, blocks until the server shuts
    down. If False, returns immediately so the script can continue
    running (e.g. to maintain gossip state or run a polling loop).

When wait=True the call blocks until the server receives a shutdown
signal (SIGTERM / Ctrl-C). Use wait=False combined with a
server_running() loop to stay alive while performing other work:

  runtime.start_server(wait=False)
  while runtime.server_running():
      yield_now()

Backward compatibility: scripts that exit without calling
start_server() continue to work — the server starts automatically
after the setup script finishes.

`,
	},

	"server_running": {
		Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
			RuntimeState.RLock()
			ch := RuntimeState.ServerRunningCh
			stale := staleServerContext(ctx)
			RuntimeState.RUnlock()
			if ch == nil || stale {
				return object.NewBoolean(false)
			}
			select {
			case <-ch:
				return object.NewBoolean(false)
			default:
				return object.NewBoolean(true)
			}
		},
		HelpText: `server_running() - Returns True while the server is running

Returns True as long as the server has not received a shutdown signal.
Returns False once the server is shutting down or if called outside
of server mode.

Typical usage with start_server(wait=False):

  runtime.start_server(wait=False)
  while runtime.server_running():
      yield_now()       # release GIL on each iteration

`,
	},
}

RuntimeLibraryFunctions contains the core runtime functions (background)

View Source
var RuntimeState = struct {
	sync.RWMutex

	// JSON-RPC stdio methods and notifications (name -> "library.function")
	JSONRPCMethods       map[string]string
	JSONRPCNotifications map[string]string

	// Background tasks
	Backgrounds        map[string]string                   // name -> "function_name"
	BackgroundArgs     map[string][]object.Object          // name -> args
	BackgroundKwargs   map[string]map[string]object.Object // name -> kwargs
	BackgroundEnvs     map[string]*object.Environment      // name -> environment
	BackgroundEvals    map[string]evaliface.Evaluator      // name -> evaluator
	BackgroundFactory  SandboxFactory                      // Factory to create new Scriptling instances
	BackgroundCtxs     map[string]context.Context          // name -> context
	BackgroundReady    bool                                // If true, start tasks immediately
	BackgroundDaemons  map[string]bool                     // name -> daemon flag (queued tasks)
	BackgroundPromises map[string]*Promise                 // name -> promise returned to the setup script (queued tasks)

	// ActiveTasks holds the promise of every background task that is currently
	// queued or running, keyed by the task name. A task name identifies one
	// task, so background() with a name that is already active starts nothing
	// and hands back the running task's promise. The entry is dropped when the
	// task ends, so a name can be reused for a later task.
	//
	// This matters because a module body is re-executed whenever one of its
	// functions is imported to serve a request: without this, a setup script
	// that registers both a route handler and a background task in the same
	// file would spawn a fresh copy of that task on every request.
	ActiveTasks map[string]*Promise // name -> promise (queued or running)

	// TaskWG tracks outstanding non-daemon background tasks. Hosts that run
	// scripts to completion (the CLI's plain-run modes) wait on it before
	// exiting so fire-and-forget tasks are not killed mid-flight. It is
	// deliberately NOT reset by ResetRuntime: a WaitGroup's counter cannot be
	// reset, and Done() calls from still-running tasks of an earlier run must
	// land on the same counter or they would panic.
	TaskWG sync.WaitGroup

	// Sync primitives
	WaitGroups map[string]*RuntimeWaitGroup
	Queues     map[string]*RuntimeQueue
	Atomics    map[string]*RuntimeAtomic
	Shareds    map[string]*RuntimeShared

	// Server lifecycle channels (nil in script mode)
	ServerStartCh   chan struct{} // closed by start_server() to signal server is ready
	ServerRunningCh chan struct{} // closed by server on shutdown
	ServerStarted   bool          // prevents double-close of ServerStartCh
	ServerCollect   func()        // set by NewServer; called inside start_server() to snapshot routes atomically

	// Plugin server registration (set via runtime.plugin, agent variant only)
	PluginName        string
	PluginVersion     string
	PluginDescription string
	PluginMetadata    map[string]any           // opaque host-defined manifest data (runtime.plugin.serve metadata=)
	PluginFunctions   map[string]string        // function name → "library.function" handler
	PluginConstants   map[string]object.Object // constant name → value
	PluginClasses     map[string]string        // exposed class name → "library.ClassName" handler
	// Plugin fetcher registration (runtime.plugin.register_fetcher): the
	// scheme the peer serves sources under, plus the "library.function"
	// handlers answering read (required) and glob (optional; empty serves
	// no matches). Set together, before start_server.
	PluginFetchScheme string
	PluginFetchRead   string
	PluginFetchGlob   string
}{
	JSONRPCMethods:       make(map[string]string),
	JSONRPCNotifications: make(map[string]string),
	Backgrounds:          make(map[string]string),
	BackgroundArgs:       make(map[string][]object.Object),
	BackgroundKwargs:     make(map[string]map[string]object.Object),
	BackgroundEnvs:       make(map[string]*object.Environment),
	BackgroundEvals:      make(map[string]evaliface.Evaluator),
	BackgroundFactory:    nil,
	BackgroundCtxs:       make(map[string]context.Context),
	BackgroundReady:      false,
	BackgroundDaemons:    make(map[string]bool),
	BackgroundPromises:   make(map[string]*Promise),
	ActiveTasks:          make(map[string]*Promise),
	WaitGroups:           make(map[string]*RuntimeWaitGroup),
	Queues:               make(map[string]*RuntimeQueue),
	Atomics:              make(map[string]*RuntimeAtomic),
	Shareds:              make(map[string]*RuntimeShared),
	ServerStartCh:        nil,
	ServerRunningCh:      nil,
	ServerStarted:        false,
	PluginFunctions:      make(map[string]string),
	PluginConstants:      make(map[string]object.Object),
	PluginClasses:        make(map[string]string),
}

RuntimeState holds all runtime state

View Source
var SyncSubLibrary = object.NewLibrary(RuntimeSyncLibraryName, map[string]*object.Builtin{
	"WaitGroup": {
		Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
			if err := errors.MinArgs(args, 1); err != nil {
				return err
			}

			name, err := args[0].AsString()
			if err != nil {
				return err
			}

			RuntimeState.Lock()
			wg, exists := RuntimeState.WaitGroups[name]
			if !exists {
				wg = &RuntimeWaitGroup{}
				RuntimeState.WaitGroups[name] = wg
			}
			RuntimeState.Unlock()

			return &object.Builtin{
				Attributes: map[string]object.Object{
					"add": &object.Builtin{
						Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
							delta := int64(1)
							if len(args) > 0 {
								if d, err := args[0].AsInt(); err == nil {
									delta = d
								}
							}
							wg.wg.Add(int(delta))
							return &object.Null{}
						},
						HelpText: "add(delta=1) - Add to the wait group counter",
					},
					"done": &object.Builtin{
						Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
							wg.wg.Done()
							return &object.Null{}
						},
						HelpText: "done() - Decrement the wait group counter",
					},
					"wait": &object.Builtin{
						Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {

							object.RunBlocking(ctx, func() { wg.wg.Wait() })
							return &object.Null{}
						},
						HelpText: "wait() - Block until counter reaches zero",
					},
				},
				HelpText: "WaitGroup - Go-style synchronization primitive",
			}
		},
		HelpText: `WaitGroup(name) - Get or create a named wait group

Parameters:
  name (string): Unique name for the wait group (shared across environments)

Example:
    wg = runtime.sync.WaitGroup("tasks")

    def worker(id):
        print(f"Worker {id}")
        wg.done()

    for i in range(10):
        wg.add(1)
        runtime.run(worker, i)

    wg.wait()`,
	},

	"Queue": {
		Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
			if err := errors.MinArgs(args, 1); err != nil {
				return err
			}

			name, err := args[0].AsString()
			if err != nil {
				return err
			}

			maxsize := 0
			if len(args) > 1 {
				if m, err := args[1].AsInt(); err == nil {
					maxsize = int(m)
				}
			}
			if m, ok := kwargs.Kwargs["maxsize"]; ok {
				if mInt, err := m.AsInt(); err == nil {
					maxsize = int(mInt)
				}
			}

			RuntimeState.Lock()
			queue, exists := RuntimeState.Queues[name]
			if !exists {
				queue = newRuntimeQueue(maxsize)
				RuntimeState.Queues[name] = queue
			}
			RuntimeState.Unlock()

			return &object.Builtin{
				Attributes: map[string]object.Object{
					"put": &object.Builtin{
						Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
							if err := errors.ExactArgs(args, 1); err != nil {
								return err
							}
							if err := queue.put(ctx, args[0]); err != nil {
								return errors.NewError("queue error: %v", err)
							}
							return &object.Null{}
						},
						HelpText: "put(item) - Add item to queue (blocks if full, respects context timeout)",
					},
					"get": &object.Builtin{
						Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
							item, err := queue.get(ctx)
							if err != nil {
								return errors.NewError("queue error: %v", err)
							}
							return item
						},
						HelpText: "get() - Remove and return item from queue (blocks if empty, respects context timeout)",
					},
					"size": &object.Builtin{
						Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
							return object.NewInteger(int64(queue.size()))
						},
						HelpText: "size() - Return number of items in queue",
					},
					"close": &object.Builtin{
						Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
							queue.close()
							return &object.Null{}
						},
						HelpText: "close() - Close the queue",
					},
				},
				HelpText: "Queue - Thread-safe queue for producer-consumer patterns",
			}
		},
		HelpText: `Queue(name, maxsize=0) - Get or create a named queue

Parameters:
  name (string): Unique name for the queue (shared across environments)
  maxsize (int): Maximum queue size (0 = unbounded)

Example:
    queue = runtime.sync.Queue("jobs", maxsize=100)

    def producer():
        for i in range(10):
            queue.put(i)

    def consumer():
        for i in range(10):
            item = queue.get()
            print(item)

    runtime.run(producer)
    runtime.run(consumer)`,
	},

	"Atomic": {
		Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
			if err := errors.MinArgs(args, 1); err != nil {
				return err
			}

			name, err := args[0].AsString()
			if err != nil {
				return err
			}

			initial := int64(0)
			if len(args) > 1 {
				if i, err := args[1].AsInt(); err == nil {
					initial = i
				}
			}
			if i := kwargs.Get("initial"); i != nil {
				if iVal, err := i.AsInt(); err == nil {
					initial = iVal
				}
			}

			RuntimeState.Lock()
			atomic, exists := RuntimeState.Atomics[name]
			if !exists {
				atomic = &RuntimeAtomic{value: initial}
				RuntimeState.Atomics[name] = atomic
			}
			RuntimeState.Unlock()

			return &object.Builtin{
				Attributes: map[string]object.Object{
					"add": &object.Builtin{
						Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
							delta := int64(1)
							if len(args) > 0 {
								if d, err := args[0].AsInt(); err == nil {
									delta = d
								} else {
									return errors.NewTypeError("INTEGER", args[0].Type().String())
								}
							}
							newVal := atomic.add(delta)
							return object.NewInteger(newVal)
						},
						HelpText: "add(delta=1) - Atomically add delta and return new value",
					},
					"get": &object.Builtin{
						Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
							return object.NewInteger(atomic.get())
						},
						HelpText: "get() - Atomically read the value",
					},
					"set": &object.Builtin{
						Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
							if err := errors.ExactArgs(args, 1); err != nil {
								return err
							}
							if val, err := args[0].AsInt(); err == nil {
								atomic.set(val)
								return &object.Null{}
							}
							return errors.NewTypeError("INTEGER", args[0].Type().String())
						},
						HelpText: "set(value) - Atomically set the value",
					},
				},
				HelpText: "Atomic integer - lock-free operations",
			}
		},
		HelpText: `Atomic(name, initial=0) - Get or create a named atomic counter

Parameters:
  name (string): Unique name for the counter (shared across environments)
  initial (int): Initial value (only used if creating new counter)

Example:
    counter = runtime.sync.Atomic("requests", initial=0)
    counter.add(1)      # Atomic increment
    counter.add(-5)     # Atomic add
    counter.set(100)    # Atomic set
    value = counter.get()  # Atomic read`,
	},

	"Shared": {
		Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
			if err := errors.MinArgs(args, 1); err != nil {
				return err
			}

			name, err := args[0].AsString()
			if err != nil {
				return err
			}

			var initial object.Object = &object.Null{}
			if len(args) > 1 {
				initial = args[1]
			}
			if i := kwargs.Get("initial"); i != nil {
				initial = i
			}

			RuntimeState.Lock()
			shared, exists := RuntimeState.Shareds[name]
			if !exists {
				shared = &RuntimeShared{value: initial}
				RuntimeState.Shareds[name] = shared
			}
			RuntimeState.Unlock()

			return &object.Builtin{
				Attributes: map[string]object.Object{
					"get": &object.Builtin{
						Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
							return shared.get()
						},
						HelpText: "get() - Get the current value (thread-safe read)",
					},
					"set": &object.Builtin{
						Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
							if err := errors.ExactArgs(args, 1); err != nil {
								return err
							}
							shared.set(args[0])
							return &object.Null{}
						},
						HelpText: "set(value) - Set the value (thread-safe write)",
					},
					"update": &object.Builtin{
						Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
							if err := errors.ExactArgs(args, 1); err != nil {
								return err
							}
							fn := args[0]
							result := shared.update(func(current object.Object) object.Object {
								eval := evaliface.FromContext(ctx)
								if eval == nil {
									return current
								}
								env := libutil.EnvFromContext(ctx)
								return eval.CallObjectFunction(ctx, fn, []object.Object{current}, nil, env)
							})
							return result
						},
						HelpText: "update(fn) - Atomically read-modify-write: fn receives current value, returns new value",
					},
				},
				HelpText: "Shared variable - thread-safe access with get()/set()/update()",
			}
		},
		HelpText: `Shared(name, initial) - Get or create a named shared variable

Parameters:
  name (string): Unique name for the variable (shared across environments)
  initial: Initial value (only used if creating new variable)

Note: Values should be treated as immutable. Use set() to replace, or
update() for atomic read-modify-write operations.

Example:
    counter = runtime.sync.Shared("counter", 0)

    def increment(current):
        return current + 1

    # Atomic increment using update()
    counter.update(increment)

    # Simple get/set for immutable values
    counter.set(42)
    value = counter.get()`,
	},
}, nil, "Cross-environment named concurrency primitives")

Functions

func CreateJSONRPCErrorInstance

func CreateJSONRPCErrorInstance(code int64, message string, data object.Object) *object.Instance

CreateJSONRPCErrorInstance creates a JSON-RPC error object from Go code.

func IsJSONRPCError

func IsJSONRPCError(obj object.Object) bool

IsJSONRPCError reports whether obj is a JSONRPCError instance.

func NewSandboxLibrary

func NewSandboxLibrary(allowedPaths []string) *object.Library

NewSandboxLibrary creates a new sandbox library with the given allowed paths. If allowedPaths is nil, all paths are allowed (no restrictions). If allowedPaths is empty slice, no paths are allowed (deny all).

func Register

func Register(registrar object.LibraryRegistrar)

Register registers the scriptling.runtime library: background tasks and the server lifecycle (start_server, server_running). Sub-libraries need their own Register call (RegisterSync, RegisterSandbox, RegisterJSONRPC, RegisterMCP, RegisterPlugin, kv.Register, http.Register); each one registered with the same registrar, before or after this call, is also reachable as an attribute of scriptling.runtime when the registrar can look libraries up (scriptling.Scriptling can).

func RegisterJSONRPC

func RegisterJSONRPC(registrar object.LibraryRegistrar)

RegisterJSONRPC registers the scriptling.runtime.jsonrpc sub-library.

func RegisterMCP

func RegisterMCP(registrar object.LibraryRegistrar)

RegisterMCP registers the scriptling.runtime.mcp sub-library.

func RegisterPlugin

func RegisterPlugin(registrar object.LibraryRegistrar)

RegisterPlugin registers the scriptling.runtime.plugin sub-library. Like the other sub-libraries it is also exposed as runtime.plugin when scriptling.runtime is registered with the same registrar, so `import scriptling.runtime as rt; rt.plugin.serve(...)` works.

func RegisterSandbox

func RegisterSandbox(registrar object.LibraryRegistrar, allowedPaths []string)

RegisterSandbox registers the sandbox library with the specified allowed paths. If allowedPaths is nil, all paths are allowed (no restrictions). If allowedPaths is empty slice, no paths are allowed (deny all).

func RegisterSync

func RegisterSync(registrar object.LibraryRegistrar)

RegisterSync registers the scriptling.runtime.sync sub-library.

func ReleaseBackgroundTasks

func ReleaseBackgroundTasks()

ReleaseBackgroundTasks sets BackgroundReady=true and starts all queued tasks

func ResetRuntime

func ResetRuntime()

ResetRuntime clears all runtime state (for testing or re-initialization). It first runs the cleanup functions libraries registered with libutil.RegisterCleanup; they stay registered for the next reset. That is how the separately packaged sub-libraries reset: scriptling.runtime.http clears its routes and closes its WebSocket connections, and scriptling.runtime.kv closes its default and named stores.

func SetBackgroundFactory

func SetBackgroundFactory(factory SandboxFactory)

SetBackgroundFactory sets the factory function for creating Scriptling instances in background tasks. Deprecated: Use SetSandboxFactory instead, which sets the factory for both sandbox and background use.

func SetMCPResourceNotifier

func SetMCPResourceNotifier(fn func(uri string))

SetMCPResourceNotifier installs fn as the target of notify_resource_updated(); nil removes it, making the call a no-op.

func SetSandboxFactory

func SetSandboxFactory(factory SandboxFactory)

SetSandboxFactory sets the factory function for creating sandbox instances. Must be called before sandbox.create() is used in scripts.

Example:

runtime.SetSandboxFactory(func() runtime.SandboxInstance {
    p := scriptling.New()
    setupMyLibraries(p)
    return p
})

func SetTaskErrorLogger

func SetTaskErrorLogger(fn func(name string, err error))

SetTaskErrorLogger replaces the handler invoked when a background task fails (its promise resolves with an error). Pass nil to restore the default stderr reporter. The handler must be safe to call from task goroutines.

func WaitBackgroundTasks

func WaitBackgroundTasks()

WaitBackgroundTasks blocks until every outstanding non-daemon background task has finished. Hosts that run scripts to completion (the CLI's plain-run modes) call it before exiting so fire-and-forget tasks are not killed mid-flight; long-running hosts (servers) do not.

func WithServerStart

func WithServerStart(ctx context.Context, ch chan struct{}) context.Context

WithServerStart marks ctx as belonging to the server whose start channel is ch. start_server() and server_running() under a context whose server has since been replaced (a setup script left over from an earlier server) neither start nor report the newer server.

Types

type Promise

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

Promise represents an async operation result

type RuntimeAtomic

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

RuntimeAtomic is a named atomic counter

func (*RuntimeAtomic) Value

func (a *RuntimeAtomic) Value() int64

Value returns the current counter value. Exported for hosts and tests that need to observe a sync.Atomic created by script code.

type RuntimeQueue

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

RuntimeQueue is a named thread-safe queue

type RuntimeShared

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

RuntimeShared is a named shared value. Values stored should be treated as immutable. Use set() to replace. For atomic read-modify-write, use update() with a callback.

type RuntimeWaitGroup

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

RuntimeWaitGroup is a named wait group

type SandboxFactory

type SandboxFactory func() SandboxInstance

SandboxFactory creates new Scriptling instances for sandbox execution. Must be set by the host application before sandbox.create() can be used. The factory should return a fully configured instance with all required libraries registered and import paths configured.

func BackgroundFactory

func BackgroundFactory() SandboxFactory

BackgroundFactory returns the currently configured background factory, or nil. Hosts that layer behaviour onto it (a server adding its package loader, say) read it, wrap it, and install the wrapper.

func GetSandboxFactory

func GetSandboxFactory() SandboxFactory

GetSandboxFactory returns the currently configured sandbox factory. Returns nil if no factory has been set.

type SandboxInstance

type SandboxInstance interface {
	SetObjectVar(name string, obj object.Object) error
	GetVarAsObject(name string) (object.Object, error)
	EvalWithContext(ctx context.Context, input string) (object.Object, error)
	SetSourceFile(name string)
	LoadLibraryIntoEnv(name string, env *object.Environment) error
	SetOutputWriter(w io.Writer)
}

SandboxInstance is the minimal interface a sandbox environment needs. This matches the Scriptling public API without importing the scriptling package. It is also used by the background task factory in scriptling.runtime.

Directories

Path Synopsis
Package http provides the scriptling.runtime.http library: route, static file and WebSocket registration for the scriptling server, plus response helpers.
Package http provides the scriptling.runtime.http library: route, static file and WebSocket registration for the scriptling server, plus response helpers.
Package kv provides the scriptling.runtime.kv library: a thread-safe key-value store (the process-wide default store plus named stores opened with kv.open), backed by snapshotkv.
Package kv provides the scriptling.runtime.kv library: a thread-safe key-value store (the process-wide default store plus named stores opened with kv.open), backed by snapshotkv.

Jump to

Keyboard shortcuts

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