@nestm/agentportal
v0.1.0-alpha.0
Published
NestJS 12 integration for AgentPortal with typed portals, managed CapsuleOS execution, and native streaming HTTP responses.
Maintainers
Readme
@nestm/agentportal
NestJS 12 integration for native AgentPortal Portals. It adds configuration, named dependency-injection registrations, explicit resource ownership, CapsuleOS-managed execution, deterministic testing helpers, and native SSE/NDJSON responses without replacing AgentPortal's lifecycle or event model.
Requirements
- Node.js 24 or newer
- NestJS
12.0.0-alpha.5or a compatible Nest 12 release - AgentPortal
0.1.x
Install the root integration and its required native peers:
pnpm add @nestm/agentportal @agentportal/portal @agentportal/provider \
@nestjs/common @nestjs/core reflect-metadata rxjsOptional entry points have isolated dependencies:
| Entry point | Additional dependencies |
| ------------------------------ | --------------------------------------------------------------------- |
| @nestm/agentportal/http | @agentportal/sdk |
| @nestm/agentportal/testing | @agentportal/sdk, @agentportal/testing, vitest |
| @nestm/agentportal/capsuleos | @capsuleos/agentportal, @capsuleos/core, @agentportal/transport |
The root entry never loads the optional HTTP, testing, transport, or CapsuleOS packages.
Root Portal
Pass either an application-owned Portal or an adapter/config pair owned by Nest. The two modes are mutually exclusive.
import { Module } from "@nestjs/common";
import { AgentPortalModule } from "@nestm/agentportal";
import { codex } from "@agentportal/codex";
@Module({
imports: [
AgentPortalModule.forRoot({
agent: codex({ mode: "app-server" }),
config: { transports, store, env: {} },
closeOptions: { timeoutMs: 10_000 },
isGlobal: true,
}),
],
})
export class AppModule {}Inject the root Portal directly:
import { Injectable } from "@nestjs/common";
import { InjectAgentPortal } from "@nestm/agentportal";
import type { Portal } from "@agentportal/portal";
@Injectable()
export class CodingService {
constructor(@InjectAgentPortal() readonly portal: Portal) {}
}forRootAsync() supports Nest's useFactory, useClass, and useExisting patterns. A prebuilt
Portal is always external and is never closed by this package. A Portal created from
{ agent, config } is closed by Nest within the configured deadline.
Named agents and Portals
Named external registrations preserve Nest request, transient, and durable scopes. Only an
explicit create definition is module-owned, and created Portals must remain singleton-scoped.
AgentPortalModule.forFeature({
agents: [{ name: "codex", useValue: codex({ mode: "app-server" }) }],
portals: [
{
name: "coding",
create: {
agent: "codex",
config: { transports, store, env: {} },
},
},
],
});constructor(
@InjectAgentPortal("coding") readonly portal: Portal,
@InjectPortalAgent("codex") readonly adapter: AgentAdapter,
) {}Duplicate normalized agent or Portal names fail during application bootstrap, including duplicates declared by separate feature modules in the same Nest application context. Native AgentPortal errors retain their identity; the wrapper introduces errors only for invalid Nest configuration, duplicate names, scopes, and missing named providers.
CapsuleOS-managed Portal
The CapsuleOS entry creates one reusable named Portal and one lazy template-resolution lease.
The bridge owns session isolation; the resolution lease owns the dedicated control sandbox used
for adapter resolution and Portal.models().
import { AgentPortalCapsuleModule } from "@nestm/agentportal/capsuleos";
AgentPortalCapsuleModule.forFeature({
name: "sandboxed-codex",
agent: codex({ mode: "app-server" }),
capsule,
bridge: {
sandbox: template,
retention: { mode: "destroy" },
bindings: {
resolver: explicitBindingResolver,
store: {
sqlite: {
path: "./capsule-bindings.db",
controllerId: "api-1",
ownershipLeaseMs: 30_000,
},
},
},
},
portal: {
store: metadataOnlySessionStore,
limits: { idleTimeoutMs: 120_000 },
},
});The integration always supplies env: {}, process transports, isolation, resolver, and probe
executor. Those fields are reserved and cannot be overridden. Shutdown uses one absolute
deadline and always attempts this order:
- Portal
- resolution lease
- module-owned bridge
- module-owned binding store
An external bridge or binding store is never closed by Nest.
Native HTTP responses
AgentPortalResponse delegates framing, replay watermarks, pull backpressure, errors, and cancel
behavior to @agentportal/sdk/web. Nest only adapts the resulting Fetch Response to Express or
Fastify.
import { AgentPortalHttpModule, AgentPortalResponse } from "@nestm/agentportal/http";
@Module({ imports: [AgentPortalHttpModule] })
export class HttpModule {}
return AgentPortalResponse.sse(run, {
heartbeatMs: 15_000,
abortOnCancel: false,
});
return AgentPortalResponse.ndjson(run, {
abortOnCancel: true,
});SSE uses the complete stamped event as data and its seq as the SSE ID. Omit
heartbeatMs—or pass false—to create no heartbeat timer. Before headers, native errors flow to
Nest exception handling. After headers, the response/socket is terminated without appending a
JSON error body. Disconnect detaches by default and aborts the native run only when
abortOnCancel: true.
Testing
The testing entry re-exports @agentportal/sdk/mock and adds Nest-specific module/override
helpers:
import {
createAgentPortalTestingModule,
createMockPortal,
overrideAgentPortal,
} from "@nestm/agentportal/testing";
const portal = await createMockPortal({
scripts: [
{
events: [
{ type: "text", payload: { content: "hello" } },
{ type: "done", payload: { status: "success" } },
],
},
],
});
const builder = Test.createTestingModule({
imports: [createAgentPortalTestingModule({ portal })],
});
overrideAgentPortal(builder, portal);No ordinary test or CI command makes a live provider call.
The blocking packed release gate also installs all CapsuleOS, AgentPortal, and Nest wrappers into one isolated Nest 12 consumer. It executes the AI SDK Harness path and the native AgentPortal path independently; neither path owns, dispatches, or normalizes the other.
Security and persistence
- Pass provider credentials and sandbox inputs explicitly; this package does not read them from ambient environment variables.
- The Capsule integration prevents host environment inheritance with
env: {}. - Use AgentPortal's metadata-only SQLite mode when prompts, output, stdin, stdout, and stderr must not be persisted.
- Treat ingress as public unless the selected CapsuleOS provider proves authentication.
- HTTP disconnect is detach-by-default so one client cannot silently cancel shared or durable work.
- Cleanup is idempotent and deadline-bound, and native primary errors are not replaced by wrapper errors when cleanup succeeds.
See SECURITY.md for the full boundary and disclosure policy.
License
BSD-3-Clause
