@memberjunction/action-runtime-host
Host-side bridge that exposes MJ services (metadata, views, queries, entity CRUD, action invocation, agent run, AI prompt execution) to sandboxed Runtime-action code via the utilities.* namespace.
Ships the default concrete implementation of RuntimeActionBridgeBuilder from @memberjunction/actions-base. @memberjunction/actions resolves it at runtime through MJGlobal.ClassFactory.CreateInstance(...).
Why this package exists (the cycle)
Section titled “Why this package exists (the cycle)”Building the bridge handler map requires touching:
ActionEngineServer(from@memberjunction/actions) — forutilities.actions.InvokeAgentRunner(from@memberjunction/ai-agents) — forutilities.agents.RunAIEngine(from@memberjunction/aiengine) — for agent / prompt metadataAIPromptRunner(from@memberjunction/ai-prompts) — forutilities.ai.ExecutePrompt
Those last three packages already depend on @memberjunction/actions. Putting the bridge source inside @memberjunction/actions (where it used to live) created a cycle that could only be hidden with await import(...) calls and as never casts — fragile, untyped, and easy to break silently.
This package sits at the top of the Actions stack, above every package it touches, so every import can be fully static and fully type-checked.
core · global · core-entities │ ├── actions-base ← contract: RuntimeActionBridgeBuilder abstract ├── code-execution ← sandbox runner ├── action-runtime ← RuntimeActionExecutor ├── aiengine · ai-prompts · ai-core-plus │ ├── actions ← ClassFactory lookup, no AI deps │ │ │ └── ai-agents │ │ │ └── action-runtime-host ← THIS PACKAGE (default bridge)How ActionEngine uses it
Section titled “How ActionEngine uses it”// Inside @memberjunction/actions (ActionEngine.RunRuntimeAction):const builder = MJGlobal.Instance.ClassFactory.CreateInstance<RuntimeActionBridgeBuilder>( RuntimeActionBridgeBuilder);if (builder) { bridgeHandlers = builder.BuildHandlers({ action, config, contextUser, abortSignal }); preamble = builder.GetPreamble();}// If no builder is registered, Runtime actions still run in pure-compute mode —// they just can't call any utilities.* bridge namespaces.Two things to notice:
- No static dependency on this package from Engine. Engine imports only the abstract from
actions-base; the concrete is resolved by name at runtime. - Graceful degradation. If no subclass is registered (e.g., this package was never loaded), pure-compute Runtime actions still work. That makes minimal builds possible.
Bootstrap wiring
Section titled “Bootstrap wiring”For the default bridge to register itself, this package has to be imported somewhere so its @RegisterClass decorator fires. The standard MJ flow handles this for you:
@memberjunction/server-bootstraplistsaction-runtime-hostas a dependency- The bootstrap manifest (
ServerBootstrap/src/generated/mj-class-registrations.ts) is regenerated bymj codegen manifestand statically importsDefaultRuntimeActionBridgeBuilderalongside every other@RegisterClass-decorated class - MJAPI’s entry point imports
@memberjunction/server-bootstrap/mj-class-registrations, which transitively triggers our registration
No manual wiring needed in consuming apps.
Overriding the default bridge
Section titled “Overriding the default bridge”The default uses @RegisterClass(RuntimeActionBridgeBuilder) with no key so any subclass registered later wins automatically via MJ’s auto-priority tiebreak. Subclass, register, done:
import { RegisterClass } from '@memberjunction/global';import { RuntimeActionBridgeBuilder, BridgeContext } from '@memberjunction/actions-base';import type { BridgeHandlerMap } from '@memberjunction/code-execution';
@RegisterClass(RuntimeActionBridgeBuilder)export class MyCustomBridge extends RuntimeActionBridgeBuilder { protected constructor() { super(); } public static get Instance(): MyCustomBridge { return super.getInstance<MyCustomBridge>(); }
public BuildHandlers(ctx: BridgeContext): BridgeHandlerMap { return { 'md.GetEntity': /* … your handler … */, 'utilities.custom.Ping': async () => 'pong', // … whatever you want to expose or override }; }
public GetPreamble(): string { return ` globalThis.utilities = { custom: { Ping: (...a) => __bridgeCall('utilities.custom.Ping', a[0]) }, // … match the handler keys above }; `; }}Import your custom package from MJAPI’s entry point (or add it to the bootstrap manifest flow) so @RegisterClass fires at startup.
The default utilities.* surface
Section titled “The default utilities.* surface”DefaultRuntimeActionBridgeBuilder.BuildHandlers(ctx) wires these handler keys to permissioned handlers that enforce RuntimeActionConfiguration.permissions and thread contextUser through every downstream call:
| Namespace | Names | Purpose |
|---|---|---|
md.* | GetEntity, GetEntityFields, GetRelatedEntities, ListEntities | Metadata read-only |
rv.* | RunView, RunViews | View queries (batched form supported) |
rq.* | RunQuery | Named-query execution |
entity.* | Load, Create, Update, Save, Delete | Entity CRUD (subject to allowedEntities) |
actions.* | Invoke, InvokeAll | Invoke other actions (subject to allowedActions) |
agents.* | Run, GetAvailable | Run AI agents (subject to allowedAgents) |
ai.* | ExecutePrompt, GetEmbedding | Execute AI prompts by name / get embeddings |
Each handler checks:
- Caller’s
RuntimeActionConfiguration.permissionsallowlist (orallowAny*flags) - The context’s
abortSignal(for cancellation) - User context is threaded through so MJ row-level security still applies
On permission denial, handlers throw; the sandbox re-throws inside the user’s code as a runtime error.
Testing
Section titled “Testing”cd packages/Actions/RuntimeHostnpm run testUnit tests validate the architectural contract (ClassFactory resolution, handler map shape, preamble-to-handlers consistency) without spinning up isolated-vm or a database. The end-to-end regression is packages/Actions/Runtime/harness/run-demos.ts — runs all 5 demo Runtime actions through the real stack.
Related packages
Section titled “Related packages”@memberjunction/actions-base— abstractRuntimeActionBridgeBuildercontract@memberjunction/actions— consumes the builder via ClassFactory@memberjunction/action-runtime— sandbox executor (what the bridge handler map gets passed to)@memberjunction/code-execution—WorkerPool+CodeExecutionService(isolated-vm sandbox)