Generalize Pi sandbox launcher

This commit is contained in:
jb
2026-08-25 15:38:17 -04:00
parent 31c9363dbb
commit bab3c4589f
17 changed files with 637 additions and 391 deletions
+16 -177
View File
@@ -1,191 +1,30 @@
#!/usr/bin/env python3
"""Manage trusted project-to-nono-profile mappings for Orca-launched Pi."""
"""Compatibility entry point for pi-sbx-profile."""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import tempfile
import shutil
import sys
from pathlib import Path
from typing import Any
MAP_PATH = Path("/srv/data/code/harness-tools/profiles/orca/project-nono-profiles.json")
DEFAULT_PROFILE = "pi-orca-local"
def git_output(path: Path, *args: str) -> str | None:
result = subprocess.run(
["git", "-C", str(path), *args],
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
return None
value = result.stdout.strip()
return value or None
ALIASES = {
"set-root": "set",
"unset-root": "unset",
}
def canonical_project_root(path: Path) -> Path:
candidate = path.expanduser().resolve()
common_dir = git_output(candidate, "rev-parse", "--path-format=absolute", "--git-common-dir")
if common_dir:
common_path = Path(common_dir).resolve()
if common_path.name == ".git":
return common_path.parent
def main() -> None:
executable = shutil.which("pi-sbx-profile")
if executable is None:
executable = str(Path(__file__).resolve().with_name("pi-sbx-profile.py"))
top_level = git_output(candidate, "rev-parse", "--show-toplevel")
if top_level:
return Path(top_level).resolve()
return candidate
def load_map() -> dict[str, Any]:
try:
contents = MAP_PATH.read_text(encoding="utf-8")
data = json.loads(contents)
except (OSError, json.JSONDecodeError) as error:
raise ValueError(f"cannot load profile map: {error}") from error
if not isinstance(data, dict):
raise ValueError("profile map must be a JSON object")
projects = data.get("projects")
if not isinstance(projects, dict):
raise ValueError("profile map projects must be a JSON object")
project_roots = data.get("projectRoots", {})
if not isinstance(project_roots, dict):
raise ValueError("profile map projectRoots must be a JSON object")
for root, profile in project_roots.items():
if not isinstance(root, str) or not Path(root).is_absolute():
raise ValueError("projectRoots keys must be absolute paths")
if not isinstance(profile, str) or not profile:
raise ValueError("projectRoots values must be non-empty profile names")
default = data.get("defaultProfile", DEFAULT_PROFILE)
if not isinstance(default, str) or not default:
raise ValueError("defaultProfile must be a non-empty string")
return data
def save_map(data: dict[str, Any]) -> None:
MAP_PATH.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(prefix=".project-nono-profiles.", dir=MAP_PATH.parent)
temporary = Path(temporary_name)
try:
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
json.dump(data, handle, indent=2, sort_keys=True)
handle.write("\n")
temporary.chmod(0o600)
os.replace(temporary, MAP_PATH)
finally:
temporary.unlink(missing_ok=True)
def profile_exists(profile: str) -> bool:
result = subprocess.run(
["nono", "profile", "show", profile],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return result.returncode == 0
def resolve_profile(
data: dict[str, Any], project: Path, launch_path: Path | None = None
) -> tuple[str, str]:
projects = data["projects"]
mapped = projects.get(str(project))
if isinstance(mapped, str) and mapped:
return mapped, "project"
route_candidates = {project}
if launch_path is not None:
route_candidates.add(launch_path)
matching_roots: list[tuple[Path, str]] = []
for root_value, profile in data.get("projectRoots", {}).items():
root = Path(root_value)
if any(candidate == root or root in candidate.parents for candidate in route_candidates):
matching_roots.append((root, profile))
if matching_roots:
_root, profile = max(matching_roots, key=lambda match: len(match[0].parts))
return profile, "project-root"
return data.get("defaultProfile", DEFAULT_PROFILE), "default"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
subparsers = parser.add_subparsers(dest="command", required=True)
for command in ("resolve", "show", "unset", "unset-root"):
subparser = subparsers.add_parser(command)
subparser.add_argument("path", nargs="?", default=os.getcwd())
for command in ("set", "set-root"):
set_parser = subparsers.add_parser(command)
set_parser.add_argument("profile")
set_parser.add_argument("path", nargs="?", default=os.getcwd())
subparsers.add_parser("list")
args = parser.parse_args()
try:
data = load_map()
except ValueError as error:
parser.error(str(error))
if args.command == "list":
print(json.dumps(data, indent=2, sort_keys=True))
return 0
launch_path = Path(args.path).expanduser().resolve()
project = canonical_project_root(launch_path)
if args.command == "resolve":
profile, _source = resolve_profile(data, project, launch_path)
print(profile)
return 0
if args.command == "show":
profile, source = resolve_profile(data, project, launch_path)
print(
json.dumps(
{
"launchPath": str(launch_path),
"projectRoot": str(project),
"profile": profile,
"source": source,
},
indent=2,
sort_keys=True,
)
)
return 0
if args.command in ("set", "set-root"):
if not profile_exists(args.profile):
parser.error(f"nono profile does not exist: {args.profile}")
mapping_key = "projects" if args.command == "set" else "projectRoots"
mapping_path = project if args.command == "set" else launch_path
data.setdefault(mapping_key, {})[str(mapping_path)] = args.profile
save_map(data)
print(f"{mapping_path} -> {args.profile}")
return 0
if args.command in ("unset", "unset-root"):
mapping_key = "projects" if args.command == "unset" else "projectRoots"
mapping_path = project if args.command == "unset" else launch_path
removed = data.setdefault(mapping_key, {}).pop(str(mapping_path), None)
save_map(data)
if removed is None:
print(f"no mapping for {mapping_path}")
else:
print(f"removed {mapping_path} -> {removed}")
return 0
raise AssertionError(f"unhandled command: {args.command}")
arguments = sys.argv[1:]
if arguments:
arguments[0] = ALIASES.get(arguments[0], arguments[0])
os.execv(executable, [executable, *arguments])
if __name__ == "__main__":
raise SystemExit(main())
main()
-14
View File
@@ -1,14 +0,0 @@
function configure_pi_orca_runtime --argument-names profile
set -g pi_orca_node_executable /usr/bin/node
set -gx PI_CODING_AGENT_DIR /home/jb/.pi/agent
if test "$profile" = pi-work
set pi_orca_node_executable /home/jb/.local/share/fnm/node-versions/v22.23.2/installation/bin/node
set PI_CODING_AGENT_DIR /home/jb/.pi/profiles/cyone
end
# Keep Pi and its subprocesses on the same Node installation. Orca inherits
# fnm's PATH, which otherwise makes tool commands resolve a different Node.
set -l node_bin (path dirname "$pi_orca_node_executable")
set -gx PATH "$node_bin" $PATH
end
+9 -47
View File
@@ -1,53 +1,15 @@
#!/usr/bin/env fish
# Select a nono profile from a trusted canonical-project map, then launch Pi in
# its own bounded cgroup. Unknown or invalid projects fail closed.
set -l resolver /home/jb/.local/bin/orca-nono-profile
# Compatibility entry point for existing Orca configuration.
set -l launcher (command -s pi-sbx)
if test -z "$launcher"
set -l script_directory (path dirname (status filename))
set launcher "$script_directory/pi-sbx.fish"
end
if not test -x "$resolver"
printf 'pi-orca: profile resolver is unavailable: %s\n' "$resolver" >&2
if not test -x "$launcher"
printf 'pi-orca: pi-sbx is unavailable: %s\n' "$launcher" >&2
exit 1
end
set -l profile ("$resolver" resolve "$PWD" 2>/dev/null)
if test $status -ne 0; or test -z "$profile"
printf 'pi-orca: could not resolve a trusted profile for %s\n' "$PWD" >&2
exit 1
end
if not /usr/bin/nono profile show "$profile" >/dev/null 2>&1
printf 'pi-orca: resolved profile is unavailable: %s\n' "$profile" >&2
exit 1
end
source /srv/data/code/harness-tools/scripts/pi-orca-runtime.fish
configure_pi_orca_runtime "$profile"
set -l node_executable "$pi_orca_node_executable"
set -l pi_agent_dir "$PI_CODING_AGENT_DIR"
if not test -x "$node_executable"
printf 'pi-orca: Node runtime is unavailable: %s\n' "$node_executable" >&2
exit 1
end
if not test -d "$pi_agent_dir"
printf 'pi-orca: Pi agent directory is unavailable: %s\n' "$pi_agent_dir" >&2
exit 1
end
set -lx ORCA_NONO_PROFILE "$profile"
set -lx PI_CODING_AGENT_DIR "$pi_agent_dir"
exec /usr/bin/systemd-run \
--user \
--scope \
--collect \
--quiet \
--slice=orca-agents.slice \
--property=CPUQuota=1600% \
--property=MemoryHigh=48G \
--property=MemoryMax=64G \
--property=MemorySwapMax=8G \
--property=TasksMax=8192 \
--property=OOMPolicy=stop \
-- /usr/bin/nono run --profile "$profile" -- "$node_executable" /home/jb/.bun/bin/pi $argv
exec "$launcher" $argv
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""Manage directory-to-profile mappings for pi-sbx."""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import tempfile
from pathlib import Path
from typing import Any
DEFAULT_CONFIG_PATH = Path("~/.config/pi-sbx/config.json")
RUNTIME_FIELDS = ("nodeExecutable", "piExecutable", "piAgentDirectory")
def config_path() -> Path:
override = os.environ.get("PI_SBX_CONFIG")
if override:
selected_path = Path(override).expanduser()
if not selected_path.is_absolute():
raise ValueError("PI_SBX_CONFIG must be an absolute path or start with ~")
return selected_path
xdg_config_home = os.environ.get("XDG_CONFIG_HOME")
if xdg_config_home:
selected_directory = Path(xdg_config_home).expanduser()
if selected_directory.is_absolute():
return selected_directory / "pi-sbx" / "config.json"
return DEFAULT_CONFIG_PATH.expanduser()
def validate_runtime(runtime: Any, field_name: str, *, require_all: bool) -> None:
if not isinstance(runtime, dict):
raise ValueError(f"{field_name} must be a JSON object")
unknown_fields = set(runtime) - set(RUNTIME_FIELDS)
if unknown_fields:
unknown = ", ".join(sorted(unknown_fields))
raise ValueError(f"{field_name} contains unknown fields: {unknown}")
if require_all:
missing_fields = set(RUNTIME_FIELDS) - set(runtime)
if missing_fields:
missing = ", ".join(sorted(missing_fields))
raise ValueError(f"{field_name} is missing fields: {missing}")
for key, value in runtime.items():
if not isinstance(value, str) or not value:
raise ValueError(f"{field_name}.{key} must be a non-empty path")
if not Path(value).expanduser().is_absolute():
raise ValueError(f"{field_name}.{key} must be an absolute path or start with ~")
def validate_config(data: Any) -> dict[str, Any]:
if not isinstance(data, dict):
raise ValueError("configuration must be a JSON object")
default_profile = data.get("defaultProfile")
if not isinstance(default_profile, str) or not default_profile:
raise ValueError("defaultProfile must be a non-empty profile name")
directories = data.get("directories")
if not isinstance(directories, dict):
raise ValueError("directories must be a JSON object")
for directory, profile in directories.items():
if not isinstance(directory, str):
raise ValueError("directories keys must be canonical absolute paths")
configured_path = Path(directory).expanduser()
if not configured_path.is_absolute() or str(configured_path.resolve()) != directory:
raise ValueError("directories keys must be canonical absolute physical paths")
if not isinstance(profile, str) or not profile:
raise ValueError("directories values must be non-empty profile names")
validate_runtime(data.get("defaultRuntime"), "defaultRuntime", require_all=True)
profile_runtimes = data.get("profileRuntimes", {})
if not isinstance(profile_runtimes, dict):
raise ValueError("profileRuntimes must be a JSON object")
for profile, runtime in profile_runtimes.items():
if not isinstance(profile, str) or not profile:
raise ValueError("profileRuntimes keys must be non-empty profile names")
validate_runtime(runtime, f"profileRuntimes.{profile}", require_all=False)
return data
def load_config(path: Path | None = None) -> dict[str, Any]:
selected_path = path or config_path()
try:
contents = selected_path.read_text(encoding="utf-8")
data = json.loads(contents)
except (OSError, json.JSONDecodeError) as error:
raise ValueError(f"cannot load configuration from {selected_path}: {error}") from error
return validate_config(data)
def save_config(data: dict[str, Any], path: Path | None = None) -> None:
validate_config(data)
selected_path = path or config_path()
selected_path.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(prefix=".config.", dir=selected_path.parent)
temporary = Path(temporary_name)
try:
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
json.dump(data, handle, indent=2, sort_keys=True)
handle.write("\n")
temporary.chmod(0o600)
os.replace(temporary, selected_path)
finally:
temporary.unlink(missing_ok=True)
def resolve_profile(data: dict[str, Any], path: Path) -> tuple[str, str, Path | None]:
launch_path = path.expanduser().resolve()
matches: list[tuple[Path, str]] = []
for directory_value, profile in data["directories"].items():
directory = Path(directory_value).expanduser().resolve()
if launch_path == directory or directory in launch_path.parents:
matches.append((directory, profile))
if matches:
directory, profile = max(matches, key=lambda match: len(match[0].parts))
return profile, "directory", directory
return data["defaultProfile"], "default", None
def runtime_for_profile(data: dict[str, Any], profile: str) -> dict[str, str]:
runtime = dict(data["defaultRuntime"])
runtime.update(data.get("profileRuntimes", {}).get(profile, {}))
return {key: str(Path(runtime[key]).expanduser()) for key in RUNTIME_FIELDS}
def resolved_configuration(data: dict[str, Any], path: Path) -> dict[str, Any]:
launch_path = path.expanduser().resolve()
profile, source, directory = resolve_profile(data, launch_path)
return {
"launchPath": str(launch_path),
"matchedDirectory": str(directory) if directory else None,
"profile": profile,
"runtime": runtime_for_profile(data, profile),
"source": source,
}
def profile_exists(profile: str) -> bool:
result = subprocess.run(
["nono", "profile", "show", profile],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return result.returncode == 0
def create_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
subparsers = parser.add_subparsers(dest="command", required=True)
resolve_parser = subparsers.add_parser("resolve")
resolve_parser.add_argument("path", nargs="?", default=os.getcwd())
resolve_parser.add_argument(
"--format",
choices=("profile", "launcher", "json"),
default="profile",
)
show_parser = subparsers.add_parser("show")
show_parser.add_argument("path", nargs="?", default=os.getcwd())
set_parser = subparsers.add_parser("set")
set_parser.add_argument("profile")
set_parser.add_argument("path", nargs="?", default=os.getcwd())
unset_parser = subparsers.add_parser("unset")
unset_parser.add_argument("path", nargs="?", default=os.getcwd())
subparsers.add_parser("list")
return parser
def main() -> int:
parser = create_parser()
args = parser.parse_args()
try:
data = load_config()
except ValueError as error:
parser.error(str(error))
if args.command == "list":
print(json.dumps(data, indent=2, sort_keys=True))
return 0
path = Path(args.path).expanduser().resolve()
if args.command in ("resolve", "show"):
resolved = resolved_configuration(data, path)
if args.command == "show" or args.format == "json":
print(json.dumps(resolved, indent=2, sort_keys=True))
elif args.format == "launcher":
runtime = resolved["runtime"]
print(resolved["profile"])
print(runtime["nodeExecutable"])
print(runtime["piExecutable"])
print(runtime["piAgentDirectory"])
else:
print(resolved["profile"])
return 0
if args.command == "set":
if not profile_exists(args.profile):
parser.error(f"nono profile does not exist: {args.profile}")
data["directories"][str(path)] = args.profile
save_config(data)
print(f"{path} -> {args.profile}")
return 0
if args.command == "unset":
removed = data["directories"].pop(str(path), None)
save_config(data)
if removed is None:
print(f"no mapping for {path}")
else:
print(f"removed {path} -> {removed}")
return 0
raise AssertionError(f"unhandled command: {args.command}")
if __name__ == "__main__":
raise SystemExit(main())
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env fish
# Select a trusted nono profile and matching Pi runtime from the current
# directory, then launch Pi in a bounded systemd scope.
set -l resolver
if set -q PI_SBX_PROFILE_COMMAND
set resolver "$PI_SBX_PROFILE_COMMAND"
else
set resolver (command -s pi-sbx-profile)
end
if test -z "$resolver"; or not test -x "$resolver"
printf 'pi-sbx: profile resolver is unavailable: %s\n' "$resolver" >&2
exit 1
end
set -l resolved ("$resolver" resolve "$PWD" --format launcher 2>/dev/null)
if test $status -ne 0; or test (count $resolved) -ne 4
printf 'pi-sbx: could not resolve a valid configuration for %s\n' "$PWD" >&2
exit 1
end
set -l profile "$resolved[1]"
set -l node_executable "$resolved[2]"
set -l pi_executable "$resolved[3]"
set -l pi_agent_dir "$resolved[4]"
set -l nono_executable (command -s nono)
if test -z "$nono_executable"; or not test -x "$nono_executable"
printf 'pi-sbx: nono is unavailable on PATH\n' >&2
exit 1
end
if not "$nono_executable" profile show "$profile" >/dev/null 2>&1
printf 'pi-sbx: resolved nono profile is unavailable: %s\n' "$profile" >&2
exit 1
end
if not test -x "$node_executable"
printf 'pi-sbx: Node runtime is unavailable: %s\n' "$node_executable" >&2
exit 1
end
if not test -f "$pi_executable"
printf 'pi-sbx: Pi executable is unavailable: %s\n' "$pi_executable" >&2
exit 1
end
if not test -d "$pi_agent_dir"
printf 'pi-sbx: Pi agent directory is unavailable: %s\n' "$pi_agent_dir" >&2
exit 1
end
set -l node_bin (path dirname "$node_executable")
set -gx PATH "$node_bin" $PATH
set -lx PI_SBX_PROFILE "$profile"
set -lx PI_CODING_AGENT_DIR "$pi_agent_dir"
# Keep Orca hook integrations working while callers migrate to PI_SBX_PROFILE.
set -lx ORCA_NONO_PROFILE "$profile"
exec /usr/bin/systemd-run \
--user \
--scope \
--collect \
--quiet \
--slice=pi-agents.slice \
--property=CPUQuota=1600% \
--property=MemoryHigh=48G \
--property=MemoryMax=64G \
--property=MemorySwapMax=8G \
--property=TasksMax=8192 \
--property=OOMPolicy=stop \
-- "$nono_executable" run --profile "$profile" -- "$node_executable" "$pi_executable" $argv
-65
View File
@@ -1,65 +0,0 @@
from __future__ import annotations
import importlib.util
import unittest
from pathlib import Path
MODULE_PATH = Path(__file__).with_name("orca-nono-profile.py")
SPEC = importlib.util.spec_from_file_location("orca_nono_profile", MODULE_PATH)
if SPEC is None or SPEC.loader is None:
raise RuntimeError(f"Could not load runner resolver from {MODULE_PATH}")
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
class ResolveProfileTests(unittest.TestCase):
def setUp(self) -> None:
self.mapping = {
"defaultProfile": "pi-orca-local",
"projectRoots": {
"/srv/work": "pi-work",
"/srv/work/special": "pi-special",
},
"projects": {
"/srv/work/special/override": "pi-override",
},
}
def test_exact_project_mapping_wins(self) -> None:
self.assertEqual(
MODULE.resolve_profile(self.mapping, Path("/srv/work/special/override")),
("pi-override", "project"),
)
def test_longest_project_root_wins(self) -> None:
self.assertEqual(
MODULE.resolve_profile(self.mapping, Path("/srv/work/special/repository")),
("pi-special", "project-root"),
)
def test_project_root_matches_descendants(self) -> None:
self.assertEqual(
MODULE.resolve_profile(self.mapping, Path("/srv/work/code/repository")),
("pi-work", "project-root"),
)
def test_linked_worktree_uses_its_launch_path_root(self) -> None:
self.assertEqual(
MODULE.resolve_profile(
self.mapping,
Path("/home/jb/code/main-checkout"),
Path("/srv/work/worktrees/feature"),
),
("pi-work", "project-root"),
)
def test_project_root_does_not_match_similar_prefix(self) -> None:
self.assertEqual(
MODULE.resolve_profile(self.mapping, Path("/srv/worker/repository")),
("pi-orca-local", "default"),
)
if __name__ == "__main__":
unittest.main()
-60
View File
@@ -1,60 +0,0 @@
from __future__ import annotations
import os
import subprocess
import unittest
from pathlib import Path
RUNTIME_SCRIPT = Path(__file__).with_name("pi-orca-runtime.fish")
SYSTEM_NODE = Path("/usr/bin/node")
WORK_NODE = Path(
"/home/jb/.local/share/fnm/node-versions/v22.23.2/installation/bin/node"
)
class ConfigurePiOrcaRuntimeTests(unittest.TestCase):
def configure_runtime(self, profile: str) -> list[str]:
environment = os.environ.copy()
environment["PATH"] = ":".join(
[str(WORK_NODE.parent), "/usr/local/bin", "/usr/bin"]
)
command = """
source $argv[1]
configure_pi_orca_runtime $argv[2]
printf '%s\\n' $pi_orca_node_executable $PI_CODING_AGENT_DIR $PATH[1] (command -v node)
"""
result = subprocess.run(
["/usr/bin/fish", "-c", command, str(RUNTIME_SCRIPT), profile],
check=True,
capture_output=True,
env=environment,
text=True,
)
return result.stdout.splitlines()
def test_default_profile_prioritizes_system_node(self) -> None:
self.assertEqual(
self.configure_runtime("pi-harness-tools"),
[
str(SYSTEM_NODE),
"/home/jb/.pi/agent",
str(SYSTEM_NODE.parent),
str(SYSTEM_NODE),
],
)
def test_work_profile_prioritizes_node_22(self) -> None:
self.assertEqual(
self.configure_runtime("pi-work"),
[
str(WORK_NODE),
"/home/jb/.pi/profiles/cyone",
str(WORK_NODE.parent),
str(WORK_NODE),
],
)
if __name__ == "__main__":
unittest.main()
+72
View File
@@ -0,0 +1,72 @@
from __future__ import annotations
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
LAUNCHER = Path(__file__).with_name("pi-sbx.fish")
class PiSbxLauncherTests(unittest.TestCase):
def run_launcher_with_resolver(self, output: str) -> subprocess.CompletedProcess[str]:
with tempfile.TemporaryDirectory() as directory:
resolver = Path(directory) / "resolver"
resolver.write_text(f"#!/bin/sh\nprintf '%s' '{output}'\n", encoding="utf-8")
resolver.chmod(0o755)
environment = os.environ.copy()
environment["PI_SBX_PROFILE_COMMAND"] = str(resolver)
return subprocess.run(
["/usr/bin/fish", str(LAUNCHER)],
env=environment,
check=False,
capture_output=True,
text=True,
)
def test_rejects_incomplete_launcher_configuration(self) -> None:
result = self.run_launcher_with_resolver("pi-local\n/usr/bin/node\n")
self.assertEqual(result.returncode, 1)
self.assertIn("could not resolve a valid configuration", result.stderr)
def test_rejects_unavailable_profile(self) -> None:
result = self.run_launcher_with_resolver(
"profile-that-does-not-exist\n/usr/bin/node\n/home/jb/.bun/bin/pi\n/home/jb/.pi/agent\n"
)
self.assertEqual(result.returncode, 1)
self.assertIn("resolved nono profile is unavailable", result.stderr)
def test_uses_nono_from_path(self) -> None:
with tempfile.TemporaryDirectory() as directory:
temporary = Path(directory)
resolver = temporary / "resolver"
resolver.write_text(
"#!/bin/sh\nprintf '%b' 'pi-local\\n/missing/node\\n/missing/pi\\n/missing/agent\\n'\n",
encoding="utf-8",
)
resolver.chmod(0o755)
nono = temporary / "nono"
nono.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
nono.chmod(0o755)
environment = os.environ.copy()
environment["PI_SBX_PROFILE_COMMAND"] = str(resolver)
environment["PATH"] = f"{temporary}:{environment['PATH']}"
result = subprocess.run(
["/usr/bin/fish", str(LAUNCHER)],
env=environment,
check=False,
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 1)
self.assertIn("Node runtime is unavailable", result.stderr)
if __name__ == "__main__":
unittest.main()
+188
View File
@@ -0,0 +1,188 @@
from __future__ import annotations
import importlib.util
import json
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
MODULE_PATH = Path(__file__).with_name("pi-sbx-profile.py")
SPEC = importlib.util.spec_from_file_location("pi_sbx_profile", MODULE_PATH)
if SPEC is None or SPEC.loader is None:
raise RuntimeError(f"Could not load profile resolver from {MODULE_PATH}")
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
class PiSbxProfileTests(unittest.TestCase):
def setUp(self) -> None:
self.config = {
"defaultProfile": "pi-local",
"directories": {
"/srv/work": "pi-work",
"/srv/work/special": "pi-special",
},
"defaultRuntime": {
"nodeExecutable": "/usr/bin/node",
"piExecutable": "~/.bun/bin/pi",
"piAgentDirectory": "~/.pi/agent",
},
"profileRuntimes": {
"pi-work": {
"nodeExecutable": "/opt/node-22/bin/node",
"piAgentDirectory": "~/.pi/profiles/work",
}
},
}
def test_longest_directory_match_wins(self) -> None:
self.assertEqual(
MODULE.resolve_profile(self.config, Path("/srv/work/special/repository")),
("pi-special", "directory", Path("/srv/work/special")),
)
def test_directory_matches_descendants(self) -> None:
self.assertEqual(
MODULE.resolve_profile(self.config, Path("/srv/work/code/repository")),
("pi-work", "directory", Path("/srv/work")),
)
def test_directory_does_not_match_similar_prefix(self) -> None:
self.assertEqual(
MODULE.resolve_profile(self.config, Path("/srv/worker/repository")),
("pi-local", "default", None),
)
def test_profile_runtime_overrides_defaults(self) -> None:
self.assertEqual(
MODULE.runtime_for_profile(self.config, "pi-work"),
{
"nodeExecutable": "/opt/node-22/bin/node",
"piExecutable": str(Path("~/.bun/bin/pi").expanduser()),
"piAgentDirectory": str(Path("~/.pi/profiles/work").expanduser()),
},
)
def test_default_runtime_is_used_without_override(self) -> None:
runtime = MODULE.runtime_for_profile(self.config, "pi-special")
self.assertEqual(runtime["nodeExecutable"], "/usr/bin/node")
self.assertEqual(runtime["piAgentDirectory"], str(Path("~/.pi/agent").expanduser()))
def test_configuration_requires_complete_default_runtime(self) -> None:
del self.config["defaultRuntime"]["piExecutable"]
with self.assertRaisesRegex(ValueError, "missing fields: piExecutable"):
MODULE.validate_config(self.config)
def test_configuration_rejects_noncanonical_directory(self) -> None:
self.config["directories"] = {"~/work": "pi-local"}
with self.assertRaisesRegex(ValueError, "canonical absolute physical paths"):
MODULE.validate_config(self.config)
def test_relative_xdg_config_home_is_ignored(self) -> None:
original_xdg = os.environ.get("XDG_CONFIG_HOME")
original_override = os.environ.pop("PI_SBX_CONFIG", None)
os.environ["XDG_CONFIG_HOME"] = "repository-controlled"
try:
self.assertEqual(MODULE.config_path(), MODULE.DEFAULT_CONFIG_PATH.expanduser())
finally:
if original_xdg is None:
os.environ.pop("XDG_CONFIG_HOME", None)
else:
os.environ["XDG_CONFIG_HOME"] = original_xdg
if original_override is not None:
os.environ["PI_SBX_CONFIG"] = original_override
def test_relative_config_override_is_rejected(self) -> None:
original_override = os.environ.get("PI_SBX_CONFIG")
os.environ["PI_SBX_CONFIG"] = "config.json"
try:
with self.assertRaisesRegex(ValueError, "PI_SBX_CONFIG must be an absolute path"):
MODULE.config_path()
finally:
if original_override is None:
os.environ.pop("PI_SBX_CONFIG", None)
else:
os.environ["PI_SBX_CONFIG"] = original_override
def test_save_and_load_configuration(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "pi-sbx" / "config.json"
MODULE.save_config(self.config, path)
self.assertEqual(MODULE.load_config(path), self.config)
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
self.assertEqual(json.loads(path.read_text(encoding="utf-8")), self.config)
def test_set_without_path_assigns_current_directory(self) -> None:
with tempfile.TemporaryDirectory() as directory:
temporary = Path(directory)
config_path = temporary / "config.json"
workdir = temporary / "repository"
binary_directory = temporary / "bin"
workdir.mkdir()
binary_directory.mkdir()
MODULE.save_config(self.config, config_path)
nono = binary_directory / "nono"
nono.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
nono.chmod(0o755)
environment = os.environ.copy()
environment["PI_SBX_CONFIG"] = str(config_path)
environment["PATH"] = f"{binary_directory}:{environment['PATH']}"
result = subprocess.run(
[str(MODULE_PATH), "set", "pi-special"],
cwd=workdir,
env=environment,
check=True,
capture_output=True,
text=True,
)
self.assertEqual(result.stdout.strip(), f"{workdir} -> pi-special")
self.assertEqual(MODULE.load_config(config_path)["directories"][str(workdir)], "pi-special")
subprocess.run(
[str(MODULE_PATH), "unset"],
cwd=workdir,
env=environment,
check=True,
capture_output=True,
text=True,
)
self.assertNotIn(str(workdir), MODULE.load_config(config_path)["directories"])
def test_legacy_set_root_command_assigns_current_directory(self) -> None:
with tempfile.TemporaryDirectory() as directory:
temporary = Path(directory)
config_path = temporary / "config.json"
workdir = temporary / "repository"
binary_directory = temporary / "bin"
workdir.mkdir()
binary_directory.mkdir()
MODULE.save_config(self.config, config_path)
nono = binary_directory / "nono"
nono.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
nono.chmod(0o755)
environment = os.environ.copy()
environment["PI_SBX_CONFIG"] = str(config_path)
environment["PATH"] = f"{binary_directory}:{environment['PATH']}"
subprocess.run(
[str(Path(__file__).with_name("orca-nono-profile.py")), "set-root", "pi-special"],
cwd=workdir,
env=environment,
check=True,
capture_output=True,
text=True,
)
self.assertEqual(MODULE.load_config(config_path)["directories"][str(workdir)], "pi-special")
if __name__ == "__main__":
unittest.main()