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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.PHONY: validate test scan-local-persistence validate-local-agents validate-local-agent-templates check-local-agent-drift validate-reasoning-cli
.PHONY: validate test scan-local-persistence validate-local-agents validate-local-agent-templates check-local-agent-drift validate-reasoning-cli validate-portable-ai validate-packaging

validate: test scan-local-persistence validate-local-agents validate-local-agent-templates check-local-agent-drift validate-reasoning-cli
validate: test scan-local-persistence validate-local-agents validate-local-agent-templates check-local-agent-drift validate-reasoning-cli validate-portable-ai validate-packaging
@test -f README.md
@test -f AGENTS.md
@test -f .github/copilot-instructions.md
Expand Down Expand Up @@ -29,3 +29,11 @@ validate-reasoning-cli:
@python3 bin/sourceosctl reasoning inspect tests/fixtures/reasoning/deterministic >/dev/null
@python3 bin/sourceosctl reasoning replay-plan tests/fixtures/reasoning/deterministic >/dev/null
@python3 bin/sourceosctl reasoning events tests/fixtures/reasoning/deterministic >/dev/null

validate-portable-ai:
@python3 bin/sourceosctl portable-ai profiles >/dev/null
@python3 bin/sourceosctl portable-ai preflight /tmp/SOURCEOS_AI --profile tiny-router >/dev/null
@python3 bin/sourceosctl portable-ai prepare /tmp/SOURCEOS_AI --profile tiny-router --dry-run >/dev/null

validate-packaging:
@python3 scripts/validate_packaging.py
14 changes: 14 additions & 0 deletions bin/sourceos-portable-ai
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
"""Standalone SourceOS Portable AI Kit CLI."""

from __future__ import annotations

import os
import sys

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from sourceosctl.commands.portable_ai_cli import main

if __name__ == "__main__":
sys.exit(main())
7 changes: 6 additions & 1 deletion bin/sourceosctl
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ if len(sys.argv) > 1 and sys.argv[1] == "native-assistant":

sys.exit(native_assistant_main(sys.argv[2:]))

if len(sys.argv) > 1 and sys.argv[1] == "portable-ai":
from sourceosctl.commands.portable_ai_cli import main as portable_ai_main

sys.exit(portable_ai_main(sys.argv[2:]))

if len(sys.argv) > 1 and sys.argv[1] == "local-agent":
from sourceosctl.commands.local_agent_registry_cli import main as local_agent_main

Expand All @@ -79,4 +84,4 @@ if len(sys.argv) > 2 and sys.argv[1:3] == ["doctor", "local-runtime"]:

from sourceosctl.cli import main

sys.exit(main())
sys.exit(main())
42 changes: 42 additions & 0 deletions docs/install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# SourceOS Devtools install and smoke guide

This guide covers the current Portable AI Kit scaffold in `sourceos-devtools`.

## Local source checkout

Run from the repository root:

```bash
python3 bin/sourceosctl portable-ai profiles
python3 bin/sourceosctl portable-ai preflight /tmp/SOURCEOS_AI --profile tiny-router
python3 bin/sourceosctl portable-ai prepare /tmp/SOURCEOS_AI --profile tiny-router --dry-run
python3 bin/sourceosctl portable-ai start-plan /tmp/SOURCEOS_AI --provider ollama-compatible --surface turtleterm
python3 bin/sourceosctl portable-ai stop-plan /tmp/SOURCEOS_AI --provider ollama-compatible
python3 bin/sourceosctl portable-ai byom verify /tmp/SOURCEOS_AI ./models/example.gguf --name example
```

The default posture is evidence-first and non-mutating. prompt egress is denied by default. Tool use is denied by default. Runtime start and stop commands emit plans; they do not launch or stop a provider.

## Homebrew scaffold

Before tap promotion, use the repository-local formula for syntax and smoke validation only:

```bash
brew install --HEAD https://raw.githubusercontent.com/SourceOS-Linux/sourceos-devtools/main/packaging/homebrew/Formula/sourceos-devtools.rb
```

After tap promotion:

```bash
brew install SourceOS-Linux/tap/sourceos-devtools
```

## Validation

```bash
python3 -m unittest discover -s tests -v
make validate
python3 scripts/validate_packaging.py
```

The packaging scaffold must not download model weights, call external providers, start local runtimes, store prompt bodies, or bypass Agent Machine / policy-gated runtime activation.
28 changes: 28 additions & 0 deletions packaging/homebrew/Formula/sourceos-devtools.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

class SourceosDevtools < Formula
desc "SourceOS developer and Portable AI Kit operator tools"
homepage "https://github.com/SourceOS-Linux/sourceos-devtools"
head "https://github.com/SourceOS-Linux/sourceos-devtools.git", branch: "main"

depends_on "python@3.12"

def install
libexec.install Dir["*"]
bin.write_exec_script libexec/"bin/sourceosctl"
bin.write_exec_script libexec/"bin/sourceos-portable-ai"
end

def caveats
<<~EOS
Portable AI Kit surfaces:
sourceosctl portable-ai profiles
sourceos-portable-ai profiles

Expected smoke marker:
PortableAIProfiles

This formula is a packaging scaffold. Runtime activation and policy gates remain in source repositories.
EOS
end
end
70 changes: 70 additions & 0 deletions scripts/validate_packaging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Validate sourceos-devtools packaging scaffolding."""

from __future__ import annotations

import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
FORMULA = ROOT / "packaging/homebrew/Formula/sourceos-devtools.rb"
INSTALL_DOC = ROOT / "docs/install.md"

REQUIRED_FORMULA_SNIPPETS = [
"class SourceosDevtools < Formula",
"SourceOS developer and Portable AI Kit operator tools",
"sourceosctl",
"sourceos-portable-ai",
"PortableAIProfiles",
]

FORBIDDEN_FORMULA_SNIPPETS = [
"ollama pull",
"ollama run",
"ollama serve",
"HUGGINGFACE",
"HF_TOKEN",
"OPENAI_API_KEY",
]

REQUIRED_DOC_SNIPPETS = [
"sourceosctl portable-ai profiles",
"portable-ai preflight",
"portable-ai prepare",
"portable-ai start-plan",
"portable-ai stop-plan",
"portable-ai byom verify",
"prompt egress is denied",
]


def fail(message: str) -> int:
print(f"ERR: {message}", file=sys.stderr)
return 1


def main() -> int:
if not FORMULA.exists():
return fail(f"missing {FORMULA.relative_to(ROOT)}")
if not INSTALL_DOC.exists():
return fail(f"missing {INSTALL_DOC.relative_to(ROOT)}")

formula = FORMULA.read_text(encoding="utf-8")
install_doc = INSTALL_DOC.read_text(encoding="utf-8")

for snippet in REQUIRED_FORMULA_SNIPPETS:
if snippet not in formula:
return fail(f"formula missing required snippet: {snippet}")
for snippet in FORBIDDEN_FORMULA_SNIPPETS:
if snippet in formula:
return fail(f"formula contains forbidden side-effect/secrets snippet: {snippet}")
for snippet in REQUIRED_DOC_SNIPPETS:
if snippet not in install_doc:
return fail(f"install doc missing required snippet: {snippet}")

print("Packaging validation passed")
return 0


if __name__ == "__main__":
raise SystemExit(main())
105 changes: 105 additions & 0 deletions sourceosctl/commands/portable_ai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from __future__ import annotations

import datetime as dt
import json
import os
import shutil
from pathlib import Path
from typing import Any

PORTABLE_LAYOUT_VERSION = "sourceos.portable-ai/v1alpha1"

PORTABLE_PROFILES: dict[str, dict[str, Any]] = {
"tiny-router": {"displayName": "Tiny Router Kit", "minimumFreeGb": 8, "recommendedFreeGb": 16},
"laptop-safe": {"displayName": "Laptop-safe Portable AI Kit", "minimumFreeGb": 16, "recommendedFreeGb": 32},
"office-local": {"displayName": "Office-local Portable AI Kit", "minimumFreeGb": 32, "recommendedFreeGb": 64},
"code-local": {"displayName": "Code-local Portable AI Kit", "minimumFreeGb": 32, "recommendedFreeGb": 64},
"field-kit": {"displayName": "Field Operator Portable AI Kit", "minimumFreeGb": 64, "recommendedFreeGb": 128},
"byom-gguf": {"displayName": "Bring-your-own GGUF Portable Kit", "minimumFreeGb": 8, "recommendedFreeGb": 64},
}

PORTABLE_DIRS = ["manifests", "models/blobs", "cache", "state/routes", "evidence/preflight", "evidence/materialization", "tmp"]


def _now() -> str:
return dt.datetime.now(dt.timezone.utc).isoformat()


def _print(payload: dict[str, Any]) -> int:
print(json.dumps(payload, indent=2, sort_keys=True))
return 0


def _root(value: str) -> Path:
return Path(value).expanduser().resolve()


def _disk(path: Path) -> dict[str, float | None]:
probe = path if path.exists() else path.parent
try:
total, used, free = shutil.disk_usage(probe)
except FileNotFoundError:
return {"totalGb": None, "usedGb": None, "freeGb": None}
gb = 1024 ** 3
return {"totalGb": round(total / gb, 2), "usedGb": round(used / gb, 2), "freeGb": round(free / gb, 2)}


def profiles(_args) -> int:
return _print({"type": "PortableAIProfiles", "apiVersion": PORTABLE_LAYOUT_VERSION, "profiles": PORTABLE_PROFILES})


def preflight(args) -> int:
target = _root(args.target_root)
profile_name = getattr(args, "profile", "laptop-safe")
profile = PORTABLE_PROFILES[profile_name]
disk = _disk(target)
parent = target if target.exists() else target.parent
writable = parent.exists() and os.access(parent, os.W_OK)
failures: list[str] = []
warnings: list[str] = []
free_gb = disk.get("freeGb")
if not writable:
failures.append("target parent is not writable")
if free_gb is not None and free_gb < profile["minimumFreeGb"]:
failures.append("free space below profile minimum")
elif free_gb is not None and free_gb < profile["recommendedFreeGb"]:
warnings.append("free space below profile recommendation")
return _print({
"type": "PortablePreflightEvidence",
"apiVersion": PORTABLE_LAYOUT_VERSION,
"capturedAt": _now(),
"targetRoot": str(target),
"profile": profile_name,
"disk": disk,
"writable": writable,
"failures": failures,
"warnings": warnings,
"decision": "fail" if failures else "warn" if warnings else "pass",
})


def prepare(args) -> int:
target = _root(args.target_root)
profile_name = args.profile
return _print({
"type": "PortablePreparePlan",
"apiVersion": PORTABLE_LAYOUT_VERSION,
"capturedAt": _now(),
"targetRoot": str(target),
"profile": profile_name,
"wouldCreateDirectories": [str(target / rel) for rel in PORTABLE_DIRS],
"wouldWriteManifest": str(target / "manifests" / "portable-ai-root.json"),
"wouldStartProvider": False,
"wouldFetchRemoteModels": False,
})


def inspect(args) -> int:
target = _root(args.target_root)
return _print({"type": "PortableAIInspect", "apiVersion": PORTABLE_LAYOUT_VERSION, "targetRoot": str(target), "exists": target.exists()})


def evidence_inspect(args) -> int:
path = Path(args.path).expanduser()
payload = json.loads(path.read_text(encoding="utf-8"))
return _print({"path": str(path), "type": payload.get("type"), "apiVersion": payload.get("apiVersion")})
44 changes: 44 additions & 0 deletions sourceosctl/commands/portable_ai_byom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import annotations

import hashlib
import json
from pathlib import Path
from typing import Any


def _print(payload: dict[str, Any]) -> int:
print(json.dumps(payload, indent=2, sort_keys=True))
return 0


def verify(args) -> int:
model_file = Path(args.model_file).expanduser().resolve()
target_root = Path(args.target_root).expanduser().resolve()
if not model_file.exists() or not model_file.is_file():
raise SystemExit(f"model file not found: {model_file}")
digest = hashlib.sha256()
with model_file.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
slug = args.name or model_file.stem
payload = {
"type": "PortableAIByomVerification",
"targetRoot": str(target_root),
"modelFile": str(model_file),
"name": slug,
"packId": args.pack_id or f"urn:srcos:model-carry-pack:byom-{slug}",
"displayName": args.display_name or slug,
"sha256": digest.hexdigest(),
"sizeBytes": model_file.stat().st_size,
"licenseRef": args.license_ref,
"sourceNote": args.source_note,
"taskClasses": args.task_class or ["operator-selected"],
"wouldCopy": bool(args.copy),
"wouldWriteManifest": bool(args.execute),
"routeEligibleBeforeReview": False,
"promptEgressDefault": "deny",
"toolUseDefault": "deny",
}
if args.execute and not args.policy_ok:
raise SystemExit("--execute requires --policy-ok")
return _print(payload)
Loading
Loading