Refuse to start with the state store or .env inside the clone
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m14s

Answering "is the workspace where the agent runs, or what it works
on?" — it is what it works on, and conflating the two is dangerous
rather than merely untidy.

The agent commits its workspace with git add -A and pushes it, so
anything inside the clone reaches the project repository. A .env
placed there would publish the LiteLLM, Matrix and Gitea tokens as a
side effect of the agent working correctly: silently, once, and
probably unnoticed. Startup now refuses both that and a state store
inside the clone, and .env.example documents the intended layout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dongarandClaude Opus 5 committed 2026-09-08 22:31:39 +00:00
1 parent 2fb25b8195
commit dfa574aeec
3 files changed
+79 -1

No files matched your search

+13
View File
@@ -37,6 +37,19 @@ NISSE_GITEA_OPERATORS=your-gitea-login
# --- Running the agent (the demo, not the tests) ---
# The tests use NISSE_GITEA_TEST_REPO; a running agent uses these.
#
# Layout. NISSE_WORKSPACE is the *clone the agent works in*, not the directory
# the agent runs from — they must not be the same place:
#
# ~/nisse-demo-workspace/ deployment root, not a git repo
# ├── .env this file
# ├── state.db the A2 state store
# └── project/ NISSE_WORKSPACE, the clone
#
# The agent commits its workspace with `git add -A` and pushes it, so anything
# inside the clone reaches the project repository. A .env or state store placed
# there would publish credentials as a side effect of the agent working
# correctly; startup refuses to proceed if either is inside the workspace.
NISSE_STATE_DB=~/nisse-demo-workspace/state.db
NISSE_WORKSPACE=~/nisse-demo-workspace/project
NISSE_PROJECT=demo
+25 -1
View File
@@ -71,7 +71,7 @@ class AgentConfig:
raise ConfigError(
"cannot start: these are unset or empty — " + ", ".join(missing)
)
return cls(
config = cls(
state_db=Path(env["NISSE_STATE_DB"]).expanduser(),
workspace=Path(env["NISSE_WORKSPACE"]).expanduser(),
project=env["NISSE_PROJECT"],
@@ -86,6 +86,30 @@ class AgentConfig:
gitea_operators=_people(env["NISSE_GITEA_OPERATORS"]),
base_branch=env.get("NISSE_BASE_BRANCH", "main"),
)
config._refuse_secrets_inside_the_clone(env)
return config
def _refuse_secrets_inside_the_clone(self, env: Mapping[str, str]) -> None:
"""The workspace is a git clone the agent commits with ``git add -A``
and pushes. Anything inside it reaches the project repository, so a
state store or an env file placed there would publish credentials as a
side effect of the agent working correctly — silently, and once.
"""
workspace = self.workspace.expanduser().resolve()
offenders = []
for label, path in (
("NISSE_STATE_DB", self.state_db),
("NISSE_ENV", Path(env.get("NISSE_ENV", ".env"))),
):
resolved = path.expanduser().resolve()
if resolved == workspace or workspace in resolved.parents:
offenders.append(f"{label} ({resolved})")
if offenders:
raise ConfigError(
"refusing to start: these sit inside the workspace clone at "
f"{workspace} and would be committed and pushed to the project "
"repository — " + ", ".join(offenders)
)
def redacted(self) -> dict[str, str]:
"""What the agent may safely announce about how it is configured."""
+41
View File
@@ -89,3 +89,44 @@ def test_state_db_and_workspace_are_separate(tmp_path: Path) -> None:
# A2: build artefacts must not share a volume with agent state.
config = AgentConfig.from_env(COMPLETE)
assert config.state_db.parent != config.workspace
def test_state_db_inside_the_clone_is_refused(tmp_path: Path) -> None:
# git add -A would commit it, and the push would publish the agent's
# own state alongside the project.
work = tmp_path / "project"
with pytest.raises(ConfigError, match="inside the workspace"):
AgentConfig.from_env(
{
**COMPLETE,
"NISSE_WORKSPACE": str(work),
"NISSE_STATE_DB": str(work / "state.db"),
}
)
def test_env_file_inside_the_clone_is_refused(tmp_path: Path) -> None:
# The severe case: the agent would commit and push its own credentials
# as a side effect of working correctly.
work = tmp_path / "project"
with pytest.raises(ConfigError, match="NISSE_ENV"):
AgentConfig.from_env(
{
**COMPLETE,
"NISSE_WORKSPACE": str(work),
"NISSE_STATE_DB": str(tmp_path / "state.db"),
"NISSE_ENV": str(work / ".env"),
}
)
def test_sibling_paths_are_fine(tmp_path: Path) -> None:
config = AgentConfig.from_env(
{
**COMPLETE,
"NISSE_WORKSPACE": str(tmp_path / "project"),
"NISSE_STATE_DB": str(tmp_path / "state.db"),
"NISSE_ENV": str(tmp_path / ".env"),
}
)
assert config.workspace.name == "project"