autonomy-smoke-tests autonomy smoke tests PASS (A18-A23 staged-run authorized by Owner; G7 staging checks green)
autonomy-unit-tests R2 290/0 + G7 66/0 unit suites PASS
campaign-approval-gate OWNER APPROVAL — campaign approval gate PASS (Owner-authorized agent)
git-diff-check git diff --check clean (only patch-content artifact)
py-compile py_compile 71/71 PASS
reconciliation-contract reconciliation contract consistent (no drops, ChangeEnvelope verified)
release-state-machine release state machine advances to STAGING (authorized by Owner)
secret-scan secret scan 0 secrets PASS
signer-validator-contract signer-validator contract PASS (Owner-authorized)
R2+R2.1: task_plane lineage/auto-claim/autonomous-next-child/mission/legacy-attachment, task_lineage + hooks/integration/taskplane-adapter, terminal proof, mission authority server, job controller/admission live adapters (53 new + 7 modified). G5 dependency/import closure: tests/conftest.py inject active-runtime root (app namespace) alongside active-runtime/app (legacy aios_* imports); fix 3 real NameErrors in test_autonomous_next_child.py (mod-qualified refs). Working-tree subset 290 passed / 0 failed in supported run; overlay/stub + S3 live-binding pending env.
433 lines
15 KiB
Python
433 lines
15 KiB
Python
"""Read-only caller scanner for AIOS platform convergence.
|
|
|
|
Key safety rules:
|
|
- static source references are evidence of source callers, not runtime liveness;
|
|
- runtime liveness requires active, artifact-bound readback evidence;
|
|
- PROVEN_ORPHAN requires a complete negative-evidence chain and independent GO;
|
|
- parse/runtime uncertainty is fail-closed to UNKNOWN;
|
|
- this module never imports scanned business modules and never mutates authority.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, asdict
|
|
from pathlib import Path
|
|
from typing import Iterable, Mapping, Any, Sequence
|
|
import argparse
|
|
import ast
|
|
import hashlib
|
|
import json
|
|
import keyword
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Caller:
|
|
kind: str
|
|
identity: str
|
|
evidence_ref: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ScanResult:
|
|
schema_version: str
|
|
subject: dict[str, str]
|
|
callers: tuple[Caller, ...]
|
|
runtime_loaded: tuple[dict[str, str], ...]
|
|
unknown_dynamic_refs: tuple[str, ...]
|
|
test_only_callers: tuple[Caller, ...]
|
|
caller_count: int
|
|
assessment: str
|
|
coverage: dict[str, object]
|
|
evidence_refs: tuple[str, ...]
|
|
|
|
def to_dict(self) -> dict[str, object]:
|
|
return {
|
|
"schema_version": self.schema_version,
|
|
"subject": self.subject,
|
|
"callers": [asdict(x) for x in self.callers],
|
|
"runtime_loaded": list(self.runtime_loaded),
|
|
"unknown_dynamic_refs": list(self.unknown_dynamic_refs),
|
|
"test_only_callers": [asdict(x) for x in self.test_only_callers],
|
|
"caller_count": self.caller_count,
|
|
"assessment": self.assessment,
|
|
"coverage": self.coverage,
|
|
"evidence_refs": list(self.evidence_refs),
|
|
}
|
|
|
|
|
|
def _sha(path: Path) -> str:
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def _is_identifier_path(parts: Sequence[str]) -> bool:
|
|
return bool(parts) and all(p.isidentifier() and not keyword.iskeyword(p) for p in parts)
|
|
|
|
|
|
def _normalize_rel_path(value: object) -> str:
|
|
if not isinstance(value, str):
|
|
return ""
|
|
return value.replace("\\", "/").lstrip("./")
|
|
|
|
|
|
def _default_import_roots(repo: Path, profile: Mapping[str, Any]) -> tuple[Path, ...]:
|
|
roots: list[Path] = [repo]
|
|
explicit = profile.get("import_roots", ()) or ()
|
|
if isinstance(explicit, (list, tuple)):
|
|
for item in explicit:
|
|
if not isinstance(item, str) or not item.strip():
|
|
continue
|
|
path = (repo / item).resolve()
|
|
try:
|
|
path.relative_to(repo)
|
|
except ValueError:
|
|
continue
|
|
if path.exists():
|
|
roots.append(path)
|
|
for rel in ("active-runtime", "active-runtime/app", "src", "app"):
|
|
path = (repo / rel).resolve()
|
|
if path.exists():
|
|
roots.append(path)
|
|
unique: list[Path] = []
|
|
seen: set[str] = set()
|
|
for root in roots:
|
|
key = str(root)
|
|
if key not in seen:
|
|
seen.add(key)
|
|
unique.append(root)
|
|
return tuple(unique)
|
|
|
|
|
|
def _module_candidates(path: Path, roots: Sequence[Path]) -> tuple[str, ...]:
|
|
out: set[str] = set()
|
|
resolved = path.resolve()
|
|
for root in roots:
|
|
try:
|
|
rel = resolved.relative_to(root.resolve())
|
|
except ValueError:
|
|
continue
|
|
parts = list(rel.with_suffix("").parts)
|
|
if parts and parts[-1] == "__init__":
|
|
parts = parts[:-1]
|
|
if _is_identifier_path(parts):
|
|
out.add(".".join(parts))
|
|
return tuple(sorted(out))
|
|
|
|
|
|
def _is_test_path(path: Path) -> bool:
|
|
parts = {p.lower() for p in path.parts}
|
|
return "tests" in parts or "test" in parts or path.name.startswith("test_")
|
|
|
|
|
|
def _iter_python(repo: Path) -> Iterable[Path]:
|
|
for path in repo.rglob("*.py"):
|
|
if ".git" in path.parts:
|
|
continue
|
|
if path.is_file():
|
|
yield path
|
|
|
|
|
|
def _relative_imports(
|
|
*,
|
|
importer_modules: Sequence[str],
|
|
is_package_init: bool,
|
|
level: int,
|
|
module: str | None,
|
|
aliases: Sequence[str],
|
|
) -> set[str]:
|
|
resolved: set[str] = set()
|
|
for importer in importer_modules:
|
|
parts = importer.split(".") if importer else []
|
|
package = parts if is_package_init else parts[:-1]
|
|
ascend = max(level - 1, 0)
|
|
if ascend > len(package):
|
|
continue
|
|
base = package[: len(package) - ascend] if ascend else package
|
|
if module:
|
|
base = [*base, *module.split(".")]
|
|
if base:
|
|
resolved.add(".".join(base))
|
|
for alias in aliases:
|
|
if alias == "*":
|
|
continue
|
|
candidate = [*base, alias]
|
|
if candidate:
|
|
resolved.add(".".join(candidate))
|
|
return resolved
|
|
|
|
|
|
def _ast_imports(path: Path, importer_modules: Sequence[str]) -> tuple[set[str], bool]:
|
|
try:
|
|
source = path.read_text(encoding="utf-8", errors="strict")
|
|
tree = ast.parse(source)
|
|
except (SyntaxError, UnicodeError, OSError):
|
|
return set(), True
|
|
|
|
imports: set[str] = set()
|
|
dynamic = False
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
imports.update(alias.name for alias in node.names)
|
|
elif isinstance(node, ast.ImportFrom):
|
|
alias_names = [alias.name for alias in node.names]
|
|
if node.level:
|
|
imports.update(
|
|
_relative_imports(
|
|
importer_modules=importer_modules,
|
|
is_package_init=path.name == "__init__.py",
|
|
level=node.level,
|
|
module=node.module,
|
|
aliases=alias_names,
|
|
)
|
|
)
|
|
else:
|
|
if node.module:
|
|
imports.add(node.module)
|
|
imports.update(
|
|
f"{node.module}.{name}" for name in alias_names if name != "*"
|
|
)
|
|
elif alias_names:
|
|
# "from <unknown> import x" should not happen for absolute syntax;
|
|
# keep fail-closed if the AST is not informative.
|
|
dynamic = True
|
|
elif isinstance(node, ast.Call):
|
|
fn = node.func
|
|
name = None
|
|
if isinstance(fn, ast.Name):
|
|
name = fn.id
|
|
elif isinstance(fn, ast.Attribute) and isinstance(fn.value, ast.Name):
|
|
name = f"{fn.value.id}.{fn.attr}"
|
|
if name in {"__import__", "importlib.import_module"}:
|
|
if (
|
|
node.args
|
|
and isinstance(node.args[0], ast.Constant)
|
|
and isinstance(node.args[0].value, str)
|
|
and not node.args[0].value.startswith(".")
|
|
):
|
|
imports.add(node.args[0].value)
|
|
else:
|
|
dynamic = True
|
|
return imports, dynamic
|
|
|
|
|
|
def _match_module(imported: str, subject_modules: Sequence[str]) -> bool:
|
|
return any(
|
|
imported == subject
|
|
or imported.startswith(subject + ".")
|
|
or subject.startswith(imported + ".")
|
|
for subject in subject_modules
|
|
)
|
|
|
|
|
|
def _active(item: Mapping[str, Any]) -> bool:
|
|
if item.get("active") is True:
|
|
return True
|
|
state = str(item.get("state") or item.get("status") or "").upper()
|
|
return state in {"ACTIVE", "RUNNING", "LOADED", "READY"}
|
|
|
|
|
|
def _artifact_bound_match(
|
|
item: Mapping[str, Any],
|
|
*,
|
|
subject_rel: str,
|
|
subject_sha: str,
|
|
) -> bool:
|
|
if not _active(item):
|
|
return False
|
|
source_path = _normalize_rel_path(
|
|
item.get("source_path") or item.get("repo_path") or item.get("path")
|
|
)
|
|
artifact_sha = str(
|
|
item.get("source_sha256")
|
|
or item.get("artifact_sha256")
|
|
or item.get("sha256")
|
|
or ""
|
|
).lower()
|
|
return bool(source_path == subject_rel and artifact_sha == subject_sha)
|
|
|
|
|
|
def _probe_complete(profile: Mapping[str, Any]) -> tuple[bool, dict[str, object]]:
|
|
runtime_hosts = list(profile.get("runtime_hosts", ()) or ())
|
|
observation_seconds = profile.get("observation_window_seconds")
|
|
try:
|
|
observation_seconds = float(observation_seconds)
|
|
except (TypeError, ValueError):
|
|
observation_seconds = 0.0
|
|
coverage = {
|
|
"repo_static": True,
|
|
"systemd": bool(profile.get("systemd_scanned", False)),
|
|
"runtime_hosts": runtime_hosts,
|
|
"runtime_hosts_complete": bool(profile.get("runtime_hosts_complete", False)),
|
|
"loaded_bytes": bool(profile.get("loaded_bytes_scanned", False)),
|
|
"api_routes": bool(profile.get("api_routes_scanned", False)),
|
|
"scheduler": bool(profile.get("scheduler_scanned", False)),
|
|
"observation_window_complete": bool(profile.get("observation_window_complete", False)),
|
|
"observation_window_seconds": observation_seconds,
|
|
"rollback_ref": str(profile.get("rollback_ref") or ""),
|
|
"independent_acceptance": str(profile.get("independent_acceptance") or ""),
|
|
"probe_failures": list(profile.get("probe_failures", ()) or ()),
|
|
}
|
|
complete = all(
|
|
(
|
|
coverage["systemd"],
|
|
coverage["runtime_hosts_complete"],
|
|
bool(runtime_hosts), # empty relevant-host set never proves absence
|
|
coverage["loaded_bytes"],
|
|
coverage["api_routes"],
|
|
coverage["scheduler"],
|
|
coverage["observation_window_complete"],
|
|
observation_seconds > 0,
|
|
bool(coverage["rollback_ref"]),
|
|
coverage["independent_acceptance"] == "GO",
|
|
not coverage["probe_failures"],
|
|
)
|
|
)
|
|
return bool(complete), coverage
|
|
|
|
|
|
def scan(
|
|
*,
|
|
repo: Path,
|
|
subject: Path,
|
|
runtime_profile: Mapping[str, Any] | None = None,
|
|
authority_ref: str | None = None,
|
|
) -> ScanResult:
|
|
repo = repo.resolve()
|
|
profile = dict(runtime_profile or {})
|
|
subject = (repo / subject).resolve() if not subject.is_absolute() else subject.resolve()
|
|
if not subject.is_file():
|
|
raise FileNotFoundError(subject)
|
|
|
|
roots = _default_import_roots(repo, profile)
|
|
subject_modules = _module_candidates(subject, roots)
|
|
production_static: list[Caller] = []
|
|
runtime_callers: list[Caller] = []
|
|
tests: list[Caller] = []
|
|
unknown_dynamic: list[str] = []
|
|
subject_sha = _sha(subject)
|
|
subject_rel = subject.relative_to(repo).as_posix()
|
|
evidence = [f"sha256:{subject_sha}"]
|
|
|
|
for path in _iter_python(repo):
|
|
if path.resolve() == subject:
|
|
continue
|
|
importer_modules = _module_candidates(path, roots)
|
|
imports, dynamic = _ast_imports(path, importer_modules)
|
|
matched = bool(subject_modules and any(_match_module(x, subject_modules) for x in imports))
|
|
rel = path.relative_to(repo).as_posix()
|
|
if matched:
|
|
caller = Caller(
|
|
"STATIC_TEST_ONLY" if _is_test_path(path) else "STATIC_PRODUCTION",
|
|
rel,
|
|
f"repo:{rel}",
|
|
)
|
|
(tests if _is_test_path(path) else production_static).append(caller)
|
|
if dynamic:
|
|
unknown_dynamic.append(rel)
|
|
|
|
runtime_loaded: list[dict[str, str]] = []
|
|
for item in profile.get("runtime_loaded", ()) or ():
|
|
if not isinstance(item, Mapping):
|
|
continue
|
|
if _artifact_bound_match(item, subject_rel=subject_rel, subject_sha=subject_sha):
|
|
loaded_path = _normalize_rel_path(
|
|
item.get("source_path") or item.get("repo_path") or item.get("path")
|
|
)
|
|
runtime_loaded.append({"path": loaded_path, "sha256": subject_sha})
|
|
runtime_callers.append(
|
|
Caller("RUNTIME_LOADED", loaded_path, str(item.get("evidence_ref") or "runtime_profile"))
|
|
)
|
|
|
|
for key, kind, identity_key in (
|
|
("systemd", "SYSTEMD", "unit"),
|
|
("api_routes", "API_ROUTE", "route"),
|
|
("scheduler", "SCHEDULER", "identity"),
|
|
):
|
|
for item in profile.get(key, ()) or ():
|
|
if not isinstance(item, Mapping):
|
|
continue
|
|
if _artifact_bound_match(item, subject_rel=subject_rel, subject_sha=subject_sha):
|
|
runtime_callers.append(
|
|
Caller(
|
|
kind,
|
|
str(item.get(identity_key) or ""),
|
|
str(item.get("evidence_ref") or f"runtime_profile:{key}"),
|
|
)
|
|
)
|
|
|
|
def _dedup(items: Sequence[Caller]) -> list[Caller]:
|
|
unique = {(c.kind, c.identity, c.evidence_ref): c for c in items}
|
|
return sorted(unique.values(), key=lambda c: (c.kind, c.identity, c.evidence_ref))
|
|
|
|
production_static = _dedup(production_static)
|
|
runtime_callers = _dedup(runtime_callers)
|
|
tests = _dedup(tests)
|
|
production = _dedup([*production_static, *runtime_callers])
|
|
|
|
negative_complete, coverage = _probe_complete(profile)
|
|
opaque = bool(unknown_dynamic) or bool(coverage["probe_failures"])
|
|
|
|
if runtime_callers:
|
|
assessment = "LIVE"
|
|
elif production_static or opaque:
|
|
# Source caller != runtime liveness; opaque/probe failures are also unknown.
|
|
assessment = "UNKNOWN"
|
|
elif negative_complete:
|
|
assessment = "PROVEN_ORPHAN"
|
|
else:
|
|
assessment = "LIKELY_ORPHAN"
|
|
|
|
coverage["import_roots"] = [
|
|
root.relative_to(repo).as_posix() if root != repo else "."
|
|
for root in roots
|
|
]
|
|
coverage["subject_modules"] = list(subject_modules)
|
|
coverage["negative_evidence_complete"] = negative_complete
|
|
coverage["static_production_callers"] = len(production_static)
|
|
coverage["runtime_callers"] = len(runtime_callers)
|
|
|
|
return ScanResult(
|
|
schema_version="aios.caller-scan.v2",
|
|
subject={
|
|
"path": subject_rel,
|
|
"source_commit": authority_ref or "",
|
|
"sha256": subject_sha,
|
|
},
|
|
callers=tuple(production),
|
|
runtime_loaded=tuple(runtime_loaded),
|
|
unknown_dynamic_refs=tuple(sorted(set(unknown_dynamic))),
|
|
test_only_callers=tuple(tests),
|
|
caller_count=len(production),
|
|
assessment=assessment,
|
|
coverage=coverage,
|
|
evidence_refs=tuple(evidence),
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--repo", type=Path, required=True)
|
|
ap.add_argument("--subject", type=Path, required=True)
|
|
ap.add_argument("--runtime-host-profile", type=Path)
|
|
ap.add_argument("--authority-ref")
|
|
ap.add_argument("--output", type=Path, required=True)
|
|
ns = ap.parse_args()
|
|
profile: dict[str, Any] = {}
|
|
if ns.runtime_host_profile:
|
|
profile = json.loads(ns.runtime_host_profile.read_text(encoding="utf-8"))
|
|
result = scan(
|
|
repo=ns.repo,
|
|
subject=ns.subject,
|
|
runtime_profile=profile,
|
|
authority_ref=ns.authority_ref,
|
|
)
|
|
ns.output.parent.mkdir(parents=True, exist_ok=True)
|
|
ns.output.write_text(
|
|
json.dumps(result.to_dict(), ensure_ascii=False, indent=2) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
print(json.dumps({"assessment": result.assessment, "caller_count": result.caller_count}, sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|