Add Novita sandbox provider plugin (#7595)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Paperclip already separates agent adapters from execution
environments, so agents can run locally, over SSH, or through sandbox
providers.
> - Sandbox provider plugins let Paperclip add new cloud runtimes
without changing each agent adapter.
> - Novita Agent Sandbox is a cloud runtime for AI agent workloads with
isolated filesystems, command execution, templates, timeout controls,
and pause/resume behavior.
> - Paperclip currently has sandbox provider examples for Daytona and
Cloudflare, but not Novita.
> - This pull request adds a Novita sandbox provider plugin using the
existing provider-plugin lifecycle.
> - The benefit is that Paperclip users can run existing adapters such
as Codex, Claude, Gemini, OpenCode, Cursor, or ACPX inside Novita Agent
Sandbox environments.

## Linked Issues or Issue Description

Fixes #7596

## What Changed

- Added `packages/plugins/sandbox-providers/novita` as a standalone
sandbox provider plugin package.
- Registered provider key `novita` with `kind: "sandbox_provider"` and
`environment.drivers.register` capability.
- Implemented Novita environment lifecycle hooks: validate config,
probe, acquire lease, resume lease, release lease, destroy lease,
realize workspace, and execute commands.
- Added config support for `apiKey`, `domain`, `template`,
`requestedCwd`, `timeoutMs`, `requestTimeoutMs`, `secure`, `autoPause`,
and `reuseLease`.
- Added README documentation for setup, configuration, and lifecycle
behavior.
- Added tests for manifest shape, config parsing, safe shell command
wrapping, stdin delimiter safety, and env-key validation.

## Verification

From `packages/plugins/sandbox-providers/novita`:

- `pnpm typecheck`
- `pnpm test`

The tests avoid live Novita API calls and cover the provider's static
contract and command-wrapping behavior. Live end-to-end verification
requires a Paperclip instance with the plugin installed and a Novita API
key configured as either a Paperclip secret or `NOVITA_API_KEY` in the
worker environment.

## Risks

- This adds a new direct dependency on the Novita Sandbox JS SDK
(`novita-sandbox`). Socket/Snyk should review the package as part of
normal dependency checks.
- The implementation relies on Novita SDK command execution semantics;
live provider behavior should be verified with a real Novita sandbox
before marking the plugin production-ready.
- `reuseLease` maps Paperclip release behavior to Novita `betaPause()`.
If pause is unavailable for a selected template, the plugin falls back
to best-effort kill during release.
- Low migration risk for existing users because this is a new standalone
provider plugin and does not change existing adapters or built-in
providers.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI GPT-5 Codex via Codex CLI, with repository file access, shell
command execution, GitHub CLI/API usage, and local TypeScript/Vitest
verification. Web and local documentation context were used for Novita
Sandbox SDK/API behavior.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Devin Foley <devin@paperclip.ing>
This commit is contained in:
Alex
2026-06-18 09:34:12 +08:00
committed by GitHub
parent f3e01c63bd
commit b18669452f
14 changed files with 866 additions and 4 deletions
@@ -0,0 +1,2 @@
export { default } from "./plugin.js";
export { default as manifest } from "./manifest.js";
@@ -0,0 +1,84 @@
import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
const PLUGIN_ID = "paperclip.novita-sandbox-provider";
const PLUGIN_VERSION = "0.1.0";
const manifest: PaperclipPluginManifestV1 = {
id: PLUGIN_ID,
apiVersion: 1,
version: PLUGIN_VERSION,
displayName: "Novita Sandbox Provider",
description:
"Sandbox provider plugin that provisions Novita Agent Sandbox environments for Paperclip agent runs.",
author: "Novita AI",
categories: ["automation"],
capabilities: ["environment.drivers.register"],
entrypoints: {
worker: "./dist/worker.js",
},
environmentDrivers: [
{
driverKey: "novita",
kind: "sandbox_provider",
displayName: "Novita Agent Sandbox",
description:
"Provisions Novita Agent Sandbox instances with configurable templates, idle timeout, workspace path, and lease reuse.",
configSchema: {
type: "object",
properties: {
apiKey: {
type: "string",
format: "secret-ref",
description:
"Environment-specific Novita API key. Paste a key or an existing Paperclip secret reference; saved environments store pasted values as company secrets. Falls back to NOVITA_API_KEY if omitted.",
},
domain: {
type: "string",
description:
"Optional Novita API domain override. Leave empty to use the SDK default.",
},
template: {
type: "string",
description:
"Novita sandbox template ID or name. Leave blank to use the SDK's default base template.",
},
requestedCwd: {
type: "string",
default: "/home/user/paperclip-workspace",
description: "Workspace directory to create inside the sandbox lease.",
},
timeoutMs: {
type: "number",
default: 300000,
description:
"Sandbox lifetime and default per-command timeout in milliseconds.",
},
requestTimeoutMs: {
type: "number",
default: 30000,
description:
"HTTP/RPC request timeout for Novita SDK calls in milliseconds.",
},
secure: {
type: "boolean",
default: true,
description: "Use secure connections when supported by the Novita SDK.",
},
autoPause: {
type: "boolean",
default: false,
description: "Enable Novita sandbox auto-pause behavior when supported by the selected template.",
},
reuseLease: {
type: "boolean",
default: false,
description:
"Pause and later resume the sandbox across Paperclip runs instead of killing it on release.",
},
},
},
},
],
};
export default manifest;
@@ -0,0 +1,145 @@
import { describe, expect, it, vi } from "vitest";
import manifest from "./manifest.js";
const mockSandboxConnect = vi.hoisted(() => vi.fn());
const mockSandboxCreate = vi.hoisted(() => vi.fn());
vi.mock("novita-sandbox", () => ({
Sandbox: {
connect: mockSandboxConnect,
create: mockSandboxCreate,
},
}));
import plugin, { buildShellCommand, parseNovitaDriverConfig } from "./plugin.js";
describe("Novita sandbox provider plugin", () => {
it("declares a sandbox provider environment driver", () => {
expect(manifest.capabilities).toContain("environment.drivers.register");
expect(manifest.environmentDrivers).toHaveLength(1);
expect(manifest.environmentDrivers?.[0]).toMatchObject({
driverKey: "novita",
kind: "sandbox_provider",
displayName: "Novita Agent Sandbox",
});
});
it("parses defaults", () => {
expect(parseNovitaDriverConfig({})).toMatchObject({
apiKey: null,
domain: null,
template: null,
requestedCwd: "/home/user/paperclip-workspace",
timeoutMs: 300_000,
requestTimeoutMs: 30_000,
secure: null,
autoPause: false,
reuseLease: false,
});
});
it("parses configured values", () => {
expect(parseNovitaDriverConfig({
apiKey: "sk-test",
domain: "https://sandbox.example.test",
template: "paperclip-template",
requestedCwd: "/workspace",
timeoutMs: 600000,
requestTimeoutMs: 45000,
secure: true,
autoPause: true,
reuseLease: true,
})).toMatchObject({
apiKey: "sk-test",
domain: "https://sandbox.example.test",
template: "paperclip-template",
requestedCwd: "/workspace",
timeoutMs: 600_000,
requestTimeoutMs: 45_000,
secure: true,
autoPause: true,
reuseLease: true,
});
});
it("builds a quoted shell command with cwd, env, args, and stdin", () => {
const command = buildShellCommand({
command: "node",
args: ["-e", "console.log(process.env.MESSAGE)"],
cwd: "/workspace/project",
env: { MESSAGE: "hello world" },
stdin: "input body",
});
expect(command).toContain("cd '/workspace/project'");
expect(command).toContain("export MESSAGE='hello world';");
expect(command).toContain("'node' '-e' 'console.log(process.env.MESSAGE)'");
expect(command).toContain("printf '%s' 'input body' > '/tmp/.paperclip-stdin-");
expect(command).toMatch(/< '\/tmp\/\.paperclip-stdin-[^']+'/);
expect(command).toMatch(/rm -f '\/tmp\/\.paperclip-stdin-[^']+'/);
expect(command).toContain("exit $status");
});
it("does not use a heredoc delimiter for stdin", () => {
const command = buildShellCommand({
command: "cat",
stdin: "before\nPAPERCLIP_STDIN\nafter",
});
expect(command).toContain("before\nPAPERCLIP_STDIN\nafter");
expect(command).not.toContain("<<");
});
it("uses a unique stdin path for each command", () => {
const first = buildShellCommand({
command: "cat",
stdin: "first",
});
const second = buildShellCommand({
command: "cat",
stdin: "second",
});
const firstPath = first.match(/\/tmp\/\.paperclip-stdin-[^']+/)?.[0];
const secondPath = second.match(/\/tmp\/\.paperclip-stdin-[^']+/)?.[0];
expect(firstPath).toBeTruthy();
expect(secondPath).toBeTruthy();
expect(firstPath).not.toBe(secondPath);
expect(first).not.toContain("/tmp/.paperclip-stdin <<");
expect(second).not.toContain("/tmp/.paperclip-stdin <<");
});
it("rejects unsafe environment variable keys", () => {
expect(() => buildShellCommand({
command: "env",
env: { "BAD-KEY": "value" },
})).toThrow("Invalid sandbox environment variable key");
});
it("returns a command failure when execute is called for an expired sandbox lease", async () => {
mockSandboxConnect.mockRejectedValueOnce(new Error("sandbox not found"));
const result = await plugin.definition.onEnvironmentExecute?.({
companyId: "company-1",
environmentId: "env-1",
issueId: "issue-1",
runId: "run-1",
config: { apiKey: "sk-test" },
lease: { providerLeaseId: "sb-expired", metadata: {} },
command: "true",
});
expect(result).toEqual({
exitCode: 1,
signal: null,
timedOut: false,
stdout: "",
stderr: "Novita sandbox lease is no longer available.\n",
metadata: {
provider: "novita",
sandboxId: "sb-expired",
expired: true,
},
});
});
});
@@ -0,0 +1,492 @@
import { randomUUID } from "node:crypto";
import { definePlugin } from "@paperclipai/plugin-sdk";
import type {
PluginEnvironmentAcquireLeaseParams,
PluginEnvironmentDestroyLeaseParams,
PluginEnvironmentExecuteParams,
PluginEnvironmentExecuteResult,
PluginEnvironmentLease,
PluginEnvironmentProbeParams,
PluginEnvironmentProbeResult,
PluginEnvironmentRealizeWorkspaceParams,
PluginEnvironmentRealizeWorkspaceResult,
PluginEnvironmentReleaseLeaseParams,
PluginEnvironmentResumeLeaseParams,
PluginEnvironmentValidateConfigParams,
PluginEnvironmentValidationResult,
} from "@paperclipai/plugin-sdk";
import { Sandbox } from "novita-sandbox";
export interface NovitaDriverConfig {
apiKey: string | null;
domain: string | null;
template: string | null;
requestedCwd: string;
timeoutMs: number;
requestTimeoutMs: number;
secure: boolean | null;
autoPause: boolean;
reuseLease: boolean;
}
function parseOptionalString(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function parsePositiveInteger(value: unknown, fallback: number): number {
if (value == null || value === "") return fallback;
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : fallback;
}
export function parseNovitaDriverConfig(raw: Record<string, unknown>): NovitaDriverConfig {
return {
apiKey: parseOptionalString(raw.apiKey),
domain: parseOptionalString(raw.domain),
template: parseOptionalString(raw.template),
requestedCwd: parseOptionalString(raw.requestedCwd) ?? "/home/user/paperclip-workspace",
timeoutMs: parsePositiveInteger(raw.timeoutMs, 300_000),
requestTimeoutMs: parsePositiveInteger(raw.requestTimeoutMs, 30_000),
secure: typeof raw.secure === "boolean" ? raw.secure : null,
autoPause: raw.autoPause === true,
reuseLease: raw.reuseLease === true,
};
}
function validateNovitaDriverConfig(config: NovitaDriverConfig): string[] {
const errors: string[] = [];
if (!config.apiKey && !process.env.NOVITA_API_KEY?.trim()) {
errors.push("Novita sandbox environments require an API key in config or NOVITA_API_KEY.");
}
if (!config.requestedCwd.startsWith("/")) {
errors.push("requestedCwd must be an absolute path.");
}
if (config.timeoutMs < 10_000) {
errors.push("timeoutMs must be at least 10000.");
}
if (config.requestTimeoutMs < 1_000) {
errors.push("requestTimeoutMs must be at least 1000.");
}
return errors;
}
function resolveApiKey(config: NovitaDriverConfig): string {
const apiKey = config.apiKey ?? process.env.NOVITA_API_KEY?.trim() ?? "";
if (!apiKey) {
throw new Error("Novita sandbox environments require an API key in config or NOVITA_API_KEY.");
}
return apiKey;
}
function sandboxOpts(config: NovitaDriverConfig) {
return {
apiKey: resolveApiKey(config),
...(config.domain ? { domain: config.domain } : {}),
...(config.secure == null ? {} : { secure: config.secure }),
timeoutMs: config.timeoutMs,
requestTimeoutMs: config.requestTimeoutMs,
};
}
function formatErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'"'"'`)}'`;
}
function singleQuoteForPrintf(value: string): string {
return `'${value.replace(/'/g, `'"'"'`)}'`;
}
function isValidShellEnvKey(value: string): boolean {
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
}
function buildStdinPath(): string {
return `/tmp/.paperclip-stdin-${randomUUID()}`;
}
export function buildShellCommand(input: {
command: string;
args?: string[];
cwd?: string;
env?: Record<string, string>;
stdin?: string;
}): string {
const envEntries = Object.entries(input.env ?? {});
for (const [key] of envEntries) {
if (!isValidShellEnvKey(key)) {
throw new Error(`Invalid sandbox environment variable key: ${key}`);
}
}
const exports = envEntries.map(([key, value]) => `export ${key}=${shellQuote(value)};`);
const argv = [input.command, ...(input.args ?? [])].map(shellQuote).join(" ");
const stdinPath = typeof input.stdin === "string" ? buildStdinPath() : null;
const stdin = stdinPath ? `printf '%s' ${singleQuoteForPrintf(input.stdin ?? "")} > ${shellQuote(stdinPath)}` : "";
const cwd = input.cwd?.trim() || "/";
const commandLines = stdinPath
? [
"set +e",
`${argv} < ${shellQuote(stdinPath)}`,
"status=$?",
`rm -f ${shellQuote(stdinPath)}`,
"exit $status",
]
: [`exec ${argv}`];
return [
"set -e",
stdin,
`cd ${shellQuote(cwd)}`,
...exports,
...commandLines,
].filter(Boolean).join("\n");
}
async function createSandbox(params: PluginEnvironmentAcquireLeaseParams | PluginEnvironmentProbeParams, config: NovitaDriverConfig) {
const metadata = {
"paperclip-provider": "novita",
"paperclip-company-id": params.companyId,
"paperclip-environment-id": params.environmentId,
...(params.issueId ? { "paperclip-issue-id": params.issueId } : {}),
...("runId" in params ? { "paperclip-run-id": params.runId } : {}),
};
const opts = {
...sandboxOpts(config),
metadata,
autoPause: config.autoPause,
};
return config.template
? await Sandbox.create(config.template, opts)
: await Sandbox.create(opts);
}
async function connectSandbox(config: NovitaDriverConfig, sandboxId: string) {
return await Sandbox.connect(sandboxId, sandboxOpts(config));
}
function isSandboxNotFoundError(error: unknown): boolean {
const message = formatErrorMessage(error).toLowerCase();
return message.includes("not found") || message.includes("404");
}
async function getSandboxOrNull(config: NovitaDriverConfig, sandboxId: string) {
try {
return await connectSandbox(config, sandboxId);
} catch (error) {
if (isSandboxNotFoundError(error)) return null;
throw error;
}
}
async function detectShellCommand(sandbox: Sandbox, config: NovitaDriverConfig): Promise<"bash" | "sh"> {
try {
const result = await sandbox.commands.run(
"if command -v bash >/dev/null 2>&1; then printf bash; else printf sh; fi",
{ cwd: "/", timeoutMs: config.requestTimeoutMs },
);
return result.stdout.trim() === "bash" ? "bash" : "sh";
} catch {
return "sh";
}
}
async function ensureWorkspace(sandbox: Sandbox, remoteCwd: string, config: NovitaDriverConfig) {
await sandbox.commands.run(`mkdir -p ${shellQuote(remoteCwd)}`, {
cwd: "/",
timeoutMs: config.requestTimeoutMs,
});
}
function leaseMetadata(input: {
config: NovitaDriverConfig;
sandbox: Sandbox;
shellCommand: "bash" | "sh";
remoteCwd: string;
resumedLease: boolean;
}) {
return {
provider: "novita",
shellCommand: input.shellCommand,
sandboxId: input.sandbox.sandboxId,
template: input.config.template,
timeoutMs: input.config.timeoutMs,
requestTimeoutMs: input.config.requestTimeoutMs,
autoPause: input.config.autoPause,
reuseLease: input.config.reuseLease,
remoteCwd: input.remoteCwd,
resumedLease: input.resumedLease,
};
}
async function releaseSandbox(config: NovitaDriverConfig, sandboxId: string) {
const sandbox = await getSandboxOrNull(config, sandboxId);
if (!sandbox) return;
if (config.reuseLease) {
await sandbox.betaPause({ requestTimeoutMs: config.requestTimeoutMs }).catch(async () => {
await sandbox.kill({ requestTimeoutMs: config.requestTimeoutMs }).catch(() => undefined);
});
return;
}
await sandbox.kill({ requestTimeoutMs: config.requestTimeoutMs }).catch(() => undefined);
}
async function executeInSandbox(
sandbox: Sandbox,
params: PluginEnvironmentExecuteParams,
config: NovitaDriverConfig,
): Promise<PluginEnvironmentExecuteResult> {
const command = buildShellCommand({
command: params.command,
args: params.args,
cwd: params.cwd,
env: params.env,
stdin: params.stdin,
});
try {
const result = await sandbox.commands.run(command, {
cwd: "/",
timeoutMs: params.timeoutMs ?? config.timeoutMs,
});
return {
exitCode: result.exitCode,
signal: null,
timedOut: false,
stdout: result.stdout,
stderr: result.stderr,
metadata: {
provider: "novita",
sandboxId: sandbox.sandboxId,
},
};
} catch (error) {
const commandError = error as {
exitCode?: number;
stdout?: string;
stderr?: string;
error?: string;
timedOut?: boolean;
};
if (typeof commandError.exitCode === "number" || commandError.timedOut === true) {
return {
exitCode: commandError.exitCode ?? 124,
signal: null,
timedOut: commandError.timedOut === true,
stdout: commandError.stdout ?? "",
stderr: commandError.stderr ?? commandError.error ?? "",
metadata: {
provider: "novita",
sandboxId: sandbox.sandboxId,
},
};
}
throw error;
}
}
const plugin = definePlugin({
async setup(ctx) {
ctx.logger.info("Novita sandbox provider plugin ready");
},
async onHealth() {
return { status: "ok", message: "Novita sandbox provider plugin healthy" };
},
async onEnvironmentValidateConfig(
params: PluginEnvironmentValidateConfigParams,
): Promise<PluginEnvironmentValidationResult> {
const config = parseNovitaDriverConfig(params.config);
const errors = validateNovitaDriverConfig(config);
if (errors.length > 0) {
return { ok: false, errors };
}
return {
ok: true,
normalizedConfig: { ...config },
};
},
async onEnvironmentProbe(
params: PluginEnvironmentProbeParams,
): Promise<PluginEnvironmentProbeResult> {
const config = parseNovitaDriverConfig(params.config);
try {
const sandbox = await createSandbox(params, config);
try {
await ensureWorkspace(sandbox, config.requestedCwd, config);
const shellCommand = await detectShellCommand(sandbox, config);
return {
ok: true,
summary: `Connected to Novita sandbox ${sandbox.sandboxId}.`,
metadata: leaseMetadata({
config,
sandbox,
shellCommand,
remoteCwd: config.requestedCwd,
resumedLease: false,
}),
};
} finally {
await sandbox.kill({ requestTimeoutMs: config.requestTimeoutMs }).catch(() => undefined);
}
} catch (error) {
return {
ok: false,
summary: "Novita sandbox probe failed.",
metadata: {
provider: "novita",
template: config.template,
timeoutMs: config.timeoutMs,
requestTimeoutMs: config.requestTimeoutMs,
reuseLease: config.reuseLease,
error: formatErrorMessage(error),
},
};
}
},
async onEnvironmentAcquireLease(
params: PluginEnvironmentAcquireLeaseParams,
): Promise<PluginEnvironmentLease> {
const config = parseNovitaDriverConfig(params.config);
const sandbox = await createSandbox(params, config);
try {
const remoteCwd = params.requestedCwd?.trim() || config.requestedCwd;
await ensureWorkspace(sandbox, remoteCwd, config);
const shellCommand = await detectShellCommand(sandbox, config);
return {
providerLeaseId: sandbox.sandboxId,
metadata: leaseMetadata({
config,
sandbox,
shellCommand,
remoteCwd,
resumedLease: false,
}),
};
} catch (error) {
await sandbox.kill({ requestTimeoutMs: config.requestTimeoutMs }).catch(() => undefined);
throw error;
}
},
async onEnvironmentResumeLease(
params: PluginEnvironmentResumeLeaseParams,
): Promise<PluginEnvironmentLease> {
if (!params.providerLeaseId) {
return {
providerLeaseId: null,
metadata: {
provider: "novita",
expired: true,
},
};
}
const config = parseNovitaDriverConfig(params.config);
const sandbox = await getSandboxOrNull(config, params.providerLeaseId);
if (!sandbox) {
return {
providerLeaseId: null,
metadata: {
provider: "novita",
expired: true,
},
};
}
await sandbox.setTimeout(config.timeoutMs, { requestTimeoutMs: config.requestTimeoutMs }).catch(() => undefined);
const remoteCwd =
typeof params.leaseMetadata?.remoteCwd === "string" && params.leaseMetadata.remoteCwd.trim().length > 0
? params.leaseMetadata.remoteCwd.trim()
: config.requestedCwd;
await ensureWorkspace(sandbox, remoteCwd, config);
const shellCommand = await detectShellCommand(sandbox, config);
return {
providerLeaseId: sandbox.sandboxId,
metadata: leaseMetadata({
config,
sandbox,
shellCommand,
remoteCwd,
resumedLease: true,
}),
};
},
async onEnvironmentReleaseLease(
params: PluginEnvironmentReleaseLeaseParams,
): Promise<void> {
if (!params.providerLeaseId) return;
const config = parseNovitaDriverConfig(params.config);
await releaseSandbox(config, params.providerLeaseId);
},
async onEnvironmentDestroyLease(
params: PluginEnvironmentDestroyLeaseParams,
): Promise<void> {
if (!params.providerLeaseId) return;
const config = parseNovitaDriverConfig(params.config);
const sandbox = await getSandboxOrNull(config, params.providerLeaseId);
await sandbox?.kill({ requestTimeoutMs: config.requestTimeoutMs }).catch(() => undefined);
},
async onEnvironmentRealizeWorkspace(
params: PluginEnvironmentRealizeWorkspaceParams,
): Promise<PluginEnvironmentRealizeWorkspaceResult> {
const config = parseNovitaDriverConfig(params.config);
const remoteCwd =
typeof params.lease.metadata?.remoteCwd === "string" && params.lease.metadata.remoteCwd.trim().length > 0
? params.lease.metadata.remoteCwd.trim()
: params.workspace.remotePath ?? params.workspace.localPath ?? config.requestedCwd;
if (params.lease.providerLeaseId) {
const sandbox = await getSandboxOrNull(config, params.lease.providerLeaseId);
if (sandbox) {
await ensureWorkspace(sandbox, remoteCwd, config);
}
}
return {
cwd: remoteCwd,
metadata: {
provider: "novita",
remoteCwd,
},
};
},
async onEnvironmentExecute(
params: PluginEnvironmentExecuteParams,
): Promise<PluginEnvironmentExecuteResult> {
if (!params.lease.providerLeaseId) {
return {
exitCode: 1,
signal: null,
timedOut: false,
stdout: "",
stderr: "No provider lease ID available for execution.\n",
};
}
const config = parseNovitaDriverConfig(params.config);
const sandbox = await getSandboxOrNull(config, params.lease.providerLeaseId);
if (!sandbox) {
return {
exitCode: 1,
signal: null,
timedOut: false,
stdout: "",
stderr: "Novita sandbox lease is no longer available.\n",
metadata: {
provider: "novita",
sandboxId: params.lease.providerLeaseId,
expired: true,
},
};
}
return await executeInSandbox(sandbox, params, config);
},
});
export default plugin;
@@ -0,0 +1,5 @@
import { runWorker } from "@paperclipai/plugin-sdk";
import plugin from "./plugin.js";
export default plugin;
runWorker(plugin, import.meta.url);