Files
PaperClipAI/packages/shared/src/feature-catalog.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

307 lines
11 KiB
TypeScript

import { z } from "zod";
import { instanceExperimentalSettingsSchema } from "./validators/instance.js";
/**
* Feature catalog for cloud-managed instances.
*
* The instance-settings zod schema is the feature manifest; this module adds
* only metadata about the flags the schema already declares. Keys are derived
* from the schema type, so adding, removing, or renaming a boolean flag in
* `instanceExperimentalSettingsSchema` without updating the metadata map is a
* compile error (and vice versa).
*
* Tiers:
* - `preference`: tenant-controllable taste setting; the cloud harness does
* not manage it.
* - `managed`: the cloud harness may set this per fleet/stack via
* `PAPERCLIP_MANAGED_CONFIG`.
* - `floor`: pinned by code on managed instances; no flag value may widen it.
*/
export const FEATURE_TIERS = ["preference", "managed", "floor"] as const;
export type FeatureTier = (typeof FEATURE_TIERS)[number];
type ExperimentalSettings = z.infer<typeof instanceExperimentalSettingsSchema>;
/**
* The boolean flag keys of the experimental settings schema. Non-flag keys
* (activation timestamps, numeric tuning values) are excluded.
*/
export type InstanceFeatureKey = {
[K in keyof ExperimentalSettings]: ExperimentalSettings[K] extends boolean ? K : never;
}[keyof ExperimentalSettings];
export interface FeatureCatalogEntry {
title: string;
description: string;
tier: FeatureTier;
/** Desired default on cloud-managed instances. */
cloudDefault: boolean;
/** Must match the schema default; enforced by test. */
selfHostedDefault: boolean;
}
export const INSTANCE_FEATURE_CATALOG: Record<InstanceFeatureKey, FeatureCatalogEntry> = {
enableEnvironments: {
title: "Environments",
description:
"Show environment management in company settings and allow project and agent environment assignment controls.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableManagedSandboxOnly: {
title: "Managed Sandbox Only",
description:
"Hide the local environment and run all agents in the platform-managed sandbox environment.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableIsolatedWorkspaces: {
title: "Isolated Workspaces",
description:
"Show execution workspace controls in project configuration and allow isolated workspace behavior for task runs.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableStreamlinedLeftNavigation: {
title: "Streamlined Left Navigation",
description: "Use the streamlined main sidebar navigation layout.",
tier: "preference",
cloudDefault: true,
selfHostedDefault: true,
},
enableApps: {
title: "Apps",
description:
"Show the Apps navigation and allow access to app connections, gateways, and advanced app tooling.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enablePipelines: {
title: "Pipelines",
description: "Enable pipeline definitions and pipeline-driven case production surfaces.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableCases: {
title: "Cases",
description:
"Durable work products that tasks create and iterate on. Adds the Cases tab and the agent case API.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableConferenceRoomChat: {
title: "Conference Room Chat",
description:
"Add the Conference Room team chat, the live activity feed, and the redesigned onboarding; restyles task threads as chat bubbles.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableClassicTaskInterface: {
title: "Classic Task Interface",
description:
"Restore the pre-chat task detail page: the page-level header with inline description editor, the plain comment thread, and the fixed Properties sidebar. Chat-only features (streaming activity folding, inline plan/question cards, the three-mode composer) are unavailable in the classic view.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableTaskWatchdogs: {
title: "Task Watchdogs",
description:
"Show task detail controls for configuring watchdog agents that verify stopped task subtrees and restore live paths when work should continue.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableIssuePlanDecompositions: {
title: "Task Plan Decomposition Panel",
description: "Show accepted-plan decomposition history on task detail pages.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableExperimentalFileViewer: {
title: "Experimental File Viewer",
description:
"Show task detail controls for browsing and previewing workspace files relative to a task.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableStatusCards: {
title: "Status Cards",
description:
"Enable the experimental shared status-card board, update engine, and gated API.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableExternalObjects: {
title: "External Objects",
description:
"Detect external URLs in issues and show resolved status for pull requests, tickets, and other referenced work objects.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableSmokeLab: {
title: "Smoke Lab",
description:
"Add the Smoke Lab tab and dashboard card for exercising integration paths against deterministic local fixtures. Private deployments only.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableBuiltInAgents: {
title: "Built-in Agents",
description:
"Show Paperclip-managed built-in agent surfaces, including roster badges, the Built-in agents tab, and setup controls.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableBetaSkills: {
title: "Beta skills",
description: "Allow agents to pin beta releases of the Paperclip core skill.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableSummaries: {
title: "Summaries",
description:
"Show Summarizer-generated status slots on project and workspace pages, with on-demand refresh and revision history.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableDecisions: {
title: "Decisions",
description:
"Show the Decisions item in the main sidebar — the attention home that surfaces tasks awaiting input.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableGoalsSidebarLink: {
title: "Goals Sidebar Link",
description: "Restore the Goals item in the main sidebar while the goals surface is being evaluated.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableSimplifiedEnglishInteractions: {
title: "Simplified English Interactions",
description:
"Instruct agents to write user interactions (confirmations, questions, suggested tasks) in ASD-STE100 Simplified Technical English with brief decision context.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableServerInfoDebugView: {
title: "Server Info Debug View",
description:
"Show a Server section in the account drawer with the current server restart time and running commit.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
autoRestartDevServerWhenIdle: {
title: "Auto-Restart Dev Server When Idle",
description:
"In local development, wait for queued and running agent runs to finish, then restart the server automatically when backend changes make the current boot stale.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableIssueGraphLivenessAutoRecovery: {
title: "Auto-Create Recovery Tasks",
description:
"Let the heartbeat scheduler create recovery tasks for task dependency chains found inside the configured lookback window.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableWorkspaceBranchReconcileForward: {
title: "Workspace Branch Reconcile Forward",
description:
"Let execution workspaces reconcile a diverged recorded branch forward instead of failing branch containment.",
tier: "managed",
cloudDefault: true,
selfHostedDefault: true,
},
enableWorkspaceDirtyQuarantineRepair: {
title: "Workspace Dirty Quarantine Repair",
description:
"Let workspace runtime recovery quarantine and repair dirty execution workspaces before runs.",
tier: "managed",
cloudDefault: true,
selfHostedDefault: true,
},
enableOwnerInstanceAdmin: {
title: "Owner Instance Admin",
description:
"On cloud-managed instances, grant the stack owner instance-admin access to their own dedicated instance. Elevation is computed at the trusted-header auth boundary; no instance admin role rows are created. Inert on self-hosted instances.",
tier: "managed",
cloudDefault: true,
selfHostedDefault: false,
},
enableSandboxDuplexBridge: {
title: "Sandbox Duplex Bridge",
description:
"Let a run open the sandbox duplex command-stream bridge when the provider grants the capability. The host reads this per run before it selects the transport. Off keeps the file bridge for every run.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableWorktreeRunExecution: {
title: "Worktree Run Execution",
description:
"Let the scheduler execute runs inside an isolated git-worktree preview instance for tasks created after activation.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
};
export const INSTANCE_FEATURE_KEYS = Object.keys(INSTANCE_FEATURE_CATALOG).sort() as InstanceFeatureKey[];
/**
* Shape of the `feature-catalog.json` release artifact the cloud harness
* imports per app release and validates feature writes against.
*/
export const featureCatalogArtifactSchema = z
.object({
catalogVersion: z.string().min(1),
features: z.record(
z.string().min(1),
z.object({ tier: z.enum(FEATURE_TIERS) }).strict(),
),
})
.strict();
export type FeatureCatalogArtifact = z.infer<typeof featureCatalogArtifactSchema>;
export function buildFeatureCatalogArtifact(catalogVersion: string): FeatureCatalogArtifact {
if (catalogVersion.trim().length === 0) {
throw new Error("catalogVersion must be a non-empty string");
}
const features: FeatureCatalogArtifact["features"] = {};
for (const key of INSTANCE_FEATURE_KEYS) {
features[key] = { tier: INSTANCE_FEATURE_CATALOG[key].tier };
}
return { catalogVersion, features };
}
/** Deterministic serialization (sorted keys, trailing newline) for the artifact file. */
export function renderFeatureCatalogArtifact(catalogVersion: string): string {
return `${JSON.stringify(buildFeatureCatalogArtifact(catalogVersion), null, 2)}\n`;
}