> ## Documentation Index
> Fetch the complete documentation index at: https://codexceed.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# avatar.workspace

Workspace — a tracked, path-confined handle to the repo (§8, §15).

Tools touch the filesystem and run commands *only* through this handle, so a
tool physically cannot reach outside the workspace root. It also owns the diff
baseline: at construction it pins the current git HEAD, and `diff()` compares
the working tree against that pinned baseline (not the git index), so the task's
delta is well-defined and the harness never needs to commit (§15).

## Classes

### `AmbiguousMatchError`

The `old` anchor matched more than once and `replace_all` was not set (ADR-0015).

Args:
path: The workspace-relative file the anchor matched in.
count: How many times the anchor matched (>1); exposed as `.count` for the tool's message.

```python theme={null}
AmbiguousMatchError(path: str, count: int) -> None
```

### `CommandOutput`

The captured result of one command run through the workspace.

```python theme={null}
CommandOutput(command: str, stdout: str, stderr: str, exit_code: int | None, timed_out: bool = False) -> None
```

### `DirtyWorkspaceError`

The workspace has uncommitted changes at open and they were not acknowledged (§15).

### `EmptyAnchorError`

`old` was empty — it would match between every character and rewrite the file (ADR-0015).

Rejected at the `Workspace.replace` chokepoint so a direct SDK caller can't corrupt a file,
not only the `str_replace` tool layer.

### `MatchNotFoundError`

The `old` anchor was absent from the file — a stale or mistyped anchor (§10).

### `PatchError`

A patch failed to apply cleanly (stale context) — model-correctable (§10).

Application is all-or-nothing: when this is raised, nothing was written.

### `PathOutsideWorkspaceError`

Raised when a requested path resolves outside the workspace root.

### `ReplaceError`

A string-anchored replace could not be applied cleanly (ADR-0015) — model-correctable.

All-or-nothing: when raised, the file is byte-for-byte unchanged.

### `SensitivePathError`

Raised when a *resolved* path matches the sensitive-path denylist (§11, Phase 2.5).

Enforced at the workspace chokepoint (not just the permission gate), so the
check sees the symlink-resolved target — an innocuously-named symlink cannot
launder a secret — and a non-gated caller still cannot read/patch it.

### `Workspace`

A tracked, path-confined handle to the repo; tools reach the FS only here (§8, §15).

Args:
root: The workspace root all paths are confined to.
allow\_dirty: When `True`, skip the clean-tree check at open (§15).
sensitive\_path\_globs: The denylist refused on read/patch (resolved-path check,
§11). Defaults to the built-in set (secure by default); the runner threads
`HarnessConfig.sensitive_path_globs` through to match the permission gate.
log\_path: The harness's own event-journal path (`HarnessConfig.log_path`), hidden
from the agent's file tools so it can't list/read/search the harness's plumbing.
sandbox: The execution strategy applied at the command seam (ADR-0042). Defaults to
`NoSandbox` (the full inherited environment — today's behavior), so a bare
`Workspace(root)` and every read-only inspection stay unchanged; the entry points
that actually run model/verifier commands inject `make_sandbox(config.sandbox_mode)`.

```python theme={null}
Workspace(root: Path | str, *, allow_dirty: bool = False, sensitive_path_globs: Sequence[str] | None = None, log_path: Path | str | None = None, sandbox: Sandbox | None = None) -> None
```

#### `Workspace.apply_patch(self, diff: str) -> list[str]`

Apply a (possibly multi-file) unified diff atomically; return changed paths.

An internal/SDK primitive — no longer a model-facing tool (the agent edits via
`replace`/`str_replace`, ADR-0015), but retained for programmatic callers and for
multi-file/creation diffs `replace` does not cover.

Confinement first: every target path must resolve inside the root, else
`PathOutsideWorkspaceError` (nothing written). Then a `git apply --check`
dry run gates the real apply, so a stale diff raises `PatchError` and the
workspace is left byte-for-byte unchanged — all-or-nothing (§10).

Args:
diff: The unified diff to apply.

Returns:
Sorted workspace-relative paths the diff changed.

Raises:
PatchError: When the diff names no targets or fails to apply cleanly.

#### `Workspace.baseline_paths(self) -> list[str]`

Workspace-relative paths tracked at the *pinned baseline* commit (empty when none).

Lets the verifier tell a genuinely test-less repo (a legitimate no-tests skip) from
one whose tests were suppressed after the baseline (ADR-0042, Threat A): if the
baseline HAD tests but a frozen `kind="test"` check now collects none, that exit-5 is
laundering, not absence. Reads the baseline tree via `git ls-tree` — not the working
tree — so a just-deleted/emptied test is still seen as having existed. Empty when the
workspace is not a git repo (no baseline was pinned).

Returns:
The sorted workspace-relative POSIX paths tracked at the pinned baseline.

#### `Workspace.contains(self, rel_path: str) -> bool`

Whether `rel_path` resolves inside the root, without raising (for the gate).

Args:
rel\_path: The path to test.

Returns:
`True` if `rel_path` resolves inside the root, else `False`.

#### `Workspace.diff(self) -> str`

Working-tree delta vs. the pinned baseline (empty when no baseline).

Returns:
The unified diff against the pinned baseline, empty when none.

#### `Workspace.is_ignored(self, relpath: str) -> bool`

Whether `relpath` (workspace-relative POSIX) is hidden harness plumbing.

Args:
relpath: A workspace-relative POSIX path.

Returns:
`True` for the harness's own journal directory (any file under it) or, when
the journal sits directly in the root, its file and `latest.jsonl` pointer.

#### `Workspace.list_files(self, glob: str) -> list[str]`

Return workspace-relative paths of files matching `glob`, sorted.

A glob that matches a *directory* expands to the files under it (recursively),
so `list_files("pkg")` lists `pkg/`'s contents rather than silently returning
nothing — the dogfood gap where `rich*` matched a dir and was dropped.

Hidden (dot-prefixed) entries are skipped by wildcards, mirroring ripgrep's
default — pathlib's glob matches them, so a venv or `.git` inside the workspace
otherwise turns `*`/`**/*` into thousands of junk paths. A pattern that *names*
a dot-prefixed segment (e.g. `.github/**/*`) opts into hidden, the same way an
explicit path does for rg; `read_file` on an explicit hidden path always works.

Args:
glob: The glob pattern to match against the root.

Returns:
Sorted workspace-relative paths of the matching files (dir matches expanded;
hidden entries skipped unless the pattern names a dot-prefixed segment).

#### `Workspace.read(self, path: str, line_range: tuple[int, int] | None = None) -> str`

Read a workspace file, optionally a 1-indexed inclusive line range.

Args:
path: The file to read.
line\_range: A 1-indexed inclusive `(start, end)` range, or `None` for all.

Returns:
The file text, sliced to `line_range` when given.

Raises:
FileNotFoundError: When the path is the harness's own (hidden) journal.

#### `Workspace.remove(self, path: str) -> str`

Delete a workspace file and stage the removal so `diff()` reflects it (ADR-0015).

The deletion counterpart to `write_file`/`replace` — the capability the removed
`apply_patch` `/dev/null` hunk used to carry. Staging the removal makes a
baseline-tracked file show as deleted in `git diff &lt;baseline>` and lets a
transiently-created file (staged by `write_file`) net back to zero. Confinement
and the sensitive-path denylist apply at this chokepoint like every other write.

Args:
path: The workspace-relative file to delete.

Returns:
The workspace-relative path deleted.

Raises:
FileNotFoundError: When the target does not exist (nothing to delete).

#### `Workspace.replace(self, path: str, old: str, new: str, *, replace_all: bool = False) -> str`

Swap an exact string in a file; the anchor proves non-staleness (§10, ADR-0015).

The string-anchored modification primitive that supersedes `apply_patch`'s diff
costume: `old` must match the *current* file text exactly — read-before-edit (§10)
enforced by the anchor, not by line arithmetic. It must match once, unless
`replace_all`. All-or-nothing: a rejected match leaves the file byte-for-byte
unchanged. Confinement, the sensitive-path denylist, and staging apply at this
chokepoint like every other write; the resulting diff is *derived* by `diff()`,
never authored by the model (§5).

Args:
path: The workspace-relative file to edit.
old: The exact existing text to find (the anchor); must be non-empty.
new: The replacement text; an empty string deletes the matched span.
replace\_all: Replace every occurrence instead of requiring a unique match.

Returns:
The workspace-relative path edited.

Raises:
FileNotFoundError: When the target file does not exist (create with `write_file`).
EmptyAnchorError: When `old` is empty (would rewrite the whole file).
MatchNotFoundError: When `old` is absent (stale or mistyped anchor).
AmbiguousMatchError: When `old` matches more than once and `replace_all` is unset.

Note:
The input contract lives HERE, at the chokepoint, not only in the `str_replace`
tool — a direct SDK caller gets the same guarantees. Confinement and the
sensitive-path denylist apply via `_resolve`/`_assert_not_sensitive`. An empty
`new` is deliberately allowed (span deletion); an empty `old` is rejected.

#### `Workspace.run(self, command: str, timeout: int | None = None) -> CommandOutput`

Run `command` confined to the root, capturing output; bounded by `timeout`.

Never raises into the loop (ADR-0007 robustness floor): a missing binary,
an empty command, or an unparseable command line all come back as a failed
`CommandOutput` (shell convention: exit 127 = command not found) so the
verifier and tools see a legible failure, not a `FileNotFoundError`.

Args:
command: The shell-style command to run.
timeout: Seconds before the command is killed, or `None` for no bound.

Returns:
The captured stdout, stderr, exit code, and timeout flag.

#### `Workspace.stage(self, paths: list[str]) -> None`

`git add` the given paths so they enter the pinned-HEAD `diff` (§15).

Mirrors `apply_patch`'s `git apply --index`: a command-created (untracked)
file is invisible to `git diff &lt;baseline>` until staged, so staging is what
makes codegen/migration output show up in the diff, artifact, and verifier.
No-op when not a git repo or given no paths.

Args:
paths: Workspace-relative paths to stage.

#### `Workspace.status_paths(self) -> set[str]`

Workspace-relative paths git currently sees as changed *or untracked*.

Used to attribute a command's side effects: snapshot before and after a
`run` and the delta is what the command touched. Includes untracked files
(unlike `diff`, which is `git diff &lt;baseline>`), so command-created files
are visible. Empty when not a git repo.

Returns:
The set of paths from `git status --porcelain` (rename → its new path).

#### `Workspace.write_file(self, path: str, content: str, *, overwrite: bool = False) -> str`

Create a file with `content` and stage it; refuse an existing target by default.

The plain-content twin of `apply_patch` for the no-anchor case (ADR-0003 B):
creation needs no diff costume, while *modification* stays diff-anchored —
without `overwrite`, an existing target raises `FileExistsError` so the
clean-apply staleness invariant can't be bypassed casually. Confinement and
the sensitive-path denylist apply at this chokepoint like every other access,
and the new file is staged so it appears in `diff()` (matching `apply_patch --index` and the `run_command` mutation capture).

Args:
path: The workspace-relative file to create.
content: The full file content to write.
overwrite: `True` to deliberately replace an existing file.

Returns:
The workspace-relative path written.

Raises:
FileExistsError: When the target exists and `overwrite` is `False`.

## Functions

### `path_is_sensitive(rel_path: str, globs: Sequence[str]) -> bool`

Whether `rel_path` matches any denylist glob (§11, Phase 2.5).

A pattern *without* a slash matches any single path component (gitignore-style
"match anywhere" — so `.env` hits `a/b/.env` and `.ssh` hits `.ssh/id_rsa`).
A pattern *with* a slash is matched against the whole relative path.

Matching is **case-insensitive** (ADR-0021): a case-insensitive filesystem (macOS APFS,
Windows) resolves `CREDENTIALS` to the same file as `credentials`, so a case-sensitive
denylist is bypassable by varying case. We case-fold both sides via `fnmatchcase` on
lowered strings — deterministic across platforms (unlike `fnmatch`, whose case behavior
rides `os.path.normcase`, a no-op on macOS) and conservative (over-matching a denylist is
the safe direction).

Args:
rel\_path: The workspace-relative path to test.
globs: The denylist patterns.

Returns:
`True` if any pattern matches, else `False`.
