Documentation
¶
Overview ¶
Package plugins runs third-party TypeScript plugins (spec 5.3–5.5): manifest validation, the sandbox and the ctx API. It does not import core; the host side is injected.
Index ¶
- Constants
- Variables
- func Build(dir, main string) ([]byte, error)
- func BuildWith(c Compiler, dir, main string) ([]byte, error)
- func EgressFor(kind InvocationKind, m Manifest, granted, holdsMail, scopeReadMail bool) (hosts []string, needsApproval bool)
- func FailReason(err error) string
- func HashDir(dir string) (string, error)
- func HostPort(u *url.URL) string
- func QuestionBytes(qs []Question) int
- func ScrubPaths(s string) string
- func Snapshot(src, dst string) (sourceHash string, err error)
- type ActionInfo
- type Answer
- type AttachmentMeta
- type CallScope
- type Client
- type Clock
- type Compiler
- type ConfigField
- type ConfigSnapshot
- type DraftInput
- type DraftUpdate
- type FetchRequest
- type FetchResponse
- type Hop
- type Host
- type Invocation
- type InvocationKind
- type MailMessage
- type MailSummary
- type Manifest
- type NetError
- type NetOptions
- type Option
- type PermissionError
- type Permissions
- type Plugin
- func (p *Plugin) Actions() []ActionInfo
- func (p *Plugin) Card(ctx context.Context, inv Invocation, m MailMessage) (res any, matched bool, err error)
- func (p *Plugin) Emit(ctx context.Context, inv Invocation, event string, args ...any) error
- func (p *Plugin) EmitResult(ctx context.Context, inv Invocation, event string, args ...any) (any, error)
- func (p *Plugin) Events() []string
- func (p *Plugin) Handles(event string) bool
- func (p *Plugin) HasCard() bool
- func (p *Plugin) RunAction(ctx context.Context, inv Invocation, id string, target any) (any, error)
- func (p *Plugin) RunTool(ctx context.Context, inv Invocation, name string, args any) (any, error)
- func (p *Plugin) Tools() []ToolInfo
- func (p *Plugin) VM() *VM
- type Question
- type Text
- type ToolInfo
- type VM
- func (v *VM) Call(ctx context.Context, inv Invocation, fn func(c *CallScope) (any, error)) (any, error)
- func (v *VM) Close()
- func (v *VM) HostFunc(name string, fn func(c *CallScope, call goja.FunctionCall) goja.Value) goja.Value
- func (v *VM) Init(ctx context.Context, inv Invocation, jsCtx func(c *CallScope) goja.Value) error
- type VMOptions
- type Watchdog
- type When
Constants ¶
const ( MaxBodyBytes = 256 << 10 // each of a message's text and html, as a plugin gets it MaxStorageValue = 64 << 10 MaxStorageTotal = 1 << 20 // keys and values of one plugin MaxLLMBytes = 64 << 10 // llm.complete prompt and answer; llm.decide input and question text MaxQuestions = 128 // per llm.decide call: rejected beyond, never split MaxFetchBody = 1 << 20 EventReceived = "message.received" EventBeforeSend = "message.beforeSend" )
Host-side data limits (spec 5.5).
const ( MaxFileBytes = 5 << 20 MaxTotalBytes = 20 << 20 )
Snapshot limits.
const ( MaxFetchResponse = 5 << 20 FetchTimeout = 30 * time.Second )
Network limits of ctx.net.fetch (spec 5.5).
const BuildDir = ".build"
BuildDir is where a snapshot keeps its compiled code; it is not part of the snapshot.
const MaxResultBytes = 1 << 20
MaxResultBytes caps the JSON a plugin call hands back to the host.
Variables ¶
Functions ¶
func BuildWith ¶
BuildWith compiles the snapshot in dir with c. The result is cached in dir/.build under the snapshot's hash, main and c.Key(); other builds there are removed.
func EgressFor ¶
func EgressFor(kind InvocationKind, m Manifest, granted, holdsMail, scopeReadMail bool) (hosts []string, needsApproval bool)
EgressFor is the egress rule for one call of kind into plugin m (plan 4 规格解释): the hosts its Invocation may fetch from, and whether the call needs an egress approval listing them first. granted: the plugin's current permissions are granted; holdsMail: the plugin was ever handed mail content (its storage may keep it, across calls, updates and restarts); scopeReadMail: the call's input may carry mail content (a Wit run that has read mail calling a tool, an action on a selection). Init, card and beforeSend never fetch; a hook gets every declared host; an action or a tool needs approval when the plugin holds net and any of mail:read, holdsMail or scopeReadMail. Since ctx.storage outlives a call, "this call read no mail" is never trusted for a plugin that has or ever had mail.
func FailReason ¶
FailReason is the audit reason of a request the policy allowed that failed: dns_error, tls_error or network_error.
func HostPort ¶
HostPort is u's lowercased host with its port, the scheme's default (https 443, http 80) when it has none: the destination an audit row names.
func QuestionBytes ¶
QuestionBytes is the size of the questions' text: instructions, values and descriptions.
func ScrubPaths ¶
ScrubPaths replaces absolute paths in s with "<path>".
Types ¶
type ActionInfo ¶
type Answer ¶
Question and Answer are typed judge questions and answers, as llm.decide and Wit's classify_mail take and give them: the OpenAI Decisions API's shape (predicate, choice, score; a refusal answer), whatever the judge.
type AttachmentMeta ¶
type CallScope ¶
type CallScope struct {
// contains filtered or unexported fields
}
CallScope is one top-level call. It is only valid on the owner goroutine, inside the Call that made it (including host functions JS invokes then).
func (*CallScope) Async ¶
Async returns a promise settled by fn, which runs on its own goroutine with the call's context. The result goes through rt.ToValue (json tags name struct fields); an error rejects with a plain Error whose message is redacted and path-scrubbed. The settlement is posted back to the owner goroutine tagged with this call; if the call has ended by then it is dropped and never runs.
func (*CallScope) Context ¶
Context ends with the call; synchronous host work should honour it, since Interrupt cannot stop a running Go function.
func (*CallScope) Invocation ¶
func (c *CallScope) Invocation() Invocation
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is ctx.net.fetch's HTTP client. It resolves names itself and dials only addresses it has checked (no DNS rebinding between check and connect), never uses a proxy, and re-checks every redirect.
func NewClient ¶
func NewClient(opts NetOptions) *Client
func (*Client) Do ¶
func (c *Client) Do(ctx context.Context, allowed []string, req FetchRequest) (FetchResponse, []Hop, error)
Do fetches req if its URL is allowed (host[:port], 443 by default) and returns the response, every hop for the audit (none only for an unparseable URL), and an error; a policy refusal or the timeout is a *NetError. No partial body is ever returned.
type Compiler ¶
type Compiler interface {
// Key identifies the compiler version and options: builds cached under
// another key are not reused.
Key() string
Compile(dir, main string) ([]byte, error)
}
Compiler turns a snapshot into the script the plugin VM runs.
var ESBuild Compiler = esbuildCompiler{}
ESBuild bundles TypeScript or JavaScript with esbuild.
type ConfigField ¶
type ConfigField struct {
Key string `json:"key"`
Type string `json:"type"`
Label Text `json:"label"`
Options []Option `json:"options,omitempty"`
AllowCustom bool `json:"allowCustom,omitempty"`
Required bool `json:"required,omitempty"`
Default json.RawMessage `json:"default,omitempty"`
When *When `json:"when,omitempty"`
}
ConfigField is one setting the user fills in. Type is text, secret, number, boolean or select. A secret has no default and is stored in the keychain.
func (ConfigField) Accepts ¶
func (f ConfigField) Accepts(v json.RawMessage) bool
Accepts reports whether v is a value f can hold (not for a secret).
type ConfigSnapshot ¶
ConfigSnapshot is a plugin's configuration at one config version; Err is why it could not be read (ctx.config then throws). Never changed once made.
type DraftInput ¶
type DraftUpdate ¶
type DraftUpdate struct {
DraftID int64 `json:"draftId"`
Revision int64 `json:"revision"`
HTML string `json:"html"`
}
DraftUpdate replaces a draft's body if the draft is still at Revision.
type FetchRequest ¶
type FetchResponse ¶
type Hop ¶
type Hop struct{ Dest, Decision, Reason string }
Hop is one request of a fetch, the first or a redirect: its host:port and its outcome. Decision is "allow" (answered, or redirected onward), "deny" (refused by the policy; Reason is the NetError code) or "error" (the policy allowed it and it failed; Reason is dns_error, network_error, tls_error, timeout or cancelled).
type Host ¶
type Host interface {
StorageGet(ctx context.Context, key string) (string, bool, error)
StorageSet(ctx context.Context, key, value string) error
StorageDelete(ctx context.Context, key string) error
MailGet(ctx context.Context, id int64) (MailMessage, error)
MailSearch(ctx context.Context, q string, limit int) ([]MailSummary, error)
MailSetFlags(ctx context.Context, ids []int64, add, remove []string) error
MailMove(ctx context.Context, ids []int64, folderID int64) error
MailCreateDraft(ctx context.Context, d DraftInput) (int64, error)
MailUpdateDraft(ctx context.Context, u DraftUpdate) (int64, error)
LLMComplete(ctx context.Context, prompt string) (string, error)
LLMDecide(ctx context.Context, input string, qs []Question) ([]Answer, error)
Fetch(ctx context.Context, inv Invocation, req FetchRequest) (FetchResponse, error)
AuditEgress(ctx context.Context, inv Invocation, dest, decision, reason string) error
}
Host is what the core gives a plugin's ctx. Permissions are checked before any of it is called. It has no way to send mail, and must never get one.
type Invocation ¶
type Invocation struct {
ID string
Kind InvocationKind
Write bool
EgressHosts []string
// Unapproved: the call needed an egress approval it did not get, so it
// has no hosts; a fetch to a declared host is denied as approval_required.
Unapproved bool
// Approved: the user approved this call's egress (its hosts were listed).
// An action or tool call without it is checked again as it fetches.
Approved bool
// Config is the call's ctx.config, taken when the host admitted the call
// (before it waits for the VM); nil makes ctx.config throw.
Config *ConfigSnapshot
}
Invocation is what the host allows one top-level call into a plugin: whether it may write mail, and the hosts it may fetch from (none: no egress). What the plugin declared when registering does not widen it.
type InvocationKind ¶
type InvocationKind string
const ( KindInit InvocationKind = "init" KindHook InvocationKind = "hook" KindBeforeSend InvocationKind = "beforeSend" KindAction InvocationKind = "action" KindCard InvocationKind = "card" KindTool InvocationKind = "tool" )
type MailMessage ¶
type MailMessage struct {
ID int64 `json:"id"`
Subject string `json:"subject"`
From string `json:"from"`
To []string `json:"to"`
Cc []string `json:"cc"`
Date time.Time `json:"date"`
Text string `json:"text"`
HTML string `json:"html"`
Truncated bool `json:"truncated"`
Attachments []AttachmentMeta `json:"attachments"`
}
MailMessage is a message as a plugin sees it. Text and HTML are cut to the host's limit, with Truncated set.
type MailSummary ¶
type Manifest ¶
type Manifest struct {
Name string `json:"name"`
Version string `json:"version"`
Description Text `json:"description"` // one line, shown under the name in settings
Main string `json:"main"`
Permissions Permissions `json:"permissions"`
Config []ConfigField `json:"config"`
}
Manifest is a plugin's plugin.json (spec 5.3–5.4).
func ParseManifest ¶
ParseManifest parses and strictly validates plugin.json of the plugin installed in the directory dirName. A manifest that fails is not loaded.
func (Manifest) PermissionHash ¶
PermissionHash identifies what the user granted: it changes when the mail, net or llm permissions change, and not with their order, case, a ":443" or the config.
type NetError ¶
type NetError struct{ Reason, Host string }
NetError is a fetch the policy refused. Reason is a stable code: host_not_allowed, scheme, ip_literal, userinfo, private_address, too_many_redirects, too_large or timeout.
type NetOptions ¶
type NetOptions struct {
Resolver interface {
LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error)
}
Dial func(ctx context.Context, network, addr string) (net.Conn, error)
AllowHTTP, AllowLoopback bool
MaxBody int64 // response body; 0 = MaxFetchResponse
Timeout time.Duration // whole call, redirects and body included; 0 = FetchTimeout
UserAgent string // "Godwit-Plugin/<version>"
// contains filtered or unexported fields
}
NetOptions configures a Client. Zero values are the real network and the spec's limits; AllowHTTP and AllowLoopback exist for tests only.
type PermissionError ¶
type PermissionError struct{ Perm string }
PermissionError is a ctx API call the manifest or the invocation does not allow. Its message names only the permission.
func (*PermissionError) Error ¶
func (e *PermissionError) Error() string
func (*PermissionError) Is ¶
func (e *PermissionError) Is(target error) bool
type Permissions ¶
type Permissions struct {
Mail []string `json:"mail"`
Net []string `json:"net"`
LLM bool `json:"llm"`
}
Permissions: Mail holds "read" and/or "write" (there is no send); Net the hosts, each "host" or "host:port", lowercased and without ":443" after ParseManifest.
type Plugin ¶
type Plugin struct {
Name string
// contains filtered or unexported fields
}
Plugin is a loaded plugin: its VM and what it registered during initialization. Registrations never change after Load.
func Load ¶
Load builds the ctx object and runs the plugin's initialization in an init invocation (no writes, no egress, cfg as ctx.config), recording what it registers.
func (*Plugin) Actions ¶
func (p *Plugin) Actions() []ActionInfo
func (*Plugin) Card ¶
func (p *Plugin) Card(ctx context.Context, inv Invocation, m MailMessage) (res any, matched bool, err error)
Card runs the card's match(m) and, if its result is truthy, render(m), both under inv, and returns render's result; matched is false when match was not truthy. ctx bounds both calls together.
func (*Plugin) Emit ¶
Emit runs the plugin's handler for event with args (plain JSON-like Go data) as one top-level call under inv, awaiting it if it is async.
func (*Plugin) EmitResult ¶
func (p *Plugin) EmitResult(ctx context.Context, inv Invocation, event string, args ...any) (any, error)
EmitResult is Emit that returns the handler's (awaited) result as plain JSON data.
func (*Plugin) RunAction ¶
RunAction runs the registered action id on target under inv and returns its (awaited) result as plain JSON data.
type Question ¶
Question and Answer are typed judge questions and answers, as llm.decide and Wit's classify_mail take and give them: the OpenAI Decisions API's shape (predicate, choice, score; a refusal answer), whatever the judge.
type Text ¶
Text is a label: a plain string, or {"default": ..., "<lang>": ...}.
func (Text) For ¶
For returns the text for lang, then for its base language ("de" for "de-AT"), then the default.
func (Text) MarshalJSON ¶
func (*Text) UnmarshalJSON ¶
type VM ¶
type VM struct {
// contains filtered or unexported fields
}
VM is one plugin's goja runtime. Every touch of the runtime happens on its owner goroutine: top-level calls are queued and run one at a time there, and async host work posts its completion back to it. Nothing returned by Call is a goja.Value.
func (*VM) Call ¶
func (v *VM) Call(ctx context.Context, inv Invocation, fn func(c *CallScope) (any, error)) (any, error)
Call runs fn on the owner goroutine as one top-level call, after any call already running or queued. If fn returns a goja.Value it is awaited when it is a promise (pumping only this call's async completions) and exported as plain JSON data (map[string]any, []any, float64, string, bool, nil; at most MaxResultBytes, else ErrTooLarge). Go results are normalized the same way through encoding/json; one holding a goja.Value, func or channel is an error. The call ends at settlement, ctx's end, Close (ErrClosed), the kind's time limit (ErrTimeout) or the watchdog (ErrMemoryLimit); JS still running is interrupted.
func (*VM) Close ¶
func (v *VM) Close()
Close refuses new calls (ErrClosed), cancels the running one and waits for it, drops pending completions and stops the owner goroutine.
func (*VM) HostFunc ¶
func (v *VM) HostFunc(name string, fn func(c *CallScope, call goja.FunctionCall) goja.Value) goja.Value
HostFunc wraps a host function for JS. Call it on the owner goroutine (from inside a Call or Init). fn gets the scope of the call JS invoked it in. To throw, fn panics; JS always gets a plain Error (no Go value behind it, so e.value is undefined) whose message went through Redact and path scrubbing:
- an error: "<name>: " + its message
- a goja error value (rt.NewTypeError(...), ...): its message, keeping TypeError as TypeError
- anything else, Go runtime errors included: "<name>: internal error"
Interrupts and stack overflows pass through untouched.
type VMOptions ¶
VMOptions: a nil Clock is the real clock, a nil Watchdog the default one, a nil Redact the identity.
type Watchdog ¶
Watchdog is the soft heap limit: goja has no per-runtime memory cap, so while a call runs it samples a process-wide meter and interrupts that call with ErrMemoryLimit when the meter has grown by more than Threshold since the call began. ponytail: process-wide, so it cannot tell the plugin's allocations from the app's; a child process with GOMEMLIMIT is the hard cap if one is ever needed.
Zero fields take defaults: Meter reads runtime.MemStats.HeapAlloc, Clock is the VM's, Interval 10 ms, Threshold 256 MB.
type When ¶
type When struct {
Key string `json:"key"`
Equals json.RawMessage `json:"equals,omitempty"`
NotEquals json.RawMessage `json:"notEquals,omitempty"`
}
When shows a field only if the earlier field Key equals (or does not equal) a value of that field's type.