Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion loopx/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
rel_or_abs,
require_single_goal_state_route,
resolve_runtime_root,
validate_goal_id_path_segment,
)
from .control_plane.goals.active_state_metadata import markdown_blockquote, markdown_frontmatter_string
from .registry_writability import probe_registry_write_path
Expand Down Expand Up @@ -327,10 +328,10 @@ def bootstrap_project(
creation_operation_id: str | None = None,
) -> dict[str, Any]:
project = project.expanduser().resolve()
goal_id = validate_goal_id_path_segment(goal_id or default_goal_id(project))
registry_path = registry_path.expanduser()
if not registry_path.is_absolute():
registry_path = project / registry_path
goal_id = goal_id or default_goal_id(project)
explicit_state_file = state_file is not None
state_file = state_file or registered_goal_state_file(
project, goal_id, read_json_if_exists(registry_path)
Expand Down
40 changes: 40 additions & 0 deletions tests/control_plane/test_goal_scoped_log_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,46 @@ def test_history_evidence_cli_rejects_unsafe_goal_without_writing_outside_root(
assert not (runtime_root / "escape").exists()


def test_bootstrap_rejects_unsafe_goal_id_before_any_write(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
import json

from loopx.cli import main

project = tmp_path / "project"
registry_path = project / ".loopx" / "registry.json"
runtime_root = tmp_path / "runtime"
escaped_state = tmp_path / "escaped" / "ACTIVE_GOAL_STATE.md"

exit_code = main(
[
"--registry",
str(registry_path),
"--runtime-root",
str(runtime_root),
"--format",
"json",
"bootstrap",
"--project",
str(project),
"--goal-id",
"../../../escaped",
"--objective",
"Synthetic path boundary check",
"--no-global-sync",
]
)

assert exit_code == 1
payload = json.loads(capsys.readouterr().out)
assert payload["ok"] is False
assert payload["error"] == "goal id must be a single path segment"
assert not registry_path.exists()
assert not escaped_state.exists()


def test_cli_rollout_event_does_not_create_path_outside_goals_root(tmp_path: Path) -> None:
from loopx.cli_rollout import append_cli_rollout_event

Expand Down
Loading