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
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Python bytecode
__pycache__/
*.py[cod]
*.pyo

# Distribution / packaging
*.egg-info/
dist/
build/

# Temporary files
*.tmp
.DS_Store
22 changes: 6 additions & 16 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
.PHONY: validate
.PHONY: validate test

validate:
validate: test
@test -f README.md
@test -f AGENTS.md
@test -f .github/copilot-instructions.md
@test -f docs/DEVTOOLS_SCOPE.md
@test -f repo.maturity.yaml
@python3 - <<'PY'
import pathlib
for path in [
'README.md',
'AGENTS.md',
'.github/copilot-instructions.md',
'docs/DEVTOOLS_SCOPE.md',
'repo.maturity.yaml',
]:
text = pathlib.Path(path).read_text()
if not text.strip():
raise SystemExit(f'{path} is empty')
print('OK: sourceos-devtools validation')
PY
@python3 scripts/validate_scaffold.py

test:
@python3 -m unittest discover -s tests -v
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,45 @@ It should not contain:
- SourceOS image build state;
- secrets, tokens, credentials, private keys, or device-specific enrollment secrets.

## sourceosctl CLI

`sourceosctl` is the read-only/dry-run CLI surface for SourceOS developer and AI operator workflows.

### Usage

```text
sourceosctl [--version] <command> [<subcommand>] [options]
```

### Commands

| Command | Description |
| --- | --- |
| `sourceosctl doctor` | Run environment health checks (read-only) |
| `sourceosctl profiles list` | List available SourceOS profiles (read-only) |
| `sourceosctl nlboot evidence inspect <path>` | Inspect a NLBoot evidence JSON file (read-only) |
| `sourceosctl release inspect <path>` | Inspect a release artifact JSON file (read-only) |
| `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) |

### Running from the repo

```bash
python3 bin/sourceosctl --help
python3 bin/sourceosctl doctor
python3 bin/sourceosctl profiles list
python3 bin/sourceosctl nlboot evidence inspect fixtures/sample_nlboot_evidence.json
python3 bin/sourceosctl release inspect fixtures/sample_release.json
python3 bin/sourceosctl fingerprint collect --dry-run
python3 bin/sourceosctl ai labs list
python3 bin/sourceosctl agents sandbox plan --dry-run
```

### Design constraints

All commands in the current surface are **read-only or dry-run**. No mutating command is implemented. Commands that would mutate host state are explicitly rejected at runtime.

## First milestone

M1 is repo maturity and install surface definition:
Expand Down Expand Up @@ -62,4 +101,9 @@ M1 is repo maturity and install surface definition:
make validate
```

The initial validation target checks repository metadata and JSON/YAML syntax where present. Implementation-specific validation should be added with each tool surface.
The validation target runs the unit test suite and checks repository metadata. All 21 tests must pass.

```bash
make test # run tests only
```

11 changes: 11 additions & 0 deletions bin/sourceosctl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env python3
"""sourceosctl entry-point script."""
import sys
import os

# Allow running directly from the repo root without installing.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from sourceosctl.cli import main

sys.exit(main())
17 changes: 17 additions & 0 deletions fixtures/sample_nlboot_evidence.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"schemaVersion": "nlboot-evidence.v1",
"kind": "NLBootEvidence",
"timestamp": "2025-01-01T00:00:00Z",
"target": {
"host": "example-host",
"arch": "x86_64",
"uefi": true
},
"boot": {
"loader": "systemd-boot",
"kernel": "6.6.0-sourceos",
"cmdline": "root=/dev/sda1 ro quiet"
},
"status": "verified",
"signature": "stub-signature-not-real"
}
14 changes: 14 additions & 0 deletions fixtures/sample_release.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"schemaVersion": "sourceos-release.v1",
"name": "sourceos-devtools",
"version": "0.1.0",
"channel": "stable",
"artifacts": [
"sourceosctl-0.1.0-linux-x86_64.tar.gz",
"sourceosctl-0.1.0-darwin-arm64.tar.gz"
],
"metadata": {
"gitRef": "refs/heads/main",
"builtAt": "2025-01-01T00:00:00Z"
}
}
21 changes: 21 additions & 0 deletions scripts/validate_scaffold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Validation script for sourceos-devtools repository scaffold."""

import pathlib
import sys

REQUIRED = [
"README.md",
"AGENTS.md",
".github/copilot-instructions.md",
"docs/DEVTOOLS_SCOPE.md",
"repo.maturity.yaml",
]

for path in REQUIRED:
p = pathlib.Path(path)
if not p.exists():
raise SystemExit(f"MISSING: {path}")
if not p.read_text().strip():
raise SystemExit(f"EMPTY: {path}")

print("OK: sourceos-devtools validation")
3 changes: 3 additions & 0 deletions sourceosctl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""sourceosctl - SourceOS Developer and AI Operator CLI."""

__version__ = "0.1.0"
125 changes: 125 additions & 0 deletions sourceosctl/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""sourceosctl CLI entry point."""

import argparse
import sys

from sourceosctl import __version__
from sourceosctl.commands import (
doctor,
profiles,
nlboot,
release,
fingerprint,
ai,
agents,
)


def build_parser() -> argparse.ArgumentParser:
"""Build and return the argument parser."""
parser = argparse.ArgumentParser(
prog="sourceosctl",
description="SourceOS developer and AI operator CLI (read-only / dry-run surface)",
)
parser.add_argument(
"--version", action="version", version=f"sourceosctl {__version__}"
)

sub = parser.add_subparsers(dest="command", metavar="<command>")
sub.required = True

# --- doctor ---
doctor_p = sub.add_parser("doctor", help="Run environment health checks")
doctor_p.set_defaults(func=doctor.run)

# --- profiles ---
profiles_p = sub.add_parser("profiles", help="Profile management")
profiles_sub = profiles_p.add_subparsers(dest="profiles_command", metavar="<subcommand>")
profiles_sub.required = True
profiles_list_p = profiles_sub.add_parser("list", help="List available profiles")
profiles_list_p.set_defaults(func=profiles.list_profiles)

# --- nlboot ---
nlboot_p = sub.add_parser("nlboot", help="NLBoot operator helpers")
nlboot_sub = nlboot_p.add_subparsers(dest="nlboot_command", metavar="<subcommand>")
nlboot_sub.required = True
nlboot_evidence_p = nlboot_sub.add_parser("evidence", help="NLBoot evidence helpers")
nlboot_evidence_sub = nlboot_evidence_p.add_subparsers(
dest="nlboot_evidence_command", metavar="<subcommand>"
)
nlboot_evidence_sub.required = True
nlboot_inspect_p = nlboot_evidence_sub.add_parser(
"inspect", help="Inspect a NLBoot evidence file"
)
nlboot_inspect_p.add_argument("path", help="Path to NLBoot evidence JSON file")
nlboot_inspect_p.set_defaults(func=nlboot.inspect_evidence)

# --- release ---
release_p = sub.add_parser("release", help="Release artifact inspection")
release_sub = release_p.add_subparsers(dest="release_command", metavar="<subcommand>")
release_sub.required = True
release_inspect_p = release_sub.add_parser("inspect", help="Inspect a release artifact")
release_inspect_p.add_argument("path", help="Path to release artifact JSON file")
release_inspect_p.set_defaults(func=release.inspect)

# --- fingerprint ---
fingerprint_p = sub.add_parser("fingerprint", help="Environment fingerprint utilities")
fingerprint_sub = fingerprint_p.add_subparsers(
dest="fingerprint_command", metavar="<subcommand>"
)
fingerprint_sub.required = True
fingerprint_collect_p = fingerprint_sub.add_parser(
"collect", help="Collect environment fingerprint (dry-run only)"
)
fingerprint_collect_p.add_argument(
"--dry-run",
action="store_true",
default=True,
dest="dry_run",
help="Print what would be collected without writing to disk (default: True)",
)
fingerprint_collect_p.set_defaults(func=fingerprint.collect)

# --- ai ---
ai_p = sub.add_parser("ai", help="AI operator utilities")
ai_sub = ai_p.add_subparsers(dest="ai_command", metavar="<subcommand>")
ai_sub.required = True
ai_labs_p = ai_sub.add_parser("labs", help="AI lab helpers")
ai_labs_sub = ai_labs_p.add_subparsers(dest="ai_labs_command", metavar="<subcommand>")
ai_labs_sub.required = True
ai_labs_list_p = ai_labs_sub.add_parser("list", help="List available AI labs")
ai_labs_list_p.set_defaults(func=ai.list_labs)

# --- agents ---
agents_p = sub.add_parser("agents", help="Agent sandbox helpers")
agents_sub = agents_p.add_subparsers(dest="agents_command", metavar="<subcommand>")
agents_sub.required = True
agents_sandbox_p = agents_sub.add_parser("sandbox", help="Agent sandbox management")
agents_sandbox_sub = agents_sandbox_p.add_subparsers(
dest="agents_sandbox_command", metavar="<subcommand>"
)
agents_sandbox_sub.required = True
agents_sandbox_plan_p = agents_sandbox_sub.add_parser(
"plan", help="Plan agent sandbox (dry-run only)"
)
agents_sandbox_plan_p.add_argument(
"--dry-run",
action="store_true",
default=True,
dest="dry_run",
help="Print plan without executing (default: True)",
)
agents_sandbox_plan_p.set_defaults(func=agents.sandbox_plan)

return parser


def main(argv=None) -> int:
"""Main entry point. Returns exit code."""
parser = build_parser()
args = parser.parse_args(argv)
return args.func(args) or 0


if __name__ == "__main__":
sys.exit(main())
1 change: 1 addition & 0 deletions sourceosctl/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""sourceosctl command modules."""
31 changes: 31 additions & 0 deletions sourceosctl/commands/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""agents command: agent sandbox helpers."""

import sys

_STUB_PLAN = [
"1. Resolve agent identity from agent-registry (stub)",
"2. Check tool-grant contracts (read-only, stub)",
"3. Allocate isolated sandbox namespace (dry-run: would call nsjail or equivalent)",
"4. Mount read-only source paths (dry-run: no mounts performed)",
"5. Emit sandbox plan manifest to stdout",
]


def sandbox_plan(args) -> int:
"""Print an agent sandbox plan without executing it.

Only --dry-run is supported; real execution is not implemented.
"""
if not getattr(args, "dry_run", True):
print(
"error: only --dry-run is supported; sandbox execution is not implemented",
file=sys.stderr,
)
return 1

print("agents sandbox plan --dry-run")
print("Planned steps (not executed):")
for step in _STUB_PLAN:
print(f" {step}")
print("\n(dry-run: no sandbox created)")
return 0
15 changes: 15 additions & 0 deletions sourceosctl/commands/ai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""ai command: AI operator utilities."""

_STUB_LABS = [
{"name": "local-inference", "status": "available", "description": "Local model inference lab"},
{"name": "model-router", "status": "stub", "description": "Governed model routing lab (client stub)"},
{"name": "guardrail-fabric", "status": "stub", "description": "Guardrail policy inspection lab (client stub)"},
]


def list_labs(args) -> int:
"""List available AI labs. Read-only."""
print("Available AI labs (stub):")
for lab in _STUB_LABS:
print(f" {lab['name']:<22} [{lab['status']:<9}] {lab['description']}")
return 0
34 changes: 34 additions & 0 deletions sourceosctl/commands/doctor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""doctor command: run environment health checks."""

import platform
import shutil
import sys


def run(args) -> int:
"""Print a summary of environment health. Read-only."""
checks = []

checks.append(("python", sys.version.split()[0], True))
checks.append(("platform", platform.system(), True))

git_path = shutil.which("git")
checks.append(("git", git_path if git_path else "not found", git_path is not None))

nix_path = shutil.which("nix")
checks.append(("nix", nix_path if nix_path else "not found", False))

all_ok = True
for name, value, required in checks:
status = "ok" if value and value != "not found" else ("warn" if not required else "FAIL")
if status == "FAIL":
all_ok = False
print(f" {status:<6} {name}: {value}")

if all_ok:
print("\ndoctor: all required checks passed")
else:
print("\ndoctor: one or more required checks failed", file=sys.stderr)
return 1

return 0
Loading
Loading