pub struct Workspace { /* private fields */ }Expand description
One workspace, resolved: the runtime it borrows, the model, and everything discovered on disk, ready to mint runs from.
Held by reference by every run it mints, so a host keeps one per repository
for as long as it wants to send prompts at it. Dropping it does not end the
runs already minted — a PreparedRun owns its session — but the MCP
connections go with it, and so does the runtime when this held the last
Arc.
Send and Sync: the runtime is shared through Arcs and creates
sessions from &self, so concurrent minting from one workspace needs no
lock of basis’s own.
§What a workspace’s guards do and do not cover
Everything a repository says about itself — its .basis/hooks.json chain,
its ShellAccess posture, its .git carve-out, its memory roots — reaches
a run through the session this workspace mints: the policy is stated per
session, and the hook chain is registered for this workspace’s own
ToolAudience, which every session minted or
resumed here carries and every delegated child inherits.
A session this workspace did not mint carries none of it, even in this
directory. An agent a host creates for itself through
mentra_runtime, or drives through
run::prepare_with_session, has no tool
audience at all, and mentra never consults an audience-scoped registration
for such an agent — so a .basis/hooks.json deny hook guarding this
repository is not asked about that agent’s calls, and its policy is the
runtime’s rather than this workspace’s, however its base directory is
spelled. basis routed on the call’s working directory before mentra could
scope a registration, and that is what changed. The two things a host can
do about it: register a
RuntimeBuilder::with_interceptor
guard, which is global and therefore judges every session on the runtime;
or mint through prepare and resume,
which is what puts a run inside this workspace’s guards in the first place.
Implementations§
Source§impl Workspace
impl Workspace
Sourcepub async fn open(path: impl Into<PathBuf>) -> Result<Self, RunError>
pub async fn open(path: impl Into<PathBuf>) -> Result<Self, RunError>
Opens path with basis’s defaults: a private runtime with the provider
auto-detected from the environment, the newest model it offers, and
every convention discovered where convention says to look.
builder is the same call with the knobs exposed —
including with_runtime, for the
host that opens many workspaces on one Runtime.
Sourcepub fn builder(path: impl Into<PathBuf>) -> WorkspaceBuilder
pub fn builder(path: impl Into<PathBuf>) -> WorkspaceBuilder
Configures a workspace before opening it.
Sourcepub fn prepare(&self, spec: impl Into<RunSpec>) -> Result<PreparedRun, RunError>
pub fn prepare(&self, spec: impl Into<RunSpec>) -> Result<PreparedRun, RunError>
Mints a run: a fresh conversation against this workspace.
Synchronous, and deliberately so — everything expensive already
happened. What this does is spawn a session on the existing runtime and
hand back the PreparedRun that drives it.
The spec’s prompt may be empty. Once a session outlives a turn, a
conversation with nothing said yet is a real state — it is what ACP’s
session/new opens — so the emptiness check belongs where a prompt is
actually sent, which is PreparedRun::execute_with_approver and
PreparedRun::send_with_options. (The free run keeps
its own up-front check, because a one-shot caller that passed nothing
wants to hear about it before a session exists.)
Sourcepub fn resume(
&self,
agent_id: &str,
spec: impl Into<RunSpec>,
) -> Result<PreparedRun, RunError>
pub fn resume( &self, agent_id: &str, spec: impl Into<RunSpec>, ) -> Result<PreparedRun, RunError>
Picks up a conversation a previous process left behind.
agent_id is PreparedRun::agent_id, not the session id: mentra
persists agents, and a session is one process’s view of one. Resuming
replays the transcript from the store, so the first turn after this
already knows everything the last one did.
Resuming is also where a “…for this session” approval answer dies
(see ApprovalDecision). A new one needs
nothing basis does about it: mentra 0.27 remembers it into
PermissionRuleScope::Process (mentra#53), a rung owned by the live
session that gave it and never written to the runtime store, so
resume_session_with_options already hands back a session with a
fresh handle and an empty rung, whatever the agent id. A row a
pre-0.12 basis binary remembered into the durable Session scope
instead is a different matter — mentra still loads and matches those —
so the attach still clears that one scope for exactly that legacy
case, and a corrupt or unwritable rules.json fails the resume with
RunError::SessionRulesNotCleared naming the file
to repair or delete. Project- and global-scope rules are durable by
definition and are unaffected either way.
The workspace has to be the one the conversation belongs to, and this
checks it: mentra’s store is keyed by agent rather than by path, so an
id alone cannot say where its conversation ran, and a resume restates
this workspace’s policy, tool audience and persisted-row tag onto
whatever it is given. Under another repository’s conversation those are
the wrong .git carve-out, the wrong shell posture and the wrong
roster, while the agent stays based in its own directory — which
mentra’s file tools always allow writes under. So an agent based
somewhere other than root is refused with
RunError::WorkspaceMismatch, before the resume
touches it at all. A caller that means “one of mine” takes the id from
store::list for this workspace.
mentra does not persist a model’s context window
(Agent::from_loaded always resumes at None — set_model is the
only way back), so a resumed session starts with an unknown one. This
reapplies this workspace’s own resolved model exactly when the resumed
conversation is still on it and this resume is not also changing
reasoning — the same model prepare would have minted
— which restores mentra’s own compaction threshold as well as
PreparedRun::context_window. A reasoning-changing resume leaves the
window unknown so it performs one persisted mutation rather than two. A conversation
PreparedRun::set_model had already moved elsewhere keeps whatever
that call left it at instead: basis has no window for a model it does
not resolve, and forcing this one back would silently undo a choice
the caller made.
Sourcepub fn fingerprint(&self) -> Snapshot
pub fn fingerprint(&self) -> Snapshot
A cheap stand-in for everything in this workspace a run could see.
The utility ADR-0014 kept when watch was deleted, on the type its
ledger row promised it to. The semantics are crate::fingerprint’s
verbatim: a digest over git ls-files plus HEAD, stat only, and
every uncertain answer resolving to changed rather than unchanged.
Fingerprints the workspace as it is now, not as it was when the workspace was opened — that is the whole point, since a caller’s loop asks it repeatedly against one long-lived workspace.
Blocking: it spawns git and stats files. An async caller belongs on a
blocking thread — tokio::task::spawn_blocking, or the equivalent.
Sourcepub fn path(&self) -> &Path
pub fn path(&self) -> &Path
The directory this workspace’s runs are scoped to.
The same value as root, and deliberately so: a caller
that opened a relative or symlinked spelling gets back the directory
that spelling named, not the spelling. Both names are kept because
hosts use both.
Sourcepub fn root(&self) -> &Path
pub fn root(&self) -> &Path
The workspace root: absolute, symlinks followed, resolved once at
open. What the run header reports, what the agent is
based in, what the runtime’s policy roots are built from, and what
fingerprint reads — one directory under one
spelling, so workspace and context_files can never disagree.
pub fn provider(&self) -> &str
Sourcepub fn context(&self) -> &WorkspaceContext
pub fn context(&self) -> &WorkspaceContext
The context documents discovered at open, weakest precedence first.
Sourcepub fn memories(&self) -> &[Memory]
pub fn memories(&self) -> &[Memory]
The memories discovered at open, name-ordered, a workspace memory shadowing a global one of the same name.
Frontmatter only: the index in the system prompt is what a memory
costs by default, and the body stays on disk for the model — or a host
showing its user what the agent remembers — to read on demand. See
crate::memory for the convention.
Sourcepub fn skills(&self) -> &[LoadedSkill]
pub fn skills(&self) -> &[LoadedSkill]
Every skill a run from this workspace could reach at open, after layering, name-ordered.
On a private runtime that is exactly this workspace’s four roots. On a
shared one (ADR-0018) the registry is the runtime’s and additive, so it
is this workspace’s roots and whatever a sibling workspace open at the
time had registered — which is what a run can actually load_skill, and
therefore the honest answer to what this reports.
LoadedSkill::root is how to tell the
two apart: a root under root is this repository’s.
A snapshot, taken once at open like everything else here. A sibling that opens afterwards adds skills this list does not name, and one that drops takes its own away — since mentra 0.24 a workspace hands its roots back when it goes, so a shared runtime no longer accumulates the skills of every repository a host has ever opened on it.
Sourcepub fn templates(&self) -> &[Template]
pub fn templates(&self) -> &[Template]
The prompt templates this workspace defines, after layering, name-ordered. Over ACP these become the client’s commands.
Sourcepub fn mcp_servers(&self) -> &[String]
pub fn mcp_servers(&self) -> &[String]
The MCP servers connected at open, by the names that took effect — which is the configured name unless another workspace on the shared runtime already held it, in which case it carries a deterministic suffix. Names only: nothing here echoes a command or a credential.
Sourcepub fn declared_tools(&self) -> &[String]
pub fn declared_tools(&self) -> &[String]
The tools this workspace’s manifests declared, by name, after layering — this workspace’s own first, name-ordered within each manifest.
Names only, for mcp_servers’s reason: nothing
here echoes a command or a credential.
Sourcepub fn host_tools(&self) -> &[String]
pub fn host_tools(&self) -> &[String]
The native tools the host supplied for this workspace
(WorkspaceBuilder::with_tool),
by name, in the order it supplied them.
Names only, for mcp_servers’s reason. Reported
beside declared_tools because the two are one
question for anything looking at a run — what did this open put within
the model’s reach that basis did not? — and the answer is not
otherwise readable: these are registered for this workspace’s audience,
which mentra’s own registry readers do not walk.
Sourcepub fn config(&self) -> &Config
pub fn config(&self) -> &Config
What config.json said about this workspace, and which file said it.
The answers here are already in force — the model below is what they
resolved to — so this is for the host that reports its own
configuration, or that wants to hand the same value to a shared
Runtime’s builder rather than read the files twice.
Sourcepub fn config_files(&self) -> &[ContextFile]
pub fn config_files(&self) -> &[ContextFile]
The config files that took effect, most specific first.
Reported the way .mcp.json’s and .basis/tools.json’s sources are,
and for a milder version of their reason: what this file decides —
which model, which provider — a run already names in its own header, so
the question left is which file said so, and a repository that
resolved a model nobody expected should be able to find out in one
place.
Sourcepub fn declared_tool_files(&self) -> &[ContextFile]
pub fn declared_tool_files(&self) -> &[ContextFile]
The tool manifests that took effect, most specific first.
A file that says which programs the model may run is the last thing that
should apply invisibly, which is why discovery reports its sources the
way .mcp.json’s does.
Sourcepub fn mentra_runtime(&self) -> &Runtime
pub fn mentra_runtime(&self) -> &Runtime
The mentra runtime the runs are minted on, for a host that wants mentra’s own surface — the task board, teams, the store — alongside basis’s.
The same bargain as PreparedRun::session: basis does not hide mentra,
and reaching past basis’s surface is a supported thing to do rather than
a workaround. Renamed from runtime() when ADR-0018 gave basis a
Runtime of its own, so the name says whose surface comes back.