| name | workspace-isolation |
| description | Workspace isolation boundaries. |
| origin | pi-crew |
| triggers | ["workspace isolation","cross-workspace access","escape boundary","worktree safety","agent isolation"] |
workspace-isolation
pi-crew enforces workspace isolation so that agents, runs, and live sessions from one project folder cannot be accessed from another. The workspace boundary is manifest.cwd — the directory where a run was initiated.
Workspace Boundary Definition
manifest.cwd is the canonical workspace root. Every run record carries the directory where it was created.
Why it matters: Pi can have multiple workspace folders open simultaneously. Without isolation, an agent from workspace A could be steered/controlled from workspace B.
Rules:
- Every run's
manifest.cwd is set at creation time
- Every live agent handle carries
workspaceId = manifest.cwd
- Widget queries filter by
manifest.cwd
- API operations reject cross-workspace access
Live Agent Workspace Check
LiveAgentHandle.workspaceId field (added to prevent cross-workspace access):
interface LiveAgentHandle {
workspaceId: string;
}
Enforcement in api.ts (team-tool operations):
listActiveLiveAgentsByWorkspace(manifest.cwd);
const live = getLiveAgent(agentId);
if (live && live.workspaceId !== manifest.cwd)
return result(`Live agent '${agentId}' does not belong to workspace ${manifest.cwd}.`, { status: "error" }, true);
Enforcement in live-agent-manager.ts:
export function listLiveAgentsByWorkspace(workspaceId: string): LiveAgentHandle[] {
return listLiveAgents().filter((a) => a.workspaceId === workspaceId);
}
Team Workspace Modes
single (default)
- All agents run in the project root (
manifest.cwd)
- No worktree creation
- Simpler, but all workers share the same git state
worktree (parallel isolation)
- Each task (or phase) gets its own git worktree
- Worktree path:
<repo-root>/.worktrees/<runId>/<taskId>/
- Branch name:
crew/<runId>-<taskId> (sanitized)
- Allows parallel code-changing tasks without git conflicts
Entry point in team-runner.ts:
const worktree = workspaceMode === "worktree" && task.worktree !== undefined
? { path: task.worktree.path, branch: task.worktree.branch, reused: task.worktree.reused }
: undefined;
Worktree lifecycle:
-
Creation (prepareTaskWorkspace in worktree-manager.ts):
- Check leader repo is clean (
assertCleanLeader)
git worktree add <path> <branch>
- Link
node_modules if present
- Mark reused if already exists
-
Naming convention:
Branch: crew/<sanitized-runId>-<sanitized-taskId>
Path: .worktrees/<runId>/<taskId>/
-
Cleanup (on task/run completion):
- Check dirty state
git worktree remove <path> --force (only if force=true)
- Preserve dirty worktrees unless explicitly forced
Safety rules:
- Leader repo must be clean before creating worktrees
- One owner per file/symbol/migration path
- Branch names derived deterministically from run/task IDs (no user-controlled path fragments)
Cross-Workspace Prevention
In api.ts (handleTeamToolCall):
if (operation === "list-live-agents") {
return result(JSON.stringify(
listActiveLiveAgentsByWorkspace(loaded.manifest.cwd),
null, 2
), { action: "api", status: "ok", runId: loaded.manifest.runId });
}
In cancel.ts:
- Verifies run ownership before allowing cancel
- Cross-session cancel rejected unless force=true
In respond.ts:
- Verifies task ownership before responding
- Cross-session respond rejected unless force=true
In crash-recovery.ts:
purgeStaleActiveRunIndex only affects runs in the current workspace (cwd)
reconcileAllStaleRuns only scans the current workspace's .crew/state/runs/
Live Session Workspace
LiveSessionConfig carries workspaceId:
interface LiveSessionConfig {
workspaceId: string;
}
Propagation chain:
team-tool.ts (handleTeamToolCall)
→ TeamContext { workspaceId: cwd }
→ LiveSessionConfig { workspaceId }
→ registerLiveAgent({ workspaceId })
→ LiveAgentHandle { workspaceId }
Configuration
defaults.ts isolation settings:
const DEFAULT_PATHS = {
crewRoot: ".crew",
stateRoot: ".crew/state",
};
All paths are resolved relative to manifest.cwd, ensuring state stays under the project root.
Enforcement — Workspace Isolation Gate
Before performing cross-workspace operations, verify:
If ANY answer is NO → Stop. Verify workspace isolation before proceeding.
Anti-patterns
- Passing raw cwd without validation: Always use
resolveContainedPath to ensure paths stay under workspace root.
- Cross-workspace respond/cancel: Even with force=true, foreign session operations should be rejected. Check
ownerSessionId.
- Symlink traversal: Use
resolveRealContainedPath to resolve symlinks and detect escape attempts.
- Worktree name collision: Use deterministic names from run/task IDs. Never accept user-controlled branch names.
- Dirty worktree removal: Never force-remove worktrees with uncommitted changes unless explicitly confirmed.
Source patterns
src/extension/team-tool/api.ts — workspaceId filter in list-live-agents, steer-agent, follow-up-agent, stop-agent, resume-agent
src/runtime/live-agent-manager.ts — workspaceId in LiveAgentHandle, listLiveAgentsByWorkspace, listActiveLiveAgentsByWorkspace
src/runtime/live-session-runtime.ts — LiveSessionConfig, workspaceId in session creation
src/runtime/team-runner.ts — workspaceId passed through executeTeamRun
src/state/state-store.ts — initRunManifest with cwd, manifest.cwd
src/worktree/worktree-manager.ts — prepareTaskWorkspace, assertCleanLeader, linkNodeModulesIfPresent
src/config/defaults.ts — DEFAULT_PATHS (state under project root)
Verification
cd pi-crew
node --experimental-strip-types -e "
import { listLiveAgentsByWorkspace, listActiveLiveAgentsByWorkspace } from './src/runtime/live-agent-manager.ts';
console.log('By workspace:', listLiveAgentsByWorkspace(process.cwd()).length);
console.log('Active by workspace:', listActiveLiveAgentsByWorkspace(process.cwd()).length);
"
node --experimental-strip-types -e "
import { prepareTaskWorkspace } from './src/worktree/worktree-manager.ts';
// Requires a clean git repo and workspaceMode='worktree'
"
npx tsc --noEmit
node --experimental-strip-types --test test/unit/worktree-manager.test.ts test/unit/isolation-policy.test.ts
npm test