mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-13 23:39:21 +02:00
## 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
130 lines
5.4 KiB
TypeScript
130 lines
5.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT,
|
|
SANDBOX_CAPABILITY_KEYS,
|
|
classifyEnvironmentCapabilities,
|
|
} from "../services/environment-runtime.js";
|
|
|
|
// The worker verbs a fully-capable provider advertises. A built-in driver maps
|
|
// its own methods onto these verb names, so the general classifier reads one
|
|
// verb vocabulary for every driver.
|
|
const ALL_PROVIDER_METHODS = [
|
|
"environmentResumeLease",
|
|
"environmentReleaseLease",
|
|
"environmentDestroyLease",
|
|
"environmentExecute",
|
|
"environmentSyncIn",
|
|
"environmentSyncOut",
|
|
"duplexChannelOpen",
|
|
"environmentRunnerIngressEndpoint",
|
|
];
|
|
|
|
describe("general capability classifier", () => {
|
|
it("returns every Boolean field for a full sandbox declaration input", () => {
|
|
// A sandbox provider that verifies every prerequisite verb and declares every
|
|
// capability resolves the whole eight-field set to true. The classifier reads
|
|
// the sandbox driver's static support definition, so it names no driver.
|
|
const effective = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ALL_PROVIDER_METHODS,
|
|
declared: {
|
|
reusableLeases: true,
|
|
nativeSyncIn: true,
|
|
nativeSyncOut: true,
|
|
persistentProcessSessions: true,
|
|
independentControlCommands: true,
|
|
incrementalSessionOutput: true,
|
|
concurrentSyncOperations: true,
|
|
duplexCommandStream: true,
|
|
runnerWebSocketIngress: true,
|
|
},
|
|
supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.sandbox.supportedCapabilities,
|
|
});
|
|
|
|
// The result carries exactly the eight capability fields, each a Boolean.
|
|
expect(Object.keys(effective).sort()).toEqual([...SANDBOX_CAPABILITY_KEYS].sort());
|
|
for (const key of SANDBOX_CAPABILITY_KEYS) {
|
|
expect(typeof effective[key]).toBe("boolean");
|
|
expect(effective[key]).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("default rule one: an absent declaration defers to verification for a worker-property field", () => {
|
|
// A worker-property capability has no opt-in declaration. An absent
|
|
// declaration defers to the verified baseline, so a verified verb grants the
|
|
// capability and an unverified verb denies it.
|
|
const effective = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["environmentSyncIn", "environmentSyncOut"],
|
|
declared: null,
|
|
supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.sandbox.supportedCapabilities,
|
|
});
|
|
|
|
// Verified sync verbs grant native sync without any declaration.
|
|
expect(effective.nativeSyncIn).toBe(true);
|
|
expect(effective.nativeSyncOut).toBe(true);
|
|
// The worker did not verify the execute or reuse verbs, so the baseline denies
|
|
// the matching worker-property capabilities.
|
|
expect(effective.persistentProcessSessions).toBe(false);
|
|
expect(effective.independentControlCommands).toBe(false);
|
|
expect(effective.reusableLeases).toBe(false);
|
|
});
|
|
|
|
it("default rule two: opt-in fields deny by default without a declaration", () => {
|
|
// The opt-in fields are behavioral guarantees. An absent declaration denies
|
|
// them even when the worker verifies the prerequisite verb.
|
|
const effective = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["environmentExecute", "environmentSyncIn", "environmentSyncOut", "duplexChannelOpen"],
|
|
declared: null,
|
|
supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.sandbox.supportedCapabilities,
|
|
});
|
|
|
|
expect(effective.incrementalSessionOutput).toBe(false);
|
|
expect(effective.concurrentSyncOperations).toBe(false);
|
|
expect(effective.duplexCommandStream).toBe(false);
|
|
});
|
|
|
|
it("a built-in driver static definition denies every capability it does not support", () => {
|
|
// The local and SSH drivers run no provider capability model, so their static
|
|
// support definition names no capability. Every capability resolves false even
|
|
// with every verb verified and every capability declared.
|
|
for (const driver of ["local", "ssh"] as const) {
|
|
const effective = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ALL_PROVIDER_METHODS,
|
|
declared: {
|
|
reusableLeases: true,
|
|
nativeSyncIn: true,
|
|
nativeSyncOut: true,
|
|
persistentProcessSessions: true,
|
|
independentControlCommands: true,
|
|
incrementalSessionOutput: true,
|
|
concurrentSyncOperations: true,
|
|
duplexCommandStream: true,
|
|
runnerWebSocketIngress: true,
|
|
},
|
|
supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT[driver].supportedCapabilities,
|
|
});
|
|
for (const key of SANDBOX_CAPABILITY_KEYS) {
|
|
expect(effective[key]).toBe(false);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("defines static capability support for the four drivers", () => {
|
|
expect(Object.keys(ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT).sort()).toEqual([
|
|
"local",
|
|
"plugin",
|
|
"sandbox",
|
|
"ssh",
|
|
]);
|
|
// The two remote provider drivers support the whole capability set; the two
|
|
// host drivers support none.
|
|
expect(ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.sandbox.supportedCapabilities.size).toBe(
|
|
SANDBOX_CAPABILITY_KEYS.length,
|
|
);
|
|
expect(ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.plugin.supportedCapabilities.size).toBe(
|
|
SANDBOX_CAPABILITY_KEYS.length,
|
|
);
|
|
expect(ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.local.supportedCapabilities.size).toBe(0);
|
|
expect(ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.ssh.supportedCapabilities.size).toBe(0);
|
|
});
|
|
});
|