From 2aca63d50512e44c9ce43e63b5a42f6751a3bd0e Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 11:11:01 -0400 Subject: [PATCH 1/4] Add local model door CLI helpers --- sourceosctl/commands/local_model.py | 249 ++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 sourceosctl/commands/local_model.py diff --git a/sourceosctl/commands/local_model.py b/sourceosctl/commands/local_model.py new file mode 100644 index 0000000..ddd0efa --- /dev/null +++ b/sourceosctl/commands/local_model.py @@ -0,0 +1,249 @@ +"""Local Model Door helpers. + +This module probes local model runtime state and renders routing/profile plans. +It never pulls weights, starts daemons, sends prompts, or performs inference. +""" + +from __future__ import annotations + +import datetime as _dt +import hashlib +import json +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Any, Dict + + +DEFAULT_PROFILE_REF = "urn:srcos:model-profile:local-llama32-1b" +QUALITY_PROFILE_REF = "urn:srcos:model-profile:local-llama32-3b" +DEFAULT_ROUTER_BINDING_REF = "urn:socioprophet:model-router-binding:demo-user-local-llama32" + +LOCAL_MODEL_PROFILES = { + "local-llama32-1b": { + "profileRef": DEFAULT_PROFILE_REF, + "displayName": "Local Llama 3.2 1B Router", + "runtime": "ollama", + "model": "llama3.2:1b", + "parameterClass": "1b", + "roles": [ + "router", + "triage", + "summarization", + "rewrite", + "office-assist", + "agent-machine-assist", + "offline-fallback", + "privacy-first-chat", + ], + "policy": { + "localOnlyDefault": True, + "sendPromptOffDeviceDefault": False, + "allowToolUse": False, + "allowNetwork": False, + "requiresExplicitPull": True, + }, + }, + "local-llama32-3b": { + "profileRef": QUALITY_PROFILE_REF, + "displayName": "Local Llama 3.2 3B Quality Fallback", + "runtime": "ollama", + "model": "llama3.2:3b", + "parameterClass": "3b", + "roles": [ + "summarization", + "rewrite", + "office-assist", + "agent-machine-assist", + "offline-fallback", + "coding-assist", + "privacy-first-chat", + ], + "policy": { + "localOnlyDefault": True, + "sendPromptOffDeviceDefault": False, + "allowToolUse": False, + "allowNetwork": False, + "requiresExplicitPull": True, + }, + }, +} + + +def _print_json(payload: Dict[str, Any]) -> int: + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +def _ollama_path() -> str | None: + return shutil.which("ollama") + + +def _ollama_list() -> dict[str, Any]: + path = _ollama_path() + if not path: + return {"available": False, "path": None, "models": [], "error": "ollama not found on PATH"} + try: + completed = subprocess.run( + [path, "list"], + check=False, + capture_output=True, + text=True, + timeout=8, + ) + except Exception as exc: # pragma: no cover - defensive around host runtime + return {"available": True, "path": path, "models": [], "error": str(exc)} + + models: list[str] = [] + for line in completed.stdout.splitlines()[1:]: + parts = line.split() + if parts: + models.append(parts[0]) + return { + "available": True, + "path": path, + "returnCode": completed.returncode, + "models": models, + "error": completed.stderr.strip() or None, + } + + +def _profile(profile_name: str) -> dict[str, Any]: + try: + return LOCAL_MODEL_PROFILES[profile_name] + except KeyError: + known = ", ".join(sorted(LOCAL_MODEL_PROFILES)) + raise SystemExit(f"unknown local model profile: {profile_name}; known profiles: {known}") + + +def _prompt_hash(text: str | None) -> str | None: + if text is None: + return None + return "sha256:" + hashlib.sha256(text.encode("utf-8")).hexdigest() + + +def doctor(args) -> int: + """Inspect local model runtime availability without pulling or running models.""" + runtime = _ollama_list() + installed = set(runtime.get("models", [])) + profiles = [] + for profile in LOCAL_MODEL_PROFILES.values(): + model = profile["model"] + profiles.append( + { + "profileRef": profile["profileRef"], + "model": model, + "runtime": profile["runtime"], + "installed": model in installed, + "roles": profile["roles"], + } + ) + return _print_json( + { + "type": "LocalModelDoctor", + "capturedAt": _dt.datetime.now(_dt.timezone.utc).isoformat(), + "runtime": runtime, + "profiles": profiles, + "policy": { + "pullsModels": False, + "startsServices": False, + "runsInference": False, + "promptEgressDefault": "deny", + "promptEvidence": "hash-only", + }, + } + ) + + +def profiles(args) -> int: + """List built-in local model profile refs consumed from sourceos-model-carry.""" + return _print_json( + { + "type": "LocalModelProfiles", + "profiles": LOCAL_MODEL_PROFILES, + "sourceRepo": "SourceOS-Linux/sourceos-model-carry", + } + ) + + +def plan(args) -> int: + """Render a local model runtime plan without pulling or running models.""" + profile = _profile(args.profile) + runtime = _ollama_list() + installed = profile["model"] in set(runtime.get("models", [])) + return _print_json( + { + "type": "LocalModelPlan", + "profile": profile, + "runtime": runtime, + "installed": installed, + "wouldPull": False, + "wouldStartService": False, + "wouldRunInference": False, + "explicitInstallCommand": f"ollama pull {profile['model']}", + "explicitRunCommand": f"ollama run {profile['model']}", + "policy": profile["policy"], + } + ) + + +def route(args) -> int: + """Render a hash-only route decision for a task class.""" + runtime = _ollama_list() + installed = set(runtime.get("models", [])) + default = LOCAL_MODEL_PROFILES["local-llama32-1b"] + quality = LOCAL_MODEL_PROFILES["local-llama32-3b"] + has_default = default["model"] in installed + has_quality = quality["model"] in installed + + target = "base-local" if has_default else "quality-local" if has_quality else "hosted-policy-required" + if args.task_class in {"office-assist", "rewrite", "summarization"} and args.personalization_ref: + target = "personal-local-policy-checked" + + return _print_json( + { + "type": "LocalModelRouteDecision", + "capturedAt": _dt.datetime.now(_dt.timezone.utc).isoformat(), + "taskClass": args.task_class, + "target": target, + "defaultLocalProfileRef": DEFAULT_PROFILE_REF, + "qualityFallbackLocalProfileRef": QUALITY_PROFILE_REF, + "routerBindingRef": args.router_binding_ref, + "personalizationRef": args.personalization_ref, + "promptHash": _prompt_hash(args.prompt) if args.prompt else None, + "promptStored": False, + "runtimeAvailable": runtime.get("available", False), + "installedModels": sorted(installed), + "requiresPolicyForHostedFallback": target == "hosted-policy-required", + "evidence": { + "emitRouteDecision": True, + "emitRuntimeHealth": True, + "emitGovernanceRefs": True, + "promptHashOnly": True, + }, + } + ) + + +def evidence_inspect(args) -> int: + path = Path(args.path) + if not path.exists(): + print(f"error: evidence file not found: {path}", file=sys.stderr) + return 1 + try: + payload = json.loads(path.read_text()) + except json.JSONDecodeError as exc: + print(f"error: invalid JSON: {exc}", file=sys.stderr) + return 1 + return _print_json( + { + "path": str(path), + "type": payload.get("type"), + "taskClass": payload.get("taskClass"), + "target": payload.get("target"), + "promptStored": payload.get("promptStored"), + "promptHashPresent": bool(payload.get("promptHash")), + "routerBindingRef": payload.get("routerBindingRef"), + } + ) From 5658e230465914c4228a67789fa397b5d174e683 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 11:13:20 -0400 Subject: [PATCH 2/4] Wire Local Model Door commands into sourceosctl --- sourceosctl/cli.py | 79 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/sourceosctl/cli.py b/sourceosctl/cli.py index b346517..309c38f 100644 --- a/sourceosctl/cli.py +++ b/sourceosctl/cli.py @@ -12,6 +12,7 @@ fingerprint, ai, agents, + local_model, agent_machine, office, ) @@ -134,6 +135,84 @@ def build_parser() -> argparse.ArgumentParser: ) agents_sandbox_plan_p.set_defaults(func=agents.sandbox_plan) + # --- local-model --- + local_model_p = sub.add_parser("local-model", help="Local Model Door helpers") + local_model_sub = local_model_p.add_subparsers( + dest="local_model_command", metavar="" + ) + local_model_sub.required = True + + local_model_doctor_p = local_model_sub.add_parser( + "doctor", help="Inspect local model runtime availability without pulling or inference" + ) + local_model_doctor_p.set_defaults(func=local_model.doctor) + + local_model_profiles_p = local_model_sub.add_parser( + "profiles", help="List built-in local model profile references" + ) + local_model_profiles_p.set_defaults(func=local_model.profiles) + + local_model_plan_p = local_model_sub.add_parser( + "plan", help="Render a local model runtime plan without pulling weights" + ) + local_model_plan_p.add_argument( + "--profile", + default="local-llama32-1b", + choices=sorted(local_model.LOCAL_MODEL_PROFILES), + help="Local model profile key", + ) + local_model_plan_p.set_defaults(func=local_model.plan) + + local_model_route_p = local_model_sub.add_parser( + "route", help="Render a hash-only local model route decision" + ) + local_model_route_p.add_argument( + "--task-class", + required=True, + choices=[ + "router", + "triage", + "summarization", + "rewrite", + "office-assist", + "agent-machine-assist", + "offline-fallback", + "coding-assist", + "privacy-first-chat", + "complex-reasoning", + ], + help="Task class to route", + ) + local_model_route_p.add_argument( + "--prompt", + default=None, + help="Optional prompt text; only a SHA-256 hash is emitted", + ) + local_model_route_p.add_argument( + "--personalization-ref", + default=None, + help="Optional personal model/adaptation governance reference", + ) + local_model_route_p.add_argument( + "--router-binding-ref", + default=local_model.DEFAULT_ROUTER_BINDING_REF, + help="Model-router binding reference", + ) + local_model_route_p.set_defaults(func=local_model.route) + + local_model_evidence_p = local_model_sub.add_parser( + "evidence", help="Local model evidence helpers" + ) + local_model_evidence_sub = local_model_evidence_p.add_subparsers( + dest="local_model_evidence_command", metavar="" + ) + local_model_evidence_sub.required = True + local_model_evidence_inspect_p = local_model_evidence_sub.add_parser( + "inspect", help="Inspect local model route evidence JSON" + ) + local_model_evidence_inspect_p.add_argument("path", help="Path to local model evidence JSON") + local_model_evidence_inspect_p.set_defaults(func=local_model.evidence_inspect) + # --- agent-machine --- agent_machine_p = sub.add_parser("agent-machine", help="Agent Machine helpers") agent_machine_sub = agent_machine_p.add_subparsers( From 358d062db427443d914b6bdb82c98842e14468a0 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 11:15:02 -0400 Subject: [PATCH 3/4] Add Local Model Door CLI tests --- tests/test_local_model_cli.py | 80 +++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/test_local_model_cli.py diff --git a/tests/test_local_model_cli.py b/tests/test_local_model_cli.py new file mode 100644 index 0000000..12285af --- /dev/null +++ b/tests/test_local_model_cli.py @@ -0,0 +1,80 @@ +"""Unit tests for sourceosctl Local Model Door commands.""" + +import json +import os +import pathlib +import sys +import tempfile +import unittest +from unittest import mock + +_REPO_ROOT = pathlib.Path(__file__).parent.parent +sys.path.insert(0, str(_REPO_ROOT)) + +from sourceosctl.cli import main +from sourceosctl.commands import local_model + + +class TestLocalModelCommands(unittest.TestCase): + def test_profiles_direct(self): + self.assertEqual(local_model.profiles(mock.Mock()), 0) + + @mock.patch("sourceosctl.commands.local_model.shutil.which", return_value=None) + def test_doctor_without_ollama(self, _which): + self.assertEqual(main(["local-model", "doctor"]), 0) + + @mock.patch("sourceosctl.commands.local_model.shutil.which", return_value="/usr/bin/ollama") + @mock.patch("sourceosctl.commands.local_model.subprocess.run") + def test_doctor_with_ollama_models(self, run, _which): + run.return_value = mock.Mock( + returncode=0, + stdout="NAME ID SIZE MODIFIED\nllama3.2:1b abc 1.3 GB now\n", + stderr="", + ) + self.assertEqual(main(["local-model", "doctor"]), 0) + run.assert_called_once() + self.assertEqual(run.call_args.args[0], ["/usr/bin/ollama", "list"]) + + @mock.patch("sourceosctl.commands.local_model.shutil.which", return_value=None) + def test_plan_does_not_pull_model(self, _which): + self.assertEqual(main(["local-model", "plan", "--profile", "local-llama32-1b"]), 0) + + @mock.patch("sourceosctl.commands.local_model.shutil.which", return_value=None) + def test_route_hash_only_with_prompt(self, _which): + self.assertEqual( + main([ + "local-model", + "route", + "--task-class", + "office-assist", + "--prompt", + "sensitive prompt body should not be emitted", + "--personalization-ref", + "urn:socioprophet:personal-tuning-contract:demo-user-local-llama32", + ]), + 0, + ) + + def test_evidence_inspect_valid(self): + payload = { + "type": "LocalModelRouteDecision", + "taskClass": "office-assist", + "target": "personal-local-policy-checked", + "promptStored": False, + "promptHash": "sha256:example", + "routerBindingRef": "urn:socioprophet:model-router-binding:demo-user-local-llama32", + } + with tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False) as handle: + json.dump(payload, handle) + tmp_path = handle.name + try: + self.assertEqual(main(["local-model", "evidence", "inspect", tmp_path]), 0) + finally: + os.unlink(tmp_path) + + def test_evidence_inspect_missing(self): + self.assertEqual(main(["local-model", "evidence", "inspect", "/nonexistent/local-model.json"]), 1) + + +if __name__ == "__main__": + unittest.main() From 3fef9e6bfb66a5251a85588df63272b8552f50d8 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sat, 2 May 2026 11:16:18 -0400 Subject: [PATCH 4/4] Document Local Model Door CLI surface --- README.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f4998c..76f4656 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ It should contain: - model-router client utilities; - guardrail/eval/evidence helpers; - agent sandbox/run helpers; +- Local Model Door runtime detection and route planning helpers; - Agent Machine local mount and secure host-interface helpers; - Office Plane dry-run, guarded execution, inspection, and evidence helpers; - fingerprint and proof bundle tools; @@ -60,6 +61,11 @@ sourceosctl [--version] [] [options] | `sourceosctl fingerprint collect --dry-run` | Print environment fingerprint fields (dry-run only) | | `sourceosctl ai labs list` | List available AI labs (read-only) | | `sourceosctl agents sandbox plan --dry-run` | Print agent sandbox plan (dry-run only) | +| `sourceosctl local-model doctor` | Inspect local model runtime and installed models without pulling weights or inference | +| `sourceosctl local-model profiles` | List SourceOS Local Model Door profile refs | +| `sourceosctl local-model plan --profile local-llama32-1b` | Render local model runtime plan without installing or running models | +| `sourceosctl local-model route --task-class office-assist` | Render hash-only model route decision under local-first policy | +| `sourceosctl local-model evidence inspect ` | Inspect local model route evidence JSON | | `sourceosctl agent-machine mounts plan` | Render Agent Machine local mount plan for dev/docs/downloads roots (dry-run) | | `sourceosctl agent-machine mounts init --dry-run` | Render mount initialization plan; no directories or mounts are created | | `sourceosctl agent-machine mounts init --execute --policy-ok` | Create only scoped local output/download directories and emit AgentMachineMountEvidence | @@ -89,6 +95,10 @@ python3 bin/sourceosctl release inspect-archive fixtures/nlboot_release_valid python3 bin/sourceosctl fingerprint collect --dry-run python3 bin/sourceosctl ai labs list python3 bin/sourceosctl agents sandbox plan --dry-run +python3 bin/sourceosctl local-model doctor +python3 bin/sourceosctl local-model profiles +python3 bin/sourceosctl local-model plan --profile local-llama32-1b +python3 bin/sourceosctl local-model route --task-class office-assist --prompt "local prompt text is hashed only" python3 bin/sourceosctl agent-machine mounts plan python3 bin/sourceosctl agent-machine mounts init --dry-run python3 bin/sourceosctl agent-machine mounts init --execute --policy-ok --evidence-out ./mount-evidence.json @@ -104,6 +114,24 @@ python3 bin/sourceosctl office convert ./example.docx --to pdf --dry-run python3 bin/sourceosctl office convert ./example.docx --to pdf --execute --policy-ok --evidence-out ./office-convert-evidence.json ``` +### Local Model Door defaults + +The Local Model Door aligns with: + +- `SourceOS-Linux/sourceos-model-carry` for local model profiles; +- `SocioProphet/model-router` for routing; +- `SocioProphet/model-governance-ledger` for personal tuning contracts; +- `SociOS-Linux/socios` for opt-in personalization orchestration. + +Default profiles: + +| Profile key | Model | Role | +| --- | --- | --- | +| `local-llama32-1b` | `llama3.2:1b` | laptop-safe router, triage, summarization, rewrite, Office assist | +| `local-llama32-3b` | `llama3.2:3b` | quality local fallback | + +The Local Model Door does **not** pull model weights, start Ollama, run inference, send prompts off-device, or authorize tool use. `local-model route --prompt ...` emits only a SHA-256 prompt hash. + ### Agent Machine local mount defaults The first Agent Machine mount slice aligns with the SourceOS contracts in `SourceOS-Linux/sourceos-spec`: @@ -178,13 +206,15 @@ M1 is repo maturity and install surface definition: - `SociOS-Linux/nlboot`: boot/recovery client and evidence records. - `SourceOS-Linux/sourceos-spec`: canonical SourceOS schemas and contracts. - `SourceOS-Linux/sourceos-boot`: SourceOS boot/recovery integration. +- `SourceOS-Linux/sourceos-model-carry`: local model profiles and carry-layer service refs. - `SourceOS-Linux/agent-term`: terminal-native SourceOS operator ChatOps console. - `SociOS-Linux/workstation-contracts`: workstation/CI conformance contracts and IPC receipts. +- `SociOS-Linux/socios`: opt-in automation and personalization orchestration. - `SocioProphet/prophet-workspace`: workspace product semantics, Professional Workrooms, and OfficeArtifact contracts. - `SocioProphet/homebrew-prophet`: Homebrew install formulae. - `SocioProphet/model-router`: governed model/service routing. - `SocioProphet/guardrail-fabric`: guardrail policy client integration. -- `SocioProphet/model-governance-ledger`: evidence and promotion records. +- `SocioProphet/model-governance-ledger`: evidence, consent, evaluation, promotion, and personalization governance records. - `SocioProphet/agent-registry`: governed agent identity/tool-grant contracts. - `SocioProphet/agentplane`: governed execution, placement, run, replay, and evidence.