How the Cordis Shadow Configuration System Works: Complete Technical Guide

The Cordis shadow configuration system tracks caller context through a symbol-based shadow mechanism that wraps service access in transparent proxies, enabling isolated service state with preserved caller metadata.

Cordis implements a sophisticated shadow configuration system to maintain caller context isolation while allowing services to trace their origin. This architecture, found in the cordiverse/cordis repository, solves the fundamental problem of context leakage in dependency injection frameworks. Unlike simple context passing, Cordis uses symbol-based shadows and traceable proxies to ensure that services always know who created them without exposing their internal state.

What Is the Cordis Shadow System?

The shadow system consists of two interconnected mechanisms working together in packages/core/src/utils.ts. First, the shadow symbol acts as a hidden property key for storing caller references. Second, the createTraceable function builds proxies that temporarily inject this shadow during property access.

The core symbol is defined as a shared symbol to ensure cross-realm compatibility:

// packages/core/src/utils.ts
export const symbols = {
  shadow: Symbol.for('cordis.shadow'),
  // ... other symbols
}

This design choice means Symbol.for('cordis.shadow') returns identical values even across different module boundaries, critical for plugin ecosystems.

How Shadows Store Caller Metadata

When a service instantiates, its constructor receives a caller context. Cordis captures this relationship by storing the caller as a shadow on the service's own context object. The createShadow function performs this operation:

// packages/core/src/utils.ts#L41-L46
export function createShadow<T extends object>(origin: T, ctx: Context) {
  return ctx.extend({
    [symbols.shadow]: origin,
  }) as T
}

The createShadow function extends the provided context with the symbols.shadow property pointing to the original caller. This allows services to access their creator through this.ctx[symbols.shadow] while keeping their own context tree separate.

Transparent Proxying with createTraceable

Property access on traceable objects triggers temporary shadow creation. The createTraceable proxy handler in packages/core/src/utils.ts#L57-L90 intercepts gets and sets to inject the correct caller context:

// Simplified flow from createTraceable implementation
const handler = {
  get(target, prop, receiver) {
    // Check if property has its own tracker
    const value = Reflect.get(target, prop, receiver)
    
    // Create temporary shadow with caller context preserved
    const shadow = createShadow(ctx[symbols.shadow] ?? ctx, ctx)
    
    // Return wrapped value that uses shadow for nested calls
    return wrapWithShadow(value, shadow)
  }
}

This proxy ensures that when you access service.someMethod(), the this context inside someMethod sees the original caller, not the service's internal shadow. Functions receive special treatment through createShadowMethod, which binds this to the shadow object.

Context Extension Preserves Shadows

The Context.extend method in packages/core/src/context.ts#L55-L63 maintains shadow continuity when creating derived contexts:

// From context.ts extend implementation
extend(meta: Context.Meta) {
  const self = getTraceable(this, this)
  const shadow = this[symbols.shadow]
  
  const ctx = Object.assign(Object.create(self), {
    [symbols.shadow]: shadow,
    // ... other meta
  })
  
  return ctx
}

This preservation is essential for plugin composition. When a plugin extends a context, any existing shadow from upstream callers travels with it, maintaining the full ancestry chain.

Practical Service Implementation

A standard service automatically participates in the shadow system:

import { Context, Service, symbols } from '@cordis/core'

class Greeter extends Service {
  constructor(ctx: Context) {
    super(ctx, 'greeter')
  }
  
  hello() {
    // Access the caller context through the shadow symbol
    const caller = this.ctx[symbols.shadow] as Context
    return `Hello from ${caller.config.name}`
  }
}

// Usage: caller context is automatically preserved
const root = new Context({ name: 'main-app' })
const greeter = root.provide('greeter', Greeter)
greeter.hello() // "Hello from main-app"

The Service base class handles shadow registration during construction, making the pattern transparent to implementers.

Opting Out with noShadow Services

Services can disable shadow tracking by setting noShadow: true in their tracker configuration. This is useful for lightweight utilities that don't need caller tracing:

import { Context, Service, symbols } from '@cordis/core'

class Simple extends Service {
  static inject = []
  
  [Service.tracker] = {
    property: 'ctx',
    noShadow: true  // Disables shadow creation
  }
  
  constructor(public ctx: Context) {}
  
  checkShadow() {
    // this.ctx[symbols.shadow] is undefined
    return this.ctx[symbols.shadow]
  }
}

The test suite in packages/core/tests/shadow.spec.ts#L54-L71 validates this behavior, confirming that noShadow services bypass the wrapping logic entirely.

Fiber Resolution Uses Shadows

The reflective fiber system in packages/core/src/reflect.ts#L82-L84 leverages shadows to determine execution context:

// From reflect.ts fiber resolution
const fiber = ctx[symbols.shadow] ?? ctx

This fallback pattern—shadow first, then direct context—ensures that fiber-bound operations always associate with the caller's runtime state, not the service's internal fiber tree. The shadow becomes the bridge between static service definition and dynamic execution context.

Shadow System Architecture Summary

Component Location Responsibility
symbols.shadow utils.ts#L49-L50 Global symbol for shadow property key
createShadow utils.ts#L41-L46 Builds shadow-wrapped contexts
createTraceable utils.ts#L57-L90 Proxy factory for transparent shadow injection
Context.extend context.ts#L55-L63 Preserves shadows during context derivation
Fiber resolution reflect.ts#L82-L84 Uses shadow for runtime context selection

Summary

  • Cordis shadow configuration uses Symbol.for('cordis.shadow') to store caller references without polluting the public context API.

  • The createShadow function in packages/core/src/utils.ts wraps contexts with caller metadata, enabling services to trace their origin.

  • createTraceable proxies inject temporary shadows during property access, ensuring nested calls maintain correct caller context.

  • noShadow: true in Service.tracker configuration opts out of shadow wrapping for lightweight services.

  • Fiber resolution prefers shadows over direct contexts, binding runtime operations to caller state rather than service internals.

Frequently Asked Questions

What problem does the Cordis shadow system solve?

The shadow system prevents context leakage in dependency injection frameworks. Without shadows, services would expose their internal context tree to consumers, making it impossible to distinguish between the caller's state and the service's own configuration. Shadows maintain this boundary while still allowing services to access caller metadata when needed.

How do I access the caller context in my service?

Access this.ctx[symbols.shadow] where symbols is imported from @cordis/core. The shadow contains the context that originally instantiated or injected your service. Note that this returns undefined for services with noShadow: true or when accessed from root-level contexts without parents.

Can I disable shadow tracking for performance?

Yes. Set [Service.tracker] = { property: 'ctx', noShadow: true } in your service class. This bypasses createShadow and createTraceable wrapping, returning raw values directly. Use this only for services that never need caller introspection, as it breaks the symbols.shadow access pattern.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →