Files
PaperClipAI/server/src/services/environment-driver-traits.ts
T
Dotta 0a422fda52 feat(runner): add remote execution substrate (#12638)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Paperclip Runner gives native runs a durable and governed execution
path.
> - The current native path runs on the control-plane host.
> - Remote environments need an authenticated execution-target contract.
> - The contract must not change direct adapters or enable new runtimes
by default.
> - This pull request adds the remote execution substrate and Daytona
ingress.
> - The benefit is a bounded base for later remote runner transport
work.

## Linked Issues or Issue Description

Refs #12616.
Refs #12352.

**Subsystem affected**

Cross-cutting. This change touches runner transport, server
orchestration, plugin contracts, and shared settings.

**Problem or motivation**

Native execution cannot resolve an authenticated runner ingress through
a remote environment. The server also lacks one provider-neutral
contract for remote execution targets.

**Proposed solution**

Add a default-off runner preview ingress capability. Add
transport-neutral runner connectivity. Add remote execution target and
lifecycle handling. Add a Daytona ingress implementation with redacted
credentials.

**Alternatives considered**

A provider-specific server path would duplicate orchestration and
authorization. A public endpoint without an environment contract would
weaken the trust boundary.

**Roadmap alignment**

This work supports the Cloud and Sandbox agents milestone. It also
supports self-healing runs and governed tool access.

## What Changed

- Added execution-target traits for local, SSH, and sandbox
environments.
- Added plugin RPC contracts for runner ingress endpoints.
- Added authenticated Daytona preview ingress.
- Added transport-neutral PRP outbound connections.
- Added remote runner artifact verification and fail-closed provider
selection.
- Added bounded native session resume, cancellation, and lifecycle
recovery.
- Preserved Codex-only selection for fresh experimental runner starts.
- Preserved all direct adapter execution and finalization paths.
- Removed stale Pi provider-pack requirements that security review
rejected.
- Kept the rollout controls off by default.
- Did not change pnpm-lock.yaml, Cargo, database migrations, or GitHub
workflows.

## Verification

- GitHub Actions will run the repository test, typecheck, build,
security, and policy gates.
- Focused tests cover ingress validation, redaction, execution targets,
remote lifecycle, cancellation, resume, and legacy adapter selection.
- Local tests were not run. The requested verification policy uses
GitHub Actions for this series.
- `git diff --check origin/master...HEAD` passes.
- The diff contains 52 files.

## Risks

- Remote execution crosses a trust boundary.
- The implementation validates target capabilities, artifact digests,
provider-pack pins, and connection metadata.
- The feature remains default-off.
- Fresh native selection remains Codex-only.
- Existing direct adapters remain on the legacy path.
- This PR does not yet make remote Codex runnable. The next PR adds the
Rust WSS and TLS transport.

## Model Used

OpenAI Codex with GPT-5.6. The work used high-reasoning agent mode,
repository tools, GitHub tools, and parallel code-audit agents.

## 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 or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal
Paperclip ticket id
- [ ] 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 risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-09-01 01:29:06 -05:00

177 lines
7.2 KiB
TypeScript

/**
* The static per-driver traits for the four environment drivers.
*
* This module is a dependency leaf: it imports no other service module. It
* imports one type from `environment-runtime.ts` (`SandboxCapabilityKey`), but
* a type-only import compiles to nothing, so it adds no runtime dependency
* edge. `environment-runtime.ts` imports `workspace-realization.ts` at value
* level, so `workspace-realization.ts` cannot import `environment-runtime.ts`
* back. It imports this leaf module instead, and this module stays a leaf so
* the import graph has no cycle.
*
* Each trait below is a fixed fact about a driver family. It is a code fact,
* not a fact about one lease, so a consumer reads it as a plain table lookup
* instead of a driver-name condition.
*/
import type { EnvironmentDriver } from "@paperclipai/shared";
import type { SandboxCapabilityKey } from "./environment-runtime.js";
/**
* The static capability support definition for one environment driver. It names
* the eight capabilities the driver family can support at all. A capability the
* definition does not name resolves `false` for the driver, whatever a
* declaration, a verified verb, or a narrowing says. The general classifier
* reads this static definition for a built-in driver in place of a live worker
* method list.
*
* The definition is the code-level ground truth for a driver. It replaces a
* driver-identity condition: a consumer asks the classifier for a capability
* instead of matching the driver name.
*/
export interface EnvironmentDriverCapabilitySupport {
readonly driver: EnvironmentDriver;
/** The capability keys the driver family can support. */
readonly supportedCapabilities: ReadonlySet<SandboxCapabilityKey>;
}
const NO_CAPABILITY_SUPPORT: ReadonlySet<SandboxCapabilityKey> = new Set<SandboxCapabilityKey>();
// The nine capability keys, written out here as literal strings. This module
// does not import `environment-runtime.ts` as a value (see the module comment
// above), so it cannot read `SANDBOX_CAPABILITY_KEYS` from there. Keep this
// list equal to that list.
const ALL_CAPABILITY_SUPPORT: ReadonlySet<SandboxCapabilityKey> = new Set<SandboxCapabilityKey>([
"reusableLeases",
"nativeSyncIn",
"nativeSyncOut",
"persistentProcessSessions",
"independentControlCommands",
"incrementalSessionOutput",
"concurrentSyncOperations",
"duplexCommandStream",
"runnerWebSocketIngress",
]);
/**
* The static capability support for the four environment drivers.
*
* - `local` runs commands on the host file system with no provider capability
* model, so it supports none of the eight capabilities. Every capability
* resolves `false`.
* - `ssh` runs commands on a remote host through the SSH transport with no
* provider capability model, so it supports none of the eight capabilities
* either.
* - `sandbox` runs the full provider capability model. A built-in provider maps
* its own methods through `builtinSandboxProviderVerifiedMethods`, and a
* plugin-backed provider reports live worker methods, so the driver can support
* every capability. The classifier still intersects the per-provider
* declaration, the verified methods, and the per-lease narrowing.
* - `plugin` resolves its capabilities from the live plugin worker method list,
* so it can support every capability. The classifier intersects the live
* verified methods and the declaration.
*/
export const ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT: Record<
EnvironmentDriver,
EnvironmentDriverCapabilitySupport
> = {
local: { driver: "local", supportedCapabilities: NO_CAPABILITY_SUPPORT },
ssh: { driver: "ssh", supportedCapabilities: NO_CAPABILITY_SUPPORT },
sandbox: { driver: "sandbox", supportedCapabilities: ALL_CAPABILITY_SUPPORT },
plugin: { driver: "plugin", supportedCapabilities: ALL_CAPABILITY_SUPPORT },
};
/**
* The static per-driver traits every runtime consumer reads instead of a
* driver-identity condition. Each field is a fixed code fact about the driver
* family; it never varies per lease.
*/
export interface EnvironmentDriverTraits {
readonly driver: EnvironmentDriver;
/**
* True when the driver realizes a workspace through the runtime driver's
* `realizeWorkspace` method. Read by the run orchestrator
* (`environment-run-orchestrator.ts`, `realizeForRun`) to decide whether to
* call the driver before it resolves the execution target. The `plugin`
* driver skips this step: its execution target resolves the workspace
* itself, so the orchestrator never calls `realizeWorkspace` for it.
*/
readonly realizesWorkspace: boolean;
/**
* True when the driver runs the workspace on a target other than the host
* file system. Read by `isRemoteExecutionEnvironmentDriver`
* (`heartbeat.ts`) to decide whether a host-local directory path is present
* on the run target.
*/
readonly runsWorkspaceOffHost: boolean;
/**
* True when the driver stages a multi-source remote workspace through a
* confined per-project runtime. Read by `isConfinedRemoteStagingDriver`
* (`heartbeat.ts`) to decide whether a referenced (mentioned) project
* stages under that confinement guard.
*/
readonly confinesStagedProjects: boolean;
/**
* True when the driver resolves a per-lease capability snapshot that a
* consumer reads today. Read by `resolveEnvironmentExecutionTarget`
* (`environment-execution-target.ts`) to decide whether to call the
* general capability resolver for a lease on this driver.
*
* Only the `sandbox` driver has a consumer today. The `plugin` driver
* implements `resolveCapabilities`, but no consumer reads a plugin
* snapshot yet, so this stays `false` for `plugin` here. A change to
* `true` changes runtime behavior, so the board owns that decision.
*/
readonly hasLeaseCapabilityModel: boolean;
}
/**
* The static traits for the four environment drivers. See {@link
* EnvironmentDriverTraits} for what each field means and which consumer reads
* it.
*/
export const ENVIRONMENT_DRIVER_TRAITS: Record<EnvironmentDriver, EnvironmentDriverTraits> = {
local: {
driver: "local",
realizesWorkspace: true,
runsWorkspaceOffHost: false,
confinesStagedProjects: false,
hasLeaseCapabilityModel: false,
},
ssh: {
driver: "ssh",
realizesWorkspace: true,
runsWorkspaceOffHost: true,
confinesStagedProjects: false,
hasLeaseCapabilityModel: false,
},
sandbox: {
driver: "sandbox",
realizesWorkspace: true,
runsWorkspaceOffHost: true,
confinesStagedProjects: true,
hasLeaseCapabilityModel: true,
},
plugin: {
driver: "plugin",
realizesWorkspace: false,
runsWorkspaceOffHost: true,
confinesStagedProjects: false,
hasLeaseCapabilityModel: false,
},
};
/**
* Read the static traits for a driver key. Returns `null` for an unknown or
* absent driver, so a caller applies its own fallback default (the four
* drivers above are the only registered drivers today).
*/
export function getEnvironmentDriverTraits(
driver: string | null | undefined,
): EnvironmentDriverTraits | null {
if (!driver) return null;
return Object.prototype.hasOwnProperty.call(ENVIRONMENT_DRIVER_TRAITS, driver)
? ENVIRONMENT_DRIVER_TRAITS[driver as EnvironmentDriver]
: null;
}