arkaos 4.1.1 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/VERSION +1 -1
  3. package/arka/SKILL.md +1 -1
  4. package/arka/skills/flow/SKILL.md +15 -0
  5. package/arka/skills/fusion/SKILL.md +91 -0
  6. package/config/constitution.yaml +15 -7
  7. package/config/hooks/session-start.sh +22 -0
  8. package/config/models.yaml +70 -0
  9. package/core/fusion/__init__.py +5 -0
  10. package/core/fusion/__pycache__/__init__.cpython-313.pyc +0 -0
  11. package/core/fusion/__pycache__/cli.cpython-313.pyc +0 -0
  12. package/core/fusion/__pycache__/engine.cpython-313.pyc +0 -0
  13. package/core/fusion/cli.py +37 -0
  14. package/core/fusion/engine.py +153 -0
  15. package/core/governance/__pycache__/redo_counter.cpython-313.pyc +0 -0
  16. package/core/governance/redo_counter.py +81 -0
  17. package/core/hooks/__pycache__/pre_tool_use.cpython-313.pyc +0 -0
  18. package/core/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
  19. package/core/hooks/pre_tool_use.py +55 -3
  20. package/core/obsidian/__pycache__/relator.cpython-313.pyc +0 -0
  21. package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
  22. package/core/runtime/__pycache__/llm_provider.cpython-314.pyc +0 -0
  23. package/core/runtime/__pycache__/model_router.cpython-313.pyc +0 -0
  24. package/core/runtime/__pycache__/model_router_cli.cpython-313.pyc +0 -0
  25. package/core/runtime/__pycache__/ollama_discovery.cpython-313.pyc +0 -0
  26. package/core/runtime/__pycache__/openrouter_provider.cpython-313.pyc +0 -0
  27. package/core/runtime/__pycache__/openrouter_provider.cpython-314.pyc +0 -0
  28. package/core/runtime/llm_provider.py +10 -1
  29. package/core/runtime/model_router.py +204 -0
  30. package/core/runtime/model_router_cli.py +151 -0
  31. package/core/runtime/ollama_discovery.py +96 -0
  32. package/core/runtime/openrouter_provider.py +172 -0
  33. package/core/sync/__pycache__/__init__.cpython-312.pyc +0 -0
  34. package/core/sync/__pycache__/engine.cpython-312.pyc +0 -0
  35. package/core/sync/__pycache__/manifest.cpython-312.pyc +0 -0
  36. package/core/workflow/__pycache__/frontend_gate.cpython-313.pyc +0 -0
  37. package/core/workflow/frontend_gate.py +162 -0
  38. package/dashboard/app/layouts/default.vue +7 -0
  39. package/dashboard/app/pages/models.vue +404 -0
  40. package/installer/autostart.js +16 -2
  41. package/installer/cli.js +23 -0
  42. package/installer/keys.js +1 -0
  43. package/package.json +1 -1
  44. package/pyproject.toml +1 -1
  45. package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
  46. package/scripts/dashboard-api.py +79 -0
  47. package/scripts/start-dashboard.sh +25 -1
@@ -0,0 +1,172 @@
1
+ """OpenRouter provider — one API key, hundreds of models.
2
+
3
+ Model Fabric PR-B. One of the backends behind the ``LLMProvider``
4
+ Protocol in ``core/runtime/llm_provider.py``; callers go through
5
+ ``get_llm_provider()`` or pass an explicit model resolved by
6
+ ``core/runtime/model_router.py`` (e.g. role ``review`` →
7
+ ``openrouter/deepseek/deepseek-v4-pro``).
8
+
9
+ OpenAI-compatible ``/api/v1/chat/completions`` via stdlib ``urllib`` —
10
+ no extra dependencies, mirroring ``ollama_provider.py``. Key resolution:
11
+ ``OPENROUTER_API_KEY`` env, then ``~/.arkaos/keys.json``. Model from
12
+ constructor, then ``OPENROUTER_MODEL`` env, then
13
+ ``models.yaml`` alias ``openrouter.default``.
14
+
15
+ OpenRouter returns token usage (and native cached-token counts for
16
+ providers that support caching) in the ``usage`` block; both are
17
+ forwarded so cost telemetry stays accurate.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import os
24
+ import urllib.error
25
+ import urllib.request
26
+ from pathlib import Path
27
+
28
+ from core.runtime.llm_provider import LLMResponse, LLMUnavailable
29
+
30
+ _API_BASE = "https://openrouter.ai/api/v1"
31
+ _TIMEOUT_S = 180 # frontier models on long prompts; fusion panels wait
32
+ _KEYS_PATH = Path.home() / ".arkaos" / "keys.json"
33
+
34
+ # Attribution headers — OpenRouter ranks apps by these; harmless otherwise.
35
+ _APP_HEADERS = {
36
+ "HTTP-Referer": "https://github.com/andreagroferreira/arka-os",
37
+ "X-Title": "ArkaOS",
38
+ }
39
+
40
+
41
+ def _read_key_from_keys_json() -> str:
42
+ try:
43
+ data = json.loads(_KEYS_PATH.read_text(encoding="utf-8"))
44
+ except (OSError, json.JSONDecodeError):
45
+ return ""
46
+ value = data.get("OPENROUTER_API_KEY", "")
47
+ return value.strip() if isinstance(value, str) else ""
48
+
49
+
50
+ def _default_model_from_fabric() -> str:
51
+ """Resolve the `openrouter.default` alias from models.yaml, if set."""
52
+ try:
53
+ from core.runtime.model_router import load_config
54
+ config, _ = load_config()
55
+ except Exception:
56
+ return ""
57
+ return config.aliases.get("openrouter", {}).get("default", "")
58
+
59
+
60
+ class OpenRouterProvider:
61
+ """Provider that hits the OpenRouter chat-completions API."""
62
+
63
+ ENV_KEY = "OPENROUTER_API_KEY"
64
+ ENV_MODEL = "OPENROUTER_MODEL"
65
+
66
+ def __init__(self, model: str | None = None, api_base: str | None = None) -> None:
67
+ self._model_override = model
68
+ self._api_base = (api_base or _API_BASE).rstrip("/")
69
+
70
+ def name(self) -> str:
71
+ return "openrouter"
72
+
73
+ def _resolve_key(self) -> str:
74
+ env_value = os.environ.get(self.ENV_KEY, "").strip()
75
+ return env_value or _read_key_from_keys_json()
76
+
77
+ def _resolve_model(self) -> str:
78
+ if self._model_override:
79
+ return self._model_override
80
+ env_value = os.environ.get(self.ENV_MODEL, "").strip()
81
+ return env_value or _default_model_from_fabric()
82
+
83
+ def is_available(self) -> bool:
84
+ """Static capability check: a key and a model are configured.
85
+
86
+ No network probe — OpenRouter is a paid remote API and this is
87
+ called on every provider-chain walk; a dead network surfaces as
88
+ ``LLMUnavailable`` at call time and the chain moves on.
89
+ """
90
+ return bool(self._resolve_key() and self._resolve_model())
91
+
92
+ def complete(
93
+ self,
94
+ prompt: str,
95
+ *,
96
+ max_tokens: int = 2000,
97
+ system: str = "",
98
+ ) -> LLMResponse:
99
+ key = self._resolve_key()
100
+ if not key:
101
+ raise LLMUnavailable(
102
+ "OpenRouter key not configured (set OPENROUTER_API_KEY or "
103
+ "add it via `npx arkaos keys`)"
104
+ )
105
+ model = self._resolve_model()
106
+ if not model:
107
+ raise LLMUnavailable(
108
+ "OpenRouter model not configured (set OPENROUTER_MODEL, pass "
109
+ "model=, or set aliases.openrouter.default in models.yaml)"
110
+ )
111
+ data = self._post_chat(key, self._build_payload(model, prompt, system, max_tokens))
112
+ return self._to_response(data, model)
113
+
114
+ def _build_payload(
115
+ self, model: str, prompt: str, system: str, max_tokens: int
116
+ ) -> dict:
117
+ messages: list[dict] = []
118
+ if system:
119
+ messages.append({"role": "system", "content": system})
120
+ messages.append({"role": "user", "content": prompt})
121
+ return {
122
+ "model": model,
123
+ "messages": messages,
124
+ "max_tokens": max_tokens,
125
+ "stream": False,
126
+ "usage": {"include": True}, # cost + cached tokens in response
127
+ }
128
+
129
+ def _post_chat(self, key: str, payload: dict) -> dict:
130
+ request = urllib.request.Request(
131
+ f"{self._api_base}/chat/completions",
132
+ data=json.dumps(payload).encode("utf-8"),
133
+ headers={
134
+ "Content-Type": "application/json",
135
+ "Authorization": f"Bearer {key}",
136
+ **_APP_HEADERS,
137
+ },
138
+ )
139
+ try:
140
+ with urllib.request.urlopen(request, timeout=_TIMEOUT_S) as response:
141
+ raw = response.read().decode("utf-8")
142
+ except urllib.error.HTTPError as exc:
143
+ detail = exc.read().decode("utf-8", errors="replace")[:300]
144
+ raise LLMUnavailable(
145
+ f"OpenRouter HTTP {exc.code}: {detail}"
146
+ ) from exc
147
+ except (urllib.error.URLError, TimeoutError, OSError) as exc:
148
+ raise LLMUnavailable(f"OpenRouter request failed: {exc}") from exc
149
+ try:
150
+ return json.loads(raw)
151
+ except json.JSONDecodeError as exc:
152
+ raise LLMUnavailable(f"OpenRouter returned invalid JSON: {exc}") from exc
153
+
154
+ def _to_response(self, data: dict, model: str) -> LLMResponse:
155
+ if data.get("error"):
156
+ message = data["error"].get("message", "unknown error")
157
+ raise LLMUnavailable(f"OpenRouter error: {message}")
158
+ choices = data.get("choices") or []
159
+ text = ""
160
+ if choices:
161
+ text = ((choices[0].get("message") or {}).get("content") or "").strip()
162
+ usage = data.get("usage") or {}
163
+ cached = int(
164
+ (usage.get("prompt_tokens_details") or {}).get("cached_tokens", 0) or 0
165
+ )
166
+ return LLMResponse(
167
+ text=text,
168
+ tokens_in=int(usage.get("prompt_tokens", 0) or 0),
169
+ tokens_out=int(usage.get("completion_tokens", 0) or 0),
170
+ cached_tokens=cached,
171
+ model=str(data.get("model") or model),
172
+ )
@@ -0,0 +1,162 @@
1
+ """Frontend excellence gate — PreToolUse enforcement for UI file edits.
2
+
3
+ Constitution: ``excellence-mandate`` (NON-NEGOTIABLE, Excellence Reform
4
+ 2026-07-05). UI work must load the frontend design skills and judge
5
+ against a named benchmark BEFORE code is written. This gate makes that
6
+ duty mechanical: a Write/Edit/MultiEdit touching a UI file requires an
7
+ ``[arka:design] <skills/benchmark>`` marker in the recent assistant
8
+ messages — the same ceremony contract as ``[arka:routing]``
9
+ (flow_enforcer) and ``[arka:dispatch]`` (specialist_enforcer).
10
+
11
+ Modes via ``hooks.frontendGate`` in ``~/.arkaos/config.json``:
12
+
13
+ absent / "warn" → nudge on stderr, allow (rollout default)
14
+ true / "hard" → deny UI edits without the marker
15
+ false / "off" → gate disabled
16
+
17
+ Bypasses: ``[arka:trivial] <reason>`` marker (same contract as the
18
+ evidence flow — single-file edits under 10 lines) and
19
+ ``ARKA_BYPASS_DESIGN=1`` env (logged for accountability).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import os
26
+ import re
27
+ from dataclasses import asdict, dataclass
28
+ from datetime import datetime, timezone
29
+ from pathlib import Path
30
+
31
+ from core.shared import safe_session_id as _safe_session_id_module
32
+ from core.workflow.flow_enforcer import (
33
+ TRIVIAL_RE,
34
+ _load_last_assistant_messages,
35
+ )
36
+ from core.workflow.specialist_enforcer import _locked_append
37
+
38
+ CONFIG_PATH = Path.home() / ".arkaos" / "config.json"
39
+ TELEMETRY_PATH = Path.home() / ".arkaos" / "telemetry" / "frontend-gate.jsonl"
40
+
41
+ ASSISTANT_WINDOW = 20
42
+
43
+ DESIGN_RE = re.compile(r"\[arka:design\]\s*\S+", re.IGNORECASE)
44
+
45
+ _GATED_TOOLS = frozenset({"Write", "Edit", "MultiEdit"})
46
+
47
+ # Component + stylesheet extensions. Plain .ts/.js are excluded: they are
48
+ # ambiguous (stores, utils, configs) and would flood the gate with false
49
+ # positives — the component file is where the visual decision lands.
50
+ UI_SUFFIXES = frozenset({
51
+ ".vue", ".tsx", ".jsx", ".svelte", ".astro",
52
+ ".css", ".scss", ".sass", ".less",
53
+ })
54
+
55
+
56
+ @dataclass
57
+ class Decision:
58
+ """Outcome of frontend-gate evaluation."""
59
+
60
+ allow: bool
61
+ reason: str
62
+ mode: str = "warn"
63
+ target_file: str = ""
64
+ marker_found: str | None = None
65
+
66
+ def to_stderr_message(self) -> str:
67
+ if self.reason not in ("no-design-marker",):
68
+ return ""
69
+ head = "[arka:suggest]" if self.allow else "[ARKA:DESIGN]"
70
+ verb = "should" if self.allow else "MUST"
71
+ return (
72
+ f"{head} UI edit to {self.target_file or 'this file'} without "
73
+ f"design evidence. Frontend work {verb} load the design skills "
74
+ f"(frontend-design, ui-ux-pro-max, project design system) at "
75
+ f"maximum effort and judge against a named benchmark FIRST "
76
+ f"(constitution `excellence-mandate`). Emit "
77
+ f"`[arka:design] <skills> benchmark=<name>` before UI edits, "
78
+ f"or `[arka:trivial] <reason>` for sub-10-line fixes."
79
+ )
80
+
81
+
82
+ def _mode() -> str:
83
+ """Resolve ``hooks.frontendGate`` to 'off' | 'warn' | 'hard'."""
84
+ if not CONFIG_PATH.exists():
85
+ return "warn"
86
+ try:
87
+ data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
88
+ except (json.JSONDecodeError, OSError):
89
+ return "warn"
90
+ raw = data.get("hooks", {}).get("frontendGate", "warn")
91
+ if raw in (False, "off", "false"):
92
+ return "off"
93
+ if raw in (True, "hard", "true"):
94
+ return "hard"
95
+ return "warn"
96
+
97
+
98
+ def is_ui_file(file_path: str) -> bool:
99
+ """True when the path carries a gated UI extension."""
100
+ return Path(file_path).suffix.lower() in UI_SUFFIXES
101
+
102
+
103
+ def _scan(messages: list[str]) -> str | None:
104
+ """Return the matched evidence marker, or None."""
105
+ for message in messages:
106
+ match = DESIGN_RE.search(message) or TRIVIAL_RE.search(message)
107
+ if match:
108
+ return match.group(0)
109
+ return None
110
+
111
+
112
+ def evaluate(
113
+ tool_name: str,
114
+ transcript_path: str,
115
+ session_id: str,
116
+ cwd: str,
117
+ tool_input: dict,
118
+ messages: list[str] | None = None,
119
+ ) -> Decision:
120
+ """Evaluate one tool call against the frontend excellence gate."""
121
+ file_path = str(tool_input.get("file_path", ""))
122
+ if tool_name not in _GATED_TOOLS or not is_ui_file(file_path):
123
+ return Decision(allow=True, reason="not-ui-scope", target_file=file_path)
124
+ mode = _mode()
125
+ if mode == "off":
126
+ return Decision(allow=True, reason="flag-off", mode=mode,
127
+ target_file=file_path)
128
+ if os.environ.get("ARKA_BYPASS_DESIGN") == "1":
129
+ return Decision(allow=True, reason="env-bypass", mode=mode,
130
+ target_file=file_path)
131
+ if messages is None:
132
+ messages = _load_last_assistant_messages(
133
+ transcript_path, ASSISTANT_WINDOW
134
+ )
135
+ marker = _scan(messages)
136
+ if marker is not None:
137
+ return Decision(allow=True, reason="design-evidence", mode=mode,
138
+ target_file=file_path, marker_found=marker)
139
+ return Decision(allow=(mode != "hard"), reason="no-design-marker",
140
+ mode=mode, target_file=file_path)
141
+
142
+
143
+ def record_telemetry(session_id: str, tool: str, decision: Decision) -> None:
144
+ """Append a structured record to the frontend-gate telemetry log.
145
+
146
+ Drops the record silently when session_id fails the safe-id check
147
+ (path-traversal mitigation, CWE-22). Never blocks the hook.
148
+ """
149
+ safe = _safe_session_id_module.safe_session_id(session_id)
150
+ if safe is None:
151
+ return
152
+ entry = {
153
+ "ts": datetime.now(timezone.utc).isoformat(),
154
+ "session_id": safe,
155
+ "tool": tool,
156
+ **asdict(decision),
157
+ }
158
+ try:
159
+ with _locked_append(TELEMETRY_PATH) as fh:
160
+ fh.write(json.dumps(entry) + "\n")
161
+ except OSError:
162
+ pass
@@ -52,6 +52,13 @@ const links = [[{
52
52
  onSelect: () => {
53
53
  open.value = false
54
54
  }
55
+ }, {
56
+ label: 'Models',
57
+ icon: 'i-lucide-layers',
58
+ to: '/models',
59
+ onSelect: () => {
60
+ open.value = false
61
+ }
55
62
  }, {
56
63
  label: 'Tasks',
57
64
  icon: 'i-lucide-list-checks',