Compare commits

...
Author SHA1 Message Date
MervinPraisonandClaude Opus 4.8 2797667e04 docs: add SECURITY.md with private disclosure policy (fixes #290)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-16 16:28:01 +00:00
Mervin Praison f23aacdec9 Merge pull request #289 from MervinPraison/claude/issue-286-20260915
fix: auto-load ~/.praisonai/.env on aiui run so env: channel tokens resolve
2026-09-16 14:43:31 +01:00
MervinPraison 17cd4d9f86 Release v0.3.145 2026-09-16 13:00:52 +00:00
Mervin Praison 43935e4e41 fix: forward extra kwargs through channels chat bridge (fixes #287)
fix: forward extra kwargs through channels chat bridge (fixes #287)
2026-09-16 13:58:08 +01:00
MervinPraisonandClaude Opus 4.8 8dc8b9f49f fix: auto-load ~/.praisonai/.env on aiui run so env: tokens resolve (fixes #286)
Channels configured with env:VAR token references silently resolved to
empty strings when the dashboard was started without the shell exporting
the tokens, so bots showed 'Connected' but never replied. aiui run now
loads the standard PraisonAI .env files (~/.praisonai/.env, then ./.env)
into the process environment at startup, matching the PraisonAI CLI
convention. Existing process vars are never overridden and parsing is
dependency-free (no python-dotenv).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-15 13:16:37 +00:00
MervinPraisonandClaude Opus 4.8 135b05435c fix: forward extra kwargs through channels chat bridge (fixes #287)
The chat bridge installed over bot._session.chat used a fixed
(agent, user_id, text) signature. When the bot internals invoked the
session with per-conversation keyword arguments (chat_id, platform,
thread_id, ...) the wrapper raised
`TypeError: _wrapped_chat() got an unexpected keyword argument 'chat_id'`
and the bot silently failed to reply across all platforms (Telegram,
Slack, Discord).

Add `*args, **kwargs` to `_wrapped_chat` and forward them untouched to
the original session chat so memory/session continuity is preserved.
Minimal, backward-compatible change; no new params or dependencies.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-15 13:11:51 +00:00
MervinPraisonandCursor be502d823f chore(automation): bump hub to v1.1.8 for dual OAuth failover
Uses pick-oauth-token pre-flight probe and TOKEN_B backup.

Co-authored-by: Cursor <[email protected]>
2026-09-15 07:49:29 +01:00
Mervin Praison 9be1266747 Merge pull request #285 from MervinPraison/fix/voice-doctor-mcp-probe-timeout
fix(voice-agent): warn on slow MCP doctor probe instead of failing
2026-09-14 17:45:00 +01:00
MervinPraisonandCursor 8ce13a9fec fix(voice-agent): warn on slow MCP doctor probe instead of failing
Use a 2s timeout for GET /api/mcp/servers and downgrade timeout-style
errors to warn so stdio MCP connect does not fail the whole doctor run.

Follow-up to #273 (example-only; no core SDK changes).

Co-authored-by: Cursor <[email protected]>
2026-09-14 14:51:08 +01:00
Mervin Praison e6a346aa22 Merge pull request #284 from MervinPraison/chore/automation-hub-v1.1.7
chore(automation): bump github-automation-template hub to v1.1.7
2026-09-14 14:22:51 +01:00
MervinPraisonandCursor b7b0cc2109 chore(automation): bump github-automation-template hub to v1.1.7
Fixes FINAL detection and triggerLogins fallback for merge gate auto-merge.

Co-authored-by: Cursor <[email protected]>
2026-09-14 14:22:39 +01:00
19 changed files with 319 additions and 24 deletions

No files matched your search

+2 -2
View File
@@ -3,7 +3,7 @@
"stack": "claude",
"profile": "sdk",
"mode": "hub",
"hubRef": "v1.1.6",
"templateVersion": "1.1.6",
"hubRef": "v1.1.8",
"templateVersion": "1.1.7",
"installedAt": "2026-09-07T10:02:11Z"
}
+4
View File
@@ -4,6 +4,10 @@
module.exports = {
repoFullName: 'MervinPraison/PraisonAIUI',
gitUser: 'MervinPraison',
gitEmail: '[email protected]',
triggerLogins: ['MervinPraison', 'github-actions[bot]'],
allowedTriageBots: ['praisonai-triage-agent[bot]'],
productPathPrefixes: ['src/praisonaiui/', 'src/frontend/', 'tests/'],
sensitivePathPatterns: [
/^\.github\/workflows\//,
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
permissions:
issues: write
pull-requests: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].6
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
secrets: inherit
with:
hub_ref: v1.1.6
hub_ref: v1.1.8
+2 -2
View File
@@ -16,8 +16,8 @@ permissions:
jobs:
recover:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].6
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
secrets: inherit
with:
hub_ref: v1.1.6
hub_ref: v1.1.8
pr_number: ${{ github.event.inputs.pr_number || '' }}
+2 -2
View File
@@ -25,8 +25,8 @@ permissions:
jobs:
detect-and-trigger:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].6
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
secrets: inherit
with:
hub_ref: v1.1.6
hub_ref: v1.1.8
pr_number: ${{ github.event.inputs.pr_number || '' }}
+4 -4
View File
@@ -43,9 +43,9 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].6
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].8
with:
hub_ref: v1.1.6
hub_ref: v1.1.8
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
@@ -87,9 +87,9 @@ jobs:
issues: write
actions: read
id-token: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].6
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
secrets: inherit
with:
hub_ref: v1.1.6
hub_ref: v1.1.8
pr_number: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
merge_method: ${{ github.event.inputs.merge_method || 'merge' }}
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
issues: write
actions: read
id-token: write
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].6
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
secrets: inherit
with:
hub_ref: v1.1.6
hub_ref: v1.1.8
+2 -2
View File
@@ -25,9 +25,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].6
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].8
with:
hub_ref: v1.1.6
hub_ref: v1.1.8
- name: Detect conflicting PRs and trigger Claude
uses: actions/github-script@v7
with:
+2 -2
View File
@@ -13,7 +13,7 @@ permissions:
jobs:
sync:
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].6
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
secrets: inherit
with:
hub_ref: v1.1.6
hub_ref: v1.1.8
+37
View File
@@ -0,0 +1,37 @@
# Security Policy
We take the security of PraisonAIUI seriously and appreciate responsible
disclosure of vulnerabilities.
## Supported Versions
Security fixes are applied to the latest released version of PraisonAIUI.
Please make sure you are on the most recent release before reporting.
## Reporting a Vulnerability
**Please do not open a public issue for security vulnerabilities.**
Report privately through GitHub's **Private Vulnerability Reporting**:
1. Go to the [Security tab](https://github.com/MervinPraison/PraisonAIUI/security)
of this repository.
2. Click **"Report a vulnerability"** to open a private advisory.
3. Include as much detail as you can:
- Affected component(s) and version.
- Steps to reproduce.
- Impact assessment.
- Any suggested fix or mitigation.
If you are unable to use private reporting, you may open a public issue that
contains **no technical specifics** and requests a private channel, and a
maintainer will follow up.
## What to Expect
- We aim to acknowledge new reports within a few business days.
- We will keep you informed as we investigate and work on a fix.
- Once a fix is released, we are happy to credit you in the advisory (unless
you prefer to remain anonymous).
Thank you for helping keep PraisonAIUI and its users safe.
@@ -17,6 +17,25 @@ def _tcp_open(host: str, port: int, timeout: float = 2.0) -> bool:
return False
_MCP_PROBE_TIMEOUT_SEC = 2.0
def _is_slow_mcp_probe(detail: str) -> bool:
lowered = detail.lower()
return any(token in lowered for token in ("timed out", "timeout", "temporarily unavailable"))
def _mcp_servers_probe(app_port: int) -> tuple[str, str]:
"""Probe MCP list endpoint; slow stdio connect must not fail the whole doctor run."""
status, detail = _http_status(
f"http://127.0.0.1:{app_port}/api/mcp/servers",
timeout=_MCP_PROBE_TIMEOUT_SEC,
)
if status == "fail" and _is_slow_mcp_probe(detail):
return "warn", detail
return status, detail
def _http_status(url: str, timeout: float = 5.0) -> tuple[str, str]:
try:
req = Request(url, headers={"User-Agent": "praisonai-voice-doctor"})
@@ -64,7 +83,7 @@ def run_voice_doctor(*, app_port: int = 8001, sidecar_port: int = 8002) -> dict[
checks.append({"name": "Speech Engine config", "status": status, "detail": detail})
status, detail = _http_status(f"http://127.0.0.1:{app_port}/api/voice/realtime/config")
checks.append({"name": "GPT Realtime config", "status": status, "detail": detail})
status, detail = _http_status(f"http://127.0.0.1:{app_port}/api/mcp/servers")
status, detail = _mcp_servers_probe(app_port)
checks.append({"name": "MCP servers API", "status": status, "detail": detail})
else:
checks.append(
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "aiui"
version = "0.3.144"
version = "0.3.145"
description = "YAML-driven website generator - CLI and compiler"
readme = "README.md"
license = {text = "MIT"}
+1 -1
View File
@@ -1,3 +1,3 @@
"""Version information."""
__version__ = "0.3.144"
__version__ = "0.3.145"
+48
View File
@@ -44,6 +44,43 @@ def _icon(symbol: str, fallback: str) -> str:
return symbol if _supports_unicode() else fallback
def _load_env_files() -> int:
"""Load environment variables from the standard PraisonAI ``.env`` files.
Mirrors the PraisonAI CLI convention so ``env:VAR`` references (e.g. in
Channels config) resolve when the server was started without the shell
exporting the tokens. Reads ``~/.praisonai/.env`` first, then a ``.env`` in
the current working directory. Existing process env vars are never
overridden, and parsing is intentionally dependency-free (no python-dotenv).
Returns the number of variables newly set into ``os.environ``.
"""
import os
loaded = 0
for env_path in (Path.home() / ".praisonai" / ".env", Path.cwd() / ".env"):
try:
if not env_path.is_file():
continue
for line in env_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
if line.startswith("export "):
line = line[len("export "):]
key, value = line.split("=", 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
if key and key not in os.environ:
os.environ[key] = value
loaded += 1
except Exception:
# Never let env loading break startup — resolution falls back to
# the existing process environment.
continue
return loaded
app = typer.Typer(
name="aiui",
help="PraisonAIUI - YAML-driven website generator",
@@ -1380,6 +1417,17 @@ def run(
console.print(f"[red]Error:[/red] App file not found: {app_file}")
raise typer.Exit(code=1)
# Auto-load standard PraisonAI env files so `env:VAR` references (e.g. in
# Channels config) resolve even when the shell did not export the tokens.
# This matches the PraisonAI CLI convention and fixes the "Connected but
# bot never replies" class of issues caused by empty env-resolved tokens.
_loaded = _load_env_files()
if _loaded:
console.print(
f"[green]{_icon('✓', 'OK')}[/green] Loaded {_loaded} environment "
f"variable(s) from PraisonAI config"
)
is_yaml = app_file.suffix in (".yaml", ".yml")
if is_yaml:
+13 -2
View File
@@ -652,13 +652,24 @@ class ChannelsFeature(BaseFeatureProtocol):
if hasattr(bot, "_session") and hasattr(bot._session, "chat"):
_original_chat = bot._session.chat
async def _wrapped_chat(agent: Any, user_id: str, text: str) -> str:
async def _wrapped_chat(
agent: Any,
user_id: str,
text: str,
*args: Any,
**kwargs: Any,
) -> str:
"""Wrap agent chat to broadcast responses AND intermediate
tool/step events to the Chat UI.
Hooks into the agent's StreamEventEmitter (same pattern as
PraisonAIProvider._run_direct_mode) to capture tool_call,
reasoning, and content events during execution.
Any extra positional/keyword arguments (e.g. ``chat_id``,
``platform``, ``thread_id``) are forwarded untouched to the
wrapped session ``chat`` so per-conversation memory/session
continuity is preserved instead of raising ``TypeError``.
"""
from .chat import _enrich_tool_payload, get_chat_manager
@@ -831,7 +842,7 @@ class ChannelsFeature(BaseFeatureProtocol):
drain_task = asyncio.create_task(_drain_events())
try:
response = await _original_chat(agent, user_id, text)
response = await _original_chat(agent, user_id, text, *args, **kwargs)
finally:
# Signal drain to stop and clean up callback
try:
+88
View File
@@ -0,0 +1,88 @@
"""Regression coverage for issue #287: the channels chat bridge dropped
extra keyword arguments (e.g. ``chat_id``, ``platform``) because the
``_wrapped_chat`` wrapper installed over ``bot._session.chat`` had a fixed
``(agent, user_id, text)`` signature.
When the bot internals invoked the session with per-conversation kwargs the
wrapper raised ``TypeError: _wrapped_chat() got an unexpected keyword
argument 'chat_id'`` and the bot silently failed to reply. The wrapper must
forward any extra positional/keyword arguments untouched to the original
session ``chat`` so memory/session continuity is preserved.
"""
from __future__ import annotations
from typing import Any
import pytest
from praisonaiui.features.channels import ChannelsFeature
class _FakeSession:
"""Minimal session whose ``chat`` records forwarded arguments."""
def __init__(self) -> None:
self.received_args: tuple = ()
self.received_kwargs: dict = {}
async def chat(self, agent: Any, user_id: str, text: str, *args: Any, **kwargs: Any) -> str:
self.received_args = args
self.received_kwargs = dict(kwargs)
return f"reply to {text}"
class _FakeBot:
def __init__(self) -> None:
self._session = _FakeSession()
@pytest.mark.asyncio
async def test_wrapped_chat_forwards_kwargs() -> None:
feature = ChannelsFeature()
bot = _FakeBot()
feature._attach_chat_bridge("chan-1", bot, "telegram")
# The bridge replaced the session chat with the wrapper.
assert bot._session.chat.__name__ == "_wrapped_chat"
reply = await bot._session.chat(
object(),
"user-42",
"Hi",
chat_id="987654321",
platform="telegram",
user_id_meta="987654321",
)
assert reply == "reply to Hi"
assert bot._session.received_kwargs["chat_id"] == "987654321"
assert bot._session.received_kwargs["platform"] == "telegram"
@pytest.mark.asyncio
async def test_wrapped_chat_forwards_extra_positional_args() -> None:
feature = ChannelsFeature()
bot = _FakeBot()
feature._attach_chat_bridge("chan-2", bot, "slack")
reply = await bot._session.chat(object(), "user-7", "Hello", "extra")
assert reply == "reply to Hello"
assert bot._session.received_args == ("extra",)
@pytest.mark.asyncio
async def test_wrapped_chat_still_works_without_kwargs() -> None:
"""No-kwargs path (web UI / existing callers) must keep working."""
feature = ChannelsFeature()
bot = _FakeBot()
feature._attach_chat_bridge("chan-3", bot, "discord")
reply = await bot._session.chat(object(), "user-9", "Ping")
assert reply == "reply to Ping"
assert bot._session.received_kwargs == {}
+68
View File
@@ -0,0 +1,68 @@
"""Unit tests for `_load_env_files` — auto-loading PraisonAI `.env` files.
Regression coverage for issue #286: `env:VAR` channel token references
resolved to empty strings because `aiui run` never loaded the standard
`~/.praisonai/.env` file, so bots showed "Connected" but never replied.
"""
import os
from praisonaiui.cli import _load_env_files
def _write_env(path, body):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(body, encoding="utf-8")
def test_loads_home_praisonai_env(tmp_path, monkeypatch):
"""Variables in ~/.praisonai/.env are loaded into the process env."""
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("TELEGRAM_BOT_TOKEN", raising=False)
_write_env(tmp_path / ".praisonai" / ".env", 'TELEGRAM_BOT_TOKEN="123:AAtoken"\n')
loaded = _load_env_files()
assert loaded >= 1
assert os.environ["TELEGRAM_BOT_TOKEN"] == "123:AAtoken"
def test_does_not_override_existing_env(tmp_path, monkeypatch):
"""An already-set process variable is never clobbered by the file."""
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("SLACK_BOT_TOKEN", "xoxb-from-shell")
_write_env(tmp_path / ".praisonai" / ".env", "SLACK_BOT_TOKEN=xoxb-from-file\n")
_load_env_files()
assert os.environ["SLACK_BOT_TOKEN"] == "xoxb-from-shell"
def test_ignores_comments_blanks_and_export_prefix(tmp_path, monkeypatch):
"""Comments/blank lines are skipped and `export ` prefixes are stripped."""
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("FOO", raising=False)
monkeypatch.delenv("BAR", raising=False)
_write_env(
tmp_path / ".praisonai" / ".env",
"# a comment\n\nexport FOO=foo\nBAR='bar'\n",
)
_load_env_files()
assert os.environ["FOO"] == "foo"
assert os.environ["BAR"] == "bar"
def test_missing_files_are_harmless(tmp_path, monkeypatch):
"""No env files present → returns 0 and does not raise."""
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
monkeypatch.chdir(tmp_path)
assert _load_env_files() == 0
+20
View File
@@ -478,6 +478,26 @@ def test_voice_doctor_report(monkeypatch):
assert report["summary"]["failed"] >= 1
def test_voice_doctor_mcp_probe_timeout_is_warn(monkeypatch):
monkeypatch.setenv("OPENAI_API_KEY", "sk-test-key-12345678")
def fake_http_status(url: str, timeout: float = 5.0):
if "/api/mcp/servers" in url:
assert timeout == 2.0
return "fail", "timed out"
return "pass", "HTTP 200"
with _voice_modules() as imp:
doctor = imp("integrations.voice.doctor")
monkeypatch.setattr(doctor, "_tcp_open", lambda *args, **kwargs: True)
monkeypatch.setattr(doctor, "_http_status", fake_http_status)
report = doctor.run_voice_doctor(app_port=8001, sidecar_port=59998)
mcp = next(c for c in report["checks"] if c["name"] == "MCP servers API")
assert mcp["status"] == "warn"
assert mcp["detail"] == "timed out"
def test_call_detail_ui_includes_analytics():
with _voice_modules() as imp:
ui = imp("integrations.voice.call_detail_ui")
Generated
+1 -1
View File
@@ -207,7 +207,7 @@ wheels = [
[[package]]
name = "aiui"
version = "0.3.144"
version = "0.3.145"
source = { editable = "." }
dependencies = [
{ name = "pydantic" },