mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-11 06:30:25 +00:00
Gated sessions run workspace+scratch dual-root; artifact listing scans scratch only. Artifact chips resolve across workspace, scratch, and granted roots; request_directory registers for all sessions.
96 lines
3.8 KiB
Python
96 lines
3.8 KiB
Python
"""Workspace roots — the directories a session is allowed to touch.
|
|
|
|
A Cowork session is "orphan": it owns a per-conversation **scratch** dir (the primary root,
|
|
writable, the default save location) and may gain access to additional folders, each chosen
|
|
read-only or read-write. The same `list[RootDir]` object is shared by reference across the
|
|
PermissionEngine (scoping), the file toolkit (resolution), and the context injector (so the
|
|
agent is told which dirs it has), so Slice C can mutate it in place at runtime and all three
|
|
see the change. Index 0 is always the primary.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Iterable
|
|
|
|
|
|
@dataclass
|
|
class RootDir:
|
|
path: Path
|
|
writable: bool = False
|
|
label: str = "" # display name; defaults to the dir's basename
|
|
|
|
def __post_init__(self) -> None:
|
|
self.path = Path(self.path).expanduser().resolve()
|
|
if not self.label:
|
|
self.label = self.path.name or str(self.path)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {"path": str(self.path), "writable": self.writable, "label": self.label}
|
|
|
|
|
|
def normalize_roots(roots: Iterable[Any] | None) -> list[RootDir]:
|
|
"""Coerce a mixed list (RootDir | dict{path,writable,label} | str/Path) into RootDirs.
|
|
Bare str/Path entries are treated as read-only; pass dicts/RootDirs to grant write.
|
|
"""
|
|
out: list[RootDir] = []
|
|
for r in roots or []:
|
|
if isinstance(r, RootDir):
|
|
out.append(r)
|
|
elif isinstance(r, dict):
|
|
out.append(
|
|
RootDir(
|
|
path=r["path"],
|
|
writable=bool(r.get("writable", False)),
|
|
label=r.get("label", ""),
|
|
)
|
|
)
|
|
elif isinstance(r, (str, Path)):
|
|
out.append(RootDir(path=r, writable=False))
|
|
else: # duck-typed object with .path/.writable
|
|
out.append(
|
|
RootDir(
|
|
path=getattr(r, "path"),
|
|
writable=bool(getattr(r, "writable", False)),
|
|
)
|
|
)
|
|
return out
|
|
|
|
|
|
def render_context(roots: list[RootDir]) -> str:
|
|
"""The `<system-context>` body listing the dirs available this turn. Empty when no roots."""
|
|
if not roots:
|
|
return ""
|
|
lines = ["Available directories (you may use file/shell tools within these):"]
|
|
has_side_scratch = any(i > 0 and r.label == "scratch" for i, r in enumerate(roots))
|
|
for i, r in enumerate(roots):
|
|
access = "read-write" if r.writable else "read-only"
|
|
if i == 0 and r.label == "scratch":
|
|
tag = " — primary scratch, the default place to save files"
|
|
elif i == 0:
|
|
tag = " — the session's workspace (relative paths resolve here)"
|
|
elif r.label == "scratch":
|
|
tag = (
|
|
" — your scratch directory: temporary files, and artifacts you don't "
|
|
"want to leave inside the workspace"
|
|
)
|
|
else:
|
|
tag = ""
|
|
lines.append(f"- {r.path} [{access}]{tag}")
|
|
if has_side_scratch:
|
|
lines.append(
|
|
"Relative paths resolve against the workspace; pass an absolute path to use "
|
|
"another directory. Writes are only allowed in read-write directories. Put "
|
|
"reports, analyses, and other non-repo deliverables in the scratch directory "
|
|
"(they appear in the user's Artifacts panel) — write into the workspace only "
|
|
"for changes that belong in it."
|
|
)
|
|
else:
|
|
lines.append(
|
|
"Relative paths resolve against the primary directory; pass an absolute path to use "
|
|
"another directory. Writes are only allowed in read-write directories. If the user "
|
|
"cares where a deliverable lands, ask; otherwise save it in the primary scratch."
|
|
)
|
|
return "\n".join(lines)
|