@nestm/capsuleos
v0.1.0-alpha.0
Published
Typed CapsuleOS integration for NestJS 12 with testing and AI SDK Harness subpaths.
Maintainers
Readme
@nestm/capsuleos
Typed CapsuleOS integration for NestJS 12. It provides a small dependency-injection boundary around a Capsule facade while preserving CapsuleOS provider aliases, native errors, and application ownership.
CapsuleOsModule.forRoot()andforRootAsync()- Mutually exclusive provider-map and prebuilt-Capsule registration
- Typed
CapsuleOsService, token, and injection decorator - Framework-neutral fakes through
@nestm/capsuleos/testing - Named AI SDK Harness sandbox providers through
@nestm/capsuleos/ai-sdk-harness
The root entrypoint does not import provider SDKs, testing utilities, or AI SDK Harness. It never allocates a sandbox at Nest bootstrap.
Requirements
- Node 24 or newer
- NestJS
^12.0.0-alpha.5 - CapsuleOS
^0.2.0 - ESM
Installation
pnpm add @nestm/capsuleos capsuleos @nestjs/common @nestjs/core reflect-metadata rxjsInstall the CapsuleOS provider package your application uses separately. Construct its client with explicit credentials; this package does not read provider credentials from ambient environment variables.
Provider-map registration
import { Module } from "@nestjs/common";
import { CapsuleOsModule, defineCapsuleOsConfig, secret } from "@nestm/capsuleos";
import { vercel } from "@capsuleos/vercel";
const capsuleConfig = defineCapsuleOsConfig({
providers: {
vercel: vercel({
token: secret(explicitToken),
teamId: explicitTeamId,
projectId: explicitProjectId,
}),
},
defaults: { provider: "vercel" },
});
@Module({
imports: [CapsuleOsModule.forRoot({ ...capsuleConfig, isGlobal: true })],
})
export class AppModule {}The application owns every provider and native provider client. CapsuleOsModule creates only the
Capsule facade and does not close the provider instances. Root registration is global by default;
pass isGlobal: false for a module-local registration.
Prebuilt Capsule registration
Use capsule when the application already constructed and owns a Capsule. capsule and providers
are mutually exclusive, and defaults or event-buffer settings cannot modify a prebuilt facade.
import { createCapsule } from "capsuleos";
const capsule = createCapsule({ providers: capsuleConfig.providers });
CapsuleOsModule.forRoot({ capsule });Nest preserves the exact object identity and never closes a prebuilt Capsule.
Injection and typing
import { Inject, Injectable } from "@nestjs/common";
import { CAPSULE_OS, CapsuleOsService, InjectCapsuleOs, type Capsule } from "@nestm/capsuleos";
@Injectable()
export class SandboxService {
constructor(
@InjectCapsuleOs()
readonly capsule: Capsule<typeof capsuleConfig.providers>,
readonly capsuleOs: CapsuleOsService<typeof capsuleConfig.providers>,
@Inject(CAPSULE_OS)
readonly sameCapsule: Capsule<typeof capsuleConfig.providers>,
) {}
}To make the provider map the default generic argument across an application, augment the public type registry once:
declare module "@nestm/capsuleos" {
interface CapsuleOsTypeRegistry {
providers: typeof capsuleConfig.providers;
}
}CapsuleOsService then exposes typed capsule, providers, sandboxes, and events without a local
generic argument.
Async registration
forRootAsync() supports Nest's useFactory, useClass, and useExisting patterns:
CapsuleOsModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
providers: {
vercel: vercel({
token: secret(config.getOrThrow("VERCEL_TOKEN")),
teamId: config.getOrThrow("VERCEL_TEAM_ID"),
projectId: config.getOrThrow("VERCEL_PROJECT_ID"),
}),
},
defaults: { provider: "vercel" },
}),
});Class and existing factories implement CapsuleOsOptionsFactory.createCapsuleOsOptions(). The
createCapsuleOsOptions() identity helper preserves concrete provider-map types in those factories.
Testing
Install @capsuleos/testing and import the optional testing entrypoint:
import { Test } from "@nestjs/testing";
import { createCapsuleOsTestingModule, overrideCapsuleOsProviders } from "@nestm/capsuleos/testing";
const builder = Test.createTestingModule({
imports: [createCapsuleOsTestingModule()],
});
overrideCapsuleOsProviders(builder, {
providers: { fake: myFakeProvider },
});The generated testing module is local by default and performs no sandbox allocation.
AI SDK Harness
The optional entrypoint delegates sandbox behavior to @capsuleos/ai-sdk-harness and requires the
exact @ai-sdk/[email protected] compatibility patch:
pnpm add @capsuleos/ai-sdk-harness@^0.2.0 @ai-sdk/[email protected]import {
CapsuleOsHarnessModule,
InjectCapsuleOsHarnessSandbox,
SqliteCapsuleOsHarnessBindings,
type HarnessV1SandboxProvider,
} from "@nestm/capsuleos/ai-sdk-harness";
const bindings = new SqliteCapsuleOsHarnessBindings({
path: "./capsuleos-harness.db",
});
CapsuleOsHarnessModule.register({
name: "coding",
provider: "vercel",
sandbox: { spec: agentSandboxSpec },
bridgePort: 43_123,
bindings,
});
@Injectable()
export class CodingSandbox {
constructor(
@InjectCapsuleOsHarnessSandbox("coding")
readonly provider: HarnessV1SandboxProvider,
) {}
}registerAsync() supports useFactory, useClass, and useExisting. Registration constructs a
Harness sandbox provider but does not create a sandbox; allocation starts only when Harness asks it
to create a session. Named registrations are independent and do not register Harness agents.
The upstream adapter's default auto transport is capability-driven. It prefers a loopback-only,
base64-line-framed process tunnel when the provider advertises process.stdin, and otherwise uses
stable provider ingress:
| Provider | Default Harness transport | Status | | ------------------- | ------------------------- | ---------------------------------------------- | | Vercel Sandbox | Provider ingress | Supported | | Daytona | Process tunnel | Supported | | Docker guest mode | Process tunnel | Supported | | Docker classic mode | Process tunnel | Supported with an exec-stdin-capable transport | | Apple Container | Process tunnel | Supported | | AWS Lambda MicroVMs | Process tunnel | Supported within configured provider limits | | Firecracker | Process tunnel | Preview; requires a compatible supervisor | | libkrun | Process tunnel | Experimental opt-in |
Automatic transport selection happens before a binding is reserved or a sandbox is allocated. A
provider that offers neither process stdin nor stable ingress fails that preflight. Explicit
Daytona provider-ingress remains experimental because its signed endpoint expires; normal
Daytona use selects the process tunnel instead.
The application owns bindings and must close an application-created SQLite backend during its own
shutdown. See SECURITY.md before exposing Harness ingress or persisting lifecycle
state.
Ownership summary
| Resource | Owner |
| ------------------------------------------- | -------------------------------------------------------- |
| Provider SDK/client passed in providers | Application; never closed by Nest |
| Prebuilt Capsule | Application; identity preserved and never closed by Nest |
| Capsule facade constructed from providers | Nest module; providers remain external |
| Harness binding store and lease manager | Application |
| Harness sandbox sessions | Upstream Harness provider/runner lifecycle |
Package entrypoints
| Import | Purpose | Optional peer |
| --------------------------------- | ------------------------------------------- | ---------------------------------------------- |
| @nestm/capsuleos | Root Nest module and upstream CapsuleOS API | none beyond root peers |
| @nestm/capsuleos/testing | Fake providers and Nest override helpers | @capsuleos/testing |
| @nestm/capsuleos/ai-sdk-harness | Named Harness sandbox providers | @capsuleos/ai-sdk-harness, @ai-sdk/harness |
