Files
PaperClipAI/server/src/__tests__/environment-routes.test.ts
T
Dotta 0c1e7504c0 fix(runner): persist warm Daytona workspaces across turns (#12904)
## Thinking Path

> - Daytona preserves a stopped sandbox filesystem, but deleting or
replacing a sandbox removes its only remote copy.
> - Warm reuse therefore improves latency but cannot be Paperclip's
durability boundary.
> - The host execution workspace must remain authoritative after every
successful turn, while same-run recovery must avoid overwriting
unexported remote work.
> - Result proposal, workspace export/merge, and terminal completion
need a durable, replayable ordering so a crash never starts a duplicate
provider turn.
> - A paid browser acceptance suite must exercise both legacy Codex and
Runner Codex for three real turns on one continuously warm Daytona
sandbox.

## Linked Issues or Issue Description

Refs #12901.

Runner Codex did not previously export successful Daytona workspace
changes back to the authoritative host workspace. That made warm reuse
depend on Daytona's remote filesystem and left deleted/replacement
sandboxes without a reliable reconstruction path. The existing paid
fixture also lacked a focused three-turn continuity case for both Codex
adapters.

## What Changed

- Persist versioned, atomic native workspace-sync descriptors and
durable seeds in `PAPERCLIP_HOME`, without credentials or a database
migration.
- Classify fresh, warm, replacement, and same-run-recovery workspace
preparation explicitly; ambiguous lease/root/digest evidence fails
closed.
- Finalize native workspace export/merge after semantic result proposal
and before run completion, with idempotent replay that never submits a
second provider turn.
- Surface legacy Codex workspace restoration failures instead of masking
them, while preserving an earlier provider error when both fail.
- Keep healthy reusable Daytona leases warm for legacy and native
adapters, stamp finalized workspace generations, and retain existing
cleanup behavior for per-turn or unhealthy leases.
- Preserve Runner Codex's provider process/session across warm turns,
including bounded post-terminal tail draining and exact authority
rotation.
- Add the exact paid `daytona-warm-continuity` matrix:
  - `legacy-codex × daytona × warm-three-turn`
  - `runner-codex × daytona × warm-three-turn`
- Drive all three turns through the browser, verify ordered file
continuity and stable lease/workspace/runtime identities, capture
per-turn timings, and delete the sandbox immediately after assertions.
- Document `pnpm test:e2e:runner -- --suite daytona-warm-continuity`; no
package script was added.

## Verification

- `pnpm typecheck` — passed, including migration safety (no migration
added)
- Focused server/runner Vitest coverage — 144 passed
- `pnpm test:e2e:runner:unit` — 114 passed
- `pnpm test:e2e:runner:typecheck` — passed
- `pnpm --filter @paperclipai/paperclip-runner test:codex` — 66 passed,
1 helper ignored
- `native-session-executor.test.ts` — 139 passed, including safe
fail-closed cleanup after remote runner identity capture failure
- Paid local browser acceptance, exact post-rebase Linux/amd64 runner
binary:
- Runner Codex — passed in 1.7m; 3 runs; lease outcomes `created,
resumed, resumed`; 10/10 matchers; cleanup passed
- Legacy Codex — passed in 2.7m; 3 runs; lease outcomes `created,
resumed, resumed`; 10/10 matchers; cleanup passed
- [Protected paid GitHub Actions
campaign](https://github.com/paperclipai/paperclip/actions/runs/34026735033)
against `7da42a91b95fa7fb2df126668ef7e37afb3b2b9d` — passed 2/2:
- Runner Codex — 3 runs; lease outcomes `created, resumed, resumed`;
evidence and cleanup passed
- Legacy Codex — 3 runs; lease outcomes `created, resumed, resumed`;
evidence and cleanup passed
  - Merge/enforcement, S3 history, and Pages publication jobs passed
- Paid result artifacts were scanned for both provider credentials;
neither secret was present.
- Current PR checks — 31 passed, 1 expected Storybook skip; Greptile
5/5; Superagent security scan passed
- `git diff --check origin/master...HEAD` — passed
- Confirmed no `package.json`, lockfile, migration, or SQL changes.

## Risks

- Workspace synchronization now sits on the terminal-success path, so a
remote export failure deliberately prevents false success. Retryable
state retains its lease/seed; loss of the only unexported remote copy
fails closed.
- Warm provider reuse has strict identity and quiescence checks.
Mismatched or ambiguous evidence blocks reuse rather than risking
concurrent provider work.
- The paid suite incurs Daytona and Codex cost only in the existing
protected scheduled/manual workflow and explicitly destroys its sandbox
after each cell.

## Model Used

OpenAI Codex with GPT-5 agentic reasoning, repository inspection, real
browser E2E execution, Rust/TypeScript test execution, and GitHub
Actions diagnostics.

## 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 linked an existing issue or described the issue
in-PR
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name contains no internal ticket id
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have documented the dedicated suite invocation without adding a
package script
- [x] I have considered and documented risks above
- [x] All Paperclip CI gates are green on the current revision
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
on the current revision
- [x] I will address all reviewer comments before requesting merge
2026-09-06 08:16:12 -05:00

3338 lines
116 KiB
TypeScript

import type { Server } from "node:http";
import express from "express";
import request from "supertest";
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { unprocessable } from "../errors.js";
import { environmentRoutes } from "../routes/environments.js";
import { errorHandler } from "../middleware/index.js";
const mockAccessService = vi.hoisted(() => ({
canUser: vi.fn(),
hasPermission: vi.fn(),
decide: vi.fn(),
}));
const mockAgentService = vi.hoisted(() => ({
getById: vi.fn(),
}));
const mockIssueService = vi.hoisted(() => ({
getById: vi.fn(),
clearExecutionWorkspaceEnvironmentSelection: vi.fn(),
}));
const mockProjectService = vi.hoisted(() => ({
getById: vi.fn(),
clearExecutionWorkspaceEnvironmentSelection: vi.fn(),
}));
const mockEnvironmentRuntimeService = vi.hoisted(() => ({
destroyReusableSandboxLeasesForEnvironment: vi.fn(async () => ({ destroyed: 0, failed: 0, skippedLiveRun: 0 })),
}));
const mockCloseWarmNativeSessionsForEnvironment = vi.hoisted(() =>
vi.fn(async () => ({ closed: 0, busy: 0, failed: 0 })),
);
const mockInstanceSettingsService = vi.hoisted(() => ({
listCompanyIds: vi.fn(),
getGeneral: vi.fn(),
getExperimental: vi.fn(),
}));
const mockEnvironmentService = vi.hoisted(() => ({
list: vi.fn(),
getById: vi.fn(),
create: vi.fn(),
update: vi.fn(),
removeIfDeletable: vi.fn(),
getDeleteBlastRadius: vi.fn(),
hasUnresolvedPendingCleanupLeases: vi.fn(),
listLeases: vi.fn(),
getLeaseById: vi.fn(),
}));
const mockEnvironmentCustomImageService = vi.hoisted(() => ({
getOverview: vi.fn(),
getActiveTemplate: vi.fn(),
getSessionById: vi.fn(),
startSetupSession: vi.fn(),
refreshSetupSession: vi.fn(),
finishSetupSession: vi.fn(),
cancelSetupSession: vi.fn(),
rollbackTemplate: vi.fn(),
disableTemplate: vi.fn(),
cleanupExpiredSetupSessions: vi.fn(),
}));
const mockLogActivity = vi.hoisted(() => vi.fn());
const mockProbeEnvironment = vi.hoisted(() => vi.fn());
const mockSecretService = vi.hoisted(() => ({
create: vi.fn(),
normalizeEnvBindingsForPersistence: vi.fn(),
listBindingCompanyIdsForTarget: vi.fn(),
resolveSecretValue: vi.fn(),
resolveSecretValueForEphemeralAccess: vi.fn(),
syncEnvBindingsForTarget: vi.fn(),
syncSecretRefsForTarget: vi.fn(),
replaceSecretRefsForInstanceTarget: vi.fn(),
describeSecretRefs: vi.fn(),
remove: vi.fn(),
}));
const mockValidatePluginEnvironmentDriverConfig = vi.hoisted(() => vi.fn());
const mockValidatePluginSandboxProviderConfig = vi.hoisted(() => vi.fn());
const mockListReadyPluginEnvironmentDrivers = vi.hoisted(() => vi.fn());
const mockResolvePluginSandboxProviderDriverByKey = vi.hoisted(() => vi.fn());
const mockStartPluginEnvironmentInteractiveSetup = vi.hoisted(() => vi.fn());
const mockGetPluginEnvironmentInteractiveSetup = vi.hoisted(() => vi.fn());
const mockCapturePluginEnvironmentTemplate = vi.hoisted(() => vi.fn());
const mockCancelPluginEnvironmentInteractiveSetup = vi.hoisted(() => vi.fn());
const mockDeletePluginEnvironmentTemplate = vi.hoisted(() => vi.fn());
const mockExecutionWorkspaceService = vi.hoisted(() => ({
clearEnvironmentSelection: vi.fn(),
}));
vi.mock("../services/index.js", () => ({
issueService: () => mockIssueService,
instanceSettingsService: () => mockInstanceSettingsService,
environmentCustomImageService: () => mockEnvironmentCustomImageService,
environmentService: () => mockEnvironmentService,
logActivity: mockLogActivity,
projectService: () => mockProjectService,
}));
vi.mock("../services/environment-probe.js", () => ({
probeEnvironment: mockProbeEnvironment,
}));
vi.mock("../services/secrets.js", () => ({
secretService: () => mockSecretService,
}));
vi.mock("../services/environments.js", () => ({
environmentService: () => mockEnvironmentService,
}));
vi.mock("../services/environment-runtime.js", () => ({
environmentRuntimeService: () => mockEnvironmentRuntimeService,
}));
vi.mock("../services/native-runtime/native-session-executor.js", () => ({
closeWarmNativeSessionsForEnvironment:
mockCloseWarmNativeSessionsForEnvironment,
}));
vi.mock("../services/execution-workspaces.js", () => ({
executionWorkspaceService: () => mockExecutionWorkspaceService,
}));
vi.mock("../services/plugin-environment-driver.js", () => ({
// The runtime reads this published constant at import time. Mirror the real
// value so the mocked module keeps the same reusable-lease method contract.
REUSABLE_LEASE_WORKER_METHODS: ["environmentResumeLease", "environmentReleaseLease", "environmentDestroyLease"],
listReadyPluginEnvironmentDrivers: mockListReadyPluginEnvironmentDrivers,
resolvePluginSandboxProviderDriverByKey: mockResolvePluginSandboxProviderDriverByKey,
startPluginEnvironmentInteractiveSetup: mockStartPluginEnvironmentInteractiveSetup,
getPluginEnvironmentInteractiveSetup: mockGetPluginEnvironmentInteractiveSetup,
capturePluginEnvironmentTemplate: mockCapturePluginEnvironmentTemplate,
cancelPluginEnvironmentInteractiveSetup: mockCancelPluginEnvironmentInteractiveSetup,
deletePluginEnvironmentTemplate: mockDeletePluginEnvironmentTemplate,
validatePluginEnvironmentDriverConfig: mockValidatePluginEnvironmentDriverConfig,
validatePluginSandboxProviderConfig: mockValidatePluginSandboxProviderConfig,
}));
function createEnvironment() {
const now = new Date("2026-04-16T05:00:00.000Z");
return {
id: "env-1",
companyId: "company-1",
name: "Local",
description: "Current development machine",
driver: "local",
status: "active" as const,
config: { shell: "zsh" },
envVars: {},
metadata: { source: "manual" },
createdAt: now,
updatedAt: now,
};
}
function createDeleteBlastRadius(overrides: Partial<{
isManagedLocal: boolean;
isInstanceDefault: boolean;
agentDefaultCount: number;
executionWorkspaceSelectionCount: number;
issueSelectionCount: number;
projectSelectionCount: number;
secretBindingCount: number;
activeLeaseCount: number;
activeCustomImageSetupSessionCount: number;
pendingCleanupLeaseCount: number;
reusableSandboxLeaseCount: number;
reusableSandboxLeaseHolders: Array<{
leaseId: string;
executionWorkspaceId: string | null;
executionWorkspaceName: string | null;
issueId: string | null;
issueIdentifier: string | null;
issueTitle: string | null;
}>;
}> = {}) {
const staticReferences = {
isManagedLocal: overrides.isManagedLocal ?? false,
isInstanceDefault: overrides.isInstanceDefault ?? false,
agentDefaultCount: overrides.agentDefaultCount ?? 0,
executionWorkspaceSelectionCount: overrides.executionWorkspaceSelectionCount ?? 0,
issueSelectionCount: overrides.issueSelectionCount ?? 0,
projectSelectionCount: overrides.projectSelectionCount ?? 0,
secretBindingCount: overrides.secretBindingCount ?? 0,
};
const activeRuntimeUse = {
activeLeaseCount: overrides.activeLeaseCount ?? 0,
activeCustomImageSetupSessionCount: overrides.activeCustomImageSetupSessionCount ?? 0,
hasActiveRuntimeUse:
(overrides.activeLeaseCount ?? 0) > 0
|| (overrides.activeCustomImageSetupSessionCount ?? 0) > 0,
};
const pendingCleanupLeaseCount = overrides.pendingCleanupLeaseCount ?? 0;
const reusableSandboxLeaseCount = overrides.reusableSandboxLeaseCount ?? 0;
const reusableSandboxLeaseHolders =
overrides.reusableSandboxLeaseHolders
?? Array.from({ length: reusableSandboxLeaseCount }, (_, index) => ({
leaseId: `lease-${index + 1}`,
executionWorkspaceId: null,
executionWorkspaceName: null,
issueId: null,
issueIdentifier: null,
issueTitle: null,
}));
const deleteBlockedReasons = [
...(staticReferences.isManagedLocal ? ["managed_local" as const] : []),
...(staticReferences.isInstanceDefault ? ["instance_default" as const] : []),
...(pendingCleanupLeaseCount > 0 ? ["pending_sandbox_cleanup" as const] : []),
...(reusableSandboxLeaseCount > 0 ? ["reusable_sandbox_lease" as const] : []),
];
return {
environmentId: "env-1",
canDelete: deleteBlockedReasons.length === 0,
deleteBlockedReasons,
pendingCleanupLeaseCount,
reusableSandboxLeaseCount,
reusableSandboxLeaseHolders,
staticReferences,
activeRuntimeUse,
};
}
let server: Server | null = null;
let currentActor: Record<string, unknown> = {
type: "board",
userId: "user-1",
source: "local_implicit",
};
const routeOptions: Record<string, unknown> = {};
const originalSecretsProviderEnv = process.env.PAPERCLIP_SECRETS_PROVIDER;
// The routes open a transaction around environment writes and their binding
// syncs. Service calls are mocked, so the executor never runs a real query —
// it only needs to be identity-checkable in assertions.
const routeDbTx = { __routeDbTx: true };
const routeDb = {
transaction: async <T>(fn: (tx: unknown) => Promise<T>): Promise<T> =>
fn(routeDbTx),
};
function createApp(actor: Record<string, unknown>, options: Record<string, unknown> = {}) {
currentActor = actor;
for (const key of Object.keys(routeOptions)) {
delete routeOptions[key];
}
Object.assign(routeOptions, options);
if (server) return server;
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
(req as any).actor = currentActor;
next();
});
app.use("/api", environmentRoutes(routeDb as any, routeOptions as any));
app.use(errorHandler);
server = app.listen(0);
return server;
}
describe("environment routes", () => {
afterAll(async () => {
if (originalSecretsProviderEnv === undefined) {
delete process.env.PAPERCLIP_SECRETS_PROVIDER;
} else {
process.env.PAPERCLIP_SECRETS_PROVIDER = originalSecretsProviderEnv;
}
if (!server) return;
await new Promise<void>((resolve, reject) => {
server?.close((err) => {
if (err) reject(err);
else resolve();
});
});
server = null;
});
beforeEach(() => {
mockAccessService.canUser.mockReset();
mockAccessService.hasPermission.mockReset();
mockAccessService.decide.mockReset();
mockAgentService.getById.mockReset();
mockIssueService.getById.mockReset();
mockIssueService.clearExecutionWorkspaceEnvironmentSelection.mockReset();
mockProjectService.getById.mockReset();
mockProjectService.clearExecutionWorkspaceEnvironmentSelection.mockReset();
mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment.mockReset();
mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment.mockResolvedValue(
{ destroyed: 0, failed: 0, skippedLiveRun: 0 },
);
mockCloseWarmNativeSessionsForEnvironment.mockReset();
mockCloseWarmNativeSessionsForEnvironment.mockResolvedValue({
closed: 0,
busy: 0,
failed: 0,
});
mockInstanceSettingsService.listCompanyIds.mockReset();
mockInstanceSettingsService.getGeneral.mockReset();
mockInstanceSettingsService.getGeneral.mockResolvedValue({ executionMode: "any" });
mockInstanceSettingsService.getExperimental.mockReset();
mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: false });
mockEnvironmentService.list.mockReset();
mockEnvironmentService.list.mockResolvedValue([]);
mockEnvironmentService.getById.mockReset();
mockEnvironmentService.create.mockReset();
mockEnvironmentService.update.mockReset();
mockEnvironmentService.removeIfDeletable.mockReset();
mockEnvironmentService.getDeleteBlastRadius.mockReset();
mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockReset();
mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(false);
mockEnvironmentService.listLeases.mockReset();
mockEnvironmentService.getLeaseById.mockReset();
mockExecutionWorkspaceService.clearEnvironmentSelection.mockReset();
Object.values(mockEnvironmentCustomImageService).forEach((mock) => mock.mockReset());
mockEnvironmentCustomImageService.getOverview.mockResolvedValue({
activeTemplate: null,
activeSession: null,
latestSession: null,
});
mockEnvironmentCustomImageService.getActiveTemplate.mockResolvedValue(null);
mockEnvironmentCustomImageService.getSessionById.mockResolvedValue(null);
mockLogActivity.mockReset();
mockProbeEnvironment.mockReset();
mockSecretService.create.mockReset();
mockSecretService.normalizeEnvBindingsForPersistence.mockReset();
mockSecretService.listBindingCompanyIdsForTarget.mockReset();
mockSecretService.resolveSecretValue.mockReset();
mockSecretService.resolveSecretValueForEphemeralAccess.mockReset();
mockSecretService.syncEnvBindingsForTarget.mockReset();
mockSecretService.syncSecretRefsForTarget.mockReset();
mockSecretService.replaceSecretRefsForInstanceTarget.mockReset();
mockSecretService.describeSecretRefs.mockReset();
mockSecretService.describeSecretRefs.mockResolvedValue([]);
mockSecretService.remove.mockReset();
mockSecretService.create.mockResolvedValue({
id: "11111111-1111-1111-1111-111111111111",
});
mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1"]);
mockIssueService.clearExecutionWorkspaceEnvironmentSelection.mockResolvedValue(0);
mockProjectService.clearExecutionWorkspaceEnvironmentSelection.mockResolvedValue(0);
mockExecutionWorkspaceService.clearEnvironmentSelection.mockResolvedValue(0);
mockSecretService.normalizeEnvBindingsForPersistence.mockImplementation(async (_companyId, env) => env ?? {});
mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue([]);
mockSecretService.syncEnvBindingsForTarget.mockResolvedValue([]);
mockSecretService.syncSecretRefsForTarget.mockResolvedValue([]);
mockSecretService.replaceSecretRefsForInstanceTarget.mockResolvedValue([]);
mockSecretService.remove.mockResolvedValue(null);
mockSecretService.resolveSecretValueForEphemeralAccess.mockResolvedValue("resolved-provider-key");
delete process.env.PAPERCLIP_SECRETS_PROVIDER;
mockValidatePluginEnvironmentDriverConfig.mockReset();
mockValidatePluginEnvironmentDriverConfig.mockImplementation(async ({ config }) => config);
mockValidatePluginSandboxProviderConfig.mockReset();
mockValidatePluginSandboxProviderConfig.mockImplementation(async ({ provider, config }) => ({
normalizedConfig: config,
pluginId: `plugin-${provider}`,
pluginKey: `plugin.${provider}`,
driver: {
driverKey: provider,
kind: "sandbox_provider",
displayName: provider,
configSchema: { type: "object" },
},
}));
mockResolvePluginSandboxProviderDriverByKey.mockReset();
mockResolvePluginSandboxProviderDriverByKey.mockImplementation(async ({ driverKey }) => (
driverKey === "secure-plugin"
? {
pluginId: "plugin-secure",
pluginKey: "acme.secure-sandbox-provider",
driver: {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
}
: null
));
mockListReadyPluginEnvironmentDrivers.mockReset();
mockListReadyPluginEnvironmentDrivers.mockResolvedValue([]);
mockStartPluginEnvironmentInteractiveSetup.mockReset();
mockGetPluginEnvironmentInteractiveSetup.mockReset();
mockCapturePluginEnvironmentTemplate.mockReset();
mockCancelPluginEnvironmentInteractiveSetup.mockReset();
mockDeletePluginEnvironmentTemplate.mockReset();
mockAccessService.decide.mockResolvedValue({
allowed: true,
explanation: "Allowed by test harness",
});
});
it("lists instance-scoped environments through the company route alias", async () => {
mockEnvironmentService.list.mockResolvedValue([createEnvironment()]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app).get("/api/companies/company-1/environments?driver=local");
expect(res.status).toBe(200);
expect(res.body).toHaveLength(1);
expect(mockEnvironmentService.list).toHaveBeenCalledWith({
status: undefined,
driver: "local",
});
});
it("redacts environment config for non-admin board readers", async () => {
mockEnvironmentService.list.mockResolvedValue([createEnvironment()]);
const app = createApp({
type: "board",
userId: "user-2",
source: "session",
companyIds: ["company-1"],
memberships: [{ companyId: "company-1", status: "active", membershipRole: "member" }],
isInstanceAdmin: false,
});
const res = await request(app).get("/api/companies/company-1/environments");
expect(res.status).toBe(200);
expect(res.body[0]).toMatchObject({
id: "env-1",
name: "Local",
config: {},
envVars: {},
metadata: null,
});
});
it("redacts environment detail config for non-admin board readers", async () => {
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
const app = createApp({
type: "board",
userId: "user-2",
source: "session",
companyIds: ["company-1"],
memberships: [{ companyId: "company-1", status: "active", membershipRole: "member" }],
isInstanceAdmin: false,
});
const res = await request(app).get("/api/environments/env-1");
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
id: "env-1",
config: {},
envVars: {},
metadata: null,
});
});
describe("platform-provisioned environment floor on cloud-managed instances", () => {
function createPlatformSandboxEnvironment() {
const now = new Date("2026-04-16T05:00:00.000Z");
return {
id: "env-managed-1",
companyId: "company-1",
name: "Daytona",
description: "Managed sandbox environment",
driver: "sandbox",
status: "active" as const,
config: {
provider: "daytona",
image: "custom-image:latest",
target: "us",
apiKey: "config-credential-must-never-echo",
},
envVars: { MY_AGENT_TOOL_SETTING: "tenant-env-value" },
metadata: { managedByPaperclip: true, managedSandboxProvider: "daytona" },
createdAt: now,
updatedAt: now,
};
}
const ownerAdminActor = {
type: "board",
userId: "owner-1",
source: "cloud_tenant",
companyIds: ["company-1"],
memberships: [{ companyId: "company-1", status: "active", membershipRole: "owner" }],
isInstanceAdmin: true,
};
// A minimal valid managed-config document whose `environments` entry makes
// the managed-sandbox provisioner own the marked sandbox slot row.
const MANAGED_CONFIG_WITH_SANDBOX_ENTRY = JSON.stringify({
v: 1,
mode: "cloud",
catalogVersion: "2026.720.0",
features: {},
plugins: { autoInstall: [] },
environments: [{ name: "Sandbox", provider: "daytona" }],
});
beforeEach(() => {
process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN = "test-server-token";
});
afterEach(() => {
delete process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN;
});
it("never echoes credential-shaped config keys, while tenant env vars round-trip", async () => {
mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment());
const app = createApp(ownerAdminActor);
const res = await request(app).get("/api/environments/env-managed-1");
expect(res.status).toBe(200);
// Env vars are the tenant-owned field on the managed sandbox row
// (the platform never writes them), so they echo for editing.
expect(res.body.envVars).toEqual({ MY_AGENT_TOOL_SETTING: "tenant-env-value" });
expect(res.body.config).toEqual({
provider: "daytona",
image: "custom-image:latest",
target: "us",
});
expect(res.body.metadata).toMatchObject({ managedByPaperclip: true });
expect(JSON.stringify(res.body)).not.toContain("config-credential-must-never-echo");
});
it("keeps blanking env vars on legacy kubernetes-marker rows", async () => {
// Pre-generalization builds may have written platform values into
// these rows' env vars, so the legacy floor stays absolute there.
mockEnvironmentService.getById.mockResolvedValue({
...createPlatformSandboxEnvironment(),
metadata: {
managedByPaperclip: true,
managedSandboxProvider: "kubernetes",
managedKubernetesSandbox: true,
},
});
const app = createApp(ownerAdminActor);
const res = await request(app).get("/api/environments/env-managed-1");
expect(res.status).toBe(200);
expect(res.body.envVars).toEqual({});
});
it("exposes structural config to restricted company readers instead of blanking it", async () => {
mockEnvironmentService.list.mockResolvedValue([createPlatformSandboxEnvironment()]);
const app = createApp({
type: "board",
userId: "user-2",
source: "session",
companyIds: ["company-1"],
memberships: [{ companyId: "company-1", status: "active", membershipRole: "member" }],
isInstanceAdmin: false,
});
const res = await request(app).get("/api/companies/company-1/environments");
expect(res.status).toBe(200);
expect(res.body[0].config).toEqual({
provider: "daytona",
image: "custom-image:latest",
target: "us",
});
// Tenant env vars can carry pasted credentials, so restricted readers
// get the same blank envVars posture as on every other environment.
expect(res.body[0].envVars).toEqual({});
expect(res.body[0].metadata).toMatchObject({ managedByPaperclip: true });
});
it("rejects updates to platform-provisioned rows, including for instance admins", async () => {
mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment());
const app = createApp(ownerAdminActor);
const res = await request(app).patch("/api/environments/env-managed-1").send({ name: "Renamed" });
expect(res.status).toBe(403);
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
});
it("allows an envVars-only patch on the managed sandbox row", async () => {
const row = createPlatformSandboxEnvironment();
mockEnvironmentService.getById.mockResolvedValue(row);
mockEnvironmentService.update.mockResolvedValue({
...row,
envVars: { MY_AGENT_TOOL_SETTING: "updated-value", EXTRA: "added" },
});
const app = createApp(ownerAdminActor);
const res = await request(app)
.patch("/api/environments/env-managed-1")
.send({ envVars: { MY_AGENT_TOOL_SETTING: "updated-value", EXTRA: "added" } });
expect(res.status).toBe(200);
expect(mockEnvironmentService.update).toHaveBeenCalledWith(
"env-managed-1",
expect.objectContaining({
envVars: { MY_AGENT_TOOL_SETTING: "updated-value", EXTRA: "added" },
}),
expect.anything(),
);
expect(res.body.envVars).toEqual({ MY_AGENT_TOOL_SETTING: "updated-value", EXTRA: "added" });
});
it("scopes the envVars patch to an explicit companyId query for a multi-company actor", async () => {
const row = createPlatformSandboxEnvironment();
mockEnvironmentService.getById.mockResolvedValue(row);
mockEnvironmentService.update.mockResolvedValue({ ...row, envVars: { A: "1" } });
// No prior bindings and two memberships: neither inference path can
// pin a company, so the explicit query context must carry the save.
mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue([]);
const app = createApp({
...ownerAdminActor,
companyIds: ["company-1", "company-2"],
memberships: [
{ companyId: "company-1", status: "active", membershipRole: "owner" },
{ companyId: "company-2", status: "active", membershipRole: "member" },
],
});
const res = await request(app)
.patch("/api/environments/env-managed-1?companyId=company-1")
.send({ envVars: { A: "1" } });
expect(res.status).toBe(200);
expect(mockSecretService.normalizeEnvBindingsForPersistence).toHaveBeenCalledWith(
"company-1",
{ A: "1" },
expect.anything(),
);
});
it("falls back to the instance's only company when the actor's memberships cannot pin one", async () => {
const row = createPlatformSandboxEnvironment();
mockEnvironmentService.getById.mockResolvedValue(row);
mockEnvironmentService.update.mockResolvedValue({ ...row, envVars: { A: "1" } });
mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue([]);
// An instance admin provisioned without membership rows — the
// owner-as-admin shape on managed stacks.
const app = createApp({
...ownerAdminActor,
companyIds: [],
memberships: [],
});
const res = await request(app)
.patch("/api/environments/env-managed-1")
.send({ envVars: { A: "1" } });
expect(res.status).toBe(200);
expect(mockSecretService.normalizeEnvBindingsForPersistence).toHaveBeenCalledWith(
"company-1",
{ A: "1" },
expect.anything(),
);
});
it("still fails closed when no companyId context is resolvable on a multi-company instance", async () => {
mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment());
mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue([]);
mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1", "company-2"]);
const app = createApp({
...ownerAdminActor,
companyIds: ["company-1", "company-2"],
memberships: [
{ companyId: "company-1", status: "active", membershipRole: "owner" },
{ companyId: "company-2", status: "active", membershipRole: "member" },
],
});
const res = await request(app)
.patch("/api/environments/env-managed-1")
.send({ envVars: { A: "1" } });
expect(res.status).toBe(422);
expect(res.body.error).toContain("requires a companyId context");
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
});
it("rejects a patch that mixes envVars with any other field on the managed sandbox row", async () => {
mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment());
const app = createApp(ownerAdminActor);
const res = await request(app)
.patch("/api/environments/env-managed-1")
.send({ envVars: { A: "1" }, name: "Renamed" });
expect(res.status).toBe(403);
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
});
it("rejects an envVars-only patch on a legacy kubernetes-marker row", async () => {
mockEnvironmentService.getById.mockResolvedValue({
...createPlatformSandboxEnvironment(),
metadata: {
managedByPaperclip: true,
managedSandboxProvider: "kubernetes",
managedKubernetesSandbox: true,
},
});
const app = createApp(ownerAdminActor);
const res = await request(app)
.patch("/api/environments/env-managed-1")
.send({ envVars: { A: "1" } });
expect(res.status).toBe(403);
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
});
it("still rejects deletion of the managed sandbox row", async () => {
mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment());
const app = createApp(ownerAdminActor);
const res = await request(app).delete("/api/environments/env-managed-1");
expect(res.status).toBe(403);
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
});
it("hides the local environment from every read surface under managed-sandbox-only", async () => {
mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: true });
const localRow = {
...createPlatformSandboxEnvironment(),
id: "env-local-1",
name: "Local",
driver: "local",
config: {},
envVars: {},
metadata: { managedByPaperclip: true, defaultForInstance: true },
};
mockEnvironmentService.list.mockResolvedValue([localRow, createPlatformSandboxEnvironment()]);
const app = createApp(ownerAdminActor);
const listRes = await request(app).get("/api/companies/company-1/environments");
expect(listRes.status).toBe(200);
expect(listRes.body.map((row: { id: string }) => row.id)).toEqual(["env-managed-1"]);
mockEnvironmentService.getById.mockResolvedValue(localRow);
const byIdRes = await request(app).get("/api/environments/env-local-1");
expect(byIdRes.status).toBe(404);
});
it("keeps the local environment visible when managed-sandbox-only is off", async () => {
const localRow = {
...createPlatformSandboxEnvironment(),
id: "env-local-1",
name: "Local",
driver: "local",
config: {},
envVars: {},
metadata: { managedByPaperclip: true },
};
mockEnvironmentService.list.mockResolvedValue([localRow]);
const app = createApp(ownerAdminActor);
const listRes = await request(app).get("/api/companies/company-1/environments");
expect(listRes.status).toBe(200);
expect(listRes.body.map((row: { id: string }) => row.id)).toEqual(["env-local-1"]);
});
it("allows a marker-clear-only patch to unblock a row with a stale legacy kubernetes marker", async () => {
// A sandbox row carrying only the legacy wrapper marker does not hold
// the managed sandbox slot (`environments_managed_sandbox_idx` keys on
// `managedByPaperclip`), and with the persisted execution mode not
// forcing kubernetes nothing selects rows by that marker either — so
// the marker is a stale leftover, not live platform state.
const staleRow = {
...createPlatformSandboxEnvironment(),
id: "env-legacy-1",
metadata: { managedKubernetesSandbox: true },
};
mockEnvironmentService.getById.mockResolvedValue(staleRow);
mockEnvironmentService.update.mockResolvedValue({ ...staleRow, metadata: {} });
const app = createApp(ownerAdminActor);
const res = await request(app)
.patch("/api/environments/env-legacy-1")
.send({ metadata: { managedKubernetesSandbox: false } });
expect(res.status).toBe(200);
expect(mockEnvironmentService.update).toHaveBeenCalled();
});
it("allows a marker-clear-only patch on a non-slot driver with a stale platform marker", async () => {
const staleRow = {
...createPlatformSandboxEnvironment(),
id: "env-stale-ssh-1",
driver: "ssh",
metadata: { managedByPaperclip: true },
};
mockEnvironmentService.getById.mockResolvedValue(staleRow);
mockEnvironmentService.update.mockResolvedValue({ ...staleRow, metadata: {} });
const app = createApp(ownerAdminActor);
const res = await request(app)
.patch("/api/environments/env-stale-ssh-1")
.send({ metadata: { managedByPaperclip: false, managedKubernetesSandbox: false } });
expect(res.status).toBe(200);
expect(mockEnvironmentService.update).toHaveBeenCalled();
});
it("refuses the marker-clear patch on the sandbox slot row while managed provisioning is configured", async () => {
// With a managed-config `environments` entry, driver=sandbox +
// managedByPaperclip is THE provisioner-owned slot row, adopted and
// refreshed on every boot — clearing its markers would reclassify it
// tenant-managed and let the next PATCH/DELETE bypass the write floor.
process.env.PAPERCLIP_MANAGED_CONFIG = MANAGED_CONFIG_WITH_SANDBOX_ENTRY;
try {
mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment());
const app = createApp(ownerAdminActor);
const res = await request(app)
.patch("/api/environments/env-managed-1")
.send({ metadata: { managedByPaperclip: false, managedKubernetesSandbox: false } });
expect(res.status).toBe(403);
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
} finally {
delete process.env.PAPERCLIP_MANAGED_CONFIG;
}
});
it("refuses the marker-clear patch on the sandbox slot row under the forced kubernetes execution mode", async () => {
// PAPERCLIP_EXECUTION_MODE=kubernetes is the other bootstrap path that
// owns (adopts and refreshes) the single marked sandbox row.
process.env.PAPERCLIP_EXECUTION_MODE = "kubernetes";
try {
mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment());
const app = createApp(ownerAdminActor);
const res = await request(app)
.patch("/api/environments/env-managed-1")
.send({ metadata: { managedByPaperclip: false, managedKubernetesSandbox: false } });
expect(res.status).toBe(403);
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
} finally {
delete process.env.PAPERCLIP_EXECUTION_MODE;
}
});
it("refuses the marker-clear patch on a kubernetes-marked row while the persisted execution mode forces kubernetes", async () => {
// `findKubernetesEnvironment` selects sandbox rows by the legacy
// marker alone whenever the persisted executionMode forces kubernetes
// — including when the bootstrap env that seeded the setting is gone
// (rollback / config drift, which the heartbeat handles explicitly).
// Clearing the marker would declassify the live runtime row.
mockInstanceSettingsService.getGeneral.mockResolvedValue({ executionMode: "kubernetes" });
mockEnvironmentService.getById.mockResolvedValue({
...createPlatformSandboxEnvironment(),
id: "env-legacy-1",
metadata: { managedKubernetesSandbox: true },
});
const app = createApp(ownerAdminActor);
const res = await request(app)
.patch("/api/environments/env-legacy-1")
.send({ metadata: { managedKubernetesSandbox: false } });
expect(res.status).toBe(403);
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
});
it("refuses the marker-clear patch on the kubernetes-managed row when only the persisted execution mode remains forced", async () => {
// Drift variant with the fully-stamped managed row: no managed-config
// entry and no bootstrap env, but the persisted executionMode still
// forces kubernetes, so the marker keeps selecting this row for runs.
mockInstanceSettingsService.getGeneral.mockResolvedValue({ executionMode: "kubernetes" });
mockEnvironmentService.getById.mockResolvedValue({
...createPlatformSandboxEnvironment(),
metadata: {
managedByPaperclip: true,
managedSandboxProvider: "kubernetes",
managedKubernetesSandbox: true,
},
});
const app = createApp(ownerAdminActor);
const res = await request(app)
.patch("/api/environments/env-managed-1")
.send({ metadata: { managedByPaperclip: false, managedKubernetesSandbox: false } });
expect(res.status).toBe(403);
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
});
it("allows the marker-clear patch on a marked sandbox row when no provisioning path is configured", async () => {
// Without a managed-config `environments` entry or a forced execution
// mode, nothing on this instance provisions a sandbox environment, so a
// platform marker on a sandbox row can only be a stale leftover of the
// old unrestricted API — the recovery hatch must apply or the row is
// locked forever.
const staleRow = createPlatformSandboxEnvironment();
mockEnvironmentService.getById.mockResolvedValue(staleRow);
mockEnvironmentService.update.mockResolvedValue({ ...staleRow, metadata: {} });
const app = createApp(ownerAdminActor);
const res = await request(app)
.patch("/api/environments/env-managed-1")
.send({ metadata: { managedByPaperclip: false, managedKubernetesSandbox: false } });
expect(res.status).toBe(200);
expect(mockEnvironmentService.update).toHaveBeenCalled();
});
it("refuses the marker-clear patch on the managed local row", async () => {
// The local slot needs no configuration check: on a cloud-managed
// instance `ensureLocalEnvironment` adopts and stamps the single local
// row from every caller, so its markers are always live platform state.
mockEnvironmentService.getById.mockResolvedValue({
...createPlatformSandboxEnvironment(),
id: "env-local-1",
driver: "local",
metadata: { managedByPaperclip: true, defaultForInstance: true },
});
const app = createApp(ownerAdminActor);
const res = await request(app)
.patch("/api/environments/env-local-1")
.send({ metadata: { managedByPaperclip: false } });
expect(res.status).toBe(403);
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
});
it("rejects non-marker-only patches to platform-provisioned rows even from instance admins", async () => {
mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment());
const app = createApp(ownerAdminActor);
const res = await request(app)
.patch("/api/environments/env-managed-1")
.send({ name: "Renamed", metadata: { managedByPaperclip: false } });
expect(res.status).toBe(403);
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
});
it("rejects deletes of platform-provisioned rows, including for instance admins", async () => {
mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment());
const app = createApp(ownerAdminActor);
const res = await request(app).delete("/api/environments/env-managed-1");
expect(res.status).toBe(403);
expect(res.body.details).toMatchObject({ code: "environment_platform_managed" });
expect(mockEnvironmentService.getDeleteBlastRadius).not.toHaveBeenCalled();
expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled();
});
it("rejects creates that stamp platform markers so tenants cannot self-lock rows", async () => {
const app = createApp(ownerAdminActor);
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Fake managed",
driver: "sandbox",
config: { provider: "daytona" },
metadata: { managedByPaperclip: true },
});
expect(res.status).toBe(422);
expect(res.body.details).toMatchObject({ code: "environment_platform_marker_reserved" });
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
});
it("rejects tenant patches that stamp platform markers so the row cannot become locked", async () => {
const tenantEnvironment = {
...createPlatformSandboxEnvironment(),
id: "env-tenant-1",
metadata: { source: "manual" },
};
mockEnvironmentService.getById.mockResolvedValue(tenantEnvironment);
const app = createApp(ownerAdminActor);
const res = await request(app)
.patch("/api/environments/env-tenant-1")
.send({ metadata: { source: "manual", managedKubernetesSandbox: true } });
expect(res.status).toBe(422);
expect(res.body.details).toMatchObject({ code: "environment_platform_marker_reserved" });
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
});
it("accepts platform markers in client payloads on self-hosted instances", async () => {
delete process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN;
const existing = {
...createPlatformSandboxEnvironment(),
id: "env-tenant-1",
metadata: { source: "manual" },
};
mockEnvironmentService.getById.mockResolvedValue(existing);
mockEnvironmentService.update.mockResolvedValue({
...existing,
metadata: { source: "manual", managedByPaperclip: true },
});
const app = createApp({
type: "board",
userId: "admin-1",
source: "session",
isInstanceAdmin: true,
});
const res = await request(app)
.patch("/api/environments/env-tenant-1")
.send({ metadata: { source: "manual", managedByPaperclip: true } });
expect(res.status).toBe(200);
expect(mockEnvironmentService.update).toHaveBeenCalled();
});
it("still updates tenant-created environments for instance admins on cloud-managed instances", async () => {
const tenantEnvironment = {
...createPlatformSandboxEnvironment(),
id: "env-tenant-1",
metadata: { source: "manual" },
};
mockEnvironmentService.getById.mockResolvedValue(tenantEnvironment);
mockEnvironmentService.update.mockResolvedValue({ ...tenantEnvironment, name: "Renamed" });
const app = createApp(ownerAdminActor);
const res = await request(app).patch("/api/environments/env-tenant-1").send({ name: "Renamed" });
expect(res.status).toBe(200);
expect(res.body.name).toBe("Renamed");
expect(mockEnvironmentService.update).toHaveBeenCalled();
});
it("does not floor writes to platform-marked rows on self-hosted instances", async () => {
delete process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN;
const existing = createPlatformSandboxEnvironment();
mockEnvironmentService.getById.mockResolvedValue(existing);
mockEnvironmentService.update.mockResolvedValue({ ...existing, name: "Renamed" });
const app = createApp({
type: "board",
userId: "admin-1",
source: "session",
isInstanceAdmin: true,
});
const res = await request(app).patch("/api/environments/env-managed-1").send({ name: "Renamed" });
expect(res.status).toBe(200);
expect(res.body.name).toBe("Renamed");
});
it("leaves tenant-created environments unfloored for instance admins", async () => {
const tenantEnvironment = {
...createPlatformSandboxEnvironment(),
id: "env-tenant-1",
metadata: { source: "manual" },
};
mockEnvironmentService.getById.mockResolvedValue(tenantEnvironment);
const app = createApp(ownerAdminActor);
const res = await request(app).get("/api/environments/env-tenant-1");
expect(res.status).toBe(200);
expect(res.body.envVars).toEqual({ MY_AGENT_TOOL_SETTING: "tenant-env-value" });
expect(res.body.config.apiKey).toBe("config-credential-must-never-echo");
});
it("does not floor platform-marked rows on self-hosted instances", async () => {
delete process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN;
mockEnvironmentService.getById.mockResolvedValue(createPlatformSandboxEnvironment());
const app = createApp({
type: "board",
userId: "admin-1",
source: "session",
isInstanceAdmin: true,
});
const res = await request(app).get("/api/environments/env-managed-1");
expect(res.status).toBe(200);
expect(res.body.envVars).toEqual({ MY_AGENT_TOOL_SETTING: "tenant-env-value" });
expect(res.body.config.apiKey).toBe("config-credential-must-never-echo");
});
});
it("rejects non-admin blast-radius reads for instance-scoped environments", async () => {
const app = createApp({
type: "board",
userId: "user-2",
source: "session",
companyIds: ["company-1"],
memberships: [{ companyId: "company-1", status: "active", membershipRole: "member" }],
isInstanceAdmin: false,
});
const res = await request(app).get("/api/environments/env-1/delete-blast-radius");
expect(res.status).toBe(403);
expect(mockEnvironmentService.getDeleteBlastRadius).not.toHaveBeenCalled();
});
it("returns delete blast radius counts for instance admins", async () => {
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
agentDefaultCount: 2,
secretBindingCount: 3,
activeLeaseCount: 1,
}));
const app = createApp({
type: "board",
userId: "admin-1",
source: "session",
companyIds: ["company-1"],
isInstanceAdmin: true,
});
const res = await request(app).get("/api/environments/env-1/delete-blast-radius");
expect(res.status).toBe(200);
expect(res.body).toEqual({
environmentId: "env-1",
canDelete: true,
deleteBlockedReasons: [],
pendingCleanupLeaseCount: 0,
reusableSandboxLeaseCount: 0,
reusableSandboxLeaseHolders: [],
staticReferences: {
isManagedLocal: false,
isInstanceDefault: false,
agentDefaultCount: 2,
executionWorkspaceSelectionCount: 0,
issueSelectionCount: 0,
projectSelectionCount: 0,
secretBindingCount: 3,
},
activeRuntimeUse: {
activeLeaseCount: 1,
activeCustomImageSetupSessionCount: 0,
hasActiveRuntimeUse: true,
},
});
expect(res.body).not.toHaveProperty("config");
expect(res.body).not.toHaveProperty("envVars");
expect(res.body).not.toHaveProperty("metadata");
});
it("returns 404 for missing delete blast radius targets", async () => {
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(null);
const app = createApp({
type: "board",
userId: "admin-1",
source: "session",
companyIds: ["company-1"],
isInstanceAdmin: true,
});
const res = await request(app).get("/api/environments/missing/delete-blast-radius");
expect(res.status).toBe(404);
expect(res.body.error).toBe("Environment not found");
});
it("returns provider capabilities for the company", async () => {
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
expect(res.status).toBe(200);
expect(res.body.drivers.ssh).toBe("supported");
expect(res.body.sandboxProviders.fake.supportsRunExecution).toBe(false);
expect(res.body.sandboxProviders).not.toHaveProperty("fake-plugin");
});
it("returns installed plugin-backed sandbox capabilities for environment creation", async () => {
mockListReadyPluginEnvironmentDrivers.mockResolvedValue([
{
pluginId: "plugin-1",
pluginKey: "acme.secure-sandbox-provider",
driverKey: "secure-plugin",
displayName: "Secure Sandbox",
description: "Provisions schema-driven cloud sandboxes.",
supportsInteractiveSetup: true,
interactiveSetupConnectionTypes: ["ssh"],
supportsTemplateCapture: true,
templateRefKind: "snapshot",
templateConfigBinding: {
field: "template",
unsetFields: ["image"],
},
supportsTemplateDelete: true,
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
},
},
},
]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
expect(res.status).toBe(200);
expect(res.body.sandboxProviders["secure-plugin"]).toMatchObject({
status: "supported",
supportsRunExecution: true,
// The mock manifest omits reusable leases, so the capability defaults to
// false and agrees with the execution guard.
supportsReusableLeases: false,
supportsInteractiveSetup: true,
interactiveSetupConnectionTypes: ["ssh"],
supportsTemplateCapture: true,
templateRefKind: "snapshot",
templateConfigBinding: {
field: "template",
unsetFields: ["image"],
},
supportsTemplateDelete: true,
displayName: "Secure Sandbox",
source: "plugin",
pluginKey: "acme.secure-sandbox-provider",
pluginId: "plugin-1",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
},
},
});
expect(res.body.adapters.find((row: any) => row.adapterType === "codex_local").sandboxProviders["secure-plugin"])
.toBe("supported");
});
it("publishes reusable leases from the nested capability, not the legacy flag, so the API agrees with acquisition", async () => {
// The manifest sets the legacy flag `true` but the nested override `false`.
// Acquisition lets the nested value win and refuses reuse. The published
// API value must derive from the same declaration resolver and present the
// provider as not reusable, even when the worker verified both lifecycle
// methods.
mockListReadyPluginEnvironmentDrivers.mockResolvedValue([
{
pluginId: "plugin-1",
pluginKey: "acme.legacy-override-provider",
driverKey: "override-plugin",
displayName: "Override Sandbox",
supportsReusableLeases: true,
sandboxCapabilities: { reusableLeases: false },
reusableLeaseMethodsVerified: true,
configSchema: { type: "object", properties: {} },
},
]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
expect(res.status).toBe(200);
expect(res.body.sandboxProviders["override-plugin"].supportsReusableLeases).toBe(false);
});
it("publishes reusable leases from the legacy flag when the manifest omits the nested override and the worker verified both methods", async () => {
mockListReadyPluginEnvironmentDrivers.mockResolvedValue([
{
pluginId: "plugin-1",
pluginKey: "acme.legacy-only-provider",
driverKey: "legacy-plugin",
displayName: "Legacy Sandbox",
supportsReusableLeases: true,
reusableLeaseMethodsVerified: true,
configSchema: { type: "object", properties: {} },
},
]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
expect(res.status).toBe(200);
expect(res.body.sandboxProviders["legacy-plugin"].supportsReusableLeases).toBe(true);
});
it("does not publish reusable leases when the declaration allows them but the worker omits a lifecycle method", async () => {
// A positive declaration alone is not enough. Acquisition verifies both
// reuse lifecycle methods live and falls back to an ephemeral lease when one
// is missing. The published value must agree and present as not reusable.
mockListReadyPluginEnvironmentDrivers.mockResolvedValue([
{
pluginId: "plugin-1",
pluginKey: "acme.unverified-reuse-provider",
driverKey: "unverified-plugin",
displayName: "Unverified Reuse Sandbox",
sandboxCapabilities: { reusableLeases: true },
reusableLeaseMethodsVerified: false,
configSchema: { type: "object", properties: {} },
},
]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app).get("/api/companies/company-1/environments/capabilities");
expect(res.status).toBe(200);
expect(res.body.sandboxProviders["unverified-plugin"].supportsReusableLeases).toBe(false);
});
it("rejects agent list reads for instance-scoped environments", async () => {
mockEnvironmentService.list.mockResolvedValue([createEnvironment()]);
mockAgentService.getById.mockResolvedValue({
id: "agent-1",
companyId: "company-1",
role: "engineer",
permissions: { canCreateAgents: false },
});
mockAccessService.hasPermission.mockResolvedValue(false);
const app = createApp({
type: "agent",
agentId: "agent-1",
companyId: "company-1",
source: "agent_key",
runId: "run-1",
});
const res = await request(app).get("/api/companies/company-1/environments");
expect(res.status).toBe(403);
expect(res.body.error).toContain("Board access required");
expect(mockEnvironmentService.list).not.toHaveBeenCalled();
});
it("rejects agent detail reads for instance-scoped environments", async () => {
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
mockAgentService.getById.mockResolvedValue({
id: "agent-1",
companyId: "company-1",
role: "cto",
permissions: { canCreateAgents: true },
});
mockAccessService.hasPermission.mockResolvedValue(false);
const app = createApp({
type: "agent",
agentId: "agent-1",
companyId: "company-1",
source: "agent_key",
runId: "run-1",
});
const res = await request(app).get("/api/environments/env-1");
expect(res.status).toBe(403);
expect(res.body.error).toContain("Board access required");
});
it("creates an environment and logs activity", async () => {
const environment = createEnvironment();
mockEnvironmentService.create.mockResolvedValue(environment);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Local",
driver: "local",
description: "Current development machine",
config: { shell: "zsh" },
});
expect(res.status).toBe(201);
expect(mockEnvironmentService.create).toHaveBeenCalledWith({
name: "Local",
driver: "local",
description: "Current development machine",
status: "active",
config: { shell: "zsh" },
envVars: {},
}, undefined, { db: routeDbTx });
expect(mockLogActivity).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
companyId: "company-1",
actorType: "user",
actorId: "user-1",
agentId: null,
runId: null,
action: "environment.created",
entityType: "environment",
entityId: environment.id,
}),
);
});
it("returns conflict when creating a second local environment", async () => {
mockEnvironmentService.list.mockResolvedValue([createEnvironment()]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Another Local",
driver: "local",
config: {},
});
expect(res.status).toBe(409);
expect(res.body.error).toBe("A local environment already exists for this instance.");
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
});
it("rejects non-admin board users even when they have company environment permissions", async () => {
const environment = createEnvironment();
mockAccessService.canUser.mockResolvedValue(true);
mockEnvironmentService.create.mockResolvedValue(environment);
const app = createApp({
type: "board",
userId: "user-1",
source: "session",
companyIds: ["company-1"],
isInstanceAdmin: false,
});
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Local",
driver: "local",
config: {},
});
expect(res.status).toBe(403);
expect(res.body.error).toContain("Instance admin access required");
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
});
it("rejects non-admin board users without instance admin access", async () => {
mockAccessService.canUser.mockResolvedValue(false);
const app = createApp({
type: "board",
userId: "user-1",
source: "session",
companyIds: ["company-1"],
isInstanceAdmin: false,
});
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Local",
driver: "local",
config: {},
});
expect(res.status).toBe(403);
expect(res.body.error).toContain("Instance admin access required");
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
});
it("rejects agent environment creation even with explicit company grants", async () => {
const environment = createEnvironment();
mockAgentService.getById.mockResolvedValue({
id: "agent-1",
companyId: "company-1",
role: "engineer",
permissions: { canCreateAgents: false },
});
mockAccessService.hasPermission.mockResolvedValue(true);
mockEnvironmentService.create.mockResolvedValue(environment);
const app = createApp({
type: "agent",
agentId: "agent-1",
companyId: "company-1",
source: "agent_key",
runId: "run-1",
});
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Local",
driver: "local",
config: {},
});
expect(res.status).toBe(403);
expect(res.body.error).toContain("board operators");
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
});
it("rejects deleting the managed local environment", async () => {
const environment = createEnvironment();
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
isManagedLocal: true,
}));
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const res = await request(app).delete("/api/environments/env-1");
expect(res.status).toBe(409);
expect(res.body.error).toBe("Cannot delete the managed local environment.");
expect(res.body.details).toEqual({ deleteBlockedReasons: ["managed_local"] });
expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled();
expect(mockExecutionWorkspaceService.clearEnvironmentSelection).not.toHaveBeenCalled();
});
it("rejects deleting the current instance default environment", async () => {
const environment = {
...createEnvironment(),
driver: "ssh" as const,
name: "SSH Fixture",
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: null,
knownHosts: null,
strictHostKeyChecking: true,
},
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
isInstanceDefault: true,
}));
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const res = await request(app).delete("/api/environments/env-1");
expect(res.status).toBe(409);
expect(res.body.error).toBe(
"Cannot delete the current instance default environment. Set a new default environment before deleting this one.",
);
expect(res.body.details).toEqual({ deleteBlockedReasons: ["instance_default"] });
expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled();
});
it("rejects deleting an environment with a pending sandbox cleanup", async () => {
const environment = {
...createEnvironment(),
driver: "ssh" as const,
name: "SSH Fixture",
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: null,
knownHosts: null,
strictHostKeyChecking: true,
},
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
pendingCleanupLeaseCount: 1,
}));
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const res = await request(app).delete("/api/environments/env-1");
expect(res.status).toBe(409);
expect(res.body.error).toBe(
"Cannot delete this environment while a sandbox cleanup is pending. Wait for the cleanup sweep to destroy the orphan sandbox, then retry.",
);
expect(res.body.details).toEqual({ deleteBlockedReasons: ["pending_sandbox_cleanup"] });
expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled();
});
it("rejects deleting an environment with a live reusable sandbox lease", async () => {
const environment = {
...createEnvironment(),
driver: "sandbox" as const,
name: "Reusable Sandbox Fixture",
config: {
provider: "fake-plugin",
image: "fixture:test",
reuseLease: true,
},
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
activeLeaseCount: 1,
reusableSandboxLeaseCount: 1,
}));
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const res = await request(app).delete("/api/environments/env-1");
expect(res.status).toBe(409);
expect(res.body.error).toBe(
"Cannot delete this environment while it has a reusable sandbox lease. Remove the associated execution workspace or issue so Paperclip can destroy the sandbox, then retry.",
);
expect(res.body.details).toEqual({ deleteBlockedReasons: ["reusable_sandbox_lease"] });
expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled();
expect(mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment).not.toHaveBeenCalled();
});
it("destroys reusable sandbox leases and deletes with explicit consent", async () => {
const environment = createEnvironment();
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.getDeleteBlastRadius
.mockResolvedValueOnce(createDeleteBlastRadius({ reusableSandboxLeaseCount: 2 }))
.mockResolvedValueOnce(createDeleteBlastRadius());
mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment.mockResolvedValue({
destroyed: 2,
failed: 0,
skippedLiveRun: 0,
});
mockEnvironmentService.removeIfDeletable.mockResolvedValue(environment);
mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1"]);
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true");
expect(res.status).toBe(200);
expect(
mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment,
).toHaveBeenCalledExactlyOnceWith({
environmentId: "env-1",
failureReason: "environment_deleted",
});
expect(
mockCloseWarmNativeSessionsForEnvironment,
).toHaveBeenCalledExactlyOnceWith({
environmentId: "env-1",
reason: "environment deleted",
});
expect(mockEnvironmentService.removeIfDeletable).toHaveBeenCalledWith(
"env-1",
);
expect(res.body.destroyedReusableSandboxLeaseCount).toBe(2);
});
it("keeps rejecting a consented delete when a non-lease blocker remains", async () => {
const environment = createEnvironment();
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({
isInstanceDefault: true,
reusableSandboxLeaseCount: 1,
}));
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true");
// Destroying provider sandboxes and then rejecting on the other gate would
// be an irreversible action with nothing gained, so the destroy must not run.
expect(res.status).toBe(409);
expect(mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment).not.toHaveBeenCalled();
expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled();
});
it("keeps rejecting when leases survive the consented destroy", async () => {
const environment = createEnvironment();
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.getDeleteBlastRadius
.mockResolvedValueOnce(createDeleteBlastRadius({ reusableSandboxLeaseCount: 1 }))
.mockResolvedValueOnce(createDeleteBlastRadius({ pendingCleanupLeaseCount: 1 }));
mockEnvironmentRuntimeService.destroyReusableSandboxLeasesForEnvironment.mockResolvedValue({
destroyed: 1,
failed: 1,
skippedLiveRun: 0,
});
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const res = await request(app).delete("/api/environments/env-1?destroyReusableSandboxLeases=true");
expect(res.status).toBe(409);
// The rejection names what the consented destroy already did: provider
// destruction is not transactional with the delete guard.
expect(res.body.details).toEqual({
deleteBlockedReasons: ["pending_sandbox_cleanup"],
destroyedReusableSandboxLeaseCount: 1,
});
expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled();
});
it("rejects a driver or provider config change while a sandbox cleanup is pending", async () => {
const environment = {
...createEnvironment(),
id: "env-ssh",
driver: "ssh" as const,
name: "SSH Fixture",
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: null,
knownHosts: null,
strictHostKeyChecking: true,
},
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(true);
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const res = await request(app)
.patch("/api/environments/env-ssh")
.send({
config: {
host: "changed.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
},
});
expect(res.status).toBe(409);
expect(res.body.error).toBe(
"Cannot change the driver or provider config while a sandbox cleanup is pending. Wait for the cleanup sweep to destroy the orphan sandbox, then retry.",
);
expect(res.body.details).toEqual({ code: "environment_pending_sandbox_cleanup" });
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
});
it("allows a non-provider update while a sandbox cleanup is pending", async () => {
const environment = {
...createEnvironment(),
id: "env-ssh",
driver: "ssh" as const,
name: "SSH Fixture",
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: null,
knownHosts: null,
strictHostKeyChecking: true,
},
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(true);
mockEnvironmentService.update.mockResolvedValue({ ...environment, description: "Updated" });
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const res = await request(app)
.patch("/api/environments/env-ssh")
.send({ description: "Updated" });
expect(res.status).toBe(200);
expect(mockEnvironmentService.hasUnresolvedPendingCleanupLeases).not.toHaveBeenCalled();
expect(mockEnvironmentService.update).toHaveBeenCalled();
});
it("describes an environment's secret refs with owner metadata", async () => {
const secretId = "22222222-2222-2222-2222-222222222222";
mockEnvironmentService.getById.mockResolvedValue({
...createEnvironment(),
id: "env-sandbox",
name: "Daytona",
driver: "sandbox" as const,
config: {
provider: "secure-plugin",
template: "base",
apiKey: secretId,
timeoutMs: 450000,
reuseLease: true,
},
});
mockSecretService.describeSecretRefs.mockResolvedValue([
{
configPath: "apiKey",
secretId,
name: "DAYTONA_API_KEY",
status: "active",
companyId: "company-2",
companyName: "Other Team",
},
]);
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const res = await request(app).get("/api/environments/env-sandbox/secret-refs");
expect(res.status).toBe(200);
expect(res.body.refs).toEqual([
{
configPath: "apiKey",
secretId,
name: "DAYTONA_API_KEY",
status: "active",
companyId: "company-2",
companyName: "Other Team",
},
]);
expect(mockSecretService.describeSecretRefs).toHaveBeenCalledWith([
{ secretId, configPath: "apiKey", versionSelector: "latest" },
]);
});
it("denies secret-ref descriptors to agents without instance environment access", async () => {
mockAgentService.getById.mockResolvedValue({
id: "agent-1",
companyId: "company-1",
role: "engineer",
permissions: { canCreateAgents: false },
});
mockAccessService.hasPermission.mockResolvedValue(false);
const app = createApp({
type: "agent",
agentId: "agent-1",
companyId: "company-1",
source: "agent_key",
});
const res = await request(app).get("/api/environments/env-1/secret-refs");
expect(res.status).toBe(403);
expect(mockSecretService.describeSecretRefs).not.toHaveBeenCalled();
});
it("clears environment selections and secret bindings across all companies when deleting an environment", async () => {
const environment = {
...createEnvironment(),
id: "env-ssh",
name: "SSH Fixture",
driver: "ssh" as const,
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: {
type: "secret_ref",
secretId: "11111111-1111-1111-1111-111111111111",
version: "latest",
},
knownHosts: null,
strictHostKeyChecking: true,
},
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius());
mockEnvironmentService.removeIfDeletable.mockResolvedValue(environment);
mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1", "company-2"]);
const app = createApp({
type: "board",
userId: "admin-1",
source: "local_implicit",
});
const res = await request(app).delete("/api/environments/env-ssh");
expect(res.status).toBe(200);
expect(mockEnvironmentService.removeIfDeletable).toHaveBeenCalledWith("env-ssh");
for (const companyId of ["company-1", "company-2"]) {
expect(mockExecutionWorkspaceService.clearEnvironmentSelection)
.toHaveBeenCalledWith(companyId, "env-ssh");
expect(mockIssueService.clearExecutionWorkspaceEnvironmentSelection)
.toHaveBeenCalledWith(companyId, "env-ssh");
expect(mockProjectService.clearExecutionWorkspaceEnvironmentSelection)
.toHaveBeenCalledWith(companyId, "env-ssh");
expect(mockSecretService.syncEnvBindingsForTarget).toHaveBeenCalledWith(
companyId,
{ targetType: "environment", targetId: "env-ssh" },
{},
);
expect(mockSecretService.syncSecretRefsForTarget).toHaveBeenCalledWith(
companyId,
{ targetType: "environment", targetId: "env-ssh" },
[],
{ replaceAll: true },
);
}
expect(mockSecretService.remove).toHaveBeenCalledWith("11111111-1111-1111-1111-111111111111");
expect(mockLogActivity).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
companyId: "company-1",
action: "environment.deleted",
entityType: "environment",
entityId: "env-ssh",
}),
);
});
it("rejects invalid SSH config on create", async () => {
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "SSH Fixture",
driver: "ssh",
config: {
host: "ssh.example.test",
username: "ssh-user",
},
});
expect(res.status).toBe(422);
expect(res.body.error).toContain("remote workspace path");
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
});
it("normalizes SSH private keys into secret refs before persistence", async () => {
const environment = {
...createEnvironment(),
id: "env-ssh",
name: "SSH Fixture",
driver: "ssh" as const,
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: {
type: "secret_ref",
secretId: "11111111-1111-1111-1111-111111111111",
version: "latest",
},
knownHosts: null,
strictHostKeyChecking: true,
},
};
mockEnvironmentService.create.mockResolvedValue(environment);
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "SSH Fixture",
driver: "ssh",
config: {
host: "ssh.example.test",
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: " super-secret-key ",
},
});
expect(res.status).toBe(201);
expect(mockEnvironmentService.create).toHaveBeenCalledWith(expect.objectContaining({
config: expect.objectContaining({
privateKey: null,
privateKeySecretRef: {
type: "secret_ref",
secretId: "11111111-1111-1111-1111-111111111111",
version: "latest",
},
}),
envVars: {},
}), undefined, { db: routeDbTx });
expect(JSON.stringify(mockEnvironmentService.create.mock.calls[0][0])).not.toContain("super-secret-key");
expect(mockSecretService.create).toHaveBeenCalledWith(
"company-1",
expect.objectContaining({
provider: "local_encrypted",
value: "super-secret-key",
}),
expect.any(Object),
);
});
it("uses the configured provider for SSH private key secret materialization", async () => {
process.env.PAPERCLIP_SECRETS_PROVIDER = "aws_secrets_manager";
const environment = {
...createEnvironment(),
id: "env-ssh",
name: "SSH Fixture",
driver: "ssh" as const,
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: {
type: "secret_ref",
secretId: "11111111-1111-1111-1111-111111111111",
version: "latest",
},
knownHosts: null,
strictHostKeyChecking: true,
},
};
mockEnvironmentService.create.mockResolvedValue(environment);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "SSH Fixture",
driver: "ssh",
config: {
host: "ssh.example.test",
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: "super-secret-key",
},
});
expect(res.status).toBe(201);
expect(mockSecretService.create).toHaveBeenCalledWith(
"company-1",
expect.objectContaining({
provider: "aws_secrets_manager",
value: "super-secret-key",
}),
expect.any(Object),
);
});
it("rejects persisted fake sandbox environments", async () => {
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Fake Sandbox",
driver: "sandbox",
config: {
provider: "fake",
image: " ubuntu:24.04 ",
},
});
expect(res.status).toBe(422);
expect(res.body.error).toContain("reserved for internal probes");
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
});
it("creates a sandbox environment with normalized Fake plugin config", async () => {
const environment = {
...createEnvironment(),
id: "env-sandbox-fake-plugin",
name: "Fake plugin Sandbox",
driver: "sandbox" as const,
config: {
provider: "fake-plugin",
image: "fake:test",
timeoutMs: 450000,
reuseLease: true,
},
};
mockEnvironmentService.create.mockResolvedValue(environment);
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Fake plugin Sandbox",
driver: "sandbox",
config: {
provider: "fake-plugin",
image: "fake:test",
timeoutMs: "450000",
reuseLease: true,
},
});
expect(res.status).toBe(201);
expect(mockValidatePluginSandboxProviderConfig).toHaveBeenCalledWith({
db: expect.anything(),
workerManager: pluginWorkerManager,
provider: "fake-plugin",
config: {
image: "fake:test",
timeoutMs: 450000,
reuseLease: true,
},
});
expect(mockEnvironmentService.create).toHaveBeenCalledWith({
name: "Fake plugin Sandbox",
driver: "sandbox",
status: "active",
config: {
provider: "fake-plugin",
image: "fake:test",
timeoutMs: 450000,
reuseLease: true,
},
envVars: {},
}, undefined, { db: routeDbTx });
expect(mockSecretService.create).not.toHaveBeenCalled();
});
it("keeps host-owned sandbox flags and drops a removed flag a saved config still carries", async () => {
// The host owns run-log streaming and runner lifecycle. A provider plugin
// may normalize only its own driver fields, so it drops these host flags.
// The host must re-apply them or a saved warm environment silently becomes
// per-turn at execution time.
//
// `streamAgentSessionOutput` is a removed operator flag. A saved config can
// still carry it, but session-output streaming now follows the capability
// snapshot alone. The removed key must load and then drop, so it never
// reaches the persisted config.
const environment = {
...createEnvironment(),
id: "env-sandbox-fake-plugin",
name: "Streamed Sandbox",
driver: "sandbox" as const,
config: { provider: "fake-plugin", image: "fake:test" },
};
mockEnvironmentService.create.mockResolvedValue(environment);
mockValidatePluginSandboxProviderConfig.mockImplementation(
async ({ provider, config }) => {
// Drop the host flag to reproduce a plugin that allowlists driver fields.
const {
streamRunLogs,
runnerLifecycleMode,
runnerIdleTimeoutMs,
...driverConfig
} = config as Record<string, unknown>;
void streamRunLogs;
void runnerLifecycleMode;
void runnerIdleTimeoutMs;
return {
normalizedConfig: driverConfig,
pluginId: `plugin-${provider}`,
pluginKey: `plugin.${provider}`,
driver: {
driverKey: provider,
kind: "sandbox_provider",
displayName: provider,
configSchema: { type: "object" },
},
};
},
);
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Streamed Sandbox",
driver: "sandbox",
config: {
provider: "fake-plugin",
image: "fake:test",
streamRunLogs: false,
runnerLifecycleMode: "warm",
runnerIdleTimeoutMs: 180_000,
streamAgentSessionOutput: true,
},
});
expect(res.status).toBe(201);
const persisted = mockEnvironmentService.create.mock.calls[0][0].config as Record<string, unknown>;
// The removed key never reaches the persisted config.
expect(persisted.streamAgentSessionOutput).toBeUndefined();
expect(persisted.streamRunLogs).toBe(false);
expect(persisted.runnerLifecycleMode).toBe("warm");
expect(persisted.runnerIdleTimeoutMs).toBe(180_000);
});
it("creates a schema-driven sandbox environment with secret-ref fields persisted as secrets", async () => {
const environment = {
...createEnvironment(),
id: "env-sandbox-secure-plugin",
name: "Secure Sandbox",
driver: "sandbox" as const,
config: {
provider: "secure-plugin",
template: "base",
apiKey: "11111111-1111-1111-1111-111111111111",
timeoutMs: 450000,
reuseLease: true,
},
};
mockEnvironmentService.create.mockResolvedValue(environment);
mockValidatePluginSandboxProviderConfig.mockResolvedValue({
normalizedConfig: {
template: "base",
apiKey: "test-provider-key",
timeoutMs: 450000,
reuseLease: true,
},
pluginId: "plugin-secure",
pluginKey: "acme.secure-sandbox-provider",
driver: {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
});
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Secure Sandbox",
driver: "sandbox",
config: {
provider: "secure-plugin",
template: " base ",
apiKey: " test-provider-key ",
timeoutMs: "450000",
reuseLease: true,
},
});
expect(res.status).toBe(201);
expect(mockValidatePluginSandboxProviderConfig).toHaveBeenCalledWith({
db: expect.anything(),
workerManager: pluginWorkerManager,
provider: "secure-plugin",
config: {
template: " base ",
apiKey: " test-provider-key ",
timeoutMs: 450000,
reuseLease: true,
},
});
expect(mockEnvironmentService.create).toHaveBeenCalledWith({
name: "Secure Sandbox",
driver: "sandbox",
status: "active",
config: {
provider: "secure-plugin",
template: "base",
apiKey: "11111111-1111-1111-1111-111111111111",
timeoutMs: 450000,
reuseLease: true,
},
envVars: {},
}, undefined, { db: routeDbTx });
expect(JSON.stringify(mockEnvironmentService.create.mock.calls[0][0])).not.toContain("test-provider-key");
expect(mockSecretService.create).toHaveBeenCalledWith(
"company-1",
expect.objectContaining({
provider: "local_encrypted",
value: "test-provider-key",
}),
expect.any(Object),
);
});
it("persists a picker-submitted secret_ref binding object as the bare secret id", async () => {
const secretId = "11111111-1111-1111-1111-111111111111";
const environment = {
...createEnvironment(),
id: "env-sandbox-secure-plugin",
name: "Secure Sandbox",
driver: "sandbox" as const,
config: {
provider: "secure-plugin",
template: "base",
apiKey: secretId,
timeoutMs: 450000,
reuseLease: true,
},
};
mockEnvironmentService.create.mockResolvedValue(environment);
mockValidatePluginSandboxProviderConfig.mockImplementation(async ({ config }: { config: Record<string, unknown> }) => ({
normalizedConfig: { ...config },
pluginId: "plugin-secure",
pluginKey: "acme.secure-sandbox-provider",
driver: {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
}));
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Secure Sandbox",
driver: "sandbox",
config: {
provider: "secure-plugin",
template: "base",
apiKey: { type: "secret_ref", secretId, version: "latest" },
timeoutMs: 450000,
reuseLease: true,
},
});
expect(res.status).toBe(201);
expect(mockEnvironmentService.create).toHaveBeenCalledWith({
name: "Secure Sandbox",
driver: "sandbox",
status: "active",
config: {
provider: "secure-plugin",
template: "base",
apiKey: secretId,
timeoutMs: 450000,
reuseLease: true,
},
envVars: {},
}, undefined, { db: routeDbTx });
expect(mockSecretService.create).not.toHaveBeenCalled();
expect(mockSecretService.replaceSecretRefsForInstanceTarget).toHaveBeenCalledWith(
{ targetType: "environment", targetId: "env-sandbox-secure-plugin" },
[{ secretId, configPath: "apiKey", versionSelector: "latest" }],
{ db: routeDbTx },
);
});
it("uses the configured provider for schema-driven sandbox secret fields", async () => {
process.env.PAPERCLIP_SECRETS_PROVIDER = "aws_secrets_manager";
const environment = {
...createEnvironment(),
id: "env-sandbox-secure-plugin",
name: "Secure Sandbox",
driver: "sandbox" as const,
config: {
provider: "secure-plugin",
template: "base",
apiKey: "11111111-1111-1111-1111-111111111111",
timeoutMs: 450000,
reuseLease: true,
},
};
mockEnvironmentService.create.mockResolvedValue(environment);
mockValidatePluginSandboxProviderConfig.mockResolvedValue({
normalizedConfig: {
template: "base",
apiKey: "test-provider-key",
timeoutMs: 450000,
reuseLease: true,
},
pluginId: "plugin-secure",
pluginKey: "acme.secure-sandbox-provider",
driver: {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
});
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Secure Sandbox",
driver: "sandbox",
config: {
provider: "secure-plugin",
template: "base",
apiKey: "test-provider-key",
timeoutMs: "450000",
reuseLease: true,
},
});
expect(res.status).toBe(201);
expect(mockSecretService.create).toHaveBeenCalledWith(
"company-1",
expect.objectContaining({
provider: "aws_secrets_manager",
value: "test-provider-key",
}),
expect.any(Object),
);
});
it("validates plugin environment config through the plugin driver host", async () => {
const environment = {
...createEnvironment(),
id: "env-plugin",
name: "Plugin Sandbox",
driver: "plugin" as const,
config: {
pluginKey: "acme.environments",
driverKey: "sandbox",
driverConfig: {
template: "normalized",
},
},
};
mockValidatePluginEnvironmentDriverConfig.mockResolvedValue(environment.config);
mockEnvironmentService.create.mockResolvedValue(environment);
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Plugin Sandbox",
driver: "plugin",
config: {
pluginKey: "acme.environments",
driverKey: "sandbox",
driverConfig: {
template: "base",
},
},
});
expect(res.status).toBe(201);
expect(mockValidatePluginEnvironmentDriverConfig).toHaveBeenCalledWith({
db: expect.anything(),
workerManager: pluginWorkerManager,
config: {
pluginKey: "acme.environments",
driverKey: "sandbox",
driverConfig: {
template: "base",
},
},
});
expect(mockEnvironmentService.create).toHaveBeenCalledWith(expect.objectContaining({
config: environment.config,
envVars: {},
}), undefined, { db: routeDbTx });
});
it("rejects agent mutations for instance-scoped environments", async () => {
mockAgentService.getById.mockResolvedValue({
id: "agent-1",
companyId: "company-1",
role: "engineer",
permissions: { canCreateAgents: false },
});
mockAccessService.hasPermission.mockResolvedValue(false);
const app = createApp({
type: "agent",
agentId: "agent-1",
companyId: "company-1",
source: "agent_key",
runId: "run-1",
});
const res = await request(app)
.post("/api/companies/company-1/environments")
.send({
name: "Sandbox host",
driver: "local",
});
expect(res.status).toBe(403);
expect(res.body.error).toContain("board operators");
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
});
it("lists leases for an environment after company access is confirmed", async () => {
const environment = createEnvironment();
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.listLeases.mockResolvedValue([
{
id: "lease-1",
companyId: "company-1",
environmentId: environment.id,
executionWorkspaceId: "workspace-1",
issueId: null,
heartbeatRunId: null,
status: "active",
providerLeaseId: "provider-lease-1",
acquiredAt: new Date("2026-04-16T05:00:00.000Z"),
lastUsedAt: new Date("2026-04-16T05:05:00.000Z"),
expiresAt: null,
releasedAt: null,
metadata: { provider: "fake" },
createdAt: new Date("2026-04-16T05:00:00.000Z"),
updatedAt: new Date("2026-04-16T05:05:00.000Z"),
},
]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app).get(`/api/environments/${environment.id}/leases?status=active`);
expect(res.status).toBe(200);
expect(mockEnvironmentService.listLeases).toHaveBeenCalledWith(environment.id, {
status: "active",
});
});
it("returns a single lease after company access is confirmed", async () => {
mockEnvironmentService.getLeaseById.mockResolvedValue({
id: "lease-1",
companyId: "company-1",
environmentId: "env-1",
executionWorkspaceId: "workspace-1",
issueId: null,
heartbeatRunId: "run-1",
status: "active",
leasePolicy: "ephemeral",
provider: "ssh",
providerLeaseId: "ssh://ssh-user@example.test:22/workspace",
acquiredAt: new Date("2026-04-16T05:00:00.000Z"),
lastUsedAt: new Date("2026-04-16T05:05:00.000Z"),
expiresAt: null,
releasedAt: null,
failureReason: null,
cleanupStatus: null,
metadata: { remoteCwd: "/workspace" },
createdAt: new Date("2026-04-16T05:00:00.000Z"),
updatedAt: new Date("2026-04-16T05:05:00.000Z"),
});
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app).get("/api/environment-leases/lease-1");
expect(res.status).toBe(200);
expect(res.body.provider).toBe("ssh");
expect(mockEnvironmentService.getLeaseById).toHaveBeenCalledWith("lease-1");
});
it("rejects agent access regardless of company when environment management is instance-scoped", async () => {
mockEnvironmentService.list.mockResolvedValue([]);
const app = createApp({
type: "agent",
agentId: "agent-2",
companyId: "company-2",
source: "agent_key",
runId: "run-2",
});
const res = await request(app).get("/api/companies/company-1/environments");
expect(res.status).toBe(403);
expect(res.body.error).toContain("Board access required");
expect(mockEnvironmentService.list).not.toHaveBeenCalled();
});
it("logs a redacted update summary instead of raw config or metadata", async () => {
const environment = createEnvironment();
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.update.mockResolvedValue({
...environment,
status: "archived",
});
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app)
.patch(`/api/environments/${environment.id}?companyId=company-1`)
.send({
status: "archived",
config: {
apiKey: "super-secret",
token: "another-secret",
},
metadata: {
password: "do-not-log",
},
});
expect(res.status).toBe(200);
expect(mockLogActivity).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
action: "environment.updated",
details: {
changedFields: ["config", "metadata", "status"],
status: "archived",
configChanged: true,
configTopLevelKeyCount: expect.any(Number),
metadataChanged: true,
metadataTopLevelKeyCount: 1,
},
}),
);
expect(JSON.stringify(mockLogActivity.mock.calls[0][1].details)).not.toContain("super-secret");
expect(JSON.stringify(mockLogActivity.mock.calls[0][1].details)).not.toContain("do-not-log");
});
it("resets config instead of inheriting SSH secrets when switching to local without an explicit config", async () => {
const environment = {
...createEnvironment(),
name: "SSH Fixture",
driver: "ssh" as const,
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: "super-secret-key",
knownHosts: "known-host",
strictHostKeyChecking: true,
},
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockEnvironmentService.update.mockResolvedValue({
...createEnvironment(),
driver: "local" as const,
config: {},
});
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app)
.patch(`/api/environments/${environment.id}?companyId=company-1`)
.send({
driver: "local",
});
expect(res.status).toBe(200);
expect(mockEnvironmentService.update).toHaveBeenCalledWith(environment.id, {
driver: "local",
config: {},
}, { db: routeDbTx });
expect(JSON.stringify(mockEnvironmentService.update.mock.calls[0][1])).not.toContain("super-secret-key");
expect(JSON.stringify(mockEnvironmentService.update.mock.calls[0][1])).not.toContain("known-host");
});
it("re-points a sandbox secret ref to another company's secret even when existing bindings disagree", async () => {
const oldSecretId = "11111111-1111-1111-1111-111111111111";
const newSecretId = "22222222-2222-2222-2222-222222222222";
const existing = {
...createEnvironment(),
id: "env-sandbox",
name: "Daytona",
driver: "sandbox" as const,
config: {
provider: "secure-plugin",
template: "base",
apiKey: oldSecretId,
timeoutMs: 450000,
reuseLease: true,
},
};
const updated = {
...existing,
config: { ...existing.config, apiKey: newSecretId },
};
mockEnvironmentService.getById.mockResolvedValue(existing);
mockEnvironmentService.update.mockResolvedValue(updated);
mockValidatePluginSandboxProviderConfig.mockImplementation(async ({ config }: { config: Record<string, unknown> }) => ({
normalizedConfig: { ...config },
pluginId: "plugin-secure",
pluginKey: "acme.secure-sandbox-provider",
driver: {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
}));
// The environment's only binding still lives in another company. Before
// bindings moved with the referenced secret, this state made every save
// from the caller's company fail, with no route-level way out.
mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue(["company-old"]);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager: {} });
const res = await request(app)
.patch("/api/environments/env-sandbox?companyId=company-new")
.send({
config: {
apiKey: { type: "secret_ref", secretId: newSecretId, version: "latest" },
},
});
expect(res.status).toBe(200);
expect(mockEnvironmentService.update).toHaveBeenCalledWith(
"env-sandbox",
expect.objectContaining({
config: expect.objectContaining({ apiKey: newSecretId }),
}),
{ db: routeDbTx },
);
expect(mockSecretService.replaceSecretRefsForInstanceTarget).toHaveBeenCalledWith(
{ targetType: "environment", targetId: "env-sandbox" },
[{ secretId: newSecretId, configPath: "apiKey", versionSelector: "latest" }],
{ db: routeDbTx },
);
// Explicit caller context wins outright; stale bindings are never consulted.
expect(mockSecretService.listBindingCompanyIdsForTarget).not.toHaveBeenCalled();
expect(mockSecretService.syncSecretRefsForTarget).not.toHaveBeenCalled();
});
it("fails the whole save when a referenced secret cannot be bound", async () => {
const existing = {
...createEnvironment(),
id: "env-sandbox",
name: "Daytona",
driver: "sandbox" as const,
config: {
provider: "secure-plugin",
template: "base",
apiKey: "11111111-1111-1111-1111-111111111111",
timeoutMs: 450000,
reuseLease: true,
},
};
mockEnvironmentService.getById.mockResolvedValue(existing);
mockEnvironmentService.update.mockResolvedValue({
...existing,
config: { ...existing.config, apiKey: "33333333-3333-3333-3333-333333333333" },
});
mockSecretService.replaceSecretRefsForInstanceTarget.mockRejectedValue(
unprocessable("Secret referenced at apiKey was not found", { code: "secret_missing" }),
);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
}, { pluginWorkerManager: {} });
const res = await request(app)
.patch("/api/environments/env-sandbox?companyId=company-1")
.send({
config: {
apiKey: { type: "secret_ref", secretId: "33333333-3333-3333-3333-333333333333", version: "latest" },
},
});
expect(res.status).toBe(422);
expect(res.body.error).toContain("was not found");
});
it("requires explicit SSH config when switching from local to SSH", async () => {
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app)
.patch("/api/environments/env-1?companyId=company-1")
.send({
driver: "ssh",
});
expect(res.status).toBe(422);
expect(res.body.error).toContain("host");
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
});
it("rejects switching an environment to the built-in fake sandbox provider", async () => {
mockEnvironmentService.getById.mockResolvedValue(createEnvironment());
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app)
.patch("/api/environments/env-1?companyId=company-1")
.send({
driver: "sandbox",
config: {
provider: "fake",
image: "ubuntu:24.04",
},
});
expect(res.status).toBe(422);
expect(res.body.error).toContain("reserved for internal probes");
expect(mockEnvironmentService.update).not.toHaveBeenCalled();
});
it("returns 404 when patching a missing environment", async () => {
mockEnvironmentService.getById.mockResolvedValue(null);
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
});
const res = await request(app)
.patch("/api/environments/missing-env")
.send({ status: "archived" });
expect(res.status).toBe(404);
expect(res.body.error).toBe("Environment not found");
expect(mockLogActivity).not.toHaveBeenCalled();
});
it("probes an SSH environment and logs the result", async () => {
const environment = {
...createEnvironment(),
name: "SSH Fixture",
driver: "ssh" as const,
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
knownHosts: null,
strictHostKeyChecking: true,
},
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockProbeEnvironment.mockResolvedValue({
ok: true,
driver: "ssh",
summary: "Connected to ssh-user@ssh.example.test and verified the remote workspace path.",
details: {
host: "ssh.example.test",
},
});
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
runId: "run-1",
});
const res = await request(app)
.post(`/api/environments/${environment.id}/probe`)
.send({});
expect(res.status).toBe(200);
expect(res.body.ok).toBe(true);
expect(mockProbeEnvironment).toHaveBeenCalledWith(expect.anything(), environment, {
// The instance has exactly one company, so the secret-context fallback
// resolves it even though the actor carries no memberships.
companyId: "company-1",
pluginWorkerManager: undefined,
applyCustomImageTemplate: false,
acquireSandboxRuntimeLease: false,
});
expect(mockLogActivity).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
companyId: "company-1",
action: "environment.probed",
entityType: "environment",
entityId: environment.id,
details: expect.objectContaining({
driver: "ssh",
ok: true,
}),
}),
);
});
it("requires explicit companyId when probing a secret-backed environment without inferable secret context", async () => {
const environment = {
...createEnvironment(),
driver: "ssh" as const,
config: {
host: "ssh.example.test",
port: 22,
username: "ssh-user",
remoteWorkspacePath: "/srv/paperclip/workspace",
privateKey: null,
privateKeySecretRef: {
type: "secret_ref",
secretId: "11111111-1111-4111-8111-111111111111",
version: "latest",
},
knownHosts: null,
strictHostKeyChecking: true,
},
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockSecretService.listBindingCompanyIdsForTarget.mockResolvedValue([]);
// A multi-company instance keeps the context genuinely ambiguous — a
// single-company instance would resolve via the instance fallback.
mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1", "company-2"]);
const app = createApp({
type: "board",
userId: "user-1",
source: "session",
companyIds: ["company-1", "company-2"],
memberships: [
{ companyId: "company-1", status: "active", membershipRole: "member" },
{ companyId: "company-2", status: "active", membershipRole: "member" },
],
isInstanceAdmin: true,
runId: "run-1",
});
const res = await request(app)
.post(`/api/environments/${environment.id}/probe`)
.send({});
expect(res.status).toBe(422);
expect(res.body.error).toContain("explicit companyId");
expect(mockProbeEnvironment).not.toHaveBeenCalled();
});
it("probes a sandbox environment and logs the result", async () => {
const environment = {
...createEnvironment(),
id: "env-sandbox",
name: "Fake Sandbox",
driver: "sandbox" as const,
config: {
provider: "fake",
image: "ubuntu:24.04",
reuseLease: true,
},
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockProbeEnvironment.mockResolvedValue({
ok: true,
driver: "sandbox",
summary: "Fake sandbox provider is ready for image ubuntu:24.04.",
details: {
provider: "fake",
image: "ubuntu:24.04",
reuseLease: true,
},
});
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
runId: "run-1",
});
const res = await request(app)
.post(`/api/environments/${environment.id}/probe`)
.send({});
expect(res.status).toBe(200);
expect(res.body.driver).toBe("sandbox");
expect(mockProbeEnvironment).toHaveBeenCalledWith(expect.anything(), environment, {
companyId: "company-1",
pluginWorkerManager: undefined,
applyCustomImageTemplate: true,
acquireSandboxRuntimeLease: true,
});
expect(mockLogActivity).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
companyId: "company-1",
action: "environment.probed",
entityType: "environment",
entityId: environment.id,
details: expect.objectContaining({
driver: "sandbox",
ok: true,
}),
}),
);
});
it("probes saved sandbox environments with the active custom image template without company context", async () => {
const environment = {
...createEnvironment(),
id: "env-sandbox",
name: "Daytona Sandbox",
driver: "sandbox" as const,
config: {
provider: "daytona",
image: "ubuntu:24.04",
reuseLease: true,
},
};
mockEnvironmentService.getById.mockResolvedValue(environment);
mockProbeEnvironment.mockResolvedValue({
ok: true,
driver: "sandbox",
summary: "Connected to Daytona sandbox.",
details: {
provider: "daytona",
snapshot: "captured-template",
},
});
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
runId: "run-1",
});
const res = await request(app)
.post(`/api/environments/${environment.id}/probe`)
.send({});
expect(res.status).toBe(200);
expect(res.body.driver).toBe("sandbox");
expect(mockProbeEnvironment).toHaveBeenCalledWith(expect.anything(), environment, {
companyId: "company-1",
pluginWorkerManager: undefined,
applyCustomImageTemplate: true,
acquireSandboxRuntimeLease: true,
});
});
it("probes unsaved provider config without persisting secrets", async () => {
mockProbeEnvironment.mockResolvedValue({
ok: true,
driver: "sandbox",
summary: "Fake plugin sandbox provider is ready.",
details: { provider: "fake-plugin" },
});
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
runId: "run-1",
}, { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments/probe-config")
.send({
name: "Draft Fake plugin",
driver: "sandbox",
config: {
provider: "fake-plugin",
template: "base",
apiKey: "unsaved-test-key",
timeoutMs: 300000,
reuseLease: true,
},
});
expect(res.status).toBe(200);
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
expect(mockSecretService.create).not.toHaveBeenCalled();
expect(mockProbeEnvironment).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
id: "unsaved",
driver: "sandbox",
config: expect.objectContaining({
apiKey: "unsaved-test-key",
}),
}),
expect.objectContaining({
pluginWorkerManager,
resolvedConfig: expect.objectContaining({
driver: "sandbox",
}),
}),
);
expect(JSON.stringify(mockLogActivity.mock.calls[0][1].details)).not.toContain("unsaved-test-key");
});
it("resolves selected secret refs before probing unsaved provider config", async () => {
mockValidatePluginSandboxProviderConfig.mockResolvedValue({
normalizedConfig: {
template: "base",
apiKey: "11111111-1111-1111-1111-111111111111",
timeoutMs: 300000,
reuseLease: true,
},
pluginId: "plugin-secure",
pluginKey: "acme.secure-sandbox-provider",
driver: {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
timeoutMs: { type: "number" },
reuseLease: { type: "boolean" },
},
},
},
});
mockProbeEnvironment.mockResolvedValue({
ok: true,
driver: "sandbox",
summary: "Secure sandbox provider is ready.",
details: { provider: "secure-plugin" },
});
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-1",
source: "local_implicit",
runId: "run-1",
}, { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments/probe-config")
.send({
name: "Draft Secure Sandbox",
driver: "sandbox",
config: {
provider: "secure-plugin",
template: "base",
apiKey: "11111111-1111-1111-1111-111111111111",
timeoutMs: 300000,
reuseLease: true,
},
});
expect(res.status).toBe(200);
expect(mockEnvironmentService.create).not.toHaveBeenCalled();
expect(mockSecretService.create).not.toHaveBeenCalled();
expect(mockSecretService.resolveSecretValueForEphemeralAccess).toHaveBeenCalledWith(
"company-1",
"11111111-1111-1111-1111-111111111111",
"latest",
{
consumerType: "system",
consumerId: "environment-probe-config",
configPath: "apiKey",
actorType: "user",
actorId: "user-1",
actorSource: "local_implicit",
heartbeatRunId: "run-1",
},
);
expect(mockProbeEnvironment).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
id: "unsaved",
driver: "sandbox",
config: expect.objectContaining({
apiKey: "resolved-provider-key",
}),
}),
expect.objectContaining({
pluginWorkerManager,
resolvedConfig: expect.objectContaining({
driver: "sandbox",
config: expect.objectContaining({
apiKey: "resolved-provider-key",
}),
}),
}),
);
expect(JSON.stringify(mockLogActivity.mock.calls[0][1].details)).not.toContain("resolved-provider-key");
});
it("rejects sandbox draft probes for non-admin board users", async () => {
mockValidatePluginSandboxProviderConfig.mockResolvedValue({
normalizedConfig: {
template: "base",
apiKey: "11111111-1111-1111-1111-111111111111",
},
pluginId: "plugin-secure",
pluginKey: "acme.secure-sandbox-provider",
driver: {
driverKey: "secure-plugin",
kind: "sandbox_provider",
displayName: "Secure Sandbox",
configSchema: {
type: "object",
properties: {
template: { type: "string" },
apiKey: { type: "string", format: "secret-ref" },
},
},
},
});
mockAccessService.canUser.mockResolvedValue(true);
mockAccessService.decide.mockResolvedValue({
allowed: false,
explanation: "Missing permission: secrets:read",
});
const pluginWorkerManager = {};
const app = createApp({
type: "board",
userId: "user-2",
source: "session",
companyIds: ["company-1"],
memberships: [{ companyId: "company-1", status: "active", membershipRole: "member" }],
}, { pluginWorkerManager });
const res = await request(app)
.post("/api/companies/company-1/environments/probe-config")
.send({
name: "Draft Secure Sandbox",
driver: "sandbox",
config: {
provider: "secure-plugin",
template: "base",
apiKey: "11111111-1111-1111-1111-111111111111",
},
});
expect(res.status).toBe(403);
expect(res.body.error).toContain("Instance admin access required");
expect(mockSecretService.resolveSecretValueForEphemeralAccess).not.toHaveBeenCalled();
expect(mockProbeEnvironment).not.toHaveBeenCalled();
});
});