feat(environments): refer to the managed default environment by name, not the sandbox driver key (#11838)

## 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 "<provider>
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 "<provider> 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
This commit is contained in:
Devin Foley 2026-08-21 12:51:22 -07:00 committed by GitHub
parent 0148c2c6eb
commit adfbe2d4b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 168 additions and 93 deletions

View File

@ -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 () => {

View File

@ -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;

View File

@ -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",
);
});

View File

@ -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;

View File

@ -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);

View File

@ -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 });
}

View File

@ -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.",
};
}

View File

@ -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 {

View File

@ -715,7 +715,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
effectiveEffort = "";
await onLog(
"stderr",
`[paperclip] Claude CLI in the sandbox does not advertise --effort; omitting configured effort "${effort}". Upgrade the sandbox CLI/image to restore reasoning-effort control.\n`,
`[paperclip] Claude CLI in the environment does not advertise --effort; omitting configured effort "${effort}". Upgrade the environment CLI/image to restore reasoning-effort control.\n`,
);
}
}

View File

@ -261,8 +261,8 @@ export async function testEnvironment(
code: "claude_effort_flag_unsupported",
level: "warn",
message:
"Claude CLI in the sandbox does not advertise --effort; the probe omitted the configured reasoning effort.",
hint: "Upgrade the sandbox CLI/template to a newer Claude Code release to restore reasoning-effort control.",
"Claude CLI in the environment does not advertise --effort; the probe omitted the configured reasoning effort.",
hint: "Upgrade the environment CLI/template to a newer Claude Code release to restore reasoning-effort control.",
});
}
}
@ -346,8 +346,8 @@ export async function testEnvironment(
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.",
});
}
} else if ((probe.exitCode ?? 1) === 0) {

View File

@ -628,8 +628,8 @@ export async function testCodexAcpEnvironment(
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.",
});
}
}

View File

@ -430,9 +430,9 @@ export async function testEnvironment(
checks.push({
code: ADAPTER_AUTH_MISSING_CHECK_CODE,
level: "warn",
message: "The sandbox has no ready authentication for this adapter.",
message: "This environment has no ready authentication for this adapter.",
...(detail ? { detail } : {}),
hint: "Provide credentials for this adapter, or start login in the sandbox.",
hint: "Provide credentials for this adapter, or start login in the environment.",
});
}
} else {

View File

@ -51,9 +51,9 @@ export const INSTANCE_FEATURE_CATALOG: Record<InstanceFeatureKey, FeatureCatalog
selfHostedDefault: false,
},
enableManagedSandboxOnly: {
title: "Managed Sandbox Only",
title: "Managed Environment Only",
description:
"Hide the local environment and run all agents in the platform-managed sandbox environment.",
"Hide the local environment and run all agents in the platform-managed environment.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,

View File

@ -323,7 +323,7 @@ describe("agent live run routes", () => {
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",

View File

@ -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({

View File

@ -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();

View File

@ -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.",
};
}

View File

@ -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);

View File

@ -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" })],

View File

@ -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) {
<option value="">Default: {inheritedEnvironmentLabel}</option>
{environmentOptions.map((environment) => (
<option key={environment.id} value={environment.id}>
{environment.name} · {environment.driver}
{environmentDisplayLabel(environment)}
</option>
))}
</select>
@ -2052,7 +2053,7 @@ function AdapterLoginTerminalState({
return (
<div className="flex items-center gap-2 text-(length:--text-micro) text-foreground">
<Check className="size-3 shrink-0" />
<span>Authenticated. The sandbox has credentials now.</span>
<span>Authenticated. The environment has credentials now.</span>
</div>
);
}
@ -2180,7 +2181,7 @@ function DisplayedCodeLoginPanel({
return (
<div className="rounded-md border border-border bg-muted/40 px-3 py-2 space-y-2">
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-medium text-foreground">Sign in to the sandbox</span>
<span className="text-xs font-medium text-foreground">Sign in to the environment</span>
<div className="flex items-center gap-1.5">
{isActive && (
<Button
@ -2628,7 +2629,7 @@ function SubmittedBrowserCodeLoginPanel({
return (
<div className="rounded-md border border-border bg-muted/40 px-3 py-2 space-y-2">
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-medium text-foreground">Sign in to the sandbox</span>
<span className="text-xs font-medium text-foreground">Sign in to the environment</span>
<div className="flex items-center gap-1.5">
{isActive && (
<Button
@ -2790,7 +2791,7 @@ function SubmittedBrowserCodeLoginPanel({
{isStored && (
<div className="flex items-center gap-2 text-(length:--text-micro) text-foreground">
<Check className="size-3 shrink-0" />
<span>Authenticated. The sandbox has credentials now.</span>
<span>Authenticated. The environment has credentials now.</span>
</div>
)}

View File

@ -656,7 +656,12 @@ const TimelineList = memo(function TimelineList({
{run.environment ? (
<span>
Environment <span className="text-foreground">{run.environment.name}</span>
<span> · {run.environment.driver}</span>
{/* 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" ? (
<span> · {run.environment.driver}</span>
) : null}
</span>
) : null}
{run.environmentLease?.provider ? (

View File

@ -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(),

View File

@ -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";
}

View File

@ -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
<option value="">No environment</option>
{runSelectableEnvironments.map((environment) => (
<option key={environment.id} value={environment.id}>
{environment.name} · {environment.driver}
{environmentDisplayLabel(environment)}
</option>
))}
</select>

View File

@ -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.

View File

@ -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

View File

@ -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",

View File

@ -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 },

View File

@ -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<Environment, "name" | "driver" | "metadata">,
): 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.

View File

@ -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,

View File

@ -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) => (
<option key={environment.id} value={environment.id}>
{environment.name} · {environment.driver}
{environmentDisplayLabel(environment)}
</option>
))}
</select>
@ -1821,7 +1821,10 @@ export function CompanyEnvironments({ mode = "list" }: CompanyEnvironmentsProps)
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2 text-sm font-medium">
<span>
{environment.name} <span className="text-muted-foreground">· {environment.driver}</span>
{environment.name}
{isPlatformManagedEnvironment(environment) ? null : (
<span className="text-muted-foreground"> · {environment.driver}</span>
)}
</span>
{isPlatformManagedEnvironment(environment) ? (
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs font-normal text-muted-foreground">
@ -1842,6 +1845,13 @@ export function CompanyEnvironments({ mode = "list" }: CompanyEnvironmentsProps)
<div className="text-xs text-muted-foreground">
{(() => {
const summary = summarizeSandboxConfig(environment.config as Record<string, unknown>);
// 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}` : ""}`;
})()}
</div>
@ -1926,7 +1936,7 @@ export function CompanyEnvironments({ mode = "list" }: CompanyEnvironmentsProps)
</span>
</div>
<p className="mt-1 max-w-3xl text-sm text-muted-foreground">
{editingEnvironment.description ?? "Your agent runs in a sandbox managed by Paperclip."}
{editingEnvironment.description ?? "Your agent runs on a computer managed by Paperclip."}
</p>
<p className="mt-1 max-w-3xl text-xs text-muted-foreground">
This environment is provisioned and maintained for you. You can add environment
@ -2176,7 +2186,7 @@ export function CompanyEnvironments({ mode = "list" }: CompanyEnvironmentsProps)
)}
<ToggleField
label="Stream run logs"
hint="Stream the agent CLI's output live while sandbox runs execute (recommended). Turn off to deliver output only when the run finishes."
hint="Stream the agent CLI's output live while runs execute (recommended). Turn off to deliver output only when the run finishes."
checked={environmentForm.sandboxConfig.streamRunLogs !== false}
onChange={(checked) =>
setEnvironmentForm((current) => ({

View File

@ -705,14 +705,14 @@ export function InstanceExperimentalSettings() {
/>
<ExperimentalToggleCard
title="Managed Sandbox Only"
description="Hide the local environment and run all agents in the platform-managed sandbox environment."
title="Managed Environment Only"
description="Hide the local environment and run all agents in the platform-managed environment."
checked={enableManagedSandboxOnly}
onCheckedChange={(checked) => 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 ? (