feat(provenance): implement Ed25519 issuer persistent store and seven-path writer

This commit is contained in:
Codex
2026-07-21 09:33:07 +08:00
parent 9e46ac0504
commit cbb2e232f5
+831
View File
@@ -0,0 +1,831 @@
"""AIOS V2 可信 Run Provenance 签发、持久化与受控写入。
生产策略要求 Provenance 同时覆盖七类路径:代码变更、Git 操作、部署、
Runtime 执行、Evidence 写入、回滚和审计。任何缺失、篡改、过期、撤销、
重放或上下文不匹配都默认拒绝。
"""
from __future__ import annotations
import base64
import hashlib
import json
import os
import sqlite3
import stat
import threading
import uuid
from dataclasses import asdict, dataclass, field, replace
from datetime import datetime, timedelta, timezone
from enum import Enum
from pathlib import Path
from typing import Any, Mapping, Protocol, Sequence
PROVENANCE_VERSION = "aios.v2.run-provenance.v1"
PRODUCTION_POLICY_VERSION = "aios.v2.production.v1"
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def parse_timestamp(value: str) -> datetime:
normalized = str(value or "").strip()
if not normalized:
raise ValueError("timestamp is required")
if normalized.endswith("Z"):
normalized = normalized[:-1] + "+00:00"
parsed = datetime.fromisoformat(normalized)
if parsed.tzinfo is None:
raise ValueError("timestamp must include timezone")
return parsed.astimezone(timezone.utc)
def required(value: str, label: str) -> str:
normalized = str(value or "").strip()
if not normalized:
raise ValueError(f"{label} is required")
return normalized
def canonical_json_bytes(value: Mapping[str, Any]) -> bytes:
return json.dumps(
dict(value),
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
default=str,
).encode("utf-8")
def digest_payload(value: Mapping[str, Any]) -> str:
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
def _validate_digest(value: str) -> str:
normalized = required(value, "request_digest").lower()
if len(normalized) != 64 or any(ch not in "0123456789abcdef" for ch in normalized):
raise ValueError("request_digest must be a SHA-256 hexadecimal digest")
return normalized
class ExecutionPath(str, Enum):
CODE_CHANGE = "code_change"
GIT_OPERATION = "git_operation"
DEPLOYMENT = "deployment"
RUNTIME_EXECUTE = "runtime_execute"
EVIDENCE_WRITE = "evidence_write"
ROLLBACK = "rollback"
AUDIT = "audit"
REQUIRED_PRODUCTION_PATHS = frozenset(item.value for item in ExecutionPath)
class GitOperation(str, Enum):
READ_REF = "read_ref"
FETCH = "fetch"
CREATE_BRANCH = "create_branch"
COMMIT = "commit"
MERGE = "merge"
PUSH = "push"
UPDATE_EVIDENCE = "update_evidence"
class ProvenanceStatus(str, Enum):
ISSUED = "issued"
VERIFIED = "verified"
USED = "used"
EXPIRED = "expired"
REVOKED = "revoked"
class ProvenanceError(RuntimeError):
"""Run Provenance 基础错误。"""
class ProvenanceDenied(ProvenanceError):
"""策略拒绝当前操作。"""
class ProvenanceNotFound(ProvenanceDenied):
"""未找到 Provenance。"""
class ProvenanceSignatureError(ProvenanceDenied):
"""签名无效。"""
class ProvenanceExpired(ProvenanceDenied):
"""Provenance 已过期。"""
class ProvenanceRevoked(ProvenanceDenied):
"""Provenance 已撤销。"""
class ProvenanceReplay(ProvenanceDenied):
"""检测到相同授权摘要重放。"""
class Signer(Protocol):
key_id: str
def sign(self, payload: bytes) -> str: ...
class SignatureVerifier(Protocol):
key_id: str
fingerprint: str
def verify(self, payload: bytes, signature: str) -> bool: ...
def _load_crypto() -> tuple[Any, Any, Any]:
try:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey,
Ed25519PublicKey,
)
except ImportError as exc: # pragma: no cover - depends on deployment image
raise RuntimeError(
"cryptography package with Ed25519 support is required"
) from exc
return serialization, Ed25519PrivateKey, Ed25519PublicKey
def _check_private_key_file(path: Path) -> bytes:
resolved = path.resolve()
metadata = resolved.stat()
if not stat.S_ISREG(metadata.st_mode):
raise RuntimeError("private key reference is not a regular file")
if metadata.st_mode & 0o077:
raise RuntimeError("private key file permissions must be 0600 or stricter")
return resolved.read_bytes()
def _check_public_key_file(path: Path) -> bytes:
resolved = path.resolve()
metadata = resolved.stat()
if not stat.S_ISREG(metadata.st_mode):
raise RuntimeError("public key reference is not a regular file")
if metadata.st_mode & 0o022:
raise RuntimeError("public key file must not be group/world writable")
return resolved.read_bytes()
class Ed25519PrivateKeySigner:
"""Ed25519 Owner/Control Plane 签发器。"""
def __init__(self, private_key: Any, *, key_id: str) -> None:
self.private_key = private_key
self.key_id = required(key_id, "key_id")
@classmethod
def from_file(cls, path: str | Path, *, key_id: str) -> "Ed25519PrivateKeySigner":
serialization, private_cls, _ = _load_crypto()
value = _check_private_key_file(Path(path))
if value.startswith(b"-----BEGIN"):
key = serialization.load_pem_private_key(value, password=None)
elif len(value) == 32:
key = private_cls.from_private_bytes(value)
else:
raise RuntimeError("private key must be PEM or a raw 32-byte Ed25519 seed")
if not isinstance(key, private_cls):
raise RuntimeError("private key is not Ed25519")
return cls(key, key_id=key_id)
def sign(self, payload: bytes) -> str:
return base64.b64encode(self.private_key.sign(bytes(payload))).decode("ascii")
def public_verifier(self) -> "Ed25519PublicKeyVerifier":
return Ed25519PublicKeyVerifier(self.private_key.public_key(), key_id=self.key_id)
class Ed25519PublicKeyVerifier:
"""Ed25519 签名验证器。"""
def __init__(self, public_key: Any, *, key_id: str) -> None:
serialization, _, public_cls = _load_crypto()
if not isinstance(public_key, public_cls):
raise RuntimeError("public key is not Ed25519")
self.public_key = public_key
self.key_id = required(key_id, "key_id")
raw = public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
self.fingerprint = hashlib.sha256(raw).hexdigest()
@classmethod
def from_file(cls, path: str | Path, *, key_id: str) -> "Ed25519PublicKeyVerifier":
serialization, _, public_cls = _load_crypto()
value = _check_public_key_file(Path(path))
if value.startswith(b"-----BEGIN"):
key = serialization.load_pem_public_key(value)
elif len(value) == 32:
key = public_cls.from_public_bytes(value)
else:
raise RuntimeError("public key must be PEM or raw 32-byte Ed25519 key")
if not isinstance(key, public_cls):
raise RuntimeError("public key is not Ed25519")
return cls(key, key_id=key_id)
def verify(self, payload: bytes, signature: str) -> bool:
try:
decoded = base64.b64decode(required(signature, "signature"), validate=True)
self.public_key.verify(decoded, bytes(payload))
return True
except Exception:
return False
@dataclass(frozen=True)
class RunProvenance:
provenance_id: str
run_id: str
issuer: str
tenant_id: str
task_id: str
agent_id: str
principal_id: str
allowed_paths: Sequence[str]
allowed_git_operations: Sequence[str]
issued_at: str
expires_at: str
policy_version: str
key_id: str
nonce: str
signature: str = ""
schema_version: str = PROVENANCE_VERSION
def __post_init__(self) -> None:
for name in (
"provenance_id",
"run_id",
"issuer",
"tenant_id",
"task_id",
"agent_id",
"principal_id",
"issued_at",
"expires_at",
"policy_version",
"key_id",
"nonce",
"schema_version",
):
object.__setattr__(self, name, required(getattr(self, name), name))
if self.schema_version != PROVENANCE_VERSION:
raise ValueError(f"unsupported schema_version: {self.schema_version}")
issued = parse_timestamp(self.issued_at)
expires = parse_timestamp(self.expires_at)
if expires <= issued:
raise ValueError("expires_at must be later than issued_at")
paths = tuple(sorted({ExecutionPath(item).value for item in self.allowed_paths}))
operations = tuple(
sorted({GitOperation(item).value for item in self.allowed_git_operations})
)
object.__setattr__(self, "allowed_paths", paths)
object.__setattr__(self, "allowed_git_operations", operations)
object.__setattr__(self, "signature", str(self.signature or "").strip())
def unsigned_dict(self) -> dict[str, Any]:
return {
"schema_version": self.schema_version,
"provenance_id": self.provenance_id,
"run_id": self.run_id,
"issuer": self.issuer,
"tenant_id": self.tenant_id,
"task_id": self.task_id,
"agent_id": self.agent_id,
"principal_id": self.principal_id,
"allowed_paths": list(self.allowed_paths),
"allowed_git_operations": list(self.allowed_git_operations),
"issued_at": self.issued_at,
"expires_at": self.expires_at,
"policy_version": self.policy_version,
"key_id": self.key_id,
"nonce": self.nonce,
}
def canonical_bytes(self) -> bytes:
return canonical_json_bytes(self.unsigned_dict())
def as_dict(self) -> dict[str, Any]:
value = self.unsigned_dict()
value["signature"] = self.signature
return value
@classmethod
def from_dict(cls, value: Mapping[str, Any]) -> "RunProvenance":
return cls(**dict(value))
class ProvenanceIssuer:
"""只由 Owner 或 AIOS Control Plane 持有的签发服务。"""
def __init__(self, signer: Signer, store: "SQLiteProvenanceStore") -> None:
self.signer = signer
self.store = store
def issue(
self,
*,
run_id: str,
issuer: str,
tenant_id: str,
task_id: str,
agent_id: str,
principal_id: str,
allowed_paths: Sequence[str] = tuple(REQUIRED_PRODUCTION_PATHS),
allowed_git_operations: Sequence[str] = tuple(item.value for item in GitOperation),
ttl_seconds: int = 3600,
policy_version: str = PRODUCTION_POLICY_VERSION,
provenance_id: str | None = None,
nonce: str | None = None,
now: datetime | None = None,
) -> RunProvenance:
if ttl_seconds <= 0:
raise ValueError("ttl_seconds must be positive")
normalized_paths = frozenset(ExecutionPath(item).value for item in allowed_paths)
if policy_version == PRODUCTION_POLICY_VERSION and normalized_paths != REQUIRED_PRODUCTION_PATHS:
missing = sorted(REQUIRED_PRODUCTION_PATHS - normalized_paths)
extra = sorted(normalized_paths - REQUIRED_PRODUCTION_PATHS)
raise ProvenanceDenied(
f"production provenance must cover exactly seven paths; missing={missing}, extra={extra}"
)
normalized_operations = tuple(
sorted({GitOperation(item).value for item in allowed_git_operations})
)
if ExecutionPath.GIT_OPERATION.value in normalized_paths and not normalized_operations:
raise ProvenanceDenied("git_operation path requires explicit allowed Git operations")
issued = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
unsigned = RunProvenance(
provenance_id=provenance_id or f"prov_{uuid.uuid4().hex}",
run_id=run_id,
issuer=issuer,
tenant_id=tenant_id,
task_id=task_id,
agent_id=agent_id,
principal_id=principal_id,
allowed_paths=tuple(normalized_paths),
allowed_git_operations=normalized_operations,
issued_at=issued.isoformat().replace("+00:00", "Z"),
expires_at=(issued + timedelta(seconds=ttl_seconds))
.isoformat()
.replace("+00:00", "Z"),
policy_version=policy_version,
key_id=self.signer.key_id,
nonce=nonce or uuid.uuid4().hex,
)
signed = replace(unsigned, signature=self.signer.sign(unsigned.canonical_bytes()))
self.store.save_issued(signed)
return signed
@dataclass(frozen=True)
class ProvenanceAuthorization:
authorization_id: str
provenance_id: str
run_id: str
tenant_id: str
task_id: str
agent_id: str
principal_id: str
execution_path: str
git_operation: str | None
request_digest: str
policy_version: str
key_id: str
public_key_fingerprint: str
authorized_at: str = field(default_factory=utc_now)
def as_dict(self) -> dict[str, Any]:
return asdict(self)
class SQLiteProvenanceStore:
"""持久化 Provenance、状态、授权使用和不可变事件。"""
def __init__(self, path: str | Path) -> None:
self.path = Path(path).resolve()
self.path.parent.mkdir(parents=True, exist_ok=True)
self._lock = threading.RLock()
self._connection = sqlite3.connect(
str(self.path), timeout=30.0, check_same_thread=False
)
self._connection.row_factory = sqlite3.Row
with self._lock:
self._connection.execute("PRAGMA journal_mode=WAL")
self._connection.execute("PRAGMA synchronous=FULL")
self._connection.execute("PRAGMA foreign_keys=ON")
self._connection.executescript(
"""
CREATE TABLE IF NOT EXISTS provenance_records (
provenance_id TEXT PRIMARY KEY,
payload_json TEXT NOT NULL,
status TEXT NOT NULL,
signature_verified INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS provenance_usage (
authorization_id TEXT PRIMARY KEY,
provenance_id TEXT NOT NULL,
execution_path TEXT NOT NULL,
git_operation TEXT NOT NULL DEFAULT '',
request_digest TEXT NOT NULL,
authorized_at TEXT NOT NULL,
receipt_json TEXT NOT NULL,
UNIQUE(provenance_id, execution_path, git_operation, request_digest),
FOREIGN KEY(provenance_id) REFERENCES provenance_records(provenance_id)
);
CREATE TABLE IF NOT EXISTS provenance_events (
event_id TEXT PRIMARY KEY,
provenance_id TEXT NOT NULL,
event_type TEXT NOT NULL,
details_json TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY(provenance_id) REFERENCES provenance_records(provenance_id)
);
CREATE INDEX IF NOT EXISTS idx_provenance_usage_id
ON provenance_usage(provenance_id, authorized_at);
CREATE INDEX IF NOT EXISTS idx_provenance_events_id
ON provenance_events(provenance_id, created_at);
"""
)
self._connection.commit()
def close(self) -> None:
with self._lock:
self._connection.close()
def _event(
self,
provenance_id: str,
event_type: str,
details: Mapping[str, Any] | None = None,
) -> None:
self._connection.execute(
"INSERT INTO provenance_events VALUES (?, ?, ?, ?, ?)",
(
f"pevt_{uuid.uuid4().hex}",
provenance_id,
event_type,
json.dumps(dict(details or {}), ensure_ascii=False, sort_keys=True),
utc_now(),
),
)
def save_issued(self, provenance: RunProvenance) -> None:
payload = json.dumps(
provenance.as_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
now = utc_now()
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
prior = self._connection.execute(
"SELECT payload_json FROM provenance_records WHERE provenance_id = ?",
(provenance.provenance_id,),
).fetchone()
if prior is not None:
if prior["payload_json"] != payload:
raise ProvenanceDenied("provenance_id reused with different payload")
self._connection.rollback()
return
self._connection.execute(
"INSERT INTO provenance_records VALUES (?, ?, ?, 0, ?, ?)",
(
provenance.provenance_id,
payload,
ProvenanceStatus.ISSUED.value,
now,
now,
),
)
self._event(provenance.provenance_id, "issued")
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def get(self, provenance_id: str) -> RunProvenance:
with self._lock:
row = self._connection.execute(
"SELECT payload_json FROM provenance_records WHERE provenance_id = ?",
(required(provenance_id, "provenance_id"),),
).fetchone()
if row is None:
raise ProvenanceNotFound("provenance not found")
value = json.loads(row["payload_json"])
if not isinstance(value, Mapping):
raise ProvenanceError("stored provenance payload is invalid")
return RunProvenance.from_dict(value)
def status(self, provenance_id: str) -> ProvenanceStatus:
with self._lock:
row = self._connection.execute(
"SELECT status FROM provenance_records WHERE provenance_id = ?",
(required(provenance_id, "provenance_id"),),
).fetchone()
if row is None:
raise ProvenanceNotFound("provenance not found")
return ProvenanceStatus(row["status"])
def mark_verified(self, provenance_id: str, *, fingerprint: str) -> None:
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
current = self.status(provenance_id)
if current in {ProvenanceStatus.REVOKED, ProvenanceStatus.EXPIRED}:
raise ProvenanceDenied(f"cannot verify provenance in state {current.value}")
next_status = (
ProvenanceStatus.USED.value
if current == ProvenanceStatus.USED
else ProvenanceStatus.VERIFIED.value
)
self._connection.execute(
"UPDATE provenance_records SET status = ?, signature_verified = 1, updated_at = ? WHERE provenance_id = ?",
(next_status, utc_now(), provenance_id),
)
self._event(
provenance_id,
"verified",
{"public_key_fingerprint": fingerprint},
)
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def mark_expired(self, provenance_id: str) -> None:
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
current = self.status(provenance_id)
if current == ProvenanceStatus.REVOKED:
self._connection.rollback()
return
self._connection.execute(
"UPDATE provenance_records SET status = ?, updated_at = ? WHERE provenance_id = ?",
(ProvenanceStatus.EXPIRED.value, utc_now(), provenance_id),
)
self._event(provenance_id, "expired")
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def revoke(self, provenance_id: str, *, reason: str, principal_id: str) -> None:
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
self.get(provenance_id)
self._connection.execute(
"UPDATE provenance_records SET status = ?, updated_at = ? WHERE provenance_id = ?",
(ProvenanceStatus.REVOKED.value, utc_now(), provenance_id),
)
self._event(
provenance_id,
"revoked",
{
"reason": required(reason, "reason"),
"principal_id": required(principal_id, "principal_id"),
},
)
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def record_authorization(self, authorization: ProvenanceAuthorization) -> None:
receipt = json.dumps(
authorization.as_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
with self._lock:
self._connection.execute("BEGIN IMMEDIATE")
try:
current = self.status(authorization.provenance_id)
if current in {ProvenanceStatus.REVOKED, ProvenanceStatus.EXPIRED}:
raise ProvenanceDenied(f"provenance state is {current.value}")
try:
self._connection.execute(
"INSERT INTO provenance_usage VALUES (?, ?, ?, ?, ?, ?, ?)",
(
authorization.authorization_id,
authorization.provenance_id,
authorization.execution_path,
authorization.git_operation or "",
authorization.request_digest,
authorization.authorized_at,
receipt,
),
)
except sqlite3.IntegrityError as exc:
raise ProvenanceReplay(
"identical provenance path/operation/request digest already used"
) from exc
self._connection.execute(
"UPDATE provenance_records SET status = ?, updated_at = ? WHERE provenance_id = ?",
(
ProvenanceStatus.USED.value,
authorization.authorized_at,
authorization.provenance_id,
),
)
self._event(
authorization.provenance_id,
"used",
{
"authorization_id": authorization.authorization_id,
"execution_path": authorization.execution_path,
"git_operation": authorization.git_operation,
"request_digest": authorization.request_digest,
},
)
self._connection.commit()
except Exception:
self._connection.rollback()
raise
def authorization_count(self, provenance_id: str) -> int:
with self._lock:
row = self._connection.execute(
"SELECT COUNT(*) AS total FROM provenance_usage WHERE provenance_id = ?",
(required(provenance_id, "provenance_id"),),
).fetchone()
return int(row["total"])
def events(self, provenance_id: str) -> list[dict[str, Any]]:
with self._lock:
rows = self._connection.execute(
"SELECT event_type, details_json, created_at FROM provenance_events WHERE provenance_id = ? ORDER BY created_at, event_id",
(required(provenance_id, "provenance_id"),),
).fetchall()
return [
{
"event_type": row["event_type"],
"details": json.loads(row["details_json"]),
"created_at": row["created_at"],
}
for row in rows
]
class ProvenanceVerifier:
"""验证 Owner 签名、上下文、路径、Git 操作、时效和重放。"""
def __init__(
self,
store: SQLiteProvenanceStore,
verifiers: Mapping[str, SignatureVerifier],
) -> None:
if not verifiers:
raise ValueError("at least one public key verifier is required")
self.store = store
self.verifiers = {required(key, "key_id"): value for key, value in verifiers.items()}
def authorize(
self,
*,
provenance_id: str,
run_id: str,
tenant_id: str,
task_id: str,
agent_id: str,
principal_id: str,
execution_path: str,
request_digest: str,
git_operation: str | None = None,
now: datetime | None = None,
) -> ProvenanceAuthorization:
provenance = self.store.get(provenance_id)
status = self.store.status(provenance.provenance_id)
if status == ProvenanceStatus.REVOKED:
raise ProvenanceRevoked("provenance is revoked")
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
if current >= parse_timestamp(provenance.expires_at):
self.store.mark_expired(provenance.provenance_id)
raise ProvenanceExpired("provenance is expired")
verifier = self.verifiers.get(provenance.key_id)
if verifier is None:
raise ProvenanceSignatureError("no trusted public key for provenance key_id")
if not provenance.signature or not verifier.verify(
provenance.canonical_bytes(), provenance.signature
):
raise ProvenanceSignatureError("invalid provenance signature")
expected = {
"run_id": required(run_id, "run_id"),
"tenant_id": required(tenant_id, "tenant_id"),
"task_id": required(task_id, "task_id"),
"agent_id": required(agent_id, "agent_id"),
"principal_id": required(principal_id, "principal_id"),
}
for field_name, expected_value in expected.items():
if getattr(provenance, field_name) != expected_value:
raise ProvenanceDenied(f"provenance {field_name} mismatch")
path = ExecutionPath(execution_path).value
if path not in provenance.allowed_paths:
raise ProvenanceDenied(f"execution path is not allowed: {path}")
operation: str | None = None
if path == ExecutionPath.GIT_OPERATION.value:
if git_operation is None:
raise ProvenanceDenied("git operation is required for git_operation path")
operation = GitOperation(git_operation).value
if operation not in provenance.allowed_git_operations:
raise ProvenanceDenied(f"Git operation is not allowed: {operation}")
elif git_operation is not None:
raise ProvenanceDenied("git_operation is only valid for git_operation path")
digest = _validate_digest(request_digest)
self.store.mark_verified(
provenance.provenance_id, fingerprint=verifier.fingerprint
)
authorization = ProvenanceAuthorization(
authorization_id=f"pauth_{uuid.uuid4().hex}",
provenance_id=provenance.provenance_id,
run_id=provenance.run_id,
tenant_id=provenance.tenant_id,
task_id=provenance.task_id,
agent_id=provenance.agent_id,
principal_id=provenance.principal_id,
execution_path=path,
git_operation=operation,
request_digest=digest,
policy_version=provenance.policy_version,
key_id=provenance.key_id,
public_key_fingerprint=verifier.fingerprint,
)
self.store.record_authorization(authorization)
return authorization
class ProvenanceControlledWriter:
"""七路径统一策略入口并持久写入授权回执。"""
def __init__(
self,
verifier: ProvenanceVerifier,
receipt_root: str | Path,
*,
writer_id: str = "aios-v2-provenance-writer",
) -> None:
self.verifier = verifier
self.receipt_root = Path(receipt_root).resolve()
self.receipt_root.mkdir(parents=True, exist_ok=True)
self.writer_id = required(writer_id, "writer_id")
self._lock = threading.RLock()
@staticmethod
def _component(value: str) -> str:
normalized = required(value, "path component")
if normalized in {".", ".."} or "/" in normalized or "\\" in normalized:
raise ProvenanceDenied("invalid receipt path component")
return normalized
def authorize(self, **values: Any) -> ProvenanceAuthorization:
authorization = self.verifier.authorize(**values)
tenant = self._component(authorization.tenant_id)
provenance_id = self._component(authorization.provenance_id)
target_dir = self.receipt_root / tenant / provenance_id
target = target_dir / f"{authorization.authorization_id}.json"
payload = authorization.as_dict()
payload["writer_id"] = self.writer_id
with self._lock:
target_dir.mkdir(parents=True, exist_ok=True)
temporary = target.with_suffix(f".tmp-{os.getpid()}")
temporary.write_text(
json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
encoding="utf-8",
)
os.chmod(temporary, 0o640)
os.replace(temporary, target)
return authorization
def authorize_code_change(self, **values: Any) -> ProvenanceAuthorization:
return self.authorize(execution_path=ExecutionPath.CODE_CHANGE.value, **values)
def authorize_git_operation(
self, *, git_operation: str, **values: Any
) -> ProvenanceAuthorization:
return self.authorize(
execution_path=ExecutionPath.GIT_OPERATION.value,
git_operation=git_operation,
**values,
)
def authorize_deployment(self, **values: Any) -> ProvenanceAuthorization:
return self.authorize(execution_path=ExecutionPath.DEPLOYMENT.value, **values)
def authorize_runtime_execute(self, **values: Any) -> ProvenanceAuthorization:
return self.authorize(execution_path=ExecutionPath.RUNTIME_EXECUTE.value, **values)
def authorize_evidence_write(self, **values: Any) -> ProvenanceAuthorization:
return self.authorize(execution_path=ExecutionPath.EVIDENCE_WRITE.value, **values)
def authorize_rollback(self, **values: Any) -> ProvenanceAuthorization:
return self.authorize(execution_path=ExecutionPath.ROLLBACK.value, **values)
def authorize_audit(self, **values: Any) -> ProvenanceAuthorization:
return self.authorize(execution_path=ExecutionPath.AUDIT.value, **values)