Skip to content

v0.13.0 — hardening for 1.0

Choose a tag to compare

@zeestack zeestack released this 29 Oct 01:32
· 8 commits to main since this release
84e5e92

From the 1.0 milestone: the AI/MCP security review and the bugs the API
reference turned up. Requires Node.js 22.

Breaking (the 1.0 upgrade guide covers each):

  • The deprecated aliases from 0.12 are removed from dendra: resolveProc,
    invokeUnary, invokeStream and the Proc, AnyProc, CallSite,
    Transport and ProcedureKind types (import them from dendra/adapter);
    createMemoryStore, recallMemory, rememberFacts and formatMemoryBlock
    (from dendra/memory); toolSource and ToolSourceOptions (now
    lazyToolSource and LazyToolSourceOptions).
  • engines requires Node.js 22 or later.
  • An .ai() procedure whose input has no JSON Schema throws where it's defined,
    and maxBodyBytes caps JSON bodies (both below).

From the AI/MCP security review
(#42,
docs/security-review-ai-mcp.md).

  • Unexpected errors are masked. An error that isn't a DendraError used to
    reach clients and the model with its own message, which can carry SQL,
    hostnames or credentials. It now arrives as INTERNAL: Internal error over
    HTTP, streams, WebSockets, job snapshots, the MCP bridge and tool calls, and
    the original is logged with console.error. After-hooks still get the
    original. If clients relied on such a message, throw a DendraError instead.
  • createToolExecutor only throws DendraErrors, so a hand-written agent loop
    can pass the message to the model. A tool without .ai() answers
    NOT_FOUND, like a missing one, so the model can't probe for procedures.
  • createMcpToolSource throws a remote tool's error as a new BAD_GATEWAY (502)
    code, with the server's text.
  • lazyToolset throws when two tools sanitize to the same name (a.b and
    a_b), which used to route calls to the wrong one.
  • Zod, ArkType and Valibot schemas reach OpenAPI and AI tools. Libraries
    that implement Standard JSON Schema publish their JSON Schema to OpenAPI and
    tool definitions, as s.* does: Zod 4 and ArkType, and Valibot through
    toStandardJsonSchema from @valibot/to-json-schema
    (#65). Before, a tool with a
    Zod input showed the model no parameters. Breaking: an .ai() procedure
    whose input has no JSON Schema (Zod 3, plain Valibot, a z.date()) now throws
    where it's defined.
  • Per-tool MCP annotations. .ai({ annotations }) sets MCP's title,
    readOnlyHint, destructiveHint, idempotentHint and openWorldHint over
    the defaults dendra derives from .get() and confirm, so a POST that only
    reads, or a write that's safe to repeat, is described correctly
    (#59). inspectRouter shows
    each tool's annotations, so a test can assert them. New type
    ToolAnnotations.
  • createMcpRouter mounts an external MCP server's tools in your router as
    .ai() procedures ({ ...appRouter, gh: await createMcpRouter(client) }),
    so confirm (with createApprovals), your .use() guards, after-hooks and
    inspectRouter cover them like local tools
    (#58). Remote descriptions
    are cut to 1,024 characters there and in createMcpToolSource
    (maxDescriptionLength). The AI reference has a new section on treating
    external servers as untrusted.
  • Streaming tools are bounded and cancellable. A .stream() procedure
    exposed with .ai() stops collecting after 1,000 chunks or 30 s, set per tool
    with .ai({ maxItems, timeoutMs }). It answers with what arrived and aborts
    the handler's signal, so a subscription can't hang an agent or grow without
    limit (#57). Tool executors
    take execute(name, args, { signal }); runAgent passes its signal, so
    aborting a run cancels the call in flight. mergeToolsets, ToolSource and
    createMcpBridge().callTool pass the signal through. createMcpToolSource
    stops waiting but can't cancel the remote call. Behaviour change: a
    streaming tool that ran longer than 30 s used to wait for its end; raise
    timeoutMs if it needs longer.
  • A stream that yields undefined sends null over NDJSON and WebSockets, as
    SSE already did. NDJSON used to write the line undefined, which made the
    client throw. A tool that returns undefined gives the MCP bridge and
    runAgent the text null instead of no text
    (#64).
  • maxBodyBytes caps JSON bodies too. RPC, batch and REST JSON bodies were
    read whole with no limit; now, like uploads, they're refused past the cap
    (default 10 MB) with 413 PAYLOAD_TOO_LARGE, while reading
    (#62). Raise maxBodyBytes
    if you accept larger JSON.
  • approvals.list(ctx) only returns actions in the caller's exact scope. When
    scope(ctx) returned undefined (say, a user with no tenant), it used to
    return every tenant's pending actions
    (#63).
  • The example app's stdio MCP server only calls tools it listed and leaves out
    confirm tools; its agent route builds ctx from the request instead of a user
    id in the body.

Full docs: https://products.techfoundry.io/dendra/ · Upgrade guide: https://products.techfoundry.io/dendra/upgrading/