Developer preview. Pre-admission. Not for sensitive, proprietary, regulated, or production code. Expect breaking changes and data loss. Read the limits.

Building on the harness

[!WARNING] The extension surfaces are implemented but pre-admission. They can change without notice, and they are not hardened for sensitive codebases. Read the security model before exposing any extension to a model.

Bridlewise is arranged so a capability is declared once and its projections follow. This page names the owners and the extension points; it is a map, not a promise of API stability.

The shape

shared/                  wire contracts, routes, errors, UI schema
server/                  loopback API and trust boundary
  blocks/                one declaration → route + schema + client + UI contract
  tools/                 the web executor and tool settings
  llm/                   provider persistence and the redaction choke point
  run/                   run claim, stream, and recording
engine/                  standalone CLI and library
  src/tools/             tool manifest and built-in tools
  src/runtime/           provider adapter, tool registry, sessions
  src/editor/            transactional editor
  src/intelligence/      maps, graphs, symbols, slices
  src/quality/           quality packs
  src/memory/            unified and scale memory
  src/timeline/          append-only, hash-chained ledger
  src/safety/            pre-dispatch and runtime gates
  guardian/              Landlock guardian

The server drives the engine through engine/src/index.ts. The engine is provider- and transport-neutral; it owns domain behavior, not HTTP or UI.

Add a block (the server’s capability unit)

A block is one backend capability. It declares a read, actions, or both, and the route, validation, client binding, schema entry, and UI descriptor are derived from that declaration.

// server/blocks/my-feature.ts
import { z } from "zod";
import { defineBlock } from "./define.ts";

export const myFeature = defineBlock({
  id: "my.feature",                 // namespace.name
  title: "My feature",
  description: "One sentence a surface can render.",
  read: {
    params: z.object({ projectId: z.string().uuid() }),
    result: z.object({ value: z.string() }),
    handler: async ({ projectId }, ctx) => ({ value: await load(projectId, ctx) }),
  },
  actions: {
    refresh: {
      title: "Refresh",
      consequence: "Recomputes the value from the current project state.",
      params: z.object({ projectId: z.string().uuid() }),
      handler: async ({ projectId }, ctx) => ({ refreshed: await refresh(projectId, ctx) }),
    },
  },
});

The block is mounted by the registry; do not add a parallel route. The read route is GET /api/b/my.feature, and the action route is POST /api/b/my.feature/refresh. Every block read uses the shared envelope, including causality metadata. The UI descriptor is the contract the cockpit renders; a control without a title and consequence is refused at startup.

Owner: server/blocks/define.ts. Contract: shared/block-contract.ts.

Add a tool

Tools are governed by one manifest and one executor. A model-facing tool declares its identity, input/output schemas, capabilities, resource bounds, and adapter.

// engine/src/tools/my-tool.ts
import { createToolRegistry } from "../runtime/tool-registry.ts";

const registry = createToolRegistry({ repositoryRoot });
registry.register({
  definition: {
    name: "my_tool",
    description: "Reads one bounded value from the project.",
    parameters: { type: "object", properties: { key: { type: "string" } }, required: ["key"] },
  },
  handler: (args, context) => readValue(context.repositoryRoot, String(args.key)),
});

For a tool that the web executor dispatches, register it through the canonical tool surface in server/tools/ and the engine manifest in engine/src/tools/. Do not add a second execution path. A tool that needs an unconfigured adapter must return a typed blocker; it must not silently fall back.

Owner: engine/src/tools/tool-manifest.ts, engine/src/runtime/tool-registry.ts, server/tools/tool-executor.ts.

Add a provider

The provider adapter speaks the OpenAI Responses protocol and can fall back to Chat Completions. Provider records and encrypted credentials are stored per user in the web database; the engine adapter is constructed with an explicit provider config and key.

import { createProviderAdapter } from "bridlewise";

const adapter = createProviderAdapter({
  provider: {
    id: "local",
    baseUrl: "http://127.0.0.1:11434/v1",
    timeoutMs: 120_000,
    retries: { maxRetries: 2, initialBackoffMs: 500, maxBackoffMs: 8_000, backoffMultiplier: 2 },
  },
  apiKey: process.env.LOCAL_PROVIDER_KEY ?? "",
});

for await (const event of adapter.streamChatCompletion(request)) {
  // text, tool-call, usage, or finish event
}

The web path never reads a provider key from the environment at runtime. A user saves a provider through Settings; the key is encrypted in the database and resolved at the call site. The redaction owner runs before egress and before display.

Owner: engine/src/runtime/provider-adapter.ts, server/llm/providers.ts, server/llm/redaction.ts.

Add an MCP server

MCP servers are configured in engine/bridlewise.config.json under tools.mcpServers. Transports: stdio, Streamable HTTP, and a custom WebSocket transport. stdio servers are confined through Landlock to their own harness home. Tool names are minted as mcp__<serverId>__<toolName>.

{
  "tools": {
    "mcpServers": {
      "docs": {
        "url": "https://mcp.example.dev/mcp",
        "enabled": true
      }
    }
  }
}

A disabled or unreachable server contributes nothing. Credentials are resolved only from the declared allowlist, never from arbitrary HARNESS_* variables. Test the server from Settings; the connection status is recorded.

Owner: engine/src/mcp/, server/integrations/mcp-runtime-bridge.ts.

Embed the engine

engine/src/index.ts is the public barrel. It exposes the CLI runner, provider adapter, tool registry, interactive session, timeline store and recorder, orchestrator, memory stores, transactional editor, metrics collector, and pinned config loader.

import {
  createTimelineStore,
  createTimelineRecorder,
  createMemoryStore,
  createTransactionalEditor,
} from "bridlewise";

const store = createTimelineStore({ path: "/var/lib/bridlewise/timeline.db" });
const recorder = createTimelineRecorder({ store });
const memory = createMemoryStore({ path: "/var/lib/bridlewise/memory.db" });
const editor = createTransactionalEditor({ repositoryRoot: "/work/project" });

The engine has no HTTP server and no UI. The server owns auth, multi-user scoping, and transport. Keep that boundary.

Add a quality check or language pack

Quality packs live in engine/src/quality/ and engine/src/quality-packs/. A check declares its id, the languages it applies to, the parser input, the diagnostic shape, and whether it is a hard failure. A language pack declares the grammar and the checks it supports.

Do not add a check that reports a vague “problem”. A diagnostic must name the file, the range, the rule, and the evidence. A check that cannot run because a parser or fixture is absent must return a typed refusal.

Owner: engine/src/quality/, engine/src/quality-packs/.

Add a test

Public smoke tests live in tests/public/ and use node:test. The development floor has the larger suites; see Testing. A new capability needs:

Conventions

Where to start

The logbook index

Search the record

Type to search the curated record.

↑↓ move Enter open Esc close