···105105 output is forced (reading `config` is free), mirroring `system.build.toplevel`.
106106- Capabilities are keyed attrsets under `capabilities.<name>`. Do not put
107107 `name` in the capability body; the attrset key is the identity.
108108+ `context_status` and `context_read` are reserved internal tool names.
108109- The agent's `shell` is the baseline PATH baked into the manifest; keep it
109110 minimal. Tool-specific programs go in that capability's `grants.packages`.
110111 `shell.env` adds env vars to every call (reserved names rejected at build
···190190A capability declares:
191191192192- the attrset key as its name, plus `description` and model-facing `params`
193193+ (`context_status` and `context_read` are reserved for internal context tools)
193194- `policy`: `auto`, `ask-once`, `ask-always`, or `deny`
194195- `grants.packages`: package binaries available only to that tool
195196- `grants.network.allowedHosts`: proxy-allowed HTTP(S) hosts
···1414from pydantic import BaseModel, Field, field_validator, model_validator
15151616from tartarus.constants import CERT_ENV_VARS, STRICT_CONFIG
1717+from tartarus.context import CONTEXT_TOOL_NAMES
1718from typing_extensions import Self
18191920···139140 # For kind == "control": which registry operation this tool performs,
140141 # one of "status" | "output" | "stop". None for every other kind.
141142 control: Literal["status", "output", "stop"] | None = None
143143+144144+ @field_validator("name")
145145+ @classmethod
146146+ def _reject_reserved_tool_name(cls, v: str) -> str:
147147+ if v in CONTEXT_TOOL_NAMES:
148148+ raise ValueError(f"capability name '{v}' is reserved")
149149+ return v
142150143151 @field_validator("timeout", mode="before")
144152 @classmethod
···332340 )
333341 upper = key.upper()
334342 if upper in _RESERVED_SHELL_ENV_NAMES:
335335- raise ValueError(f"shellEnv key '{key}' is reserved and cannot be overridden")
343343+ raise ValueError(
344344+ f"shellEnv key '{key}' is reserved and cannot be overridden"
345345+ )
336346 if upper.endswith("_PROXY"):
337347 raise ValueError(
338348 f"shellEnv key '{key}' matches the reserved *_PROXY suffix"
···358368 name = tool.get("name")
359369 if not isinstance(name, str):
360370 raise ValueError("tool 'name' must be a string")
371371+ if name in CONTEXT_TOOL_NAMES:
372372+ raise ValueError(f"tool name '{name}' is reserved")
361373 capability = self.capabilities.get(name)
362374 if capability is None:
363375 raise ValueError(f"tool '{name}' has no matching capability")
···9898 self._flushed = len(messages)
9999 return messages
100100101101- def append(self, messages: list[dict]) -> None:
102102- """Persist any messages added since the last flush."""
101101+ @property
102102+ def flushed_count(self) -> int:
103103+ return self._flushed
104104+105105+ def append(self, messages: list[dict]) -> int | None:
106106+ """Persist any messages added since the last flush.
107107+108108+ Returns the first appended message index, or None when there was no new
109109+ tail to write.
110110+ """
103111 tail = messages[self._flushed :]
104112 if not tail:
105105- return
113113+ return None
114114+ start_index = self._flushed
106115 if self._dir:
107116 os.makedirs(self._dir, exist_ok=True)
108117 try:
···112121 except OSError as error:
113122 raise SessionError(f"cannot write session {self._path}: {error}") from error
114123 self._flushed = len(messages)
124124+ return start_index
115125116126 def first_user_message(self) -> str | None:
117127 """First user-authored text in the session, for listing previews."""