Files
PaperClipAI/doc/project-environment-compatibility.md
T
Nicky LeachandClaude Opus 5.5 14cb69245f fix(projects): do not offer a single project storage option (#20875)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A project uses one workspace storage owner. The user chooses it when
they create the project.
> - The **Available in** selector shows the storage owners that the user
can choose.
> - Sometimes the default environment cannot be used, and only one
storage owner is available. In that case, the dialog shows a selector
with only one option. The user must select it before they can create the
project.
> - A selector with only one option is not a real choice.
> - This pull request uses the only available storage owner as the
default. The selector appears only when the user has two or more
options.
> - The benefit is that project creation asks for a choice only when
there is one.

## Linked Issues or Issue Description

Refs #20835

**What happened?**
An example is an instance in managed-sandbox-only mode that has the
agent-computer plugin but no managed sandbox. The local environment is
hidden, and the managed-only rule finds no default. The
project-environments endpoint returns one storage group and an error.
The new project dialog shows **Available in** with only one option and
disables **Create project** until the user selects it. An API request
without `environmentId` fails with `project_environment_unavailable`.

**What did you expect?**
When only one storage owner is available, the project uses it. No
selector appears.

**Steps to reproduce**
1. Turn on `enableManagedSandboxOnly`. Install the agent-computer
plugin. Do not add a sandbox environment.
2. Open **Create project**.
3. See an **Available in** selector with one option, and a disabled
**Create project** button.

## What Changed

- `projectEnvironmentOptions`: when the configured default is
unavailable and only one storage group exists, the first environment of
that group becomes `defaultEnvironmentId`, and `error` is null. When two
or more groups exist, an unavailable default still produces the error.
The server does not guess.
- Project create uses the same default, so an API request without
`environmentId` now succeeds in the single-group case.
- `NewProjectDialog` shows the selector only when `groups.length > 1`.
- Tests:
- A new server test covers managed-only mode with only the
agent-computer environment.
- A new server test covers an unavailable default with two storage
groups (the server still refuses to guess).
- The archived-default test now expects the server to use the only
remaining storage.
  - A new UI test checks that the dialog never shows one option.
- Docs: `doc/project-environment-compatibility.md` and
`docs/api/goals-and-projects.md`.

## Verification

- `pnpm vitest run src/__tests__/project-environments.test.ts` in
`server/`: 16 passed.
- `pnpm vitest run src/components/NewProjectDialog` in `ui/`: 12 passed.
- `tsc --noEmit` in `ui/`: clean.
- Server `tsc` reports 9 errors on this machine. None are in the changed
files. They are in `packages/adapter-utils` and a native-runtime test,
because local dependencies are stale. CI typechecks with a fresh
install.

## Risks

- Low risk. The change applies only when the default is unavailable and
exactly one storage group exists. In that case, the server picks the
group that the user would have had to choose anyway. Run placement does
not depend on the project, so this does not change where runs execute.
- No migration.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use
(shell, file edits, test runs).

## 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>
2026-11-11 09:49:33 -08:00

94 lines
5.5 KiB
Markdown

# 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_driver` with `supportsTasks: true`.
Its manifest must declare `environment.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
`plugin` driver. 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.
When the configured default is unavailable and only one storage owner is
available, that owner becomes the default, because there is nothing to choose.
When two or more owners are available, an unavailable default produces an error
and permits an explicit available choice. It does not silently select another
storage owner. The UI shows the **Available in** selector only when there are two
or more storage owners. 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.