@memberjunction/action-runtime-host
v5.35.0
Published
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 `utilities.*`. Ships a default implementation of `RuntimeActionBridgeBuilder` that `@
Maintainers
Keywords
Readme
@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)
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
// 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
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
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
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
cd packages/Actions/RuntimeHost
npm 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
@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)
