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.
243 lines
11 KiB
Python
243 lines
11 KiB
Python
"""Narrow staging Mission ingress authenticated by the registered Owner signer.
|
|
|
|
The signed payload binds the method, resource, body and idempotency key. This
|
|
adapter neither signs requests nor grants Git/worker/production capabilities.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import stat
|
|
import time
|
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
from typing import Any
|
|
|
|
from .task_plane.errors import (
|
|
ForbiddenError, TaskPlaneHTTPError, UnauthorizedError, ValidationError,
|
|
)
|
|
|
|
AUDIENCE = "aios.task-mission-authority.staging.v1"
|
|
NAMESPACE = "aios-owner-approval-v1"
|
|
MAX_BODY = 131072
|
|
REQUEST_FIELDS = frozenset({
|
|
"schema", "audience", "method", "path", "body", "idempotency_key",
|
|
"issued_at", "expires_at", "nonce",
|
|
})
|
|
|
|
|
|
def canonical_bytes(value: Any) -> bytes:
|
|
return json.dumps(value, sort_keys=True, separators=(",", ":"),
|
|
ensure_ascii=False, allow_nan=False).encode("utf-8")
|
|
|
|
|
|
def strict_object(pairs):
|
|
result = {}
|
|
for key, value in pairs:
|
|
if key in result:
|
|
raise ValidationError("duplicate JSON key")
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
class SignedMissionIngress:
|
|
def __init__(
|
|
self, service, verifier, *, terminal_proof_importer=None, clock=time.time
|
|
):
|
|
if not callable(getattr(verifier, "verify", None)):
|
|
raise ValueError("registered Owner verifier required")
|
|
identity = getattr(verifier, "signer_id", None)
|
|
if not isinstance(identity, str) or not identity.strip():
|
|
raise ValueError("registered signer identity required")
|
|
self.service = service
|
|
self.verifier = verifier
|
|
self.identity = identity
|
|
self.terminal_proof_importer = terminal_proof_importer
|
|
self.clock = clock
|
|
|
|
def handle(self, envelope: dict) -> tuple[int, dict]:
|
|
if not isinstance(envelope, dict) or set(envelope) != {
|
|
"request", "signature", "key_id"
|
|
}:
|
|
raise ValidationError("invalid signed mission envelope")
|
|
request = envelope["request"]
|
|
if not isinstance(request, dict) or set(request) != REQUEST_FIELDS:
|
|
raise ValidationError("invalid mission request fields")
|
|
if (request["schema"] != "aios.signed-mission-request.v1"
|
|
or request["audience"] != AUDIENCE):
|
|
raise ForbiddenError("mission request audience mismatch")
|
|
issued, expires = request["issued_at"], request["expires_at"]
|
|
if (type(issued) is not int or type(expires) is not int
|
|
or not 0 < expires - issued <= 300
|
|
or not issued <= self.clock() < expires):
|
|
raise UnauthorizedError("mission request expired or not yet valid")
|
|
if (not isinstance(request["nonce"], str)
|
|
or re.fullmatch(r"[A-Za-z0-9_-]{16,128}", request["nonce"]) is None):
|
|
raise ValidationError("invalid mission request nonce")
|
|
if (not isinstance(envelope["signature"], str)
|
|
or not isinstance(envelope["key_id"], str)):
|
|
raise UnauthorizedError("Owner signature required")
|
|
try:
|
|
verified = self.verifier.verify(
|
|
namespace=NAMESPACE, payload=canonical_bytes(request),
|
|
signature=envelope["signature"], key_id=envelope["key_id"],
|
|
)
|
|
except Exception:
|
|
verified = False
|
|
if verified is not True:
|
|
raise UnauthorizedError("Owner signature invalid")
|
|
method, path, body = request["method"], request["path"], request["body"]
|
|
key = request["idempotency_key"]
|
|
if method == "POST" and path == "/missions":
|
|
if not isinstance(body, dict) or not isinstance(key, str) or not key.strip():
|
|
raise ValidationError("mission body and idempotency key required")
|
|
return 201, self.service.create_mission(
|
|
body, key, actor_identity=self.identity,
|
|
)
|
|
if (method == "GET" and isinstance(path, str)
|
|
and re.fullmatch(r"/missions/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}", path)
|
|
and body is None and key is None):
|
|
return 200, self.service.get_mission(
|
|
path.split("/")[2], actor_identity=self.identity,
|
|
)
|
|
if (method == "POST" and isinstance(path, str)
|
|
and re.fullmatch(r"/tasks/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}/delegate", path)):
|
|
if not isinstance(body, dict) or not isinstance(key, str) or not key.strip():
|
|
raise ValidationError("child body and idempotency key required")
|
|
return 201, self.service.delegate_mission(
|
|
path.split("/")[2], body, key, actor_identity=self.identity,
|
|
)
|
|
if (method == "POST" and isinstance(path, str)
|
|
and re.fullmatch(
|
|
r"/missions/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}/legacy-attachments",
|
|
path,
|
|
)):
|
|
if not isinstance(body, dict) or not isinstance(key, str) or not key.strip():
|
|
raise ValidationError(
|
|
"legacy attachment body and idempotency key required"
|
|
)
|
|
result = self.service.attach_legacy_attachment(
|
|
path.split("/")[2], body, key, actor_identity=self.identity,
|
|
)
|
|
return (200 if result.get("idempotent") is True else 201), result
|
|
if (method == "GET" and isinstance(path, str)
|
|
and re.fullmatch(
|
|
r"/missions/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}/"
|
|
r"legacy-attachments/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}",
|
|
path,
|
|
) and body is None and key is None):
|
|
pieces = path.split("/")
|
|
return 200, self.service.get_legacy_attachment(
|
|
pieces[2], pieces[4], actor_identity=self.identity,
|
|
)
|
|
if (method == "POST" and isinstance(path, str)
|
|
and re.fullmatch(
|
|
r"/tasks/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}/reconcile-terminal-proof",
|
|
path,
|
|
)):
|
|
if body != {} or not isinstance(key, str) or not key.strip():
|
|
raise ValidationError(
|
|
"terminal proof trigger requires an empty body and idempotency key"
|
|
)
|
|
if self.terminal_proof_importer is None:
|
|
raise ForbiddenError("terminal proof importer is not configured")
|
|
return 200, self.terminal_proof_importer.reconcile(
|
|
path.split("/")[2], key, actor_identity=self.identity,
|
|
)
|
|
raise ForbiddenError("operation outside mission ingress scope")
|
|
|
|
|
|
def load_registered_verifier(path: Path):
|
|
metadata = path.lstat()
|
|
if (not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != 0
|
|
or metadata.st_mode & 0o022):
|
|
raise ValueError("Owner verifier must be a root-owned immutable regular file")
|
|
spec = importlib.util.spec_from_file_location("aios_mission_owner_verifier", path)
|
|
if spec is None or spec.loader is None:
|
|
raise ValueError("Owner verifier module unavailable")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module.GrantRootOwnerApprovalVerifier()
|
|
|
|
|
|
def make_handler(ingress: SignedMissionIngress):
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def log_message(self, *_args):
|
|
pass # Do not log signed payloads or transport credentials.
|
|
|
|
def respond(self, status, value):
|
|
raw = canonical_bytes(value)
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Cache-Control", "no-store")
|
|
self.send_header("Content-Length", str(len(raw)))
|
|
self.end_headers()
|
|
self.wfile.write(raw)
|
|
|
|
def do_POST(self):
|
|
try:
|
|
if self.path != "/v1/signed-request":
|
|
raise ForbiddenError("unsupported mission ingress route")
|
|
if self.headers.get("Transfer-Encoding"):
|
|
raise ValidationError("transfer encoding is unsupported")
|
|
lengths = self.headers.get_all("Content-Length", [])
|
|
if len(lengths) != 1 or not lengths[0].isdigit():
|
|
raise ValidationError("one content length required")
|
|
length = int(lengths[0])
|
|
if not 0 < length <= MAX_BODY:
|
|
raise ValidationError("mission request too large or empty")
|
|
self.connection.settimeout(10)
|
|
raw = self.rfile.read(length)
|
|
if len(raw) != length:
|
|
raise ValidationError("truncated mission request")
|
|
envelope = json.loads(raw, object_pairs_hook=strict_object)
|
|
status, result = ingress.handle(envelope)
|
|
self.respond(status, result)
|
|
except TaskPlaneHTTPError as exc:
|
|
self.respond(exc.status_code, exc.to_payload())
|
|
except (ValueError, UnicodeError, RecursionError):
|
|
self.respond(400, {"error": "invalid_json"})
|
|
except Exception:
|
|
self.respond(500, {"error": "mission_authority_unavailable"})
|
|
return Handler
|
|
|
|
|
|
def main() -> None:
|
|
from .task_plane.api import TaskPlaneService
|
|
from .task_plane.mission import MissionAuthority, MissionPrincipalBinding
|
|
from .task_plane.policy import IdentityVerifier, PolicyEngine
|
|
from .task_plane.store import TaskPlaneStore
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--db", type=Path, required=True)
|
|
parser.add_argument("--owner-verifier", type=Path, required=True)
|
|
parser.add_argument("--port", type=int, required=True)
|
|
args = parser.parse_args()
|
|
if not 1024 <= args.port <= 65535:
|
|
parser.error("unprivileged loopback port required")
|
|
verifier = load_registered_verifier(args.owner_verifier)
|
|
identity = verifier.signer_id
|
|
store = TaskPlaneStore(str(args.db), high_assurance=True)
|
|
authority = MissionAuthority(store, principal_bindings=[MissionPrincipalBinding(
|
|
authenticated_identity=identity, owner_principal_id=identity,
|
|
acceptance_verifier_id="identity-lineage-live-closure-v1",
|
|
allowed_task_capabilities=(
|
|
"code.read", "code.write", "test.execute",
|
|
"governed.write.file", "governed.write.branch",
|
|
),
|
|
max_risk="medium",
|
|
)])
|
|
service = TaskPlaneService(store, policy=PolicyEngine(
|
|
identity_verifier=IdentityVerifier({identity}), execution_enabled=False,
|
|
identity_capabilities={identity: {"task.creator", "task.read", "task.delegate"}},
|
|
), mission_authority=authority)
|
|
ingress = SignedMissionIngress(service, verifier)
|
|
# SQLite's existing connection and transaction lock share one request thread.
|
|
HTTPServer(("127.0.0.1", args.port), make_handler(ingress)).serve_forever()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|