Files
PaperClipAI/doc/plugins/SANDBOX_PROVIDER_CAPABILITIES.md
T
Nicky LeachandPaperclip 3061ce6901 feat(sandbox): stream session output by capability, drop three operator flags (#11557)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandboxed agents use provider capabilities to select safe execution
paths
> - Session output still depends on three operator flags that duplicate
capability data
> - Duplicate flags can drift from the verified sandbox capability
snapshot
> - This pull request makes the capability snapshot the only streaming
decision and removes the obsolete flags
> - The benefit is default streaming with a poll fallback when a
capability or stream fails

## Linked Issues or Issue Description

**What existing behavior does this improve?**

ACP sandbox session-output streaming and sandbox execution
configuration.

**Subsystem affected**

Cross-cutting (multiple of the above): server/, packages/shared/,
packages/adapter-utils/, and packages/plugins/.

**Current behavior**

Session-output streaming requires operator flags in the server and
Daytona plugin configuration. Saved configurations can retain a removed
key.

**Proposed behavior**

The verified capability snapshot selects streaming. The Daytona plugin
uses persistent sessions by default, keeps bypass commands one-shot, and
falls back from the log stream to polling. Removed configuration keys
become inert.

**Reason and benefit**

One capability source prevents configuration drift. The fallback keeps
output available when capability resolution or log streaming fails.

**Breaking changes**

The three operator flags no longer control session-output streaming.
Existing saved keys load but have no effect.

## What Changed

- Remove `useSessions` and `useLogStream` from the Daytona plugin
configuration and manifest.
- Remove `streamAgentSessionOutput` from server configuration, shared
types, and execution-target plumbing.
- Select streaming from `persistentProcessSessions` and
`independentControlCommands`.
- Keep poll fallback on capability resolution failure and stream
failure.
- Strip removed keys from strict fake-sandbox and catchall plugin
configuration.
- Update the sandbox capability documentation and focused tests.

## Verification

- `tsc --noEmit` passed in `packages/shared`, `packages/adapter-utils`,
`server`, and the Daytona plugin.
- Daytona `plugin.test.ts` passed 139 tests.
- Server capability, configuration, route, and runtime suites passed 160
tests.
- `packages/adapter-utils` `execution-target-sandbox.test.ts` passed 44
tests.
- The capability matrix covers stream, poll, and resolution-failure
paths.
- Removed-key tests cover strict fake-sandbox and catchall plugin
schemas.

## Risks

- A capability snapshot that lacks either required session capability
uses polling.
- A log stream failure uses polling and can increase request count.
- Existing removed configuration keys no longer change behavior.
- The isolated-worktree Daytona Vitest run has a pre-existing missing
`packages/adapters/droid-local` reference. CI and standard checkouts use
the committed configuration.

## Model Used

OpenAI Codex, GPT-5, tool use and code review assistance. The exact
runtime context window is managed by the Codex platform.

## 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-17 13:25:30 -07:00

181 lines
8.9 KiB
Markdown

# Sandbox provider capability contract
A sandbox provider plugin declares an environment driver with
`kind: "sandbox_provider"`. Each driver can declare a set of optional sandbox
capabilities. This document is the contract for a third-party provider author.
It states what to declare, which worker methods each capability needs, what an
omitted key means, and when the host narrows or denies a capability.
Read [Sandbox file-sync lifecycle hooks](./SANDBOX_FILE_SYNC_HOOKS.md) for the
native file-transfer hooks. Read the driver declaration shape in
[the plugin specification](./PLUGIN_SPEC.md).
## How the host resolves an effective capability
The host never trusts a declaration alone. For every run it resolves each
capability as the intersection of three inputs:
```
effective = verified ∩ declared ∩ narrowing
```
- **verified** — the methods the live worker advertised in
`InitializeResult.supportedMethods`. The host maps each capability to the
worker methods it needs (see the table below). A capability is verified only
when the worker advertises every required method. An empty or missing method
list verifies nothing, so every capability resolves `false`.
- **declared** — the values in the driver declaration. A declared key is
optional (see the next section).
- **narrowing** — a per-run restriction from the lease policy or the resolved
provider config. A narrowing can only remove a capability, never add one.
A capability is effective only when all three allow it. A declaration therefore
never grants a capability that the worker did not verify.
## The declaration is optional and partial
Declare capabilities through the nested `sandboxCapabilities` object on the
driver declaration:
```ts
environmentDrivers: [
{
driverKey: "my-provider",
kind: "sandbox_provider",
displayName: "My Provider",
configSchema: { type: "object", properties: {} },
sandboxCapabilities: {
reusableLeases: true,
nativeSyncIn: true,
nativeSyncOut: true,
persistentProcessSessions: false,
independentControlCommands: false,
incrementalSessionOutput: false,
},
},
]
```
Every key is optional. For a valid, identified provider each key has one of three
states:
- **Omitted** — the host defers to verified worker discovery. The capability is
effective when the worker advertises the required methods and no narrowing
removes it. Omission is the correct default for a provider that follows the
standard method contract. `reusableLeases` and `incrementalSessionOutput` are
the two exceptions: an omitted key never grants either capability. Both are
opt-in (see the next two sections).
- **`false`** — the host narrows the capability to off. The capability is never
effective, even when the worker advertises the required methods.
- **`true`** — the host still requires the verified prerequisites. A `true`
value never grants a capability without them. It documents intent and lets the
host present the capability, but the worker must still advertise the required
methods.
## Reusable leases need an explicit opt-in
Reusable leases are the exception to the omission rule above. The host grants
reusable-lease acquisition only when the declaration sets `reusableLeases` to
`true`. The provider opts in through one of two fields:
- the nested `sandboxCapabilities.reusableLeases: true`, or
- the legacy `supportsReusableLeases: true`.
An omitted key leaves `reusableLeases` unset. An unset key does not make the
provider eligible for reusable-lease acquisition, and it does not advertise
provider-level reusable support. The host then always creates an ephemeral lease.
The opt-in never removes the other prerequisites. The worker must still verify
all three lifecycle methods, `environmentResumeLease`, `environmentReleaseLease`,
and `environmentDestroyLease`, and per-run narrowing still applies.
The two opt-in fields have a fixed precedence. The host keeps the legacy
`supportsReusableLeases` field for backward compatibility, and it folds the field
into `sandboxCapabilities.reusableLeases`.
- When only `supportsReusableLeases` is present, the host reads it as
`reusableLeases`.
- When both `supportsReusableLeases` and `sandboxCapabilities.reusableLeases` are
present, the nested value wins.
A manifest with legacy `true` and nested `false` therefore resolves to `false`.
Prefer the nested `sandboxCapabilities.reusableLeases` in a new manifest.
## Incremental session output needs an explicit opt-in
Incremental session output is the second exception to the omission rule. The host
selects the session-output streaming path only when the declaration sets
`incrementalSessionOutput` to `true`. An omitted key resolves the capability to
`false`, so the host keeps the output-file poll path.
The reason is that this key is a behavioral guarantee, not a worker-method
property. A generic one-shot provider can keep persistent process sessions and run
independent control commands, yet it never emits incremental stdout and stderr
from a live session. The two broad capabilities do not imply incremental output,
so the host requires the provider to declare the behavior. A provider that streams
incremental session output declares `sandboxCapabilities.incrementalSessionOutput:
true`; every other provider omits the key and keeps the poll path.
The opt-in never removes the prerequisites. The worker must still verify
`environmentExecute`, and per-run narrowing still applies. A config-resolution
failure narrows the capability to off (see [Failure behavior](#failure-behavior)).
## The capabilities and their worker-method prerequisites
| Capability | Required worker methods | Meaning |
| --- | --- | --- |
| `reusableLeases` | `environmentResumeLease`, `environmentReleaseLease`, **and** `environmentDestroyLease` | The host retains a provider lease and resumes it across runs. |
| `nativeSyncIn` | `environmentSyncIn` | The host transfers files into the sandbox through the native inbound hook. |
| `nativeSyncOut` | `environmentSyncOut` | The host transfers files out of the sandbox through the native outbound hook. |
| `persistentProcessSessions` | `environmentExecute` | The provider keeps a persistent process session open across commands. |
| `independentControlCommands` | `environmentExecute` | The provider runs a one-shot control command beside a long-lived command. |
| `incrementalSessionOutput` | `environmentExecute` | The provider streams incremental stdout and stderr from a live session. Opt-in: an omitted key resolves `false`. |
Reusable leases need all three lifecycle methods. The host resumes a lease with
`environmentResumeLease`, ends it with `environmentReleaseLease`, and tears down a
stale lease with `environmentDestroyLease`. The reuse path destroys a stale lease
when a resume fails, so a provider that cannot destroy a lease would strand it. A
worker that omits any of the three methods never gets reusable leases, even with a
positive declaration. The host then always creates an ephemeral lease.
The host advertises and consumes the two native sync methods as a pair. Define
both or neither. See [Sandbox file-sync lifecycle hooks](./SANDBOX_FILE_SYNC_HOOKS.md).
## Target and config narrowing
A narrowing removes a capability that the provider verified and declared but that
this run cannot use.
- **Ephemeral lease policy.** A lease that the host does not retain never reuses.
The host narrows `reusableLeases` to off for an ephemeral lease and keeps it on
only for a reuse-by-environment lease.
- **Kubernetes Job backend.** A Job-backed lease, or a lease the host marked as
unable to run native file sync, disables native sync. The host narrows
`nativeSyncIn` and `nativeSyncOut` to off and keeps the base64-over-exec
fallback.
## Failure behavior
The host fails closed on two failure states. It never grants a capability from an
unknown state.
- **Config-resolution failure.** A provider whose config the host cannot resolve
is untrusted. The host narrows `persistentProcessSessions` and
`incrementalSessionOutput` to off instead of allowing either through an empty
config.
- **Exact-plugin identity failure.** A retained lease pins the exact plugin that
acquired it. When that plugin is absent, or when it no longer declares this
provider key with the `sandbox_provider` kind, the host cannot establish the
declaration. It resolves every effective capability to `false`, no matter what
methods a stale worker still advertises. An omitted `sandboxCapabilities`
object on a valid, identified plugin is a different state; the host defers to
verified discovery for that case.
## Concurrency capabilities are not part of the contract
Earlier drafts listed two concurrency keys, `concurrentSyncAndExec` and
`concurrentSyncOperations`. The runtime never exposed a scheduling choice that
read either key, so a declaration had no effect. The host removed both keys. The
strict capability validator now rejects them as unknown keys. The host can
reintroduce a concurrency capability when a runtime path enforces it.