From 58ff7db6a21377b206d565462167b730aa0258e0 Mon Sep 17 00:00:00 2001 From: "Claude SSH (Paperclip agent)" Date: Sat, 23 May 2026 15:00:17 -0700 Subject: [PATCH] feat(ui): add advanced-options disclosure to JsonSchemaForm + simplify exe.dev manifest (PAPA-410, PAPA-411) - JsonSchemaForm now splits properties into essentials vs advanced based on the new x-paperclip-advanced keyword. Advanced fields render inside a collapsed disclosure, sub-grouped by x-paperclip-group; a validation error on a hidden field force-opens the disclosure. Backward compatible: schemas without the keyword render exactly as before. - Promote x-paperclip-advanced / x-paperclip-group to the shared JsonSchema type so plugin authors get autocomplete. - Rewrite the exe.dev manifest: apiKey and sshPrivateKey are the only essentials with plain-language descriptions; everything else moves under SSH access / VM resources / VM creation / API + runtime groups. - CompanyEnvironments: drop the 2-col grid wrapper so JsonSchemaForm controls its own layout. PAPA-410 and PAPA-411 are duplicate sub-issues of PAPA-407 with the same scope; the PAPA-418 reconciliation kept PAPA-410 canonical but the work covers both. Co-Authored-By: Claude Opus 4.7 --- .../sandbox-providers/exe-dev/src/manifest.ts | 194 +++++++++++------- packages/shared/src/types/plugin.ts | 18 +- ui/src/components/JsonSchemaForm.test.tsx | 146 +++++++++++++ ui/src/components/JsonSchemaForm.tsx | 148 +++++++++++-- ui/src/pages/CompanyEnvironments.tsx | 40 ++-- 5 files changed, 428 insertions(+), 118 deletions(-) diff --git a/packages/plugins/sandbox-providers/exe-dev/src/manifest.ts b/packages/plugins/sandbox-providers/exe-dev/src/manifest.ts index 8e71d6c7900..c8bf36d3441 100644 --- a/packages/plugins/sandbox-providers/exe-dev/src/manifest.ts +++ b/packages/plugins/sandbox-providers/exe-dev/src/manifest.ts @@ -26,106 +26,150 @@ const manifest: PaperclipPluginManifestV1 = { configSchema: { type: "object", properties: { + // ---- Essentials (always visible, in this order) ---- apiKey: { type: "string", format: "secret-ref", description: - "Environment-specific exe.dev API token. Needs `/exec` permission for at least `new`, `ls`, and `rm`. Paste a token or an existing Paperclip secret reference; saved environments store pasted values as company secrets. Falls back to EXE_API_KEY if omitted.", - }, - apiUrl: { - type: "string", - description: - "Optional exe.dev HTTPS API base URL or /exec endpoint. Defaults to https://exe.dev/exec.", - }, - namePrefix: { - type: "string", - description: "Optional prefix used when generating VM names.", - default: "paperclip", - }, - image: { - type: "string", - description: "Optional container image to use when creating the VM.", - }, - command: { - type: "string", - description: "Optional container command passed to `exe.dev new --command`.", - }, - cpu: { - type: "number", - description: "Optional CPU count passed to `exe.dev new --cpu`.", - }, - memory: { - type: "string", - description: "Optional memory size such as `4GB`.", - }, - disk: { - type: "string", - description: "Optional disk size such as `20GB`.", - }, - comment: { - type: "string", - description: "Optional short note attached to created VMs.", - }, - env: { - type: "object", - description: "Optional environment variables applied at VM creation time.", - additionalProperties: { type: "string" }, - }, - integrations: { - type: "array", - description: "Optional exe.dev integrations to attach during VM creation.", - items: { type: "string" }, - }, - tags: { - type: "array", - description: "Optional tags to apply during VM creation.", - items: { type: "string" }, - }, - setupScript: { - type: "string", - description: "Optional first-boot setup script passed to `exe.dev new --setup-script`.", - }, - prompt: { - type: "string", - description: "Optional Shelley prompt passed to `exe.dev new --prompt`.", - }, - timeoutMs: { - type: "number", - description: "Timeout for VM lifecycle and SSH operations in milliseconds.", - default: 300000, - }, - reuseLease: { - type: "boolean", - description: - "Whether to keep the VM alive between runs instead of deleting it on release.", - default: false, - }, - sshUser: { - type: "string", - description: "Optional SSH username for direct VM access.", + "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: 4096, description: - "Optional exe.dev-registered SSH private key. Paste the private key or an existing Paperclip secret reference; saved environments store pasted values as company secrets. If omitted, Paperclip falls back to sshIdentityFile, then the host's default SSH agent/keychain.", + "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: - "Optional absolute path to the SSH private key the Paperclip host should use for VM access when sshPrivateKey is omitted. Leave both blank to rely on the host's default SSH agent/keychain.", + "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", }, }, }, diff --git a/packages/shared/src/types/plugin.ts b/packages/shared/src/types/plugin.ts index f9330a48005..351b2dda0ee 100644 --- a/packages/shared/src/types/plugin.ts +++ b/packages/shared/src/types/plugin.ts @@ -38,8 +38,24 @@ import type { Routine, RoutineTrigger, RoutineVariable } from "./routine.js"; /** * A JSON Schema object used for plugin config schemas and tool parameter schemas. * Plugins provide these as plain JSON Schema compatible objects. + * + * The Paperclip extension keywords below are recognised by the Paperclip UI + * but are otherwise ignored by standard JSON Schema validators. */ -export type JsonSchema = Record; +export type JsonSchema = { + /** + * When true, the Paperclip config UI hides this property behind an + * "Advanced options" disclosure. Defaults to false (always visible). + */ + "x-paperclip-advanced"?: boolean; + /** + * Optional sub-section heading used to group advanced properties inside + * the disclosure (e.g. "SSH access", "VM resources"). Ignored when + * `x-paperclip-advanced` is not true. + */ + "x-paperclip-group"?: string; + [key: string]: unknown; +}; export type { PluginDatabaseCoreReadTable, diff --git a/ui/src/components/JsonSchemaForm.test.tsx b/ui/src/components/JsonSchemaForm.test.tsx index 458d2d19364..c438c3def2f 100644 --- a/ui/src/components/JsonSchemaForm.test.tsx +++ b/ui/src/components/JsonSchemaForm.test.tsx @@ -204,6 +204,152 @@ describe("JsonSchemaForm secret-ref rendering", () => { }); }); + it("renders no Advanced disclosure when no field opts in", async () => { + const root = createRoot(container); + + await act(async () => { + root.render( + {}} + />, + ); + }); + + // No disclosure button should be present in the passthrough case. + const buttons = Array.from(container.querySelectorAll("button")); + const advancedButton = buttons.find((b) => + b.textContent?.includes("Advanced options"), + ); + expect(advancedButton).toBeUndefined(); + + // Both fields render in the flat layout: the secret picker (rendered as + // a -
- {selectedSandboxProvider?.description ? ( -
- {selectedSandboxProvider.description} -
- ) : null} - {selectedSandboxSchema ? ( - - setEnvironmentForm((current) => ({ ...current, sandboxConfig: values }))} - errors={sandboxConfigErrors} - /> - ) : ( -
- This provider does not declare additional configuration fields. -
- )} -
+ {selectedSandboxProvider?.description ? ( +
+ {selectedSandboxProvider.description} +
+ ) : null} + {selectedSandboxSchema ? ( + + setEnvironmentForm((current) => ({ ...current, sandboxConfig: values }))} + errors={sandboxConfigErrors} + /> + ) : ( +
+ This provider does not declare additional configuration fields. +
+ )} ) : null}