from dataclasses import dataclass, field from typing import Any, Literal from pydantic import BaseModel from wf_authoring import NodeSpec from wf_core import ReducerSpec from wf_core.runtime.ops.merges import ReducerDefinition from wf_platform.refs import CapabilityRef # Source kind is source-origin metadata, not a provider interface: # system=built-ins, connection=upstream/stateful providers, python=trusted local code. SourceKind = Literal["system", "connection", "python"] JsonObject = dict[str, Any] SOURCE_PREVIEW_LIMIT = 3 @dataclass(frozen=True, slots=True) class SourceVisibility: planner: bool = False client: bool = False admin_dashboard: bool = True @dataclass(frozen=True, slots=True) class SourcePermissions: safe_for_workflow: bool = False calls_upstream: bool = False mutates_config: bool = False mutates_auth: bool = False @dataclass(frozen=True, slots=True) class SourcePolicy: """Runtime/deployment policy for one capability source. Platform sources are process-provided built-ins such as `wf.std`. They are not account-bound and should not require deployment source bindings. """ platform: bool = False binding_required: bool = True def __post_init__(self) -> None: """Reject contradictory deployment policy combinations.""" if self.platform and self.binding_required: raise ValueError("platform sources cannot require bindings") class SourceVisibilitySnapshot(BaseModel): """Serializable visibility flags for one source inventory snapshot.""" planner: bool = False client: bool = False admin_dashboard: bool = True class SourcePermissionsSnapshot(BaseModel): """Serializable permission flags for one source inventory snapshot.""" safe_for_workflow: bool = False calls_upstream: bool = False mutates_config: bool = False mutates_auth: bool = False class SourcePolicySnapshot(BaseModel): """Serializable source policy used by CLI/API inventory responses.""" platform: bool = False binding_required: bool = True class NodeSpecInventory(BaseModel): """Serializable public contract for one executable node spec.""" name: str description: str | None = None outcomes: tuple[str, ...] input_schema: JsonObject output_schema: JsonObject is_async: bool accepts_context: bool class ReducerInventory(BaseModel): """Serializable public contract for one pure reducer.""" name: str ref: CapabilityRef description: str | None = None config_schema: JsonObject class SourceCapabilityInventory(BaseModel): """Serializable names owned by one source, grouped by capability kind.""" tools: tuple[str, ...] = () node_specs: tuple[str, ...] = () node_spec_details: tuple[NodeSpecInventory, ...] = () reducers: tuple[str, ...] = () reducer_details: tuple[ReducerInventory, ...] = () prompts: tuple[str, ...] = () resources: tuple[str, ...] = () class SourceCapabilityPreview(BaseModel): """Small sorted capability-name sample for compact source discovery.""" tools: tuple[str, ...] = () node_specs: tuple[str, ...] = () reducers: tuple[str, ...] = () prompts: tuple[str, ...] = () resources: tuple[str, ...] = () class SourceCapabilityHasMore(BaseModel): """Whether each compact source preview omitted owned capabilities.""" tools: bool = False node_specs: bool = False reducers: bool = False prompts: bool = False resources: bool = False class SourceStatus(BaseModel): """Serializable source metadata without the full owned-name inventory.""" id: str kind: SourceKind enabled: bool visibility: SourceVisibilitySnapshot permissions: SourcePermissionsSnapshot policy: SourcePolicySnapshot description: str | None = None tool_count: int node_spec_count: int reducer_count: int prompt_count: int resource_count: int preview: SourceCapabilityPreview has_more: SourceCapabilityHasMore class SourceInventory(SourceStatus): """Serializable source snapshot with the capability names it owns.""" capabilities: SourceCapabilityInventory @dataclass(slots=True) class CapabilityBuckets: tools: dict[str, Any] = field(default_factory=dict) node_specs: dict[str, NodeSpec[Any, Any]] = field(default_factory=dict) reducers: dict[str, ReducerSpec] = field(default_factory=dict) reducer_definitions: dict[str, ReducerDefinition] = field(default_factory=dict) prompts: dict[str, Any] = field(default_factory=dict) resources: dict[str, Any] = field(default_factory=dict) @dataclass(slots=True) class CapabilitySource: id: str kind: SourceKind capabilities: CapabilityBuckets = field(default_factory=CapabilityBuckets) enabled: bool = True visibility: SourceVisibility = field(default_factory=SourceVisibility) permissions: SourcePermissions = field(default_factory=SourcePermissions) policy: SourcePolicy = field(default_factory=SourcePolicy) description: str | None = None def as_status(self) -> SourceStatus: """Return serializable source metadata without owned capability names.""" return SourceStatus( id=self.id, kind=self.kind, enabled=self.enabled, visibility=SourceVisibilitySnapshot( planner=self.visibility.planner, client=self.visibility.client, admin_dashboard=self.visibility.admin_dashboard, ), permissions=SourcePermissionsSnapshot( safe_for_workflow=self.permissions.safe_for_workflow, calls_upstream=self.permissions.calls_upstream, mutates_config=self.permissions.mutates_config, mutates_auth=self.permissions.mutates_auth, ), policy=SourcePolicySnapshot( platform=self.policy.platform, binding_required=self.policy.binding_required, ), description=self.description, tool_count=len(self.capabilities.tools), node_spec_count=len(self.capabilities.node_specs), reducer_count=len(self.capabilities.reducers), prompt_count=len(self.capabilities.prompts), resource_count=len(self.capabilities.resources), preview=SourceCapabilityPreview( tools=_preview_names(self.capabilities.tools, SOURCE_PREVIEW_LIMIT), node_specs=_preview_names( self.capabilities.node_specs, SOURCE_PREVIEW_LIMIT, ), reducers=_preview_names( self.capabilities.reducers, SOURCE_PREVIEW_LIMIT ), prompts=_preview_names(self.capabilities.prompts, SOURCE_PREVIEW_LIMIT), resources=_preview_names( self.capabilities.resources, SOURCE_PREVIEW_LIMIT ), ), has_more=SourceCapabilityHasMore( tools=_has_more(self.capabilities.tools, SOURCE_PREVIEW_LIMIT), node_specs=_has_more( self.capabilities.node_specs, SOURCE_PREVIEW_LIMIT ), reducers=_has_more(self.capabilities.reducers, SOURCE_PREVIEW_LIMIT), prompts=_has_more(self.capabilities.prompts, SOURCE_PREVIEW_LIMIT), resources=_has_more(self.capabilities.resources, SOURCE_PREVIEW_LIMIT), ), ) def as_inventory(self) -> SourceInventory: """Return a serializable source snapshot plus owned capability names.""" return SourceInventory( **self.as_status().model_dump(), capabilities=SourceCapabilityInventory( tools=tuple(sorted(self.capabilities.tools)), node_specs=tuple(sorted(self.capabilities.node_specs)), node_spec_details=tuple( _node_spec_inventory(spec) for spec in sorted( self.capabilities.node_specs.values(), key=lambda spec: spec.name, ) ), reducers=tuple(sorted(self.capabilities.reducers)), reducer_details=tuple( ReducerInventory( name=reducer.name, ref=CapabilityRef.parse(reducer.name), description=reducer.description, config_schema=reducer.config_schema, ) for reducer in sorted( self.capabilities.reducers.values(), key=lambda reducer: reducer.name, ) ), prompts=tuple(sorted(self.capabilities.prompts)), resources=tuple(sorted(self.capabilities.resources)), ), ) def _node_spec_inventory(spec: NodeSpec[Any, Any]) -> NodeSpecInventory: """Project one executable node spec into a serializable public contract.""" node_def = spec.to_node_def() return NodeSpecInventory( name=spec.name, description=spec.description, outcomes=tuple(node_def.outcomes), input_schema=node_def.input_schema.model_dump(mode="json"), output_schema=node_def.output_schema.model_dump(mode="json"), is_async=spec.is_async, accepts_context=spec.accepts_context, ) def _preview_names(values: dict[str, Any], limit: int) -> tuple[str, ...]: """Return a tiny deterministic sample so list views stay inspectable.""" return tuple(sorted(values)[:limit]) def _has_more(values: dict[str, Any], limit: int) -> bool: """Return whether the compact preview omitted owned capability names.""" return len(values) > limit