Skip to content

Commit 32a6e98

Browse files
authored
Replay Portable AI Kit scaffold on current main
Replays the Portable AI Kit scaffold from draft #16 onto current main without overwriting newer command routers. Changes: - add portable-ai route to current bin/sourceosctl - add minimal Portable AI command parser and helper modules - add standalone sourceos-portable-ai entrypoint - add repo-local Homebrew formula scaffold - add packaging validator and install/smoke guide - wire Portable AI and packaging validation into current Makefile Boundary: reduced current-main scaffold only; preserves existing router state and avoids direct merge of the stale 58-behind draft branch. Validation observed on head a7fd8d2: - sourceos-devtools validate: success - operation-conformance: success Supersedes draft #16.
1 parent df351e7 commit 32a6e98

10 files changed

Lines changed: 463 additions & 3 deletions

File tree

Makefile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
.PHONY: validate test scan-local-persistence validate-local-agents validate-local-agent-templates check-local-agent-drift validate-reasoning-cli
1+
.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
22

3-
validate: test scan-local-persistence validate-local-agents validate-local-agent-templates check-local-agent-drift validate-reasoning-cli
3+
validate: test scan-local-persistence validate-local-agents validate-local-agent-templates check-local-agent-drift validate-reasoning-cli validate-portable-ai validate-packaging
44
@test -f README.md
55
@test -f AGENTS.md
66
@test -f .github/copilot-instructions.md
@@ -29,3 +29,11 @@ validate-reasoning-cli:
2929
@python3 bin/sourceosctl reasoning inspect tests/fixtures/reasoning/deterministic >/dev/null
3030
@python3 bin/sourceosctl reasoning replay-plan tests/fixtures/reasoning/deterministic >/dev/null
3131
@python3 bin/sourceosctl reasoning events tests/fixtures/reasoning/deterministic >/dev/null
32+
33+
validate-portable-ai:
34+
@python3 bin/sourceosctl portable-ai profiles >/dev/null
35+
@python3 bin/sourceosctl portable-ai preflight /tmp/SOURCEOS_AI --profile tiny-router >/dev/null
36+
@python3 bin/sourceosctl portable-ai prepare /tmp/SOURCEOS_AI --profile tiny-router --dry-run >/dev/null
37+
38+
validate-packaging:
39+
@python3 scripts/validate_packaging.py

bin/sourceos-portable-ai

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env python3
2+
"""Standalone SourceOS Portable AI Kit CLI."""
3+
4+
from __future__ import annotations
5+
6+
import os
7+
import sys
8+
9+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
10+
11+
from sourceosctl.commands.portable_ai_cli import main
12+
13+
if __name__ == "__main__":
14+
sys.exit(main())

bin/sourceosctl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ if len(sys.argv) > 1 and sys.argv[1] == "native-assistant":
6464

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

67+
if len(sys.argv) > 1 and sys.argv[1] == "portable-ai":
68+
from sourceosctl.commands.portable_ai_cli import main as portable_ai_main
69+
70+
sys.exit(portable_ai_main(sys.argv[2:]))
71+
6772
if len(sys.argv) > 1 and sys.argv[1] == "local-agent":
6873
from sourceosctl.commands.local_agent_registry_cli import main as local_agent_main
6974

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

8085
from sourceosctl.cli import main
8186

82-
sys.exit(main())
87+
sys.exit(main())

docs/install.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# SourceOS Devtools install and smoke guide
2+
3+
This guide covers the current Portable AI Kit scaffold in `sourceos-devtools`.
4+
5+
## Local source checkout
6+
7+
Run from the repository root:
8+
9+
```bash
10+
python3 bin/sourceosctl portable-ai profiles
11+
python3 bin/sourceosctl portable-ai preflight /tmp/SOURCEOS_AI --profile tiny-router
12+
python3 bin/sourceosctl portable-ai prepare /tmp/SOURCEOS_AI --profile tiny-router --dry-run
13+
python3 bin/sourceosctl portable-ai start-plan /tmp/SOURCEOS_AI --provider ollama-compatible --surface turtleterm
14+
python3 bin/sourceosctl portable-ai stop-plan /tmp/SOURCEOS_AI --provider ollama-compatible
15+
python3 bin/sourceosctl portable-ai byom verify /tmp/SOURCEOS_AI ./models/example.gguf --name example
16+
```
17+
18+
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.
19+
20+
## Homebrew scaffold
21+
22+
Before tap promotion, use the repository-local formula for syntax and smoke validation only:
23+
24+
```bash
25+
brew install --HEAD https://raw.githubusercontent.com/SourceOS-Linux/sourceos-devtools/main/packaging/homebrew/Formula/sourceos-devtools.rb
26+
```
27+
28+
After tap promotion:
29+
30+
```bash
31+
brew install SourceOS-Linux/tap/sourceos-devtools
32+
```
33+
34+
## Validation
35+
36+
```bash
37+
python3 -m unittest discover -s tests -v
38+
make validate
39+
python3 scripts/validate_packaging.py
40+
```
41+
42+
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.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
class SourceosDevtools < Formula
4+
desc "SourceOS developer and Portable AI Kit operator tools"
5+
homepage "https://github.com/SourceOS-Linux/sourceos-devtools"
6+
head "https://github.com/SourceOS-Linux/sourceos-devtools.git", branch: "main"
7+
8+
depends_on "python@3.12"
9+
10+
def install
11+
libexec.install Dir["*"]
12+
bin.write_exec_script libexec/"bin/sourceosctl"
13+
bin.write_exec_script libexec/"bin/sourceos-portable-ai"
14+
end
15+
16+
def caveats
17+
<<~EOS
18+
Portable AI Kit surfaces:
19+
sourceosctl portable-ai profiles
20+
sourceos-portable-ai profiles
21+
22+
Expected smoke marker:
23+
PortableAIProfiles
24+
25+
This formula is a packaging scaffold. Runtime activation and policy gates remain in source repositories.
26+
EOS
27+
end
28+
end

scripts/validate_packaging.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env python3
2+
"""Validate sourceos-devtools packaging scaffolding."""
3+
4+
from __future__ import annotations
5+
6+
import sys
7+
from pathlib import Path
8+
9+
ROOT = Path(__file__).resolve().parents[1]
10+
FORMULA = ROOT / "packaging/homebrew/Formula/sourceos-devtools.rb"
11+
INSTALL_DOC = ROOT / "docs/install.md"
12+
13+
REQUIRED_FORMULA_SNIPPETS = [
14+
"class SourceosDevtools < Formula",
15+
"SourceOS developer and Portable AI Kit operator tools",
16+
"sourceosctl",
17+
"sourceos-portable-ai",
18+
"PortableAIProfiles",
19+
]
20+
21+
FORBIDDEN_FORMULA_SNIPPETS = [
22+
"ollama pull",
23+
"ollama run",
24+
"ollama serve",
25+
"HUGGINGFACE",
26+
"HF_TOKEN",
27+
"OPENAI_API_KEY",
28+
]
29+
30+
REQUIRED_DOC_SNIPPETS = [
31+
"sourceosctl portable-ai profiles",
32+
"portable-ai preflight",
33+
"portable-ai prepare",
34+
"portable-ai start-plan",
35+
"portable-ai stop-plan",
36+
"portable-ai byom verify",
37+
"prompt egress is denied",
38+
]
39+
40+
41+
def fail(message: str) -> int:
42+
print(f"ERR: {message}", file=sys.stderr)
43+
return 1
44+
45+
46+
def main() -> int:
47+
if not FORMULA.exists():
48+
return fail(f"missing {FORMULA.relative_to(ROOT)}")
49+
if not INSTALL_DOC.exists():
50+
return fail(f"missing {INSTALL_DOC.relative_to(ROOT)}")
51+
52+
formula = FORMULA.read_text(encoding="utf-8")
53+
install_doc = INSTALL_DOC.read_text(encoding="utf-8")
54+
55+
for snippet in REQUIRED_FORMULA_SNIPPETS:
56+
if snippet not in formula:
57+
return fail(f"formula missing required snippet: {snippet}")
58+
for snippet in FORBIDDEN_FORMULA_SNIPPETS:
59+
if snippet in formula:
60+
return fail(f"formula contains forbidden side-effect/secrets snippet: {snippet}")
61+
for snippet in REQUIRED_DOC_SNIPPETS:
62+
if snippet not in install_doc:
63+
return fail(f"install doc missing required snippet: {snippet}")
64+
65+
print("Packaging validation passed")
66+
return 0
67+
68+
69+
if __name__ == "__main__":
70+
raise SystemExit(main())
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from __future__ import annotations
2+
3+
import datetime as dt
4+
import json
5+
import os
6+
import shutil
7+
from pathlib import Path
8+
from typing import Any
9+
10+
PORTABLE_LAYOUT_VERSION = "sourceos.portable-ai/v1alpha1"
11+
12+
PORTABLE_PROFILES: dict[str, dict[str, Any]] = {
13+
"tiny-router": {"displayName": "Tiny Router Kit", "minimumFreeGb": 8, "recommendedFreeGb": 16},
14+
"laptop-safe": {"displayName": "Laptop-safe Portable AI Kit", "minimumFreeGb": 16, "recommendedFreeGb": 32},
15+
"office-local": {"displayName": "Office-local Portable AI Kit", "minimumFreeGb": 32, "recommendedFreeGb": 64},
16+
"code-local": {"displayName": "Code-local Portable AI Kit", "minimumFreeGb": 32, "recommendedFreeGb": 64},
17+
"field-kit": {"displayName": "Field Operator Portable AI Kit", "minimumFreeGb": 64, "recommendedFreeGb": 128},
18+
"byom-gguf": {"displayName": "Bring-your-own GGUF Portable Kit", "minimumFreeGb": 8, "recommendedFreeGb": 64},
19+
}
20+
21+
PORTABLE_DIRS = ["manifests", "models/blobs", "cache", "state/routes", "evidence/preflight", "evidence/materialization", "tmp"]
22+
23+
24+
def _now() -> str:
25+
return dt.datetime.now(dt.timezone.utc).isoformat()
26+
27+
28+
def _print(payload: dict[str, Any]) -> int:
29+
print(json.dumps(payload, indent=2, sort_keys=True))
30+
return 0
31+
32+
33+
def _root(value: str) -> Path:
34+
return Path(value).expanduser().resolve()
35+
36+
37+
def _disk(path: Path) -> dict[str, float | None]:
38+
probe = path if path.exists() else path.parent
39+
try:
40+
total, used, free = shutil.disk_usage(probe)
41+
except FileNotFoundError:
42+
return {"totalGb": None, "usedGb": None, "freeGb": None}
43+
gb = 1024 ** 3
44+
return {"totalGb": round(total / gb, 2), "usedGb": round(used / gb, 2), "freeGb": round(free / gb, 2)}
45+
46+
47+
def profiles(_args) -> int:
48+
return _print({"type": "PortableAIProfiles", "apiVersion": PORTABLE_LAYOUT_VERSION, "profiles": PORTABLE_PROFILES})
49+
50+
51+
def preflight(args) -> int:
52+
target = _root(args.target_root)
53+
profile_name = getattr(args, "profile", "laptop-safe")
54+
profile = PORTABLE_PROFILES[profile_name]
55+
disk = _disk(target)
56+
parent = target if target.exists() else target.parent
57+
writable = parent.exists() and os.access(parent, os.W_OK)
58+
failures: list[str] = []
59+
warnings: list[str] = []
60+
free_gb = disk.get("freeGb")
61+
if not writable:
62+
failures.append("target parent is not writable")
63+
if free_gb is not None and free_gb < profile["minimumFreeGb"]:
64+
failures.append("free space below profile minimum")
65+
elif free_gb is not None and free_gb < profile["recommendedFreeGb"]:
66+
warnings.append("free space below profile recommendation")
67+
return _print({
68+
"type": "PortablePreflightEvidence",
69+
"apiVersion": PORTABLE_LAYOUT_VERSION,
70+
"capturedAt": _now(),
71+
"targetRoot": str(target),
72+
"profile": profile_name,
73+
"disk": disk,
74+
"writable": writable,
75+
"failures": failures,
76+
"warnings": warnings,
77+
"decision": "fail" if failures else "warn" if warnings else "pass",
78+
})
79+
80+
81+
def prepare(args) -> int:
82+
target = _root(args.target_root)
83+
profile_name = args.profile
84+
return _print({
85+
"type": "PortablePreparePlan",
86+
"apiVersion": PORTABLE_LAYOUT_VERSION,
87+
"capturedAt": _now(),
88+
"targetRoot": str(target),
89+
"profile": profile_name,
90+
"wouldCreateDirectories": [str(target / rel) for rel in PORTABLE_DIRS],
91+
"wouldWriteManifest": str(target / "manifests" / "portable-ai-root.json"),
92+
"wouldStartProvider": False,
93+
"wouldFetchRemoteModels": False,
94+
})
95+
96+
97+
def inspect(args) -> int:
98+
target = _root(args.target_root)
99+
return _print({"type": "PortableAIInspect", "apiVersion": PORTABLE_LAYOUT_VERSION, "targetRoot": str(target), "exists": target.exists()})
100+
101+
102+
def evidence_inspect(args) -> int:
103+
path = Path(args.path).expanduser()
104+
payload = json.loads(path.read_text(encoding="utf-8"))
105+
return _print({"path": str(path), "type": payload.get("type"), "apiVersion": payload.get("apiVersion")})
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from __future__ import annotations
2+
3+
import hashlib
4+
import json
5+
from pathlib import Path
6+
from typing import Any
7+
8+
9+
def _print(payload: dict[str, Any]) -> int:
10+
print(json.dumps(payload, indent=2, sort_keys=True))
11+
return 0
12+
13+
14+
def verify(args) -> int:
15+
model_file = Path(args.model_file).expanduser().resolve()
16+
target_root = Path(args.target_root).expanduser().resolve()
17+
if not model_file.exists() or not model_file.is_file():
18+
raise SystemExit(f"model file not found: {model_file}")
19+
digest = hashlib.sha256()
20+
with model_file.open("rb") as handle:
21+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
22+
digest.update(chunk)
23+
slug = args.name or model_file.stem
24+
payload = {
25+
"type": "PortableAIByomVerification",
26+
"targetRoot": str(target_root),
27+
"modelFile": str(model_file),
28+
"name": slug,
29+
"packId": args.pack_id or f"urn:srcos:model-carry-pack:byom-{slug}",
30+
"displayName": args.display_name or slug,
31+
"sha256": digest.hexdigest(),
32+
"sizeBytes": model_file.stat().st_size,
33+
"licenseRef": args.license_ref,
34+
"sourceNote": args.source_note,
35+
"taskClasses": args.task_class or ["operator-selected"],
36+
"wouldCopy": bool(args.copy),
37+
"wouldWriteManifest": bool(args.execute),
38+
"routeEligibleBeforeReview": False,
39+
"promptEgressDefault": "deny",
40+
"toolUseDefault": "deny",
41+
}
42+
if args.execute and not args.policy_ok:
43+
raise SystemExit("--execute requires --policy-ok")
44+
return _print(payload)

0 commit comments

Comments
 (0)