## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A repository project gets its working files from core staging or from a workspace storage plugin > - #20459 reported that an installed storage plugin attached every new repository project, so sandbox agents could not use those projects > - #20587 fixed creation with an **Available in** choice that selects storage > - #20651 then saved an environment on every new project and used it before the agent default to place runs. That solved a problem that did not exist: the project needs storage, not a run environment > - Creation saves the first environment of the chosen group, so a project silently sends every agent's runs to that environment. Project Configuration also shows two controls (Environment and Workspace storage) for one decision > - This pull request removes the project run environment and makes storage the only placement choice a project owns. Storage is fixed at creation > - The benefit is that runs follow the agent again, and project settings show one clear, read-only fact about storage ## Linked Issues or Issue Description Refs #20459. Follow-up to #20587 and #20651. Refs #11644. That issue asks for a project-level run environment override. This pull request removes the override that #20651 added for it, because the override is not necessary to fix #20459. #11644 stays a separate feature request. Related: #18176 (open) repairs placement through the legacy workspace-policy field. This pull request does not restore project placement, so it conflicts with the direction of #18176. ## What Changed - **Remove the project run environment.** Migration `0466` drops `projects.environment_id`. Run placement no longer reads the project. Runs use the agent default, then the instance default, then local execution. The low-trust task sandbox and the enforced sandbox policies keep their precedence. - **Keep the #20651 recovery and binding guards.** A recovered run still uses its admitted environment lease (source `run`). Company-binding checks before admission and during lease creation stay. - **Keep the creation choice.** **Available in** in New Project still selects storage. It still shows only when the environments need more than one storage owner. That can only occur when a storage plugin is installed. `POST /projects` with `environmentId` selects the storage of that environment and does not save the environment. - **Fix storage at creation.** `PATCH /projects/:id` rejects any storage change with `409 project_storage_locked`, from core to a plugin and from a plugin to core. Before, a core project could move to a plugin. - **Drop legacy writes.** Older clients can still send `executionWorkspacePolicy.environmentId`. The service removes it on create and update. - **Simplify Project Configuration.** Remove the Environment and Workspace storage selectors. Show an advisory **Available in** list of the environments that can use the project's storage. Show it when a storage plugin is installed or when the project uses one. When the project's plugin is not installed, show **Storage plugin unavailable**. Without a storage plugin, show nothing. - **Issue workspace card.** Remove the "project default" environment label. - **Shared contract.** Remove `Project.environmentId`, the `environmentId` project field, and `expectedStorageProviderPluginId` from the update schema. The create schema keeps `environmentId` and `expectedStorageProviderPluginId` from #20587. - **Docs and maps.** Update `doc/project-environment-compatibility.md`, `docs/api/goals-and-projects.md`, and the project feature and agent-access maps. - **SDK source pins.** Refresh `tests/runner-e2e/source-sdk-public-inputs.json` for the two shared files. The shared file blobs return to their values before #20651. The refresh also corrects the `pnpm-lock.yaml` pin and `lockSha256`: `master` pins an older lockfile blob than the one it contains. ## Verification - Targeted server suites pass (227 tests): `project-environments`, `project-workspace-provider.integration`, `heartbeat/environment-selection`, `execution-workspace-policy`, `environment-selection-route-guards`, `environment-run-orchestrator`, `openapi-routes`. - Related server suites pass (349 tests): `environment-service`, `environment-routes`, `plugin-workspaces.integration`, `issues-service`. - Targeted UI suites pass: `ProjectProperties.concurrency`, `IssueWorkspaceCard`, `NewProjectDialog`, `NewProjectDialog.managed-sandbox`, `ProjectDetail`. - New tests: - Creation saves storage, not an environment. Core and plugin projects both reject a storage change with `project_storage_locked`. - A legacy policy `environmentId` is dropped on create and update. - A new run follows the agent default for any project. A recovered run keeps its admitted lease after the agent default changes. - Project Configuration has no storage or environment selector. The note is hidden without a plugin. It lists the core or plugin environments for the project's storage. It shows **Storage plugin unavailable** for a removed plugin. - `pnpm --filter @paperclipai/server typecheck`, `pnpm --filter @paperclipai/ui typecheck`, and `pnpm --filter @paperclipai/shared typecheck` pass. - `pnpm --filter @paperclipai/db check:migrations`, `pnpm check:token-gates`, `node scripts/check-maps.mjs`, and `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check` pass. - Not run locally: the full `pnpm test:run`, `pnpm build`, and browser suites. CI supplies that coverage. - Manual check: open Project Configuration on an instance with a storage plugin. Confirm that there is no Environment or Workspace storage selector, and that **Available in** lists the environments. ## Risks - **Migration.** `0466` drops `projects.environment_id` and its foreign key. The migration deletes the saved project environments. Apply it before you serve the new revision. An old server revision that reads `projects` fails until it stops. This is the same pattern as earlier drop-column migrations (`0229`, `0289`). - **Behavior change for existing projects.** Projects created after #20651 shipped have a saved environment that overrides the agent. After this change, their runs follow the agent default again. This is the intended fix. Before #20651, a project environment selection was not applied to runs (#11644), so no older behavior is lost. - **No path from core to a plugin.** A core project, including one created before a plugin was installed, cannot move to the plugin. To use plugin storage, create a new project. - **API.** A `PATCH` that sends `environmentId` or `expectedStorageProviderPluginId` no longer has an effect. The schema strips these fields. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`) by Anthropic, in Claude Code, with tool use (shell, file edits, test runs). Extended thinking was available. ## 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 - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
5.2 KiB
Project environments and repository storage
A repository project belongs to one storage owner. Creation shows Available in when the configured environments require more than one owner. Each choice names all environments that use that owner. The choice selects storage only. It is not an allowlist, and it does not override an agent's run environment. A project has no run environment of its own.
Compatibility rule
Use the existing workspace execution contract, not environment names, driver names alone, images, operating systems, endpoints, or complete config equality:
- Local, SSH, and sandbox environments use core repository staging. They can use the same repository project even across driver types and sandbox providers.
- Plugin-managed storage belongs to an installed plugin ID. It can be consumed
only by that plugin's registered
environment_driverwithsupportsTasks: true. Its manifest must declareenvironment.drivers.register,workspaces.manage, a workspace provider, and the project resource lifecycle. - Two task environments from that installed plugin share its project storage
contract, even when their other settings differ. Storage operations are scoped
by company, project, and plugin; they do not take an environment configuration.
Separate installed storage plugins remain incompatible even when both use the
plugindriver. Core cannot transfer their managed bindings between them. - Unknown plugin drivers and the probe-only fake sandbox do not establish a compatible project storage contract. They are excluded from creation choices.
These are project repository compatibility rules. They do not assert that arbitrary local paths exist remotely, that a retained task workspace can switch hosts, or that an adapter, credential, image, or running worker is ready. Existing run-time validation still checks those conditions. Cross-plugin storage sharing would require a new execution/storage contract; matching config fields cannot establish it.
Defaults and validation
For an agent API request, use that agent's default environment. For a person,
use their primary agent's default environment when present. Fall back to the
instance default, then local execution. There is no separate user or company
defaultEnvironmentId setting today. Managed-only and forced Kubernetes policies
retain their execution restrictions and default redirection.
GET /api/companies/:companyId/project-environments returns environment IDs and
names grouped by storage owner, plus the caller's effective default. It excludes
archived environments, disabled/uninstalled storage plugins, and managed
environments bound only to another company. It does not contact workers or return
provider configs, credentials, endpoints, or host paths. A cold worker does not
change a durable storage choice.
POST /api/companies/:companyId/projects accepts an optional environmentId.
The server validates the choice again and stores only its storage owner. The
project is then available to the compatible environments in that group; no single
run environment is saved. An explicit executionWorkspacePolicy.storageProviderPluginId
continues to work. The UI sends both the environment and its observed storage
owner in expectedStorageProviderPluginId, so a changed environment cannot
silently select different storage. This guard does not create a workspace-mode
policy; instance task-isolation defaults remain inherited. An explicit policy
and environment must agree when both are supplied. Without either,
storage follows the caller's effective environment. A plugin's
defaultForProjects claim alone no longer attaches a project to that plugin.
The hidden repository-free “My private tasks” project is independent of environment/storage availability, so creating a private note never requires repairing an environment.
An unavailable configured default produces an error and permits an explicit
available choice. It does not silently select another storage owner. Existing
projects keep their stored owner. This change does not migrate managed storage
or detach projects that contain existing work. A managed project run in an
incompatible environment fails with project_environment_incompatible before a
workspace provider operation, with a message naming the storage and environment.
Project Configuration
Storage is fixed when the project is created. PATCH /api/projects/:id rejects a
change of executionWorkspacePolicy.storageProviderPluginId with 409
project_storage_locked, in both directions: core to plugin and plugin to core.
A policy update that omits the storage owner keeps the stored owner.
Project Configuration has no storage or environment control. When a storage plugin is installed, or the project uses one, it shows an advisory Available in list: the environments that can use the project's storage. If the project's storage plugin is no longer installed, it shows Storage plugin unavailable. Without a storage plugin, the advisory is hidden.
Runs use the agent default, then the instance default, then local execution. An
operator-selected low-trust task sandbox and enforced sandbox policies retain
precedence. A project does not change run placement. Older clients may still send
executionWorkspacePolicy.environmentId; the service drops it.