Skip to content

Latest commit

 

History

History
233 lines (182 loc) · 8.62 KB

File metadata and controls

233 lines (182 loc) · 8.62 KB

@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() and forRootAsync()
  • 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 rxjs

Install 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/harness@1.0.58 compatibility patch:

pnpm add @capsuleos/ai-sdk-harness@^0.2.0 @ai-sdk/harness@1.0.58
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