6. Host tier — @get-bb/plugin-sdk/host

The host tier is plugin code that runs in a child process on a host machine (the core machine or an enrolled remote host). Node is allowed here. @get-bb/plugin-sdk/host is the authoring surface: defineHostEntry builds the entry's default export, defineHostRole and defineEnvironmentProvider build the rows the bootstrap selects, defineHostCommands / implementHostCommands / signalEmitter give typed commands and signals, and roleStorage / roleTempStorage open the role's private directory. SDK source: packages/plugin-sdk/src/host/index.ts. Kernel: packages/kernel-host (spec 05). Built wins; every deviation is marked.

Status legend in the tables: built (shipped and exercised by a kernel caller), served (the role process answers it, no kernel caller found), no-op, absent, unwired.

6.1 defineHostEntry and the bootstrap

Member Signature (abridged) What it does Spec As built
defineHostEntry (input: HostEntryInput) => Required<HostEntry> The default export of host.ts. Fills apiVersion: 1; turns role rows into roles[role][name]; derives commands as the union of the rpc rows' tables; defaults signals to {} and dispose to a no-op. Two rows with the same (role, name)conflict; one command name in two rpc rows → conflict. 05 §4.7, §6.2 built; HostEntryInput has no commands (SDK README dev. 8)
HostEntryInput { roles?: HostRoleDefinition[] | HostEntry["roles"]; signals?: Record<string, HostSignalDef>; dispose?: () => Promise<void> | void } roles is either rows from defineHostRole or the map itself (a provider bridge is { "provider-bridge": { codex: defineBridge(…) } }). signals keys must equal contributes.hostSignals. 05 §6.2 built
HostEntry { apiVersion: 1; roles?: Partial<Record<HostRoleName, Record<string, unknown>>>; commands?; signals?; dispose? } kernel-host's erased shape of the default export (port/environment-provider.ts); commands and signals are Record<string, unknown>. In a test, narrow before calling: const handler: unknown = entry.commands["host-greet"]; if (typeof handler !== "function") throw new Error("no host-greet"); await handler({ name: "bb" }, ctx.context) (hello-slot host.test.ts); the table validates input and output itself (§6.3), so invalid_input is observable there. 05 §6.2 built; defineHostEntry returns Required<HostEntry>, no typed index

The bootstrap is one program for every launch.kind: "module" role, including provider bridges: bin/bb-host-role.mjslocal/bootstrap.ts#main (05 §6.2). What it does, in order:

  1. Parses the kernel-owned argv: --plugin --generation --artifact --role --name --export --data-dir --temp-dir --cwd [--env <envId> --write-roots <json> | --root <RootRef json>] --limits <RoleLimits json>.
  2. umask 077, chdir(--cwd) (the scope root, or the temp dir for scope none), ignores SIGINT, installs SIGTERM → default.dispose()exit(0).
  3. Registers a resolver hook so the entry's bare @get-bb/plugin-sdk/* imports resolve from the host's own install, not the artifact cache (local/sdk-resolve.ts).
  4. Imports <artifact>/meta.json#entry (default host.mjs) and selects the export: the named export --export if present, else default.roles[role][name]; neither → exit 1.
  5. provider-bridge: the export must be a defineBridge() object; prints role/ready and hands stdio to runBridge. Every other role: serveRole prints role/ready then runs a JSON-RPC 2.0 line loop on stdio (@bb/bridge-kit peer; maxLineBytes from --limits).
  6. Dispatch in serveRole: env/provision|reconnect|destroy|summarize when the export has a provision function (an EnvironmentProvider); <pluginId>/<command> → the export's property of that name called as (params ?? {}, ctx); role/dispose → abort lifecycle; anything else → JSON-RPC METHOD_NOT_FOUND. stdin EOF aborts lifecycle and ends the process.

Deviations from 05 §6.2 in the built bootstrap (local/bootstrap.ts): the export is duck-typed (an object with provision is a provider; kind: "provider-bridge" is a bridge; anything else is a command table), not validated with a Standard Schema; the spec's "or commands for the rpc role" fallback is not implemented, so only roles[role][name] is ever selected (D14); the child does not self-exit on idle — idle eviction is the supervisor's (§6.7). launch.kind: "exec" spawns command args directly with the same cwd/env/stdio discipline and no bootstrap code.

6.2 Roles

Member Signature (abridged) What it does Spec As built
defineHostRole <C extends HostCommandsContract>(input: HostRoleInput<C>) => HostRoleDefinition One row. environment-provider: refuses a provider whose .name differs from the row name (invalid_contract). Command roles: the export is implementHostCommands(contract, commands). provider-bridge is not a defineHostRole input — pass the map to defineHostEntry with a defineBridge() object (@get-bb/plugin-sdk/bridge). 05 §6.1 built
HostRoleInput<C> { role: "environment-provider"; name; provider: EnvironmentProvider } | { role: CommandRoleName; name; contract: C; commands: HostCommandHandlers<C> } The two row shapes. 05 §6.1 built
HostRoleDefinition { role: HostRoleName; name: string; export: EnvironmentProvider | HostCommandTable } What defineHostEntry places at roles[role][name]. built
HostRoleName "provider-bridge" | "environment-provider" | "skill-discovery" | "terminal-backend" | "ai-service" | "rpc" The closed list (HOST_ROLE_NAMES, port/schemas.ts); an unknown role is a manifest error. No open-target role (D14). 05 §2.10, §6.1 built
CommandRoleName Extract<HostRoleName, "skill-discovery" | "terminal-backend" | "ai-service" | "rpc"> The roles whose export is a command table. 05 §6.1 built

What each role is for (05 §6.1) and what drives it today:

Role Purpose Scope Driven by today
provider-bridge A provider plugin's bridge child (protocol v3, 06 §6.2); name is the flat provider id (codex). Never relayed to core. env built: bridges/driver.ts
environment-provider Provisions, reconnects, destroys and summarizes environments; provider id <pluginId>/<name>. none (gets the env on provision) built: local/environments.ts calls env/provision, env/reconnect, env/destroy, notifies env/cancel; env/summarize is served by the bootstrap with no kernel-host caller found
skill-discovery A command role the skills plugin uses for staging/listing skills in a scope (11b). env or root served by the bootstrap's <pluginId>/<command> dispatch; no first-party driver in kernel-host
terminal-backend Alternative pty source for panel-terminal over ctx.openLane("pty"). env served as a command role only; openLane is absent (§6.4), so the pty path has no built transport
ai-service Plugin-declared commands a server half calls through hostClient (e.g. inference.complete). none served; the server-side ctx.hostClient is unwired (§6.3)
rpc The owning plugin's general commands and signals; the only rows whose tables feed entry.commands. none, env or root served; ctx.hostClient unwired in the product, wired end to end by createTestPlugin(...).host.role(name) (@get-bb/plugin-sdk/testing)

One process per (pluginId, generation, role, name, scope); the scope comes from the caller that starts the role (RoleSpec.scope), not from the manifest.

6.3 Host commands and signals

The contract object lives in the plugin's contracts module and is the one thing both ends validate against (05 §4.7). The manifest carries names only (§6.8).

Member Signature (abridged) What it does Spec As built
defineHostCommands ({ id, commands: Record<string, HostCommandDef>, signals?: Record<string, HostSignalDef> }) => HostCommandsContract<C, S> id is the plugin id (^[a-z0-9][a-z0-9-]{0,63}$); command and signal names are kebab-case ^[a-z][a-z0-9-]*$; omitted signals = {}. Wire method names are <id>/<command>. Re-exported from /contracts and /server. 05 §4.7 built
HostCommandDef<In, Out> { readonly input: StandardSchemaV1; readonly output: StandardSchemaV1 } One command's schemas. 05 §4.7 built
HostSignalDef<P> { readonly payload: StandardSchemaV1 } One signal's payload schema. 05 §4.7 built
HostCommandsContract<C, S> { readonly id: string; readonly commands: C; readonly signals: S } The contract object. 05 §4.7 built
implementHostCommands <C>(contract: C, handlers: HostCommandHandlers<C>) => HostCommandTable Builds the table the bootstrap dispatches to. Missing handler or a handler for an undeclared name → invalid_contract at definition time. Each entry validates params with input (invalid_input), calls the handler, validates the result with output (invalid_output), and returns the JSON round-trip of the validated output. 05 §4.7 built
HostCommandHandler<D> (input: InferOutput<D["input"]>, ctx: RoleContext) => Promise<InferInput<D["output"]>> A typed handler; it receives the parsed input. 05 §4.7 built
HostCommandHandlers<C> { [N in keyof C["commands"]]: HostCommandHandler<…> } The handler record defineHostRole takes. built
HostCommandTable Record<string, (params: JsonValue, ctx: RoleContext) => Promise<JsonValue>> The erased table at roles.rpc[name] and entry.commands. 05 §4.7 built
signalEmitter <C>(contract: C, ctx: Pick<RoleContext, "emitSignal">) => (name, payload) => Promise<void> Validates payload against signals[name].payload (invalid_input; unknown name → invalid_contract) and calls ctx.emitSignal with the JSON round-trip. On the wire it is the role/signal notification, which rpc/role-client.ts parses. 05 §4.7 built

The server side (@get-bb/plugin-sdk/server, server/host-client.ts): ctx.hostClient(contract, name) returns TypedHostClient<C>call(command, input, opts?: HostCallOptions) validates input before the frame leaves core and output after rpc.res arrives; onSignal(name, listener) validates the payload and logs instead of delivering a payload outside the schema (hostClient.onSignal: payload rejected by the signal schema); onExit(listener) receives HostExit { roleId, exitCode, signal }. HostCallOptions is { hostId?: string | null; timeoutMs?: number | null } (null = the core's own host / the loader's default deadline). name must be a contributes.hostRoles[].name of this plugin (precondition otherwise).

As built: the loader's hostClient factory is unwired in the product — a call answers service_unavailable (D15; as-built §6 "Loader hostClient unwired"). Host tiers still start. In tests createTestPlugin wires host.role(name) to the same roles[role][name][command] dispatch the bootstrap uses and relays signals and exits. Also as built: 05 §4.7's rule that the loader refuses a host entry whose commands/signals exports differ from contributes.hostCommands/hostSignals has no implementation in kernel-loader, kernel-host or plugin-build; the manifest lists are parsed (kernel-loader/src/manifest.ts), each hostSignals name is subscribed for relay (loader/load.ts), and an undeclared <pluginId>/<command> is answered by the role process with METHOD_NOT_FOUND.

// contracts.ts — schemas only (examples/plugins/hello-slot/src/contracts.ts)
export const helloHost = defineHostCommands({
  id: "hello-slot",
  commands: { "host-greet": { input: z.strictObject({ name: z.string() }), output: hostGreetingSchema } },
});
// host.ts — the role process answers it; ctx is the RoleContext of §6.4
export default defineHostEntry({
  roles: [defineHostRole({ role: "rpc", name: "echo", contract: helloHost,
    commands: { "host-greet": async ({ name }, ctx) => ({ greeting: `Hello, ${name}`, pid: process.pid, role: ctx.name }) } })],
});

Server-side calls at dc07292bf. Core wires ctx.hostClient through createHostClients(...).clientFor (packages/core/src/core.ts); createRoleSpecs is shared by ctx.hostClient, kernel/host-runtime, and kernel/bridge-driver. The "unwired" status in D15 describes 6f3d4592f.

6.4 RoleContext

HostRuntime (port/host-runtime.ts, 05 §2.1) is the kernel's nine-member port, held by core and never by a plugin: fs (stat/read/write/list/search/mkdir/move/remove/exists/browse, every call scoped), exec (run buffered, spawn streaming), vcs (git, repoInfo, lock — a host-wide named mutex), pty (open/attach/list), watch (set, events), environments (provision/reconnect/destroy/cancel/loaded), artifacts (ensure/has/gc), roles (start/attach/list/stop), events (append, flush-before-result); plus hostId and info. A role process gets the narrowed, scope-bound form below (bindScope; pty and watch are absent inside a role process by design, BindablePorts). Built from local/bootstrap.ts#roleContextFor.

Member Signature (abridged) What it does Spec As built
pluginId, generation, role, name string, string, HostRoleName, string Identity from the argv. 05 §6.3 built
paths { dataDir: string; tempDir: string } plugins/host/<id>/ and plugins/host/<id>/tmp under the host data dir, 0700; tmp is wiped on every role start (local/roles.ts). 05 §6.3, §2.12 built
lifecycle { signal: AbortSignal } Aborts on role/dispose or stdin EOF. SIGTERM runs default.dispose() directly. Nothing in kernel-host sends role/dispose today. 05 §6.3 built
scope { kind: "env"; envId; root; writeRoots } | { kind: "root"; ref: RootRef; root } | { kind: "none" } Where the role was started. With none the ports below are bound to the plugin root ({ kind: "plugin", pluginId } = dataDir). 05 §6.3 built
fs ScopedFs (stat, read, write, list, search, mkdir, move, remove, exists, browse) The fs port with the scope bound; contained to the scope root and writeRoots; escaping → host/unknown_root. 05 §2.3, §6.3 built
exec ScopedExec (run(spec), spawn(spec)) ExecSpec { command, args, cwd, env, stdin: "closed" | "pipe", timeoutMs, maxOutputBytes, processGroup }; cwd must be inside the scope. 05 §2.4 built
vcs ScopedVcs (git(args, opts), repoInfo(), lock(name, fn)) lock is keyed on the repo's common dir and <pluginId>/<lockName>. 05 §2.5 built
process { killByCwd(root, { graceMs }): Promise<number>; spawnGroup(spec): Promise<ExecProcess> } killByCwd SIGTERMs every process rooted under root, waits, SIGKILLs survivors, up to 5 rounds, returns the count; spawnGroup is exec.spawn (process group when the platform supports it). 05 §5.5, §7 built
watch (paths, { recursive, ignore, debounceMs }, handler) => () => void Would deliver WatchEvents. 05 §6.3 no-op: returns a no-op unsubscribe, no events (D14; kernel-host README follow-ups)
openLane (kind: "pty" | "exec") => Promise<Lane> Lanes for custom handles and terminal-backend. 05 §6.3, §4.9 absent from the built type (D14)
emitSignal (name: string, payload: JsonValue) => void role/signal notification to the host; use signalEmitter so the payload is validated first. 05 §4.7 built
emitEvent (event: Omit<HostEventInput, "actor">) => void role/event notification; the kernel stamps actor: { kind: "plugin", id: pluginId } and appends through EventsPort (parsed in local/environments.ts). 05 §2.11, §6.3 built
retain () => Lease ({ release() }) Keeps the process past the idle timeout. 05 §6.4 no-op in the child; leases are held host-side on RoleInstance.retain() (D14)
log { info(msg), warn(msg), error(msg) } Writes [info]/[warn]/[error] lines to stderr; the supervisor keeps the last maxStderrLines, 16 KiB per line, and reports them on exit. The role/log notification exists in roleNotifications but the built log does not use it. 05 §6.3 built

6.5 Environment providers

Member Signature (abridged) What it does Spec As built
defineEnvironmentProvider <Options extends JsonObject>(provider: EnvironmentProvider<Options>) => EnvironmentProvider<Options> Identity with one check: name matches ^[a-z][a-z0-9-]*$ (invalid_contract). The provider id is <pluginId>/<name>. 05 §5.2, §5.3 built
EnvironmentProvider<Options> { name; label; ownsRoot: boolean; options: StandardSchemaV1<Options>; provision(req, ctx); reconnect(req, ctx); destroy(req, ctx); summarize(handle, ctx) } options is required: core validates API input and renders the picker from it. ownsRoot: true → the row is managed (bb created the directory and destroys it). 05 §5.2 built
provision ({ envId, options, source: ProjectSource, signal: AbortSignal, progress: ProgressSink }, ctx) => Promise<EnvironmentHandle> The bootstrap validates options with the provider's schema first (INVALID_PARAMS on issues), runs the call under an AbortController keyed by envId; the env/cancel notification aborts it. 05 §5.2 built (env/provision, env/cancel)
reconnect ({ envId, handle }, ctx) => Promise<EnvironmentHandle> After a host restart: re-validate and re-lock; return the (possibly refreshed) handle. 05 §5.2 built (env/reconnect)
destroy ({ envId, handle, signal, progress }, ctx) => Promise<void> Remove what provision made; abortable like provision. 05 §5.2, §5.5 built (env/destroy)
summarize (handle, ctx) => Promise<EnvironmentSummary> The chip-sized description the UI reads. 05 §5.2 served (env/summarize in the bootstrap); no kernel-host caller found
EnvironmentHandle { kind: "local-path"; root: string; writeRoots: string[]; persisted: JsonObject } | { kind: "custom"; writeRoots: string[]; persisted: JsonObject } local-path: the kernel serves fs/exec/vcs/pty natively at root. custom: the provider would serve them over role RPC and lanes; no v1 implementation, and openLane is absent. persisted is opaque to core and handed back on reconnect/destroy. 05 §5.2 local-path built; custom shape only
EnvironmentSummary { label: string; path: string | null; isRepo; isWorktree; branch | null; baseBranch | null; defaultBranch | null } Persisted as the environment row's path + vcs columns. 05 §5.1 built
ProgressSink { step(key, text, status: "started" | "completed" | "failed"): void; output(line): void } role/progress notifications; they land in the thread timeline as environment/provision-progress events. 05 §5.2 built
ProjectSource { id; projectId; hostId; path; gitRemoteUrl: string | null; isDefault: boolean } The source the environment is provisioned from; path is absolute on the host. 05 §5.0 built

Containment rules a provider must follow (05 §5.2, §5.5; examples/plugins/hello-slot/src/host.ts):

  • A persisted handle is input: the row may be stale, tampered with, or mis-routed from another provider. Re-derive the expected root from envId and refuse a handle whose kind or root differs (precondition) before any filesystem call.
  • Do filesystem work through roleStorage(ctx) (§6.6) or the scoped ports; both refuse a path that escapes their root. A provider role's scope is none, so ctx.fs/exec/vcs are bound to the plugin root (dataDir).
  • Destroy order: the kernel stops the env's bridges and ptys → the provider calls ctx.process.killByCwd(root, { graceMs: 2000 }) → the provider removes the directory → core applies destroy.completed. A result lost to a disconnect becomes destroy.lost; reconcile answers it on the next session.
  • Per-environment mutation serialization (05 §4.5) is not implemented on the host; ctx.vcs.lock is the mutex you have (packages/kernel-host/README.md, "Deviations" part 2).
// a provider that owns a directory under the role's data dir (abridged from hello-slot `scratch`)
export const scratch = defineEnvironmentProvider<Record<string, never>>({
  name: "scratch", label: "Scratch directory", ownsRoot: true, options: z.strictObject({}),
  async provision({ envId, progress }, ctx) {
    const storage = roleStorage(ctx); const rel = `scratch/${envId}`;   // envId checked to be one path segment
    progress.step("mkdir", "creating", "started"); await storage.mkdir(rel); progress.step("mkdir", "created", "completed");
    return { kind: "local-path", root: storage.path(rel), writeRoots: [storage.path(rel)], persisted: {} };
  },
  async reconnect({ envId, handle }, ctx) { /* refuse a handle whose root ≠ storage.path(`scratch/${envId}`) */ return handle; },
  async destroy({ envId, handle }, ctx) { await roleStorage(ctx).remove(`scratch/${envId}`, { recursive: true }); },
  async summarize(handle) { return { label: "scratch", path: handle.kind === "local-path" ? handle.root : null, isRepo: false, isWorktree: false, branch: null, baseBranch: null, defaultBranch: null }; },
});

Changes at dc07292bf (kernel-host/src/port/environment-provider.ts, host-runtime.ts): provision receives source: ProjectSource | nullnull when the project has no source on this host; a provider that needs one refuses with host/source_required (env-local's local-dir and git-worktree do). The role answers env/describe {}{label, ownsRoot, optionsSchema} (the bootstrap converts the zod options to JSON Schema) for provider discovery (05 §6.5). EnvironmentsPort gains summarize({providerId, handle})EnvironmentSummary and describe({providerId})ProviderDescriptor. Provision progress reaches core as env.progress {envId, step, output} notifications; the host no longer appends an environment/provision-progress event.

6.6 Host storage

Member Signature (abridged) What it does Spec As built
roleStorage (ctx: Pick<RoleContext, "paths">) => HostStorage createHostStorage(ctx.paths.dataDir): the role's private dir (plugins/host/<id>/, 0700) as a Storage port. 05 §2.12 built
roleTempStorage (ctx: Pick<RoleContext, "paths">) => HostStorage Same over ctx.paths.tempDir; cleared on every role start. 05 §2.12, 00 §3.7 built
HostStorage = Storage (kernel-store) One implementation on every machine: kernel-store's createLocalStorage (R36 M4; kernel-host README deviation "HostStorage is kernel-store's createLocalStorage"). Never reachable over the remote-facing port. 05 §2.12 built

Every HostStorage method (packages/kernel-store/src/storage.ts); paths are relative to the root, and every call contains them lexically and physically — a path that escapes the root or crosses a symlink out of it is forbidden (storage-local.ts):

Method Signature (abridged) What it does
root readonly string The absolute root.
scope(dir) (dir: StorageScope) => Storage A narrower Storage at <root>/<dir>; dir must match ^(plugins\/(store|packages|src|data|host)(\/<id>)?|[a-z-]+)$.
path(rel) (rel: string) => string The absolute path, for import(), exec cwd and handle roots only — never for fs calls outside the port.
read / write read(path): Promise<Uint8Array>; write(path, bytes, { mode: 0o600 | 0o644; atomic: boolean }) atomic = temp + fsync + rename; every option is explicit.
rename / mkdir / list / stat / remove / copyTree rename(from, to); mkdir(path) (recursive); list(dir): StorageEntry[]; stat(path): StorageStat | null; remove(path, { recursive }); copyTree(from, to) The directory primitives. StorageEntry { name, kind, bytes, mtime }; StorageStat adds mode.
appendLog (path, line) => Promise<void> One JSONL line; rotates past 8 MiB, keeps five files per stem.
unpack (tarball: Uint8Array | ReadableStream, dest, { readOnly }) => Promise<{ files: string[] }> Tar (gzip detected from magic bytes); refuses links and contains every entry.
watch (path, cb: (e: StorageWatchEvent) => void) => Disposer { kind: "create" | "update" | "delete"; path }.
sqlite / sqliteReadOnly sqlite(path): Promise<SqliteHandle>; sqliteReadOnly(path): SqliteReadOnlyHandle An async read/write handle (exec/run/get/all/batch/close) owned by the caller; a synchronous query_only handle (get/all/close).

6.7 Limits and process hygiene

Role-RPC admission per (pluginId, generation) on a host (port/schemas.ts, local/roles.ts#admit, local/role-rpc.ts, rpc/role-client.ts; 05 §4.7): 256 active calls (service_unavailable past it), 32 MiB in-flight input (host/too_large), 8 MiB per result (host/too_large, checked on both the local and remote client), 30 s default per call (timeout); timeoutMs: null on the remote client waits indefinitely for a retained long call.

RoleLimits (manifest limits, defaults filled by the loader, DEFAULT_ROLE_LIMITS): maxRssBytes 1 GiB, idleMs 300 000, startTimeoutMs 10 000, stopGraceMs 5 000, maxLineBytes 1 MiB, maxStderrLines 1 000. The supervisor (local/roles.ts): waits for the role/ready line within startTimeoutMs else SIGKILL + host/role_start_failed; SIGKILLs a process that emits a stdout line over maxLineBytes; relays only lines that start with {; stops an idle process after idleMs when no host-side lease is held (any line in either direction resets the clock; idleMs: 0 disables); stop = leader SIGTERM → group → SIGKILL after stopGraceMs; never restarts — the next call that needs the role starts it again; every exit is forwarded to core as roles.exited {roleId, pluginId, role, name, exit, stderr}. maxRssBytes is carried in the spec but no enforcement was found in local/roles.ts.

Process env of a role child (local/shell-env.ts#roleProcessEnv, local/process.ts#sanitizeInheritedEnv; 05 §7, R18), in four layers: (1) the sanitized inherited env (05 §7: every BB_*, NODE_ENV, and npm_config_* dropped); (2) the login-shell PATH, captured once per host boot; (3) RoleSpec.env, the role's own non-BB_* keys; (4) the kernel overlay BB_CLI, BB_HOST_ID, and BB_SERVER_URL when the host has one. RoleSpec.env may carry no BB_* key at all (roleSpecSchema refine; stricter than 05 §7's BB_BRIDGE_RECORD exception). The per-thread names (BB_THREAD_ID, BB_PROJECT_ID, BB_ENVIRONMENT_ID, BB_THREAD_STORAGE) and plugin-contributed BB_* values from threads/agent-config are merged at bridge spawn only (mergeThreadEnv; a contributed kernel-owned name is overwritten and logged) — a role process is not a thread and never sees them. KERNEL_ENV_NAMES is the kernel-owned list. Also: umask 077; own process group where supported; cwd = scope root or the temp dir; call ctx.process.killByCwd before removing any directory (05 §5.5).

6.8 Manifest keys for the host tier

package.json#bb (01 §2.2, §2.6; kernel-loader/src/manifest.ts; R9):

Key Shape Rule
host "./src/host.ts" Source of the host entry; built to dist/host.mjs (artifact meta.json#entry, default host.mjs).
contracts "./src/contracts.ts" The module that exports the defineHostCommands object; also what the server half imports.
contributes.hostRoles[] { role: HostRoleName; name: ^[a-z0-9][a-z0-9-]*$; launch?: { kind: "module"; export } | { kind: "exec"; command; args }; limits?: Partial<RoleLimits> } launch defaults to { kind: "module", export: <name> }, so with a defineHostEntry default export no named export is needed; limits defaults are filled once. Duplicate (role, name) is refused. providers[].bridge must name a provider-bridge row.
contributes.hostCommands string[] Names only (^[a-z][a-zA-Z0-9-]*$ in the loader; defineHostCommands itself requires kebab-case). Schemas are code. No entry-vs-manifest comparison is built (§6.3).
contributes.hostSignals string[] Names only; the loader subscribes each for relay to hostClient.onSignal.

There is no hostRoots key and no bridgeEntries key (R9; 05 §6.5). Declared roles are not capabilities: a host can run a plugin's roles once artifacts.ensure of its host entry succeeded there (05 §6.5).

6.9 Types index

Type One line
CommandRoleName skill-discovery | terminal-backend | ai-service | rpc — roles whose export is a HostCommandTable (§6.2).
EnvironmentHandle local-path { root, writeRoots, persisted } or custom { writeRoots, persisted } (§6.5).
EnvironmentProvider<Options> The provider role contract: name, label, ownsRoot, options, provision, reconnect, destroy, summarize (§6.5).
EnvironmentSummary { label, path, isRepo, isWorktree, branch, baseBranch, defaultBranch } (§6.5).
HostCommandDef { input, output } Standard Schemas for one command (§6.3).
HostCommandHandler<D> (input, ctx: RoleContext) => Promise<output> (§6.3).
HostCommandHandlers<C> Handler record keyed by the contract's command names (§6.3).
HostCommandTable Erased Record<string, (params: JsonValue, ctx) => Promise<JsonValue>> the bootstrap dispatches to (§6.3).
HostCommandsContract<C, S> { id, commands, signals } from defineHostCommands (§6.3).
HostEntry kernel-host's shape of the default export: { apiVersion: 1; roles?; commands?; signals?; dispose? } (§6.1).
HostEntryInput { roles?, signals?, dispose? } for defineHostEntry (§6.1).
HostRoleDefinition { role, name, export } one row (§6.2).
HostRoleInput<C> The provider-row or command-row input of defineHostRole (§6.2).
HostRoleName The closed six-role list (§6.2).
HostSignalDef { payload } Standard Schema for one signal (§6.3).
HostStorage = Storage; the role's private dir as a port (§6.6).
ProgressSink { step(key, text, status), output(line) } (§6.5).
ProjectSource { id, projectId, hostId, path, gitRemoteUrl, isDefault } (§6.5).
RoleContext The role process's context (§6.4).
RootRef { kind: "source"; sourceId; path } | { kind: "thread-storage"; threadId } | { kind: "plugin"; pluginId } — a contained root; source carries the trusted path core filled in (D15, deviation from 05 §2.2).

Testing (@get-bb/plugin-sdk/testing, covered in its own section): fakeRoleContext(init) builds a RoleContext whose fs/exec/vcs/process throw host/unsupported unless supplied and records signals, events and logs; createTestPlugin(...).host.role(name) dispatches roles[role][name][command] exactly as the bootstrap does.