Commit Graph

4 Commits

Author SHA1 Message Date
Devin Foley a937b89a47 fix(daytona): valid memory input in env config form + size presets (#8389)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Environments are configured through a JSON-schema-driven form
(`JsonSchemaForm`), and each sandbox provider (here, Daytona) supplies a
manifest describing its fields
> - In the Daytona environment config form, the "memory" field rendered
as a free-text input; on save the value round-tripped to `0`, producing
a server-side "number must be greater than zero" error even when the
user typed a valid number like `2`
> - Rather than patch the free-text input, memory is now a true dropdown
of the sandbox sizes Daytona actually supports, so an invalid value can
no longer be typed or coerced
> - The dropdown leads with a blank "None" row that is selected by
default (meaning "not configured — use Daytona's defaults"); `0` is
never offered because it is not a valid configuration
> - The benefit is that operators pick a valid memory size from a
constrained list, it saves correctly as an integer, and leaving it unset
cleanly omits the field instead of submitting `0`

## Linked Issues or Issue Description

No public GitHub issue exists; describing the bug inline per the bug
report template.

### What happened?

In the Daytona environment configuration form, the memory field is a
free-text input labelled "gigabytes of RAM". Entering a value such as
`2` and saving coerced the value to `0`, and the server rejected the
save with "the number must be greater than zero". There was no way to
enter a valid memory size through the form.

### Expected behavior

Selecting a valid memory size (e.g. `2`) keeps that value and saves
cleanly as an integer. Leaving memory unset is valid and submits no
value (Daytona defaults apply). `0` is never selectable.

### Steps to reproduce

1. Open the environment configuration form for a Daytona sandbox
provider.
2. In the memory field, type `2`.
3. Click Save.
4. Observe the value becomes `0` and the form errors with "the number
must be greater than zero".

## What Changed

- Daytona manifest: `memory` is now an `enum` of the supported sandbox
sizes `[1, 2, 4, 8]` (GiB). It stays optional, so "not configured"
remains valid. `0` is not in the list.
- `JsonSchemaForm` `EnumField`: optional enums now render a leading
blank **None** row that is selected by default when no value is set,
letting the user express "not configured" and clear a previous selection
(Radix `Select` forbids an empty-string item value, so the unset state
maps to a sentinel that translates back to `undefined`).
- `JsonSchemaForm` `EnumField`: when every enum option is numeric, the
selected value is coerced back to a number on change so the payload
keeps the schema's integer type (a stringified `"2"` would otherwise
fail server-side integer validation — this is what fixes the original
bug for the dropdown path).
- Tests: added `EnumField` coverage in `JsonSchemaForm.test.tsx` (blank
row present, no `0`, blank selected by default, numeric coercion, blank
→ unset) and Daytona manifest coverage in the plugin test (memory enum
is `[1,2,4,8]`, excludes `0`, stays optional).

## Verification

- `cd ui && npx vitest run src/components/JsonSchemaForm.test.tsx
src/pages/CompanyEnvironments.test.tsx` → 16/16 passing (14 + 2).
- `vitest run` in `packages/plugins/sandbox-providers/daytona` → 19/19
passing (incl. 3 new manifest tests).
- `cd ui && npx tsc --noEmit` → clean.
- Behavior: the memory field now renders as a dropdown showing **None /
1 / 2 / 4 / 8**, with **None** selected by default. Picking `2` stores
the integer `2`; picking **None** clears the field so it is omitted from
the payload (no `0`, no "must be greater than zero" error).

## Risks

- Low risk. The blank-row + numeric-coercion changes live in
`EnumField`. The blank row is only added for **optional** enums
(required enums are unaffected); numeric coercion only triggers when
every option is numeric, so existing string enums (`egressMode`,
`backend`, `sessionStrategy`) are unchanged. The manifest change is
scoped to the Daytona provider.

## Model Used

Claude (Anthropic), Opus-class model, used via the Paperclip agent
workflow (author + reviewer agents) with 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 (none found)
- [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
- [x] My branch name describes the change and contains no internal
Paperclip ticket id
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-06-20 17:58:47 -07:00
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
Devin Foley c0c5a8263d feat(ui): wire SecretBindingPicker into JsonSchemaForm secret-ref fields (#6339)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Plugin authors expose configuration via JSON schemas, including
secret fields marked `format: "secret-ref"`
> - At the same time, Paperclip already has a first-class secrets store,
and `SecretBindingPicker` is the canonical UI for binding to one of
those stored secrets
> - But `JsonSchemaForm`'s `SecretField` rendered only a plain password
input, so configuring an E2B (or Modal / Cloudflare / Daytona) sandbox
required leaving the form, copying a secret UUID, and pasting it back
> - This pull request wires `SecretBindingPicker` into `SecretField` so
every plugin secret-ref field gets the picker plus an optional raw-value
fallback
> - The benefit is that secret reuse becomes one click instead of a tab
switch, and the raw-paste path still works for one-off keys or long
SSH-style secrets

## What Changed

- `ui/src/components/JsonSchemaForm.tsx` `SecretField` now renders
`SecretBindingPicker` above the existing password/textarea input.
UUID-shaped values are treated as bound refs (no raw input shown).
Non-UUID values keep the password/textarea visible (auto-opened) for SSH
keys and other long secrets. Empty fields show the picker plus a small
"Or paste a raw value" toggle.
- Selecting a secret writes the secret UUID to the form value — the
server-side resolution in `server/src/services/environment-config.ts`
(`resolveConfigSecretRefsForRuntime` / `collectEnvironmentSecretRefs`)
is unchanged. The version selector on the picker is suppressed
(`allowVersionSelector={false}`) because plugin secret refs always
resolve at `"latest"`.
- `ui/src/components/JsonSchemaForm.test.tsx` mocks the picker (which
requires `CompanyContext` + `QueryClient` providers) and adds coverage
for: picker render, UUID-bound state hides the raw input, picker
selection writes the UUID through `onChange`, raw text keeps the
password fallback. The original multiline (SSH key) case still asserts a
textarea + no password input.

## Verification

- `pnpm --filter @paperclipai/ui test
src/components/JsonSchemaForm.test.tsx` → 4/4 passing
- `pnpm --filter @paperclipai/ui test src/pages/PluginSettings.test.tsx`
→ 5/5 passing (existing consumer of `JsonSchemaForm`)
- `pnpm --filter @paperclipai/ui exec tsc --noEmit` → clean
- Manual: in the company Environments page, edit an environment with a
sandbox driver that exposes a `secret-ref` field (e.g., E2B `apiKey`).
The field should render the secret dropdown above the raw-value toggle;
selecting an active secret persists its UUID, and saving the form
continues to resolve the secret at runtime.

Before/after screenshots: deferred — change was validated by
[@devinfoley](https://github.com/devinfoley) on the main Paperclip
instance before this PR was opened. Happy to add screenshots if a
reviewer wants them.

## Risks

- Low risk. The change is additive in the SecretField: the raw-value
password/textarea path is preserved and auto-opens whenever the stored
value is not a UUID, so existing SSH-key entries and unsaved raw values
are untouched.
- The new heuristic is "if `value` is a UUID, treat it as a bound
secret". A user who somehow pasted a UUID as a literal value (not as a
secret ref) would now see it rendered as a bound (possibly missing)
secret in the picker. The previous UI already treated UUID values as
opaque secret refs at save time (server converts UUIDs straight
through), so the runtime behavior is unchanged.
- Picker pulls company secrets via the existing `secretsApi.list` query.
No new endpoints, no migrations.

## Model Used

- Provider: Anthropic
- Model: Claude Opus 4.7 (`claude-opus-4-7`)
- Capabilities: tool use, extended reasoning
- Surfaced through: Claude Code via Paperclip heartbeat (issue PAPA-377)

## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots — deferred; user validated locally before opening the PR.
Will add if requested.
- [x] I have updated relevant documentation to reflect my changes (no
docs needed — internal behavior of an existing form field)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-18 21:17:41 -07:00
Devin Foley 5a64cf52a1 Add exe.dev sandbox provider plugin (#5688)
> _Stacked on top of #5685#5686#5687. Diff against master includes
commits from earlier PRs in the stack — review focuses on the two new
commits (`Add long-secret textarea variant to JsonSchemaForm
SecretField` + `Add exe.dev sandbox provider plugin`)._

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Each agent runs in a sandbox environment, and operators choose the
provider — today E2B, Daytona, and (in this stack) Cloudflare
> - exe.dev offers per-VM sandboxes via a small CLI / HTTP API — useful
for operators who want full Linux VMs (vs container/runtime-only
sandboxes)
> - The plugin shape mirrors the e2b plugin: lifecycle hooks (`new`,
`ls`, `rm`) drive exe.dev's CLI; SSH plumbing handles direct VM access
for adapters that need it
> - exe.dev VMs come up bare — `node` is not preinstalled, so the
Paperclip sandbox callback bridge (a Node script) needs Node 20
installed at VM init via `--setup-script`. The plugin defaults the setup
script to a Nodesource install
> - The auth field accepts long SSH private keys, which need a textarea
variant of the existing `SecretField` in `JsonSchemaForm` — added behind
a `maxLength > THRESHOLD` opt-in so other secret fields are unaffected
> - The benefit is that operators get exe.dev as a fully working sandbox
provider out of the box, with no manual VM provisioning required

## What Changed

**Shared UI support (`Add long-secret textarea variant to JsonSchemaForm
SecretField`):**

- `ui/src/components/JsonSchemaForm.tsx` + new
`JsonSchemaForm.test.tsx`: when a secret-formatted field declares
`maxLength` larger than the existing single-line threshold, render a
monospace textarea instead of the masked input. Short secrets (API keys,
tokens) keep the existing masked-input + show/hide toggle behavior.

**The exe.dev plugin (`Add exe.dev sandbox provider plugin`):**

- `packages/plugins/sandbox-providers/exe-dev/`: plugin entry, manifest,
plugin runtime, README, and 19-test Vitest suite.
- Manifest fields: API token (with `secret-ref` + `/exec` permission
notes — needs `new`, `ls`, `rm`), API URL override, optional SSH
username, optional SSH private key (uses the new `JsonSchemaForm`
textarea variant via `maxLength: 4096`), optional SSH identity-file
path, optional setup script.
- Default `--setup-script` is a Nodesource Node 20 install. exe.dev VMs
come up bare and the Paperclip sandbox callback bridge is a Node script,
so without Node preinstalled the bridge can't start. Operators can
override by supplying their own setup script.
- `runLifecycleCommand` redacts env values from the executed command
before surfacing it in error messages, so secrets passed via
`--env=KEY=VALUE` don't leak into operator-visible failures.
- The plugin distinguishes exe.dev's SSH onboarding failures (`Please
complete registration by running: ssh exe.dev`) from general SSH
failures and surfaces a clear remediation message.
- `scripts/release-package-manifest.json`: register the new plugin for
CI publish alongside the existing daytona / e2b providers.

## Verification

- `pnpm typecheck`
- `pnpm exec vitest run --no-coverage
ui/src/components/JsonSchemaForm.test.tsx`
- `(cd packages/plugins/sandbox-providers/exe-dev && pnpm test)` — 19
passing

For an operator-side smoke test:

1. Get an exe.dev API token with `/exec` permission for `new`, `ls`,
`rm`.
2. Register the plugin in your Paperclip instance, configure an
environment with the token.
3. Create a sandbox env whose provider is `exe-dev`, then run a Codex or
Claude job against it. The default Node 20 setup script should bring the
VM up automatically.

## Risks

- Adds a new sandbox provider plugin that follows the existing daytona /
e2b shape; behavior on existing providers is unchanged.
- The `JsonSchemaForm` textarea variant only engages for fields that opt
in via `maxLength` larger than the existing threshold. All existing
secret fields (which don't declare a `maxLength`) keep their current
rendering. Test coverage pins both paths.
- The redaction in `runLifecycleCommand` is a defense-in-depth measure;
the test suite exercises the redaction path. If the redaction misses a
future env-arg shape, the worst case is restored behavior (secrets in
error messages), which is what the existing daytona / e2b plugins also
do today.
- Default setup script downloads from `deb.nodesource.com` over HTTPS at
VM init. Operators on air-gapped networks or with a different package
strategy can override the setup script.

## Model Used

- Provider: Anthropic
- Model: Claude Opus 4.7 (1M context)
- Capabilities used: extended reasoning, tool use (Read/Edit/Bash/Grep)

## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots — UI change is a textarea variant of an existing secret
field; will attach screenshots before requesting merge
- [x] I have updated relevant documentation to reflect my changes
(plugin README, manifest descriptions)
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-11 07:42:18 -07:00