deepseek-ai/deepseek-harness · error

client-modules: ctx.baseUrl is unset — the node half needs t

Error message

client-modules: ctx.baseUrl is unset — the node half needs the config-tree anchor to resolve plugin packages

What it means

ClientModuleRegistry's node half resolves every composed plugin package through createRequire(ctx.baseUrl) — baseUrl is the cordis.yml config-tree directory, whose package declares every composed plugin as a dependency, the only anchor that survives pnpm's isolated node_modules. If the Context has no baseUrl, the service refuses to construct. This is a fail-loud configuration error: the plugin was mounted outside a Loader config tree.

Source

Thrown at packages/client/modules/src/index.ts:308

  private readonly rebuildListeners = new Set<(id: string, rev: string) => void>()
  private readonly graphListeners = new Set<() => void>()
  private readonly dirty = new Set<string>()
  private readonly resolvePkgJson: (spec: string) => string
  private flushQueued = false
  private composed: WebBootGraph

  /**
   * Build the service: subscribe, seed, and run the activation flush.
   * @param ctx - plugin context carrying webServer and loader.
   */
  constructor(ctx: Context) {
    super(ctx, 'clientModules')
    // Resolution anchor: the config tree's baseUrl (the cordis.yml directory,
    // whose package declares every composed plugin as a dependency). The
    // modules package's own URL would miss sibling packages under pnpm's
    // isolated node_modules.
    if (ctx.baseUrl === undefined) {
      throw new Error('client-modules: ctx.baseUrl is unset — the node half needs the config-tree anchor to resolve plugin packages')
    }
    const require = createRequire(ctx.baseUrl)
    this.resolvePkgJson = spec => require.resolve(`${spec}/package.json`)

    // Subscribe before seeding so a fiber arriving mid-activation lands in the
    // same dirty set (Set idempotence makes the overlap harmless). An entry-less
    // fiber is a child plugin or a manual mount — never a loader row; O(1) drop.
    ctx.on('internal/plugin', (fiber) => {
      const entryName = fiber.entry?.options.name
      if (entryName === undefined) return
      this.dirty.add(entryName)
      if (this.flushQueued) return
      this.flushQueued = true
      queueMicrotask(() => {
        this.flushQueued = false
        this.flush((err) => { ctx.logger.warn(err) })
      })
    })

View on GitHub (pinned to b150a551b8)

Solutions

  1. Boot through the Cordis Loader with a real cordis.yml so the config tree sets ctx.baseUrl to the config directory.
  2. If embedding programmatically, anchor the context at the directory whose package.json declares the composed plugins as dependencies before applying client-modules.
  3. In tests, use REAL composition: boot a test cordis.yml through the Loader instead of ctx.plugin(...) on a bare context.

Example fix

// before — programmatic mount on a bare context
const ctx = app.root.ctx           // ctx.baseUrl === undefined
ctx.plugin(ClientModuleRegistry)   // throws

// after — load a config tree so the registry anchors at the cordis.yml directory
const loader = new Loader({ root: appDir })
const ctx = await loader.load('cordis.yml')  // ctx.baseUrl = dirname(cordis.yml)
Defensive patterns

Strategy: validation

Validate before calling

import type { Context } from '@deepseek-ai/cordis'
// guard before mounting client-modules
if (ctx.baseUrl === undefined) {
  throw new Error('client-modules needs a Loader config tree — boot from a cordis.yml')
}
ctx.plugin(ClientModuleRegistry)

Type guard

import type { Context } from '@deepseek-ai/cordis'
const hasConfigTreeAnchor = (ctx: Context): ctx is Context & { baseUrl: string } =>
  typeof ctx.baseUrl === 'string'

Prevention

When it happens

Trigger: Programmatically mounting ClientModuleRegistry (directly or via a plugin composition) onto a hand-built Context that never loaded a cordis.yml — e.g. ctx.plugin(ClientModuleRegistry) in a test harness or an embedded boot that bypasses the Loader's config-tree load, leaving ctx.baseUrl undefined.

Common situations: Unit tests constructing bare Contexts and applying plugins directly; embedding the web app in another Cordis application that boots plugins from code instead of a config file; a custom entry point that mounts client-modules before any config tree exists.

Related errors


AI-assisted analysis of deepseek-ai/deepseek-harness@b150a551b8 (2026-08-24). Data as JSON: /api/errors/4baaaa6d80edb3c0. Report an issue: GitHub.