fix: isolate fresh task sources from restricted project workspaces (#16095)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Each task must have an isolated workspace that its agent can read.
> - Older project checkouts can contain worktrees for restricted tasks.
> - The source access check correctly rejects these shared checkouts.
> - A fresh task then fails even when its agent can clone the project
remote.
> - This pull request gives fresh default tasks a separate clone of the
same remote.
> - The task can start while the restricted task files remain protected.

## Linked Issues or Issue Description

**What happened?**

Fresh tasks fail with `Task workspace access is no longer available`
when their default project checkout contains a restricted task worktree.
The caller has access to the new task and its remote repository. The
shared local Git source is unsafe to copy.

**Expected behavior**

Use a separate clone of the same authorized remote for a fresh default
task. Keep source access checks and responsible-user Git credentials in
force.

**Steps to reproduce**

1. Put a restricted task worktree inside a project checkout.
2. Assign a new default task to an agent that cannot read the restricted
task.
3. Start the new task. The current source check fails before workspace
creation.
4. With this fix, the task uses a separate remote source. The restricted
files are absent. Later runs reuse the same owned source.

Related work: Refs #15749, Refs #15528. This change keeps their source
retention and workspace ownership checks.

## What Changed

- Add a remote source path scoped to the company and task.
- Clone through the run’s Git connection after the shared source fails
its access check.
- Reject reused workspaces, explicit selections, pinned branches, local
sources, credential-bearing URLs, and symlink paths.
- Check source access again before publishing the workspace.
- Resolve later runs from the verified recorded source. Ignore leftover
clones that the saved workspace does not own.
- Record provenance for new implicit Git defaults. Read the current task
binding before fallback. Preserve explicit, inherited and ambiguous
legacy checkout choices.
- Keep the effective task strategy during startup validation of matching
implicit defaults. Preserve the existing explicit-selection write check.
- Add regression coverage and document the fallback.

## Verification

- Merged master `c8efdbfba43` and resolved both rounds of catalog/index
conflicts. All upstream suites, classifications, source references and
story IDs are preserved. Both fix branches combine cleanly in either
merge order, with no catalog fingerprint drift.
- Passed locally: 313 focused source-isolation, preparation,
task-creation, binding and trust tests before the final startup fix. The
final fix passes all 100 source-trust, task-binding and
environment-selection tests. Earlier workspace-resolution and
persisted-source suites also passed.
- Real-Git regressions cover all three review findings. Reconstruction
ignores an unrecorded clone and preserves unpublished commits/files.
Explicit and parent-inherited checkout IDs without typed selections
retain their denial and branch tips without cloning or acquiring an
environment. A protected-project heartbeat completes and resumes using
the task’s Git strategy with read access. Explicit selection still
requires writes, and a mismatched default is rejected.
- Passed: the complete Storybook catalog check with no drift errors,
feature and agent map structure checks, and all 17 catalog/index script
tests. The generated index counts match the merged catalog. The current
upstream verification-route inventory remains unchanged. The catalog
contains all 262 authored suites and 2,481 story IDs.
- Passed: full workspace typecheck and build after the final guard
changes, on latest head `79d2adadcb6d3d7a04b9943a91bdb8b0b6abea6d`.
- Latest head `79d2adadcb6d3d7a04b9943a91bdb8b0b6abea6d`: complete CI
passed (54 successful checks, two intentional skips), including the full
general/serialized suites and all eight browser shards. Fresh Greptile
Apex review is 5/5 on this same commit, with no unresolved findings or
recommendations. The full local test command was attempted earlier
without a complete aggregate result; the complete suite result is from
CI.
- Manual check: configure a shared project checkout with a restricted
task worktree. Admit a fresh default task using the authorized remote.
Confirm separate Git storage and no restricted local branch or file.
Reuse, close and reopen the task workspace. Confirm that explicit,
literal branch and low-trust selections retain the denied source.

## Risks

- A clean remote clone excludes unpublished local work. The fallback
applies only to fresh default tasks and rejects existing workspace or
branch selections.
- A private remote can fail to clone when the responsible user lacks Git
credentials. The run then reports the failure; access checks are not
bypassed.
- Older untyped checkout bindings cannot prove default provenance. They
keep the source denial instead of silently dropping unpublished work.
- New implicit Git defaults use the existing workspace-intent contract.
The source directory uses existing instance storage. No schema migration
is required.

## Model Used

OpenAI GPT-6, model ID `gpt-6`, through Codex. Used reasoning,
repository inspection, code editing, terminal tools, and test execution.
The API session does not expose an exact context window size.

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip authored and GitHub committed 2026-10-13 12:31:40 -07:00
1 parent 393d1c4108
commit c7f4b74111
16 files changed
+516 -58

No files matched your search

+20
View File
@@ -36,6 +36,26 @@ See [Project Repository Checkouts](DEVELOPING.md#project-repository-checkouts) f
ignore rules, and reuse behavior. Responsible-user credential rules still apply.
A repository selection never delegates credentials.
Fresh tasks using the default repository workspace can clone the same authorized
remote into a company- and task-scoped source directory when an existing project
checkout contains another task's restricted workspace. The clone uses the run's
Git connection and keeps separate Git metadata. Source access checks still apply
before the task workspace is published. Low-trust review runs cannot use this
persistent clone fallback. This fallback does not copy unpublished
local changes or replace reused workspaces, explicit workspace selections, pinned
branches, or repositories that only exist on the local filesystem.
Configured branch templates must derive the branch from the task identifier, ID,
title, or slug. Literal or task-independent templates retain the source denial,
so a fresh clone cannot replace a selected local branch with a new empty branch.
New implicit Git workspace defaults record their project-source provenance.
An untyped checkout binding from an older task is ambiguous and keeps the denial,
including explicit checkout IDs and inherited parent sources. Admission rechecks
the current binding rather than inferring a default from the selected checkout ID.
Startup validation of these recorded defaults keeps the task's effective workspace
strategy. It does not substitute the agent's shared strategy or grant project writes.
Workspace close and reopen use the recorded isolated source. Both operations
verify its company, task, repository URL, and separate Git storage before use.
## Discovery and setup
The server checks company membership, grant ownership/status, and organization-grant
+18
View File
@@ -39,6 +39,24 @@ workspace write authority. Accepting a new pending choice advances its binding
revision; superseded retries fail while current-choice retries remain idempotent.
Binding revisions and request keys fence retries;
selection applies at the next normal admission without interrupting a live run.
Fresh standard-trust tasks with an implicit repository default can use an owned
remote clone when the shared default source fails the restricted-task workspace
guard. This does not grant access to the private local checkout or its Git metadata.
The normal admitted Git identity must already authorize the configured remote.
Creation of a fallback source is unavailable for explicit, parent/channel, reused
or low-trust selections, or branch templates without a task-derived placeholder;
an existing verified owned source remains reusable. Literal branch selections
retain the original denial without cloning or replacing their unpublished commits.
New implicit Git defaults record project-source provenance. Untyped explicit,
inherited and older ambiguous checkout bindings retain the denial; admission reads
the current company-scoped binding before considering the fallback.
Local paths, unsupported transports and credential-bearing URLs are not fallback
sources. Company/task/remote bindings, real paths, Git storage and the exact origin
are revalidated for reuse, cleanup and reopening. A failed save removes only the
owned task worktree. The [isolated-source service](../../server/src/services/isolated-remote-workspace-source.ts)
and its local regression tests establish source and lifecycle boundaries; live
agent/provider qualification remains separate.
Writable aliases must also be compatible with current and retained physical
source projects' trust boundaries. Incompatible choices are rejected before
dispatch; a projectless alias cannot bypass required sandbox containment.
+19
View File
@@ -34,6 +34,25 @@ Automated: [workspace card](../../ui/src/components/IssueWorkspaceCard.test.tsx)
Manual: Run a task that writes a small file, follow the bound workspace, and inspect the file and revision. Reload and verify links still refer to the same workspace, not another checkout or a stale run.
For fresh standard-trust tasks using an implicit repository default, create a
disposable project checkout whose shared Git storage overlaps a private task
workspace. Verify that source admission creates a separate checkout from the
configured remote, with no private local branches, commits or shared Git storage.
Reusing, closing and reopening that task must retain the company/task-bound source;
a failed workspace save must clean up only its owned checkout. Explicit workspace,
parent/channel selections, literal or task-independent branch templates, low-trust
tasks, and local or credential-bearing remote
URLs must not use this fallback. A selected checkout requires recorded default
provenance. Create tasks
through the task service with an explicit checkout ID or an inherited parent
source but no typed selection; verify denial, no clone or environment acquisition,
and preservation of unpublished branch commits. Older ambiguous bindings and
source choices changed during preparation must retain the denial. The
[remote-source tests](../../server/src/services/isolated-remote-workspace-source.test.ts)
and [preparation tests](../../server/src/services/heartbeat/workspace-preparation.test.ts)
exercise the real Git isolation and lifecycle guards. They do not qualify live
remote credentials or a provider deployment.
For task workspace selection, use the authorized task workspace API to choose a
task directory, project source, or existing workspace. Verify that a change
made during a run stays pending until the next normal admission. Submit another
+14 -14
View File
@@ -80,7 +80,7 @@ Creation, assignment, lists, properties, comments, and completion.
| [Tasks and conversations/Task creation and lifecycle/External Objects](../../ui/storybook/stories/tasks/external-objects.stories.tsx) | `tasks/properties` | unreviewed / production / fixtures | 8 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation visibility now defaults to hiding automated independent tasks, with a show agent-created tasks control and inbox participation exceptions. Source alignment reviewed; these fixture stories do not establish live server pagination or authorization. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. |
| [Tasks and conversations/Task creation and lifecycle/Product/Issue Management](../../ui/storybook/stories/tasks/issue-management.stories.tsx) | `tasks/properties`, `tasks/organize` | unreviewed / production / fixtures | 11 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation visibility now defaults to hiding automated independent tasks, with a show agent-created tasks control and inbox participation exceptions. Source alignment reviewed; these fixture stories do not establish live server pagination or authorization. Focused creation-visibility stories mount the production collection with simulated hierarchy reads and assert hidden roots, opt-in inclusion, and the unchecked filter control. Creation visibility examples and fixture setup now live in a support module so the surface matrix stays below 1000 lines; exported story IDs are unchanged. Creation visibility now shares the subscribed preference across modern and legacy controls; hierarchy child reads share a bounded rendering budget. Source alignment inspected; server behavior is tested separately. Hierarchy fetch and rendering budgets are separate; loaded context remains during paging. The large-tree UI regression exercises delayed reads in both list interfaces. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. |
| [Tasks and conversations/Task creation and lifecycle/Components/Entity pickers/Mobile](../../ui/storybook/stories/tasks/mobile-entity-pickers.stories.tsx) | `tasks/create` | unreviewed / production / fixtures | 3 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. |
| [Tasks and conversations/Task creation and lifecycle/Composer/New task](../../ui/storybook/stories/tasks/new-task-composer.stories.tsx) | `tasks/create`, `workspaces/binding` | current / production / fixtures | 40 / 25 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. WorktreePicker, ReuseWorktree, CreateWithReusedWorktree and NewWorktreeAfterReuse exercise workspace selection and submitted fixture request values. Actual admission/binding and execution remain outside this story. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. |
| [Tasks and conversations/Task creation and lifecycle/Composer/New task](../../ui/storybook/stories/tasks/new-task-composer.stories.tsx) | `tasks/create`, `workspaces/binding` | current / production / fixtures | 40 / 25 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. WorktreePicker, ReuseWorktree, CreateWithReusedWorktree and NewWorktreeAfterReuse exercise workspace selection and submitted fixture request values. Actual admission/binding and execution remain outside this story. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
| [Tasks and conversations/Task creation and lifecycle/Tasks/Runner activity preview](../../ui/storybook/stories/tasks/runner-activity.stories.tsx) | `tasks/discussion`, `runs-adapters/runs` | current / production / fixtures | 14 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. |
| [Tasks and conversations/Task creation and lifecycle/Paperclip/Successful Run Handoff](../../ui/storybook/stories/tasks/successful-run-handoff.stories.tsx) | `tasks/completion`, `recovery/diagnosis` | unreviewed / production / fixtures | 7 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Master reconciliation inspected assignment-history recipient suggestions and manual-selection retention; existing stories do not establish the new assignment-history behavior. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. The added verification recipe documents source-only retained-file repair; existing fixtures do not establish live provider recovery. |
| [Tasks and conversations/Task creation and lifecycle/Tasks/Task chat review fixes](../../ui/storybook/stories/tasks/task-chat-chain-of-thought.stories.tsx) | `tasks/discussion`, `runs-adapters/runs` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. |
@@ -191,9 +191,9 @@ Versioned documents, annotations, files, outputs, and artifact library.
| [Tasks and conversations/Documents, attachments, and work products/Product/Document Comments](../../ui/storybook/stories/documents-artifacts/document-comments.stories.tsx) | `documents-artifacts/annotations` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
| [Tasks and conversations/Documents, attachments, and work products/Document revisions](../../ui/storybook/stories/documents-artifacts/document-revisions.stories.tsx) | `documents-artifacts/documents` | current / production / fixtures | 9 / 4 | IssueDocumentsSection renders current/edit/historical preview and append-only restore with mutation error. Read/loading failures retain shipped fallback rendering; no persisted concurrency or authorization proof. |
| [Tasks and conversations/Documents, attachments, and work products/Components/File Viewer Sheet](../../ui/storybook/stories/documents-artifacts/file-viewer.stories.tsx) | `documents-artifacts/attachments` | unreviewed / production / fixtures | 13 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
| [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/01 Preview](../../ui/storybook/stories/documents-artifacts/html-artifacts/01-preview.stories.tsx) | `documents-artifacts/attachments`, `workspaces/files-git` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. |
| [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/02 Workspace](../../ui/storybook/stories/documents-artifacts/html-artifacts/02-workspace.stories.tsx) | `documents-artifacts/attachments`, `workspaces/files-git` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. |
| [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/03 Task journey](../../ui/storybook/stories/documents-artifacts/html-artifacts/03-task-journey.stories.tsx) | `documents-artifacts/attachments`, `workspaces/files-git` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. |
| [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/01 Preview](../../ui/storybook/stories/documents-artifacts/html-artifacts/01-preview.stories.tsx) | `documents-artifacts/attachments`, `workspaces/files-git` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
| [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/02 Workspace](../../ui/storybook/stories/documents-artifacts/html-artifacts/02-workspace.stories.tsx) | `documents-artifacts/attachments`, `workspaces/files-git` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
| [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/03 Task journey](../../ui/storybook/stories/documents-artifacts/html-artifacts/03-task-journey.stories.tsx) | `documents-artifacts/attachments`, `workspaces/files-git` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
| [Tasks and conversations/Documents, attachments, and work products/Task Chat/Rich Work Product Cards](../../ui/storybook/stories/documents-artifacts/rich-work-product-cards.stories.tsx) | `documents-artifacts/work-products` | unreviewed / production / fixtures | 9 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
| [Tasks and conversations/Documents, attachments, and work products/Tasks/Artifact Gallery](../../ui/storybook/stories/documents-artifacts/task-artifact-gallery.stories.tsx) | `documents-artifacts/library`, `documents-artifacts/work-products` | unreviewed / production / fixtures | 8 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
| [Tasks and conversations/Documents, attachments, and work products/Tasks/Text file tabs](../../ui/storybook/stories/documents-artifacts/task-text-files.stories.tsx) | `documents-artifacts/attachments` | unreviewed / production / fixtures | 7 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. |
@@ -295,9 +295,9 @@ Project lifecycle, task context, repository configuration, and defaults.
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
| --- | --- | --- | --- | --- |
| [Projects and execution/Projects and repositories/Proposals/Project repos/New project](../../ui/storybook/stories/projects/new-project-repos.stories.tsx) | `projects/repositories` | proposed / mixed / fixtures | 18 / 5 | RepositoryEditor is shipped, but NewProjectPrototype composes its own creation flow. Preserve proposal until route parity is checked. |
| [Projects and execution/Projects and repositories/Workspaces/Project runtime provision command](../../ui/storybook/stories/projects/project-execution-workspace-strategy.stories.tsx) | `projects/defaults`, `workspaces/provisioning` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. |
| [Projects and execution/Projects and repositories/Workspaces/Project runtime provision command](../../ui/storybook/stories/projects/project-execution-workspace-strategy.stories.tsx) | `projects/defaults`, `workspaces/provisioning` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
| [Projects and execution/Projects and repositories/Proposals/Project repos/Configuration](../../ui/storybook/stories/projects/project-repos-configuration.stories.tsx) | `projects/repositories` | proposed / mixed / fixtures | 12 / 0 | Production properties plus local configuration page composition; repository interaction is simulated. |
| [Projects and execution/Projects and repositories/Product/Projects Goals Workspaces](../../ui/storybook/stories/projects/projects-goals-workspaces.stories.tsx) | `projects/lifecycle`, `goals/hierarchy`, `workspaces/binding` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed nullable project references and guarded runtime invalidations. Existing summaries remain project-bound and retain their routes; null-project execution workspace rendering and controls remain a coverage gap. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. |
| [Projects and execution/Projects and repositories/Product/Projects Goals Workspaces](../../ui/storybook/stories/projects/projects-goals-workspaces.stories.tsx) | `projects/lifecycle`, `goals/hierarchy`, `workspaces/binding` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed nullable project references and guarded runtime invalidations. Existing summaries remain project-bound and retain their routes; null-project execution workspace rendering and controls remain a coverage gap. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
### Goals and work alignment
@@ -317,14 +317,14 @@ Provisioning, task bindings, services/logs, Git/files, and closure.
| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |
| --- | --- | --- | --- | --- |
| [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Task activity](../../ui/storybook/stories/workspaces/browser-use-activity.stories.tsx) | `workspaces/services`, `tasks/discussion` | unreviewed / production / fixtures | 7 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. |
| [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Footer](../../ui/storybook/stories/workspaces/browser-use-footer.stories.tsx) | `workspaces/services`, `tasks/discussion` | unreviewed / production / fixtures | 21 / 2 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. |
| [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Panel](../../ui/storybook/stories/workspaces/browser-use.stories.tsx) | `workspaces/services`, `tasks/discussion` | unreviewed / production / fixtures | 28 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. |
| [Projects and execution/Workspaces, services, and files/Components/SearchableSelect/Workspace picker](../../ui/storybook/stories/workspaces/searchable-select.stories.tsx) | `workspaces/binding` | unreviewed / production / fixtures | 9 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. |
| [Projects and execution/Workspaces, services, and files/Close readiness](../../ui/storybook/stories/workspaces/workspace-close.stories.tsx) | `workspaces/closure` | current / production / fixtures | 7 / 7 | Readiness dialog with loading, inspection failure/403, running-task block and pending/accepted archive response. No real workspace cleanup, Git removal or service shutdown is performed. |
| [Projects and execution/Workspaces, services, and files/Issue/Workspace File Browser](../../ui/storybook/stories/workspaces/workspace-file-browser.stories.tsx) | `workspaces/files-git` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. |
| [Projects and execution/Workspaces, services, and files/Workspaces/Runtime provisioning status](../../ui/storybook/stories/workspaces/workspace-runtime-provisioning.stories.tsx) | `workspaces/provisioning` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed ExecutionWorkspaceDetail null-project handling. This suite mounts only the unchanged RuntimeProvisionStatusValue inside a local context row; it does not exercise the full detail page or projectless queries. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. |
| [Projects and execution/Workspaces, services, and files/Workspaces/Service control bar](../../ui/storybook/stories/workspaces/workspace-service-control-bar.stories.tsx) | `workspaces/services` | unreviewed / production / fixtures | 16 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. |
| [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Task activity](../../ui/storybook/stories/workspaces/browser-use-activity.stories.tsx) | `workspaces/services`, `tasks/discussion` | unreviewed / production / fixtures | 7 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
| [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Footer](../../ui/storybook/stories/workspaces/browser-use-footer.stories.tsx) | `workspaces/services`, `tasks/discussion` | unreviewed / production / fixtures | 21 / 2 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
| [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Panel](../../ui/storybook/stories/workspaces/browser-use.stories.tsx) | `workspaces/services`, `tasks/discussion` | unreviewed / production / fixtures | 28 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
| [Projects and execution/Workspaces, services, and files/Components/SearchableSelect/Workspace picker](../../ui/storybook/stories/workspaces/searchable-select.stories.tsx) | `workspaces/binding` | unreviewed / production / fixtures | 9 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
| [Projects and execution/Workspaces, services, and files/Close readiness](../../ui/storybook/stories/workspaces/workspace-close.stories.tsx) | `workspaces/closure` | current / production / fixtures | 7 / 7 | Readiness dialog with loading, inspection failure/403, running-task block and pending/accepted archive response. No real workspace cleanup, Git removal or service shutdown is performed. The separate remote-source recipe requires Git isolation and preservation of unpublished task work; these close-readiness fixtures do not exercise repository cloning or reconstruction. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
| [Projects and execution/Workspaces, services, and files/Issue/Workspace File Browser](../../ui/storybook/stories/workspaces/workspace-file-browser.stories.tsx) | `workspaces/files-git` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
| [Projects and execution/Workspaces, services, and files/Workspaces/Runtime provisioning status](../../ui/storybook/stories/workspaces/workspace-runtime-provisioning.stories.tsx) | `workspaces/provisioning` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed ExecutionWorkspaceDetail null-project handling. This suite mounts only the unchanged RuntimeProvisionStatusValue inside a local context row; it does not exercise the full detail page or projectless queries. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
| [Projects and execution/Workspaces, services, and files/Workspaces/Service control bar](../../ui/storybook/stories/workspaces/workspace-service-control-bar.stories.tsx) | `workspaces/services` | unreviewed / production / fixtures | 16 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings. |
### Execution environments
+28 -28
View File
@@ -1235,7 +1235,7 @@
"status": "unreviewed",
"implementation": "production",
"boundary": "fixtures",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work.",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/tasks.md",
@@ -1246,7 +1246,7 @@
"ui/storybook/fixtures/browser-use.tsx",
"ui/storybook/stories/workspaces/browser-use-activity.stories.tsx"
],
"fingerprint": "f0e77b99e99a69532f45d58546a2324893543911771e850a47ca3452818bcf3d",
"fingerprint": "00c08cb6566e12b7fd9dce179296397813ea3e2ab4ee47d27cd1ca948496ca15",
"stableId": "tasks-browser-use-task-activity",
"sidebarTitle": "Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Task activity"
},
@@ -1260,7 +1260,7 @@
"status": "unreviewed",
"implementation": "production",
"boundary": "fixtures",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work.",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/tasks.md",
@@ -1270,7 +1270,7 @@
"ui/storybook/fixtures/browser-use.tsx",
"ui/storybook/stories/workspaces/browser-use-footer.stories.tsx"
],
"fingerprint": "b60b13c6ff934b038616eee7da6aeeaac0f595aeb7c8d80d2a2f787c7bb0dbac",
"fingerprint": "fda7ec26101f5b6fe68da1f7b6a96c364878c5f14884d639e406412e8341959f",
"stableId": "tasks-browser-use-footer",
"sidebarTitle": "Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Footer"
},
@@ -1306,7 +1306,7 @@
"status": "unreviewed",
"implementation": "production",
"boundary": "fixtures",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work.",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/tasks.md",
@@ -1317,7 +1317,7 @@
"ui/storybook/fixtures/paperclipData.ts",
"ui/storybook/stories/workspaces/browser-use.stories.tsx"
],
"fingerprint": "7dea88462587a256933271c48cfef7ff339f55acd1f77a38bf89ad537c63f3ae",
"fingerprint": "dafe2721334717ad745e72100e23577dbe3816a3b9ee9224a114ff84fbbb4729",
"stableId": "tasks-browser-use-panel",
"sidebarTitle": "Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Panel"
},
@@ -2710,7 +2710,7 @@
"status": "unreviewed",
"implementation": "production",
"boundary": "fixtures",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence.",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/documents-artifacts.md",
@@ -2719,7 +2719,7 @@
"ui/storybook/stories/documents-artifacts/html-artifacts/01-preview.stories.tsx",
"ui/storybook/support/html-artifacts/fixtures.ts"
],
"fingerprint": "9f96f1e4abf214d663906e05b0a8ce231f06217fc5f022415a84df33c1d0ac96",
"fingerprint": "404690079b8b85bf7aaab867266a14096f7d57bea2665032efba2f82617008df",
"stableId": "html-artifacts-01-preview",
"sidebarTitle": "Tasks and conversations/Documents, attachments, and work products/HTML artifacts/01 Preview"
},
@@ -2733,7 +2733,7 @@
"status": "unreviewed",
"implementation": "production",
"boundary": "fixtures",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence.",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/documents-artifacts.md",
@@ -2743,7 +2743,7 @@
"ui/storybook/stories/documents-artifacts/html-artifacts/02-workspace.stories.tsx",
"ui/storybook/support/html-artifacts/fixtures.ts"
],
"fingerprint": "47c33fce5f296031d43f31d7f099abcef40393cfe51e565db542b7e8230a00a3",
"fingerprint": "4ced4be164f64cb1dd0158cf3afc9939e63e4447a3911b33e664544277693f0b",
"stableId": "html-artifacts-02-workspace",
"sidebarTitle": "Tasks and conversations/Documents, attachments, and work products/HTML artifacts/02 Workspace"
},
@@ -2757,7 +2757,7 @@
"status": "unreviewed",
"implementation": "production",
"boundary": "fixtures",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence.",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/documents-artifacts.md",
@@ -2770,7 +2770,7 @@
"ui/storybook/stories/documents-artifacts/html-artifacts/03-task-journey.stories.tsx",
"ui/storybook/support/html-artifacts/fixtures.ts"
],
"fingerprint": "470396b938981d10ddc74d03cec0808aaa1fb305dcdeb8056b308c2752f7cea7",
"fingerprint": "c5cb892dd95ec005e52e6c61de414aedb5fe2a443a729038767028edd3c52b6d",
"stableId": "html-artifacts-03-task-journey",
"sidebarTitle": "Tasks and conversations/Documents, attachments, and work products/HTML artifacts/03 Task journey"
},
@@ -3230,7 +3230,7 @@
"status": "current",
"implementation": "production",
"boundary": "fixtures",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. WorktreePicker, ReuseWorktree, CreateWithReusedWorktree and NewWorktreeAfterReuse exercise workspace selection and submitted fixture request values. Actual admission/binding and execution remain outside this story. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work.",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. WorktreePicker, ReuseWorktree, CreateWithReusedWorktree and NewWorktreeAfterReuse exercise workspace selection and submitted fixture request values. Actual admission/binding and execution remain outside this story. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/tasks.md",
@@ -3240,7 +3240,7 @@
"ui/storybook/stories/tasks/new-task-composer.stories.tsx",
"maps/feature-map/workspaces.md"
],
"fingerprint": "db3b18dcf012cfd2174900456dd8f8189b2f0b4287d84fbb62562bb0325a0001",
"fingerprint": "efb21d7fdb67867b886668295e55ac1e05f67c71029fb85b7f341c9662217705",
"stableId": "composer-new-task",
"sidebarTitle": "Tasks and conversations/Task creation and lifecycle/Composer/New task"
},
@@ -4003,7 +4003,7 @@
"status": "unreviewed",
"implementation": "production",
"boundary": "fixtures",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence.",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/projects.md",
@@ -4012,7 +4012,7 @@
"ui/storybook/fixtures/paperclipData.ts",
"ui/storybook/stories/projects/project-execution-workspace-strategy.stories.tsx"
],
"fingerprint": "618a389f8aa005657fc633c9a0ebdc34e0fc77df6d1cc8d0474330590d4c081c",
"fingerprint": "89c541df010f3ea0dafa6ed29e02882ffbbecca1aaa90a2606176ffe6b8f9f4d",
"stableId": "workspaces-project-runtime-provision-command",
"sidebarTitle": "Projects and execution/Projects and repositories/Workspaces/Project runtime provision command"
},
@@ -4056,7 +4056,7 @@
"status": "unreviewed",
"implementation": "production",
"boundary": "fixtures",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed nullable project references and guarded runtime invalidations. Existing summaries remain project-bound and retain their routes; null-project execution workspace rendering and controls remain a coverage gap. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence.",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed nullable project references and guarded runtime invalidations. Existing summaries remain project-bound and retain their routes; null-project execution workspace rendering and controls remain a coverage gap. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/goals.md",
@@ -4072,7 +4072,7 @@
"ui/storybook/fixtures/paperclipData.ts",
"ui/storybook/stories/projects/projects-goals-workspaces.stories.tsx"
],
"fingerprint": "83a6815f2ceea9c18eb9145f2abf7f90588dafe833c20abcd93050ee1c66114a",
"fingerprint": "e7330baf164a462aaef557989a6334fd61e9b1f378dd6674f34f4e7bbe8f03bb",
"stableId": "product-projects-goals-workspaces",
"sidebarTitle": "Projects and execution/Projects and repositories/Product/Projects Goals Workspaces"
},
@@ -4748,14 +4748,14 @@
"status": "unreviewed",
"implementation": "production",
"boundary": "fixtures",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence.",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/workspaces.md",
"ui/src/components/SearchableSelect.tsx",
"ui/storybook/stories/workspaces/searchable-select.stories.tsx"
],
"fingerprint": "524803af7c926b60e8c636031f703d4bb4abe45cd4832c7985f81c8f4d0cb035",
"fingerprint": "e34c07642d243f0011ef4c589cfd061f1b20477f10ee3e299f5c197982acee3c",
"stableId": "components-searchableselect-workspace-picker",
"sidebarTitle": "Projects and execution/Workspaces, services, and files/Components/SearchableSelect/Workspace picker"
},
@@ -5623,7 +5623,7 @@
"status": "unreviewed",
"implementation": "production",
"boundary": "fixtures",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence.",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/workspaces.md",
@@ -5631,7 +5631,7 @@
"ui/src/components/IssueWorkspaceCard.tsx",
"ui/storybook/stories/workspaces/workspace-file-browser.stories.tsx"
],
"fingerprint": "980fcd9a6905ac1eae10efeb7979dd8b3769c2cc7d625d4e0ba4b091f4b4d614",
"fingerprint": "a37dfdde49f89e907842859bfebde7d3c88a4ebabce4d003a9e4f722b73bb098",
"stableId": "issue-workspace-file-browser",
"sidebarTitle": "Projects and execution/Workspaces, services, and files/Issue/Workspace File Browser"
},
@@ -5644,14 +5644,14 @@
"status": "unreviewed",
"implementation": "production",
"boundary": "fixtures",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed ExecutionWorkspaceDetail null-project handling. This suite mounts only the unchanged RuntimeProvisionStatusValue inside a local context row; it does not exercise the full detail page or projectless queries. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence.",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed ExecutionWorkspaceDetail null-project handling. This suite mounts only the unchanged RuntimeProvisionStatusValue inside a local context row; it does not exercise the full detail page or projectless queries. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/workspaces.md",
"ui/src/pages/ExecutionWorkspaceDetail.tsx",
"ui/storybook/stories/workspaces/workspace-runtime-provisioning.stories.tsx"
],
"fingerprint": "f8a7316fb6637151d8da32b637136408cd6fd9d505fe2a2a47f3c0d670b0eeec",
"fingerprint": "4a4e9dc99829632e221a1c558f54502bbba8b1547768d0fdce97cad3c0c5fc02",
"stableId": "workspaces-runtime-provisioning-status",
"sidebarTitle": "Projects and execution/Workspaces, services, and files/Workspaces/Runtime provisioning status"
},
@@ -5664,14 +5664,14 @@
"status": "unreviewed",
"implementation": "production",
"boundary": "fixtures",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence.",
"notes": "Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. The added verification recipe documents remote-source isolation and lifecycle guards; existing fixtures do not establish live provider recovery. The source recipe now excludes literal and task-independent branch templates; these fixtures do not execute that backend admission guard. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/workspaces.md",
"ui/src/components/WorkspaceServiceControlBar.tsx",
"ui/storybook/stories/workspaces/workspace-service-control-bar.stories.tsx"
],
"fingerprint": "b9cd51c58b31ab445e2997b9f2a23c7a7b3f4ecd7b6dd39fc0f35ae0779311df",
"fingerprint": "01ffe1c604d850af606be5ebd4a4ba652716ef6b1ca833d6aa6f8d564ced64c1",
"stableId": "workspaces-service-control-bar",
"sidebarTitle": "Projects and execution/Workspaces, services, and files/Workspaces/Service control bar"
},
@@ -5803,7 +5803,7 @@
"status": "current",
"implementation": "production",
"boundary": "fixtures",
"notes": "Readiness dialog with loading, inspection failure/403, running-task block and pending/accepted archive response. No real workspace cleanup, Git removal or service shutdown is performed.",
"notes": "Readiness dialog with loading, inspection failure/403, running-task block and pending/accepted archive response. No real workspace cleanup, Git removal or service shutdown is performed. The separate remote-source recipe requires Git isolation and preservation of unpublished task work; these close-readiness fixtures do not exercise repository cloning or reconstruction. The remote fallback requires current default provenance; these examples do not prove admission for explicit or inherited checkout bindings.",
"reviewedAt": "2026-10-13",
"sources": [
"maps/feature-map/workspaces.md",
@@ -5815,7 +5815,7 @@
],
"stableId": "feature-workspace-close",
"sidebarTitle": "Projects and execution/Workspaces, services, and files/Close readiness",
"fingerprint": "b3d1d32d7f1f51f837ecd04f64ca1b6b7e8b36c921d54b638898ef06512617cc"
"fingerprint": "08b1e604b188130184296c8aee286f8dde12c8026654f0760388bd492e1ffe5b"
},
{
"file": "ui/storybook/stories/budgets-costs/budget-resolution.stories.tsx",
@@ -26,6 +26,7 @@ import { instanceSettingsService } from "../services/instance-settings.ts";
import { environmentRuntimeService } from "../services/environment-runtime.js";
import { findNativeChatWorkspaceScope } from "../services/native-runtime/native-chat-workspace.js";
import { drainHeartbeatRunsToQuiescence } from "./helpers/drain-heartbeat-runs.js";
import { executionWorkspaceService } from "../services/execution-workspaces.js";
const execute = vi.hoisted(() => vi.fn(async (_input: any) => ({ exitCode: 0, signal: null, timedOut: false })));
vi.mock("../adapters/index.js", () => ({
@@ -149,6 +150,7 @@ suite("heartbeat physical workspace source trust", () => {
["configured", "both", "local_default"], ["builtin", "both", "local_default"],
["configured", "both", "local_new_default"], ["builtin", "both", "local_new_default"],
["configured", "both", "local_new_primary"], ["builtin", "both", "local_new_primary"],
["configured", "agent", "local_effective_strategy"], ["builtin", "agent", "local_effective_strategy"],
] as const)("places standard source %s provisioning with %s grants on %s", async (provisionKind, grants, placement) => {
const implicitDefault = placement.startsWith("local_");
const newerDefault = placement === "local_new_default" || placement === "local_new_primary";
@@ -171,16 +173,18 @@ suite("heartbeat physical workspace source trust", () => {
} });
if (newerDefault) await db.insert(projectWorkspaces).values({ id: randomUUID(), companyId, projectId, name: "Older source", sourceType: "local_path", cwd: path.join(root, companyId, "older-source"), isPrimary: placement === "local_new_default", createdAt: new Date(0) });
await db.insert(projectWorkspaces).values({ id: sourceId, companyId, projectId, name: "Source", sourceType: "local_path", cwd, repoUrl: pathToFileURL(cwd).href, isPrimary: placement !== "local_new_default" });
await db.insert(agents).values({ id: agentId, companyId, name: "Reader", status: "idle", adapterType: "codex_local", adapterConfig: { fullAuto: grants === "both" }, permissions: {} });
await db.insert(agents).values({ id: agentId, companyId, name: "Reader", status: "idle", adapterType: "codex_local", adapterConfig: { fullAuto: grants === "both",
...(placement === "local_effective_strategy" ? { workspaceStrategy: { type: "project_primary" } } : {}) }, permissions: {} });
await db.insert(companyMemberships).values({ companyId, principalType: "agent", principalId: agentId, status: "active", membershipRole: "member" });
const taskInput = { id: issueId, companyId, projectId: implicitDefault ? projectId : null, title: "Read-only isolated source", status: "todo" as const, assigneeAgentId: agentId,
projectWorkspaceId: sourceId, executionWorkspaceSettings: { mode: "isolated_workspace", workspaceStrategy: { type: "git_worktree",
worktreeParentDir: path.join(root, companyId, "worktrees"), ...(provisionKind === "configured" ? { provisionCommand: `printf host-script-ran > '${marker}'` } : {}) } },
workspaceSelection: implicitDefault ? null : { version: 1 as const, source: "explicit" as const, selection: { kind: "configured_source" as const, projectWorkspaceId: sourceId, mode: "managed_isolated" as const } } };
if (newerDefault) {
if (newerDefault || placement === "local_effective_strategy") {
const { projectWorkspaceId: _source, workspaceSelection: _selection, ...createInput } = taskInput;
const created = await issueService(db).create(companyId, createInput);
expect(created.projectWorkspaceId).toBe(sourceId);
expect(created.workspaceSelection).toMatchObject({ source: "project" });
} else await db.insert(issues).values(taskInput);
if (grants !== "none") await db.insert(principalPermissionGrants).values({ companyId, principalType: "agent", principalId: agentId,
permissionKey: "tasks:assign_scope", scope: { projectId, assigneeAgentId: agentId } });
@@ -189,6 +193,12 @@ suite("heartbeat physical workspace source trust", () => {
const actor = { type: "agent" as const, agentId, companyId, source: "agent_jwt" as const, onBehalfOfUserId: "responsible-user" };
expect(await accessService(db).decide({ actor, action: "project:read", resource: { type: "project", companyId, projectId } })).toMatchObject({ allowed: true });
expect(await accessService(db).decide({ actor, action: "project:write_workspace", resource: { type: "issue", companyId, projectId, issueId, assigneeAgentId: agentId }, scope: { projectId, assigneeAgentId: agentId } })).toMatchObject({ allowed: grants === "both" });
if (placement === "local_effective_strategy") {
const selection = { kind: "configured_source" as const, projectWorkspaceId: sourceId, mode: "managed_isolated" as const };
await expect(executionWorkspaceService(db).validateSelection({ companyId, actor, issueId, selection })).rejects.toThrow("project:write_workspace");
await expect(executionWorkspaceService(db).validateSelection({ companyId, actor, issueId,
selection: { ...selection, mode: "shared" }, preserveImplicitDefaultSettings: true })).rejects.toThrow("Task workspace default changed");
}
const environmentId = randomUUID();
await db.insert(environments).values({ id: environmentId, name: `Standard sandbox ${companyId}`, driver: "sandbox", status: "active", config: { provider: "fake", image: "fake:test" } });
await db.update(agents).set({ defaultEnvironmentId: placement === "sandbox" ? environmentId : null }).where(eq(agents.id, agentId));
@@ -205,6 +215,7 @@ suite("heartbeat physical workspace source trust", () => {
const [workspace] = await db.select().from(executionWorkspaces).where(eq(executionWorkspaces.id, bound.executionWorkspaceId!));
expect(workspace.cwd).not.toBe(cwd);
expect(await readFile(path.join(workspace.cwd!, "source.txt"), "utf8")).toBe("read-only source bytes");
expect(await readFile(path.join(cwd, "source.txt"), "utf8")).toBe("read-only source bytes");
const calls = execute.mock.calls.filter(([input]) => input.runId === run!.id);
expect(calls).toHaveLength(1);
expect(calls[0][0].context.paperclipEnvironment.driver).toBe(placement === "sandbox" ? "sandbox" : "local");
@@ -213,7 +224,6 @@ suite("heartbeat physical workspace source trust", () => {
await rm(marker, { force: true });
if (grants === "none") await rm(workspace.cwd!, { recursive: true, force: true });
await db.update(issues).set({ status: "todo" }).where(eq(issues.id, issueId));
const { executionWorkspaceService } = await import("../services/execution-workspaces.js");
if (placement === "local" && grants === "both") await db.update(issues).set({ projectId, workspaceSelection: null }).where(eq(issues.id, issueId));
await executionWorkspaceService(db).update(workspace.id, { metadata: { ...workspace.metadata,
_controllerProvisionCopyOnly: false, config: { provisionCommand: `printf host-script-ran > '${marker}'` } } });
@@ -3921,6 +3921,7 @@ process.stderr.write(${JSON.stringify(stderr)}, () => { process.exitCode = ${cod
const cleanup = await cleanupExecutionWorkspaceArtifacts({
workspace: {
companyId: "company-1",
id: "execution-workspace-1",
cwd: workspace.cwd,
providerType: "git_worktree",
@@ -3997,6 +3998,7 @@ process.stderr.write(${JSON.stringify(stderr)}, () => { process.exitCode = ${cod
const cleanup = await cleanupExecutionWorkspaceArtifacts({
workspace: {
companyId: "company-1",
id: "execution-workspace-reused-cleanup",
cwd: reused.cwd,
providerType: "git_worktree",
@@ -4065,6 +4067,7 @@ process.stderr.write(${JSON.stringify(stderr)}, () => { process.exitCode = ${cod
const cleanup = await cleanupExecutionWorkspaceArtifacts({
workspace: {
companyId: "company-1",
id: "execution-workspace-1",
cwd: workspace.cwd,
providerType: "git_worktree",
@@ -4129,6 +4132,7 @@ process.stderr.write(${JSON.stringify(stderr)}, () => { process.exitCode = ${cod
const cleanup = await cleanupExecutionWorkspaceArtifacts({
workspace: {
companyId: "company-1",
id: "execution-workspace-1",
cwd: workspace.cwd,
providerType: "git_worktree",
@@ -4205,6 +4209,7 @@ process.stderr.write(${JSON.stringify(stderr)}, () => { process.exitCode = ${cod
await cleanupExecutionWorkspaceArtifacts({
workspace: {
companyId: "company-1",
id: "execution-workspace-1",
cwd: workspace.cwd,
providerType: "git_worktree",
@@ -10011,6 +10016,7 @@ describe("realizeExecutionWorkspace with an exact existing branch", () => {
) {
return {
workspace: {
companyId: "company-1",
id: "execution-workspace-1",
cwd: workspace.cwd,
providerType: "git_worktree",
+16 -4
View File
@@ -1,4 +1,5 @@
import { assertTaskWorkspaceAccess, assertTaskWorkspaceSourceProjectAccess } from "./task-workspace-source-access.js";
import { verifiedRecordedIsolatedRemoteSource } from "./isolated-remote-workspace-source.js";
import { parseObject } from "../adapters/utils.js";
import { resolveCoreTrustPreset } from "./trust-preset-resolver.js";
import { CONTROLLER_PROVISION_COPY_ONLY, workspaceRequiresCopyOnlyProvisioning } from "./workspace-host-provision.js";
@@ -1397,7 +1398,7 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
return resolveEffectiveWorkspaceStrategyType(mode, config);
}
async function validateSelection(input: { taskCreation?: TaskWorkspaceCreationTrustContext; companyId: string; actor: AuthorizationActor; selection: TaskWorkspaceSelection; issueId?: string | null; parentIssueId?: string | null; assigneeAgentId?: string | null; assigneeUserId?: string | null; assigneeAdapterOverrides?: unknown; projectId?: string | null; executionPolicy?: unknown }, reader: Db | DbTransaction = db) {
async function validateSelection(input: { taskCreation?: TaskWorkspaceCreationTrustContext; companyId: string; actor: AuthorizationActor; selection: TaskWorkspaceSelection; issueId?: string | null; parentIssueId?: string | null; assigneeAgentId?: string | null; assigneeUserId?: string | null; assigneeAdapterOverrides?: unknown; projectId?: string | null; executionPolicy?: unknown; preserveImplicitDefaultSettings?: boolean }, reader: Db | DbTransaction = db) {
const selection = taskWorkspaceSelectionSchema.parse(input.selection);
if (selection.kind === "existing") {
const [workspace] = await reader.select().from(executionWorkspaces).where(and(
@@ -1422,15 +1423,24 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
));
if (!source) throw notFound("Workspace source is unavailable or inaccessible");
const mode = selection.mode === "shared" ? "shared_workspace" : "isolated_workspace";
let executionWorkspaceSettings: Record<string, unknown> = { mode };
if (input.preserveImplicitDefaultSettings) {
const [task] = input.issueId ? await reader.select({ settings: issues.executionWorkspaceSettings,
intent: issues.workspaceSelection }).from(issues).where(and(eq(issues.id, input.issueId),
eq(issues.companyId, input.companyId), await issueReadSqlCondition(reader, input.actor))) : [];
if (!task || !["project", "task_default"].includes(task.intent?.source ?? "")
|| !isDeepStrictEqual(task.intent?.selection, selection)) throw conflict("Task workspace default changed");
executionWorkspaceSettings = { ...parseObject(task.settings), mode };
}
const strategy = await resolveSourceWorkspaceStrategy({ ...input, typedSelection: true,
executionWorkspaceSettings: { mode }, executionWorkspacePolicy: source.executionWorkspacePolicy }, reader);
executionWorkspaceSettings, executionWorkspacePolicy: source.executionWorkspacePolicy }, reader);
await assertTaskWorkspaceSourceProjectAccess(reader, input.actor, input.companyId, source.projectId, {
write: strategy !== "git_worktree",
issueId: input.issueId, parentIssueId: input.parentIssueId,
assigneeAgentId: input.assigneeAgentId, assigneeUserId: input.assigneeUserId,
});
return { executionWorkspaceId: null, projectWorkspaceId: source.id, executionWorkspacePreference: null,
executionWorkspaceSettings: { mode: selection.mode === "shared" ? "shared_workspace" : "isolated_workspace" } };
executionWorkspaceSettings };
}
return { executionWorkspaceId: null, projectWorkspaceId: null, executionWorkspacePreference: null,
executionWorkspaceSettings: { mode: "shared_workspace" } };
@@ -3280,7 +3290,9 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
.where(and(eq(projects.companyId, snapshot.companyId), snapshot.projectId ? eq(projects.id, snapshot.projectId) : sql`false`))
.then((rows) => parseProjectExecutionWorkspacePolicy(rows[0]?.executionWorkspacePolicy)),
]);
let resolvedBaseCwd = projectWorkspace?.cwd ?? null;
let resolvedBaseCwd: string | null;
try { resolvedBaseCwd = await verifiedRecordedIsolatedRemoteSource(snapshot) ?? projectWorkspace?.cwd ?? null; }
catch { return { ok: false, code: "rebuild_failed", message: "Failed to rebuild the execution workspace" }; }
if (isClosedExecutionWorkspaceStatus(snapshot.status) && resolvedBaseCwd == null && snapshot.strategyType === "git_worktree" && snapshot.projectId) {
try {
const managedWorkspace = await ensureManagedProjectWorkspace({
@@ -414,7 +414,8 @@ export async function selectHeartbeatEnvironment(db: Db, input: HeartbeatEnviron
if (taskWorkspaceIntent && !boundSourceWorkspace && !nativeRecoveryExecutionWorkspaceId) {
await executionWorkspacesSvc.validateSelection({ companyId: agent.companyId,
actor: { type: "agent", agentId: agent.id, companyId: agent.companyId, runId: run.id, source: "agent_jwt", onBehalfOfUserId: responsibleUserId === "local-board" ? null : responsibleUserId },
selection: taskWorkspaceIntent.selection, issueId });
selection: taskWorkspaceIntent.selection, issueId,
preserveImplicitDefaultSettings: ["project", "task_default"].includes(taskWorkspaceIntent.source) });
}
const explicitTaskDirectory = taskWorkspaceIntent?.selection.kind === "task_directory";
const useIsolatedTaskDirectory = issueRef !== null && shouldUseIsolatedTaskDirectory({
@@ -1,5 +1,5 @@
import { execFile as execFileCallback } from "node:child_process";
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile, access, rename } from "node:fs/promises";
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile, access, rename, realpath } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
@@ -19,6 +19,10 @@ import { workspaceOperationService } from "../workspace-operations.js";
import { nativeWorkspaceSyncInternals, prepareNativeWorkspaceSync } from "../native-runtime/native-workspace-sync.js";
import { createHeartbeatWorkspaceResolver, prepareProjectRepositoryWorkspaces, resolveExecutionWorkspaceReuseRequestForIssue } from "./workspaces.js";
import { prepareHeartbeatWorkspace, type HeartbeatWorkspacePreparationInput } from "./workspace-preparation.js";
import * as managedRepositoryCheckout from "../managed-repository-checkout.js";
import { isolatedRemoteSourcePath } from "../isolated-remote-workspace-source.js";
import { cleanupExecutionWorkspaceArtifacts } from "../workspace-runtime.js";
import { issueService } from "../issues.js";
const execFile = promisify(execFileCallback);
const support = await getEmbeddedPostgresTestSupport();
@@ -116,6 +120,141 @@ describe.skipIf(!support.supported)("heartbeat workspace preparation boundary",
expect(input.controllerLease.assertOwned).toHaveBeenCalledTimes(2);
});
it.each(["create", "failed-save", "shared-retry-after-failed-save", "literal-branch", "legacy-explicit-source", "inherited-source", "legacy-untyped-source", "changed-source-intent", "context-source"] as const)("preserves restricted source isolation during %s", async scenario => {
const { input, source, issue: originalIssue } = await fixture(true);
await db.update(companies).set({ issueCounter: 1 }).where(eq(companies.id, originalIssue.companyId));
const issue = await issueService(db).create(originalIssue.companyId, {
title: "Prepare the selected source", projectId: originalIssue.projectId,
assigneeAgentId: input.agent.id, executionWorkspaceSettings: { mode: "isolated_workspace" },
...(scenario === "legacy-explicit-source" ? { projectWorkspaceId: originalIssue.projectWorkspaceId } : {}),
...(scenario === "inherited-source" ? { parentId: originalIssue.id } : {}),
workspaceSelectionActor: { type: "agent", source: "agent_jwt", companyId: originalIssue.companyId, agentId: input.agent.id },
});
input.task.issueRef = issue;
input.task.issueId = issue.id;
input.task.context.issueId = issue.id;
input.task.workspaceSelection = issue.workspaceSelection ?? null;
await db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, input.run.id));
const [run] = await db.insert(heartbeatRuns).values({ companyId: issue.companyId, agentId: input.agent.id,
issueId: issue.id, scopeKind: "issue", status: "running", runtimeMode: "legacy" }).returning();
input.run = run;
if (scenario === "legacy-untyped-source") {
await db.update(issues).set({ workspaceSelection: null }).where(eq(issues.id, issue.id));
input.task.workspaceSelection = null;
} else if (scenario === "changed-source-intent") {
await db.update(issues).set({ workspaceSelection: { ...issue.workspaceSelection!, source: "explicit" } }).where(eq(issues.id, issue.id));
} else if (scenario === "context-source") {
await db.update(issues).set({ projectWorkspaceId: null, workspaceSelection: null }).where(eq(issues.id, issue.id));
input.task.issueRef = { ...issue, projectWorkspaceId: null };
input.task.workspaceSelection = null;
input.task.context.projectWorkspaceId = issue.projectWorkspaceId;
}
const remoteUrl = "https://example.test/public/repository.git";
await db.update(projectWorkspaces).set({ repoUrl: remoteUrl }).where(eq(projectWorkspaces.id, issue.projectWorkspaceId!));
const publishedSource = join(home, `published-${issue.id}`);
await execFile("git", ["clone", "--no-hardlinks", source, publishedSource]);
const [privateIssue] = await db.insert(issues).values({ companyId: issue.companyId, title: "Private work",
visibility: "private", projectId: issue.projectId }).returning();
const privateCwd = join(source, "private-work");
await execFile("git", ["-C", source, "worktree", "add", "-b", "restricted-task", privateCwd, "main"]);
await writeFile(join(privateCwd, "confidential.txt"), "Private source must not be copied");
await execFile("git", ["-C", privateCwd, "add", "confidential.txt"]);
await execFile("git", ["-C", privateCwd, "commit", "-m", "Restricted task work"]);
const privateCommit = (await execFile("git", ["-C", privateCwd, "rev-parse", "HEAD"])).stdout.trim();
const [privateWorkspace] = await db.insert(executionWorkspaces).values({ companyId: issue.companyId,
name: "Private workspace", projectId: issue.projectId, sourceIssueId: privateIssue.id, cwd: privateCwd, mode: "isolated_workspace",
strategyType: "git_worktree", providerType: "git_worktree", providerRef: privateCwd, branchName: "restricted-task", status: "idle" }).returning();
await db.update(issues).set({ executionWorkspaceId: privateWorkspace.id }).where(eq(issues.id, privateIssue.id));
const clone = vi.spyOn(managedRepositoryCheckout, "materializeManagedProjectWorkspace").mockImplementation(async (cwd, options) => {
expect(options.repoUrl).toBe(remoteUrl);
expect(options.localSource).toBeUndefined();
expect(options.resolveGitAuth).toBeTypeOf("function");
await execFile("git", ["clone", "--no-hardlinks", publishedSource, cwd]);
await execFile("git", ["-C", cwd, "remote", "set-url", "origin", remoteUrl]);
return { cwd, warning: null };
});
const isolatedSource = isolatedRemoteSourcePath(issue.companyId, issue.id);
if (["literal-branch", "legacy-explicit-source", "inherited-source", "legacy-untyped-source", "changed-source-intent", "context-source"].includes(scenario)) {
const selectedBranch = scenario === "literal-branch" ? "feature/literal-pin" : `test-${issue.identifier}`;
await execFile("git", ["-C", source, "checkout", "-b", selectedBranch]);
await writeFile(join(source, "selected-work.txt"), "Selected unpublished branch work\n");
await execFile("git", ["-C", source, "add", "selected-work.txt"]);
await execFile("git", ["-C", source, "commit", "-m", "Selected branch work"]);
const branchTip = (await execFile("git", ["-C", source, "rev-parse", "HEAD"])).stdout.trim();
await execFile("git", ["-C", source, "checkout", "main"]);
if (scenario === "literal-branch") input.config.mergedConfig = { workspaceStrategy: { type: "git_worktree", baseRef: "main", branchTemplate: "feature/literal-pin" } };
const acquire = vi.spyOn(input.services.envOrchestrator, "acquireForRun");
await expect(prepareHeartbeatWorkspace(db, input)).rejects.toThrow("Task workspace access is no longer available");
expect(clone).not.toHaveBeenCalled();
expect(acquire).not.toHaveBeenCalled();
expect((await readIssue(issue.id)).executionWorkspaceId).toBeNull();
expect((await execFile("git", ["-C", source, "rev-parse", `refs/heads/${selectedBranch}`])).stdout.trim()).toBe(branchTip);
await expect(access(isolatedSource)).rejects.toMatchObject({ code: "ENOENT" });
return;
}
expect(issue.workspaceSelection).toMatchObject({ source: "project", selection: {
kind: "configured_source", projectWorkspaceId: issue.projectWorkspaceId, mode: "managed_isolated",
} });
if (scenario === "failed-save" || scenario === "shared-retry-after-failed-save") {
vi.spyOn(input.services.executionWorkspacesSvc, "create").mockRejectedValueOnce(new Error("Workspace persistence failed"));
await expect(prepareHeartbeatWorkspace(db, input)).rejects.toThrow("Workspace persistence failed");
const worktrees = (await execFile("git", ["-C", isolatedSource, "worktree", "list", "--porcelain"])).stdout;
expect(worktrees.match(/^worktree /gm)).toHaveLength(1);
await expect(execFile("git", ["-C", isolatedSource, "rev-parse", "--verify", `refs/heads/test-${issue.identifier}`])).rejects.toBeDefined();
expect((await readIssue(issue.id)).executionWorkspaceId).toBeNull();
expect(await readFile(join(privateCwd, "confidential.txt"), "utf8")).toBe("Private source must not be copied");
if (scenario === "shared-retry-after-failed-save") {
await execFile("git", ["-C", source, "remote", "add", "origin", remoteUrl]);
await db.update(issues).set({ visibility: "open" }).where(eq(issues.id, privateIssue.id));
const saved = await prepareHeartbeatWorkspace(db, input);
const workspace = saved.persistedExecutionWorkspace!;
expect(saved.executionWorkspace.baseCwd).toBe(source);
expect(workspace.metadata?.isolatedRemoteSource).toBeUndefined();
await writeFile(join(workspace.cwd!, "unpublished-work.txt"), "Keep the shared-source task commits\n");
await execFile("git", ["-C", workspace.cwd!, "add", "unpublished-work.txt"]);
await execFile("git", ["-C", workspace.cwd!, "commit", "-m", "Unpublished task work"]);
const branchTip = (await execFile("git", ["-C", workspace.cwd!, "rev-parse", "HEAD"])).stdout.trim();
await expect(execFile("git", ["-C", isolatedSource, "cat-file", "-e", branchTip])).rejects.toBeDefined();
await execFile("git", ["-C", source, "worktree", "remove", "--force", workspace.cwd!]);
await reuse(input, workspace);
const rebuilt = await prepareHeartbeatWorkspace(db, input);
expect(rebuilt.executionWorkspace.baseCwd).toBe(await realpath(source));
expect((await execFile("git", ["-C", rebuilt.executionWorkspace.cwd, "rev-parse", "HEAD"])).stdout.trim()).toBe(branchTip);
expect(await readFile(join(rebuilt.executionWorkspace.cwd, "unpublished-work.txt"), "utf8")).toBe("Keep the shared-source task commits\n");
expect(clone).toHaveBeenCalledOnce();
}
return;
}
const result = await prepareHeartbeatWorkspace(db, input);
expect(result.executionWorkspace.baseCwd).toBe(isolatedSource);
expect(await readFile(join(result.executionWorkspace.cwd, "README.md"), "utf8")).toBe("source content\n");
await expect(access(join(result.executionWorkspace.cwd, "private-work"))).rejects.toMatchObject({ code: "ENOENT" });
expect(clone).toHaveBeenCalledOnce();
await expect(execFile("git", ["-C", isolatedSource, "cat-file", "-e", privateCommit])).rejects.toBeDefined();
await expect(execFile("git", ["-C", isolatedSource, "rev-parse", "--verify", "refs/heads/restricted-task"])).rejects.toBeDefined();
expect(await realpath(join(isolatedSource, ".git"))).not.toBe(await realpath(join(source, ".git")));
await reuse(input, result.persistedExecutionWorkspace!);
const resumed = await prepareHeartbeatWorkspace(db, input);
expect(resumed.persistedExecutionWorkspace!.id).toBe(result.persistedExecutionWorkspace!.id);
expect(resumed.executionWorkspace.baseCwd).toBe(await realpath(isolatedSource));
expect(clone).toHaveBeenCalledOnce();
expect((await readIssue(privateIssue.id)).visibility).toBe("private");
const workspace = resumed.persistedExecutionWorkspace!;
const cleanup = await cleanupExecutionWorkspaceArtifacts({ workspace,
projectWorkspace: { cwd: source, cleanupCommand: null }, runCleanupCommands: false });
expect(cleanup.warnings).toEqual([]);
await expect(access(workspace.cwd!)).rejects.toMatchObject({ code: "ENOENT" });
await db.update(executionWorkspaces).set({ status: "archived" }).where(eq(executionWorkspaces.id, workspace.id));
const reopened = await executionWorkspaceService(db).reopenClosedIsolatedExecutionWorkspaceForIssue({ workspaceId: workspace.id,
issue: { id: issue.id, companyId: issue.companyId, projectId: issue.projectId },
actor: { agentId: input.agent.id, actorType: "agent" },
authorizationActor: { type: "agent", source: "agent_jwt", agentId: input.agent.id, companyId: issue.companyId },
});
expect(reopened).toMatchObject({ ok: true });
expect(await readFile(join(workspace.cwd!, "README.md"), "utf8")).toBe("source content\n");
expect(await readFile(join(privateCwd, "confidential.txt"), "utf8")).toBe("Private source must not be copied");
});
it.each(["sandbox", "ssh", "local", null] as const)("uses the selected %s placement for controller provisioning", async driver => {
const { input, source, issue, envOrchestrator } = await fixture(true);
const marker = join(source, "host-provision-ran");
@@ -19,7 +19,7 @@ import path from "node:path";
import { CONTROLLER_PROVISION_COPY_ONLY, workspaceRequiresCopyOnlyProvisioning } from "../workspace-host-provision.js";
import { and, asc, eq } from "drizzle-orm";
import { type ExecutionWorkspace } from "@paperclipai/shared";
import { heartbeatRuns, projectWorkspaces } from "@paperclipai/db";
import { heartbeatRuns, issues, projectWorkspaces } from "@paperclipai/db";
import { getStartupTracer } from "../../instrumentation.js";
import { createHostDuplexObservabilityRecorder } from "../duplex-observability-recorder.js";
import { incrementToolRuntimeMetricCounter } from "../tool-runtime-metrics.js";
@@ -29,6 +29,7 @@ import { readNativeWorkspaceSyncReference, readNativeWorkspaceSyncLocalCwd } fro
import { parseObject } from "../../adapters/utils.js";
import { materializeNativeChatTaskRoot } from "../native-runtime/native-chat-workspace.js";
import { materializeIsolatedTaskDirectory } from "../isolated-task-directory.js";
import { canIsolateRemoteWorkspaceSource, materializeIsolatedRemoteWorkspaceSource } from "../isolated-remote-workspace-source.js";
import {
assertIsolatedWorkspaceSourceReadAccess,
retainIsolatedWorkspaceSourceAccess,
@@ -484,12 +485,38 @@ export async function prepareHeartbeatWorkspace(db: Db, input: HeartbeatWorkspac
);
const isolatedWorkspaceSourceActor = { type: "agent" as const, source: "agent_jwt" as const, companyId: agent.companyId,
agentId: agent.id, runId: run.id, onBehalfOfUserId: responsibleUserId === "local-board" ? null : responsibleUserId };
const isolatedWorkspaceSourceAccess = !isDotRun && issueId && !persistedNativeExecutionInput && !nativeRecoveryExecutionWorkspaceId
let isolatedWorkspaceSourceAccess = null;
let isolatedRemoteSource: Record<string, unknown> | null = null;
if (!isDotRun && issueId && !persistedNativeExecutionInput && !nativeRecoveryExecutionWorkspaceId
&& (latestWorkspaceStrategyType === "git_worktree"
|| (workspaceReuseProvisioningPolicy.shouldRestoreExistingWorkspace && reusableExistingExecutionWorkspace?.strategyType === "git_worktree"))
? await assertIsolatedWorkspaceSourceReadAccess(db, {
|| (workspaceReuseProvisioningPolicy.shouldRestoreExistingWorkspace && reusableExistingExecutionWorkspace?.strategyType === "git_worktree"))) {
try {
isolatedWorkspaceSourceAccess = await assertIsolatedWorkspaceSourceReadAccess(db, {
companyId: agent.companyId, sourceCwd: executionWorkspaceBase.baseCwd, actor: isolatedWorkspaceSourceActor,
}) : null;
});
} catch (error) {
const [sourceChoice] = await db.select({ projectWorkspaceId: issues.projectWorkspaceId,
executionWorkspaceId: issues.executionWorkspaceId,
selection: issues.workspaceSelection }).from(issues)
.where(and(eq(issues.id, issueId), eq(issues.companyId, agent.companyId)));
if (!sourceChoice || sourceChoice.executionWorkspaceId
|| (sourceChoice.projectWorkspaceId && sourceChoice.projectWorkspaceId !== resolvedWorkspace.workspaceId)) throw error;
if (!canIsolateRemoteWorkspaceSource({ error, repoUrl: executionWorkspaceBase.repoUrl,
reuseExisting: requestedShouldReuseExisting, hasExistingWorkspace: Boolean(existingExecutionWorkspace),
selectedProjectWorkspaceId: sourceChoice.projectWorkspaceId ?? readNonEmptyString(context.projectWorkspaceId),
selection: sourceChoice.selection, strategy: parseObject(hostExecutionWorkspaceConfig.workspaceStrategy),
trustPresetKind: trustPreset.kind })) throw error;
const cwd = await materializeIsolatedRemoteWorkspaceSource({ companyId: agent.companyId,
issueId, repoUrl: executionWorkspaceBase.repoUrl!, resolveGitAuth: workspaceGitAuthProvider });
executionWorkspaceBase.baseCwd = cwd;
resolvedWorkspace.cwd = cwd;
isolatedRemoteSource = { schema: "paperclip.isolated-remote-source.v1", companyId: agent.companyId,
issueId, repoUrl: executionWorkspaceBase.repoUrl };
isolatedWorkspaceSourceAccess = await assertIsolatedWorkspaceSourceReadAccess(db, {
companyId: agent.companyId, sourceCwd: cwd, actor: isolatedWorkspaceSourceActor,
});
}
}
const beforeIsolatedSourceCopy = isolatedWorkspaceSourceAccess
? (sourceCwd: string, targetCwd: string) => retainIsolatedWorkspaceSourceAccess(db, {
companyId: agent.companyId, sourceCwd, targetCwd,
@@ -751,6 +778,7 @@ export async function prepareHeartbeatWorkspace(db: Db, input: HeartbeatWorkspac
}
const nextExecutionWorkspaceMetadata = {
...baseExecutionWorkspaceMetadata,
...(isolatedRemoteSource ? { isolatedRemoteSource } : {}),
...(copyOnlyProvisioning ? { [CONTROLLER_PROVISION_COPY_ONLY]: true } : {}),
...(persistedWorktreeInstanceRoot
? {
@@ -832,6 +860,7 @@ export async function prepareHeartbeatWorkspace(db: Db, input: HeartbeatWorkspac
try {
await cleanupExecutionWorkspaceArtifacts({
workspace: {
companyId: agent.companyId,
id:
reusableExistingExecutionWorkspace?.id ??
workspaceReuseRequest.requestedExecutionWorkspaceId ??
@@ -59,6 +59,7 @@ import {
type RealizedExecutionWorkspace,
} from "../workspace-runtime.js";
import { resolvePersistedGitWorkspaceSource } from "../persisted-workspace-source.js";
import { verifiedRecordedIsolatedRemoteSource } from "../isolated-remote-workspace-source.js";
import { issueService } from "../issues.js";
import { projectService } from "../projects.js";
import {
@@ -3339,6 +3340,7 @@ export function createHeartbeatWorkspaceResolver(db: Db) {
repoName: deriveRepoNameFromRepoUrl(workspace.repoUrl),
}) : null;
const candidateBaseCwds = [
await verifiedRecordedIsolatedRemoteSource(workspace),
managedBase,
managedBase && workspace.repoUrl
? `${managedBase}-${createHash("sha256").update(workspace.repoUrl).digest("hex").slice(0, 12)}` : null,
@@ -0,0 +1,71 @@
import { describe, expect, it } from "vitest";
import { forbidden } from "../errors.js";
import { canIsolateRemoteWorkspaceSource, recordedIsolatedRemoteSource } from "./isolated-remote-workspace-source.js";
const allowed = {
error: forbidden("Task workspace access is no longer available"),
repoUrl: "https://example.test/company/repository.git",
reuseExisting: false,
hasExistingWorkspace: false,
selectedProjectWorkspaceId: null,
selection: null,
strategy: { type: "git_worktree" },
trustPresetKind: "standard",
};
describe("isolated remote source admission", () => {
it("allows a fresh default remote source after a workspace privacy denial", () => {
expect(canIsolateRemoteWorkspaceSource(allowed)).toBe(true);
});
it.each([
{ reuseExisting: true },
{ hasExistingWorkspace: true },
{ selectedProjectWorkspaceId: "chosen-source" },
{ strategy: { type: "git_worktree", existingBranch: "unpublished-work" } },
{ strategy: { type: "git_worktree", branchTemplate: "feature/literal-pin" } },
{ strategy: { type: "git_worktree", branchTemplate: "feature/{{unknown}}" } },
{ strategy: { type: "git_worktree", branchTemplate: "feature/{{agent.name}}" } },
{ error: forbidden("Project access denied") },
{ error: new Error("Task workspace access is no longer available") },
{ repoUrl: "file:///private/source" },
{ repoUrl: "/private/source" },
{ repoUrl: "https://user:password@example.test/repo" },
{ repoUrl: null },
{ trustPresetKind: "low_trust_review" },
{ trustPresetKind: "denied" },
{ trustPresetKind: "unknown" },
])("preserves the original denial for %j", patch => {
expect(canIsolateRemoteWorkspaceSource({ ...allowed, ...patch })).toBe(false);
});
it.each(["test-{{issue.identifier}}", "feature/{{issue.id}}", "feature/{{slug}}"])("allows a task-derived branch template: %s", branchTemplate => {
expect(canIsolateRemoteWorkspaceSource({ ...allowed, strategy: { type: "git_worktree", branchTemplate } })).toBe(true);
});
it.each(["explicit", "parent", "channel", "operator_cwd", "legacy"] as const)("preserves %s source intent", source => {
expect(canIsolateRemoteWorkspaceSource({ ...allowed, selection: {
version: 1, source, selection: { kind: "configured_source", projectWorkspaceId: "source", mode: "managed_isolated" },
} })).toBe(false);
});
it("requires a recorded default to match the selected source", () => {
const selection = { version: 1 as const, source: "project" as const,
selection: { kind: "configured_source" as const, projectWorkspaceId: "source", mode: "managed_isolated" as const } };
expect(canIsolateRemoteWorkspaceSource({ ...allowed, selectedProjectWorkspaceId: "source", selection })).toBe(true);
expect(canIsolateRemoteWorkspaceSource({ ...allowed, selectedProjectWorkspaceId: "other-source", selection })).toBe(false);
});
});
describe("recorded isolated source identity", () => {
const workspace = { companyId: "company", sourceIssueId: "task", repoUrl: allowed.repoUrl,
metadata: { isolatedRemoteSource: { schema: "paperclip.isolated-remote-source.v1", companyId: "company", issueId: "task", repoUrl: allowed.repoUrl } } };
it("keeps ordinary workspace sources unchanged", () => {
expect(recordedIsolatedRemoteSource({ ...workspace, metadata: null })).toBeNull();
});
it.each([{ companyId: "other-company" }, { sourceIssueId: "other-task" }, { repoUrl: "https://example.test/other" },
{ metadata: { isolatedRemoteSource: { schema: "unknown" } } },
])("rejects a changed source binding: %j", patch => {
expect(() => recordedIsolatedRemoteSource({ ...workspace, ...patch })).toThrow("source identity changed");
});
});
@@ -0,0 +1,120 @@
import path from "node:path";
import fs from "node:fs/promises";
import { execFile as execFileCallback } from "node:child_process";
import { promisify } from "node:util";
import type { TaskWorkspaceIntent } from "@paperclipai/shared";
import type { GitRemoteAuthProvider } from "./git-credentials.js";
import { resolvePaperclipInstanceRoot } from "../home-paths.js";
import { materializeManagedProjectWorkspace } from "./managed-repository-checkout.js";
const execFile = promisify(execFileCallback);
export function isolatedRemoteSourcePath(companyId: string, issueId: string): string {
if ([companyId, issueId].some(id => !/^[a-zA-Z0-9_-]+$/.test(id))) {
throw new Error("Invalid isolated repository source identity");
}
return path.join(resolvePaperclipInstanceRoot(), "isolated-repository-sources", companyId, issueId, "checkout");
}
export function recordedIsolatedRemoteSource(input: {
companyId?: string;
sourceIssueId: string | null;
repoUrl: string | null;
metadata?: Record<string, unknown> | null;
}): string | null {
const value = input.metadata?.isolatedRemoteSource;
if (value === undefined) return null;
const source = value as Record<string, unknown> | null;
if (!source || source.schema !== "paperclip.isolated-remote-source.v1"
|| !input.companyId || !input.sourceIssueId || source.companyId !== input.companyId
|| source.issueId !== input.sourceIssueId || source.repoUrl !== input.repoUrl) {
throw new Error("Recorded isolated repository source identity changed");
}
return isolatedRemoteSourcePath(input.companyId, input.sourceIssueId);
}
async function assertRemoteSourceIdentity(cwd: string, repoUrl: string) {
const root = resolvePaperclipInstanceRoot();
const canonicalCwd = await fs.realpath(cwd);
if (canonicalCwd !== path.join(await fs.realpath(root), path.relative(root, cwd))) {
throw new Error("Isolated repository source path changed");
}
const gitDir = path.join(canonicalCwd, ".git");
const entry = await fs.lstat(gitDir);
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error("Isolated repository source Git storage changed");
const options = { timeout: 10_000, maxBuffer: 16 * 1024 };
const common = await execFile("git", ["-C", canonicalCwd, "rev-parse", "--path-format=absolute", "--git-common-dir"], options);
if (common.stdout.trim() !== gitDir) throw new Error("Isolated repository source Git storage changed");
const origin = await execFile("git", ["-C", canonicalCwd, "config", "--local", "--null", "--get-all", "remote.origin.url"], options);
if (origin.stdout !== `${repoUrl}\0`) throw new Error("Isolated repository source identity changed");
}
export async function verifiedRecordedIsolatedRemoteSource(input: Parameters<typeof recordedIsolatedRemoteSource>[0]): Promise<string | null> {
const cwd = recordedIsolatedRemoteSource(input);
if (cwd) await assertRemoteSourceIdentity(cwd, input.repoUrl!);
return cwd;
}
export function canIsolateRemoteWorkspaceSource(input: {
error: unknown;
repoUrl: string | null;
reuseExisting: boolean;
hasExistingWorkspace: boolean;
selectedProjectWorkspaceId: string | null;
selection: TaskWorkspaceIntent | null | undefined;
strategy: Record<string, unknown>;
trustPresetKind: string;
}): boolean {
const error = input.error as { status?: unknown; message?: unknown } | null;
const branchTemplate = input.strategy.branchTemplate;
const selectsFixedBranch = typeof branchTemplate === "string" && branchTemplate.length > 0
&& !/\{\{\s*(?:issue\.(?:id|identifier|title)|slug)\s*\}\}/.test(branchTemplate);
if (input.trustPresetKind !== "standard" || error?.status !== 403 || error.message !== "Task workspace access is no longer available"
|| input.reuseExisting || input.hasExistingWorkspace || input.strategy.existingBranch || selectsFixedBranch
|| (input.selection && !["project", "task_default"].includes(input.selection.source))
|| (input.selectedProjectWorkspaceId && (input.selection?.selection.kind !== "configured_source"
|| input.selection.selection.projectWorkspaceId !== input.selectedProjectWorkspaceId))) return false;
if (!input.repoUrl) return false;
try {
const url = new URL(input.repoUrl);
return ["https:", "ssh:"].includes(url.protocol) && !!url.hostname && !url.password
&& (url.protocol === "ssh:" || !url.username);
} catch { return false; }
}
export async function materializeIsolatedRemoteWorkspaceSource(input: {
companyId: string;
issueId: string;
repoUrl: string;
resolveGitAuth: GitRemoteAuthProvider;
}): Promise<string> {
const cwd = isolatedRemoteSourcePath(input.companyId, input.issueId);
const root = resolvePaperclipInstanceRoot();
await fs.mkdir(root, { recursive: true });
const canonicalRoot = await fs.realpath(root);
let parent = canonicalRoot;
for (const segment of ["isolated-repository-sources", input.companyId, input.issueId]) {
parent = path.join(parent, segment);
await fs.mkdir(parent, { mode: 0o700 }).catch((error: NodeJS.ErrnoException) => {
if (error.code !== "EEXIST") throw error;
});
const entry = await fs.lstat(parent);
if (!entry.isDirectory() || entry.isSymbolicLink() || await fs.realpath(parent) !== parent) {
throw new Error("Isolated repository source path is not a private directory");
}
}
const canonicalCwd = path.join(parent, "checkout");
const entry = await fs.lstat(canonicalCwd).catch((error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT") return null;
throw error;
});
if (entry && (!entry.isDirectory() || entry.isSymbolicLink())) {
throw new Error("Isolated repository source cannot be a symbolic link");
}
const result = await materializeManagedProjectWorkspace(canonicalCwd, {
repoUrl: input.repoUrl, resolveGitAuth: input.resolveGitAuth,
});
if (result.warning) throw new Error("Isolated repository source is not a Git checkout");
await assertRemoteSourceIdentity(cwd, input.repoUrl);
return cwd;
}
+8
View File
@@ -10188,6 +10188,14 @@ export function issueService(db: Db) {
projectWorkspaceId = await resolveProjectDefaultWorkspaceId(tx, {
companyId, projectId: issueData.projectId, policy: projectPolicy,
});
if (projectWorkspaceId && isolatedWorkspacesEnabled) {
const sourceStrategy = await executionWorkspaceService(db).resolveSourceWorkspaceStrategy({ companyId,
assigneeAgentId: issueData.assigneeAgentId, assigneeAdapterOverrides: issueData.assigneeAdapterOverrides,
executionWorkspaceSettings, executionWorkspacePolicy: projectPolicy,
typedSelection: false, projectId: issueData.projectId, executionPolicy: issueData.executionPolicy }, tx);
if (sourceStrategy === "git_worktree") workspaceIntent = { version: 1, source: "project",
selection: { kind: "configured_source", projectWorkspaceId, mode: "managed_isolated" } };
}
}
if (projectWorkspaceId && workspaceSelectionActor) {
const [readableSource] = await tx.select({ id: projectWorkspaces.id, projectId: projectWorkspaces.projectId, executionWorkspacePolicy: projects.executionWorkspacePolicy }).from(projectWorkspaces)
+5 -2
View File
@@ -1,4 +1,5 @@
import { sanitizeRuntimeServiceBaseEnv } from "./runtime-service-env.js";
import { verifiedRecordedIsolatedRemoteSource } from "./isolated-remote-workspace-source.js";
export { sanitizeRuntimeServiceBaseEnv } from "./runtime-service-env.js";
import { execFile, spawn, type ChildProcess } from "node:child_process";
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
@@ -4155,6 +4156,7 @@ async function deleteGitBranchAtVerifiedTip(input: {
export async function cleanupExecutionWorkspaceArtifacts(input: {
workspace: {
id: string;
companyId: string;
cwd: string | null;
providerType: string;
providerRef: string | null;
@@ -4181,15 +4183,16 @@ export async function cleanupExecutionWorkspaceArtifacts(input: {
}) {
const warnings: string[] = [];
const workspacePath = input.workspace.providerRef ?? input.workspace.cwd;
const isolatedSource = await verifiedRecordedIsolatedRemoteSource(input.workspace);
const repoRoot = input.workspace.providerType === "git_worktree" && workspacePath
? await resolveGitRepoRootForWorkspaceCleanup(
workspacePath,
input.projectWorkspace?.cwd ?? null,
isolatedSource ?? input.projectWorkspace?.cwd ?? null,
)
: null;
const cleanupEnv = buildExecutionWorkspaceCleanupEnv({
workspace: input.workspace,
projectWorkspaceCwd: input.projectWorkspace?.cwd ?? null,
projectWorkspaceCwd: isolatedSource ?? input.projectWorkspace?.cwd ?? null,
});
// Callers can require the workspace to match an assessed snapshot before
// cleanup begins. Destructive paths recheck immediately before removal.