Files
paperclip/packages/plugins/sandbox-providers/exe-dev/src/manifest.ts
T
Devin Foley aea35fe695 exe.dev config UX: advanced-options disclosure, form-default fix, SSH key handling (PAPA-407) (#7025)
## Thinking Path

> - Paperclip orchestrates AI agents and provisions sandboxed execution
environments for them; one of those provisioners is the exe.dev plugin,
which runs each agent inside a long-lived VM reached over SSH.
> - The instance-config form for that plugin is rendered generically by
`JsonSchemaForm` from the plugin's `instanceConfigSchema`, so any UX
problem with the form is split between the shared form component and the
plugin's schema/runtime code.
> - Users coming in cold hit a 12-field flat config they couldn't reason
about (PAPA-407), a form that silently submitted `cpu: 0` for untouched
optional fields (PAPA-407 root cause), a `sshPrivateKey` textarea that
truncated RSA-4096 keys at 4096 chars (PAPA-449), a save flow that
accepted clearly-malformed keys and only blew up at lease time with raw
SSH stderr (PAPA-450, PAPA-451), and a manifest that didn't distinguish
"essential" from "advanced" knobs (PAPA-410 / PAPA-411 — duplicate
sub-issues with identical scope; PAPA-418 reconciliation kept PAPA-410
canonical).
> - These problems all point at the same surface (exe.dev sandbox
config) and are tightly coupled in code — PAPA-449/450/451 patch fields
that PAPA-410/411 introduce — so they get reviewed together.
> - This pull request lands the shared-form changes (advanced-options
disclosure, optional-scalar defaults) and the exe.dev-specific changes
(manifest restructure, longer `maxLength`, stderr translation, save-time
key validation) as five focused commits stacked on `master`.
> - The benefit is a config form that defaults to the two fields a new
user actually needs (API key + SSH private key) with a collapsible
disclosure for the rest, no silent truncation or zero-default
submissions, and SSH key problems surfaced at save time with actionable
messages instead of cryptic post-provision failures.

## What Changed

- **JsonSchemaForm advanced-options disclosure** (PAPA-410, PAPA-411 —
same scope, see note above): adds `x-paperclip-advanced` /
`x-paperclip-group` schema annotations and renders flagged fields behind
a collapsible "Advanced options" disclosure that auto-opens when a
hidden field has a validation error. Exe.dev manifest is restructured to
use the new annotations, so essentials (`apiKey`, `sshPrivateKey`) show
by default while the long tail of optional knobs is grouped under "SSH
access" / "VM resources" / "More options" headings.
- **Omit optional scalar defaults** (PAPA-407): `getDefaultForSchema` no
longer materialises `0` / `""` for optional
`number`/`integer`/`string`/`secret-ref` fields without an explicit
`default`. Object recursion drops properties whose default is
`undefined`. Fields that declare a `default` (e.g. `sshPort: 22`) still
round-trip. Adds a regression test against `getDefaultValues`.
- **Raise `sshPrivateKey` `maxLength`** (PAPA-449): bumps the exe.dev
manifest cap from 4096 to 8192 so RSA-4096 OpenSSH private keys (which
can exceed 4 KB with comments/metadata) aren't silently truncated at
submit.
- **Translate `invalid format` SSH stderr** (PAPA-450):
`formatSshFailure` now recognises `Load key … invalid format` in
combined stderr/stdout and returns a specific message naming the
key-format problem ("isn't an OpenSSH/PEM private key — confirm the
secret starts with `-----BEGIN … PRIVATE KEY-----` and isn't the `.pub`
or a PuTTY `.ppk` export") instead of dumping the raw stderr.
- **Save-time SSH key validation** (PAPA-451):
`onEnvironmentValidateConfig` inline-parses `sshPrivateKey` and rejects
common failure modes — pasted public keys, PuTTY `.ppk` format, missing
`-----END-----` footer, non-base64 body — so the form surfaces an inline
error before any VM is provisioned. Secret-ref bindings (UUIDs) are
still passed through unchanged.

## Verification

CI gates (`pnpm typecheck`, `pnpm test`, the targeted vitest suites
below) all pass.

Run locally:

```bash
# Shared form
pnpm --filter @paperclipai/ui exec vitest run src/components/JsonSchemaForm
# 9 tests pass — includes the new "omits optional scalar fields" regression
# and the three advanced-options-disclosure tests.

# exe.dev plugin
cd packages/plugins/sandbox-providers/exe-dev && pnpm test
# 32 tests pass — includes the new sshPrivateKey-validation cases
# and the new "invalid format" stderr-translation case.
```

Manual smoke (after reinstalling the plugin so the DB manifest
refreshes):

1. Open the exe.dev environment config page. **Default view shows API
Key + SSH Private Key only**, with an "Advanced options" disclosure for
everything else (PAPA-410 / PAPA-411).
2. Paste a `.pub` file's contents into SSH Private Key, click Save.
**Inline error** rejecting the wrong-format key (PAPA-451).
3. Re-paste a valid OpenSSH/PEM private key longer than 4096 bytes —
saves cleanly (PAPA-449).
4. Save the form with everything optional left blank — server no longer
rejects with `"cpu must be greater than 0 when provided"` (PAPA-407).
5. Force a bad key through via a stored secret-ref binding and lease a
VM — failure message names the key-format problem instead of dumping raw
SSH stderr (PAPA-450).

## Risks

- **PAPA-410 / PAPA-411 manifest restructure** is the largest surface
here. Schemas using `x-paperclip-*` extensions are forward-compatible
with stricter JSON Schema validators (extensions are ignored by
default), and the form gracefully renders a flat layout when no field
opts in.
- **PAPA-407** changes form-default behaviour: optional scalar fields
that previously round-tripped as `""` / `0` will now be `undefined` and
absent from the submitted payload. Downstream consumers that expected
the empty-string/zero shape need to treat the field as optional.
Spot-checked the existing exe.dev driver — it already uses
`parseOptionalString` / `parseOptionalInteger`, which treat missing
fields as `null` rather than `0`/`""`.
- **PAPA-451** adds a save-time check, so a
previously-saved-but-malformed `sshPrivateKey` raw value will now fail
to re-save. Bound secret-refs are unaffected, matching how the user
reaches the bad-key state today (via the secrets picker).
- **PAPA-449** simply raises a cap; no semantic risk.
- **PAPA-450** only kicks in on the "invalid format" code path; existing
onboarding-marker branch is untouched.

## Model Used

- Provider: Anthropic
- Model: Claude Opus 4.7 (`claude-opus-4-7`)
- Capabilities used: code reading, code editing, test execution, git/PR
mechanics, Paperclip API for issue coordination

## Checklist

- [x] PR body sections present (Thinking Path, What Changed,
Verification, Risks, Model Used, Checklist)
- [x] Unit tests added for the new behaviours (JsonSchemaForm
default-value omission + advanced disclosure; exe.dev plugin validation
+ stderr translation)
- [x] Existing tests still pass locally (`vitest run` on both packages)
- [x] No raw secrets, IP addresses, or machine-local config in commits
or PR body
- [x] Commits are atomic per linked issue (PAPA-410 / PAPA-411,
PAPA-407, PAPA-449, PAPA-450, PAPA-451)
- [x] Branch is up-to-date with `origin/master`

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-29 18:19:37 -07:00

181 lines
6.6 KiB
TypeScript

import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
const PLUGIN_ID = "paperclip.exe-dev-sandbox-provider";
const PLUGIN_VERSION = "0.1.1";
const manifest: PaperclipPluginManifestV1 = {
id: PLUGIN_ID,
apiVersion: 1,
version: PLUGIN_VERSION,
displayName: "exe.dev Sandbox Provider",
description:
"Sandbox provider plugin that provisions exe.dev VMs as Paperclip execution environments.",
author: "Paperclip",
categories: ["automation"],
capabilities: ["environment.drivers.register"],
entrypoints: {
worker: "./dist/worker.js",
},
environmentDrivers: [
{
driverKey: "exe-dev",
kind: "sandbox_provider",
displayName: "exe.dev VM",
description:
"Provisions exe.dev VMs through the HTTPS API, then runs commands over direct SSH for long-lived Paperclip workloads.",
configSchema: {
type: "object",
properties: {
// ---- Essentials (always visible, in this order) ----
apiKey: {
type: "string",
format: "secret-ref",
description:
"Paste your exe.dev API token, or pick a saved Paperclip secret. Create one at exe.dev → Settings → API tokens with `/exec` scope (`new`, `ls`, `rm`).",
},
sshPrivateKey: {
type: "string",
format: "secret-ref",
maxLength: 8192,
description:
"Paste the SSH private key you registered with exe.dev, or pick a saved secret. Leave blank to fall back to an on-host key (see Advanced → SSH access).",
},
// ---- Advanced: SSH access ----
sshUser: {
type: "string",
description:
"Login user on the VM. Leave blank to use the image default, usually `root`.",
"x-paperclip-advanced": true,
"x-paperclip-group": "SSH access",
},
sshIdentityFile: {
type: "string",
description:
"Absolute path to a private key on the Paperclip host. Used only when SSH Private Key is empty.",
"x-paperclip-advanced": true,
"x-paperclip-group": "SSH access",
},
sshPort: {
type: "number",
description: "SSH port for direct VM access.",
default: 22,
"x-paperclip-advanced": true,
"x-paperclip-group": "SSH access",
},
strictHostKeyChecking: {
type: "string",
description:
"Host key policy passed to ssh via StrictHostKeyChecking. Typical values are `accept-new`, `yes`, or `no`.",
default: "accept-new",
"x-paperclip-advanced": true,
"x-paperclip-group": "SSH access",
},
// ---- Advanced: VM resources ----
image: {
type: "string",
description: "Optional container image to use when creating the VM.",
"x-paperclip-advanced": true,
"x-paperclip-group": "VM resources",
},
cpu: {
type: "number",
description: "Optional CPU count passed to `exe.dev new --cpu`.",
"x-paperclip-advanced": true,
"x-paperclip-group": "VM resources",
},
memory: {
type: "string",
description: "Optional memory size such as `4GB`.",
"x-paperclip-advanced": true,
"x-paperclip-group": "VM resources",
},
disk: {
type: "string",
description: "Optional disk size such as `20GB`.",
"x-paperclip-advanced": true,
"x-paperclip-group": "VM resources",
},
// ---- Advanced: VM creation ----
command: {
type: "string",
description: "Optional container command passed to `exe.dev new --command`.",
"x-paperclip-advanced": true,
"x-paperclip-group": "VM creation",
},
env: {
type: "object",
description: "Optional environment variables applied at VM creation time.",
additionalProperties: { type: "string" },
"x-paperclip-advanced": true,
"x-paperclip-group": "VM creation",
},
integrations: {
type: "array",
description: "Optional exe.dev integrations to attach during VM creation.",
items: { type: "string" },
"x-paperclip-advanced": true,
"x-paperclip-group": "VM creation",
},
tags: {
type: "array",
description: "Optional tags to apply during VM creation.",
items: { type: "string" },
"x-paperclip-advanced": true,
"x-paperclip-group": "VM creation",
},
setupScript: {
type: "string",
description: "Optional first-boot setup script passed to `exe.dev new --setup-script`.",
"x-paperclip-advanced": true,
"x-paperclip-group": "VM creation",
},
prompt: {
type: "string",
description: "Optional Shelley prompt passed to `exe.dev new --prompt`.",
"x-paperclip-advanced": true,
"x-paperclip-group": "VM creation",
},
comment: {
type: "string",
description: "Optional short note attached to created VMs.",
"x-paperclip-advanced": true,
"x-paperclip-group": "VM creation",
},
namePrefix: {
type: "string",
description: "Optional prefix used when generating VM names.",
default: "paperclip",
"x-paperclip-advanced": true,
"x-paperclip-group": "VM creation",
},
// ---- Advanced: API + runtime ----
apiUrl: {
type: "string",
description:
"Optional exe.dev HTTPS API base URL or /exec endpoint. Defaults to https://exe.dev/exec.",
"x-paperclip-advanced": true,
"x-paperclip-group": "API + runtime",
},
timeoutMs: {
type: "number",
description: "Timeout for VM lifecycle and SSH operations in milliseconds.",
default: 300000,
"x-paperclip-advanced": true,
"x-paperclip-group": "API + runtime",
},
reuseLease: {
type: "boolean",
description:
"Whether to keep the VM alive between runs instead of deleting it on release.",
default: false,
"x-paperclip-advanced": true,
"x-paperclip-group": "API + runtime",
},
},
},
},
],
};
export default manifest;