Skip to content

feat(com): propagate caller identity across cross-environment calls and subscriptions#2851

Open
vladbazl wants to merge 5 commits into
mainfrom
vladbazl/caller-identity
Open

feat(com): propagate caller identity across cross-environment calls and subscriptions#2851
vladbazl wants to merge 5 commits into
mainfrom
vladbazl/caller-identity

Conversation

@vladbazl
Copy link
Copy Markdown
Contributor

@vladbazl vladbazl commented Apr 24, 2026

Problem.
Server-side environments need to know who is making API calls.
Identity is derived from the Socket.IO handshake (e.g. cookies, auth headers). When a request is forwarded through multiple envs — client → processing → workspace — every server-side handler in the chain must see the original client's identity.

Solution.

  1. autoLaunch receives identityExtractor. Example:
await manager.autoLaunch(new Map([['feature', 'caller-identity']]), {
    identityExtractor: (handshake) => ({ userId: handshake.auth?.userId ?? 'anonymous' }),
});
  1. In Node env entrypoint configures callerContext in COM feature config with using a standard AsyncLocalStorage library. Example:
new RuntimeEngine(
        env,
        [
            ...(workerData
                ? [
                      COM.configure({
                          config: {
                              host: new ParentPortHost(env.env),
                              id: env.env,
                              callerContext: new AsyncLocalStorage(),
                          },
                      }),
                  ]
                : []),
            ...topLevelConfig,
        ],
        new Map(options?.entries() ?? []),
    ).run(Feature);
  1. Registered APIs (services) can use a getCurrentCaller() in any method. Example:
import { getCurrentCaller } from '@dazl/engine-core';
...
const userIdentity = getCurrentCaller();

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces end-to-end caller identity propagation for cross-environment RPC calls and listener subscriptions by stamping identity onto inbound Socket.IO messages and flowing it through the message forwarding chain, exposing it to server-side handlers via an AsyncLocalStorage-backed accessor.

Changes:

  • Add callerIdentity to the core message model and propagate it on call/listen/unlisten messages.
  • Introduce AsyncLocalStorage-based caller context utilities (getCurrentCaller, runWithCaller) and wrap inbound handler execution to set/restore identity.
  • Add runtime-node support for extracting identity from Socket.IO handshake (IdentityExtractor) and stamping it onto every inbound message; add unit tests covering single-hop, chained calls, concurrency isolation, and subscription/unsubscription.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/runtime-node/test/caller-identity.unit.ts Adds unit tests for identity extraction, propagation, and isolation across calls and subscriptions.
packages/runtime-node/src/ws-node-host.ts Adds IdentityExtractor, stores per-client identity, and stamps callerIdentity onto inbound messages.
packages/runtime-node/src/node-env-manager.ts Plumbs optional identityExtractor into WsServerHost setup.
packages/core/src/com/message-types.ts Adds callerIdentity?: unknown to BaseMessage.
packages/core/src/com/index.ts Exports the new caller-context utilities.
packages/core/src/com/communication.ts Attaches callerIdentity to outbound messages and wraps inbound call/listen/unlisten handling in runWithCaller.
packages/core/src/com/caller-context.ts Introduces AsyncLocalStorage-backed caller context (getCurrentCaller, runWithCaller).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core/src/com/communication.ts Outdated
Comment thread packages/runtime-node/src/ws-node-host.ts Outdated
Comment thread packages/core/src/com/caller-context.ts
vladbazl and others added 2 commits April 24, 2026 18:07
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
@vladbazl vladbazl requested a review from idoros April 24, 2026 16:15
Comment thread packages/core/src/com/caller-context.ts Outdated
}
try {
const moduleName = 'node:async_hooks';
const { AsyncLocalStorage } = await import(moduleName);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@engine/core is isomorphic.
please do not use node APIs in isomorphic code.
avoid using a dynamic import in a try-catch to account for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactoring this part

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a configuration of COM feature [optional], without a conditional dependency (dynamic import) on NodeJS lib.

Comment thread packages/core/src/com/caller-context.ts Outdated
getStore<T = unknown>(): T | undefined;
run<R>(identity: unknown, callback: () => R): R;
}
let callerStore: CallerContext | undefined | null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid module state

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it leaks between different runs in the same runtime environment, as it depends on module evaluation to initialize and cleanup. This fact makes harder to manage and not testing friendly, by design.

It is a hack/cheat to use it a in a controlled environment, where you'd like to remove leftovers and ensure cleanup.

Not the first instance I'm commenting about this. Told the AI Squad the same when they did it in one of the tools.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part was updated, now it is configurable through COM feature settings and each thread can receive a different implementation.
The CallerContext is still set in module though, this is needed to make the Caller Context API nice and avoid changes (e.g. dependencies on Communication instance) in every place where caller identity is needed.
Also this functionality is covered with tests.

…figurable instead

Co-authored-by: Copilot <copilot@github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants