Files
aios/active-runtime/app/trace_tool_credential_governance.py
T

760 lines
32 KiB
Python

"""Scope-bound governance for traces, Tool/MCP calls, and credentials.
This module deliberately stores references and audit metadata only. Secret
material remains in the configured credential provider boundary.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from contextlib import contextmanager
import hashlib
import json
import re
import sqlite3
from pathlib import Path
from typing import Any, Iterable, Mapping
SCOPE_FIELDS = ("tenant_id", "user_id", "project_id", "conversation_id", "run_id")
PRIVILEGED_CAPABILITIES = frozenset({"*", "admin", "system.admin", "governance.admin"})
REFERENCE_RE = re.compile(r"^(systemd-credential|vault|secret-manager):[A-Za-z0-9][A-Za-z0-9._:/#-]{0,255}$")
IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,179}$")
class GovernanceError(RuntimeError):
def __init__(self, code: str, *, field: str | None = None) -> None:
super().__init__(code if field is None else f"{code}:{field}")
self.code = code
self.field = field
def _clean(value: Any) -> str | None:
if value is None:
return None
text = str(value).strip()
return text or None
def _scope_tuple(value: Iterable[str] | str | None) -> tuple[str, ...]:
if value is None:
return ()
if isinstance(value, str):
value = value.split(",")
return tuple(sorted({_clean(item) for item in value if _clean(item)}))
def _canonical(value: Any) -> str:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, default=str)
def _sha(value: Any) -> str:
return hashlib.sha256(_canonical(value).encode("utf-8")).hexdigest()
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def _identifier(value: Any, code: str) -> str:
text = _clean(value)
if text is None or IDENTIFIER_RE.fullmatch(text) is None:
raise GovernanceError(code)
return text
@dataclass(frozen=True)
class GovernanceScope:
"""Explicit resource scope; system/global resources never get fake tenants."""
scope_type: str
tenant_id: str | None = None
user_id: str | None = None
project_id: str | None = None
conversation_id: str | None = None
run_id: str | None = None
def __post_init__(self) -> None:
object.__setattr__(self, "scope_type", (_clean(self.scope_type) or "").lower())
for name in SCOPE_FIELDS:
object.__setattr__(self, name, _clean(getattr(self, name)))
if self.scope_type == "tenant":
if self.tenant_id is None:
raise GovernanceError("GOVERNANCE_TENANT_ID_REQUIRED")
elif self.scope_type in {"system", "global"}:
if any(getattr(self, name) is not None for name in SCOPE_FIELDS):
raise GovernanceError("GOVERNANCE_NON_TENANT_SCOPE_INVALID")
elif self.scope_type == "legacy":
if any(getattr(self, name) is not None for name in SCOPE_FIELDS):
raise GovernanceError("GOVERNANCE_LEGACY_SCOPE_INVALID")
else:
raise GovernanceError("GOVERNANCE_SCOPE_TYPE_INVALID")
@classmethod
def from_value(cls, value: Any) -> "GovernanceScope":
def get(name: str) -> Any:
return value.get(name) if isinstance(value, Mapping) else getattr(value, name, None)
declared = _clean(get("scope_type"))
tenant_id = _clean(get("tenant_id"))
return cls(
scope_type=declared or ("tenant" if tenant_id else "legacy"),
tenant_id=tenant_id,
user_id=get("user_id"),
project_id=get("project_id"),
conversation_id=get("conversation_id"),
run_id=get("run_id"),
)
def as_dict(self) -> dict[str, Any]:
return asdict(self)
@dataclass(frozen=True)
class GovernanceActor:
identity: str
scope: GovernanceScope
capabilities: tuple[str, ...] = ()
def __post_init__(self) -> None:
object.__setattr__(self, "identity", _identifier(self.identity, "GOVERNANCE_ACTOR_INVALID"))
object.__setattr__(self, "capabilities", _scope_tuple(self.capabilities))
@classmethod
def from_value(cls, value: Any) -> "GovernanceActor":
if isinstance(value, cls):
return value
if not isinstance(value, Mapping):
raise GovernanceError("GOVERNANCE_ACTOR_INVALID")
return cls(
identity=value.get("identity") or value.get("actor_id") or value.get("user_id"),
scope=GovernanceScope.from_value(value),
capabilities=_scope_tuple(value.get("capabilities") or value.get("capability_scope")),
)
@property
def privileged(self) -> bool:
return bool(set(self.capabilities) & PRIVILEGED_CAPABILITIES)
def authorize_scope(actor: GovernanceActor | Mapping[str, Any], resource: GovernanceScope | Mapping[str, Any]) -> None:
actor = GovernanceActor.from_value(actor)
resource = resource if isinstance(resource, GovernanceScope) else GovernanceScope.from_value(resource)
if actor.privileged:
return
if resource.scope_type in {"system", "global"}:
raise GovernanceError("GOVERNANCE_SYSTEM_SCOPE_AUTHZ_DENY")
if resource.scope_type == "legacy":
if actor.scope.scope_type != "legacy":
raise GovernanceError("GOVERNANCE_LEGACY_SCOPE_AUTHZ_DENY")
return
if actor.scope.scope_type != "tenant":
raise GovernanceError("GOVERNANCE_SCOPE_MISMATCH_DENY", field="scope_type")
for name in SCOPE_FIELDS:
expected = getattr(resource, name)
actual = getattr(actor.scope, name)
if expected is not None and expected != actual:
raise GovernanceError("GOVERNANCE_SCOPE_MISMATCH_DENY", field=name)
@dataclass(frozen=True)
class TraceContext:
trace_id: str
span_id: str
scope: GovernanceScope
parent_span_id: str | None = None
correlation_id: str | None = None
causation_id: str | None = None
task_id: str | None = None
claim_id: str | None = None
message_id: str | None = None
def __post_init__(self) -> None:
object.__setattr__(self, "trace_id", _identifier(self.trace_id, "TRACE_ID_INVALID"))
object.__setattr__(self, "span_id", _identifier(self.span_id, "TRACE_SPAN_ID_INVALID"))
for name in ("parent_span_id", "correlation_id", "causation_id", "task_id", "claim_id", "message_id"):
value = _clean(getattr(self, name))
if value is not None and IDENTIFIER_RE.fullmatch(value) is None:
raise GovernanceError("TRACE_LINEAGE_IDENTIFIER_INVALID", field=name)
object.__setattr__(self, name, value)
def child(self, *, span_id: str, actor: GovernanceActor | Mapping[str, Any], **lineage: Any) -> "TraceContext":
authorize_scope(actor, self.scope)
for name in ("trace_id", "task_id", "claim_id", "message_id", "correlation_id"):
supplied = _clean(lineage.get(name))
expected = getattr(self, name)
if supplied is not None and expected is not None and supplied != expected:
raise GovernanceError("TRACE_LINEAGE_CONTINUITY", field=name)
requested_scope = lineage.pop("scope", self.scope)
requested_scope = requested_scope if isinstance(requested_scope, GovernanceScope) else GovernanceScope.from_value(requested_scope)
if requested_scope != self.scope:
raise GovernanceError("TRACE_SCOPE_MISMATCH_DENY")
return TraceContext(
trace_id=self.trace_id,
span_id=span_id,
parent_span_id=self.span_id,
correlation_id=self.correlation_id,
causation_id=_clean(lineage.get("causation_id")) or self.span_id,
task_id=self.task_id,
claim_id=self.claim_id,
message_id=self.message_id,
scope=self.scope,
)
def as_dict(self) -> dict[str, Any]:
return {
"trace_id": self.trace_id,
"span_id": self.span_id,
"parent_span_id": self.parent_span_id,
"correlation_id": self.correlation_id,
"causation_id": self.causation_id,
"task_id": self.task_id,
"claim_id": self.claim_id,
"message_id": self.message_id,
**self.scope.as_dict(),
}
def bind_trace_lineage(
*,
actor: GovernanceActor | Mapping[str, Any],
task: Any,
message: Any,
trace_id: str,
span_id: str,
parent: TraceContext | None = None,
) -> TraceContext:
actor = GovernanceActor.from_value(actor)
task_scope = GovernanceScope.from_value(task)
message_scope = GovernanceScope.from_value(message)
if task_scope != message_scope:
raise GovernanceError("TRACE_SCOPE_MISMATCH_DENY")
authorize_scope(actor, task_scope)
def get(value: Any, name: str) -> Any:
return value.get(name) if isinstance(value, Mapping) else getattr(value, name, None)
task_id = _clean(get(task, "task_id"))
message_task = _clean(get(message, "task_id"))
if task_id and message_task and task_id != message_task:
raise GovernanceError("TRACE_LINEAGE_CONTINUITY", field="task_id")
context = TraceContext(
trace_id=trace_id,
span_id=span_id,
parent_span_id=parent.span_id if parent else None,
correlation_id=_clean(get(task, "correlation_id")) or _clean(get(message, "correlation_id")),
causation_id=_clean(get(message, "causation_id")) or (parent.span_id if parent else None),
task_id=task_id or message_task,
claim_id=_clean(get(task, "claim_id")) or _clean(get(message, "claim_id")),
message_id=_clean(get(message, "envelope_id")) or _clean(get(message, "id")),
scope=task_scope,
)
if parent is not None:
if parent.trace_id != context.trace_id or parent.scope != context.scope:
raise GovernanceError("TRACE_LINEAGE_CONTINUITY")
if parent.task_id and context.task_id and parent.task_id != context.task_id:
raise GovernanceError("TRACE_LINEAGE_CONTINUITY", field="task_id")
return context
@dataclass(frozen=True)
class ToolDescriptor:
name: str
kind: str
scope: GovernanceScope
required_capabilities: tuple[str, ...] = ()
credential_ref: str | None = None
def __post_init__(self) -> None:
object.__setattr__(self, "name", _identifier(self.name, "TOOL_NAME_INVALID"))
object.__setattr__(self, "kind", (_clean(self.kind) or "").lower())
if self.kind not in {"tool", "mcp"}:
raise GovernanceError("TOOL_KIND_INVALID")
object.__setattr__(self, "required_capabilities", _scope_tuple(self.required_capabilities))
if self.credential_ref is not None and REFERENCE_RE.fullmatch(self.credential_ref) is None:
raise GovernanceError("CREDENTIAL_SECRET_EXPOSURE_DENY")
@dataclass(frozen=True)
class CredentialGrant:
grant_id: str
credential_ref: str
scope: GovernanceScope
operations: tuple[str, ...]
task_id: str | None = None
tool_name: str | None = None
expires_at: str | None = None
def __post_init__(self) -> None:
object.__setattr__(self, "grant_id", _identifier(self.grant_id, "CREDENTIAL_GRANT_ID_INVALID"))
if REFERENCE_RE.fullmatch(self.credential_ref or "") is None:
raise GovernanceError("CREDENTIAL_SECRET_EXPOSURE_DENY")
object.__setattr__(self, "operations", _scope_tuple(self.operations))
if not self.operations:
raise GovernanceError("CREDENTIAL_OPERATIONS_REQUIRED")
if self.expires_at is not None:
parsed = datetime.fromisoformat(self.expires_at.replace("Z", "+00:00"))
if parsed.tzinfo is None:
raise GovernanceError("CREDENTIAL_EXPIRY_INVALID")
def authorize(
self,
actor: GovernanceActor | Mapping[str, Any],
*,
operation: str,
task_id: str | None = None,
tool_name: str | None = None,
now: datetime | None = None,
) -> str:
try:
authorize_scope(actor, self.scope)
except GovernanceError as exc:
raise GovernanceError("CREDENTIAL_SCOPE_DENY", field=exc.field) from exc
if operation not in self.operations:
raise GovernanceError("CREDENTIAL_SCOPE_DENY", field="operation")
if self.task_id is not None and self.task_id != _clean(task_id):
raise GovernanceError("CREDENTIAL_SCOPE_DENY", field="task_id")
if self.tool_name is not None and self.tool_name != _clean(tool_name):
raise GovernanceError("CREDENTIAL_SCOPE_DENY", field="tool_name")
if self.expires_at is not None:
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
expiry = datetime.fromisoformat(self.expires_at.replace("Z", "+00:00")).astimezone(timezone.utc)
if current >= expiry:
raise GovernanceError("CREDENTIAL_SCOPE_DENY", field="expires_at")
return self.credential_ref
def public(self) -> dict[str, Any]:
return {
"grant_id": self.grant_id,
"credential_ref": self.credential_ref,
"operations": list(self.operations),
"task_id": self.task_id,
"tool_name": self.tool_name,
"expires_at": self.expires_at,
**self.scope.as_dict(),
}
_SCHEMA = """
CREATE TABLE IF NOT EXISTS governance_schema_metadata (
singleton INTEGER PRIMARY KEY CHECK(singleton=1),
schema_version INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS governed_tools (
name TEXT PRIMARY KEY,
kind TEXT NOT NULL,
scope_json TEXT NOT NULL,
required_capabilities_json TEXT NOT NULL,
credential_ref TEXT,
descriptor_sha256 TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS governed_trace_spans (
trace_id TEXT NOT NULL,
span_id TEXT NOT NULL,
parent_span_id TEXT,
context_json TEXT NOT NULL,
context_sha256 TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(trace_id, span_id)
);
CREATE TABLE IF NOT EXISTS governed_credential_grants (
grant_id TEXT PRIMARY KEY,
grant_json TEXT NOT NULL,
grant_sha256 TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS governed_tool_invocations (
invocation_key TEXT PRIMARY KEY,
request_sha256 TEXT NOT NULL,
tool_name TEXT NOT NULL,
task_id TEXT NOT NULL,
claim_id TEXT NOT NULL,
trace_id TEXT NOT NULL,
span_id TEXT NOT NULL,
status TEXT NOT NULL,
receipt_sha256 TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS governance_audit_evidence (
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
evidence_id TEXT NOT NULL UNIQUE,
action TEXT NOT NULL,
actor_identity TEXT NOT NULL,
trace_id TEXT,
span_id TEXT,
resource_id TEXT NOT NULL,
outcome TEXT NOT NULL,
details_json TEXT NOT NULL,
previous_sha256 TEXT NOT NULL,
evidence_sha256 TEXT NOT NULL,
created_at TEXT NOT NULL
);
"""
class GovernanceStore:
def __init__(self, path: str | Path) -> None:
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
with self._connection() as connection:
has_metadata = connection.execute(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='governance_schema_metadata'"
).fetchone()
if has_metadata:
row = connection.execute(
"SELECT schema_version FROM governance_schema_metadata WHERE singleton=1"
).fetchone()
if row is not None and int(row[0]) != 1:
raise GovernanceError("GOVERNANCE_SCHEMA_VERSION_UNSUPPORTED")
connection.executescript(_SCHEMA)
row = connection.execute(
"SELECT schema_version FROM governance_schema_metadata WHERE singleton=1"
).fetchone()
if row is None:
connection.execute(
"INSERT INTO governance_schema_metadata(singleton,schema_version) VALUES(1,1)"
)
connection.commit()
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.path, timeout=30.0)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA busy_timeout=30000")
return connection
@contextmanager
def _connection(self):
connection = self._connect()
try:
yield connection
connection.commit()
except Exception:
connection.rollback()
raise
finally:
connection.close()
def _audit(
self,
connection: sqlite3.Connection,
*,
action: str,
actor: GovernanceActor,
resource_id: str,
outcome: str,
trace: TraceContext | None,
details: Mapping[str, Any],
) -> dict[str, Any]:
previous = connection.execute(
"SELECT evidence_sha256 FROM governance_audit_evidence ORDER BY sequence DESC LIMIT 1"
).fetchone()
previous_sha = str(previous[0]) if previous else ""
created_at = _now()
material = {
"action": action,
"actor_identity": actor.identity,
"trace_id": trace.trace_id if trace else None,
"span_id": trace.span_id if trace else None,
"resource_id": resource_id,
"outcome": outcome,
"details": dict(details),
"previous_sha256": previous_sha,
"created_at": created_at,
}
digest = _sha(material)
evidence_id = "gev-" + digest[:32]
connection.execute(
"""INSERT INTO governance_audit_evidence
(evidence_id,action,actor_identity,trace_id,span_id,resource_id,outcome,
details_json,previous_sha256,evidence_sha256,created_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
(
evidence_id, action, actor.identity, material["trace_id"], material["span_id"],
resource_id, outcome, _canonical(material["details"]), previous_sha, digest, created_at,
),
)
return {"evidence_id": evidence_id, "evidence_sha256": digest, **material}
def record_trace(self, trace: TraceContext, actor: GovernanceActor | Mapping[str, Any]) -> dict[str, Any]:
actor = GovernanceActor.from_value(actor)
authorize_scope(actor, trace.scope)
context = trace.as_dict()
digest = _sha(context)
with self._connection() as connection:
existing = connection.execute(
"SELECT context_sha256 FROM governed_trace_spans WHERE trace_id=? AND span_id=?",
(trace.trace_id, trace.span_id),
).fetchone()
if existing is not None and existing[0] != digest:
raise GovernanceError("TRACE_LINEAGE_CONFLICT")
if existing is not None:
return {"context": context, "context_sha256": digest, "idempotent": True}
if trace.parent_span_id:
parent = connection.execute(
"SELECT context_json FROM governed_trace_spans WHERE trace_id=? AND span_id=?",
(trace.trace_id, trace.parent_span_id),
).fetchone()
if parent is None:
raise GovernanceError("TRACE_LINEAGE_CONTINUITY", field="parent_span_id")
parent_context = json.loads(parent[0])
if GovernanceScope.from_value(parent_context) != trace.scope:
raise GovernanceError("TRACE_SCOPE_MISMATCH_DENY")
connection.execute(
"INSERT OR IGNORE INTO governed_trace_spans VALUES (?,?,?,?,?,?)",
(trace.trace_id, trace.span_id, trace.parent_span_id, _canonical(context), digest, _now()),
)
evidence = self._audit(
connection, action="TRACE_RECORDED", actor=actor,
resource_id=f"{trace.trace_id}:{trace.span_id}", outcome="ALLOW", trace=trace,
details={"context_sha256": digest},
)
return {"context": context, "context_sha256": digest, "evidence": evidence}
def register_tool(self, descriptor: ToolDescriptor, actor: GovernanceActor | Mapping[str, Any]) -> dict[str, Any]:
actor = GovernanceActor.from_value(actor)
if not actor.privileged:
raise GovernanceError("TOOL_MCP_AUTHZ_DENY")
public = {
"name": descriptor.name,
"kind": descriptor.kind,
"required_capabilities": list(descriptor.required_capabilities),
"credential_ref": descriptor.credential_ref,
**descriptor.scope.as_dict(),
}
digest = _sha(public)
with self._connection() as connection:
existing = connection.execute(
"SELECT descriptor_sha256 FROM governed_tools WHERE name=?", (descriptor.name,)
).fetchone()
if existing is not None and existing[0] != digest:
raise GovernanceError("TOOL_DESCRIPTOR_CONFLICT")
if existing is not None:
return {"descriptor": public, "descriptor_sha256": digest, "idempotent": True}
connection.execute(
"INSERT OR IGNORE INTO governed_tools VALUES (?,?,?,?,?,?)",
(descriptor.name, descriptor.kind, _canonical(descriptor.scope.as_dict()),
_canonical(descriptor.required_capabilities), descriptor.credential_ref, digest),
)
evidence = self._audit(
connection, action="TOOL_REGISTERED", actor=actor, resource_id=descriptor.name,
outcome="ALLOW", trace=None, details={"descriptor_sha256": digest},
)
return {"descriptor": public, "descriptor_sha256": digest, "evidence": evidence}
def authorize_tool(
self,
*,
actor: GovernanceActor | Mapping[str, Any],
tool_name: str,
trace: TraceContext,
requested_scope: GovernanceScope | Mapping[str, Any],
input_digest: str,
invocation_key: str,
) -> dict[str, Any]:
actor = GovernanceActor.from_value(actor)
invocation_key = _identifier(invocation_key, "TOOL_INVOCATION_KEY_INVALID")
if not re.fullmatch(r"[0-9a-f]{64}", input_digest):
raise GovernanceError("TOOL_INPUT_DIGEST_INVALID")
if trace.task_id is None or trace.claim_id is None:
raise GovernanceError("TOOL_MCP_AUTHZ_DENY", field="active_claim")
requested_scope = requested_scope if isinstance(requested_scope, GovernanceScope) else GovernanceScope.from_value(requested_scope)
if requested_scope != trace.scope:
raise GovernanceError("TOOL_SCOPE_PROPAGATION")
authorize_scope(actor, requested_scope)
with self._connection() as connection:
persisted_trace = connection.execute(
"SELECT context_sha256 FROM governed_trace_spans WHERE trace_id=? AND span_id=?",
(trace.trace_id, trace.span_id),
).fetchone()
if persisted_trace is None or persisted_trace[0] != _sha(trace.as_dict()):
raise GovernanceError("TOOL_MCP_AUTHZ_DENY", field="trace")
row = connection.execute("SELECT * FROM governed_tools WHERE name=?", (tool_name,)).fetchone()
if row is None:
raise GovernanceError("TOOL_MCP_AUTHZ_DENY")
descriptor_scope = GovernanceScope.from_value(json.loads(row["scope_json"]))
if descriptor_scope.scope_type == "tenant" and descriptor_scope != requested_scope:
raise GovernanceError("TOOL_MCP_AUTHZ_DENY")
required = set(json.loads(row["required_capabilities_json"]))
if not required.issubset(set(actor.capabilities)):
raise GovernanceError("TOOL_MCP_AUTHZ_DENY")
request_sha = _sha({
"tool_name": tool_name, "input_digest": input_digest,
"task_id": trace.task_id, "claim_id": trace.claim_id,
"trace_id": trace.trace_id, "span_id": trace.span_id,
"scope": requested_scope.as_dict(),
})
replay = connection.execute(
"SELECT * FROM governed_tool_invocations WHERE invocation_key=?",
(invocation_key,),
).fetchone()
if replay is not None:
if replay["request_sha256"] != request_sha:
raise GovernanceError("TOOL_INVOCATION_IDEMPOTENCY_CONFLICT")
if replay["status"] != "VERIFIED":
raise GovernanceError("TOOL_INVOCATION_RECONCILIATION_REQUIRED")
return {
"tool_name": tool_name, "trace_id": trace.trace_id,
"span_id": trace.span_id, "invocation_key": invocation_key,
"status": "VERIFIED", "receipt_sha256": replay["receipt_sha256"],
"idempotent": True,
}
created_at = _now()
connection.execute(
"""INSERT INTO governed_tool_invocations
(invocation_key,request_sha256,tool_name,task_id,claim_id,trace_id,span_id,
status,receipt_sha256,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?)""",
(invocation_key, request_sha, tool_name, trace.task_id, trace.claim_id,
trace.trace_id, trace.span_id, "AUTHORIZED", None, created_at, created_at),
)
evidence = self._audit(
connection, action="TOOL_AUTHORIZED", actor=actor, resource_id=tool_name,
outcome="ALLOW", trace=trace,
details={"input_sha256": input_digest, "scope_sha256": _sha(requested_scope.as_dict()),
"invocation_key": invocation_key},
)
return {
"tool_name": tool_name,
"trace_id": trace.trace_id,
"span_id": trace.span_id,
"invocation_key": invocation_key,
"status": "AUTHORIZED",
"credential_required": row["credential_ref"] is not None,
"evidence": evidence,
}
def record_tool_receipt(
self,
*,
invocation_key: str,
result_digest: str,
actor: GovernanceActor | Mapping[str, Any],
trace: TraceContext,
) -> dict[str, Any]:
actor = GovernanceActor.from_value(actor)
invocation_key = _identifier(invocation_key, "TOOL_INVOCATION_KEY_INVALID")
if not re.fullmatch(r"[0-9a-f]{64}", result_digest):
raise GovernanceError("TOOL_RECEIPT_DIGEST_INVALID")
authorize_scope(actor, trace.scope)
with self._connection() as connection:
row = connection.execute(
"SELECT * FROM governed_tool_invocations WHERE invocation_key=?",
(invocation_key,),
).fetchone()
if row is None or row["trace_id"] != trace.trace_id or row["span_id"] != trace.span_id:
raise GovernanceError("TOOL_RECEIPT_BINDING_INVALID")
if row["status"] == "VERIFIED":
if row["receipt_sha256"] != result_digest:
raise GovernanceError("TOOL_RECEIPT_CONFLICT")
return {"invocation_key": invocation_key, "status": "VERIFIED", "idempotent": True}
if row["status"] != "AUTHORIZED":
raise GovernanceError("TOOL_INVOCATION_RECONCILIATION_REQUIRED")
connection.execute(
"UPDATE governed_tool_invocations SET status='VERIFIED',receipt_sha256=?,updated_at=? "
"WHERE invocation_key=? AND status='AUTHORIZED'",
(result_digest, _now(), invocation_key),
)
evidence = self._audit(
connection, action="TOOL_RESULT_VERIFIED", actor=actor,
resource_id=row["tool_name"], outcome="ALLOW", trace=trace,
details={"invocation_key": invocation_key, "result_sha256": result_digest},
)
return {"invocation_key": invocation_key, "status": "VERIFIED", "evidence": evidence}
def store_grant(self, grant: CredentialGrant, actor: GovernanceActor | Mapping[str, Any]) -> dict[str, Any]:
actor = GovernanceActor.from_value(actor)
if not actor.privileged:
raise GovernanceError("CREDENTIAL_SCOPE_DENY")
public = grant.public()
digest = _sha(public)
with self._connection() as connection:
existing = connection.execute(
"SELECT grant_sha256 FROM governed_credential_grants WHERE grant_id=?", (grant.grant_id,)
).fetchone()
if existing is not None and existing[0] != digest:
raise GovernanceError("CREDENTIAL_GRANT_CONFLICT")
connection.execute(
"INSERT OR IGNORE INTO governed_credential_grants VALUES (?,?,?,?)",
(grant.grant_id, _canonical(public), digest, _now()),
)
evidence = self._audit(
connection, action="CREDENTIAL_GRANT_STORED", actor=actor,
resource_id=grant.grant_id, outcome="ALLOW", trace=None,
details={"grant_sha256": digest},
)
return {"grant": public, "grant_sha256": digest, "evidence": evidence}
def authorize_credential(
self,
*,
grant_id: str,
actor: GovernanceActor | Mapping[str, Any],
trace: TraceContext,
operation: str,
task_id: str | None,
tool_name: str | None,
now: datetime | None = None,
) -> dict[str, Any]:
actor = GovernanceActor.from_value(actor)
with self._connection() as connection:
row = connection.execute(
"SELECT grant_json FROM governed_credential_grants WHERE grant_id=?", (grant_id,)
).fetchone()
if row is None:
raise GovernanceError("CREDENTIAL_SCOPE_DENY")
raw = json.loads(row[0])
grant = CredentialGrant(
grant_id=raw["grant_id"], credential_ref=raw["credential_ref"],
scope=GovernanceScope.from_value(raw), operations=tuple(raw["operations"]),
task_id=raw.get("task_id"), tool_name=raw.get("tool_name"),
expires_at=raw.get("expires_at"),
)
if trace.scope != grant.scope:
raise GovernanceError("CREDENTIAL_SCOPE_DENY", field="trace_scope")
reference = grant.authorize(
actor, operation=operation, task_id=task_id, tool_name=tool_name, now=now
)
evidence = self._audit(
connection, action="CREDENTIAL_AUTHORIZED", actor=actor,
resource_id=grant_id, outcome="ALLOW", trace=trace,
details={"operation": operation, "task_id": task_id, "tool_name": tool_name,
"credential_ref_sha256": _sha(reference)},
)
return {"credential_ref": reference, "evidence": evidence}
def verify_audit_chain(self, required_actions: Iterable[str] = ()) -> bool:
ordered_actions: list[str] = []
previous = ""
with self._connection() as connection:
rows = connection.execute("SELECT * FROM governance_audit_evidence ORDER BY sequence").fetchall()
for row in rows:
details = json.loads(row["details_json"])
material = {
"action": row["action"], "actor_identity": row["actor_identity"],
"trace_id": row["trace_id"], "span_id": row["span_id"],
"resource_id": row["resource_id"], "outcome": row["outcome"],
"details": details, "previous_sha256": row["previous_sha256"],
"created_at": row["created_at"],
}
if row["previous_sha256"] != previous or row["evidence_sha256"] != _sha(material):
return False
previous = row["evidence_sha256"]
ordered_actions.append(row["action"])
position = 0
for required in required_actions:
try:
position = ordered_actions.index(required, position) + 1
except ValueError:
return False
return True
__all__ = [
"CredentialGrant", "GovernanceActor", "GovernanceError", "GovernanceScope",
"GovernanceStore", "ToolDescriptor", "TraceContext", "authorize_scope",
"bind_trace_lineage",
]