AbteeXAILabs commited on
Commit
8ab9393
·
verified ·
1 Parent(s): 63ba471

Publish expanded LumynaX product platform package

Browse files
README.md CHANGED
@@ -37,6 +37,9 @@ python quickstart.py
37
  ```bash
38
  python -m sovereigncode.cli evaluate --capsule examples/capsule.restricted-nz-code.json --request examples/request.allowed-local-edit.json
39
  python -m sovereigncode.cli plan-turn --capsule examples/capsule.restricted-nz-code.json --request examples/request.allowed-local-edit.json --route-request examples/request.code-restricted.json --registry configs/lumynax_model_registry.json
 
 
 
40
  python -m sovereigncode.cli ui --smoke
41
  python -m sovereigncode.cli ui --port 8788 --open
42
  ```
@@ -97,6 +100,30 @@ py -3 -m tinyluminax.products.sovereigncode.cli plan-turn \
97
  --registry products/lumynax-marama-route/configs/lumynax_model_registry.json
98
  ```
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  Denied training example:
101
 
102
  ```bash
@@ -128,6 +155,8 @@ py -3 -m tinyluminax.products.sovereigncode.cli ui --smoke
128
  | LumynaX Runtime Adapter | Routes model calls to local GGUF, local API, or approved LumynaX model endpoints. |
129
  | Audit Ledger | Stores decision records, prompt/output hashes, file diffs, and approval metadata. |
130
  | Operator Console | Shows the plan, policy decision, diff, tests, and approval gate before external effects. |
 
 
131
 
132
  ## New Zealand Launch Shape
133
 
@@ -151,6 +180,9 @@ py -3 -m tinyluminax.products.sovereigncode.cli ui --smoke
151
  | Personal profile capsule | `examples/capsule.personal-sovereignty-profile.json` |
152
  | Personal-memory request | `examples/request.personal-memory-read.json` |
153
  | Browser operator console | `python -m tinyluminax.products.sovereigncode.cli ui` |
 
 
 
154
  | Product blueprint | `product_blueprint.md` |
155
 
156
  ## Source Grounding
@@ -159,7 +191,7 @@ The sovereignty model is inspired by the Data Capsule pattern described in the S
159
 
160
  ## Stage
161
 
162
- This is an executable platform scaffold, not the final commercial application. The policy engine, router integration, CLI package, and browser operator console are working now; the tool broker, terminal loop, and full autonomous agent loop are the next implementation stage.
163
 
164
 
165
  # AbteeX SovereignCode Product Blueprint
 
37
  ```bash
38
  python -m sovereigncode.cli evaluate --capsule examples/capsule.restricted-nz-code.json --request examples/request.allowed-local-edit.json
39
  python -m sovereigncode.cli plan-turn --capsule examples/capsule.restricted-nz-code.json --request examples/request.allowed-local-edit.json --route-request examples/request.code-restricted.json --registry configs/lumynax_model_registry.json
40
+ python -m sovereigncode.cli policy-matrix --capsule examples/capsule.restricted-nz-code.json --request examples/request.allowed-local-edit.json
41
+ python -m sovereigncode.cli tool-check --capsule examples/capsule.restricted-nz-code.json --request examples/request.allowed-local-edit.json --tool-name workspace_reader --action read_context
42
+ python -m sovereigncode.cli opencode-config
43
  python -m sovereigncode.cli ui --smoke
44
  python -m sovereigncode.cli ui --port 8788 --open
45
  ```
 
100
  --registry products/lumynax-marama-route/configs/lumynax_model_registry.json
101
  ```
102
 
103
+ Run the policy/tool matrix:
104
+
105
+ ```bash
106
+ py -3 -m tinyluminax.products.sovereigncode.cli policy-matrix \
107
+ --capsule products/abx-sovereigncode/examples/capsule.restricted-nz-code.json \
108
+ --request products/abx-sovereigncode/examples/request.allowed-local-edit.json
109
+ ```
110
+
111
+ Check one tool before execution:
112
+
113
+ ```bash
114
+ py -3 -m tinyluminax.products.sovereigncode.cli tool-check \
115
+ --capsule products/abx-sovereigncode/examples/capsule.restricted-nz-code.json \
116
+ --request products/abx-sovereigncode/examples/request.allowed-local-edit.json \
117
+ --tool-name workspace_reader \
118
+ --action read_context
119
+ ```
120
+
121
+ Emit an OpenCode-compatible workspace config:
122
+
123
+ ```bash
124
+ py -3 -m tinyluminax.products.sovereigncode.cli opencode-config
125
+ ```
126
+
127
  Denied training example:
128
 
129
  ```bash
 
155
  | LumynaX Runtime Adapter | Routes model calls to local GGUF, local API, or approved LumynaX model endpoints. |
156
  | Audit Ledger | Stores decision records, prompt/output hashes, file diffs, and approval metadata. |
157
  | Operator Console | Shows the plan, policy decision, diff, tests, and approval gate before external effects. |
158
+ | Policy Matrix | Evaluates common tool/action scenarios against the same Data Capsule. |
159
+ | Provider Exporter | Emits OpenCode-compatible workspace config pointing through MaramaRoute. |
160
 
161
  ## New Zealand Launch Shape
162
 
 
180
  | Personal profile capsule | `examples/capsule.personal-sovereignty-profile.json` |
181
  | Personal-memory request | `examples/request.personal-memory-read.json` |
182
  | Browser operator console | `python -m tinyluminax.products.sovereigncode.cli ui` |
183
+ | Policy/tool matrix | `python -m tinyluminax.products.sovereigncode.cli policy-matrix` |
184
+ | Tool gate check | `python -m tinyluminax.products.sovereigncode.cli tool-check` |
185
+ | OpenCode workspace export | `python -m tinyluminax.products.sovereigncode.cli opencode-config` |
186
  | Product blueprint | `product_blueprint.md` |
187
 
188
  ## Source Grounding
 
191
 
192
  ## Stage
193
 
194
+ This is an executable platform scaffold, not the final commercial application. The policy engine, router integration, CLI package, policy matrix, tool gate checks, capsule summaries, OpenCode config export, operator checklist, and browser operator console are working now; the live tool broker, terminal loop, and full autonomous agent loop are the next implementation stage.
195
 
196
 
197
  # AbteeX SovereignCode Product Blueprint
SMOKE_TESTS.md CHANGED
@@ -6,6 +6,8 @@ Run from the package root:
6
  pip install -e .
7
  python quickstart.py
8
  python -m sovereigncode.cli ui --smoke
 
 
9
  python -m sovereigncode.cli evaluate --capsule examples/capsule.personal-sovereignty-profile.json --request examples/request.personal-memory-read.json
10
  ```
11
 
 
6
  pip install -e .
7
  python quickstart.py
8
  python -m sovereigncode.cli ui --smoke
9
+ python -m sovereigncode.cli policy-matrix --capsule examples/capsule.restricted-nz-code.json --request examples/request.allowed-local-edit.json
10
+ python -m sovereigncode.cli tool-check --capsule examples/capsule.restricted-nz-code.json --request examples/request.allowed-local-edit.json --tool-name workspace_reader --action read_context
11
  python -m sovereigncode.cli evaluate --capsule examples/capsule.personal-sovereignty-profile.json --request examples/request.personal-memory-read.json
12
  ```
13
 
marama_route/__init__.py CHANGED
@@ -6,6 +6,13 @@ from .gateway import (
6
  route_chat_payload,
7
  routing_request_from_chat_payload,
8
  )
 
 
 
 
 
 
 
9
  from .registry import ModelEndpoint, RoutingRequest, load_model_registry
10
  from .router import RouteDecision, SovereignModelRouter
11
  from .ui import smoke_ui as smoke_ui
@@ -17,8 +24,13 @@ __all__ = [
17
  "SovereignModelRouter",
18
  "build_chat_route_response",
19
  "build_models_response",
 
 
 
 
20
  "load_model_registry",
21
  "route_chat_payload",
 
22
  "routing_request_from_chat_payload",
23
  "smoke_ui",
24
  ]
 
6
  route_chat_payload,
7
  routing_request_from_chat_payload,
8
  )
9
+ from .platform import (
10
+ build_opencode_provider_config,
11
+ build_registry_analytics,
12
+ catalog_models,
13
+ compare_models,
14
+ route_scenario_matrix,
15
+ )
16
  from .registry import ModelEndpoint, RoutingRequest, load_model_registry
17
  from .router import RouteDecision, SovereignModelRouter
18
  from .ui import smoke_ui as smoke_ui
 
24
  "SovereignModelRouter",
25
  "build_chat_route_response",
26
  "build_models_response",
27
+ "build_opencode_provider_config",
28
+ "build_registry_analytics",
29
+ "catalog_models",
30
+ "compare_models",
31
  "load_model_registry",
32
  "route_chat_payload",
33
+ "route_scenario_matrix",
34
  "routing_request_from_chat_payload",
35
  "smoke_ui",
36
  ]
marama_route/cli.py CHANGED
@@ -7,6 +7,13 @@ from pathlib import Path
7
  from typing import Any
8
 
9
  from .gateway import build_models_response, route_chat_payload
 
 
 
 
 
 
 
10
  from .registry import RoutingRequest, load_model_registry
11
  from .router import SovereignModelRouter
12
 
@@ -41,6 +48,55 @@ def _chat_dry_run(args: argparse.Namespace) -> int:
41
  return 0 if selected is not None else 2
42
 
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  def _ui(args: argparse.Namespace) -> int:
45
  from .ui import run_ui
46
 
@@ -79,6 +135,52 @@ def build_parser() -> argparse.ArgumentParser:
79
  chat.add_argument("--request", type=Path, required=True, help="OpenAI chat request JSON.")
80
  chat.set_defaults(handler=_chat_dry_run)
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  ui = subparsers.add_parser(
83
  "ui",
84
  help="Launch the local MaramaRoute browser platform.",
 
7
  from typing import Any
8
 
9
  from .gateway import build_models_response, route_chat_payload
10
+ from .platform import (
11
+ build_opencode_provider_config,
12
+ build_registry_analytics,
13
+ catalog_models,
14
+ compare_models,
15
+ route_scenario_matrix,
16
+ )
17
  from .registry import RoutingRequest, load_model_registry
18
  from .router import SovereignModelRouter
19
 
 
48
  return 0 if selected is not None else 2
49
 
50
 
51
+ def _catalog(args: argparse.Namespace) -> int:
52
+ models = load_model_registry(args.registry)
53
+ result = catalog_models(
54
+ models,
55
+ {
56
+ "search": args.search,
57
+ "task_type": args.task,
58
+ "runtime": args.runtime,
59
+ "modality": args.modality,
60
+ "jurisdiction": args.jurisdiction,
61
+ "min_context_tokens": args.min_context_tokens,
62
+ "requires_json": args.requires_json,
63
+ "requires_tools": args.requires_tools,
64
+ "requires_local": args.requires_local,
65
+ "limit": args.limit,
66
+ },
67
+ )
68
+ print(json.dumps(result, indent=2, sort_keys=True))
69
+ return 0
70
+
71
+
72
+ def _compare(args: argparse.Namespace) -> int:
73
+ models = load_model_registry(args.registry)
74
+ model_ids = [item.strip() for value in args.model for item in value.split(",") if item.strip()]
75
+ request = _load_json_mapping(args.request) if args.request else None
76
+ result = compare_models(models, model_ids, request)
77
+ print(json.dumps(result, indent=2, sort_keys=True))
78
+ return 0 if result["ok"] else 2
79
+
80
+
81
+ def _matrix(args: argparse.Namespace) -> int:
82
+ models = load_model_registry(args.registry)
83
+ result = route_scenario_matrix(models)
84
+ print(json.dumps(result, indent=2, sort_keys=True))
85
+ return 0 if result["ok"] or args.allow_blocked_exit_zero else 2
86
+
87
+
88
+ def _analytics(args: argparse.Namespace) -> int:
89
+ print(json.dumps(build_registry_analytics(load_model_registry(args.registry)), indent=2, sort_keys=True))
90
+ return 0
91
+
92
+
93
+ def _opencode_config(args: argparse.Namespace) -> int:
94
+ models = load_model_registry(args.registry)
95
+ result = build_opencode_provider_config(models, base_url=args.base_url)
96
+ print(json.dumps(result, indent=2, sort_keys=True))
97
+ return 0
98
+
99
+
100
  def _ui(args: argparse.Namespace) -> int:
101
  from .ui import run_ui
102
 
 
135
  chat.add_argument("--request", type=Path, required=True, help="OpenAI chat request JSON.")
136
  chat.set_defaults(handler=_chat_dry_run)
137
 
138
+ catalog = subparsers.add_parser(
139
+ "catalog",
140
+ help="Search and filter the MaramaRoute model catalog.",
141
+ )
142
+ catalog.add_argument("--registry", type=Path, required=True, help="MaramaRoute model registry JSON.")
143
+ catalog.add_argument("--search", default="")
144
+ catalog.add_argument("--task", default="")
145
+ catalog.add_argument("--runtime", default="")
146
+ catalog.add_argument("--modality", default="")
147
+ catalog.add_argument("--jurisdiction", default="NZ")
148
+ catalog.add_argument("--min-context-tokens", type=int, default=0)
149
+ catalog.add_argument("--requires-json", action=argparse.BooleanOptionalAction, default=False)
150
+ catalog.add_argument("--requires-tools", action=argparse.BooleanOptionalAction, default=False)
151
+ catalog.add_argument("--requires-local", action=argparse.BooleanOptionalAction, default=False)
152
+ catalog.add_argument("--limit", type=int, default=25)
153
+ catalog.set_defaults(handler=_catalog)
154
+
155
+ compare = subparsers.add_parser(
156
+ "compare",
157
+ help="Compare routed fit for selected model ids.",
158
+ )
159
+ compare.add_argument("--registry", type=Path, required=True, help="MaramaRoute model registry JSON.")
160
+ compare.add_argument("--model", action="append", required=True, help="Model id, repeatable or comma-separated.")
161
+ compare.add_argument("--request", type=Path, default=None, help="Optional routing request JSON.")
162
+ compare.set_defaults(handler=_compare)
163
+
164
+ matrix = subparsers.add_parser(
165
+ "matrix",
166
+ help="Run the built-in sovereign routing scenario matrix.",
167
+ )
168
+ matrix.add_argument("--registry", type=Path, required=True, help="MaramaRoute model registry JSON.")
169
+ matrix.add_argument("--allow-blocked-exit-zero", action=argparse.BooleanOptionalAction, default=False)
170
+ matrix.set_defaults(handler=_matrix)
171
+
172
+ analytics = subparsers.add_parser("analytics", help="Summarise registry coverage.")
173
+ analytics.add_argument("--registry", type=Path, required=True, help="MaramaRoute model registry JSON.")
174
+ analytics.set_defaults(handler=_analytics)
175
+
176
+ opencode = subparsers.add_parser(
177
+ "opencode-config",
178
+ help="Emit an OpenCode-compatible MaramaRoute provider config.",
179
+ )
180
+ opencode.add_argument("--registry", type=Path, required=True, help="MaramaRoute model registry JSON.")
181
+ opencode.add_argument("--base-url", default="http://127.0.0.1:8787/v1")
182
+ opencode.set_defaults(handler=_opencode_config)
183
+
184
  ui = subparsers.add_parser(
185
  "ui",
186
  help="Launch the local MaramaRoute browser platform.",
marama_route/platform.py ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from collections import Counter
6
+ from typing import Any
7
+
8
+ from .gateway import build_models_response, route_chat_payload
9
+ from .registry import ModelEndpoint, RoutingRequest
10
+ from .router import SovereignModelRouter
11
+
12
+ DEFAULT_ROUTE_SCENARIOS: tuple[dict[str, Any], ...] = (
13
+ {
14
+ "name": "Restricted NZ code",
15
+ "prompt": "Refactor a private New Zealand Python service and return a JSON diff plan.",
16
+ "task_type": "code",
17
+ "modalities": ["text"],
18
+ "jurisdiction": "NZ",
19
+ "data_sensitivity": "restricted",
20
+ "min_context_tokens": 4096,
21
+ "requires_local": True,
22
+ "requires_json": True,
23
+ "requires_tools": False,
24
+ "max_fallbacks": 3,
25
+ },
26
+ {
27
+ "name": "Personal memory",
28
+ "prompt": "Summarise local operator preferences without exposing raw personal notes.",
29
+ "task_type": "general",
30
+ "modalities": ["text"],
31
+ "jurisdiction": "NZ",
32
+ "data_sensitivity": "personal",
33
+ "min_context_tokens": 4096,
34
+ "requires_local": True,
35
+ "requires_json": False,
36
+ "requires_tools": False,
37
+ "max_fallbacks": 3,
38
+ },
39
+ {
40
+ "name": "Vision document",
41
+ "prompt": "Read a scanned table image and extract structured rows.",
42
+ "task_type": "multimodal",
43
+ "modalities": ["text", "image"],
44
+ "jurisdiction": "NZ",
45
+ "data_sensitivity": "internal",
46
+ "min_context_tokens": 4096,
47
+ "requires_local": False,
48
+ "requires_json": True,
49
+ "requires_tools": False,
50
+ "max_fallbacks": 3,
51
+ },
52
+ {
53
+ "name": "Reasoning brief",
54
+ "prompt": "Reason through a procurement risk register and produce a concise decision memo.",
55
+ "task_type": "reasoning",
56
+ "modalities": ["text"],
57
+ "jurisdiction": "NZ",
58
+ "data_sensitivity": "internal",
59
+ "min_context_tokens": 8192,
60
+ "requires_local": True,
61
+ "requires_json": False,
62
+ "requires_tools": False,
63
+ "max_fallbacks": 3,
64
+ },
65
+ )
66
+
67
+
68
+ def build_registry_analytics(models: tuple[ModelEndpoint, ...]) -> dict[str, Any]:
69
+ runtimes = Counter(model.runtime for model in models)
70
+ families = Counter(model.family for model in models)
71
+ modalities = Counter(modality for model in models for modality in model.modalities)
72
+ tiers = Counter(str(model.sovereignty_tier) for model in models)
73
+ resident_nz = sum(1 for model in models if "NZ" in model.residency)
74
+ json_ready = sum(1 for model in models if model.supports_json)
75
+ tool_ready = sum(1 for model in models if model.supports_tools)
76
+ local_runtimes = sum(1 for model in models if _is_local_runtime(model.runtime))
77
+ context_values = [model.context_tokens for model in models]
78
+ return {
79
+ "model_count": len(models),
80
+ "resident_nz": resident_nz,
81
+ "local_runtimes": local_runtimes,
82
+ "json_ready": json_ready,
83
+ "tool_ready": tool_ready,
84
+ "max_context_tokens": max(context_values) if context_values else 0,
85
+ "avg_context_tokens": round(sum(context_values) / len(context_values), 2) if context_values else 0,
86
+ "runtimes": dict(sorted(runtimes.items())),
87
+ "families": dict(sorted(families.items())),
88
+ "modalities": dict(sorted(modalities.items())),
89
+ "sovereignty_tiers": dict(sorted(tiers.items())),
90
+ "top_models": [model_summary(model) for model in _top_models(models, limit=8)],
91
+ }
92
+
93
+
94
+ def catalog_models(
95
+ models: tuple[ModelEndpoint, ...],
96
+ filters: dict[str, Any] | None = None,
97
+ ) -> dict[str, Any]:
98
+ filters = filters or {}
99
+ search = str(filters.get("search") or "").strip().lower()
100
+ runtime = str(filters.get("runtime") or "").strip().lower()
101
+ family = str(filters.get("family") or "").strip().lower()
102
+ modality = str(filters.get("modality") or "").strip().lower()
103
+ task_type = str(filters.get("task_type") or "").strip().lower()
104
+ jurisdiction = str(filters.get("jurisdiction") or "").strip().upper()
105
+ min_context = int(filters.get("min_context_tokens") or 0)
106
+ limit = int(filters.get("limit") or 50)
107
+ requires_json = bool(filters.get("requires_json", False))
108
+ requires_tools = bool(filters.get("requires_tools", False))
109
+ requires_local = bool(filters.get("requires_local", False))
110
+
111
+ filtered: list[ModelEndpoint] = []
112
+ for model in models:
113
+ haystack = " ".join(
114
+ (
115
+ model.model_id,
116
+ model.repo_id,
117
+ model.family,
118
+ model.runtime,
119
+ " ".join(model.tags),
120
+ ),
121
+ ).lower()
122
+ if search and search not in haystack:
123
+ continue
124
+ if runtime and model.runtime.lower() != runtime:
125
+ continue
126
+ if family and model.family.lower() != family:
127
+ continue
128
+ if modality and modality not in {item.lower() for item in model.modalities}:
129
+ continue
130
+ if task_type and not _matches_task(model, task_type):
131
+ continue
132
+ if jurisdiction and jurisdiction not in model.residency:
133
+ continue
134
+ if min_context and model.context_tokens < min_context:
135
+ continue
136
+ if requires_json and not model.supports_json:
137
+ continue
138
+ if requires_tools and not model.supports_tools:
139
+ continue
140
+ if requires_local and not _is_local_runtime(model.runtime):
141
+ continue
142
+ filtered.append(model)
143
+
144
+ ranked = sorted(filtered, key=_catalog_sort_key, reverse=True)
145
+ return {
146
+ "ok": True,
147
+ "count": len(ranked),
148
+ "filters": filters,
149
+ "models": [model_summary(model) for model in ranked[:limit]],
150
+ }
151
+
152
+
153
+ def compare_models(
154
+ models: tuple[ModelEndpoint, ...],
155
+ model_ids: list[str],
156
+ request_payload: dict[str, Any] | None = None,
157
+ ) -> dict[str, Any]:
158
+ index = {model.model_id: model for model in models}
159
+ selected = [index[model_id] for model_id in model_ids if model_id in index]
160
+ missing = [model_id for model_id in model_ids if model_id not in index]
161
+ request = RoutingRequest.from_payload(request_payload or DEFAULT_ROUTE_SCENARIOS[0])
162
+ route_scores = SovereignModelRouter(tuple(selected)).route(request).scores if selected else {}
163
+ rows = []
164
+ for model in selected:
165
+ row = model_summary(model)
166
+ row["route_score"] = route_scores.get(model.model_id)
167
+ row["operator_score"] = _operator_score(model)
168
+ rows.append(row)
169
+ winner = max(rows, key=lambda item: (item.get("route_score") or -1, item["operator_score"]), default=None)
170
+ return {
171
+ "ok": bool(rows),
172
+ "missing": missing,
173
+ "request": request.to_dict(),
174
+ "winner": winner,
175
+ "models": rows,
176
+ }
177
+
178
+
179
+ def route_scenario_matrix(
180
+ models: tuple[ModelEndpoint, ...],
181
+ scenarios: list[dict[str, Any]] | None = None,
182
+ ) -> dict[str, Any]:
183
+ router = SovereignModelRouter(models)
184
+ rows = []
185
+ for scenario in scenarios or [dict(item) for item in DEFAULT_ROUTE_SCENARIOS]:
186
+ request = RoutingRequest.from_payload(scenario)
187
+ decision = router.route(request)
188
+ selected = decision.selected_model
189
+ rows.append(
190
+ {
191
+ "name": scenario.get("name", request.task_type),
192
+ "ok": selected is not None,
193
+ "task_type": request.task_type,
194
+ "sensitivity": request.data_sensitivity,
195
+ "selected_model": selected.model_id if selected else None,
196
+ "runtime": selected.runtime if selected else None,
197
+ "fallback_count": len(decision.fallback_models),
198
+ "rejected_count": len(decision.rejected),
199
+ "reasons": list(decision.reasons),
200
+ },
201
+ )
202
+ return {"ok": all(row["ok"] for row in rows), "scenarios": rows}
203
+
204
+
205
+ def build_opencode_provider_config(
206
+ models: tuple[ModelEndpoint, ...],
207
+ *,
208
+ base_url: str = "http://127.0.0.1:8787/v1",
209
+ provider_id: str = "abteex-marama",
210
+ ) -> dict[str, Any]:
211
+ route = SovereignModelRouter(models).route(RoutingRequest.from_payload(DEFAULT_ROUTE_SCENARIOS[0]))
212
+ default_model = route.selected_model or (_top_models(models, limit=1)[0] if models else None)
213
+ catalog = _top_models(models, limit=14)
214
+ model_entries = {
215
+ model.model_id: {
216
+ "name": model.model_id,
217
+ "context": model.context_tokens,
218
+ "modalities": list(model.modalities),
219
+ "residency": list(model.residency),
220
+ "runtime": model.runtime,
221
+ }
222
+ for model in catalog
223
+ }
224
+ return {
225
+ "$schema": "https://opencode.ai/config.json",
226
+ "provider": {
227
+ provider_id: {
228
+ "name": "AbteeX MaramaRoute",
229
+ "npm": "@ai-sdk/openai-compatible",
230
+ "options": {
231
+ "baseURL": base_url,
232
+ "apiKey": "${ABTEEX_MARAMA_API_KEY:-local-dev}",
233
+ },
234
+ "models": model_entries,
235
+ },
236
+ },
237
+ "model": f"{provider_id}/{default_model.model_id}" if default_model else "",
238
+ "small_model": f"{provider_id}/{catalog[-1].model_id}" if catalog else "",
239
+ }
240
+
241
+
242
+ def route_receipt(payload: dict[str, Any], route_result: dict[str, Any]) -> dict[str, Any]:
243
+ selected = route_result.get("route_decision", {}).get("selected_model")
244
+ receipt_payload = {
245
+ "request": payload,
246
+ "selected_model_id": selected.get("model_id") if isinstance(selected, dict) else None,
247
+ "rejected_count": len(route_result.get("route_decision", {}).get("rejected", [])),
248
+ }
249
+ digest = hashlib.sha256(
250
+ json.dumps(receipt_payload, sort_keys=True, default=str).encode("utf-8"),
251
+ ).hexdigest()
252
+ return {
253
+ "receipt_id": f"marama-{digest[:16]}",
254
+ "request_hash": digest,
255
+ "selected_model": receipt_payload["selected_model_id"],
256
+ "prompt_retention": "not_stored_by_default",
257
+ "audit_fields": [
258
+ "request_hash",
259
+ "selected_model",
260
+ "fallback_models",
261
+ "rejected_count",
262
+ "residency",
263
+ "runtime",
264
+ ],
265
+ }
266
+
267
+
268
+ def route_or_chat_payload(payload: dict[str, Any], models: tuple[ModelEndpoint, ...]) -> dict[str, Any]:
269
+ if "messages" in payload:
270
+ result = route_chat_payload(payload, models)
271
+ selected = result["route_decision"]["selected_model"]
272
+ result = {"ok": selected is not None, "mode": "openai_chat_dry_run", **result}
273
+ else:
274
+ request = RoutingRequest.from_payload(payload)
275
+ decision = SovereignModelRouter(models).route(request)
276
+ result = {
277
+ "ok": decision.selected_model is not None,
278
+ "mode": "route",
279
+ "routing_request": request.to_dict(),
280
+ "route_decision": decision.to_dict(),
281
+ }
282
+ result["receipt"] = route_receipt(payload, result)
283
+ return result
284
+
285
+
286
+ def build_models_api(models: tuple[ModelEndpoint, ...]) -> dict[str, Any]:
287
+ response = build_models_response(models)
288
+ response["analytics"] = build_registry_analytics(models)
289
+ return response
290
+
291
+
292
+ def model_summary(model: ModelEndpoint) -> dict[str, Any]:
293
+ return {
294
+ "model_id": model.model_id,
295
+ "repo_id": model.repo_id,
296
+ "family": model.family,
297
+ "runtime": model.runtime,
298
+ "modalities": list(model.modalities),
299
+ "context_tokens": model.context_tokens,
300
+ "residency": list(model.residency),
301
+ "license_id": model.license_id,
302
+ "active_params_b": model.active_params_b,
303
+ "total_params_b": model.total_params_b,
304
+ "quality_rank": model.quality_rank,
305
+ "cost_rank": model.cost_rank,
306
+ "sovereignty_tier": model.sovereignty_tier,
307
+ "supports_json": model.supports_json,
308
+ "supports_tools": model.supports_tools,
309
+ "tags": list(model.tags),
310
+ "operator_score": _operator_score(model),
311
+ }
312
+
313
+
314
+ def scenario_presets() -> list[dict[str, Any]]:
315
+ return [dict(item) for item in DEFAULT_ROUTE_SCENARIOS]
316
+
317
+
318
+ def _top_models(models: tuple[ModelEndpoint, ...], *, limit: int) -> list[ModelEndpoint]:
319
+ return sorted(models, key=_catalog_sort_key, reverse=True)[:limit]
320
+
321
+
322
+ def _catalog_sort_key(model: ModelEndpoint) -> tuple[float, int, str]:
323
+ return (_operator_score(model), model.context_tokens, model.model_id)
324
+
325
+
326
+ def _operator_score(model: ModelEndpoint) -> float:
327
+ score = 0.0
328
+ if "NZ" in model.residency:
329
+ score += 25
330
+ if _is_local_runtime(model.runtime):
331
+ score += 15
332
+ score += model.sovereignty_tier * 10
333
+ score += max(0, 10 - model.quality_rank) * 3
334
+ score -= model.cost_rank
335
+ if model.supports_json:
336
+ score += 5
337
+ if model.supports_tools:
338
+ score += 5
339
+ if model.context_tokens >= 32768:
340
+ score += 6
341
+ elif model.context_tokens >= 8192:
342
+ score += 3
343
+ return round(score, 2)
344
+
345
+
346
+ def _matches_task(model: ModelEndpoint, task_type: str) -> bool:
347
+ tags = set(model.tags)
348
+ if task_type in tags or task_type in model.family.lower() or task_type in model.model_id.lower():
349
+ return True
350
+ if task_type == "code":
351
+ return "coder" in tags or "coder" in model.model_id.lower()
352
+ if task_type == "multimodal":
353
+ return "image" in model.modalities or "multimodal" in tags
354
+ return False
355
+
356
+
357
+ def _is_local_runtime(runtime: str) -> bool:
358
+ value = runtime.lower()
359
+ return value in {"llama_cpp", "gguf", "transformers", "sentence_transformers"} or "local" in value
marama_route/ui.py CHANGED
@@ -6,9 +6,17 @@ from typing import Any
6
 
7
  from ._ui_server import serve_dashboard
8
 
9
- from .gateway import build_models_response, route_chat_payload
10
- from .registry import RoutingRequest, load_model_registry
11
- from .router import SovereignModelRouter
 
 
 
 
 
 
 
 
12
 
13
  PRODUCT_NAME = "LumynaX MaramaRoute"
14
  PACKAGE_ROOT = Path(__file__).resolve().parent
@@ -49,24 +57,7 @@ def load_json_mapping(path: Path) -> dict[str, Any]:
49
 
50
 
51
  def route_dashboard_payload(payload: dict[str, Any], registry_path: Path) -> dict[str, Any]:
52
- models = load_model_registry(registry_path)
53
- if "messages" in payload:
54
- result = route_chat_payload(payload, models)
55
- selected = result["route_decision"]["selected_model"]
56
- return {
57
- "ok": selected is not None,
58
- "mode": "openai_chat_dry_run",
59
- **result,
60
- }
61
-
62
- request = RoutingRequest.from_payload(payload)
63
- decision = SovereignModelRouter(models).route(request)
64
- return {
65
- "ok": decision.selected_model is not None,
66
- "mode": "route",
67
- "routing_request": request.to_dict(),
68
- "route_decision": decision.to_dict(),
69
- }
70
 
71
 
72
  def build_dashboard_state(registry_path: Path) -> dict[str, Any]:
@@ -76,15 +67,19 @@ def build_dashboard_state(registry_path: Path) -> dict[str, Any]:
76
  if default_openai_chat_request_path().exists()
77
  else _fallback_chat_example()
78
  )
79
- runtimes = sorted({model.runtime for model in models})
80
- resident_nz = sum(1 for model in models if "NZ" in model.residency)
81
  return {
82
  "registry_path": str(registry_path),
83
  "model_count": len(models),
84
- "resident_nz": resident_nz,
85
- "runtimes": runtimes,
86
  "example": example,
87
- "models": build_models_response(models),
 
 
 
 
 
88
  }
89
 
90
 
@@ -98,7 +93,9 @@ def handle_api_request(
98
  state = build_dashboard_state(registry_path)
99
  return 200, {"ok": True, "product": PRODUCT_NAME, "model_count": state["model_count"]}
100
  if method == "GET" and path == "/api/models":
101
- return 200, build_dashboard_state(registry_path)["models"]
 
 
102
  if method == "GET" and path == "/api/state":
103
  return 200, {"ok": True, **build_dashboard_state(registry_path)}
104
  if method == "POST" and path == "/api/route" and payload is not None:
@@ -107,6 +104,26 @@ def handle_api_request(
107
  if not result["ok"]:
108
  status = 422
109
  return status, result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  return 404, {"ok": False, "error": "not_found"}
111
 
112
 
@@ -116,11 +133,17 @@ def smoke_ui(registry_path: Path | None = None) -> dict[str, Any]:
116
  routed = route_dashboard_payload(state["example"], resolved_registry)
117
  if not routed["ok"]:
118
  raise RuntimeError("MaramaRoute UI smoke route did not select a model")
 
 
 
 
119
  return {
120
  "ok": True,
121
  "product": PRODUCT_NAME,
122
  "model_count": state["model_count"],
123
  "selected_model": routed["route_decision"]["selected_model"]["model_id"],
 
 
124
  }
125
 
126
 
@@ -136,7 +159,7 @@ def run_ui(
136
  if smoke:
137
  print(json.dumps(smoke_ui(resolved_registry), indent=2, sort_keys=True))
138
  return 0
139
- html = build_dashboard_html(build_dashboard_state(resolved_registry))
140
  return serve_dashboard(
141
  product_name=PRODUCT_NAME,
142
  html=html,
@@ -152,6 +175,334 @@ def run_ui(
152
  )
153
 
154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  def build_dashboard_html(state: dict[str, Any]) -> str:
156
  initial = json.dumps(state, sort_keys=True).replace("</", "<\\/")
157
  return f"""<!doctype html>
 
6
 
7
  from ._ui_server import serve_dashboard
8
 
9
+ from .platform import (
10
+ build_models_api,
11
+ build_opencode_provider_config,
12
+ build_registry_analytics,
13
+ catalog_models,
14
+ compare_models,
15
+ route_or_chat_payload,
16
+ route_scenario_matrix,
17
+ scenario_presets,
18
+ )
19
+ from .registry import load_model_registry
20
 
21
  PRODUCT_NAME = "LumynaX MaramaRoute"
22
  PACKAGE_ROOT = Path(__file__).resolve().parent
 
57
 
58
 
59
  def route_dashboard_payload(payload: dict[str, Any], registry_path: Path) -> dict[str, Any]:
60
+ return route_or_chat_payload(payload, load_model_registry(registry_path))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
 
63
  def build_dashboard_state(registry_path: Path) -> dict[str, Any]:
 
67
  if default_openai_chat_request_path().exists()
68
  else _fallback_chat_example()
69
  )
70
+ analytics = build_registry_analytics(models)
 
71
  return {
72
  "registry_path": str(registry_path),
73
  "model_count": len(models),
74
+ "resident_nz": analytics["resident_nz"],
75
+ "runtimes": sorted(analytics["runtimes"]),
76
  "example": example,
77
+ "models": build_models_api(models),
78
+ "analytics": analytics,
79
+ "catalog": catalog_models(models, {"limit": 14})["models"],
80
+ "scenarios": scenario_presets(),
81
+ "matrix": route_scenario_matrix(models),
82
+ "opencode_config": build_opencode_provider_config(models),
83
  }
84
 
85
 
 
93
  state = build_dashboard_state(registry_path)
94
  return 200, {"ok": True, "product": PRODUCT_NAME, "model_count": state["model_count"]}
95
  if method == "GET" and path == "/api/models":
96
+ return 200, build_models_api(load_model_registry(registry_path))
97
+ if method == "GET" and path == "/api/analytics":
98
+ return 200, {"ok": True, **build_registry_analytics(load_model_registry(registry_path))}
99
  if method == "GET" and path == "/api/state":
100
  return 200, {"ok": True, **build_dashboard_state(registry_path)}
101
  if method == "POST" and path == "/api/route" and payload is not None:
 
104
  if not result["ok"]:
105
  status = 422
106
  return status, result
107
+ if method == "POST" and path == "/api/catalog" and payload is not None:
108
+ return 200, catalog_models(load_model_registry(registry_path), payload)
109
+ if method == "POST" and path == "/api/compare" and payload is not None:
110
+ model_ids = payload.get("model_ids") or payload.get("models") or []
111
+ if isinstance(model_ids, str):
112
+ model_ids = [item.strip() for item in model_ids.split(",") if item.strip()]
113
+ request_payload = payload.get("request") if isinstance(payload.get("request"), dict) else None
114
+ result = compare_models(load_model_registry(registry_path), list(model_ids), request_payload)
115
+ return (200 if result["ok"] else 422), result
116
+ if method == "POST" and path == "/api/matrix" and payload is not None:
117
+ scenarios = payload.get("scenarios") if isinstance(payload.get("scenarios"), list) else None
118
+ return 200, route_scenario_matrix(load_model_registry(registry_path), scenarios)
119
+ if method == "POST" and path == "/api/opencode-config" and payload is not None:
120
+ return 200, {
121
+ "ok": True,
122
+ "config": build_opencode_provider_config(
123
+ load_model_registry(registry_path),
124
+ base_url=str(payload.get("base_url") or "http://127.0.0.1:8787/v1"),
125
+ ),
126
+ }
127
  return 404, {"ok": False, "error": "not_found"}
128
 
129
 
 
133
  routed = route_dashboard_payload(state["example"], resolved_registry)
134
  if not routed["ok"]:
135
  raise RuntimeError("MaramaRoute UI smoke route did not select a model")
136
+ catalog = catalog_models(load_model_registry(resolved_registry), {"task_type": "code", "limit": 3})
137
+ matrix = route_scenario_matrix(load_model_registry(resolved_registry))
138
+ if not catalog["models"] or not matrix["ok"]:
139
+ raise RuntimeError("MaramaRoute expanded UI smoke checks failed")
140
  return {
141
  "ok": True,
142
  "product": PRODUCT_NAME,
143
  "model_count": state["model_count"],
144
  "selected_model": routed["route_decision"]["selected_model"]["model_id"],
145
+ "catalog_count": catalog["count"],
146
+ "scenario_count": len(matrix["scenarios"]),
147
  }
148
 
149
 
 
159
  if smoke:
160
  print(json.dumps(smoke_ui(resolved_registry), indent=2, sort_keys=True))
161
  return 0
162
+ html = build_expanded_dashboard_html(build_dashboard_state(resolved_registry))
163
  return serve_dashboard(
164
  product_name=PRODUCT_NAME,
165
  html=html,
 
175
  )
176
 
177
 
178
+ def build_expanded_dashboard_html(state: dict[str, Any]) -> str:
179
+ initial = json.dumps(state, sort_keys=True).replace("</", "<\\/")
180
+ html = """<!doctype html>
181
+ <html lang="en">
182
+ <head>
183
+ <meta charset="utf-8" />
184
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
185
+ <title>LumynaX MaramaRoute Console</title>
186
+ <style>
187
+ :root {
188
+ --paper: #fffefa;
189
+ --ink: #0a0a0b;
190
+ --muted: #726b62;
191
+ --rule: #ded6c8;
192
+ --amber: #e08a2c;
193
+ --amber-dark: #9a5416;
194
+ --green: #275f45;
195
+ --red: #8f2e24;
196
+ --panel: #f7f1e7;
197
+ --chalk: #fff7e8;
198
+ --mono: "Cascadia Code", "SFMono-Regular", Menlo, Consolas, monospace;
199
+ --serif: Georgia, "Iowan Old Style", "Palatino Linotype", serif;
200
+ }
201
+ * { box-sizing: border-box; }
202
+ body {
203
+ margin: 0;
204
+ background:
205
+ linear-gradient(90deg, rgba(10,10,11,.035) 1px, transparent 1px) 0 0 / 44px 44px,
206
+ linear-gradient(0deg, rgba(10,10,11,.025) 1px, transparent 1px) 0 0 / 44px 44px,
207
+ var(--paper);
208
+ color: var(--ink);
209
+ font-family: var(--serif);
210
+ }
211
+ header {
212
+ display: grid;
213
+ grid-template-columns: minmax(260px, 1fr) minmax(320px, 48vw);
214
+ gap: 24px;
215
+ align-items: end;
216
+ padding: 22px 28px 16px;
217
+ border-bottom: 1px solid var(--rule);
218
+ background: rgba(255,254,250,.92);
219
+ }
220
+ h1 {
221
+ margin: 0;
222
+ font-size: clamp(34px, 5vw, 68px);
223
+ line-height: .88;
224
+ letter-spacing: 0;
225
+ }
226
+ .subtitle, label, button, input, select, .metric, .pill, th, td {
227
+ font-family: var(--mono);
228
+ font-size: 12px;
229
+ letter-spacing: 0;
230
+ }
231
+ .subtitle { margin-top: 10px; color: var(--muted); }
232
+ .metrics { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; }
233
+ .metric { border-left: 2px solid var(--amber); min-height: 60px; padding: 7px 8px; background: rgba(247,241,231,.72); }
234
+ .metric strong { display: block; font: 700 22px/1 var(--serif); color: var(--ink); overflow-wrap: anywhere; }
235
+ main { display: grid; grid-template-columns: minmax(340px, 440px) 1fr; min-height: calc(100vh - 118px); }
236
+ aside { border-right: 1px solid var(--rule); background: rgba(255,250,242,.95); padding: 18px; }
237
+ section { min-width: 0; }
238
+ .stack { display: grid; gap: 12px; }
239
+ .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
240
+ .wide { grid-column: 1 / -1; }
241
+ .panel, textarea, input, select, pre, table { border: 1px solid var(--rule); border-radius: 6px; background: var(--paper); color: var(--ink); }
242
+ .panel { padding: 14px; }
243
+ .panel h2 { margin: 0 0 10px; font-size: 18px; line-height: 1.1; }
244
+ textarea {
245
+ width: 100%;
246
+ min-height: 270px;
247
+ resize: vertical;
248
+ padding: 12px;
249
+ font: 12px/1.45 var(--mono);
250
+ }
251
+ input, select { width: 100%; min-height: 34px; padding: 0 10px; }
252
+ button {
253
+ appearance: none;
254
+ border: 1px solid var(--ink);
255
+ background: var(--ink);
256
+ color: var(--paper);
257
+ border-radius: 4px;
258
+ min-height: 34px;
259
+ padding: 0 12px;
260
+ cursor: pointer;
261
+ white-space: nowrap;
262
+ }
263
+ button.secondary { background: transparent; color: var(--ink); border-color: var(--rule); }
264
+ .toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
265
+ .content { padding: 18px; display: grid; gap: 12px; align-content: start; }
266
+ .status { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
267
+ .status .panel { min-height: 92px; }
268
+ .k { font: 12px/1.25 var(--mono); color: var(--muted); display: block; }
269
+ .v { display: block; margin-top: 8px; font-size: 18px; line-height: 1.12; overflow-wrap: anywhere; }
270
+ .ok { color: var(--green); }
271
+ .bad { color: var(--red); }
272
+ pre { margin: 0; padding: 14px; max-height: 520px; overflow: auto; font: 12px/1.45 var(--mono); }
273
+ table { width: 100%; border-collapse: collapse; overflow: hidden; }
274
+ th, td { padding: 9px 10px; text-align: left; border-bottom: 1px solid var(--rule); vertical-align: top; }
275
+ th { color: var(--muted); background: var(--panel); }
276
+ tr:last-child td { border-bottom: 0; }
277
+ .pills { display: flex; flex-wrap: wrap; gap: 6px; }
278
+ .pill { display: inline-flex; border: 1px solid var(--rule); border-radius: 999px; min-height: 24px; align-items: center; padding: 0 9px; background: var(--chalk); }
279
+ .split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
280
+ @media (max-width: 1120px) {
281
+ header, main, .split { grid-template-columns: 1fr; }
282
+ aside { border-right: 0; border-bottom: 1px solid var(--rule); }
283
+ .metrics, .status { grid-template-columns: repeat(2, minmax(0, 1fr)); }
284
+ }
285
+ @media (max-width: 680px) {
286
+ header, aside, .content { padding: 14px; }
287
+ .grid, .metrics, .status { grid-template-columns: 1fr; }
288
+ }
289
+ </style>
290
+ </head>
291
+ <body>
292
+ <header>
293
+ <div>
294
+ <h1>LumynaX<br />MaramaRoute</h1>
295
+ <div class="subtitle">Sovereign router control surface for LumynaX model selection.</div>
296
+ </div>
297
+ <div class="metrics">
298
+ <div class="metric"><strong id="metricModels">0</strong>models</div>
299
+ <div class="metric"><strong id="metricNz">0</strong>NZ resident</div>
300
+ <div class="metric"><strong id="metricLocal">0</strong>local runtime</div>
301
+ <div class="metric"><strong id="metricJson">0</strong>JSON ready</div>
302
+ <div class="metric"><strong id="metricContext">0</strong>max ctx</div>
303
+ </div>
304
+ </header>
305
+ <main>
306
+ <aside class="stack">
307
+ <div class="toolbar">
308
+ <button id="routeBtn">Route</button>
309
+ <button class="secondary" id="matrixBtn">Matrix</button>
310
+ <button class="secondary" id="providerBtn">Provider</button>
311
+ </div>
312
+ <div>
313
+ <label for="preset">PRESET</label>
314
+ <select id="preset"></select>
315
+ </div>
316
+ <div>
317
+ <label for="request">REQUEST JSON</label>
318
+ <textarea id="request" spellcheck="false"></textarea>
319
+ </div>
320
+ <div class="panel stack">
321
+ <h2>Catalog</h2>
322
+ <div class="grid">
323
+ <div>
324
+ <label for="search">SEARCH</label>
325
+ <input id="search" value="qwen" />
326
+ </div>
327
+ <div>
328
+ <label for="task">TASK</label>
329
+ <select id="task">
330
+ <option value="">any</option>
331
+ <option value="code">code</option>
332
+ <option value="reasoning">reasoning</option>
333
+ <option value="multimodal">multimodal</option>
334
+ <option value="general">general</option>
335
+ </select>
336
+ </div>
337
+ <div>
338
+ <label for="runtime">RUNTIME</label>
339
+ <select id="runtime"></select>
340
+ </div>
341
+ <div>
342
+ <label for="context">MIN CONTEXT</label>
343
+ <input id="context" type="number" min="0" value="4096" />
344
+ </div>
345
+ </div>
346
+ <div class="toolbar">
347
+ <button class="secondary" id="catalogBtn">Filter</button>
348
+ <button class="secondary" id="compareBtn">Compare Top</button>
349
+ </div>
350
+ </div>
351
+ </aside>
352
+ <section class="content">
353
+ <div class="status">
354
+ <div class="panel"><span class="k">SELECTED</span><span class="v" id="selected">pending</span></div>
355
+ <div class="panel"><span class="k">MODE</span><span class="v" id="mode">ready</span></div>
356
+ <div class="panel"><span class="k">FALLBACKS</span><span class="v" id="fallbacks">0</span></div>
357
+ <div class="panel"><span class="k">RECEIPT</span><span class="v" id="receipt">none</span></div>
358
+ </div>
359
+ <div class="split">
360
+ <div class="panel">
361
+ <h2>Route Result</h2>
362
+ <pre id="output"></pre>
363
+ </div>
364
+ <div class="panel">
365
+ <h2>Scenario Matrix</h2>
366
+ <table>
367
+ <thead><tr><th>Scenario</th><th>Model</th><th>Rejected</th></tr></thead>
368
+ <tbody id="matrixRows"></tbody>
369
+ </table>
370
+ </div>
371
+ </div>
372
+ <div class="panel">
373
+ <h2>Model Catalog</h2>
374
+ <table>
375
+ <thead><tr><th>Model</th><th>Runtime</th><th>Fit</th><th>Caps</th></tr></thead>
376
+ <tbody id="catalogRows"></tbody>
377
+ </table>
378
+ </div>
379
+ <div class="panel">
380
+ <h2>Provider Config</h2>
381
+ <pre id="provider"></pre>
382
+ </div>
383
+ </section>
384
+ </main>
385
+ <script type="application/json" id="initial-state">__INITIAL__</script>
386
+ <script>
387
+ const state = JSON.parse(document.getElementById('initial-state').textContent);
388
+ const request = document.getElementById('request');
389
+ const preset = document.getElementById('preset');
390
+ const output = document.getElementById('output');
391
+ const provider = document.getElementById('provider');
392
+ const selected = document.getElementById('selected');
393
+ const mode = document.getElementById('mode');
394
+ const fallbacks = document.getElementById('fallbacks');
395
+ const receipt = document.getElementById('receipt');
396
+ const catalogRows = document.getElementById('catalogRows');
397
+ const matrixRows = document.getElementById('matrixRows');
398
+ const runtime = document.getElementById('runtime');
399
+
400
+ function writeJson(node, value) {
401
+ node.textContent = JSON.stringify(value, null, 2);
402
+ }
403
+ function safeJson(text) {
404
+ try { return [JSON.parse(text), null]; }
405
+ catch (error) { return [null, error.message]; }
406
+ }
407
+ function setMetric(id, value) { document.getElementById(id).textContent = value; }
408
+ setMetric('metricModels', state.analytics.model_count);
409
+ setMetric('metricNz', state.analytics.resident_nz);
410
+ setMetric('metricLocal', state.analytics.local_runtimes);
411
+ setMetric('metricJson', state.analytics.json_ready);
412
+ setMetric('metricContext', state.analytics.max_context_tokens);
413
+
414
+ runtime.innerHTML = '<option value="">any</option>' + Object.keys(state.analytics.runtimes).map(item => `<option value="${item}">${item}</option>`).join('');
415
+ preset.innerHTML = state.scenarios.map((item, index) => `<option value="${index}">${item.name}</option>`).join('');
416
+ preset.addEventListener('change', () => {
417
+ request.value = JSON.stringify(state.scenarios[Number(preset.value)], null, 2);
418
+ });
419
+ request.value = JSON.stringify(state.example, null, 2);
420
+ writeJson(output, {status: 'ready', registry: state.registry_path});
421
+ writeJson(provider, state.opencode_config);
422
+
423
+ function renderCatalog(rows) {
424
+ catalogRows.innerHTML = rows.map(row => {
425
+ const caps = [
426
+ row.residency.includes('NZ') ? 'NZ' : '',
427
+ row.supports_json ? 'JSON' : '',
428
+ row.supports_tools ? 'TOOLS' : '',
429
+ `${row.context_tokens} ctx`
430
+ ].filter(Boolean).map(item => `<span class="pill">${item}</span>`).join(' ');
431
+ return `<tr><td>${row.model_id}<br><span class="k">${row.repo_id}</span></td><td>${row.runtime}</td><td>${row.operator_score}</td><td><div class="pills">${caps}</div></td></tr>`;
432
+ }).join('');
433
+ }
434
+ function renderMatrix(rows) {
435
+ matrixRows.innerHTML = rows.map(row => `<tr><td>${row.name}<br><span class="k">${row.sensitivity}</span></td><td class="${row.ok ? 'ok' : 'bad'}">${row.selected_model || 'none'}</td><td>${row.rejected_count}</td></tr>`).join('');
436
+ }
437
+ function setResult(data) {
438
+ writeJson(output, data);
439
+ const decision = data.route_decision || {};
440
+ const model = decision.selected_model || {};
441
+ selected.textContent = model.model_id || 'none';
442
+ selected.className = model.model_id ? 'v ok' : 'v bad';
443
+ mode.textContent = data.mode || 'result';
444
+ fallbacks.textContent = decision.fallback_models ? decision.fallback_models.length : 0;
445
+ receipt.textContent = data.receipt ? data.receipt.receipt_id : 'none';
446
+ }
447
+ async function postJson(path, payload) {
448
+ const response = await fetch(path, {
449
+ method: 'POST',
450
+ headers: {'Content-Type': 'application/json'},
451
+ body: JSON.stringify(payload)
452
+ });
453
+ return response.json();
454
+ }
455
+ async function route() {
456
+ const [payload, error] = safeJson(request.value);
457
+ if (error) { setResult({ok: false, error}); return; }
458
+ setResult(await postJson('/api/route', payload));
459
+ }
460
+ async function loadCatalog() {
461
+ const filters = {
462
+ search: document.getElementById('search').value,
463
+ task_type: document.getElementById('task').value,
464
+ runtime: runtime.value,
465
+ min_context_tokens: Number(document.getElementById('context').value || 0),
466
+ jurisdiction: 'NZ',
467
+ limit: 18
468
+ };
469
+ const data = await postJson('/api/catalog', filters);
470
+ renderCatalog(data.models);
471
+ writeJson(output, data);
472
+ }
473
+ async function compareTop() {
474
+ const ids = Array.from(catalogRows.querySelectorAll('tr')).slice(0, 4).map(row => row.cells[0].childNodes[0].textContent);
475
+ const data = await postJson('/api/compare', {model_ids: ids});
476
+ writeJson(output, data);
477
+ if (data.winner) {
478
+ selected.textContent = data.winner.model_id;
479
+ selected.className = 'v ok';
480
+ mode.textContent = 'compare';
481
+ }
482
+ }
483
+ async function matrix() {
484
+ const data = await postJson('/api/matrix', {scenarios: state.scenarios});
485
+ renderMatrix(data.scenarios);
486
+ writeJson(output, data);
487
+ }
488
+ async function providerConfig() {
489
+ const data = await postJson('/api/opencode-config', {base_url: 'http://127.0.0.1:8787/v1'});
490
+ writeJson(provider, data.config);
491
+ writeJson(output, data);
492
+ }
493
+ document.getElementById('routeBtn').addEventListener('click', route);
494
+ document.getElementById('catalogBtn').addEventListener('click', loadCatalog);
495
+ document.getElementById('compareBtn').addEventListener('click', compareTop);
496
+ document.getElementById('matrixBtn').addEventListener('click', matrix);
497
+ document.getElementById('providerBtn').addEventListener('click', providerConfig);
498
+ renderCatalog(state.catalog);
499
+ renderMatrix(state.matrix.scenarios);
500
+ </script>
501
+ </body>
502
+ </html>"""
503
+ return html.replace("__INITIAL__", initial)
504
+
505
+
506
  def build_dashboard_html(state: dict[str, Any]) -> str:
507
  initial = json.dumps(state, sort_keys=True).replace("</", "<\\/")
508
  return f"""<!doctype html>
product_manifest.json CHANGED
@@ -21,11 +21,18 @@
21
  "lumynax_runtime_adapter",
22
  "audit_ledger",
23
  "human_review_gate",
 
 
 
 
24
  "opencode_compatible_provider"
25
  ],
26
  "runtime_entrypoints": [
27
  "python -m tinyluminax.products.sovereigncode.cli evaluate",
28
  "python -m tinyluminax.products.sovereigncode.cli plan-turn",
 
 
 
29
  "python -m tinyluminax.products.sovereigncode.cli ui"
30
  ],
31
  "integration_surfaces": [
@@ -34,6 +41,8 @@
34
  "OpenCode custom provider config",
35
  "local CLI policy evaluator",
36
  "governed coding-turn planner",
 
 
37
  "browser operator console"
38
  ],
39
  "brand_system": {
 
21
  "lumynax_runtime_adapter",
22
  "audit_ledger",
23
  "human_review_gate",
24
+ "policy_matrix",
25
+ "tool_gate_check",
26
+ "capsule_summary",
27
+ "opencode_workspace_export",
28
  "opencode_compatible_provider"
29
  ],
30
  "runtime_entrypoints": [
31
  "python -m tinyluminax.products.sovereigncode.cli evaluate",
32
  "python -m tinyluminax.products.sovereigncode.cli plan-turn",
33
+ "python -m tinyluminax.products.sovereigncode.cli policy-matrix",
34
+ "python -m tinyluminax.products.sovereigncode.cli tool-check",
35
+ "python -m tinyluminax.products.sovereigncode.cli opencode-config",
36
  "python -m tinyluminax.products.sovereigncode.cli ui"
37
  ],
38
  "integration_surfaces": [
 
41
  "OpenCode custom provider config",
42
  "local CLI policy evaluator",
43
  "governed coding-turn planner",
44
+ "policy matrix and tool gate checker",
45
+ "browser operator checklist",
46
  "browser operator console"
47
  ],
48
  "brand_system": {
quickstart.py CHANGED
@@ -28,6 +28,24 @@ if __name__ == "__main__":
28
  "ui",
29
  "--smoke",
30
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  [
32
  "plan-turn",
33
  "--capsule",
 
28
  "ui",
29
  "--smoke",
30
  ],
31
+ [
32
+ "policy-matrix",
33
+ "--capsule",
34
+ str(ROOT / "examples" / "capsule.restricted-nz-code.json"),
35
+ "--request",
36
+ str(ROOT / "examples" / "request.allowed-local-edit.json"),
37
+ ],
38
+ [
39
+ "tool-check",
40
+ "--capsule",
41
+ str(ROOT / "examples" / "capsule.restricted-nz-code.json"),
42
+ "--request",
43
+ str(ROOT / "examples" / "request.allowed-local-edit.json"),
44
+ "--tool-name",
45
+ "workspace_reader",
46
+ "--action",
47
+ "read_context",
48
+ ],
49
  [
50
  "plan-turn",
51
  "--capsule",
sovereigncode/__init__.py CHANGED
@@ -2,6 +2,13 @@ from __future__ import annotations
2
 
3
  from .audit import AuditRecord, build_audit_record
4
  from .planner import SovereignCodingTurnPlan, ToolGrant, plan_coding_turn
 
 
 
 
 
 
 
5
  from .policy import (
6
  DataCapsule,
7
  PolicyDecision,
@@ -19,6 +26,11 @@ __all__ = [
19
  "SovereigntyPolicyEngine",
20
  "ToolGrant",
21
  "build_audit_record",
 
 
 
 
 
22
  "plan_coding_turn",
23
  "smoke_ui",
24
  ]
 
2
 
3
  from .audit import AuditRecord, build_audit_record
4
  from .planner import SovereignCodingTurnPlan, ToolGrant, plan_coding_turn
5
+ from .platform import (
6
+ build_capsule_summary,
7
+ build_opencode_workspace_config,
8
+ build_policy_matrix,
9
+ build_turn_brief,
10
+ check_tool_request,
11
+ )
12
  from .policy import (
13
  DataCapsule,
14
  PolicyDecision,
 
26
  "SovereigntyPolicyEngine",
27
  "ToolGrant",
28
  "build_audit_record",
29
+ "build_capsule_summary",
30
+ "build_opencode_workspace_config",
31
+ "build_policy_matrix",
32
+ "build_turn_brief",
33
+ "check_tool_request",
34
  "plan_coding_turn",
35
  "smoke_ui",
36
  ]
sovereigncode/cli.py CHANGED
@@ -12,6 +12,13 @@ from marama_route import RoutingRequest, load_model_registry
12
 
13
  from .audit import build_audit_record
14
  from .planner import plan_coding_turn
 
 
 
 
 
 
 
15
  from .policy import DataCapsule, SovereignRequest, SovereigntyPolicyEngine
16
 
17
 
@@ -43,10 +50,52 @@ def _plan_turn(args: argparse.Namespace) -> int:
43
  routing_request = RoutingRequest.from_payload(_load_mapping(args.route_request))
44
  models = load_model_registry(args.registry)
45
  plan = plan_coding_turn(capsule, request, routing_request, models)
46
- print(json.dumps(plan.to_dict(), indent=2, sort_keys=True))
 
 
47
  return 0 if plan.allowed or args.allow_blocked_exit_zero else 2
48
 
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  def _ui(args: argparse.Namespace) -> int:
51
  from .ui import run_ui
52
 
@@ -106,6 +155,44 @@ def build_parser() -> argparse.ArgumentParser:
106
  )
107
  plan_turn.set_defaults(handler=_plan_turn)
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  ui = subparsers.add_parser(
110
  "ui",
111
  help="Launch the local SovereignCode browser platform.",
 
12
 
13
  from .audit import build_audit_record
14
  from .planner import plan_coding_turn
15
+ from .platform import (
16
+ build_capsule_summary,
17
+ build_opencode_workspace_config,
18
+ build_policy_matrix,
19
+ build_turn_brief,
20
+ check_tool_request,
21
+ )
22
  from .policy import DataCapsule, SovereignRequest, SovereigntyPolicyEngine
23
 
24
 
 
50
  routing_request = RoutingRequest.from_payload(_load_mapping(args.route_request))
51
  models = load_model_registry(args.registry)
52
  plan = plan_coding_turn(capsule, request, routing_request, models)
53
+ payload = plan.to_dict()
54
+ payload["turn_brief"] = build_turn_brief(payload)
55
+ print(json.dumps(payload, indent=2, sort_keys=True))
56
  return 0 if plan.allowed or args.allow_blocked_exit_zero else 2
57
 
58
 
59
+ def _policy_matrix(args: argparse.Namespace) -> int:
60
+ result = build_policy_matrix(_load_mapping(args.capsule), _load_mapping(args.request))
61
+ print(json.dumps(result, indent=2, sort_keys=True))
62
+ return 0 if result["blocked_count"] >= 0 else 2
63
+
64
+
65
+ def _tool_check(args: argparse.Namespace) -> int:
66
+ tool_payload = {
67
+ "tool_name": args.tool_name,
68
+ "action": args.action,
69
+ "writes_files": args.writes_files,
70
+ "exports_data": args.exports_data,
71
+ "trains_model": args.trains_model,
72
+ "human_approved": args.human_approved,
73
+ }
74
+ result = check_tool_request(
75
+ _load_mapping(args.capsule),
76
+ _load_mapping(args.request),
77
+ tool_payload,
78
+ )
79
+ print(json.dumps(result, indent=2, sort_keys=True))
80
+ return 0 if result["ok"] or args.allow_blocked_exit_zero else 2
81
+
82
+
83
+ def _capsule_summary(args: argparse.Namespace) -> int:
84
+ print(json.dumps(build_capsule_summary(_load_mapping(args.capsule)), indent=2, sort_keys=True))
85
+ return 0
86
+
87
+
88
+ def _opencode_config(args: argparse.Namespace) -> int:
89
+ print(
90
+ json.dumps(
91
+ build_opencode_workspace_config(base_url=args.base_url, model=args.model),
92
+ indent=2,
93
+ sort_keys=True,
94
+ ),
95
+ )
96
+ return 0
97
+
98
+
99
  def _ui(args: argparse.Namespace) -> int:
100
  from .ui import run_ui
101
 
 
155
  )
156
  plan_turn.set_defaults(handler=_plan_turn)
157
 
158
+ matrix = subparsers.add_parser(
159
+ "policy-matrix",
160
+ help="Evaluate the built-in tool/action matrix against a capsule.",
161
+ )
162
+ matrix.add_argument("--capsule", type=Path, required=True, help="Data Capsule JSON/YAML file.")
163
+ matrix.add_argument("--request", type=Path, required=True, help="Base sovereign request JSON/YAML file.")
164
+ matrix.set_defaults(handler=_policy_matrix)
165
+
166
+ tool_check = subparsers.add_parser(
167
+ "tool-check",
168
+ help="Check one tool/action request before execution.",
169
+ )
170
+ tool_check.add_argument("--capsule", type=Path, required=True, help="Data Capsule JSON/YAML file.")
171
+ tool_check.add_argument("--request", type=Path, required=True, help="Base sovereign request JSON/YAML file.")
172
+ tool_check.add_argument("--tool-name", default="workspace_reader")
173
+ tool_check.add_argument("--action", default="read_context")
174
+ tool_check.add_argument("--writes-files", action=argparse.BooleanOptionalAction, default=False)
175
+ tool_check.add_argument("--exports-data", action=argparse.BooleanOptionalAction, default=False)
176
+ tool_check.add_argument("--trains-model", action=argparse.BooleanOptionalAction, default=False)
177
+ tool_check.add_argument("--human-approved", action=argparse.BooleanOptionalAction, default=False)
178
+ tool_check.add_argument("--allow-blocked-exit-zero", action=argparse.BooleanOptionalAction, default=False)
179
+ tool_check.set_defaults(handler=_tool_check)
180
+
181
+ capsule_summary = subparsers.add_parser(
182
+ "capsule-summary",
183
+ help="Summarise Data Capsule controls and risk flags.",
184
+ )
185
+ capsule_summary.add_argument("--capsule", type=Path, required=True, help="Data Capsule JSON/YAML file.")
186
+ capsule_summary.set_defaults(handler=_capsule_summary)
187
+
188
+ opencode = subparsers.add_parser(
189
+ "opencode-config",
190
+ help="Emit an OpenCode-compatible SovereignCode workspace config.",
191
+ )
192
+ opencode.add_argument("--base-url", default="http://127.0.0.1:8787/v1")
193
+ opencode.add_argument("--model", default="lumynax-infused-qwen3-coder-30b-a3b-gguf")
194
+ opencode.set_defaults(handler=_opencode_config)
195
+
196
  ui = subparsers.add_parser(
197
  "ui",
198
  help="Launch the local SovereignCode browser platform.",
sovereigncode/platform.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .audit import build_audit_record
6
+ from .policy import DataCapsule, SovereignRequest, SovereigntyPolicyEngine
7
+
8
+ TOOL_SCENARIOS: tuple[dict[str, Any], ...] = (
9
+ {
10
+ "name": "Read workspace",
11
+ "action": "read_context",
12
+ "tool_name": "workspace_reader",
13
+ "writes_files": False,
14
+ "exports_data": False,
15
+ "trains_model": False,
16
+ "human_approved": False,
17
+ },
18
+ {
19
+ "name": "Write file",
20
+ "action": "write_file",
21
+ "tool_name": "file_editor",
22
+ "writes_files": True,
23
+ "exports_data": False,
24
+ "trains_model": False,
25
+ "human_approved": False,
26
+ },
27
+ {
28
+ "name": "Approved shell",
29
+ "action": "execute_shell",
30
+ "tool_name": "test_runner",
31
+ "writes_files": False,
32
+ "exports_data": False,
33
+ "trains_model": False,
34
+ "human_approved": True,
35
+ },
36
+ {
37
+ "name": "Network export",
38
+ "action": "network_export",
39
+ "tool_name": "external_api",
40
+ "writes_files": False,
41
+ "exports_data": True,
42
+ "trains_model": False,
43
+ "human_approved": True,
44
+ },
45
+ {
46
+ "name": "Model training",
47
+ "action": "train_model",
48
+ "tool_name": "trainer",
49
+ "writes_files": False,
50
+ "exports_data": False,
51
+ "trains_model": True,
52
+ "human_approved": True,
53
+ },
54
+ {
55
+ "name": "Publish commit",
56
+ "action": "commit",
57
+ "tool_name": "git",
58
+ "writes_files": False,
59
+ "exports_data": True,
60
+ "trains_model": False,
61
+ "human_approved": True,
62
+ },
63
+ )
64
+
65
+
66
+ def build_policy_matrix(
67
+ capsule_payload: dict[str, Any],
68
+ request_payload: dict[str, Any],
69
+ scenarios: list[dict[str, Any]] | None = None,
70
+ ) -> dict[str, Any]:
71
+ capsule = DataCapsule.from_payload(capsule_payload)
72
+ engine = SovereigntyPolicyEngine()
73
+ rows = []
74
+ for scenario in scenarios or [dict(item) for item in TOOL_SCENARIOS]:
75
+ merged = dict(request_payload)
76
+ merged.update({key: value for key, value in scenario.items() if key != "name"})
77
+ request = SovereignRequest.from_payload(merged)
78
+ decision = engine.evaluate(capsule, request)
79
+ audit = build_audit_record(capsule, request, decision)
80
+ rows.append(
81
+ {
82
+ "name": scenario.get("name", request.action),
83
+ "allowed": decision.allowed,
84
+ "action": request.action,
85
+ "tool_name": request.tool_name,
86
+ "writes_files": request.writes_files,
87
+ "exports_data": request.exports_data,
88
+ "trains_model": request.trains_model,
89
+ "human_approved": request.human_approved,
90
+ "reason_count": len(decision.reasons),
91
+ "obligation_count": len(decision.obligations),
92
+ "reasons": list(decision.reasons),
93
+ "obligations": list(decision.obligations),
94
+ "audit_hash": audit.request_hash,
95
+ },
96
+ )
97
+ return {
98
+ "ok": True,
99
+ "capsule_id": capsule.capsule_id,
100
+ "rows": rows,
101
+ "allowed_count": sum(1 for row in rows if row["allowed"]),
102
+ "blocked_count": sum(1 for row in rows if not row["allowed"]),
103
+ }
104
+
105
+
106
+ def check_tool_request(
107
+ capsule_payload: dict[str, Any],
108
+ request_payload: dict[str, Any],
109
+ tool_payload: dict[str, Any],
110
+ ) -> dict[str, Any]:
111
+ merged = dict(request_payload)
112
+ merged.update(tool_payload)
113
+ capsule = DataCapsule.from_payload(capsule_payload)
114
+ request = SovereignRequest.from_payload(merged)
115
+ decision = SovereigntyPolicyEngine().evaluate(capsule, request)
116
+ audit = build_audit_record(capsule, request, decision)
117
+ return {
118
+ "ok": decision.allowed,
119
+ "tool_name": request.tool_name,
120
+ "action": request.action,
121
+ "decision": decision.to_dict(),
122
+ "audit_record": audit.to_dict(),
123
+ "operator_gate": build_operator_gate(decision.to_dict(), request.to_dict()),
124
+ }
125
+
126
+
127
+ def build_operator_gate(decision: dict[str, Any], request: dict[str, Any]) -> dict[str, Any]:
128
+ destructive = bool(
129
+ request.get("writes_files")
130
+ or request.get("exports_data")
131
+ or request.get("trains_model")
132
+ or request.get("action") in {"execute_shell", "publish", "commit", "network_export"}
133
+ )
134
+ return {
135
+ "requires_human_review": destructive or not decision.get("allowed", False),
136
+ "requires_visible_diff": bool(request.get("writes_files")),
137
+ "requires_export_manifest": bool(request.get("exports_data")),
138
+ "requires_training_consent": bool(request.get("trains_model")),
139
+ "next_gate": _next_gate(decision, request),
140
+ }
141
+
142
+
143
+ def build_turn_brief(plan: dict[str, Any]) -> dict[str, Any]:
144
+ route = plan.get("route_decision") or {}
145
+ selected = route.get("selected_model") or {}
146
+ grants = plan.get("tool_grants") or []
147
+ blocked_grants = [grant for grant in grants if not grant.get("allowed")]
148
+ return {
149
+ "allowed": bool(plan.get("allowed")),
150
+ "selected_model": selected.get("model_id"),
151
+ "runtime": selected.get("runtime"),
152
+ "tool_grants": len(grants),
153
+ "blocked_tool_grants": len(blocked_grants),
154
+ "obligation_count": len(plan.get("obligations") or []),
155
+ "blocked_reasons": plan.get("blocked_reasons") or [],
156
+ "operator_checklist": build_operator_checklist(plan),
157
+ }
158
+
159
+
160
+ def build_operator_checklist(plan: dict[str, Any]) -> list[dict[str, Any]]:
161
+ allowed = bool(plan.get("allowed"))
162
+ obligations = set(plan.get("obligations") or [])
163
+ route = plan.get("route_decision") or {}
164
+ selected = route.get("selected_model")
165
+ return [
166
+ {
167
+ "item": "policy_decision",
168
+ "status": "pass" if allowed else "blocked",
169
+ "detail": "Data Capsule policy permits the request" if allowed else "Policy blocked the request",
170
+ },
171
+ {
172
+ "item": "model_route",
173
+ "status": "pass" if selected else "blocked",
174
+ "detail": selected.get("model_id") if isinstance(selected, dict) else "No eligible model",
175
+ },
176
+ {
177
+ "item": "audit_record",
178
+ "status": "required",
179
+ "detail": "Immutable audit record must be persisted before external effects",
180
+ },
181
+ {
182
+ "item": "visible_diff",
183
+ "status": "required" if "show_diff_before_write_or_commit" in obligations else "not_required",
184
+ "detail": "Show file diff before writes or commits",
185
+ },
186
+ {
187
+ "item": "resident_runtime",
188
+ "status": "required" if "route_only_to_resident_runtime" in obligations else "not_required",
189
+ "detail": "Route high-impact data only to approved resident runtime",
190
+ },
191
+ ]
192
+
193
+
194
+ def build_opencode_workspace_config(
195
+ *,
196
+ base_url: str = "http://127.0.0.1:8787/v1",
197
+ provider_id: str = "abteex-marama",
198
+ model: str = "lumynax-infused-qwen3-coder-30b-a3b-gguf",
199
+ ) -> dict[str, Any]:
200
+ return {
201
+ "$schema": "https://opencode.ai/config.json",
202
+ "provider": {
203
+ provider_id: {
204
+ "name": "AbteeX SovereignCode via MaramaRoute",
205
+ "npm": "@ai-sdk/openai-compatible",
206
+ "options": {
207
+ "baseURL": base_url,
208
+ "apiKey": "${ABTEEX_MARAMA_API_KEY:-local-dev}",
209
+ },
210
+ "models": {
211
+ model: {
212
+ "name": model,
213
+ "attachment": False,
214
+ "reasoning": True,
215
+ },
216
+ },
217
+ },
218
+ },
219
+ "model": f"{provider_id}/{model}",
220
+ "sovereigncode": {
221
+ "capsule_file": "products/abx-sovereigncode/examples/capsule.restricted-nz-code.json",
222
+ "audit_ledger": ".sovereigncode/audit.jsonl",
223
+ "require_human_review_for": ["write_files", "execute_shell", "network_export", "commit"],
224
+ },
225
+ }
226
+
227
+
228
+ def build_capsule_summary(capsule_payload: dict[str, Any]) -> dict[str, Any]:
229
+ capsule = DataCapsule.from_payload(capsule_payload)
230
+ return {
231
+ "capsule_id": capsule.capsule_id,
232
+ "jurisdiction": capsule.jurisdiction,
233
+ "sensitivity": capsule.sensitivity,
234
+ "resident_regions": list(capsule.resident_regions),
235
+ "allowed_purposes": list(capsule.allowed_purposes),
236
+ "denied_purposes": list(capsule.denied_purposes),
237
+ "retention_days": capsule.retention_days,
238
+ "export_allowed": capsule.export_allowed,
239
+ "training_allowed": capsule.training_allowed,
240
+ "personal_detail_level": capsule.personal_detail_level,
241
+ "risk_flags": _capsule_risk_flags(capsule),
242
+ }
243
+
244
+
245
+ def tool_scenarios() -> list[dict[str, Any]]:
246
+ return [dict(item) for item in TOOL_SCENARIOS]
247
+
248
+
249
+ def _capsule_risk_flags(capsule: DataCapsule) -> list[str]:
250
+ flags = []
251
+ if capsule.sensitivity in {"personal", "restricted", "health", "iwi", "taonga"}:
252
+ flags.append("high_impact_sensitivity")
253
+ if not capsule.export_allowed:
254
+ flags.append("export_blocked")
255
+ if not capsule.training_allowed:
256
+ flags.append("training_blocked")
257
+ if capsule.retention_days <= 14:
258
+ flags.append("short_retention")
259
+ if capsule.personal_detail_level not in {"none", "anonymous"}:
260
+ flags.append("personal_detail_controls")
261
+ return flags
262
+
263
+
264
+ def _next_gate(decision: dict[str, Any], request: dict[str, Any]) -> str:
265
+ if not decision.get("allowed", False):
266
+ return "revise_request_or_capsule"
267
+ if request.get("writes_files"):
268
+ return "show_diff_before_write"
269
+ if request.get("exports_data"):
270
+ return "attach_export_manifest"
271
+ if request.get("trains_model"):
272
+ return "attach_training_consent"
273
+ if request.get("action") in {"execute_shell", "commit"}:
274
+ return "human_approval"
275
+ return "execute_with_audit"
sovereigncode/ui.py CHANGED
@@ -16,6 +16,14 @@ except ModuleNotFoundError: # standalone HF package
16
 
17
  from .audit import build_audit_record
18
  from .planner import plan_coding_turn
 
 
 
 
 
 
 
 
19
  from .policy import DataCapsule, SovereignRequest, SovereigntyPolicyEngine
20
 
21
  PRODUCT_NAME = "AbteeX SovereignCode"
@@ -153,12 +161,20 @@ def build_dashboard_state(
153
  else _fallback_personal_request()
154
  )
155
  models = load_model_registry(registry_path)
 
 
 
156
  return {
157
- "capsule": load_json_mapping(capsule_path),
158
- "request": load_json_mapping(request_path),
159
- "route_request": load_json_mapping(route_request_path),
160
  "personal_capsule": personal_capsule,
161
  "personal_request": personal_request,
 
 
 
 
 
162
  "registry_path": str(registry_path),
163
  "model_count": len(models),
164
  }
@@ -181,7 +197,27 @@ def handle_api_request(
181
  return 200, {"ok": True, **state}
182
  if method == "POST" and path in {"/api/evaluate", "/api/plan"} and payload is not None:
183
  result = evaluate_dashboard_payload(payload, registry_path)
 
 
184
  return (200 if result["ok"] else 422), result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  return 404, {"ok": False, "error": "not_found"}
186
 
187
 
@@ -213,11 +249,17 @@ def smoke_ui(
213
  if not result["ok"]:
214
  raise RuntimeError("SovereignCode UI smoke plan was blocked")
215
  selected = result["plan"]["route_decision"]["selected_model"]["model_id"]
 
 
 
 
216
  return {
217
  "ok": True,
218
  "product": PRODUCT_NAME,
219
  "model_count": state["model_count"],
220
  "selected_model": selected,
 
 
221
  }
222
 
223
 
@@ -257,7 +299,7 @@ def run_ui(
257
  resolved_route,
258
  resolved_registry,
259
  )
260
- html = build_dashboard_html(state)
261
  return serve_dashboard(
262
  product_name=PRODUCT_NAME,
263
  html=html,
@@ -274,6 +316,335 @@ def run_ui(
274
  )
275
 
276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  def build_dashboard_html(state: dict[str, Any]) -> str:
278
  initial = json.dumps(state, sort_keys=True).replace("</", "<\\/")
279
  return f"""<!doctype html>
 
16
 
17
  from .audit import build_audit_record
18
  from .planner import plan_coding_turn
19
+ from .platform import (
20
+ build_capsule_summary,
21
+ build_opencode_workspace_config,
22
+ build_policy_matrix,
23
+ build_turn_brief,
24
+ check_tool_request,
25
+ tool_scenarios,
26
+ )
27
  from .policy import DataCapsule, SovereignRequest, SovereigntyPolicyEngine
28
 
29
  PRODUCT_NAME = "AbteeX SovereignCode"
 
161
  else _fallback_personal_request()
162
  )
163
  models = load_model_registry(registry_path)
164
+ capsule = load_json_mapping(capsule_path)
165
+ request = load_json_mapping(request_path)
166
+ route_request = load_json_mapping(route_request_path)
167
  return {
168
+ "capsule": capsule,
169
+ "request": request,
170
+ "route_request": route_request,
171
  "personal_capsule": personal_capsule,
172
  "personal_request": personal_request,
173
+ "capsule_summary": build_capsule_summary(capsule),
174
+ "personal_capsule_summary": build_capsule_summary(personal_capsule),
175
+ "policy_matrix": build_policy_matrix(capsule, request),
176
+ "tool_scenarios": tool_scenarios(),
177
+ "opencode_config": build_opencode_workspace_config(),
178
  "registry_path": str(registry_path),
179
  "model_count": len(models),
180
  }
 
197
  return 200, {"ok": True, **state}
198
  if method == "POST" and path in {"/api/evaluate", "/api/plan"} and payload is not None:
199
  result = evaluate_dashboard_payload(payload, registry_path)
200
+ if "plan" in result:
201
+ result["turn_brief"] = build_turn_brief(result["plan"])
202
  return (200 if result["ok"] else 422), result
203
+ if method == "POST" and path == "/api/policy-matrix" and payload is not None:
204
+ capsule = _mapping(payload.get("capsule")) or state["capsule"]
205
+ request = _mapping(payload.get("request")) or state["request"]
206
+ return 200, build_policy_matrix(capsule, request)
207
+ if method == "POST" and path == "/api/tool-check" and payload is not None:
208
+ capsule = _mapping(payload.get("capsule")) or state["capsule"]
209
+ request = _mapping(payload.get("request")) or state["request"]
210
+ tool = _mapping(payload.get("tool"))
211
+ result = check_tool_request(capsule, request, tool)
212
+ return (200 if result["ok"] else 422), result
213
+ if method == "POST" and path == "/api/opencode-config" and payload is not None:
214
+ return 200, {
215
+ "ok": True,
216
+ "config": build_opencode_workspace_config(
217
+ base_url=str(payload.get("base_url") or "http://127.0.0.1:8787/v1"),
218
+ model=str(payload.get("model") or "lumynax-infused-qwen3-coder-30b-a3b-gguf"),
219
+ ),
220
+ }
221
  return 404, {"ok": False, "error": "not_found"}
222
 
223
 
 
249
  if not result["ok"]:
250
  raise RuntimeError("SovereignCode UI smoke plan was blocked")
251
  selected = result["plan"]["route_decision"]["selected_model"]["model_id"]
252
+ matrix = build_policy_matrix(state["capsule"], state["request"])
253
+ tool = check_tool_request(state["capsule"], state["request"], tool_scenarios()[0])
254
+ if matrix["blocked_count"] < 1 or not tool["ok"]:
255
+ raise RuntimeError("SovereignCode expanded UI smoke checks failed")
256
  return {
257
  "ok": True,
258
  "product": PRODUCT_NAME,
259
  "model_count": state["model_count"],
260
  "selected_model": selected,
261
+ "matrix_rows": len(matrix["rows"]),
262
+ "tool_check": tool["ok"],
263
  }
264
 
265
 
 
299
  resolved_route,
300
  resolved_registry,
301
  )
302
+ html = build_expanded_dashboard_html(state)
303
  return serve_dashboard(
304
  product_name=PRODUCT_NAME,
305
  html=html,
 
316
  )
317
 
318
 
319
+ def build_expanded_dashboard_html(state: dict[str, Any]) -> str:
320
+ initial = json.dumps(state, sort_keys=True).replace("</", "<\\/")
321
+ html = """<!doctype html>
322
+ <html lang="en">
323
+ <head>
324
+ <meta charset="utf-8" />
325
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
326
+ <title>AbteeX SovereignCode Console</title>
327
+ <style>
328
+ :root {
329
+ --paper: #fffefa;
330
+ --ink: #0a0a0b;
331
+ --muted: #726b62;
332
+ --rule: #ded6c8;
333
+ --amber: #e08a2c;
334
+ --amber-dark: #9a5416;
335
+ --green: #275f45;
336
+ --red: #8f2e24;
337
+ --panel: #f7f1e7;
338
+ --chalk: #fff7e8;
339
+ --mono: "Cascadia Code", "SFMono-Regular", Menlo, Consolas, monospace;
340
+ --serif: Georgia, "Iowan Old Style", "Palatino Linotype", serif;
341
+ }
342
+ * { box-sizing: border-box; }
343
+ body {
344
+ margin: 0;
345
+ background:
346
+ linear-gradient(90deg, rgba(10,10,11,.035) 1px, transparent 1px) 0 0 / 42px 42px,
347
+ linear-gradient(0deg, rgba(10,10,11,.025) 1px, transparent 1px) 0 0 / 42px 42px,
348
+ var(--paper);
349
+ color: var(--ink);
350
+ font-family: var(--serif);
351
+ }
352
+ header {
353
+ display: grid;
354
+ grid-template-columns: minmax(260px, 1fr) minmax(320px, 48vw);
355
+ gap: 24px;
356
+ align-items: end;
357
+ padding: 22px 28px 16px;
358
+ border-bottom: 1px solid var(--rule);
359
+ background: rgba(255,254,250,.92);
360
+ }
361
+ h1 {
362
+ margin: 0;
363
+ font-size: clamp(34px, 5vw, 68px);
364
+ line-height: .88;
365
+ letter-spacing: 0;
366
+ }
367
+ .subtitle, label, button, select, .metric, .k, th, td {
368
+ font-family: var(--mono);
369
+ font-size: 12px;
370
+ letter-spacing: 0;
371
+ }
372
+ .subtitle { margin-top: 10px; color: var(--muted); }
373
+ .metrics { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; }
374
+ .metric { border-left: 2px solid var(--amber); min-height: 60px; padding: 7px 8px; background: rgba(247,241,231,.72); }
375
+ .metric strong { display: block; font: 700 22px/1 var(--serif); color: var(--ink); overflow-wrap: anywhere; }
376
+ main { display: grid; grid-template-columns: minmax(360px, 480px) 1fr; min-height: calc(100vh - 118px); }
377
+ aside { border-right: 1px solid var(--rule); background: rgba(255,250,242,.95); padding: 18px; }
378
+ .content { padding: 18px; display: grid; gap: 12px; align-content: start; min-width: 0; }
379
+ .stack { display: grid; gap: 12px; }
380
+ .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
381
+ .split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
382
+ .panel, textarea, pre, table, select { border: 1px solid var(--rule); border-radius: 6px; background: var(--paper); color: var(--ink); }
383
+ .panel { padding: 14px; min-width: 0; }
384
+ .panel h2 { margin: 0 0 10px; font-size: 18px; line-height: 1.1; }
385
+ textarea {
386
+ width: 100%;
387
+ min-height: 150px;
388
+ resize: vertical;
389
+ padding: 12px;
390
+ font: 12px/1.45 var(--mono);
391
+ }
392
+ #routeRequest { min-height: 118px; }
393
+ select { width: 100%; min-height: 34px; padding: 0 10px; }
394
+ button {
395
+ appearance: none;
396
+ border: 1px solid var(--ink);
397
+ background: var(--ink);
398
+ color: var(--paper);
399
+ border-radius: 4px;
400
+ min-height: 34px;
401
+ padding: 0 12px;
402
+ cursor: pointer;
403
+ white-space: nowrap;
404
+ }
405
+ button.secondary { background: transparent; color: var(--ink); border-color: var(--rule); }
406
+ .toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
407
+ .status { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; }
408
+ .status .panel { min-height: 92px; }
409
+ .k { color: var(--muted); display: block; line-height: 1.25; }
410
+ .v { display: block; margin-top: 8px; font-size: 18px; line-height: 1.12; overflow-wrap: anywhere; }
411
+ .ok { color: var(--green); }
412
+ .bad { color: var(--red); }
413
+ pre { margin: 0; padding: 14px; max-height: 520px; overflow: auto; font: 12px/1.45 var(--mono); }
414
+ table { width: 100%; border-collapse: collapse; overflow: hidden; }
415
+ th, td { padding: 9px 10px; text-align: left; border-bottom: 1px solid var(--rule); vertical-align: top; }
416
+ th { color: var(--muted); background: var(--panel); }
417
+ tr:last-child td { border-bottom: 0; }
418
+ .pill { display: inline-flex; border: 1px solid var(--rule); border-radius: 999px; min-height: 24px; align-items: center; padding: 0 9px; margin: 0 5px 5px 0; background: var(--chalk); font: 12px/1 var(--mono); }
419
+ @media (max-width: 1180px) {
420
+ header, main, .split { grid-template-columns: 1fr; }
421
+ aside { border-right: 0; border-bottom: 1px solid var(--rule); }
422
+ .metrics, .status { grid-template-columns: repeat(2, minmax(0, 1fr)); }
423
+ }
424
+ @media (max-width: 680px) {
425
+ header, aside, .content { padding: 14px; }
426
+ .grid, .metrics, .status { grid-template-columns: 1fr; }
427
+ }
428
+ </style>
429
+ </head>
430
+ <body>
431
+ <header>
432
+ <div>
433
+ <h1>AbteeX<br />SovereignCode</h1>
434
+ <div class="subtitle">Policy decision point, route plan, tool gates, audit receipt.</div>
435
+ </div>
436
+ <div class="metrics">
437
+ <div class="metric"><strong id="metricModels">0</strong>models</div>
438
+ <div class="metric"><strong id="metricRegion">NZ</strong>region</div>
439
+ <div class="metric"><strong id="metricSensitivity">-</strong>sensitivity</div>
440
+ <div class="metric"><strong id="metricRetention">0</strong>retention</div>
441
+ <div class="metric"><strong id="metricBlocked">0</strong>blocked tools</div>
442
+ </div>
443
+ </header>
444
+ <main>
445
+ <aside class="stack">
446
+ <div class="toolbar">
447
+ <button id="planBtn">Plan Turn</button>
448
+ <button class="secondary" id="evaluateBtn">Evaluate</button>
449
+ <button class="secondary" id="matrixBtn">Matrix</button>
450
+ <button class="secondary" id="providerBtn">Provider</button>
451
+ </div>
452
+ <div class="toolbar">
453
+ <button class="secondary" id="restrictedBtn">Restricted</button>
454
+ <button class="secondary" id="personalBtn">Personal</button>
455
+ </div>
456
+ <div>
457
+ <label for="toolScenario">TOOL GATE</label>
458
+ <select id="toolScenario"></select>
459
+ </div>
460
+ <div>
461
+ <label for="capsule">DATA CAPSULE</label>
462
+ <textarea id="capsule" spellcheck="false"></textarea>
463
+ </div>
464
+ <div>
465
+ <label for="request">SOVEREIGN REQUEST</label>
466
+ <textarea id="request" spellcheck="false"></textarea>
467
+ </div>
468
+ <div>
469
+ <label for="routeRequest">ROUTE REQUEST</label>
470
+ <textarea id="routeRequest" spellcheck="false"></textarea>
471
+ </div>
472
+ </aside>
473
+ <section class="content">
474
+ <div class="status">
475
+ <div class="panel"><span class="k">DECISION</span><span class="v" id="decision">pending</span></div>
476
+ <div class="panel"><span class="k">MODEL</span><span class="v" id="selected">pending</span></div>
477
+ <div class="panel"><span class="k">GRANTS</span><span class="v" id="grants">0</span></div>
478
+ <div class="panel"><span class="k">OBLIGATIONS</span><span class="v" id="obligations">0</span></div>
479
+ <div class="panel"><span class="k">NEXT GATE</span><span class="v" id="nextGate">ready</span></div>
480
+ </div>
481
+ <div class="split">
482
+ <div class="panel">
483
+ <h2>Turn Result</h2>
484
+ <pre id="output"></pre>
485
+ </div>
486
+ <div class="panel">
487
+ <h2>Operator Checklist</h2>
488
+ <table>
489
+ <thead><tr><th>Gate</th><th>Status</th><th>Detail</th></tr></thead>
490
+ <tbody id="checkRows"></tbody>
491
+ </table>
492
+ </div>
493
+ </div>
494
+ <div class="split">
495
+ <div class="panel">
496
+ <h2>Policy Matrix</h2>
497
+ <table>
498
+ <thead><tr><th>Tool</th><th>Decision</th><th>Reasons</th></tr></thead>
499
+ <tbody id="matrixRows"></tbody>
500
+ </table>
501
+ </div>
502
+ <div class="panel">
503
+ <h2>Capsule Summary</h2>
504
+ <div id="capsuleSummary"></div>
505
+ </div>
506
+ </div>
507
+ <div class="panel">
508
+ <h2>Provider Config</h2>
509
+ <pre id="provider"></pre>
510
+ </div>
511
+ </section>
512
+ </main>
513
+ <script type="application/json" id="initial-state">__INITIAL__</script>
514
+ <script>
515
+ const state = JSON.parse(document.getElementById('initial-state').textContent);
516
+ const capsule = document.getElementById('capsule');
517
+ const request = document.getElementById('request');
518
+ const routeRequest = document.getElementById('routeRequest');
519
+ const output = document.getElementById('output');
520
+ const provider = document.getElementById('provider');
521
+ const decision = document.getElementById('decision');
522
+ const selected = document.getElementById('selected');
523
+ const grants = document.getElementById('grants');
524
+ const obligations = document.getElementById('obligations');
525
+ const nextGate = document.getElementById('nextGate');
526
+ const matrixRows = document.getElementById('matrixRows');
527
+ const checkRows = document.getElementById('checkRows');
528
+ const capsuleSummary = document.getElementById('capsuleSummary');
529
+ const toolScenario = document.getElementById('toolScenario');
530
+
531
+ function writeJson(node, value) { node.textContent = JSON.stringify(value, null, 2); }
532
+ function safeJson(text) {
533
+ try { return [JSON.parse(text), null]; }
534
+ catch (error) { return [null, error.message]; }
535
+ }
536
+ function setMetric(id, value) { document.getElementById(id).textContent = value; }
537
+ setMetric('metricModels', state.model_count);
538
+ setMetric('metricRegion', state.capsule_summary.jurisdiction);
539
+ setMetric('metricSensitivity', state.capsule_summary.sensitivity);
540
+ setMetric('metricRetention', state.capsule_summary.retention_days);
541
+ setMetric('metricBlocked', state.policy_matrix.blocked_count);
542
+ toolScenario.innerHTML = state.tool_scenarios.map((item, index) => `<option value="${index}">${item.name}</option>`).join('');
543
+
544
+ function loadRestricted() {
545
+ capsule.value = JSON.stringify(state.capsule, null, 2);
546
+ request.value = JSON.stringify(state.request, null, 2);
547
+ routeRequest.value = JSON.stringify(state.route_request, null, 2);
548
+ renderCapsuleSummary(state.capsule_summary);
549
+ }
550
+ function loadPersonal() {
551
+ capsule.value = JSON.stringify(state.personal_capsule, null, 2);
552
+ request.value = JSON.stringify(state.personal_request, null, 2);
553
+ routeRequest.value = JSON.stringify(state.route_request, null, 2);
554
+ renderCapsuleSummary(state.personal_capsule_summary);
555
+ }
556
+ function readPayload(includeRoute) {
557
+ const payload = {capsule: JSON.parse(capsule.value), request: JSON.parse(request.value)};
558
+ if (includeRoute) payload.route_request = JSON.parse(routeRequest.value);
559
+ return payload;
560
+ }
561
+ function renderMatrix(matrix) {
562
+ matrixRows.innerHTML = matrix.rows.map(row => `<tr><td>${row.name}<br><span class="k">${row.tool_name}</span></td><td class="${row.allowed ? 'ok' : 'bad'}">${row.allowed ? 'allow' : 'block'}</td><td>${row.reason_count}</td></tr>`).join('');
563
+ setMetric('metricBlocked', matrix.blocked_count);
564
+ }
565
+ function renderChecklist(rows) {
566
+ checkRows.innerHTML = rows.map(row => `<tr><td>${row.item}</td><td>${row.status}</td><td>${row.detail}</td></tr>`).join('');
567
+ }
568
+ function renderCapsuleSummary(summary) {
569
+ capsuleSummary.innerHTML = `
570
+ <div class="pill">${summary.capsule_id}</div>
571
+ <div class="pill">${summary.jurisdiction}</div>
572
+ <div class="pill">${summary.sensitivity}</div>
573
+ <div class="pill">${summary.retention_days} days</div>
574
+ ${(summary.risk_flags || []).map(item => `<div class="pill">${item}</div>`).join('')}
575
+ `;
576
+ }
577
+ function setResult(data) {
578
+ writeJson(output, data);
579
+ const plan = data.plan || {};
580
+ const brief = data.turn_brief || {};
581
+ const route = plan.route_decision || {};
582
+ const model = route.selected_model || {};
583
+ const allowed = data.ok === true;
584
+ decision.textContent = allowed ? 'allowed' : 'blocked';
585
+ decision.className = allowed ? 'v ok' : 'v bad';
586
+ selected.textContent = model.model_id || brief.selected_model || 'none';
587
+ selected.className = selected.textContent !== 'none' ? 'v ok' : 'v bad';
588
+ grants.textContent = plan.tool_grants ? plan.tool_grants.length : (brief.tool_grants || 0);
589
+ obligations.textContent = plan.obligations ? plan.obligations.length : (brief.obligation_count || 0);
590
+ if (brief.operator_checklist) renderChecklist(brief.operator_checklist);
591
+ nextGate.textContent = data.operator_gate ? data.operator_gate.next_gate : (allowed ? 'execute_with_audit' : 'revise_request');
592
+ }
593
+ async function postJson(path, payload) {
594
+ const response = await fetch(path, {
595
+ method: 'POST',
596
+ headers: {'Content-Type': 'application/json'},
597
+ body: JSON.stringify(payload)
598
+ });
599
+ return response.json();
600
+ }
601
+ async function submit(includeRoute) {
602
+ let payload;
603
+ try {
604
+ payload = readPayload(includeRoute);
605
+ } catch (error) {
606
+ setResult({ok: false, error: error.message});
607
+ return;
608
+ }
609
+ setResult(await postJson(includeRoute ? '/api/plan' : '/api/evaluate', payload));
610
+ }
611
+ async function matrix() {
612
+ const [cap, capErr] = safeJson(capsule.value);
613
+ const [req, reqErr] = safeJson(request.value);
614
+ if (capErr || reqErr) { setResult({ok: false, error: capErr || reqErr}); return; }
615
+ const data = await postJson('/api/policy-matrix', {capsule: cap, request: req});
616
+ renderMatrix(data);
617
+ writeJson(output, data);
618
+ }
619
+ async function toolCheck() {
620
+ const [cap, capErr] = safeJson(capsule.value);
621
+ const [req, reqErr] = safeJson(request.value);
622
+ if (capErr || reqErr) { setResult({ok: false, error: capErr || reqErr}); return; }
623
+ const tool = state.tool_scenarios[Number(toolScenario.value)];
624
+ setResult(await postJson('/api/tool-check', {capsule: cap, request: req, tool}));
625
+ }
626
+ async function providerConfig() {
627
+ const data = await postJson('/api/opencode-config', {base_url: 'http://127.0.0.1:8787/v1'});
628
+ writeJson(provider, data.config);
629
+ writeJson(output, data);
630
+ }
631
+ document.getElementById('planBtn').addEventListener('click', () => submit(true));
632
+ document.getElementById('evaluateBtn').addEventListener('click', () => submit(false));
633
+ document.getElementById('matrixBtn').addEventListener('click', matrix);
634
+ document.getElementById('providerBtn').addEventListener('click', providerConfig);
635
+ document.getElementById('restrictedBtn').addEventListener('click', loadRestricted);
636
+ document.getElementById('personalBtn').addEventListener('click', loadPersonal);
637
+ toolScenario.addEventListener('change', toolCheck);
638
+ loadRestricted();
639
+ renderMatrix(state.policy_matrix);
640
+ writeJson(provider, state.opencode_config);
641
+ writeJson(output, {status: 'ready', registry: state.registry_path});
642
+ </script>
643
+ </body>
644
+ </html>"""
645
+ return html.replace("__INITIAL__", initial)
646
+
647
+
648
  def build_dashboard_html(state: dict[str, Any]) -> str:
649
  initial = json.dumps(state, sort_keys=True).replace("</", "<\\/")
650
  return f"""<!doctype html>