Files
PaperClipAI/server/src/__tests__/instance-settings-service.test.ts
T
Nicky LeachandPaperclip faab2620ad feat(sandbox): add a duplex transport for Daytona behind a default-off kill switch (#11750)
## Thinking Path

> - Paperclip is an open source app that manages AI agents for work
> - Paperclip runs agents in local and remote sandbox environments
> - A sandbox needs a bounded channel for commands and asynchronous
input
> - Daytona needs a real pseudo-terminal transport for this channel
> - The sandbox gateway also needs a mode that handles channel loss
safely
> - This pull request adds the Daytona transport and gateway mode behind
a default-off kill switch
> - The benefit is a tested foundation for later transport selection

## Linked Issues or Issue Description

**Subsystem affected**

Cross-cutting (multiple of the above): sandbox providers, plugin SDK,
server settings, and shared types.

**Problem or motivation**

The merged sandbox protocol has no runtime transport for Daytona. The
generated sandbox gateway also has no duplex mode. A later
transport-selection change needs both parts and a safe per-run gate.

**Proposed solution**

Add a Daytona `duplexCommandStream` transport over a raw
pseudo-terminal. Add a generated gateway mode named `duplex_v1`. Add the
`enableSandboxDuplexBridge` setting with a default value of `false`.
Keep transport selection disabled until a later pull request.

**Alternatives considered**

Keep the protocol unused until the transport-selection change. This
would delay provider tests and leave the gateway path without direct
coverage.

**Roadmap alignment**

This change supports the completed Roadmap item for cloud and sandbox
agents. It extends the merged sandbox channel foundation in pull request
#11738.

**Additional context**

The Daytona provider remains an untrusted boundary. Deployments must use
least-privilege provider credentials and provider-side quota controls.
Operators must name an owner for duplex telemetry retention before
rollout.

## What Changed

- Add the Daytona `duplexCommandStream` capability over a raw
pseudo-terminal.
- Add a launch wrapper that disables echo and newline translation for
NDJSON frames.
- Close channels on lease release, destroy, resume of a stopped worker,
and worker shutdown.
- Declare the capability in the Daytona manifest and set
`PLUGIN_VERSION` to `0.1.5`.
- Add the worker-to-host notification sink at `ctx.duplexChannel.data`
and `ctx.duplexChannel.exit`.
- Add the generated sandbox gateway mode
`PAPERCLIP_API_BRIDGE_MODE=duplex_v1`.
- Add channel-loss results of `409 outcome_indeterminate` and `503
bridge_unavailable`.
- Add the per-run setting `enableSandboxDuplexBridge`, with a default
value of `false`.
- Add unit tests, generated-source codec tests, lifecycle tests, and a
credential-gated live Daytona test.

## Verification

- Daytona suite: 185 tests pass.
- Adapter utilities: 754 tests pass and 4 tests skip.
- Plugin SDK: 62 tests pass.
- Shared package: 28 tests pass.
- Server duplex tests pass.
- Shared, plugin SDK, server, and Daytona TypeScript checks pass.
- The live Daytona test passes 3 cases when `DAYTONA_API_KEY` is set.
- The live Daytona test skips 3 cases without `DAYTONA_API_KEY`.
- CI must run the full workspace typecheck, test, and build gates after
PR creation.

## Risks

- The Daytona control plane and pseudo-terminal remain untrusted
boundaries.
- The duplex gateway changes behavior only when the mode and per-run
setting enable it.
- A lost channel fails requests without replay, so callers must handle
indeterminate outcomes.
- The transport-selection change must require both `duplexCommandStream
=== true` and `enableSandboxDuplexBridge === true`.
- The provider credential and quota limits need operator control before
rollout.

## Model Used

OpenAI Codex, GPT-5, tool use and code execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-19 17:14:47 -07:00

395 lines
15 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import type { InstanceExperimentalSettings } from "@paperclipai/shared";
import {
applyExperimentalSettingsPatch,
normalizeExperimentalSettings,
resolveWorktreeRunExecutionActivationState,
} from "../services/instance-settings.js";
describe("instance settings service", () => {
it("ignores retired experimental flags without resetting current settings", () => {
expect(normalizeExperimentalSettings({
enableEnvironments: true,
enableManagedSandboxOnly: false,
enableIsolatedWorkspaces: true,
enableIssuePlanDecompositions: true,
enableExperimentalFileViewer: true,
enableTaskWatchdogs: true,
enableBuiltInAgents: true,
enableGoalsSidebarLink: true,
enableServerInfoDebugView: true,
autoRestartDevServerWhenIdle: true,
enableIssueGraphLivenessAutoRecovery: true,
enableWorkspaceBranchReconcileForward: true,
enableWorkspaceDirtyQuarantineRepair: false,
issueGraphLivenessAutoRecoveryLookbackHours: 48,
enableNewestFirstIssueThread: true,
})).toEqual({
enableEnvironments: true,
enableManagedSandboxOnly: false,
enableIsolatedWorkspaces: true,
enableStreamlinedLeftNavigation: true,
enableApps: false,
enableConferenceRoomChat: false,
enableClassicTaskInterface: false,
enableExternalObjects: false,
enableSmokeLab: false,
enablePipelines: false,
enableCases: false,
enableIssuePlanDecompositions: true,
enableExperimentalFileViewer: true,
enableTaskWatchdogs: true,
enableBuiltInAgents: true,
enableBetaSkills: false,
enableSummaries: false,
enableStatusCards: false,
enableDecisions: false,
enableGoalsSidebarLink: true,
enableServerInfoDebugView: true,
enableSimplifiedEnglishInteractions: false,
autoRestartDevServerWhenIdle: true,
enableIssueGraphLivenessAutoRecovery: true,
enableWorkspaceBranchReconcileForward: true,
enableWorkspaceDirtyQuarantineRepair: false,
enableOwnerInstanceAdmin: false,
enableSandboxDuplexBridge: false,
enableWorktreeRunExecution: false,
worktreeRunExecutionActivatedAt: null,
worktreeRunExecutionActivationInstanceId: null,
issueGraphLivenessAutoRecoveryLookbackHours: 48,
});
});
it("defaults enableApps to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableApps).toBe(false);
expect(normalizeExperimentalSettings({}).enableApps).toBe(false);
expect(normalizeExperimentalSettings({ enablePipelines: true }).enableApps).toBe(false);
});
it("defaults enableConferenceRoomChat to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableConferenceRoomChat).toBe(false);
expect(normalizeExperimentalSettings({}).enableConferenceRoomChat).toBe(false);
// Rows persisted before the flag existed (PAP-137) must normalize to off.
expect(
normalizeExperimentalSettings({ enableStreamlinedLeftNavigation: true }).enableConferenceRoomChat,
).toBe(false);
});
it("defaults enableClassicTaskInterface to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableClassicTaskInterface).toBe(false);
expect(normalizeExperimentalSettings({}).enableClassicTaskInterface).toBe(false);
// The retired enableTaskChatRedesign key must not bleed into the new flag:
// an install that had the chat redesign ON opted into chat-style, which is
// now the default — not into the classic view.
expect(
normalizeExperimentalSettings({ enableTaskChatRedesign: true }).enableClassicTaskInterface,
).toBe(false);
expect(
normalizeExperimentalSettings({ enableClassicTaskInterface: true }).enableClassicTaskInterface,
).toBe(true);
});
it("defaults enableSimplifiedEnglishInteractions to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableSimplifiedEnglishInteractions).toBe(false);
expect(normalizeExperimentalSettings({}).enableSimplifiedEnglishInteractions).toBe(false);
expect(
normalizeExperimentalSettings({ enableStreamlinedLeftNavigation: true })
.enableSimplifiedEnglishInteractions,
).toBe(false);
expect(
normalizeExperimentalSettings({ enableSimplifiedEnglishInteractions: true })
.enableSimplifiedEnglishInteractions,
).toBe(true);
});
it("defaults enableTaskWatchdogs to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableTaskWatchdogs).toBe(false);
expect(normalizeExperimentalSettings({}).enableTaskWatchdogs).toBe(false);
expect(
normalizeExperimentalSettings({ enableExperimentalFileViewer: true }).enableTaskWatchdogs,
).toBe(false);
});
it("defaults enableSmokeLab to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableSmokeLab).toBe(false);
expect(normalizeExperimentalSettings({}).enableSmokeLab).toBe(false);
expect(
normalizeExperimentalSettings({ enableExternalObjects: true }).enableSmokeLab,
).toBe(false);
});
it("defaults enableServerInfoDebugView to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableServerInfoDebugView).toBe(false);
expect(normalizeExperimentalSettings({}).enableServerInfoDebugView).toBe(false);
expect(
normalizeExperimentalSettings({ autoRestartDevServerWhenIdle: true }).enableServerInfoDebugView,
).toBe(false);
});
it("defaults enableGoalsSidebarLink to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableGoalsSidebarLink).toBe(false);
expect(normalizeExperimentalSettings({}).enableGoalsSidebarLink).toBe(false);
expect(
normalizeExperimentalSettings({ enableStreamlinedLeftNavigation: true }).enableGoalsSidebarLink,
).toBe(false);
});
it("defaults enableDecisions to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableDecisions).toBe(false);
expect(normalizeExperimentalSettings({}).enableDecisions).toBe(false);
expect(
normalizeExperimentalSettings({ enableStreamlinedLeftNavigation: true }).enableDecisions,
).toBe(false);
});
it("defaults workspace branch repair settings to true for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableWorkspaceBranchReconcileForward).toBe(true);
expect(normalizeExperimentalSettings({}).enableWorkspaceBranchReconcileForward).toBe(true);
expect(
normalizeExperimentalSettings({ enableIssueGraphLivenessAutoRecovery: true })
.enableWorkspaceBranchReconcileForward,
).toBe(true);
expect(normalizeExperimentalSettings(undefined).enableWorkspaceDirtyQuarantineRepair).toBe(true);
expect(normalizeExperimentalSettings({}).enableWorkspaceDirtyQuarantineRepair).toBe(true);
expect(
normalizeExperimentalSettings({ enableWorkspaceBranchReconcileForward: false })
.enableWorkspaceDirtyQuarantineRepair,
).toBe(true);
});
it("round-trips an enableConferenceRoomChat patch through the update merge", () => {
// updateExperimental merges `{ ...normalize(current), ...patch }` and
// re-normalizes; emulate that to prove the flag survives the roundtrip
// without disturbing other settings.
const current = normalizeExperimentalSettings({});
const enabled = normalizeExperimentalSettings({ ...current, enableConferenceRoomChat: true });
expect(enabled.enableConferenceRoomChat).toBe(true);
expect(enabled.enableStreamlinedLeftNavigation).toBe(true);
const disabled = normalizeExperimentalSettings({ ...enabled, enableConferenceRoomChat: false });
expect(disabled).toEqual(current);
});
it("rejects non-boolean enableConferenceRoomChat values back to the default", () => {
expect(
normalizeExperimentalSettings({ enableConferenceRoomChat: "yes" }).enableConferenceRoomChat,
).toBe(false);
});
it("defaults enableBuiltInAgents to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableBuiltInAgents).toBe(false);
expect(normalizeExperimentalSettings({}).enableBuiltInAgents).toBe(false);
expect(normalizeExperimentalSettings({ enableExternalObjects: true }).enableBuiltInAgents).toBe(false);
});
it("preserves enableBetaSkills and defaults it off for legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableBetaSkills).toBe(false);
expect(normalizeExperimentalSettings({}).enableBetaSkills).toBe(false);
expect(normalizeExperimentalSettings({ enableBetaSkills: true }).enableBetaSkills).toBe(true);
});
it("sets worktree run execution activation fields on a false to true transition", () => {
const activatedAt = new Date("2026-07-10T12:00:00.000Z");
const next = applyExperimentalSettingsPatch(
{ enableWorktreeRunExecution: false },
{ enableWorktreeRunExecution: true },
{
now: () => activatedAt,
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
},
);
expect(next.enableWorktreeRunExecution).toBe(true);
expect(next.worktreeRunExecutionActivatedAt).toBe("2026-07-10T12:00:00.000Z");
expect(next.worktreeRunExecutionActivationInstanceId).toBe("worktree-instance");
});
it("clears worktree run execution activation fields on a true to false transition", () => {
const next = applyExperimentalSettingsPatch(
{
enableWorktreeRunExecution: true,
worktreeRunExecutionActivatedAt: "2026-07-10T12:00:00.000Z",
worktreeRunExecutionActivationInstanceId: "worktree-instance",
},
{ enableWorktreeRunExecution: false },
{
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
},
);
expect(next.enableWorktreeRunExecution).toBe(false);
expect(next.worktreeRunExecutionActivatedAt).toBeNull();
expect(next.worktreeRunExecutionActivationInstanceId).toBeNull();
});
it("refreshes the activation cutoff when worktree run execution is re-toggled", () => {
const firstActivation = applyExperimentalSettingsPatch(
{ enableWorktreeRunExecution: false },
{ enableWorktreeRunExecution: true },
{
now: () => new Date("2026-07-10T12:00:00.000Z"),
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
},
);
const disabled = applyExperimentalSettingsPatch(
firstActivation,
{ enableWorktreeRunExecution: false },
{
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
},
);
const secondActivation = applyExperimentalSettingsPatch(
disabled,
{ enableWorktreeRunExecution: true },
{
now: () => new Date("2026-07-10T12:05:00.000Z"),
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
},
);
expect(secondActivation.worktreeRunExecutionActivatedAt).toBe("2026-07-10T12:05:00.000Z");
expect(secondActivation.worktreeRunExecutionActivatedAt).not.toBe(
firstActivation.worktreeRunExecutionActivatedAt,
);
});
it("strips client-supplied activation fields before applying experimental patches", () => {
const next = applyExperimentalSettingsPatch(
{ enableWorktreeRunExecution: false },
{
enableWorktreeRunExecution: false,
worktreeRunExecutionActivatedAt: "2026-07-10T12:00:00.000Z",
worktreeRunExecutionActivationInstanceId: "copied-instance",
},
{
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
},
);
expect(next.worktreeRunExecutionActivatedAt).toBeNull();
expect(next.worktreeRunExecutionActivationInstanceId).toBeNull();
});
it("resolves worktree run execution as armed only when the cutoff matches the current instance", async () => {
const experimental = normalizeExperimentalSettings({
enableWorktreeRunExecution: true,
worktreeRunExecutionActivatedAt: "2026-07-10T12:00:00.000Z",
worktreeRunExecutionActivationInstanceId: "worktree-instance",
});
await expect(
resolveWorktreeRunExecutionActivationState({
getExperimental: async () => experimental,
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
}),
).resolves.toEqual({
armed: true,
cutoff: "2026-07-10T12:00:00.000Z",
activationInstanceId: "worktree-instance",
reason: null,
});
});
it("fails closed when worktree run execution is missing a cutoff", async () => {
const experimental = normalizeExperimentalSettings({
enableWorktreeRunExecution: true,
worktreeRunExecutionActivationInstanceId: "worktree-instance",
});
await expect(
resolveWorktreeRunExecutionActivationState({
getExperimental: async () => experimental,
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
}),
).resolves.toMatchObject({
armed: false,
cutoff: null,
reason: "missing_cutoff",
});
});
it("fails closed when worktree run execution was activated by another instance", async () => {
const experimental = normalizeExperimentalSettings({
enableWorktreeRunExecution: true,
worktreeRunExecutionActivatedAt: "2026-07-10T12:00:00.000Z",
worktreeRunExecutionActivationInstanceId: "source-instance",
});
await expect(
resolveWorktreeRunExecutionActivationState({
getExperimental: async () => experimental,
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "target-instance",
},
}),
).resolves.toMatchObject({
armed: false,
cutoff: null,
activationInstanceId: "source-instance",
reason: "instance_id_mismatch",
});
});
it("fails closed on settings read errors and avoids reads outside worktree runtimes", async () => {
await expect(
resolveWorktreeRunExecutionActivationState({
getExperimental: async () => {
throw new Error("settings unavailable");
},
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "true",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
}),
).resolves.toMatchObject({
armed: false,
cutoff: null,
reason: "settings_read_error",
});
const getExperimental = vi.fn<() => Promise<InstanceExperimentalSettings>>();
await expect(
resolveWorktreeRunExecutionActivationState({
getExperimental,
runtimeEnv: {
PAPERCLIP_IN_WORKTREE: "false",
PAPERCLIP_INSTANCE_ID: "worktree-instance",
},
}),
).resolves.toMatchObject({
armed: false,
cutoff: null,
reason: "not_worktree_runtime",
});
expect(getExperimental).not.toHaveBeenCalled();
});
});