2. Contracts — @get-bb/plugin-sdk/contracts

@get-bb/plugin-sdk/contracts is what a plugin's contracts module imports (02 §9). It is browser-safe: no Node import reaches the entry, and the app tier loads it through the import map. It adds no runtime of its own; defineService, method and defineEvent wrap @bb/kernel-contract and @bb/kernel-core with the SDK's pre-checks, and everything else is a re-export. A contracts module exports defineService objects, defineEvent objects, defineHostCommands objects and declare module merges; it contains no handlers. 02 §9 places renderText in the tier entry; the built example keeps it on the method in the contracts module, and contractJsonOf drops it. The built source wins over the spec where they differ; each row says so.

Path shorthand below: <pkg>:<file> means packages/<pkg>/src/<file>. Every value and type the lock lists for this entry appears in a table in this section.

2.1 The published-contracts convention

Member Signature (abridged) What it does Spec As built
CONTRACTS_SUBPATH "./contracts" The package.json#exports key a contracts module publishes under. 01 §1, 02 §9 plugin-sdk:contracts/convention.ts
pluginPackageName (pluginId, trust = "published") => string @get-bb/plugin-<id>; @bb-local/<id> for trust: "local" (forks, bb plugin new). 01 §1 same
contractsSpecifier (pluginId, trust = "published") => string ${pluginPackageName(id, trust)}/contracts: the specifier a sibling imports. 02 §9 same

A sibling plugin imports only this subpath; bb plugin build fails any other import of a sibling (D19, plugin-build:externals.ts). bb plugin build imports the module named by package.json#bb.contracts, runs contractJsonOf (§2.9) and writes dist/contract.json; nothing is written back into package.json (03 §2.4).

// examples/plugins/hello-slot/src/contracts.ts (abridged)
export const greeter = defineService({
  id: "hello-slot/greeter", version: "1.0.0", summary: "Greet by name and count greetings",
  cli: { group: ["hello"] },
  methods: {
    greet: method({
      kind: "query", summary: "Greet someone by name",
      input: z.strictObject({ name: nameSchema, salutation: z.string().min(1).max(32).default("Hello") }),
      output: greetingSchema, expose: { tool: true },
      cli: { fields: { name: { positional: 0 } }, examples: [{ argv: "bb hello greet bb", note: "prints `Hello, bb!` and the counter" }] },
      tool: { presentation: { label: { pending: "Greeting…", completed: "Greeted" }, icon: "hello-slot/wave", intent: "generic" } },
      renderText: (g) => `${g.greeting} (count ${g.count})`,
    }),
    count: method({ kind: "mutation", summary: "Record a greeting in the plugin's data.db counter",
      input: z.strictObject({ name: nameSchema }), output: greetingSchema, cli: { fields: { name: { positional: 0 } } } }),
  },
});

2.2 defineService / defineContract and method

Member Signature (abridged) What it does Spec As built
defineService <const M>(def: ServiceInput<M>) => ServiceDef<M> Normalizes a service: checks the id, fills cli.group, command, cli.name, tool.name, builds .contract. The SDK adds: version must be a full semver; multi needs factsKey and facts; factsKey only with multi. 03 §2.1 plugin-sdk:contracts/define.tskernel-contract:define.ts
defineContract = defineService The brief's spelling; one function. plugin-sdk:contracts/define.ts
method (def: Omit<MethodInput<I,O,"query">,"kind">) => MethodDef<I,O,"query"> / (def: MethodInput<I,O,K> & {kind: K}) => MethodDef<I,O,K> Normalizes one method with every default filled once; runs the invalid_contract checks below. Two overloads: no kind means "query". 03 §2.1 (one signature) D4: two overloads; kernel-contract:define.ts
ServiceInput<M> {id, version, summary, methods: M, cli?, facts?, multi?, factsKey?} Author input to defineService. cli is {group?, aliases?, hidden?, extensible?}. 03 §2.1 kernel-contract:define.ts
ServiceDef<M> {id, version, summary, cli: ServiceCliMeta, methods: M, facts, multi, factsKey, contract} The normalized service. contract is the 02 ServiceContract the container registers; a custom method is query-shaped over the HTTP pair there. 03 §2.1 (extends ServiceContract) carries .contract instead of extending; kernel-contract:README
ServiceCliMeta {group: string[], aliases: string[][], hidden, extensible} Service-level CLI group. Default group [kebab(serviceName)]; at most two levels; extensible opens a kernel-owned group to other plugins' words (a non-kernel group is open to method-level mounts regardless). 03 §2.1, §5.3 kernel-contract:define.ts, cli/tree.ts
MethodInput<I,O,K> {kind?, input, output, command?, summary, description?, expose?, cli?, tool?, target?, auth?, local?, actors?, destructive?, blocking?, resumable?, path?, renderText?, jsonSchema?, errors?} Author input to method; every optional is a default, filled once. 03 §2.1 kernel-contract:define.ts
MethodDef<I,O,K> the normalized method, every field present (table below) What every derivation reads. Read command, cli.name, tool.name from a ServiceDef, never from a bare method() result. 03 §2.1 D4
MethodKind "query" | "mutation" | "stream" | "custom" Verb, transport and handler shape per kind. Query or mutation: the kernel applies no rule to a query's side effects — a query is a plain GET that bypasses the command bus, so it gets no command name, before/after hooks, deadlineMs, or CLI y/N; choose mutation when you want those (hello-slot: greet is a query that only reads data.db; count writes and publishes, and is a mutation). 03 §2.1 same; no side-effect check in kernel-contract:define.ts or invoke.ts
Target "core" | "host" Where the handler runs. host answers 503 service_unavailable today. 03 §5.9 D5
Auth "operator" | "machine" | "none" Which callers are admitted (§8.2). 03 §9.2 kernel-contract:http/auth.ts
Expose {http, sdk, cli, tool, ui} (all boolean) Which surfaces derive. MethodInput.expose is Partial<Expose>: give only the gates you change (expose: { tool: true }); the rest keep their defaults http/sdk/cli/ui: true, tool: false. sdk/ui are X-BB-Client.kind gates on the one HTTP route, not security. 03 §2.1 D5; kernel-contract:define.ts { ...defaults, ...def.expose }
CliFieldMeta {positional: number|null, flag, alias: string|null, ambient: Ambient|null, hidden, variadic} One input field's CLI form. One row exists for every input property (defaults filled). 03 §2.1, §5.4 kernel-contract:define.ts
CliMethodMeta {name, aliases, group: string[]|null, flagAlias, fields, examples: {argv: string, note: string}[], hidden, offline} The method's CLI word. group: null means the service group; flagAlias routes bb <group> <name> --<flag> here; examples items are { argv, note } (default []), printed in help. 03 §2.1, §5.3 same
ToolMeta {name, description, instructions, presentation: ToolPresentation, ui: {renderer}|null} Agent-tool metadata; presentation is required when expose.tool. 03 §2.1, §6.1 same
ToolPresentation {label: {pending, completed}, icon: string, intent: RenderIntent, suppress} Becomes DynamicTool.presentation verbatim; suppress filled false. icon is an @bb/ui icon name (§4.11: a core name such as Zap, an extended name, or <pluginId>/<name>; only the first two render in the slice). intentgeneric | terminal | diff | search | read | list | web | image (RenderIntent, bridge-kit/src/protocol/common.ts renderIntentSchema; §7.7 repeats the list). 03 §2.1, 06 §4.3 same; icon is typed string in kernel-contract:define.ts
RenderText<I,O> (value: InferOutput<O>, ctx: RenderCtx<InferOutput<I>>) => string Text renderer for CLI text mode and tool content. Runs where the plugin code is loaded; not in contract.json. 03 §2.1, §5.5 bivariant method type; kernel-contract:define.ts
RenderCtx<In> {width, color, tty, input} From Bb-Render: text; width=; color=; tty= or defaults {80, false, false}. 03 §2.1, §5.5 kernel-contract:context.ts
CallContext 02's {callId, actor, session, scope, signal, command, traceId} + {caller, ambient, render, deadline, since, log} What a handler receives as its second argument. 03 §2.3 extension rides a side table; kernel-contract:context.ts
CustomHandler {http(req: Request, ctx), cli?(argv, ctx)} | {httpSerializable(req: PluginHttpRequest, ctx), cli?} Handler of a custom method: Fetch-shaped in-process, or the serializable pair in either placement. 03 §2.2 kernel-contract:define.ts
MethodHandler<D> stream → (input, ctx) => AsyncIterable<O>; custom → CustomHandler; else (input, ctx) => Promise<O> Handler type per kind, from a MethodDef. 03 §2.2 same
ServiceHandlers<D> {[N in keyof D["methods"]]: MethodHandler<…>} What ctx.provide(def, handlers) takes. 03 §2.2 same
PluginHttpRequest / PluginHttpResponse {method, path, query, headers, body} / {status, headers, body}; body is {kind: "text", text} | {kind: "bytes", base64} The serializable HTTP pair; validated once by kernel-core's handle. 03 §2.2 (adds {kind: "stream"}) no stream body; >1 MiB is 413; kernel-contract:README
StandardSchemaV1 @standard-schema/spec Every input, output, payload, facts is one. zod 4 is the first-party validator. 03 §2.1 re-exported from kernel-core:schema.ts
SchemaOf<T> StandardSchemaV1<unknown, T> A schema whose output is T; the SDK's schema parameter type. plugin-sdk:contracts/handles.ts
JsonSchema JsonObject The JSON Schema carrier read from ~standard.jsonSchema.input/output; drives CLI, tool, types and docs. 03 §2.1 kernel-core:schema.ts

MethodDef fields, as method() and defineService fill them (kernel-contract:define.ts):

Field Type Default Meaning
kind MethodKind "query" query GET, mutation POST through the command bus, stream SSE, custom raw HTTP.
input StandardSchemaV1 required Root must be a closed object (additionalProperties: false, i.e. z.strictObject) unless custom (D4).
output StandardSchemaV1 required Result schema; for stream, the schema of one item.
command CommandName | null mutation: <pluginId>/<service>.<method>; else null The command-bus name; only a mutation may declare one.
deadlineMs number | null mutation: blocking.maxMs else 60_000; else null Whole-dispatch deadline the command bus enforces.
actors ReadonlyArray<Actor["kind"]> all four Actor kinds admitted; others get forbidden. Accepted on every kind (03 says mutations only).
destructive boolean false CLI asks y/N unless --yes; tool description appends a warning.
summary string required One line, 1–80 characters.
description string | null null Markdown paragraphs for docs and the tool description.
expose Expose {http: true, sdk: true, cli: true, tool: false, ui: true} Surface gates.
cli CliMethodMeta name: kebab(method), aliases: [], group: null, flagAlias: null, fields: one row per property (flag: kebab(field)), examples: [], hidden: false, offline: false CLI derivation input; offline: true is accepted and ignored until the cli tier ships (Appendix B).
tool ToolMeta | null null; name: <plugin>_<service>_<method> (snake) when given Must be present with expose.tool and absent without it.
target Target "core" "host" runs in the plugin's host tier; 503 today (D5).
auth Auth "operator" Admitted callers (§8.2). "none" on a mutation requires local: true.
local boolean false Served only to loopback callers, never through the gate.
blocking {maxMs} | null null May park on an interaction; 1..3_600_000; becomes deadlineMs and Bb-Deadline.
resumable boolean false Streams only; the handler gets ctx.since from Last-Event-ID.
path string | null null custom + kernel services only: an absolute mount outside /api/v1.
renderText RenderText | null null Text renderer; genericText otherwise.
jsonSchema {input, output} from ~standard.jsonSchema (draft 2020-12) Set it only when the validator has no converter.
errors Record<string, {status, summary}> {} Declared plugin codes <pluginId>/<snake_code> with a 4xx/5xx status; an undeclared code is rewritten to plugin_error.

invalid_contract is thrown at definition time for: a summary outside 1–80 chars; command on a non-mutation; blocking.maxMs outside 1..3_600_000; resumable off a stream; path off a custom method or off a kernel service; tool.instructions over 4096 bytes; auth: "none" on a non-local mutation; expose.tool without tool.presentation, tool without expose.tool, or expose.tool on custom; a non-strict input root; a cli.fields key that is not an input property; a multi-letter alias; a repeated positional; variadic on a non-positional or non-array field, or more than one; an errors code outside <pluginId>/<snake> or status outside 4xx/5xx; a tool name outside [a-zA-Z0-9_-]+; a service id not <pluginId>/<kebab-service>; a CLI group deeper than two levels; a non-semver version; multi without factsKey + facts; factsKey without multi; a multi contract with a mutation (kernel-core, D2).

2.3 Handles and handlers

Member Signature (abridged) What it does Spec As built
AnyContract ServiceDef | ServiceContract What provide / inject accept: a defineService object or a bare kernel contract. 02 §3 plugin-sdk:contracts/handles.ts
ServiceDefHandle<D> HandleInfo & {[M]: (input, opts?: CallOverrides) => Promise<O> | AsyncIterable<O>} The handle methods of a ServiceDef; a custom method is (PluginHttpRequest) => Promise<PluginHttpResponse>. 02 §3.6 same
HandleOf<C> C extends ServiceDef ? ServiceDefHandle<C> : ServiceHandle<C> The typed handle ctx.inject(def) and useService(def) return. 02 §3.6 same
HandlersOf<C> C extends ServiceDef ? ServiceHandlers<C> : ServiceImpl<C> The handler map ctx.provide(def, …) takes. 03 §2.2 same
FactsOf<C> / FactsOutputOf<C> InferInput<C["facts"]> / InferOutput<C["facts"]> The facts a provider passes / a consumer reads. 02 §3.5 same
isServiceDef (c: AnyContract) => c is ServiceDef True when c.contract is an object. same
contractOf (c: AnyContract) => ServiceContract The 02 view either shape binds by. 02 §3 same
ServiceContract {id, version, methods, facts, multi, factsKey} kernel-core's normalized contract; ServiceDef.contract is one. 02 §3.1 kernel-core:contract.ts
ServiceId `${string}/${string}` <pluginId>/<service>; pluginId [a-z0-9][a-z0-9-]*, service [a-z][a-zA-Z0-9.-]* (the dot admits kernel/secrets.backend); defineService narrows the service part to kebab-case. 02 §3.1 kernel-core:names.ts

2.4 Events: defineEvent

Member Signature (abridged) What it does Spec As built
defineEvent <Name extends keyof Events>(input: EventDefinitionInput<Name>) => EventDefinition<Name> Normalizes an event (wire: null, persist: null unless given). Rejects a name that is not <owner>/<name> and wire on a non-emit mode (invalid_contract). 02 §5.1 plugin-sdk:contracts/define.ts
Events interface Events {} (merged) Declaration-merging hook: declare module "@get-bb/plugin-sdk/contracts" { interface Events { "<id>/<name>": EventDecl<P, M, R> } }. Kernel events are pre-declared. 02 §5.1 kernel-core:events.ts
EventDecl<P, M, R = void> {payload: P, mode: M, result: R} The type-level declaration of one event. 02 §5.1 same
EventDefinitionInput<N> {name, mode, payload: StandardSchemaV1, scope: "server"|"client"|"host", wire?: {to: ("client"|"server")[], key: {path, prefix}|null}, persist?: {retention: "forever"|{days}}} Author input; wire.key is data (<prefix>:<payload[path]>), never code. key: null publishes every frame under key null; a client target with key: null (invalidateOn, events.on) matches every frame of that name, keyed or not (keyOf in kernel-core:schema.ts; define-query.ts t.key === null || t.key === frame.key). 02 §5.1 same
EventDefinition<N> same fields, wire and persist nullable and present What ctx.events.define / ctx.realtime.declare take. 02 §5.1 same
EventMode "emit" | "waterfall" | "parallel" emit: fire-and-forget, the only mode that crosses the wire; waterfall: around-middleware with next; parallel: all listeners, Array<R | KernelError>. There is no serial (02 §1.1 cut it). 02 §5.2 kernel-core:events.ts
EventName `${string}/${string}` <owner>/<name>; a plugin may define only its own owner; kernel/ is reserved_name. 02 §5.1 kernel-core:names.ts
Listener<N> emit (payload, meta) => void; waterfall (payload, next, meta) => Promise<R>; parallel (payload, meta) => Promise<R> Listener signature per mode. 02 §5.2 kernel-core:events.ts
// hello-slot: the merge, then the definition the server tier passes to ctx.realtime.declare / events.define
declare module "@get-bb/plugin-sdk/contracts" {
  interface Events { "hello-slot/greeted": EventDecl<GreetedPayload, "emit"> }
}
export const greeted = defineEvent({
  name: "hello-slot/greeted", mode: "emit", payload: greetedPayloadSchema, scope: "server",
  wire: { to: ["client"], key: null },
});

2.5 Commands: defineCommand

Member Signature (abridged) What it does Spec As built
defineCommand (input: ClientCommandInput<In, Out>) => CommandDefinition<In, Out> A side: "local" client command (origin: {kind: "client"}, deadlineMs: null). Server commands are never written by hand: provide() derives one per mutation. Rejects a name outside the grammar. 02 §6.3 plugin-sdk:contracts/define.ts
ClientCommandInput<In, Out> {name, input, output, actors?, destructive?} Author input; actors defaults to all four kinds, destructive to false. 02 §6.3 same
CommandDefinition<In, Out> {name, input, output, actors, side, destructive, deadlineMs, origin} The normalized command the bus stores. 02 §6.3 kernel-core:commands.ts
CommandName `${string}.${string}` <noun>.<segment>(.<segment>)*; noun [a-z][a-z0-9-]*(/[a-z][a-z0-9-]*)?; nounOf is everything before the last dot. Reserved nouns: kernel, composition, plugin, host, preference, secret. A plugin always owns <pluginId>/… nouns; other server nouns come from contributes.commands.nouns, by composition order. 02 §6.2 D2; kernel-core:names.ts
Commands interface Commands {} (merged) declare module … { interface Commands { "<name>": { input; output } } } types CommandInput/CommandOutput and the bus. 02 §6.3 kernel-core:commands.ts
CommandInput<N> / CommandOutput<N> Commands[N]["input"] / ["output"], else unknown The typed input/output of a named command. 02 §6.3 same

2.6 Host commands: defineHostCommands

Member Signature (abridged) What it does Spec As built
defineHostCommands <const C, const S = {}>(input: HostCommandsInput<C, S>) => HostCommandsContract<C, S> The one object both ends validate against: the server's ctx.hostClient(contract, name) and the role process. Checks the plugin id and kebab-case names. Manifest hostCommands/hostSignals list names only. 05 §4.7 plugin-sdk:contracts/define.ts
HostCommandsInput<C, S> {id, commands: C, signals?: S} Author input; omitting signals means the plugin emits none. 05 §4.7 same
HostCommandsContract<C, S> {id, commands: C, signals: S} Normalized; wire methods are <id>/<command>. 05 §4.7 same
HostCommandDef<In, Out> {input: In, output: Out} One command's schemas. 05 §4.7 same
HostSignalDef<P> {payload: P} One signal's payload schema. 05 §4.7 same
export const helloHost = defineHostCommands({
  id: "hello-slot",
  commands: { "host-greet": { input: z.strictObject({ name: nameSchema }), output: hostGreetingSchema } },
});

2.7 Kernel hook service contracts

These are kernel-core ServiceContract objects (not ServiceDefs); inject them by object. All are version 1.0.0.

Member Signature (abridged) What it does Spec As built
preferences kernel/preferences: get {key, scope, threadId} → {value, updatedAt}; set {…, value, expectedUpdatedAt} → {updatedAt} (mutation preference.set); clear {key, scope, threadId} → null (mutation preference.clear); list {prefix} → {entries} Profile/thread preferences with compare-and-set; owner check on writes. 02 §3.7, #29 set returns {updatedAt}; kernel-core:services.ts
PrefScope "profile" | "thread" The scopes that reach core; threadId is non-null exactly for thread. 02 §3.7 same
secrets kernel/secrets: resolve {name} → {value}; put {name, value} → null (mutation secret.set, actors: ["human"]); delete {name} → null (mutation secret.delete); list {} → {names, backend} Secrets through the active backend. 02 §3.7 same
secretsBackend kernel/secrets.backend (multi, factsKey: "backendId"): get {name} → {value}; set {name, value} → null; delete {name} → null; list {} → {names} A backend a plugin provides; query-shaped because multi contracts carry no mutations. 02 §3.7 D2
SecretsBackendImpl ServiceImpl<typeof secretsBackend> The handler map a backend provides. 02 §3.7 same
telemetry kernel/telemetry: record {name, props} → null Fire-and-forget fan-out to every sink; a no-op without sinks. 02 §3.7 same
telemetrySink kernel/telemetry.sink (multi, factsKey: "sinkId"): record {name, props} → null A sink a plugin provides. 02 §3.7 same
TelemetrySinkImpl ServiceImpl<typeof telemetrySink> The handler map a sink provides. 02 §3.7 same

2.8 Errors, envelope, actors, JSON

Member Signature (abridged) What it does Spec As built
KernelError new KernelError({code, message, issues?, data?, retryable?}); .toJSON(): KernelErrorShape; .withData(extra) The one error class on every edge. Plugins throw <pluginId>/<code> codes declared in method.errors. retryable defaults to true for timeout and service_unavailable. 02 §8 kernel-core:errors.ts
NeedsConfigurationError new NeedsConfigurationError(message, data?) code: "needs_configuration"; surfaces through plugin status. 02 §8, §4.4 same
KernelErrorShape {code, message, issues: Issue[]|null, data: JsonValue|null, retryable} The wire shape; every field present. 02 §8 same
Envelope<T> / Result<T> {ok: true, result: T} | {ok: false, error: KernelErrorShape} The one envelope (in-process, worker, HTTP); two spellings. The CLI --json prints the bare result; only a failure prints the {ok: false, error} half (§8.4, D6). 02 §8 same
Issue {path: (string|number)[], message} One validation issue. 02 §8 same
Actor {kind: "human"|"agent"|"plugin"|"system", id} human "local"; agent threadId; plugin pluginId; system a closed id set. 02 §7 kernel-core:actor.ts
Session {id, actor, client: {kind, surface, version}, createdAt, credentialId} Minted once at the boundary; client.kind ∈ app|cli|sdk|bridge|worker|internal. 02 §7 same
JsonValue / JsonObject recursive JSON types The payload vocabulary. 02 §8 kernel-core:names.ts
jsonValueSchema / jsonObjectSchema z.ZodType<JsonValue> / z.ZodType<JsonObject> The one zod JSON schema pair; numbers must be finite. kernel-core:schema.ts

2.9 contract.json and the catalog

Member Signature (abridged) What it does Spec As built
contractJsonOf (defs: readonly ServiceDef[], guide: string | null = null) => ContractJson The dist/contract.json block: services sorted by id, every method() default filled, JSON Schemas, guide = contributes.guide path. Pure data; renderText does not travel. 03 §2.4 plugin-sdk:contracts/contract-json.ts
contractJsonText (defs, guide?) => string The same block as pretty JSON. 03 §2.4 same
ContractJson {services: ContractJsonService[], guide: string | null} Root of the block. 03 §2.4 same
ContractJsonService {id, version, summary, multi, factsKey, facts: JsonSchema, cli: {group, aliases, hidden, extensible}, methods} One service. 03 §2.4 same
ContractJsonMethod {kind, command, deadlineMs, actors, destructive, input, output, summary, description, expose, cli, tool, target, auth, local, blocking, resumable, path, errors} One method; cli.fields/examples/flagAlias and tool.presentation/ui included. 03 §2.4 same
ContractJsonCliField {positional, flag, alias, ambient, hidden, variadic} CliFieldMeta on disk. 03 §2.4 same
parsePluginCatalog (value: unknown) => PluginCatalog Parses kernel/catalog.get's plugins / bb ui catalog --json at the boundary. Q38 sdk:catalog.ts
pluginCatalogSchema / pluginCatalogEntrySchema zod objects {version: 1, plugins, services, slots, eventKinds}; unknown keys stripped (I20), version: 1 is the only hard gate. Q38 same
PluginCatalog / PluginCatalogEntry z.output<…> The parsed types. Q38 same

2.10 The remaining declaration-merging hooks

Member Signature (abridged) What it does Spec As built
SlotMap interface SlotMap {} (merged) {"<slot>": {kind: SlotKind, scope: SlotScope, props: P}}; narrows app.slots.register and SlotComponentProps. 07 §5.9 plugin-sdk:app/slots.ts
SlotEntry<P> {kind, scope, props: P} The shape of one SlotMap entry. 07 §5.9 same
BbServices interface BbServices {} (merged) {"<pluginId>/<service>": ServiceClient<typeof def>} types sdk.plugins.<plugin>.<service>.<method> for @bb/sdk consumers. A plugin does not write this merge: ServiceClient is exported by @bb/sdk (packages/sdk/src/client.ts), not by any @get-bb/plugin-sdk entry, and a plugin may not import @bb/sdk (§2.1, D19); bb sdk types emits the merge from JSON Schema for scripts. 03 §4.1 sdk:client.ts
Events / Commands see §2.4 / §2.5 The other two merge targets, all augmented through @get-bb/plugin-sdk/contracts. 02 §5.1, §6.3 plugin-sdk:contracts/index.ts