anna-app-core 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ """anna-app-core: stable contract surface shared by matrix-nexus and
2
+ anna-app-runtime-local.
3
+
4
+ v0.2.0a1 — full extraction. The complete RPC dispatcher, manifest pydantic
5
+ schemas, and helper functions now live here; matrix-nexus re-exports them
6
+ for backward compatibility (see README.md).
7
+
8
+ v0.2.0a2 — docs catch-up only (README rewrite). No API changes.
9
+ """
10
+
11
+ from .acl import host_api_allows
12
+ from .dispatcher import dispatch
13
+ from .errors import HostRpcError, WindowError, WindowPermissionError
14
+ from .manifest import (
15
+ AppDevConfig,
16
+ AppManifest,
17
+ ManifestExecutaRef,
18
+ UiBundleSection,
19
+ UiHostApiSpec,
20
+ UiManifestSection,
21
+ UiSize,
22
+ UiViewSpec,
23
+ WindowViewMeta,
24
+ )
25
+ from .protocols import WindowStoreProtocol
26
+ from .runtime import scopes_from_manifest, select_view, view_meta
27
+ from .versions import DISPATCHER_VERSION, SDK_VERSION
28
+
29
+ __version__ = "0.2.0"
30
+
31
+ __all__ = [
32
+ # errors
33
+ "HostRpcError",
34
+ "WindowError",
35
+ "WindowPermissionError",
36
+ # protocols
37
+ "WindowStoreProtocol",
38
+ # acl
39
+ "host_api_allows",
40
+ # dispatcher
41
+ "dispatch",
42
+ # manifest schemas
43
+ "AppManifest",
44
+ "AppDevConfig",
45
+ "ManifestExecutaRef",
46
+ "UiBundleSection",
47
+ "UiHostApiSpec",
48
+ "UiManifestSection",
49
+ "UiSize",
50
+ "UiViewSpec",
51
+ "WindowViewMeta",
52
+ # runtime helpers
53
+ "select_view",
54
+ "scopes_from_manifest",
55
+ "view_meta",
56
+ # versions
57
+ "DISPATCHER_VERSION",
58
+ "SDK_VERSION",
59
+ "__version__",
60
+ ]
anna_app_core/acl.py ADDED
@@ -0,0 +1,151 @@
1
+ """Host-API ACL check.
2
+
3
+ Accepts either a typed ``AppManifest`` (preferred — production + harness)
4
+ or a plain ``dict`` (e.g. JSON-loaded manifest before pydantic validation).
5
+
6
+ Mirrors the pre-extraction implementation in
7
+ ``matrix-nexus/src/services/anna_app_runtime_service.py::host_api_allows``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Union
13
+
14
+ from .manifest import AppManifest
15
+
16
+
17
+ def _ui_host_api(
18
+ manifest: Union[AppManifest, dict[str, Any]],
19
+ ) -> dict[str, Any] | None:
20
+ """Return the ``ui.host_api`` mapping (ns -> list[method] | dict) or
21
+ ``None`` if the manifest has no UI section.
22
+
23
+ Note: most namespaces resolve to ``list[str]`` of allowed methods, but
24
+ ``agent`` is a structured dict (see ``AgentHostApiSpec``).
25
+ """
26
+ if isinstance(manifest, AppManifest):
27
+ if manifest.ui is None:
28
+ return None
29
+ api = manifest.ui.host_api
30
+ return {
31
+ "tools": api.tools,
32
+ "chat": api.chat,
33
+ "artifact": api.artifact,
34
+ "llm": api.llm,
35
+ "agent": api.agent.model_dump() if api.agent is not None else None,
36
+ "fs": api.fs,
37
+ "storage": api.storage,
38
+ "prefs": api.prefs,
39
+ "files": api.files,
40
+ "user_files": api.user_files,
41
+ }
42
+ ui = manifest.get("ui") if isinstance(manifest, dict) else None
43
+ if ui is None:
44
+ return None
45
+ return ui.get("host_api") or {}
46
+
47
+
48
+ def host_api_allows(
49
+ manifest: Union[AppManifest, dict[str, Any]],
50
+ ns: str,
51
+ method: str,
52
+ ) -> bool:
53
+ """Return ``True`` iff ``ns.method`` is granted by the manifest's ACL.
54
+
55
+ `window.*` is always granted. ``tools.invoke`` and ``tools.list`` are
56
+ granted whenever the ``tools`` set is non-empty (per-tool gating happens
57
+ inside the handler). ``agent.session.*`` is granted when at least one
58
+ submode (``auto`` / ``fixed``) is enabled — the handler validates the
59
+ selected submode against the structured spec.
60
+ """
61
+ if ns == "window":
62
+ return True
63
+ api_map = _ui_host_api(manifest)
64
+ if api_map is None:
65
+ return False
66
+ methods = api_map.get(ns)
67
+ if not methods:
68
+ return False
69
+ if ns == "agent":
70
+ # Structured form: ``{session: {auto: bool, fixed: {client_ids:[]}}, tools: [...]}``.
71
+ if not method.startswith("session."):
72
+ return False
73
+ sess = methods.get("session") if isinstance(methods, dict) else None
74
+ if not isinstance(sess, dict):
75
+ return False
76
+ auto_on = bool(sess.get("auto"))
77
+ fixed_on = bool(sess.get("fixed"))
78
+ return auto_on or fixed_on
79
+ if isinstance(methods, list):
80
+ if "*" in methods:
81
+ return True
82
+ if method in methods:
83
+ return True
84
+ if ns == "tools" and method in ("invoke", "list"):
85
+ return True
86
+ return False
87
+
88
+
89
+ # -----------------------------------------------------------------------------
90
+ # Scope capability — manifest.host_capabilities
91
+ # -----------------------------------------------------------------------------
92
+
93
+
94
+ _ACTIONS = ("read", "write")
95
+ _SCOPES = ("user", "app", "tool")
96
+
97
+
98
+ def _host_caps_list(
99
+ manifest: Union[AppManifest, dict[str, Any]],
100
+ ) -> list[str]:
101
+ """Normalise ``host_capabilities`` to a flat list[str]. Tolerates dict form."""
102
+ if isinstance(manifest, AppManifest):
103
+ raw: Any = list(manifest.host_capabilities or [])
104
+ else:
105
+ raw = manifest.get("host_capabilities") if isinstance(manifest, dict) else None
106
+ raw = raw or []
107
+ if isinstance(raw, dict):
108
+ return [str(k) for k, v in raw.items() if v]
109
+ if isinstance(raw, (list, tuple)):
110
+ return [str(c) for c in raw]
111
+ return []
112
+
113
+
114
+ def host_capability_allows_scope(
115
+ manifest: Union[AppManifest, dict[str, Any]],
116
+ scope: str,
117
+ action: str,
118
+ *,
119
+ cross_owner: bool = False,
120
+ ) -> bool:
121
+ """Return ``True`` iff the manifest grants APS access to ``(scope, action)``.
122
+
123
+ ``scope`` is one of ``user`` / ``app`` / ``tool``; ``action`` is ``read`` or
124
+ ``write``. ``cross_owner`` is ``True`` when the call targets a different
125
+ ``owner_id`` than the calling iframe (e.g. another app's ``scope=app``
126
+ bucket). Self-owned ``scope=app`` access stays granted by the legacy
127
+ ``aps.kv`` capability.
128
+
129
+ Capability strings recognised:
130
+
131
+ * ``aps.scope.admin`` → grants everything.
132
+ * ``aps.scope.<scope>.<action>`` → exact match.
133
+ * ``aps.kv`` → ``scope=app`` self-owned only.
134
+ """
135
+ if scope not in _SCOPES:
136
+ return False
137
+ if action not in _ACTIONS:
138
+ return False
139
+ caps = set(_host_caps_list(manifest))
140
+ if "aps.scope.admin" in caps:
141
+ return True
142
+ fine = f"aps.scope.{scope}.{action}"
143
+ if fine in caps:
144
+ return True
145
+ # Legacy: ``aps.kv`` covers self-owned scope=app reads & writes.
146
+ if scope == "app" and not cross_owner and ("aps.kv" in caps or "aps" in caps):
147
+ return True
148
+ return False
149
+
150
+
151
+ __all__ = ["host_api_allows", "host_capability_allows_scope"]