diff --git a/README.md b/README.md index 73e6b06..8a6e549 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,10 @@ SourceOS may carry: - fallback references; - ReleaseSet and BootReleaseSet bindings; - evidence collectors; -- workstation integration hints. +- workstation integration hints; +- Local Model Door profiles for laptop-safe local routing and assist models. -SourceOS must not become the authority for model promotion or model lifecycle state. +SourceOS must not become the authority for model promotion, model lifecycle state, or personal tuning authorization. ## Product goal @@ -31,7 +32,8 @@ That means: - clear per-service trust and provenance; - user, project, and agent workspace separation; - signed service references rather than unmanaged local artifact replacement; -- clean integration with SourceOS shell, boot, and profile systems. +- clean integration with SourceOS shell, boot, and profile systems; +- local model profiles that can serve routing, triage, summarization, rewriting, Office Plane assist, Agent Machine assist, and offline fallback without sending prompts off-device by default. ## Repository scope @@ -43,3 +45,46 @@ This repository contains: - `tools/` lightweight validation helpers. Runtime service implementation belongs in SocioProphet platform service repositories. Lab execution belongs in SociOS Linux lab repositories. + +## Local Model Door + +The Local Model Door is the carry-layer profile family for laptop-safe local models. + +Current examples: + +```text +examples/local-model-profile.llama32-1b.json +examples/local-model-profile.llama32-3b.json +``` + +The default laptop-safe profile is `llama3.2:1b` through Ollama. The quality fallback profile is `llama3.2:3b`. + +These profiles are local-only by default. They do not authorize network access, tool use, prompt egress, or automatic model download. Pulling/installing model weights remains explicit. + +See: + +```text +docs/local-model-door.md +contracts/local-model-profile.schema.json +``` + +## Personal tuning boundary + +Per-user tuning is mandatory as a product direction, but it is not authorized by this carry profile alone. + +The correct split is: + +| Layer | Responsibility | +|---|---| +| `SourceOS-Linux/sourceos-model-carry` | Carry local model profiles, local service references, and evidence expectations. | +| `SociOS-Linux/socios` | Opt-in orchestration for per-user tuning jobs and data preparation. | +| `SocioProphet/model-governance-ledger` | Personal tuning contracts, consent receipts, dataset lineage, evaluation receipts, promotion decisions, and revocation. | +| `SocioProphet/model-router` | Route between base local model, personally tuned adapter/model, and hosted fallbacks under policy. | + +No profile in this repository grants permission to train on user data. Personal tuning requires a separate consented `PersonalTuningContract` and evidence ledger. + +## Validation + +```bash +python3 tools/validate_local_model_profiles.py +``` diff --git a/contracts/local-model-profile.schema.json b/contracts/local-model-profile.schema.json new file mode 100644 index 0000000..22c356d --- /dev/null +++ b/contracts/local-model-profile.schema.json @@ -0,0 +1,97 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/model-carry/local-model-profile.schema.json", + "title": "SourceOS Local Model Profile", + "description": "A carry-layer profile for laptop-safe local model services used for routing, summarization, office assist, and fast privacy-preserving fallback behavior.", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "kind", + "profileId", + "displayName", + "runtime", + "model", + "roles", + "policy" + ], + "properties": { + "schemaVersion": { "const": "v0.1" }, + "kind": { "const": "LocalModelProfile" }, + "profileId": { "type": "string", "pattern": "^urn:srcos:model-profile:" }, + "displayName": { "type": "string", "minLength": 1 }, + "runtime": { + "type": "object", + "additionalProperties": false, + "required": ["kind"], + "properties": { + "kind": { "type": "string", "enum": ["ollama", "llama.cpp", "mlx", "openai-compatible", "other"] }, + "endpoint": { "type": ["string", "null"] }, + "defaultLocalPort": { "type": ["integer", "null"], "minimum": 1, "maximum": 65535 }, + "launchCommand": { "type": ["string", "null"] }, + "healthCommand": { "type": ["string", "null"] } + } + }, + "model": { + "type": "object", + "additionalProperties": false, + "required": ["provider", "family", "name", "parameterClass", "licenseRef"], + "properties": { + "provider": { "type": "string" }, + "family": { "type": "string" }, + "name": { "type": "string" }, + "parameterClass": { "type": "string", "enum": ["sub-1b", "1b", "3b", "4b", "8b", "other"] }, + "ollamaRef": { "type": ["string", "null"] }, + "ggufRef": { "type": ["string", "null"] }, + "contextWindowHint": { "type": ["integer", "null"], "minimum": 1 }, + "diskSizeHintMb": { "type": ["integer", "null"], "minimum": 1 }, + "memoryHintMb": { "type": ["integer", "null"], "minimum": 1 }, + "licenseRef": { "type": "string" }, + "sourceRef": { "type": ["string", "null"] } + } + }, + "roles": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "router", + "triage", + "summarization", + "rewrite", + "office-assist", + "agent-machine-assist", + "offline-fallback", + "coding-assist", + "privacy-first-chat" + ] + } + }, + "policy": { + "type": "object", + "additionalProperties": false, + "properties": { + "localOnlyDefault": { "type": "boolean", "default": true }, + "sendPromptOffDeviceDefault": { "type": "boolean", "default": false }, + "allowToolUse": { "type": "boolean", "default": false }, + "allowNetwork": { "type": "boolean", "default": false }, + "allowModelDownload": { "type": "boolean", "default": false }, + "requiresExplicitPull": { "type": "boolean", "default": true }, + "maxPromptChars": { "type": ["integer", "null"], "minimum": 1 }, + "fallbackProfileRefs": { "type": "array", "items": { "type": "string" } } + } + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "properties": { + "emitModelAvailability": { "type": "boolean", "default": true }, + "emitRuntimeHealth": { "type": "boolean", "default": true }, + "emitRoutingDecision": { "type": "boolean", "default": true }, + "emitPromptHashOnly": { "type": "boolean", "default": true } + } + }, + "notes": { "type": "string" } + } +} diff --git a/docs/local-model-door.md b/docs/local-model-door.md new file mode 100644 index 0000000..eaea87f --- /dev/null +++ b/docs/local-model-door.md @@ -0,0 +1,93 @@ +# Local Model Door + +The Local Model Door is the SourceOS carry-layer profile for small laptop-safe models used for local routing, triage, summarization, rewriting, Office Plane assist, Agent Machine assist, and offline fallback. + +It is not the model lifecycle authority. SourceOS carries service references, launch profiles, cache policy, and evidence expectations. `SocioProphet/model-router` remains the policy-aware routing layer. + +## Default profile posture + +The default laptop-safe profile is: + +```text +urn:srcos:model-profile:local-llama32-1b +``` + +It targets `llama3.2:1b` through Ollama as a low-memory local router/triage/summarization model. + +The higher-quality local fallback is: + +```text +urn:srcos:model-profile:local-llama32-3b +``` + +It targets `llama3.2:3b` for machines that can afford the larger local model. + +## Why Llama 3.2 first + +The profile uses the Llama 3.2 1B/3B family because it is small enough for laptop use, widely available through local runtimes, and adequate for routing, triage, summarization, rewriting, and low-risk local assist tasks. + +This does not hard-code SourceOS to Llama. The schema supports other local model families and runtimes. + +## Runtime posture + +Initial runtime target: + +```text +ollama at http://127.0.0.1:11434 +``` + +Future compatible runtimes: + +- `llama.cpp` +- `mlx` +- OpenAI-compatible local servers +- SourceOS-native model service adapters + +## Security posture + +Default profile policy: + +- local-only by default; +- do not send prompts off-device by default; +- do not authorize tool use by default; +- do not authorize network access by default; +- do not download model weights automatically; +- explicit pull/install step required; +- evidence should record availability, runtime health, routing decision, and prompt hash only. + +## Intended uses + +Good default local uses: + +- route between local/hosted model options; +- triage whether a request needs a larger model; +- summarize local files already exposed through Agent Machine or Office Door policy; +- rewrite local drafts; +- assist Office Plane artifact generation; +- provide offline fallback chat; +- provide quick local command-palette responses. + +Non-goals: + +- autonomous tool execution; +- unsandboxed shell access; +- external web access; +- hidden model download; +- replacing model governance, model promotion, or hosted high-quality model paths. + +## Integration path + +| Repo | Role | +|---|---| +| `SourceOS-Linux/sourceos-model-carry` | Local model profile contracts and examples. | +| `SocioProphet/model-router` | Policy-aware local-vs-hosted routing. | +| `SourceOS-Linux/sourceos-devtools` | `sourceosctl local-model ...` detection, plan, and evidence. | +| `SourceOS-Linux/agent-term` | Operator events for local model availability and routing requests. | +| `SocioProphet/agentplane` | Evidence artifacts when local model routing/assist participates in governed runs. | + +## Current examples + +```text +examples/local-model-profile.llama32-1b.json +examples/local-model-profile.llama32-3b.json +``` diff --git a/examples/local-model-profile.llama32-1b.json b/examples/local-model-profile.llama32-1b.json new file mode 100644 index 0000000..34ff37a --- /dev/null +++ b/examples/local-model-profile.llama32-1b.json @@ -0,0 +1,55 @@ +{ + "schemaVersion": "v0.1", + "kind": "LocalModelProfile", + "profileId": "urn:srcos:model-profile:local-llama32-1b", + "displayName": "Local Llama 3.2 1B Router", + "runtime": { + "kind": "ollama", + "endpoint": "http://127.0.0.1:11434", + "defaultLocalPort": 11434, + "launchCommand": "ollama run llama3.2:1b", + "healthCommand": "ollama list" + }, + "model": { + "provider": "Meta", + "family": "Llama 3.2", + "name": "llama3.2:1b", + "parameterClass": "1b", + "ollamaRef": "llama3.2:1b", + "ggufRef": null, + "contextWindowHint": 128000, + "diskSizeHintMb": 1332, + "memoryHintMb": 4096, + "licenseRef": "https://www.llama.com/llama3_2/license/", + "sourceRef": "https://ollama.com/library/llama3.2" + }, + "roles": [ + "router", + "triage", + "summarization", + "rewrite", + "office-assist", + "agent-machine-assist", + "offline-fallback", + "privacy-first-chat" + ], + "policy": { + "localOnlyDefault": true, + "sendPromptOffDeviceDefault": false, + "allowToolUse": false, + "allowNetwork": false, + "allowModelDownload": false, + "requiresExplicitPull": true, + "maxPromptChars": 12000, + "fallbackProfileRefs": [ + "urn:srcos:model-profile:local-llama32-3b" + ] + }, + "evidence": { + "emitModelAvailability": true, + "emitRuntimeHealth": true, + "emitRoutingDecision": true, + "emitPromptHashOnly": true + }, + "notes": "Default laptop-safe local profile for routing, triage, summarization, rewriting, and Office Plane assist. Model download remains explicit and is not performed by profile validation." +} diff --git a/examples/local-model-profile.llama32-3b.json b/examples/local-model-profile.llama32-3b.json new file mode 100644 index 0000000..931e6ca --- /dev/null +++ b/examples/local-model-profile.llama32-3b.json @@ -0,0 +1,54 @@ +{ + "schemaVersion": "v0.1", + "kind": "LocalModelProfile", + "profileId": "urn:srcos:model-profile:local-llama32-3b", + "displayName": "Local Llama 3.2 3B Quality Fallback", + "runtime": { + "kind": "ollama", + "endpoint": "http://127.0.0.1:11434", + "defaultLocalPort": 11434, + "launchCommand": "ollama run llama3.2:3b", + "healthCommand": "ollama list" + }, + "model": { + "provider": "Meta", + "family": "Llama 3.2", + "name": "llama3.2:3b", + "parameterClass": "3b", + "ollamaRef": "llama3.2:3b", + "ggufRef": null, + "contextWindowHint": 128000, + "diskSizeHintMb": 2048, + "memoryHintMb": 8192, + "licenseRef": "https://www.llama.com/llama3_2/license/", + "sourceRef": "https://ollama.com/library/llama3.2" + }, + "roles": [ + "summarization", + "rewrite", + "office-assist", + "agent-machine-assist", + "offline-fallback", + "coding-assist", + "privacy-first-chat" + ], + "policy": { + "localOnlyDefault": true, + "sendPromptOffDeviceDefault": false, + "allowToolUse": false, + "allowNetwork": false, + "allowModelDownload": false, + "requiresExplicitPull": true, + "maxPromptChars": 24000, + "fallbackProfileRefs": [ + "urn:srcos:model-profile:local-llama32-1b" + ] + }, + "evidence": { + "emitModelAvailability": true, + "emitRuntimeHealth": true, + "emitRoutingDecision": true, + "emitPromptHashOnly": true + }, + "notes": "Quality fallback local model profile for machines that can afford a slightly larger model. Still local-only by default and does not authorize tool use or network access." +} diff --git a/tools/validate_local_model_profiles.py b/tools/validate_local_model_profiles.py new file mode 100644 index 0000000..1850580 --- /dev/null +++ b/tools/validate_local_model_profiles.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Validate SourceOS LocalModelProfile examples. + +This lightweight validator is intentionally stdlib-only so the carry-layer +contracts remain easy to run on developer workstations and CI bootstrap images. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +SCHEMA = ROOT / "contracts/local-model-profile.schema.json" +EXAMPLES = sorted((ROOT / "examples").glob("local-model-profile.*.json")) + + +class ValidationError(Exception): + pass + + +def load_json(path: Path) -> Any: + try: + return json.loads(path.read_text(encoding="utf-8")) + except FileNotFoundError as exc: + raise ValidationError(f"missing file: {path.relative_to(ROOT)}") from exc + except json.JSONDecodeError as exc: + raise ValidationError(f"invalid JSON in {path.relative_to(ROOT)}: {exc}") from exc + + +def require(condition: bool, message: str) -> None: + if not condition: + raise ValidationError(message) + + +def validate_profile(path: Path, profile: dict[str, Any]) -> None: + require(profile.get("schemaVersion") == "v0.1", f"{path}: schemaVersion must be v0.1") + require(profile.get("kind") == "LocalModelProfile", f"{path}: kind must be LocalModelProfile") + require(str(profile.get("profileId", "")).startswith("urn:srcos:model-profile:"), f"{path}: profileId must be a SourceOS model-profile URN") + + runtime = profile.get("runtime", {}) + require(runtime.get("kind") in {"ollama", "llama.cpp", "mlx", "openai-compatible", "other"}, f"{path}: unsupported runtime.kind") + + model = profile.get("model", {}) + require(model.get("name"), f"{path}: model.name is required") + require(model.get("parameterClass") in {"sub-1b", "1b", "3b", "4b", "8b", "other"}, f"{path}: invalid parameterClass") + require(model.get("licenseRef"), f"{path}: licenseRef is required") + + roles = profile.get("roles", []) + require(isinstance(roles, list) and roles, f"{path}: roles must be a non-empty list") + + policy = profile.get("policy", {}) + require(policy.get("localOnlyDefault") is True, f"{path}: localOnlyDefault must default true") + require(policy.get("sendPromptOffDeviceDefault") is False, f"{path}: prompts must not leave device by default") + require(policy.get("allowToolUse") is False, f"{path}: local model profiles must not grant tool use by default") + require(policy.get("allowNetwork") is False, f"{path}: local model profiles must not grant network by default") + require(policy.get("requiresExplicitPull") is True, f"{path}: model pull/install must be explicit") + + +def main() -> int: + load_json(SCHEMA) + if not EXAMPLES: + print("ERR: no local model profile examples found", file=sys.stderr) + return 2 + + try: + for example in EXAMPLES: + profile = load_json(example) + validate_profile(example.relative_to(ROOT), profile) + print(f"ok: {example.relative_to(ROOT)}") + except ValidationError as exc: + print(f"ERR: {exc}", file=sys.stderr) + return 1 + + print("Local model profile validation passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())