Files
PaperClipAI/packages/shared/src/validators/instance.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

166 lines
4.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
instanceExperimentalSettingsSchema,
patchInstanceExperimentalSettingsSchema,
} from "./instance.js";
describe("instance experimental settings validators", () => {
it("defaults the server info debug view off", () => {
const settings = instanceExperimentalSettingsSchema.parse({});
expect(settings.enableServerInfoDebugView).toBe(false);
});
it("defaults workspace branch repair settings on", () => {
const settings = instanceExperimentalSettingsSchema.parse({});
expect(settings.enableWorkspaceBranchReconcileForward).toBe(true);
expect(settings.enableWorkspaceDirtyQuarantineRepair).toBe(true);
});
it("defaults the goals sidebar link off", () => {
const settings = instanceExperimentalSettingsSchema.parse({});
expect(settings.enableGoalsSidebarLink).toBe(false);
});
it("defaults the sandbox duplex bridge kill switch off", () => {
const settings = instanceExperimentalSettingsSchema.parse({});
expect(settings.enableSandboxDuplexBridge).toBe(false);
});
it("accepts an explicit sandbox duplex bridge kill switch value", () => {
expect(
instanceExperimentalSettingsSchema.parse({ enableSandboxDuplexBridge: true })
.enableSandboxDuplexBridge,
).toBe(true);
expect(
instanceExperimentalSettingsSchema.parse({ enableSandboxDuplexBridge: false })
.enableSandboxDuplexBridge,
).toBe(false);
});
it("accepts the sandbox duplex bridge kill switch in a patch", () => {
expect(
patchInstanceExperimentalSettingsSchema.parse({ enableSandboxDuplexBridge: true }),
).toEqual({ enableSandboxDuplexBridge: true });
});
it("defaults worktree run execution off", () => {
const settings = instanceExperimentalSettingsSchema.parse({});
expect(settings.enableWorktreeRunExecution).toBe(false);
expect(settings.worktreeRunExecutionActivatedAt).toBeNull();
expect(settings.worktreeRunExecutionActivationInstanceId).toBeNull();
});
it("strips server-managed worktree run execution fields from patches", () => {
expect(
patchInstanceExperimentalSettingsSchema.parse({
enableWorktreeRunExecution: true,
worktreeRunExecutionActivatedAt: "2026-07-10T12:00:00.000Z",
worktreeRunExecutionActivationInstanceId: "copied-instance",
}),
).toEqual({
enableWorktreeRunExecution: true,
});
});
it("defaults built-in agents off", () => {
const settings = instanceExperimentalSettingsSchema.parse({});
expect(settings.enableBuiltInAgents).toBe(false);
});
it("defaults beta skills off", () => {
const settings = instanceExperimentalSettingsSchema.parse({});
expect(settings.enableBetaSkills).toBe(false);
});
it("defaults apps off", () => {
const settings = instanceExperimentalSettingsSchema.parse({});
expect(settings.enableApps).toBe(false);
});
it("accepts worktree run execution patches", () => {
expect(
patchInstanceExperimentalSettingsSchema.parse({
enableWorktreeRunExecution: true,
}),
).toEqual({
enableWorktreeRunExecution: true,
});
});
it("defaults the decisions sidebar link off", () => {
const settings = instanceExperimentalSettingsSchema.parse({});
expect(settings.enableDecisions).toBe(false);
});
it("accepts decisions patches", () => {
expect(
patchInstanceExperimentalSettingsSchema.parse({
enableDecisions: true,
}),
).toEqual({
enableDecisions: true,
});
});
it("accepts server info debug view patches", () => {
expect(
patchInstanceExperimentalSettingsSchema.parse({
enableServerInfoDebugView: true,
}),
).toEqual({
enableServerInfoDebugView: true,
});
});
it("accepts workspace branch forward reconciliation patches", () => {
expect(
patchInstanceExperimentalSettingsSchema.parse({
enableWorkspaceBranchReconcileForward: false,
enableWorkspaceDirtyQuarantineRepair: false,
}),
).toEqual({
enableWorkspaceBranchReconcileForward: false,
enableWorkspaceDirtyQuarantineRepair: false,
});
});
it("accepts goals sidebar link patches", () => {
expect(
patchInstanceExperimentalSettingsSchema.parse({
enableGoalsSidebarLink: true,
}),
).toEqual({
enableGoalsSidebarLink: true,
});
});
it("accepts built-in agents patches", () => {
expect(
patchInstanceExperimentalSettingsSchema.parse({
enableBuiltInAgents: true,
}),
).toEqual({
enableBuiltInAgents: true,
});
});
it("accepts apps patches", () => {
expect(
patchInstanceExperimentalSettingsSchema.parse({
enableApps: true,
}),
).toEqual({
enableApps: true,
});
});
});