diff --git a/doc/project-repositories.md b/doc/project-repositories.md index 38fd6e237ec..7fbdbd1ee99 100644 --- a/doc/project-repositories.md +++ b/doc/project-repositories.md @@ -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 diff --git a/maps/agent-access-map/workspaces.md b/maps/agent-access-map/workspaces.md index bf3cafe1ed2..ec528d2febd 100644 --- a/maps/agent-access-map/workspaces.md +++ b/maps/agent-access-map/workspaces.md @@ -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. diff --git a/maps/feature-map/workspaces.md b/maps/feature-map/workspaces.md index c5bba194573..b4fbadeac52 100644 --- a/maps/feature-map/workspaces.md +++ b/maps/feature-map/workspaces.md @@ -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 diff --git a/maps/storybook-map/INDEX.md b/maps/storybook-map/INDEX.md index 489841404f4..af857db8b44 100644 --- a/maps/storybook-map/INDEX.md +++ b/maps/storybook-map/INDEX.md @@ -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 diff --git a/maps/storybook-map/catalog.json b/maps/storybook-map/catalog.json index 0fb4943831b..59df9e33b62 100644 --- a/maps/storybook-map/catalog.json +++ b/maps/storybook-map/catalog.json @@ -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", diff --git a/server/src/__tests__/heartbeat-workspace-source-trust.test.ts b/server/src/__tests__/heartbeat-workspace-source-trust.test.ts index 83406d46967..95f4478547f 100644 --- a/server/src/__tests__/heartbeat-workspace-source-trust.test.ts +++ b/server/src/__tests__/heartbeat-workspace-source-trust.test.ts @@ -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}'` } } }); diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index 0d6da1071f3..b3718143a16 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -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", diff --git a/server/src/services/execution-workspaces.ts b/server/src/services/execution-workspaces.ts index 9c4798614b8..d8ff62f5561 100644 --- a/server/src/services/execution-workspaces.ts +++ b/server/src/services/execution-workspaces.ts @@ -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 = { 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({ diff --git a/server/src/services/heartbeat/environment-selection.ts b/server/src/services/heartbeat/environment-selection.ts index 6af79911b67..f0436a60727 100644 --- a/server/src/services/heartbeat/environment-selection.ts +++ b/server/src/services/heartbeat/environment-selection.ts @@ -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({ diff --git a/server/src/services/heartbeat/workspace-preparation.test.ts b/server/src/services/heartbeat/workspace-preparation.test.ts index dbacebdb8f2..f6d1eb827d8 100644 --- a/server/src/services/heartbeat/workspace-preparation.test.ts +++ b/server/src/services/heartbeat/workspace-preparation.test.ts @@ -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"); diff --git a/server/src/services/heartbeat/workspace-preparation.ts b/server/src/services/heartbeat/workspace-preparation.ts index 9d0a5a7ba1c..0dfc5a45083 100644 --- a/server/src/services/heartbeat/workspace-preparation.ts +++ b/server/src/services/heartbeat/workspace-preparation.ts @@ -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 | 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 ?? diff --git a/server/src/services/heartbeat/workspaces.ts b/server/src/services/heartbeat/workspaces.ts index 1c37505aa95..fea3cfb32d1 100644 --- a/server/src/services/heartbeat/workspaces.ts +++ b/server/src/services/heartbeat/workspaces.ts @@ -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, diff --git a/server/src/services/isolated-remote-workspace-source.test.ts b/server/src/services/isolated-remote-workspace-source.test.ts new file mode 100644 index 00000000000..68ef7868b0b --- /dev/null +++ b/server/src/services/isolated-remote-workspace-source.test.ts @@ -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"); + }); +}); diff --git a/server/src/services/isolated-remote-workspace-source.ts b/server/src/services/isolated-remote-workspace-source.ts new file mode 100644 index 00000000000..26c949fdfbc --- /dev/null +++ b/server/src/services/isolated-remote-workspace-source.ts @@ -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 | null; +}): string | null { + const value = input.metadata?.isolatedRemoteSource; + if (value === undefined) return null; + const source = value as Record | 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[0]): Promise { + 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; + 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 { + 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; +} diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index aeb63a0c157..3a167a7865a 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -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) diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index 35fe6657772..2a553671640 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -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.