# DOX — packages/dashboard-plugin-runtime/src/server

Files in this directory. One row per source file.

| File | Purpose |
|------|---------|
| `build-declaration-sdk.ts` | `computeBuildDeclaration(plugins, {isProd, bundleRoots})` — builds the served-artifact declaration from an EXPLICIT plugin list: selects through `selectClientRegistryPlugins` then delegates the digest to `pluginRegistryHash` (no second serializer), so the declaration hash equals the embedded `PLUGIN_REGISTRY_HASH` by construction. See change: add-served-build-coherence-and-hash-parity (design D2). |
| `build-metadata.ts` | Build-declaration shape + IO. `BUILD_DECLARATION_FILENAME` (`pi-dashboard-build.json`), `BUILD_DECLARATION_SCHEMA_VERSION`, `BuildDeclaration` `{schemaVersion, pluginRegistryHash, fixturePolicy}` (`FixturePolicy` = closed enum `excluded`\|`included`). `readBuildDeclaration(dir)` → six non-throwing outcomes (`valid`\|`missing`\|`malformed`\|`schema-mismatch`\|`hash-missing`\|`policy-unknown`); absent AND unreadable both map to `missing` (EACCES message never echoed — it embeds a path). `validateBuildDeclaration`, `isUsableDeclaration`, `writeBuildDeclaration` (fixed key order). No timestamps/paths → byte-reproducible. See change: add-served-build-coherence-and-hash-parity (design D1). |
| `client-registry-set.ts` | `selectClientRegistryPlugins(discovered, {isProd, bundleRoots})` — the ONE client-registry plugin-set selector: requires `clientEntryPath`, drops `fixture:true` under `isProd`, drops plugins outside `bundleRoots` (runtime-only roots cannot enter a bundle). `bundleRootsFor(repoRoot)` = `<repoRoot>/packages`. Every hash producer selects through it, so build-time and runtime hashes cannot drift (the pre-change split that made the staleness banner never converge). See change: add-served-build-coherence-and-hash-parity (design D0). |
| `config-redact.ts` | `redactWriteOnly(config, schema)` — strips every `writeOnly:true` property, recursing `properties` + `patternProperties` + object-shaped `additionalProperties` + array `items`; pure, same-reference no-op when nothing stripped. `redactPluginConfigForClient(pluginId, config, repoRoot?)` — discovery + schema load wrapper used at every client-facing config surface (GET /api/config, POST /api/plugins/:id broadcast, toggle broadcast, `updatePluginConfig` broadcast). **FAILS CLOSED**: an unresolvable plugin or an unloadable/unparseable declared schema returns `{}` + a log, NEVER the raw config (an unreadable schema is exactly when `writeOnly` is unknown → returning it verbatim would leak the pairing token). Only a resolved plugin with NO declared schema passes through. Tasks 2.12 / GAP A; fail-closed added by task 6.1 (Audit). |
| `config-validator.ts` | `validatePluginConfig`, `applySchemaDefaults` — Ajv JSON-Schema 7 validation for plugin config writes. |
| `fixture-gate.ts` | `FIXTURE_PLUGINS_ENV` (`PI_DASHBOARD_FIXTURE_PLUGINS`), `fixturePluginsEnabled(env?)` (exactly `"1"`), `fixtureEntryAllowed(manifest,env?)`. Gates `fixture: true` server/bridge entries (loader, server bridge auto-registration) and keeps fixture clients in a production vite build. Set only by the docker test harness. See change: expose-plugin-credential-and-oauth-seams. |
| `index.ts` | Server-side barrel for `@blackbelt-technology/dashboard-plugin-runtime/server`. Re-exports `loader`, `server-context`, `ws-route-registry`, `plugin-status-store`, `config-validator`, `requirement-probes`, `service-probes/model-proxy`, plus pure-TS `../dependency-graph` (avoids pulling React-y main barrel). Also re-exports `client-registry-set`, `build-metadata`, `build-declaration-sdk` (the shared plugin-set selector + served-artifact declaration). See change: add-served-build-coherence-and-hash-parity. Re-exports `fixture-gate` + `loopback-callback`. See change: expose-plugin-credential-and-oauth-seams. |
| `installed-probe.ts` | `createIsPiExtensionInstalled({listGlobal, listLocal, now?, ttlMs?})` — host factory for the `isPiExtensionInstalled` capability (design D2). Answers from the UNION of global+local scopes (superset of the probe's global-only wiring), matched with `installedMatchesName` (id/name/displayName/source incl. `sourcesMatch`). Boolean-only; success-only cache (~30s TTL, mirrors `requirement-probes.ts`); a scan failure REJECTS — never resolves `false` — and is never cached. See change: add-blackhole-session-pipeline. |
| `loader.ts` | `discoverPlugins` + `loadServerEntries` (failure-isolated) + `getPluginStatusStore`. DISCOVERY CONTRACT: each search root is scanned ONE level (`<dir>/<entry>/package.json`), so a SCOPED package left in a node_modules tree appears as `@scope/`, has no package.json, and is NEVER discovered — user-installed plugins must be materialised as top-level directories under `~/.pi/dashboard/plugins/`, each carrying its own node_modules (`qa/tests/35-plugin-install-load.sh` does exactly this). Roots in priority order: monorepo `packages/`, `~/.pi/dashboard/plugins`, bundled `resources/plugins`. Because the monorepo root is found by walking UP from this module, a worktree boot discovers the MAIN checkout's plugins — a worktree dev boot is never a valid test of worktree plugin code. Adds `deterministicSerializePlugins` (sorted manifest fields, stable JSON) + `pluginRegistryHash(plugins)` (sha256 over serialization). Shared by vite-plugin (build-time `PLUGIN_REGISTRY_HASH` emission) + server (`/api/health.bundleHash`). See change: fix-pi-flows-end-to-end. `loadServerEntries` opens/closes the WS-route ACTIVATION WINDOW around each server entry (`beginActivation` before `await mod.default(ctx)`, `endActivation` after) — `ctx.registerWsRoute` succeeds only inside it; a disabled plugin or a failed activation calls `teardownPlugin(id)` (unregister scopes, close tracked sockets 1001, tombstone prefixes). Re-pass with the plugin re-enabled opens a fresh window → registers afresh. See change: add-browser-relay. `ServerPluginContext` + `ServerContextDeps` gain `sendExtensionMessage(sessionId, msg)` (`SendExtensionMessageFn`) — raw server→extension control message to ONE session's bridge socket, WITHOUT the `pi.events` re-emit `plugin_emit_event` does; trust-gated (priority ≤ 100) like `emitEventToSession`. See change: wire-mcp-session-token (D5). `loadServerEntries` treats a `fixture: true` plugin as client-only unless `fixtureEntryAllowed`. See change: expose-plugin-credential-and-oauth-seams. |
| `loopback-callback.ts` | `createLoopbackCallback({path='/callback',timeoutMs=300_000,signal,successHtml})` → `Promise<{redirectUri,state,waitForCode,close,server}>`. Binds `127.0.0.1:0`, self-generated 32-byte base64url `state`. State check FIRST (length-checked `timingSafeEqual`): mismatch/missing → 400 keep waiting (even with `error=`); other paths 404; state-matched `error=` rejects `LoopbackCallbackError{code}`; match → 200 page + resolve + close. Timeout/abort reject; `close()` idempotent. See change: expose-plugin-credential-and-oauth-seams. |
| `plugin-enabled.ts` | `resolvePluginEnabled(configValue, defaultEnabled)` — explicit boolean `enabled` in config wins → manifest `defaultEnabled` → else `true`. Lets `browser` ship disabled by default (`defaultEnabled:false`) without changing plugins that omit the field. Honoured by the server loader `isEnabled` (feeds `/api/health.plugins[].enabled`) and `plugin-activation-routes.ts`. Task 2.13 / GAP B. |
| `plugin-status-store.ts` | In-memory `PluginStatusStore` for `/api/health.plugins[]`. Adds `recordBridgeProbe(pluginId, snapshot)` + `getBridgeProbe(pluginId)`. `listAll()` enriches returned status rows with `lastProbe`. Plugins (e.g. flows-anthropic-bridge) push runtime telemetry via `recordBridgeProbe`; surfaces in health endpoint without dedicated REST route. See change: fix-pi-flows-end-to-end. |
| `requirement-probes.ts` | Declarative requirement probes for `PluginManifest.requires`. Exports `probePiExtension(id, listInstalled)` (cross-refs installed pi-extension set), `probeBinary(name)` (PATH lookup via tool registry), `probeService(name, detectors)` (dispatches to service-probe map), `runRequirementProbes(requires, deps)` (parallel probes → `PluginRequirementReport`), `missingFromReport(report)` (flat `string[]` for UI). 30s in-process cache keyed by category+name. See change: add-plugin-activation-ui. `installedMatchesName` source comparison delegates to `sourcesMatch`; piExtensions requirement now satisfied by git/local-build installs (behavior follows shared matcher fix, no code change). See change: roles-standalone-defaults-and-local-install-detection. Adds `probePath(rawPath, deps)` — absolute-path existence probe with `${configKey}` interpolation from the plugin's validated config (`pluginConfig`/`configSchemaKeys`/`pathExists` deps); wired into `runRequirementProbesFor` (paths after services) + `missingFromReport`. See change: add-apple-tools-imcp-plugin. Exports `installedMatchesName` (was private) so the `isPiExtensionInstalled` capability matches with the SAME logic, not a weaker source-only comparison. See change: add-blackhole-session-pipeline. `RequirementProbeDeps` drops `fetchImpl` and gains `isModelProxyEnabled?: () => boolean`; the closed service registry's sole entry is renamed `pi-model-proxy` → `model-proxy`, backed by `probeModelProxy(deps)` (no HTTP — boot-time mount flag). See change: remove-pi-model-proxy-upstream-references. |
| `server-context.ts` | `createServerPluginContext(deps, pluginId)` factory — creates `ServerPluginContext` scoped to plugin with namespaced logger + typed config accessors. `ServerPluginContext` gains `onEvent(handler)` — subscribe all forwarded pi events, returns unsubscribe. Real `registerPiHandler` channel. `sendToSession(sessionId, text)` — send prompt/command into session; `/`-prefixed text routes to extension-command dispatch. New types `OnEventFn`, `SendToSessionFn`. See change: add-goal-continuation-plugin. ServerPluginContext gains `spawnSession(opts)` hook (PluginSpawnOptions/PluginSpawnResult/SpawnSessionFn), gated to first-party plugins. ServerPluginContext + ServerContextDeps gain abortSession: AbortSessionFn ((sessionId)=>boolean). New type AbortSessionFn. Gated to trusted plugins by host (mirrors spawnSession); untrusted returns false. createServerPluginContext forwards deps.abortSession. See change: add-automation-plugin. See change: automation-ui-mockup-parity. ServerPluginContext gains `provide(name,value)`/`consume<T>(name)` cross-plugin service seam. ServerContextDeps gains provide/consume. See change: register-plugin-automation-events. ServerPluginContext gains `emitEventToSession(sessionId, eventType, data?)` (EmitEventToSessionFn); trust-gated; relays `plugin_emit_event` control message to session. See change: automation-emit-configured-event. ServerPluginContext gains `consumeAll(prefix)` — enumerate host service board by key prefix; publish/collect; in-process only. See change: decouple-automation-action-registry. ServerPluginContext + ServerContextDeps gain `abortSpawnedRun(args): Promise<boolean>` (AbortSpawnedRunFn, args `{sessionId?,spawnToken?,graceful?}`; RENAMED from `abortAutomationRun`/`AbortAutomationRunFn` — generic spawned-run kill shared by automation AND goal-supervisor, no behavior change); trust-gated by host; untrusted resolves false. Terminates a spawned driver session (Stop hard-kill / completion graceful). See changes: fix-automation-stop-zombie-runs, add-goal-session-supervisor. ServerPluginContext + ServerContextDeps gain `onSessionEnded(handler)` (OnSessionEndedFn) — subscribe session-end/unregister, returns unsubscribe; fired from the transport/liveness layer even when no terminal pi event was forwarded. See change: finalize-automation-run-on-session-death. ServerPluginContext + ServerContextDeps gain optional `modelRuntime` (`PluginModelRuntime` = `{getModelRegistry():Promise<PluginModelRegistry\|null>, streamSimple:PluginStreamSimpleFn}`) — in-process model registry + streamSimple seam (creds resolved server-side; no model-proxy loopback); absent when the proxy is unavailable so plugins degrade. New types `PluginModelRuntime`/`PluginModelRegistry`/`PluginStreamSimpleFn`. Host wires it in `server.ts` mirroring the grammar-route adapter (`system`→`context.systemPrompt`). See change: make-grammar-fully-plugin-contained. ServerPluginContext + ServerContextDeps gain optional `isPiExtensionInstalled(name): Promise<boolean>` (`IsPiExtensionInstalledFn`) — registry-backed boolean-only existence oracle, ungated, host-cached ~30s, union of global+local scopes; scan failure REJECTS (never resolves false); OPTIONAL so injected test contexts / older hosts stay valid (plugin owns the fallback); host wires via `createIsPiExtensionInstalled` (installed-probe.ts). See change: add-blackhole-session-pipeline. `RegisterPiHandlerFn` widened to `(msg, sessionId)` — the CONNECTION's registered session id, passed through `dispatchPluginPiMessage`, NOT `msg.sessionId`; lets a plugin attribute a bridge message as a trust decision (mint a session-scoped credential) rather than a hint. Additive: `(msg)`-only handlers stay valid. See change: add-dashboard-mcp-server. `PluginSpawnOptions` gains optional `scope` block (`tools`/`excludeTools`/`noBuiltinTools`/`noTools`/`skills`/`noSkills`/`extensions`/`extensionConfig`) constraining the spawned session's tool/skill/extension surface. Deliberately NO `noExtensions` toggle — disabling extension discovery would stop the dashboard bridge loading and make the session uncontrollable (design D2/D6); `extensions` is an additive allowlist, discovery still runs. New export `pluginSpawnToSessionOptions(opts): MappedSpawnOptions` — total, pure mapper (NEVER throws; plugin input is untrusted JS). Reproduces the old inline hook literal (`strategy:"headless"`, `--model`, `--name` from `automationRun?.name`) + flattens `scope` to flat `SessionFlags` argv fields plus `extensionConfig` (env). Sanitizes: malformed containers treated as absent, non-string/empty/NUL argv entries dropped, NUL env values dropped; conflicting `noTools`+`tools` BOTH forwarded (pi arbitrates, D3). New type `MappedSpawnOptions` (extends `SessionFlags` + `strategy` + `extensionConfig`). Host hook in `server.ts` calls the mapper BEFORE `pendingAutomationRunRegistry.enqueue` so a rejected input cannot strand a stale `cwd`-keyed stamp (D7). `scope.extensionConfig` values widened to `string \| string[]` — scalar env verbatim, array `JSON.stringify` (D8). ServerPluginContext + ServerContextDeps gain `registerCwdPolicy(cwd, policy)` / `unregisterCwdPolicy(cwd)` (RegisterCwdPolicyFn/UnregisterCwdPolicyFn) + type `PluginCwdPolicy` (tightening fields only, NO extensions/extensionConfig) — pin a host cwd capability floor; trust-gated (priority≤100, untrusted no-op); host wraps the server's `CwdPolicyRegistry`, which THROWS on extension fields / overly-broad targets.   `PluginSpawnOptions` gains `spawnToken?` (caller-supplied correlation token used verbatim, trusted-only, duplicate rejected via registry `has()`), `resume?: {sessionFile}` (mapper → session-level `{sessionFile, mode:"continue"}`), `initialPrompt?` (host enqueues pre-spawn, consumes on failure). `ServerPluginContext` + deps gain `mintSpawnToken()` (trusted-gated, untrusted THROWS), `renameSession(id,name)` (trusted-gated; memory + `{name}` broadcast + `rename_session` to pi; false for unknown session or empty name — empty REJECTED, not normalized), `assignSessionRef(id, ref, {persist})` (trusted-gated; registry-sanitized merge; persist=true memory+meta+broadcast, false memory-only C2e; undefined clears), `networkGuard` (host guard for plugin routes, NOT trust-gated), `onShutdown(fn)` (dispatched in stop() before `piGateway.stop()`, try/catch per sub). New types MintSpawnTokenFn/RenameSessionFn/AssignSessionRefFn/OnShutdownFn/PluginNetworkGuard. See change: relocate-goal-product-to-plugin. `ServerPluginContext` + `ServerContextDeps` gain `registerWsRoute(scope, opts)` — bound in `createServerPluginContext` to `getWsRouteRegistry().register(pluginId, ...)` (pluginId from the manifest, not self-declared); valid only during the plugin's activation window. See change: add-browser-relay. `ServerPluginContext` + `ServerContextDeps` gain `listWorkspaces(): PluginWorkspace[]` + `onWorkspacesChanged(handler): () => void` (types `PluginWorkspace`/`ListWorkspacesFn`/`OnWorkspacesChangedFn`) — read-only, store-anchored, over-fire-tolerant workspace seam; NOT trust-gated; `ServerContextDeps` fields optional (context defaults them so test hosts stay valid). See change: add-chat-gateway-team-controls. `ServerPluginContext` gains optional `credentials` (`PluginCredentials`, own namespace), `oauth.startFlow({key,loginFlow,persist})` (rejects `PluginFlowStartError{code:start_timeout|login_failed}`), `registerPiRequestHandler(type,handler)`; deps `pluginCredentials(id)`, `startPluginOAuthFlow(id,opts)` → `PluginFlowStartResult`, `registerPiRequestHandler(id,type,h)`, all bound to the manifest id. Mirrors `PluginOAuthLoginFlow`/`PluginLoginInteraction`/`PluginLoginPrompt`/`PluginLoginEvent`/`PluginOAuthCredential`; `PluginRequestHandler`. See change: expose-plugin-credential-and-oauth-seams. |
| `ws-route-registry.ts` | Plugin-owned WS route scopes (add-browser-relay D1). `WsRouteRegistry` + singleton `getWsRouteRegistry()` (mirrors plugin-status-store). `register(pluginId, scope, opts)`: kebab scope; `pathPrefix` `/ws/<seg>/`-shaped; THROWS on duplicate scope, overlapping/nested prefix (both directions), reserved core scopes (`browser`/`terminal`/`live`/`bridge`), reserved core routes (`/ws` exact path — plugin prefixes legitimately live under `/ws/<scope>/`; `/ws/terminal/`, `/live/`, `/ws/bridge` reserved as namespaces), or registration outside the activation window (`beginActivation`/`endActivation`, driven by loader.ts). `resolveScope(pathOnly)` — active scope for a prefix (server wires it into `routeScopeForUrl` via `setPluginScopeResolver`). `trackSocket(pluginId, ws)` (via `meta.trackSocket`; close-event cleanup) + `teardownPlugin(pluginId)` — close tracked sockets `close(1001, "plugin disabled")`, unregister scopes, tombstone prefixes (`isTombstonedPath` → later upgrades 404); idempotent. `WsRouteRegistration` = `{pathPrefix, admitOrigins: readonly string[], handleUpgrade(request, socket, head, meta)}`; `admitOrigins` copied by value at register time. See change: add-browser-relay. |
