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.
- anna_app_core/__init__.py +60 -0
- anna_app_core/acl.py +151 -0
- anna_app_core/dispatcher.py +732 -0
- anna_app_core/errors.py +33 -0
- anna_app_core/manifest.py +209 -0
- anna_app_core/protocols.py +130 -0
- anna_app_core/runtime.py +73 -0
- anna_app_core/versions.py +9 -0
- anna_app_core-0.2.0.dist-info/METADATA +42 -0
- anna_app_core-0.2.0.dist-info/RECORD +11 -0
- anna_app_core-0.2.0.dist-info/WHEEL +4 -0
|
@@ -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"]
|