feat(nono): add restrictive core policy contract

This commit is contained in:
jb
2026-08-30 12:49:17 -04:00
parent 54c20e2012
commit 90f8090f61
19 changed files with 1862 additions and 9 deletions
+1
View File
@@ -0,0 +1 @@
"""Capability-specific Nono policy tests."""
+20
View File
@@ -0,0 +1,20 @@
# Slice 1 core policy proofs
The hermetic contract tests inspect reviewed core/provider fragments, trusted composition, generated settings with no shell/background tool exposure, package-lock evidence, and the repository-managed LM Studio registration:
```bash
python -m unittest tests.nono.capabilities.core.test_policy
node --test --experimental-strip-types extensions/lm-studio/index.test.ts
```
The mandatory Linux host gate requires Nono 0.74.x. It creates only isolated HOME/XDG/session roots and temporary profile paths. It proves Nono composition and inherited command-denial/caller-edge behavior, personal/work Pi startup with the managed LM Studio extension loaded from isolated settings, real managed-extension model requests to the fixture endpoint, raw credential filtering, cross-session state denial, read-only versus read/write worktrees, and real selected-versus-adjacent port enforcement using loopback listeners on ports 1234 and 1235. Nono 0.74's grant is destination-port-only, as recorded in ADR 0003:
```bash
PYTHONPATH=. python tests/nono/capabilities/core/host_gate.py
```
The gate fails instead of skipping when Nono, Pi, or either fixture port is unavailable. It hashes active Pi settings, the unmanaged LM Studio source, and active Nono profile links before execution and fails if verification changes any of them. While quarantine is deferred, it also requires the active legacy source to match its reviewed lock digest; Slice 3 must replace that precondition with active-path absence and quarantined-file identity during the atomic cutover.
## Activation remains blocked
Slice 1 does not mutate active Pi settings, credentials, packages, or Nono profile links. The reviewed LM Studio source remains blocked in `config/pi-package-lock.json` until maintenance verifies the recorded legacy hash, quarantines the unmanaged active file outside Pi auto-discovery, and Slice 3 promotes this source into an immutable installed snapshot. Provider credential routes are schema-validated only; live credential evidence remains a separate explicitly approved operation.
+1
View File
@@ -0,0 +1 @@
"""Restrictive core and provider composition tests."""
+724
View File
@@ -0,0 +1,724 @@
#!/usr/bin/env python3
"""Mandatory Linux/Nono 0.74 behavioral gate for Slice 1 core policies."""
from __future__ import annotations
import hashlib
import json
import os
import shutil
import socket
import stat
import subprocess
import sys
import tempfile
import threading
from contextlib import ExitStack, closing
from pathlib import Path
from typing import Any
from tests.nono.capabilities.core.policy import compose_profile, isolated_settings, load_profile
from tests.nono.harness.fixtures import isolated_environment
ROOT = Path(__file__).resolve().parents[4]
PROFILES = ROOT / "profiles" / "nono"
LM_STUDIO = ROOT / "extensions" / "lm-studio" / "index.ts"
PI_CLI = Path(
"/home/jb/.bun/install/global/node_modules/"
"@earendil-works/pi-coding-agent/dist/bundle/cli.js"
)
PACKAGE_LOCK = ROOT / "config" / "pi-package-lock.json"
LEGACY_LM_STUDIO = Path("/home/jb/.pi/agent/extensions/lm-studio.ts")
ACTIVE_STATE_PATHS = (
Path("/home/jb/.pi/agent/settings.json"),
LEGACY_LM_STUDIO,
Path("/home/jb/.config/nono/profiles/pi-local.json"),
Path("/home/jb/.config/nono/profiles/pi-work.json"),
Path("/home/jb/.config/nono/profiles/pi-orca-local.json"),
Path("/home/jb/.config/nono/profiles/pi-harness-tools.json"),
)
RAW_CANARIES = {
"ANTHROPIC_API_KEY": "slice1-anthropic-canary",
"GH_TOKEN": "slice1-gh-canary",
"GITHUB_TOKEN": "slice1-github-canary",
"MORPH_API_KEY": "slice1-morph-canary",
"OPENAI_API_KEY": "slice1-openai-canary",
"SSH_AUTH_SOCK": "/tmp/slice1-forbidden-agent.sock",
}
class HostGateError(RuntimeError):
"""The mandatory Slice 1 host proof failed."""
def _active_state_snapshot() -> dict[str, tuple[str, str, int]]:
snapshot: dict[str, tuple[str, str, int]] = {}
for path in ACTIVE_STATE_PATHS:
if path.is_symlink():
target = str(path.readlink())
payload = path.resolve(strict=True).read_bytes()
elif path.is_file():
target = "regular"
payload = path.read_bytes()
else:
target = "absent"
payload = b""
snapshot[str(path)] = (
target,
hashlib.sha256(payload).hexdigest(),
path.lstat().st_mode if path.exists() or path.is_symlink() else 0,
)
return snapshot
def _read_regular_file(path: Path, *, limit: int = 1024 * 1024) -> bytes:
descriptor = os.open(path, os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW)
try:
before = os.fstat(descriptor)
if not stat.S_ISREG(before.st_mode) or before.st_size > limit:
raise HostGateError(f"expected one bounded regular file: {path}")
chunks: list[bytes] = []
total = 0
while True:
chunk = os.read(descriptor, min(65536, limit + 1 - total))
if not chunk:
break
chunks.append(chunk)
total += len(chunk)
if total > limit:
raise HostGateError(f"file exceeds verification limit: {path}")
after = os.fstat(descriptor)
if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) != (
after.st_dev,
after.st_ino,
after.st_size,
after.st_mtime_ns,
):
raise HostGateError(f"file changed during verification: {path}")
return b"".join(chunks)
finally:
os.close(descriptor)
def _validate_lm_studio_sources() -> bytes:
lock = json.loads(PACKAGE_LOCK.read_text(encoding="utf-8"))
entries = [
entry for entry in lock.get("extensions", []) if entry.get("name") == "lm-studio"
]
if len(entries) != 1:
raise HostGateError("package lock must contain exactly one LM Studio entry")
reviewed = entries[0].get("reviewed", {})
managed_source = _read_regular_file(LM_STUDIO)
managed_digest = hashlib.sha256(managed_source).hexdigest()
if reviewed.get("sha256") != managed_digest:
raise HostGateError("managed LM Studio source does not match package-lock evidence")
legacy_source = _read_regular_file(LEGACY_LM_STUDIO)
legacy_digest = hashlib.sha256(legacy_source).hexdigest()
if reviewed.get("legacySourceSha256") != legacy_digest:
raise HostGateError("legacy LM Studio source drifted before approved quarantine")
return managed_source
def _validate_static_profiles(nono: str, environment: dict[str, str]) -> None:
profiles = sorted(PROFILES.glob("pi-core-*.json")) + sorted(
PROFILES.glob("provider-*.json")
)
for profile in profiles:
result = subprocess.run(
[nono, "profile", "validate", "--strict", str(profile), "--json"],
env=environment,
capture_output=True,
text=True,
check=False,
timeout=10,
)
if result.returncode != 0:
raise HostGateError(f"strict validation failed for {profile}: {result.stdout}")
def _prove_provider_fragment_is_not_launchable(
nono: str, environment: dict[str, str]
) -> None:
result = subprocess.run(
[
nono,
"run",
"--profile",
str(PROFILES / "provider-lm-studio.json"),
"--",
"/usr/bin/env",
],
env=environment,
capture_output=True,
text=True,
input="",
check=False,
timeout=20,
)
if result.returncode == 0 or "directory is not readable" not in (
result.stdout + result.stderr
):
raise HostGateError("provider fragment was directly launchable")
def _prove_nono_composition(nono: str, session: Any, base_environment: dict[str, str]) -> None:
profiles = session.home / ".config" / "nono" / "profiles"
profiles.mkdir(parents=True, mode=0o700, exist_ok=True)
os.chmod(profiles, 0o700)
(profiles / "slice-one-parent-a.json").write_text(
json.dumps(
{
"extends": "default",
"meta": {"name": "slice-one-parent-a"},
"groups": {"exclude": ["system_write_linux", "user_tools"]},
"network": {"block": True},
"environment": {
"allow_vars": ["PATH"],
"deny_vars": ["SLICE_ONE_SECRET_A"],
},
"open_urls": {"allow_origins": [], "allow_localhost": False},
"allow_launch_services": False,
}
),
encoding="utf-8",
)
(profiles / "slice-one-parent-b.json").write_text(
json.dumps(
{
"extends": "default",
"meta": {"name": "slice-one-parent-b"},
"network": {"block": False, "connect_port": [1234]},
"environment": {
"allow_vars": ["HOME"],
"deny_vars": ["SLICE_ONE_SECRET_B"],
},
}
),
encoding="utf-8",
)
(profiles / "slice-one-child.json").write_text(
json.dumps(
{
"extends": ["slice-one-parent-a", "slice-one-parent-b"],
"meta": {"name": "slice-one-child"},
}
),
encoding="utf-8",
)
for profile_name, parent_name, decision in (
("slice-one-command-parent", "default", "deny"),
("slice-one-command-child", "slice-one-command-parent", "allow"),
):
(profiles / f"{profile_name}.json").write_text(
json.dumps(
{
"extends": parent_name,
"meta": {"name": profile_name},
"groups": {"exclude": ["system_write_linux"]},
"command_policies": {
"commands": {
"echo": {
"executable": "/usr/bin/echo",
"from": {
"session": {
"sandbox": {
"fs_read_file": ["/usr/bin/echo"]
},
"invocation_policy": {"default": decision},
}
},
}
}
},
}
),
encoding="utf-8",
)
environment = dict(base_environment)
environment.update(
{
"SLICE_ONE_SECRET_A": "canary-a",
"SLICE_ONE_SECRET_B": "canary-b",
"SLICE_ONE_UNLISTED": "canary-c",
}
)
shown = subprocess.run(
[nono, "profile", "show", "slice-one-child", "--json"],
env=environment,
capture_output=True,
text=True,
check=False,
timeout=10,
)
if shown.returncode != 0:
raise HostGateError(shown.stderr + shown.stdout)
resolved = json.loads(shown.stdout)
if resolved.get("network", {}).get("block") is not True:
raise HostGateError("Nono multiple inheritance widened a blocked network")
observed = subprocess.run(
[nono, "run", "--profile", "slice-one-child", "--", "/usr/bin/env"],
env=environment,
capture_output=True,
text=True,
input="",
check=False,
timeout=20,
)
if observed.returncode != 0:
raise HostGateError(observed.stderr + observed.stdout)
child_environment = {
line.partition("=")[0]
for line in observed.stdout.splitlines()
if "=" in line
}
if not {"HOME", "PATH"}.issubset(child_environment):
raise HostGateError("Nono did not merge parent environment allowlists")
if child_environment.intersection(
{"SLICE_ONE_SECRET_A", "SLICE_ONE_SECRET_B", "SLICE_ONE_UNLISTED"}
):
raise HostGateError("Nono environment composition exposed denied variables")
command_policy = subprocess.run(
[
nono,
"why",
"--profile",
"slice-one-command-child",
"--command",
"echo",
"--json",
"--",
"hello",
],
env=environment,
capture_output=True,
text=True,
check=False,
timeout=10,
)
if command_policy.returncode != 0:
raise HostGateError(command_policy.stderr + command_policy.stdout)
try:
command_decision = json.loads(command_policy.stdout)
except json.JSONDecodeError as error:
raise HostGateError(
f"Nono command policy query returned invalid JSON: "
f"{command_policy.stdout!r} {command_policy.stderr!r}"
) from error
if command_decision.get("status") != "denied":
raise HostGateError("Nono child command policy widened an inherited denial")
if command_decision.get("policy_source") != (
"command_policies.commands.echo.from.session.invocation_policy"
):
raise HostGateError("Nono command policy used an unexpected caller edge")
def _listener(port: int, stack: ExitStack, http_requests: dict[int, int]) -> None:
server = stack.enter_context(closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)))
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
server.bind(("127.0.0.1", port))
except OSError as error:
raise HostGateError(f"loopback fixture port {port} is unavailable: {error}") from error
server.listen()
http_requests[port] = 0
def serve() -> None:
while True:
try:
connection, _address = server.accept()
except OSError:
return
with connection:
request = connection.recv(4096)
if request.startswith(b"GET /v1/models "):
http_requests[port] += 1
body = b'{"data":[{"id":"slice-one-model","name":"Slice One Model"}]}'
connection.sendall(
b"HTTP/1.1 200 OK\r\n"
+ f"Content-Length: {len(body)}\r\n".encode("ascii")
+ b"Content-Type: application/json\r\nConnection: close\r\n\r\n"
+ body
)
else:
connection.sendall(b"slice-1-ok")
threading.Thread(target=serve, daemon=True).start()
def _prepare_state(session: Any, context: str, lm_studio_extension: Path) -> Path:
agent = session.state / "pi-sbx" / context / "agent"
sessions = session.state / "pi-sbx" / context / "sessions"
cache = session.cache / "pi-sbx" / context
for path in (agent, sessions, cache):
path.mkdir(parents=True, mode=0o700)
os.chmod(path, 0o700)
settings = isolated_settings("lm-studio", lm_studio_extension=lm_studio_extension)
(agent / "settings.json").write_text(
json.dumps(settings, sort_keys=True) + "\n",
encoding="utf-8",
)
os.chmod(agent / "settings.json", 0o600)
instructions = session.home / ".pi" / "agent" / "AGENTS.md"
instructions.parent.mkdir(parents=True, mode=0o700)
instructions.write_text("fixture instructions\n", encoding="utf-8")
return agent
def _write_probe(
repository: Path,
other_tmp: Path,
other_state: Path,
other_repository: Path,
installed_extension: Path,
unselected_config: Path,
) -> Path:
probe = repository / "slice_one_probe.py"
probe.write_text(
"""\
import json, os, socket
from pathlib import Path
ports = {}
for port in (1234, 1235):
try:
with socket.create_connection((\"127.0.0.1\", port), timeout=1) as connection:
connection.sendall(b\"SLICE_ONE_PROBE\")
ports[str(port)] = connection.recv(32).decode(\"utf-8\")
except Exception as error:
ports[str(port)] = type(error).__name__
def can_read(path):
try:
Path(path).read_bytes()
return True
except OSError:
return False
def can_write(path):
try:
Path(path).write_text(\"changed\", encoding=\"utf-8\")
return True
except OSError:
return False
print(json.dumps({
\"ports\": ports,
\"rawCredentials\": sorted(name for name in %r if name in os.environ),
\"otherTmpReadable\": can_read(%r),
\"otherStateReadable\": can_read(%r),
\"otherRepositoryReadable\": can_read(%r),
\"installedExtensionWritable\": can_write(%r),
\"unselectedConfigReadable\": can_read(%r),
\"workdirWritable\": can_write(Path.cwd() / \"probe-write.txt\"),
}, sort_keys=True))
"""
% (
tuple(sorted(RAW_CANARIES)),
str(other_tmp / "private.txt"),
str(other_state),
str(other_repository),
str(installed_extension),
str(unselected_config),
),
encoding="utf-8",
)
return probe
def _write_refresh_runner(repository: Path, installed_extension: Path) -> Path:
runner = repository / "lm_studio_refresh.mjs"
runner.write_text(
f"""\
import registerLmStudio from {json.dumps(installed_extension.as_uri())};
let refreshModels;
registerLmStudio({{
registerProvider(name, config) {{
if (name !== "lm-studio" || typeof config.refreshModels !== "function") {{
throw new Error("managed LM Studio provider registration is invalid");
}}
refreshModels = config.refreshModels;
}},
}});
const models = await refreshModels({{
allowNetwork: true,
signal: AbortSignal.timeout(3000),
}});
console.log(JSON.stringify({{ modelIds: models.map((model) => model.id) }}));
""",
encoding="utf-8",
)
return runner
def _run(
nono: str,
profile: Path,
repository: Path,
environment: dict[str, str],
command: list[str],
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[
nono,
"run",
"--profile",
str(profile),
"--allow-cwd",
"--workdir",
str(repository),
"--",
*command,
],
cwd=repository,
env=environment,
capture_output=True,
text=True,
input="",
check=False,
timeout=30,
)
def _last_json(stdout: str) -> dict[str, Any]:
for line in reversed(stdout.splitlines()):
try:
value = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(value, dict):
return value
raise HostGateError(f"command produced no JSON object: {stdout[-1000:]}")
def run_host_gate() -> dict[str, Any]:
if sys.platform != "linux":
raise HostGateError("Slice 1 host gate requires Linux")
nono = shutil.which("nono", path="/usr/bin:/bin")
if nono is None:
raise HostGateError("Nono is unavailable")
version = subprocess.run(
[nono, "--version"],
capture_output=True,
text=True,
check=False,
timeout=10,
env={"PATH": "/usr/bin:/bin", "LANG": "C", "LC_ALL": "C"},
)
if version.returncode != 0 or not version.stdout.strip().startswith("nono 0.74."):
raise HostGateError(f"Nono 0.74.x is required; observed {version.stdout.strip()!r}")
if not PI_CLI.is_file():
raise HostGateError(f"Pi CLI is unavailable: {PI_CLI}")
managed_lm_studio_source = _validate_lm_studio_sources()
outcomes: dict[str, Any] = {}
active_state_before = _active_state_snapshot()
listener_requests: dict[int, int] = {}
with ExitStack() as listeners:
_listener(1234, listeners, listener_requests)
_listener(1235, listeners, listener_requests)
# Nono's command-policy supervisor uses a Unix socket under TMPDIR;
# keep the isolated fixture path below Linux SUN_LEN.
with isolated_environment(prefix="s1-") as fixture:
installed_extension = fixture.root / "installed" / "lm-studio" / "index.ts"
installed_extension.parent.mkdir(parents=True, mode=0o700)
installed_extension.write_bytes(managed_lm_studio_source)
os.chmod(installed_extension, 0o400)
unselected_config = fixture.root / "unselected" / "auth.json"
unselected_config.parent.mkdir(parents=True, mode=0o700)
unselected_config.write_text("fixture credential config\n", encoding="utf-8")
os.chmod(unselected_config, 0o600)
sessions = {name: fixture.session(name) for name in ("personal", "work", "other")}
for session in sessions.values():
(session.tmp / "private.txt").write_text("private\n", encoding="utf-8")
(session.repo / "private.txt").write_text("private\n", encoding="utf-8")
personal_environment = sessions["personal"].environment(fixture.env)
_validate_static_profiles(nono, personal_environment)
_prove_provider_fragment_is_not_launchable(nono, personal_environment)
_prove_nono_composition(
nono,
sessions["personal"],
personal_environment,
)
other_state = sessions["other"].state / "pi-sbx" / "other"
other_state.mkdir(parents=True, mode=0o700)
(other_state / "private.txt").write_text("private\n", encoding="utf-8")
for context in ("personal", "work"):
session = sessions[context]
other = sessions["other"]
launch_temporary_directory = Path(
tempfile.mkdtemp(prefix=f"s1{context[0]}-", dir="/tmp")
)
os.chmod(launch_temporary_directory, 0o700)
listeners.callback(
shutil.rmtree,
launch_temporary_directory,
ignore_errors=True,
)
launch_environment = session.environment(fixture.env)
launch_environment["TMPDIR"] = str(launch_temporary_directory)
_prepare_state(session, context, installed_extension)
probe = _write_probe(
session.repo,
other.tmp,
other_state / "private.txt",
other.repo / "private.txt",
installed_extension,
unselected_config,
)
refresh_runner = _write_refresh_runner(session.repo, installed_extension)
profile = compose_profile(
load_profile(PROFILES / f"pi-core-{context}.json"),
load_profile(PROFILES / "provider-lm-studio.json"),
workdir_access="read" if context == "personal" else "readwrite",
temporary_directory=launch_temporary_directory,
lm_studio_extension=installed_extension,
)
profile_path = fixture.root / f"generated-{context}.json"
profile_path.write_text(
json.dumps(profile, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
os.chmod(profile_path, 0o600)
validation = subprocess.run(
[nono, "profile", "validate", "--strict", str(profile_path)],
env=fixture.env,
capture_output=True,
text=True,
check=False,
timeout=10,
)
if validation.returncode != 0:
raise HostGateError(validation.stderr + validation.stdout)
probe_environment = dict(launch_environment)
probe_environment.update(RAW_CANARIES)
probe_result = _run(
nono,
profile_path,
session.repo,
probe_environment,
["/usr/bin/python3", str(probe)],
)
if probe_result.returncode != 0:
raise HostGateError(probe_result.stderr + probe_result.stdout)
observed = _last_json(probe_result.stdout)
expected_write = context == "work"
if observed.get("ports", {}).get("1234") != "slice-1-ok":
raise HostGateError(f"{context}: selected LM Studio port was denied")
if observed.get("ports", {}).get("1235") == "slice-1-ok":
raise HostGateError(f"{context}: adjacent loopback port was allowed")
if observed.get("rawCredentials"):
raise HostGateError(f"{context}: raw credential names reached the sandbox")
if (
observed.get("otherTmpReadable")
or observed.get("otherStateReadable")
or observed.get("otherRepositoryReadable")
):
raise HostGateError(f"{context}: cross-context data was readable")
if observed.get("installedExtensionWritable"):
raise HostGateError(f"{context}: installed extension was writable")
if observed.get("unselectedConfigReadable"):
raise HostGateError(f"{context}: unselected configuration was readable")
if observed.get("workdirWritable") is not expected_write:
raise HostGateError(f"{context}: workdir access did not match the declaration")
rpc = _run(
nono,
profile_path,
session.repo,
launch_environment,
[
"/usr/bin/node",
str(PI_CLI),
"--mode",
"rpc",
"--no-session",
"--no-skills",
],
)
# EOF exits RPC after the process has initialized. A successful
# startup may produce no response because no command was sent.
if rpc.returncode != 0:
raise HostGateError(f"{context}: Pi startup failed: {rpc.stderr}")
requests_before_refresh = listener_requests[1234]
model_refresh = _run(
nono,
profile_path,
session.repo,
launch_environment,
[
"/usr/bin/node",
"--experimental-strip-types",
str(refresh_runner),
],
)
if model_refresh.returncode != 0:
raise HostGateError(
f"{context}: LM Studio model refresh failed: "
f"{model_refresh.stderr}{model_refresh.stdout}"
)
refreshed_models = _last_json(model_refresh.stdout)
if refreshed_models.get("modelIds") != ["slice-one-model"]:
raise HostGateError(f"{context}: LM Studio model response was not mapped")
if listener_requests[1234] != requests_before_refresh + 1:
raise HostGateError(
f"{context}: LM Studio refresh did not request the exact model endpoint"
)
outcomes[context] = observed
if _active_state_snapshot() != active_state_before:
raise HostGateError("Slice 1 verification mutated active Pi or Nono state")
tested_profiles = sorted(PROFILES.glob("pi-core-*.json")) + sorted(
PROFILES.glob("provider-*.json")
)
return {
"suite": "slice-1-host",
"status": "passed",
"nonoVersion": version.stdout.strip().split()[-1],
"contexts": sorted(outcomes),
"loopbackChecks": 4,
"isolationChecks": 12,
"piStartupChecks": 2,
"providerRequestChecks": 2,
"directLaunchDenialChecks": 1,
"staticProfileChecks": len(tested_profiles),
"compositionChecks": 3,
"commandPolicyChecks": 2,
"profileDigests": {
profile.name: hashlib.sha256(profile.read_bytes()).hexdigest()
for profile in tested_profiles
},
"lmStudioDigest": hashlib.sha256(managed_lm_studio_source).hexdigest(),
}
def main() -> int:
active_before: dict[str, tuple[str, str, int]] | None = None
try:
active_before = _active_state_snapshot()
result = run_host_gate()
if _active_state_snapshot() != active_before:
raise HostGateError("Slice 1 verification mutated active Pi or Nono state")
print(json.dumps(result, sort_keys=True))
except (HostGateError, OSError, subprocess.SubprocessError, ValueError) as error:
if active_before is not None:
try:
if _active_state_snapshot() != active_before:
error = HostGateError(
"Slice 1 verification mutated active Pi or Nono state after "
f"failing: {error}"
)
except OSError as snapshot_error:
error = HostGateError(
f"Could not verify active state after failure: {snapshot_error}; "
f"original failure: {error}"
)
print(json.dumps({"suite": "slice-1-host", "status": "failed", "error": str(error)}))
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+251
View File
@@ -0,0 +1,251 @@
"""Trusted Slice 1 policy composition contract used by capability tests."""
from __future__ import annotations
import copy
import hashlib
import json
import os
import re
import stat
from pathlib import Path
from typing import Any, Literal
PROVIDER_PROFILE_NAMES = {
"anthropic": "provider-anthropic",
"lm-studio": "provider-lm-studio",
"morph": "provider-morph",
"openai": "provider-openai",
}
PROVIDER_CREDENTIAL_DEFINITIONS = {
"anthropic": {
"upstream": "https://api.anthropic.com",
"credential_key": "ANTHROPIC_API_KEY",
"inject_header": "x-api-key",
"credential_format": "{}",
"env_var": "ANTHROPIC_API_KEY",
},
"morph": {
"upstream": "https://api.morphllm.com",
"credential_key": "MORPH_API_KEY",
"inject_header": "Authorization",
"credential_format": "Bearer {}",
"env_var": "MORPH_API_KEY",
},
"openai": {
"upstream": "https://api.openai.com/v1",
"credential_key": "OPENAI_API_KEY",
"inject_header": "Authorization",
"credential_format": "Bearer {}",
"env_var": "OPENAI_API_KEY",
},
}
RAW_CREDENTIAL_VARIABLES = frozenset(
{
"ANTHROPIC_API_KEY",
"GH_TOKEN",
"GITHUB_TOKEN",
"MORPH_API_KEY",
"OPENAI_API_KEY",
"SSH_AUTH_SOCK",
}
)
_LM_REGISTRATION = re.compile(r"registerProvider\(\s*[\"']lm-studio[\"']")
_ROOT = Path(__file__).resolve().parents[4]
_PROFILES = _ROOT / "profiles" / "nono"
_PACKAGE_LOCK = _ROOT / "config" / "pi-package-lock.json"
class CompositionError(ValueError):
"""A reviewed core/provider pair cannot produce one narrow launch policy."""
def load_profile(path: Path) -> dict[str, Any]:
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise CompositionError(f"profile is not an object: {path}")
return data
def compose_profile(
core: dict[str, Any],
provider: dict[str, Any],
*,
workdir_access: Literal["read", "readwrite"],
temporary_directory: Path,
lm_studio_extension: Path | None = None,
) -> dict[str, Any]:
"""Materialize one reviewed core and one provider into a launch profile.
This intentionally does not model Nono inheritance. Nono preserves a blocked
network across inherited parents, so the trusted generator must replace the
core's fail-closed network block with exactly one reviewed provider route.
"""
profile = copy.deepcopy(core)
core_name = _profile_name(core)
provider_name = _profile_name(provider)
if core_name not in {"pi-core-personal", "pi-core-work"}:
raise CompositionError(f"unapproved core profile: {core_name}")
if core != load_profile(_PROFILES / f"{core_name}.json"):
raise CompositionError(f"core profile differs from reviewed source: {core_name}")
expected_provider = PROVIDER_PROFILE_NAMES.get(
provider_name.removeprefix("provider-")
)
if expected_provider != provider_name:
raise CompositionError(f"unapproved provider profile: {provider_name}")
if provider != load_profile(_PROFILES / f"{provider_name}.json"):
raise CompositionError(
f"provider profile differs from reviewed source: {provider_name}"
)
_validate_provider_network(provider_name, provider.get("network"))
profile["extends"] = "default"
profile["meta"] = {
"name": f"generated-{core_name.removeprefix('pi-core-')}-{provider_name.removeprefix('provider-')}",
"description": "Generated Slice 1 composition fixture",
}
profile["workdir"] = {"access": workdir_access}
profile["network"] = copy.deepcopy(provider["network"])
environment = profile.get("environment")
if not isinstance(environment, dict):
raise CompositionError("core profile has no environment policy")
denied = set(environment.get("deny_vars", []))
if denied != RAW_CREDENTIAL_VARIABLES:
raise CompositionError("core raw-credential deny list is incomplete")
filesystem = profile.get("filesystem")
if not isinstance(filesystem, dict):
raise CompositionError("core profile has no filesystem policy")
if not temporary_directory.is_absolute():
raise CompositionError("launch temporary directory must be absolute")
try:
metadata = temporary_directory.lstat()
resolved_temporary_directory = temporary_directory.resolve(strict=True)
except OSError as error:
raise CompositionError("launch temporary directory must already exist") from error
if (
temporary_directory.is_symlink()
or not stat.S_ISDIR(metadata.st_mode)
or resolved_temporary_directory != temporary_directory
or metadata.st_uid != os.getuid()
or stat.S_IMODE(metadata.st_mode) & 0o077
or resolved_temporary_directory in {Path("/"), Path("/tmp"), Path.home()}
):
raise CompositionError(
"launch temporary directory must be one private owned directory"
)
mutable_directories = list(filesystem.get("allow", []))
if "$TMPDIR" in mutable_directories or "/tmp" in mutable_directories:
raise CompositionError("core profile contains a broad temporary grant")
mutable_directories.append(str(resolved_temporary_directory))
filesystem["allow"] = sorted(set(mutable_directories))
set_variables = dict(environment.get("set_vars", {}))
set_variables["TMPDIR"] = str(resolved_temporary_directory)
environment["set_vars"] = set_variables
if provider_name == "provider-lm-studio":
if lm_studio_extension is None:
raise CompositionError("LM Studio requires one installed extension path")
_validate_lm_studio_extension(lm_studio_extension)
reads = list(filesystem.get("read", []))
reads.append(str(lm_studio_extension))
filesystem["read"] = sorted(set(reads))
elif lm_studio_extension is not None:
raise CompositionError("non-LM providers cannot load the LM Studio extension")
return profile
def isolated_settings(
provider: str,
*,
lm_studio_extension: Path | None = None,
) -> dict[str, Any]:
if provider not in PROVIDER_PROFILE_NAMES:
raise CompositionError(f"unknown provider: {provider}")
extensions: list[str] = []
if provider == "lm-studio":
if lm_studio_extension is None:
raise CompositionError("LM Studio settings require one extension path")
_validate_lm_studio_extension(lm_studio_extension)
extensions.append(str(lm_studio_extension))
elif lm_studio_extension is not None:
raise CompositionError("LM Studio extension is forbidden for another provider")
return {
"defaultProjectTrust": "never",
"defaultTools": ["read", "edit", "write"],
"enableInstallTelemetry": False,
"extensions": extensions,
"packages": [],
"quietStartup": True,
}
def assert_single_lm_studio_registration(sources: list[Path]) -> None:
registrations: list[tuple[Path, int]] = []
for source in sources:
contents = source.read_text(encoding="utf-8")
if "http://localhost:1234" in contents:
raise CompositionError(f"legacy localhost LM Studio URL remains: {source}")
registrations.extend(
(source, match.start()) for match in _LM_REGISTRATION.finditer(contents)
)
if len(registrations) != 1:
raise CompositionError(
"expected exactly one LM Studio provider registration; found "
+ str(len(registrations))
)
def _validate_lm_studio_extension(path: Path) -> None:
if not path.is_absolute() or path.is_symlink() or not path.is_file():
raise CompositionError("LM Studio extension must be one absolute regular file")
lock = json.loads(_PACKAGE_LOCK.read_text(encoding="utf-8"))
entries = [
entry for entry in lock.get("extensions", []) if entry.get("name") == "lm-studio"
]
if len(entries) != 1:
raise CompositionError("package lock must contain one LM Studio entry")
expected_digest = entries[0].get("reviewed", {}).get("sha256")
observed_digest = hashlib.sha256(path.read_bytes()).hexdigest()
if expected_digest != observed_digest:
raise CompositionError("LM Studio extension differs from reviewed lock source")
def _profile_name(profile: dict[str, Any]) -> str:
meta = profile.get("meta")
if not isinstance(meta, dict) or not isinstance(meta.get("name"), str):
raise CompositionError("profile has no name")
return meta["name"]
def _validate_provider_network(name: str, network: Any) -> None:
if not isinstance(network, dict) or network.get("block") is not False:
raise CompositionError(f"provider route is not enabled: {name}")
credentials = network.get("credentials", [])
connect_ports = network.get("connect_port", [])
if name == "provider-lm-studio":
if (
set(network) != {"block", "connect_port"}
or credentials
or connect_ports != [1234]
):
raise CompositionError("LM Studio must grant only connect port 1234")
return
expected_credential = name.removeprefix("provider-")
if set(network) != {
"block",
"allow_domain",
"credentials",
"custom_credentials",
}:
raise CompositionError(f"provider network contains an extra route: {name}")
if network["allow_domain"] or credentials != [expected_credential] or connect_ports:
raise CompositionError(f"provider must activate only its own route: {name}")
definitions = network.get("custom_credentials")
if definitions != {
expected_credential: PROVIDER_CREDENTIAL_DEFINITIONS[expected_credential]
}:
raise CompositionError(f"provider credential definition mismatch: {name}")
+214
View File
@@ -0,0 +1,214 @@
from __future__ import annotations
import copy
import hashlib
import json
import tempfile
import unittest
from pathlib import Path
from .policy import (
CompositionError,
PROVIDER_CREDENTIAL_DEFINITIONS,
PROVIDER_PROFILE_NAMES,
RAW_CREDENTIAL_VARIABLES,
assert_single_lm_studio_registration,
compose_profile,
isolated_settings,
load_profile,
)
ROOT = Path(__file__).resolve().parents[4]
PROFILES = ROOT / "profiles" / "nono"
LM_STUDIO = ROOT / "extensions" / "lm-studio" / "index.ts"
PI_INSTALL = "/home/jb/.bun/install/global/node_modules/@earendil-works/pi-coding-agent"
JITI_INSTALL = "/home/jb/.bun/install/global/node_modules/jiti"
GLOBAL_INSTRUCTIONS = "/home/jb/.pi/agent/AGENTS.md"
class PolicyContractTests(unittest.TestCase):
def test_cores_are_fail_closed_and_separate_state(self) -> None:
for context in ("personal", "work"):
with self.subTest(context=context):
profile = load_profile(PROFILES / f"pi-core-{context}.json")
self.assertEqual(profile["extends"], "default")
self.assertEqual(profile["workdir"], {"access": "none"})
self.assertEqual(profile["network"], {"block": True})
self.assertEqual(
set(profile["environment"]["deny_vars"]),
RAW_CREDENTIAL_VARIABLES,
)
self.assertNotIn("SSH_AUTH_SOCK", profile["environment"]["allow_vars"])
self.assertIn("system_write_linux", profile["groups"]["exclude"])
self.assertIn("user_tools", profile["groups"]["exclude"])
grants = json.dumps(profile["filesystem"], sort_keys=True)
self.assertIn(f"pi-sbx/{context}", grants)
other = "work" if context == "personal" else "personal"
self.assertNotIn(f"pi-sbx/{other}", grants)
self.assertNotIn("$WORKDIR", grants)
self.assertNotIn('"/tmp"', grants)
self.assertNotIn("$TMPDIR", grants)
self.assertEqual(
set(profile["filesystem"]["allow"]),
{
f"$XDG_CACHE_HOME/pi-sbx/{context}",
f"$XDG_STATE_HOME/pi-sbx/{context}/agent",
f"$XDG_STATE_HOME/pi-sbx/{context}/sessions",
},
)
self.assertEqual(
profile["filesystem"]["read"],
[PI_INSTALL, JITI_INSTALL],
)
self.assertEqual(
set(profile["filesystem"]["read_file"]),
{GLOBAL_INSTRUCTIONS, "/usr/bin/node"},
)
self.assertEqual(
set(profile["filesystem"]["allow_file"]),
{"/dev/null", "/dev/stderr", "/dev/stdout", "/dev/tty"},
)
for capability in ("write", "write_file", "unix_socket"):
self.assertEqual(profile["filesystem"].get(capability, []), [])
def test_provider_profiles_select_one_route(self) -> None:
for provider, profile_name in PROVIDER_PROFILE_NAMES.items():
with self.subTest(provider=provider):
profile = load_profile(PROFILES / f"{profile_name}.json")
network = profile["network"]
self.assertFalse(network["block"])
if provider == "lm-studio":
self.assertEqual(set(network), {"block", "connect_port"})
self.assertEqual(network["connect_port"], [1234])
self.assertNotIn("credentials", network)
else:
self.assertEqual(
set(network),
{"block", "allow_domain", "credentials", "custom_credentials"},
)
self.assertEqual(network["allow_domain"], [])
self.assertEqual(network["credentials"], [provider])
self.assertEqual(
network["custom_credentials"],
{provider: PROVIDER_CREDENTIAL_DEFINITIONS[provider]},
)
self.assertNotIn("connect_port", network)
self.assertEqual(profile["workdir"], {"access": "none"})
self.assertIn("system_read_linux_core", profile["groups"]["exclude"])
self.assertIn("system_read_macos", profile["groups"]["exclude"])
self.assertTrue(profile["environment"]["allow_vars"])
self.assertEqual(
set(profile["environment"]["deny_vars"]),
RAW_CREDENTIAL_VARIABLES,
)
self.assertEqual(profile["open_urls"]["allow_origins"], [])
self.assertFalse(profile["allow_launch_services"])
def test_trusted_composition_replaces_block_with_one_provider(self) -> None:
core = load_profile(PROFILES / "pi-core-personal.json")
provider = load_profile(PROFILES / "provider-lm-studio.json")
with tempfile.TemporaryDirectory() as directory:
temporary_directory = Path(directory)
generated = compose_profile(
core,
provider,
workdir_access="read",
temporary_directory=temporary_directory,
lm_studio_extension=LM_STUDIO,
)
self.assertEqual(generated["workdir"], {"access": "read"})
self.assertIn(str(temporary_directory), generated["filesystem"]["allow"])
self.assertEqual(
generated["environment"]["set_vars"]["TMPDIR"],
str(temporary_directory),
)
self.assertEqual(generated["network"]["connect_port"], [1234])
self.assertFalse(generated["network"]["block"])
self.assertNotIn("command_policies", generated)
self.assertIn(str(LM_STUDIO), generated["filesystem"]["read"])
with self.assertRaisesRegex(CompositionError, "cannot load"):
compose_profile(
core,
load_profile(PROFILES / "provider-openai.json"),
workdir_access="read",
temporary_directory=temporary_directory,
lm_studio_extension=LM_STUDIO,
)
with self.assertRaisesRegex(CompositionError, "private owned"):
compose_profile(
core,
provider,
workdir_access="read",
temporary_directory=Path("/tmp"),
lm_studio_extension=LM_STUDIO,
)
mutated_core = copy.deepcopy(core)
mutated_core["filesystem"]["allow"].append("/tmp")
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(CompositionError, "differs from reviewed"):
compose_profile(
mutated_core,
provider,
workdir_access="read",
temporary_directory=Path(directory),
lm_studio_extension=LM_STUDIO,
)
def test_generated_settings_have_no_privileged_resources(self) -> None:
normal = isolated_settings("openai")
self.assertEqual(normal["packages"], [])
self.assertEqual(normal["extensions"], [])
self.assertEqual(normal["defaultTools"], ["read", "edit", "write"])
self.assertNotIn("bash", normal["defaultTools"])
self.assertEqual(normal["defaultProjectTrust"], "never")
local = isolated_settings("lm-studio", lm_studio_extension=LM_STUDIO)
self.assertEqual(local["extensions"], [str(LM_STUDIO)])
with tempfile.TemporaryDirectory() as directory:
unreviewed = Path(directory) / "lm-studio.ts"
unreviewed.write_text("unreviewed\n", encoding="utf-8")
with self.assertRaisesRegex(CompositionError, "reviewed lock source"):
isolated_settings("lm-studio", lm_studio_extension=unreviewed)
serialized = json.dumps(local).lower()
for forbidden in ("pi-workflow", "update", "background", "notify"):
self.assertNotIn(forbidden, serialized)
def test_lm_studio_registration_is_unique_and_uses_exact_ip(self) -> None:
lock = json.loads((ROOT / "config" / "pi-package-lock.json").read_text(encoding="utf-8"))
entry = next(item for item in lock["extensions"] if item["name"] == "lm-studio")
self.assertEqual(entry["source"], "extensions/lm-studio/index.ts")
self.assertEqual(entry["status"], "blocked")
self.assertEqual(
entry["reviewed"]["sha256"],
hashlib.sha256(LM_STUDIO.read_bytes()).hexdigest(),
)
assert_single_lm_studio_registration([LM_STUDIO])
contents = LM_STUDIO.read_text(encoding="utf-8")
self.assertIn('"http://127.0.0.1:1234/v1"', contents)
self.assertNotIn("http://localhost:1234", contents)
with tempfile.TemporaryDirectory() as directory:
duplicate = Path(directory) / "duplicate.ts"
duplicate.write_text(
'pi.registerProvider("lm-studio", {});\n',
encoding="utf-8",
)
with self.assertRaisesRegex(CompositionError, "exactly one"):
assert_single_lm_studio_registration([LM_STUDIO, duplicate])
duplicate.write_text(
'pi.registerProvider("lm-studio", {});\n'
'pi.registerProvider("lm-studio", {});\n',
encoding="utf-8",
)
with self.assertRaisesRegex(CompositionError, "found 2"):
assert_single_lm_studio_registration([duplicate])
duplicate.write_text(
'const baseUrl = "http://localhost:1234/v1";\n'
'pi.registerProvider("lm-studio", {});\n',
encoding="utf-8",
)
with self.assertRaisesRegex(CompositionError, "legacy localhost"):
assert_single_lm_studio_registration([duplicate])
if __name__ == "__main__":
unittest.main()