Refuse to start with the state store or .env inside the clone
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m14s
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:
1 parent
2fb25b8195
commit
dfa574aeec
3 files changed
+79
-1
No files matched your search
@@ -37,6 +37,19 @@ NISSE_GITEA_OPERATORS=your-gitea-login
|
|||||||
|
|
||||||
# --- Running the agent (the demo, not the tests) ---
|
# --- Running the agent (the demo, not the tests) ---
|
||||||
# The tests use NISSE_GITEA_TEST_REPO; a running agent uses these.
|
# 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_STATE_DB=~/nisse-demo-workspace/state.db
|
||||||
NISSE_WORKSPACE=~/nisse-demo-workspace/project
|
NISSE_WORKSPACE=~/nisse-demo-workspace/project
|
||||||
NISSE_PROJECT=demo
|
NISSE_PROJECT=demo
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class AgentConfig:
|
|||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
"cannot start: these are unset or empty — " + ", ".join(missing)
|
"cannot start: these are unset or empty — " + ", ".join(missing)
|
||||||
)
|
)
|
||||||
return cls(
|
config = cls(
|
||||||
state_db=Path(env["NISSE_STATE_DB"]).expanduser(),
|
state_db=Path(env["NISSE_STATE_DB"]).expanduser(),
|
||||||
workspace=Path(env["NISSE_WORKSPACE"]).expanduser(),
|
workspace=Path(env["NISSE_WORKSPACE"]).expanduser(),
|
||||||
project=env["NISSE_PROJECT"],
|
project=env["NISSE_PROJECT"],
|
||||||
@@ -86,6 +86,30 @@ class AgentConfig:
|
|||||||
gitea_operators=_people(env["NISSE_GITEA_OPERATORS"]),
|
gitea_operators=_people(env["NISSE_GITEA_OPERATORS"]),
|
||||||
base_branch=env.get("NISSE_BASE_BRANCH", "main"),
|
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]:
|
def redacted(self) -> dict[str, str]:
|
||||||
"""What the agent may safely announce about how it is configured."""
|
"""What the agent may safely announce about how it is configured."""
|
||||||
|
|||||||
@@ -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.
|
# A2: build artefacts must not share a volume with agent state.
|
||||||
config = AgentConfig.from_env(COMPLETE)
|
config = AgentConfig.from_env(COMPLETE)
|
||||||
assert config.state_db.parent != config.workspace
|
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"
|
||||||
Reference in new issue
Block a user