From adfbe2d4b93349991bab0ca984baed39dd9fb1d2 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Fri, 21 Aug 2026 12:51:22 -0700 Subject: [PATCH] feat(environments): refer to the managed default environment by name, not the sandbox driver key (#11838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Managed deployments provision a platform-managed default environment for agent runs; the UI shows this environment in selectors, the agent form, run details, and the environments page > - Those surfaces append the raw driver key to the environment name, so users see labels like "Paperclip Computer (sandbox)", "Paperclip Computer · sandbox", and fallback copy such as "Managed sandbox" and "The sandbox has no ready authentication" > - "sandbox" is infrastructure vocabulary, not the product name of the environment; showing it next to the managed environment's name is confusing and off-brand > - This pull request renders platform-managed environments by name alone and rewords the sandbox-phrased copy, while user-created environments keep the driver suffix so mixed lists stay distinguishable > - The benefit is that the default environment reads as one clear product name everywhere, and self-hosted users lose nothing: their own environments still show the driver ## Linked Issues or Issue Description **What existing behavior does this improve?** Display of the platform-managed default environment across the UI. **Subsystem affected** UI (environment selectors, agent config form, environments page, agents page, run details) and the claude-local/codex-local adapter auth checks. **Current behavior** The agent form labels the inherited default environment as "Name (sandbox)". Environment selectors and the environments list render "Name · sandbox". The agents page describes the environment as " sandbox provider". The agent form's fallback label is "Managed sandbox". Adapter auth checks say "The sandbox has no ready authentication for this adapter." **Proposed behavior** Platform-managed environment rows (`metadata.managedByPaperclip`) render their name alone. The fallback label is "Paperclip Computer". The agents page describes managed environments as "Managed by Paperclip". Run details omit the driver suffix for sandbox-driver environments (the adjacent Provider entry already identifies the mechanism). Adapter auth checks say "This environment has no ready authentication for this adapter." **Reason and benefit** The managed environment carries a product name. Appending the raw driver key ("sandbox") to it is noise and contradicts the product naming. User-created environments keep the driver suffix, so mixed lists stay distinguishable. **Breaking changes** None. Message text of the auth check is not read programmatically; the UI keys off `ADAPTER_AUTH_MISSING_CHECK_CODE`. Rows without the managed marker render exactly as before. ## What Changed - New `environmentDisplayLabel` helper in `ui/src/lib/managed-sandbox-environment.ts`: managed rows → name alone; other rows → "Name · driver". - `AgentConfigForm`: inherited-default label uses the helper; fallback copy "Managed sandbox" → "Paperclip Computer"; environment options use the helper. - `ProjectProperties`, `CompanyEnvironments`: environment selector options use the helper; the environments-list row hides the driver suffix on managed rows; the managed detail page's fallback description no longer says "sandbox". - `Agents` page: managed environments are described as "Managed by Paperclip" instead of " sandbox provider". - `CommentThread` run details: the driver suffix is omitted for sandbox-driver environments. - claude-local and codex-local adapters: auth-missing check message/hint reworded from "sandbox" to "environment" (ACP and environment-test paths); claude-local probe/effort/login hints reworded the same way. - Run status lines: "Syncing workspace to sandbox", "Exporting git changes from sandbox", "Starting adapter in sandbox", and friends now say "environment"; "Finalizing sandbox workspace" → "Finalizing workspace". Templated transfer-progress lines map the `sandbox` transport key to "environment" for display (`runtime-progress.ts`). - Agent form sign-in panel: "Sign in to the sandbox" → "Sign in to the environment"; "Authenticated. The sandbox has credentials now." → "…The environment has credentials now." - Feature catalog + instance settings card: "Managed Sandbox Only" → "Managed Environment Only" (setting key unchanged; the card keeps its alphabetical slot). - Server agents routes: execution-target failure and test-identity copy no longer say "sandbox"; workspace-mode label "Cloud sandbox" → "Cloud environment". - Tests: new `environmentDisplayLabel` unit cases; new `AgentConfigForm` render case asserting the managed default renders without "(sandbox)" or "· sandbox"; status-line assertions updated across adapter-utils, server heartbeat/live-run, and UI chat suites. ## Verification - `pnpm --filter @paperclipai/ui typecheck` — clean. - `pnpm --filter @paperclipai/adapter-claude-local typecheck` and `--filter @paperclipai/adapter-codex-local typecheck` — clean. - `vitest run` for `managed-sandbox-environment.test.ts`, `AgentConfigForm.render.test.tsx`, `CompanyEnvironments.test.tsx`, `Agents.test.tsx`, `CommentThread.test.tsx`, `NewAgent.test.tsx` — all green (118 tests across the two runs). ## Risks Low risk. Cosmetic label changes only; no data or API changes. Rows without `metadata.managedByPaperclip` render exactly as before, so self-hosted deployments with their own environments see no change. The only self-hosted-visible wording changes are the adapter auth-check message and the driver suffix omission on sandbox-driver rows in run details. ## Model Used - Claude (Anthropic) — claude-fable-5 (Claude Fable 5), Claude Code CLI, extended thinking, tool use. ## 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/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] 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 (no docs reference these labels) - [x] I have considered and documented any 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 --- .../settlement-characterization.test.ts | 8 +++--- .../adapter-utils/src/execution-target.ts | 2 +- .../src/runtime-progress.test.ts | 12 ++++----- .../adapter-utils/src/runtime-progress.ts | 6 ++++- .../src/sandbox-managed-runtime.test.ts | 26 +++++++++---------- .../src/sandbox-managed-runtime.ts | 14 +++++----- .../adapters/claude-local/src/server/acp.ts | 6 ++--- .../claude-local/src/server/claude-config.ts | 6 ++--- .../claude-local/src/server/execute.ts | 2 +- .../adapters/claude-local/src/server/test.ts | 8 +++--- .../adapters/codex-local/src/server/acp.ts | 4 +-- .../adapters/codex-local/src/server/test.ts | 4 +-- packages/shared/src/feature-catalog.ts | 4 +-- .../__tests__/agent-live-run-routes.test.ts | 4 +-- .../agent-test-environment-routes.test.ts | 2 +- .../__tests__/heartbeat-runtime-state.test.ts | 12 ++++----- server/src/routes/agents.ts | 6 ++--- .../heartbeat-run-runtime-status.test.ts | 2 +- .../AgentConfigForm.render.test.tsx | 21 +++++++++++++++ ui/src/components/AgentConfigForm.tsx | 15 ++++++----- ui/src/components/CommentThread.tsx | 7 ++++- ui/src/components/IssueChatThread.test.tsx | 2 +- ui/src/components/IssueWorkspaceCard.tsx | 2 +- ui/src/components/ProjectProperties.tsx | 6 ++--- ui/src/components/TaskChatThread.test.tsx | 4 +-- ui/src/components/TaskChatThread.tsx | 2 +- ui/src/lib/issue-chat-messages.test.ts | 4 +-- .../lib/managed-sandbox-environment.test.ts | 17 ++++++++++++ ui/src/lib/managed-sandbox-environment.ts | 14 ++++++++++ ui/src/pages/Agents.tsx | 13 ++++++---- ui/src/pages/CompanyEnvironments.tsx | 20 ++++++++++---- ui/src/pages/InstanceExperimentalSettings.tsx | 6 ++--- 32 files changed, 168 insertions(+), 93 deletions(-) diff --git a/packages/adapter-utils/src/acpx-engine/settlement-characterization.test.ts b/packages/adapter-utils/src/acpx-engine/settlement-characterization.test.ts index eb4aba4b1c..d546a956de 100644 --- a/packages/adapter-utils/src/acpx-engine/settlement-characterization.test.ts +++ b/packages/adapter-utils/src/acpx-engine/settlement-characterization.test.ts @@ -890,12 +890,12 @@ describe("ACP settlement — Layer B: restoreWorkspace order + native-sync selec // Phase order: config_sync (staging) → restore → finalize (finalize last). expect(runtimeStatuses).toEqual( expect.arrayContaining([ - "config_sync:Syncing workspace to sandbox", - "restore:Restoring workspace from sandbox", - "finalize:Finalizing sandbox workspace", + "config_sync:Syncing workspace to environment", + "restore:Restoring workspace from environment", + "finalize:Finalizing workspace", ]), ); - expect(runtimeStatuses.at(-1)).toBe("finalize:Finalizing sandbox workspace"); + expect(runtimeStatuses.at(-1)).toBe("finalize:Finalizing workspace"); }); it("test_git_backed_workspace_restore_phase_order", async () => { diff --git a/packages/adapter-utils/src/execution-target.ts b/packages/adapter-utils/src/execution-target.ts index 8c1aba3634..aa3c1e4064 100644 --- a/packages/adapter-utils/src/execution-target.ts +++ b/packages/adapter-utils/src/execution-target.ts @@ -644,7 +644,7 @@ export async function runAdapterExecutionTargetProcess( const env = sanitizeRemoteExecutionEnv(options.env); await options.onRuntimeProgress?.({ phase: "adapter_startup", - message: "Starting adapter in sandbox", + message: "Starting adapter in environment", }); const runLogTail = options.runLogTail?.create() ?? null; let execCommand = command; diff --git a/packages/adapter-utils/src/runtime-progress.test.ts b/packages/adapter-utils/src/runtime-progress.test.ts index 24048404e7..ef3f4b8c94 100644 --- a/packages/adapter-utils/src/runtime-progress.test.ts +++ b/packages/adapter-utils/src/runtime-progress.test.ts @@ -28,7 +28,7 @@ describe("createRuntimeProgressReporter", () => { await reporter.report(12.6 * MB, 31.4 * MB); - expect(lines).toEqual(["[paperclip] Syncing workspace to sandbox: 40% (12.6/31.4 MB)\n"]); + expect(lines).toEqual(["[paperclip] Syncing workspace to environment: 40% (12.6/31.4 MB)\n"]); }); it("omits the label when none is provided (e.g. git history)", async () => { @@ -68,7 +68,7 @@ describe("createRuntimeProgressReporter", () => { await reporter.report(5 * MB, 100 * MB); // 5% expect(lines).toHaveLength(1); - expect(lines[0]).toBe("[paperclip] Syncing workspace to sandbox: 1% (1.0/100.0 MB)\n"); + expect(lines[0]).toBe("[paperclip] Syncing workspace to environment: 1% (1.0/100.0 MB)\n"); }); it("emits when the percentage crosses a 10% step", async () => { @@ -89,7 +89,7 @@ describe("createRuntimeProgressReporter", () => { await reporter.report(15 * MB, 100 * MB); // 15% -> crosses into step 1 -> emit expect(lines).toHaveLength(2); - expect(lines[1]).toBe("[paperclip] Syncing workspace to sandbox: 15% (15.0/100.0 MB)\n"); + expect(lines[1]).toBe("[paperclip] Syncing workspace to environment: 15% (15.0/100.0 MB)\n"); }); it("emits on the time threshold even without a step crossing", async () => { @@ -112,7 +112,7 @@ describe("createRuntimeProgressReporter", () => { await reporter.report(3 * MB, 100 * MB); // 3% same step, but 2s elapsed -> emit expect(lines).toHaveLength(2); - expect(lines[1]).toBe("[paperclip] Syncing workspace to sandbox: 3% (3.0/100.0 MB)\n"); + expect(lines[1]).toBe("[paperclip] Syncing workspace to environment: 3% (3.0/100.0 MB)\n"); }); it("always emits the terminal 100% line via report reaching the total", async () => { @@ -133,7 +133,7 @@ describe("createRuntimeProgressReporter", () => { await reporter.report(100 * MB, 100 * MB); // terminal -> always emit expect(lines[lines.length - 1]).toBe( - "[paperclip] Restoring workspace from sandbox: 100% (100.0/100.0 MB)\n", + "[paperclip] Restoring workspace from environment: 100% (100.0/100.0 MB)\n", ); }); @@ -156,7 +156,7 @@ describe("createRuntimeProgressReporter", () => { await reporter.complete(); expect(lines[lines.length - 1]).toBe( - "[paperclip] Syncing workspace to sandbox: 100% (100.0/100.0 MB)\n", + "[paperclip] Syncing workspace to environment: 100% (100.0/100.0 MB)\n", ); }); diff --git a/packages/adapter-utils/src/runtime-progress.ts b/packages/adapter-utils/src/runtime-progress.ts index e217927675..46c8497bae 100644 --- a/packages/adapter-utils/src/runtime-progress.ts +++ b/packages/adapter-utils/src/runtime-progress.ts @@ -91,7 +91,11 @@ export function createRuntimeProgressReporter( const minIntervalMs = options.minIntervalMs && options.minIntervalMs > 0 ? options.minIntervalMs : 2000; const now = options.now ?? Date.now; - const prefix = `[paperclip] ${options.phase}${options.label ? ` ${options.label}` : ""} ${options.direction} ${options.target}`; + // "sandbox" is the transport key, not product vocabulary: progress lines are + // user-visible run status, and the product refers to the run's machine as an + // environment ("Paperclip Computer" on managed deployments). + const targetDisplay = options.target === "sandbox" ? "environment" : options.target; + const prefix = `[paperclip] ${options.phase}${options.label ? ` ${options.label}` : ""} ${options.direction} ${targetDisplay}`; let lastEmitAt: number | null = null; let lastStep = -1; diff --git a/packages/adapter-utils/src/sandbox-managed-runtime.test.ts b/packages/adapter-utils/src/sandbox-managed-runtime.test.ts index 49101dc73b..8e60c71fb0 100644 --- a/packages/adapter-utils/src/sandbox-managed-runtime.test.ts +++ b/packages/adapter-utils/src/sandbox-managed-runtime.test.ts @@ -427,17 +427,17 @@ describe("sandbox managed runtime", () => { await expect(readFile(path.join(localWorkspaceDir, ".claude", "settings.json"), "utf8")).resolves.toBe("{\"local\":true}\n"); await expect(readFile(path.join(localWorkspaceDir, ".paperclip-runtime", "state.json"), "utf8")).resolves.toBe("{}\n"); expect(runtimeStatuses).toEqual(expect.arrayContaining([ - "config_sync:Syncing workspace to sandbox", - "config_sync:Syncing runtime assets to sandbox", - "restore:Restoring workspace from sandbox", - "finalize:Finalizing sandbox workspace", + "config_sync:Syncing workspace to environment", + "config_sync:Syncing runtime assets to environment", + "restore:Restoring workspace from environment", + "finalize:Finalizing workspace", ])); expect(runtimeStatuses).toEqual(expect.arrayContaining([ - expect.stringMatching(/^config_sync:Syncing workspace to sandbox: 100% \(\d+\.\d\/\d+\.\d MB\)$/), - expect.stringMatching(/^config_sync:Syncing skills to sandbox: 100% \(\d+\.\d\/\d+\.\d MB\)$/), - expect.stringMatching(/^restore:Restoring workspace from sandbox: 100% \(\d+\.\d\/\d+\.\d MB\)$/), + expect.stringMatching(/^config_sync:Syncing workspace to environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/), + expect.stringMatching(/^config_sync:Syncing skills to environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/), + expect.stringMatching(/^restore:Restoring workspace from environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/), ])); - expect(runtimeStatuses.at(-1)).toBe("finalize:Finalizing sandbox workspace"); + expect(runtimeStatuses.at(-1)).toBe("finalize:Finalizing workspace"); }); it.each(["workspace", "git-workspace"])( @@ -645,11 +645,11 @@ describe("sandbox managed runtime", () => { // check above). expect(runtimeStatuses.some((status) => ( status.phase === "config_sync" && - /^Syncing workspace to sandbox: 100% \(\d+\.\d\/\d+\.\d MB\)$/.test(status.message) + /^Syncing workspace to environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/.test(status.message) ))).toBe(true); expect(runtimeStatuses.some((status) => ( status.phase === "export" && - /^Exporting git history from sandbox: 100% \(\d+\.\d\/\d+\.\d MB\)$/.test(status.message) + /^Exporting git history from environment: 100% \(\d+\.\d\/\d+\.\d MB\)$/.test(status.message) ))).toBe(true); }); @@ -1109,8 +1109,8 @@ describe("sandbox managed runtime", () => { }, }); - const uploadWorkspaceLines = lines.filter((line) => line.includes("Syncing workspace to sandbox")); - const uploadAssetLines = lines.filter((line) => line.includes("Syncing skills to sandbox")); + const uploadWorkspaceLines = lines.filter((line) => line.includes("Syncing workspace to environment")); + const uploadAssetLines = lines.filter((line) => line.includes("Syncing skills to environment")); expect(uploadWorkspaceLines.length).toBeGreaterThan(0); expect(uploadAssetLines.length).toBeGreaterThan(0); // 100 reported increments must be throttled to at most ~one line per 10% step. @@ -1120,7 +1120,7 @@ describe("sandbox managed runtime", () => { expect(uploadWorkspaceLines.every((line) => /\(\d+\.\d\/\d+\.\d MB\)/.test(line))).toBe(true); await prepared.restoreWorkspace(); - const restoreLines = lines.filter((line) => line.includes("Restoring workspace from sandbox")); + const restoreLines = lines.filter((line) => line.includes("Restoring workspace from environment")); expect(restoreLines.length).toBeGreaterThan(0); expect(restoreLines.length).toBeLessThanOrEqual(11); expect(restoreLines.some((line) => line.includes("100%"))).toBe(true); diff --git a/packages/adapter-utils/src/sandbox-managed-runtime.ts b/packages/adapter-utils/src/sandbox-managed-runtime.ts index 176adcfb90..26196a474f 100644 --- a/packages/adapter-utils/src/sandbox-managed-runtime.ts +++ b/packages/adapter-utils/src/sandbox-managed-runtime.ts @@ -959,7 +959,7 @@ export async function prepareSandboxManagedRuntime(input: { // wipes the target tree EXCEPT `.paperclip-runtime`, so the overlay tar, // which sits under `.paperclip-runtime`, survives to run its own extract. if (gitSnapshot) { - await emitRuntimeStatus(input.onRuntimeProgress, "git_sync", "Syncing git history to sandbox"); + await emitRuntimeStatus(input.onRuntimeProgress, "git_sync", "Syncing git history to environment"); const gitTarPath = path.join(tempDir, "git-workspace.tar"); const remoteGitTar = path.posix.join(runtimeRootDir, "git-workspace-upload.tar"); await withShallowGitWorkspaceClone({ @@ -986,7 +986,7 @@ export async function prepareSandboxManagedRuntime(input: { // 2. workspace-overlay tar. A git-backed overlay merges on top of the just // extracted git tree (no wipe); a plain workspace wipes every child except // the preserved names first. The extract runs AFTER the git extract. - await emitRuntimeStatus(input.onRuntimeProgress, "config_sync", "Syncing workspace to sandbox"); + await emitRuntimeStatus(input.onRuntimeProgress, "config_sync", "Syncing workspace to environment"); const workspaceTarPath = path.join(tempDir, "workspace.tar"); const workspaceArchiveDir = gitSnapshot ? path.join(tempDir, "workspace-overlay") : input.workspaceLocalDir; if (gitSnapshot) { @@ -1043,7 +1043,7 @@ export async function prepareSandboxManagedRuntime(input: { inboundTaskIsRequired.push(true); inboundTasks.push(() => runStepSpan(`stage.asset.${asset.key}`, async () => { - await emitRuntimeStatus(input.onRuntimeProgress, "config_sync", "Syncing runtime assets to sandbox"); + await emitRuntimeStatus(input.onRuntimeProgress, "config_sync", "Syncing runtime assets to environment"); const remoteAssetDir = path.posix.join(runtimeRootDir, asset.key); const remoteAssetTar = path.posix.join(runtimeRootDir, `${asset.key}-upload.tar`); // Every asset — default OR custom-provisioned (e.g. an adapter credential @@ -1137,7 +1137,7 @@ export async function prepareSandboxManagedRuntime(input: { throw new Error(`additional source projectId is not a simple path segment: ${projectId}`); } const remoteProjectDir = path.posix.join(runtimeRootDir, label); - await emitRuntimeStatus(input.onRuntimeProgress, "config_sync", "Syncing referenced project to sandbox"); + await emitRuntimeStatus(input.onRuntimeProgress, "config_sync", "Syncing referenced project to environment"); await stageConfinedSyncIn({ files: [{ sourcePath: localPath, @@ -1237,7 +1237,7 @@ export async function prepareSandboxManagedRuntime(input: { let remoteWorkspaceStatus = "dirty"; try { if (gitSnapshot) { - await emitRuntimeStatus(input.onRuntimeProgress, "export", "Exporting git changes from sandbox"); + await emitRuntimeStatus(input.onRuntimeProgress, "export", "Exporting git changes from environment"); importedRef = createImportedGitRef("sandbox"); const remoteGitBundle = path.posix.join(runtimeRootDir, "git-delta.bundle"); const remoteWorkspaceStatusPath = path.posix.join(runtimeRootDir, "workspace-status.txt"); @@ -1323,7 +1323,7 @@ export async function prepareSandboxManagedRuntime(input: { } } - await emitRuntimeStatus(input.onRuntimeProgress, "restore", "Restoring workspace from sandbox"); + await emitRuntimeStatus(input.onRuntimeProgress, "restore", "Restoring workspace from environment"); const extractedDir = path.join(tempDir, "workspace"); if (nativeSyncOut) { // Native outbound: the provider materializes the sandbox workspace into @@ -1405,7 +1405,7 @@ export async function prepareSandboxManagedRuntime(input: { : undefined, }); } finally { - await emitRuntimeStatus(input.onRuntimeProgress, "finalize", "Finalizing sandbox workspace"); + await emitRuntimeStatus(input.onRuntimeProgress, "finalize", "Finalizing workspace"); if (importedRef) { await deleteLocalGitRef({ localDir: input.workspaceLocalDir, ref: importedRef }); } diff --git a/packages/adapters/claude-local/src/server/acp.ts b/packages/adapters/claude-local/src/server/acp.ts index 449e495b9d..721e2c39ab 100644 --- a/packages/adapters/claude-local/src/server/acp.ts +++ b/packages/adapters/claude-local/src/server/acp.ts @@ -520,8 +520,8 @@ function buildAcpAuthMissingChecks(input: { checks.push({ code: ADAPTER_AUTH_MISSING_CHECK_CODE, level: "warn", - message: "The sandbox has no ready authentication for this adapter.", - hint: "Provide credentials for this adapter, or start login in the sandbox.", + message: "This environment has no ready authentication for this adapter.", + hint: "Provide credentials for this adapter, or start login in the environment.", }); } return checks; @@ -544,7 +544,7 @@ function buildAcpLoginProbeUnavailableCheck( level: "warn", message, hint: targetIsSandbox - ? "Verify that the sandbox can run `claude` and retry the Test. Set engine=cli to use the Claude CLI lane." + ? "Verify that the environment can run `claude` and retry the Test. Set engine=cli to use the Claude CLI lane." : "Verify that `claude` can run in this environment and retry the Test. Set engine=cli to use the Claude CLI lane.", }; } diff --git a/packages/adapters/claude-local/src/server/claude-config.ts b/packages/adapters/claude-local/src/server/claude-config.ts index a95f7f7629..2195a00544 100644 --- a/packages/adapters/claude-local/src/server/claude-config.ts +++ b/packages/adapters/claude-local/src/server/claude-config.ts @@ -345,7 +345,7 @@ export async function prepareSandboxClaudeProbeRuntime(input: { checks.push({ code: "claude_managed_config_dir", level: "info", - message: "Sandbox probe is using Paperclip-managed Claude config materialization.", + message: "The environment probe is using Paperclip-managed Claude config materialization.", detail: remoteClaudeConfigDir, }); } catch (err) { @@ -353,14 +353,14 @@ export async function prepareSandboxClaudeProbeRuntime(input: { // only the fixed context, the allowlisted classification, and a safe // error class name. logSandboxProbeDiagnostic( - "Could not materialize Paperclip-managed Claude config for the sandbox probe", + "Could not materialize Paperclip-managed Claude config for the environment probe", "spawn_error", { errorClass: classifyThrownErrorClass(err) }, ); checks.push({ code: "claude_managed_config_dir_failed", level: "error", - message: "Could not materialize Paperclip-managed Claude config for the sandbox probe.", + message: "Could not materialize Paperclip-managed Claude config for the environment probe.", hint: "Retry the Test. If the failure repeats, check the server log for the redacted diagnostic.", }); } finally { diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index d0cbbe8140..6dcdd81045 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -715,7 +715,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise { it("includes ephemeral current status fields on active run polling", async () => { mockHeartbeatService.decorateActiveRunStatus.mockImplementation((run) => ({ ...run, - currentStatusMessage: "Syncing workspace to sandbox", + currentStatusMessage: "Syncing workspace to environment", currentStatusUpdatedAt: new Date("2026-04-10T09:30:05.000Z"), currentToolName: "bash", lastAssistantSnippet: "Inspecting files", @@ -341,7 +341,7 @@ describe("agent live run routes", () => { { companyId: "company-1", issueId: "issue-1" }, ); expect(res.body).toMatchObject({ - currentStatusMessage: "Syncing workspace to sandbox", + currentStatusMessage: "Syncing workspace to environment", currentStatusUpdatedAt: "2026-04-10T09:30:05.000Z", currentToolName: "bash", lastAssistantSnippet: "Inspecting files", diff --git a/server/src/__tests__/agent-test-environment-routes.test.ts b/server/src/__tests__/agent-test-environment-routes.test.ts index e263c8d03c..699785dc11 100644 --- a/server/src/__tests__/agent-test-environment-routes.test.ts +++ b/server/src/__tests__/agent-test-environment-routes.test.ts @@ -314,7 +314,7 @@ describe("agent test-environment route", () => { expect.objectContaining({ code: "sandbox_test_identity", level: "info", - message: 'Sandbox test identity for "Sandbox QA".', + message: 'Environment test identity for "Sandbox QA".', detail: expect.stringContaining("paperclipLeaseId=lease-1"), }), expect.objectContaining({ diff --git a/server/src/__tests__/heartbeat-runtime-state.test.ts b/server/src/__tests__/heartbeat-runtime-state.test.ts index 24ae9173f1..50990f6b38 100644 --- a/server/src/__tests__/heartbeat-runtime-state.test.ts +++ b/server/src/__tests__/heartbeat-runtime-state.test.ts @@ -147,7 +147,7 @@ describeEmbeddedPostgres("heartbeat runtime state deduplication", () => { const heartbeat = heartbeatService(db); const status = await heartbeat.recordRuntimeProgress(run, { phase: "config_sync", - message: "Syncing workspace to sandbox", + message: "Syncing workspace to environment", currentToolName: "bash", lastAssistantSnippet: "Inspecting the repository", lastEventAt: "2026-06-24T00:00:05.000Z", @@ -159,7 +159,7 @@ describeEmbeddedPostgres("heartbeat runtime state deduplication", () => { agentId, runId, phase: "config_sync", - message: "Syncing workspace to sandbox", + message: "Syncing workspace to environment", currentToolName: "bash", lastAssistantSnippet: "Inspecting the repository", lastEventAt: new Date("2026-06-24T00:00:05.000Z"), @@ -171,7 +171,7 @@ describeEmbeddedPostgres("heartbeat runtime state deduplication", () => { issueId, status: "running", })).toMatchObject({ - currentStatusMessage: "Syncing workspace to sandbox", + currentStatusMessage: "Syncing workspace to environment", currentToolName: "bash", lastAssistantSnippet: "Inspecting the repository", lastEventAt: new Date("2026-06-24T00:00:05.000Z"), @@ -184,7 +184,7 @@ describeEmbeddedPostgres("heartbeat runtime state deduplication", () => { agentId, issueId, phase: "config_sync", - message: "Syncing workspace to sandbox", + message: "Syncing workspace to environment", currentToolName: "bash", lastAssistantSnippet: "Inspecting the repository", lastEventAt: "2026-06-24T00:00:05.000Z", @@ -245,7 +245,7 @@ describeEmbeddedPostgres("heartbeat runtime state deduplication", () => { const heartbeat = heartbeatService(db); await heartbeat.recordRuntimeProgress(staleRunningRun, { phase: "config_sync", - message: "Syncing workspace to sandbox", + message: "Syncing workspace to environment", }, issueId); expect(getHeartbeatRunRuntimeStatus(runId)).toMatchObject({ @@ -265,7 +265,7 @@ describeEmbeddedPostgres("heartbeat runtime state deduplication", () => { const lateStatus = await heartbeat.recordRuntimeProgress(staleRunningRun, { phase: "finalize", - message: "Finalizing sandbox workspace", + message: "Finalizing workspace", }, issueId); expect(lateStatus).toBeNull(); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 5b8d1b823c..2803a17a40 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -893,7 +893,7 @@ export function agentRoutes( { code: "environment_target_failed", level: "error", - message: `Could not resolve a sandbox execution target for "${environment.name}".`, + message: `Could not resolve an execution target for "${environment.name}".`, detail: err instanceof Error ? err.message : String(err), }, ], @@ -985,9 +985,9 @@ export function agentRoutes( return { code: "sandbox_test_identity", level: "info", - message: `Sandbox test identity for "${input.environmentName}".`, + message: `Environment test identity for "${input.environmentName}".`, detail: detailParts.join("; "), - hint: "Use these provider-neutral IDs when comparing model-test output with provider logs or refreshed sandbox snapshots.", + hint: "Use these provider-neutral IDs when comparing model-test output with provider logs or refreshed environment snapshots.", }; } diff --git a/server/src/services/heartbeat-run-runtime-status.test.ts b/server/src/services/heartbeat-run-runtime-status.test.ts index f603900127..80984155ee 100644 --- a/server/src/services/heartbeat-run-runtime-status.test.ts +++ b/server/src/services/heartbeat-run-runtime-status.test.ts @@ -64,7 +64,7 @@ describe("heartbeat run runtime status store", () => { agentId: "agent-1", runId: "run-1", phase: "finalize", - message: "Finalizing sandbox workspace", + message: "Finalizing workspace", }); expect(clearHeartbeatRunRuntimeStatus("run-1")).toBe(true); diff --git a/ui/src/components/AgentConfigForm.render.test.tsx b/ui/src/components/AgentConfigForm.render.test.tsx index 94a63dfdcb..d9664a98eb 100644 --- a/ui/src/components/AgentConfigForm.render.test.tsx +++ b/ui/src/components/AgentConfigForm.render.test.tsx @@ -787,6 +787,27 @@ describe("AgentConfigForm environment selector", () => { expect(selector?.textContent).toContain("Fake Sandbox · sandbox"); }); + it("labels the platform-managed instance default by name, without the driver key", async () => { + mockInstanceSettingsApi.get.mockResolvedValue({ defaultEnvironmentId: "managed-1" }); + const result = await renderForm([ + makeEnvironment({ + id: "managed-1", + name: "Paperclip Computer", + driver: "sandbox", + config: { provider: "daytona" }, + metadata: { managedByPaperclip: true }, + }), + ]); + roots.push(result.root); + + const selector = result.container.querySelector("select"); + + expect(selector?.textContent).toContain("Default: Paperclip Computer"); + expect(selector?.textContent).toContain("Paperclip Computer"); + expect(selector?.textContent).not.toContain("(sandbox)"); + expect(selector?.textContent).not.toContain("· sandbox"); + }); + it("renders non-local adapter config fields in the Adapter card", async () => { const result = await renderForm( [makeEnvironment({ id: "local-1", name: "Local", driver: "local" })], diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 50e3eadd25..496da7f9be 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -37,6 +37,7 @@ import { resolveLocalDefaultEnvironmentId, resolveManagedSandboxEnvironmentId, } from "../lib/adapter-test-environment"; +import { environmentDisplayLabel } from "../lib/managed-sandbox-environment"; import { extractModelName, extractProviderId } from "../lib/model-utils"; import { queryKeys } from "../lib/queryKeys"; import { useCompany } from "../context/CompanyContext"; @@ -679,9 +680,9 @@ export function AgentConfigForm(props: AgentConfigFormProps) { ); const managedSandboxOnly = experimentalSettings?.enableManagedSandboxOnly === true; const inheritedEnvironmentLabel = instanceDefaultEnvironment - ? `${instanceDefaultEnvironment.name} (${instanceDefaultEnvironment.driver})` + ? environmentDisplayLabel(instanceDefaultEnvironment) : managedSandboxOnly - ? "Managed sandbox" + ? "Paperclip Computer" : "Local"; // Fetch adapter models for the effective adapter type @@ -1484,7 +1485,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { {environmentOptions.map((environment) => ( ))} @@ -2052,7 +2053,7 @@ function AdapterLoginTerminalState({ return (
- Authenticated. The sandbox has credentials now. + Authenticated. The environment has credentials now.
); } @@ -2180,7 +2181,7 @@ function DisplayedCodeLoginPanel({ return (
- Sign in to the sandbox + Sign in to the environment
{isActive && (
)} diff --git a/ui/src/components/CommentThread.tsx b/ui/src/components/CommentThread.tsx index 9181d86ede..0557c37ee9 100644 --- a/ui/src/components/CommentThread.tsx +++ b/ui/src/components/CommentThread.tsx @@ -656,7 +656,12 @@ const TimelineList = memo(function TimelineList({ {run.environment ? ( Environment {run.environment.name} - · {run.environment.driver} + {/* The raw "sandbox" driver key stays off run details — the + environment's name and the Provider entry below already + identify it; other drivers (ssh, local) remain useful. */} + {run.environment.driver !== "sandbox" ? ( + · {run.environment.driver} + ) : null} ) : null} {run.environmentLease?.provider ? ( diff --git a/ui/src/components/IssueChatThread.test.tsx b/ui/src/components/IssueChatThread.test.tsx index ac548ba055..2fbba554b0 100644 --- a/ui/src/components/IssueChatThread.test.tsx +++ b/ui/src/components/IssueChatThread.test.tsx @@ -3828,7 +3828,7 @@ describe("IssueChatThread", () => { agentId: "agent-1", agentName: "Agent 1", adapterType: "codex_local", - currentStatusMessage: "Syncing git worktree to sandbox", + currentStatusMessage: "Syncing git worktree to environment", currentStatusUpdatedAt: "2026-04-06T12:00:05.000Z", currentToolName: "bash", lastEventAt: new Date(Date.now() - 2000).toISOString(), diff --git a/ui/src/components/IssueWorkspaceCard.tsx b/ui/src/components/IssueWorkspaceCard.tsx index 9d66058e93..4f2e5921ab 100644 --- a/ui/src/components/IssueWorkspaceCard.tsx +++ b/ui/src/components/IssueWorkspaceCard.tsx @@ -97,7 +97,7 @@ function workspaceModeLabel(mode: string | null | undefined) { switch (mode) { case "isolated_workspace": return "Isolated workspace"; case "operator_branch": return "Operator branch"; - case "cloud_sandbox": return "Cloud sandbox"; + case "cloud_sandbox": return "Cloud environment"; case "adapter_managed": return "Adapter managed"; default: return "Workspace"; } diff --git a/ui/src/components/ProjectProperties.tsx b/ui/src/components/ProjectProperties.tsx index 76ed0faeaa..1f8c95d24c 100644 --- a/ui/src/components/ProjectProperties.tsx +++ b/ui/src/components/ProjectProperties.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { filterManagedSandboxSelectableEnvironments } from "@/lib/managed-sandbox-environment"; +import { environmentDisplayLabel, filterManagedSandboxSelectableEnvironments } from "@/lib/managed-sandbox-environment"; import { Link } from "@/lib/router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { Project, SharedWorkspaceConcurrency } from "@paperclipai/shared"; @@ -68,7 +68,7 @@ const SHARED_WORKSPACE_CONCURRENCY_OPTIONS: { { value: "auto", label: "Auto", - help: "Concurrent runs on local/SSH runners; runs take turns in cloud sandboxes.", + help: "Concurrent runs on local/SSH runners; runs take turns in cloud environments.", }, { value: "serialize", @@ -1119,7 +1119,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa {runSelectableEnvironments.map((environment) => ( ))} diff --git a/ui/src/components/TaskChatThread.test.tsx b/ui/src/components/TaskChatThread.test.tsx index 479d35a29d..ab47408b1d 100644 --- a/ui/src/components/TaskChatThread.test.tsx +++ b/ui/src/components/TaskChatThread.test.tsx @@ -575,13 +575,13 @@ describe("TaskChatThread live transcript", () => { comments={[]} onAdd={async () => {}} issueStatus="in_progress" - activeRun={{ ...baseRun, currentStatusMessage: "Syncing workspace to sandbox" }} + activeRun={{ ...baseRun, currentStatusMessage: "Syncing workspace to environment" }} />, ); const tail = container.querySelector('[data-testid="task-chat-live-transcript"]'); expect(tail).not.toBeNull(); - expect(tail!.textContent).toContain("Syncing workspace to sandbox"); + expect(tail!.textContent).toContain("Syncing workspace to environment"); expect(tail!.textContent).not.toContain("Waiting for transcript..."); // Without a runtime status, the generic wait message still shows. diff --git a/ui/src/components/TaskChatThread.tsx b/ui/src/components/TaskChatThread.tsx index f40ff2de90..b6e27856fd 100644 --- a/ui/src/components/TaskChatThread.tsx +++ b/ui/src/components/TaskChatThread.tsx @@ -698,7 +698,7 @@ export function TaskChatThread(props: TaskChatThreadProps) { ? "Waiting to start..." : // Before the first transcript token, surface the run's // live runtime status (sandbox preparation phases like - // "Syncing workspace to sandbox" emitted via + // "Syncing workspace to environment" emitted via // onRuntimeProgress) instead of an opaque wait message. (liveRun && liveRun.id === tailRunId ? liveRun.currentStatusMessage diff --git a/ui/src/lib/issue-chat-messages.test.ts b/ui/src/lib/issue-chat-messages.test.ts index 8e0ad50189..68fa9c2c90 100644 --- a/ui/src/lib/issue-chat-messages.test.ts +++ b/ui/src/lib/issue-chat-messages.test.ts @@ -813,7 +813,7 @@ describe("buildIssueChatMessages", () => { agentId: "agent-1", agentName: "CodexCoder", adapterType: "codex_local", - currentStatusMessage: "Syncing git worktree to sandbox", + currentStatusMessage: "Syncing git worktree to environment", currentStatusUpdatedAt: "2026-04-06T12:03:05.000Z", currentToolName: "bash", lastAssistantSnippet: "Checking repository status", @@ -837,7 +837,7 @@ describe("buildIssueChatMessages", () => { custom: { kind: "live-run", runId: "run-active-1", - currentStatusMessage: "Syncing git worktree to sandbox", + currentStatusMessage: "Syncing git worktree to environment", currentStatusUpdatedAt: "2026-04-06T12:03:05.000Z", currentToolName: "bash", lastAssistantSnippet: "Checking repository status", diff --git a/ui/src/lib/managed-sandbox-environment.test.ts b/ui/src/lib/managed-sandbox-environment.test.ts index d5ea54f1d9..6de63daa96 100644 --- a/ui/src/lib/managed-sandbox-environment.test.ts +++ b/ui/src/lib/managed-sandbox-environment.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { + environmentDisplayLabel, filterManagedSandboxSelectableEnvironments, isPlatformManagedEnvironment, } from "./managed-sandbox-environment"; @@ -13,6 +14,22 @@ describe("managed sandbox environment helpers", () => { expect(isPlatformManagedEnvironment(null)).toBe(false); }); + it("labels platform-managed rows by name alone and keeps the driver suffix elsewhere", () => { + expect( + environmentDisplayLabel({ + name: "Paperclip Computer", + driver: "sandbox", + metadata: { managedByPaperclip: true }, + }), + ).toBe("Paperclip Computer"); + expect( + environmentDisplayLabel({ name: "E2B", driver: "sandbox", metadata: null }), + ).toBe("E2B · sandbox"); + expect( + environmentDisplayLabel({ name: "Build box", driver: "ssh", metadata: {} }), + ).toBe("Build box · ssh"); + }); + it("filters the local environment only under managed-sandbox-only", () => { const environments = [ { driver: "local" as const }, diff --git a/ui/src/lib/managed-sandbox-environment.ts b/ui/src/lib/managed-sandbox-environment.ts index c7233c9fa2..e1681c274d 100644 --- a/ui/src/lib/managed-sandbox-environment.ts +++ b/ui/src/lib/managed-sandbox-environment.ts @@ -13,6 +13,20 @@ export function isPlatformManagedEnvironment( return environment?.metadata?.managedByPaperclip === true; } +/** + * Display label for an environment in selectors and lists. Platform-managed + * rows render their name alone: their name is the product name for the + * default Paperclip environment, and the raw driver key ("sandbox") is + * infrastructure vocabulary we don't surface next to it. User-created rows + * keep the driver suffix so mixed lists (ssh vs sandbox) stay tellable apart. + */ +export function environmentDisplayLabel( + environment: Pick, +): string { + if (isPlatformManagedEnvironment(environment)) return environment.name; + return `${environment.name} · ${environment.driver}`; +} + /** * Client-side mirror of the server's managed-sandbox-only read filter: the * local environment never renders when `enableManagedSandboxOnly` is on. diff --git a/ui/src/pages/Agents.tsx b/ui/src/pages/Agents.tsx index f22cadd3ee..8e519f538b 100644 --- a/ui/src/pages/Agents.tsx +++ b/ui/src/pages/Agents.tsx @@ -11,6 +11,7 @@ import { useDialogActions } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useSidebar } from "../context/SidebarContext"; import { queryKeys } from "../lib/queryKeys"; +import { isPlatformManagedEnvironment } from "../lib/managed-sandbox-environment"; import { AgentStatusBadge, AgentStatusCapsule } from "../components/StatusBadge"; import { AgentActionButtons } from "../components/AgentActionButtons"; import { MembershipAction } from "../components/MembershipAction"; @@ -129,11 +130,13 @@ function describeEnvironment( environment: Environment, capabilities?: EnvironmentCapabilities | null, ): EnvironmentDescriptor { - const detail = environment.driver === "sandbox" - ? `${getSandboxProviderLabel(environment, capabilities)} sandbox provider` - : environment.driver === "local" - ? "Paperclip host" - : formatEnvironmentDriver(environment.driver); + const detail = isPlatformManagedEnvironment(environment) + ? "Managed by Paperclip" + : environment.driver === "sandbox" + ? `${getSandboxProviderLabel(environment, capabilities)} sandbox provider` + : environment.driver === "local" + ? "Paperclip host" + : formatEnvironmentDriver(environment.driver); return { label: environment.name, diff --git a/ui/src/pages/CompanyEnvironments.tsx b/ui/src/pages/CompanyEnvironments.tsx index f2899f5add..3a3667d210 100644 --- a/ui/src/pages/CompanyEnvironments.tsx +++ b/ui/src/pages/CompanyEnvironments.tsx @@ -43,7 +43,7 @@ import { import { useBreadcrumbs } from "@/context/BreadcrumbContext"; import { useCompany } from "@/context/CompanyContext"; import { useToast } from "@/context/ToastContext"; -import { isPlatformManagedEnvironment } from "@/lib/managed-sandbox-environment"; +import { environmentDisplayLabel, isPlatformManagedEnvironment } from "@/lib/managed-sandbox-environment"; import { queryKeys } from "@/lib/queryKeys"; import { Link, useNavigate, useParams } from "@/lib/router"; import { buildSameOriginWebSocketUrl } from "@/lib/websocket-url"; @@ -1790,7 +1790,7 @@ export function CompanyEnvironments({ mode = "list" }: CompanyEnvironmentsProps) )} {nonLocalEnvironments.map((environment) => ( ))} @@ -1821,7 +1821,10 @@ export function CompanyEnvironments({ mode = "list" }: CompanyEnvironmentsProps)
- {environment.name} · {environment.driver} + {environment.name} + {isPlatformManagedEnvironment(environment) ? null : ( + · {environment.driver} + )} {isPlatformManagedEnvironment(environment) ? ( @@ -1842,6 +1845,13 @@ export function CompanyEnvironments({ mode = "list" }: CompanyEnvironmentsProps)
{(() => { const summary = summarizeSandboxConfig(environment.config as Record); + // The managed row's badge already says "Managed by + // Paperclip"; repeating provider vocabulary like + // "sandbox provider" next to the default environment + // is noise the product avoids. + if (isPlatformManagedEnvironment(environment)) { + return summary ?? "Provisioned and maintained for you."; + } return `${sandboxProviderDisplayName} sandbox provider${summary ? ` · ${summary}` : ""}`; })()}
@@ -1926,7 +1936,7 @@ export function CompanyEnvironments({ mode = "list" }: CompanyEnvironmentsProps)

- {editingEnvironment.description ?? "Your agent runs in a sandbox managed by Paperclip."} + {editingEnvironment.description ?? "Your agent runs on a computer managed by Paperclip."}

This environment is provisioned and maintained for you. You can add environment @@ -2176,7 +2186,7 @@ export function CompanyEnvironments({ mode = "list" }: CompanyEnvironmentsProps) )} setEnvironmentForm((current) => ({ diff --git a/ui/src/pages/InstanceExperimentalSettings.tsx b/ui/src/pages/InstanceExperimentalSettings.tsx index 672a516ddb..284fed071b 100644 --- a/ui/src/pages/InstanceExperimentalSettings.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.tsx @@ -705,14 +705,14 @@ export function InstanceExperimentalSettings() { /> toggleMutation.mutate({ enableManagedSandboxOnly: checked })} disabled={toggleMutation.isPending} settingKey="enableManagedSandboxOnly" managed={managedKeys.enableManagedSandboxOnly} - ariaLabel="Toggle managed sandbox only experimental setting" + ariaLabel="Toggle managed environment only experimental setting" /> {inWorktree ? (