Skip to content

fix: persist gateway signing secret so pairing/approval survive restarts - #3781

Open
praisonai-triage-agent[bot] wants to merge 2 commits into
mainfrom
claude/issue-3779-20260807-0915
Open

fix: persist gateway signing secret so pairing/approval survive restarts#3781
praisonai-triage-agent[bot] wants to merge 2 commits into
mainfrom
claude/issue-3779-20260807-0915

Conversation

@praisonai-triage-agent

@praisonai-triage-agent praisonai-triage-agent Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #3779

Summary

The gateway signs pairing codes and interactive approve/deny buttons with HMAC secrets. The callback-button path (bots/_pairing_ui.py) defaulted to a per-process random secret under a different env var (PRAISONAI_CALLBACK_SECRET), so any pending Approve/Deny button issued before a restart silently failed signature verification afterwards.

This PR unifies both signing helpers onto the single persisted, auto-provisioned secret that the pairing-code path already used (~/.praisonai/gateway/.gateway_secret, created 0600), so buttons and codes keep working across restarts with zero operator action. An explicit env override still wins.

Changes

  • bots/_pairing_ui.py: _get_callback_secret() now resolves through the persisted gateway secret store (_load_or_create_secret). PRAISONAI_CALLBACK_SECRET override still takes precedence; the persisted read is cached.
  • gateway/pairing.py: collapsed the stale ephemeral _get_secret() onto the persisted store and fixed its docstring (it previously claimed codes would not survive restarts).
  • doctor/checks/bot_checks.py: corrected the security-check message β€” the secret now persists by default; the env-var is only needed to share one secret across hosts.

Layer placement

Wrapper only (gateway operational state). No core SDK, no new params, no new dependencies.

Validation

  • Pairing e2e tests pass (3 passed).
  • Doctor bot-security tests pass (6 passed).
  • Functional check: callback signature verifies after a simulated restart (secret reloaded from disk 0600); explicit env override wins; _get_secret() stable across calls.

Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Pairing codes now remain stable across gateway restarts when the gateway secret is persisted.
    • Callback authentication now consistently uses the configured secret, a persisted secret, or a safe temporary fallback.
    • Improved secret handling prevents unnecessary regeneration and pairing interruptions.
  • Documentation

    • Updated diagnostic guidance to explain automatic secret provisioning and recommend configuring a shared gateway secret when running across multiple hosts.

…tons survive restarts (fixes #3779)

Route the callback-button HMAC secret (bots/_pairing_ui.py) through the
same auto-provisioned, owner-only persisted secret used by the pairing
code path, so approve/deny buttons keep verifying across process restarts
with zero operator action. Explicit PRAISONAI_CALLBACK_SECRET override
still wins. Also collapse the stale ephemeral _get_secret() onto the
persisted store and correct the doctor check message.

Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com>
@MervinPraison

Copy link
Copy Markdown
Owner

@coderabbitai review

@MervinPraison

Copy link
Copy Markdown
Owner

/review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor
βœ… Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@MervinPraison MervinPraison added pipeline/blocked:ci Blocked: CI not green on HEAD pipeline/blocked:no-final Blocked: no FINAL @claude trigger yet pipeline/final-claude-pending Reviews done; waiting for FINAL @claude labels Aug 7, 2026
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more β†’

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account β†’

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us β†’

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 525bfacd-0688-42b0-a4f2-260ff4d0259f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • πŸ” Trigger review
πŸ“ Walkthrough

Walkthrough

Gateway pairing and callback HMAC secrets now use an explicit environment override or a persisted gateway secret. The gateway creates and reuses the secret across restarts. Doctor guidance now describes this behavior and cross-host configuration.

Changes

Gateway secret persistence

Layer / File(s) Summary
Shared persisted secret resolution
src/praisonai-bot/praisonai_bot/gateway/pairing.py, src/praisonai-bot/praisonai_bot/bots/_pairing_ui.py
Pairing and callback HMAC secrets now prefer their environment overrides, then use the persisted gateway secret. Pairing falls back to the shared loader, while callback retrieval retains a defensive per-process fallback if persistence fails.
Secret configuration guidance
src/praisonai-code/praisonai_code/cli/features/doctor/checks/bot_checks.py
The doctor warning now describes the persisted auto-provisioned secret and recommends PRAISONAI_GATEWAY_SECRET for sharing a secret across hosts.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: mervinpraison

πŸš₯ Pre-merge checks | βœ… 5
βœ… Passed checks (5 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title clearly summarizes the primary change: persisting the gateway signing secret so pairing and approval survive restarts.
Linked Issues check βœ… Passed The changes satisfy issue #3779 by sharing a persisted gateway secret, preserving environment overrides, and supporting restart-safe pairing and approvals.
Out of Scope Changes check βœ… Passed The modified files address only gateway secret persistence, callback signing, pairing signing, and the related doctor warning.
Docstring Coverage βœ… Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
πŸ“ Generate docstrings
  • Create stacked PR
  • Commit on current branch
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-3779-20260807-0915

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR unifies callback and pairing-code signing around the persisted gateway secret while retaining explicit environment overrides.

  • Routes callback signing through the auto-provisioned gateway secret store.
  • Hardens secret-file reads, permissions, and concurrent creation.
  • Updates gateway diagnostics and an interactive-surface test stub.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains within the eligible follow-up-review scope.

No blocking failure remains.

Important Files Changed

Filename Overview
src/praisonai-bot/praisonai_bot/bots/_pairing_ui.py Callback signing now uses the persisted gateway secret by default while preserving the callback-specific override and a logged degraded fallback.
src/praisonai-bot/praisonai_bot/gateway/pairing.py Pairing secret resolution now consistently uses the persisted store, with fail-closed reads and exclusive concurrent creation.
src/praisonai-code/praisonai_code/cli/features/doctor/checks/bot_checks.py Diagnostic guidance now explains automatic persistence and the multi-host override requirements.
src/praisonai/tests/unit/cli/test_interactive_surfaces_parity.py The test double accepts the newly supported read-only execution argument.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Pairing code or callback signing] --> B{Explicit override configured?}
  B -->|Callback override| C[PRAISONAI_CALLBACK_SECRET]
  B -->|Gateway override| D[PRAISONAI_GATEWAY_SECRET]
  B -->|No override| E[Load persisted gateway secret]
  E --> F{Secret file exists?}
  F -->|Valid| G[Read and enforce owner-only permissions]
  F -->|Missing| H[Create secret file exclusively with mode 0600]
  C --> I[Sign or verify HMAC]
  D --> I
  G --> I
  H --> I
Loading

Reviews (2): Last reviewed commit: "fix: harden gateway secret store and fix..." | Re-trigger Greptile

@MervinPraison

Copy link
Copy Markdown
Owner

@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.

Phase 1: Review per AGENTS.md

  1. Protocol-driven: check heavy implementations vs core SDK
  2. Backward compatible: ensure zero feature regressions
  3. Performance: no hot-path regressions
  4. SDK value: review in depth whether the change genuinely adds value to the SDK β€” never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep
  5. Do not bloat the Agent class with additional params β€” only if absolutely required; we already support many params.
  6. Repo routing: agent-callable tools β†’ PraisonAI-Tools; lifecycle plugins β†’ PraisonAI-Plugins; optional sandbox backends β†’ PraisonAI-Plugins (praisonai.sandbox entry point) β€” request changes if wrongly added to praisonaiagents/

Phase 2: FIX Valid Issues
7. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix
8. Also independently identify and fix any gaps or issues you find in the changed code β€” do not rely only on prior reviewer feedback
9. Push all code fixes directly to THIS branch (do NOT create a new PR)
10. Comment a summary of exact files modified and what you skipped

Phase 3: Final Verdict
11. If all issues are resolved, approve the PR / close the Issue
12. If blocking issues remain, request changes / leave clear action items

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

πŸ€– Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/praisonai-bot/praisonai_bot/bots/_pairing_ui.py`:
- Around line 28-35: Update _get_secret to remove the secrets.token_hex(32)
fallback and broad Exception suppression, allowing failures from
_load_or_create_secret and secret-store permission operations to propagate. If
documented storage failures must be handled, catch only those specific
exceptions, reuse one shared cached fallback, and emit an explicit warning so
the key remains consistent with gateway pairing codes.

In `@src/praisonai-bot/praisonai_bot/gateway/pairing.py`:
- Line 41: Update _load_or_create_secret to reject empty or whitespace-only
persisted content before decoding or using it. Only create and persist a new
secret when the path is missing; propagate read failures for existing paths
without overwriting them, ensuring pairing and callback signing never use an
empty key.
- Line 41: Update _load_or_create_secret to make initial .gateway_secret
creation atomic by using exclusive file creation (O_CREAT | O_EXCL) or an
equivalent lock. When another process wins the race, reread the existing secret
instead of generating or returning a different value, while preserving the
normal load behavior.

In `@src/praisonai-code/praisonai_code/cli/features/doctor/checks/bot_checks.py`:
- Around line 161-165: Update the gateway secret warning in the global_warnings
flow to document the PRAISONAI_CALLBACK_SECRET override: instruct users to unset
it or set it to the same value on every host so callback signatures align, while
retaining the existing PRAISONAI_GATEWAY_SECRET guidance.
πŸͺ„ Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 27d15325-3cea-44f1-bfa0-4398f7902f28

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 2251822 and fa702ec.

πŸ“’ Files selected for processing (3)
  • src/praisonai-bot/praisonai_bot/bots/_pairing_ui.py
  • src/praisonai-bot/praisonai_bot/gateway/pairing.py
  • src/praisonai-code/praisonai_code/cli/features/doctor/checks/bot_checks.py

Comment on lines +28 to +35
try:
from praisonai_bot.gateway.pairing import _DEFAULT_STORE_DIR, _load_or_create_secret

return _load_or_create_secret(_DEFAULT_STORE_DIR).decode()
except Exception: # pragma: no cover - defensive fallback
import secrets

return secrets.token_hex(32)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ—„οΈ Data Integrity & Integration | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate relevant files =="
fd -a '_pairing_ui.py|pairing.py' . || true

echo "== git diff stat =="
git diff --stat || true

echo "== inspect _pairing_ui.py around relevant section =="
if [ -f src/praisonai-bot/praisonai_bot/bots/_pairing_ui.py ]; then
  wc -l src/praisonai-bot/praisonai_bot/bots/_pairing_ui.py
  cat -n src/praisonai-bot/praisonai_bot/bots/_pairing_ui.py | sed -n '1,140p'
fi

echo "== locate shared secret symbols =="
python3 - <<'PY'
from pathlib import Path
root = Path('src/praisonai-bot')
for p in root.rglob('*.py'):
    if (p.name == 'pairing.py' or p.name == '_pairing_ui.py') != 1:
        # print pairs where at least one target exists currently
        pass
for p in [root/'praisonai_bot'/gateway/'pairing.py', root/'praisonai_bot'/bots/'_pairing_ui.py']:
    if p.exists():
        print(f"-- {p} --")
        lines = p.read_text().splitlines()
        for i,l in enumerate(lines,1):
            if 'secret' in l.lower() or '_DEFAULT_STORE_DIR' in l or '_load_or_create_secret' in l:
                print(f"{i}: {l}")
PY

Repository: MervinPraison/PraisonAI

Length of output: 6742


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect gateway pairing.py secret symbols =="
wc -l src/praisonai-bot/praisonai_bot/gateway/pairing.py
cat -n src/praisonai-bot/praisonai_bot/gateway/pairing.py | sed -n '1,260p'

echo "== inspect bot pairing command secret symbols =="
wc -l src/praisonai-bot/praisonai_bot/cli/commands/pairing.py
cat -n src/praisonai-bot/praisonai_bot/cli/commands/pairing.py | sed -n '1,220p'

echo "== inspect callback verify usages =="
rg -n "_get_callback_secret|verify.*callback|hmac|callback_secret|PRAISONAI_CALLBACK_SECRET" src/praisonai-bot/praisonai_bot -S

Repository: MervinPraison/PraisonAI

Length of output: 24308


Do not replace secret-store failures with an untracked random key.

_get_secret() and _load_or_create_secret() can fail while reading, saving, or signing secret permissions. This except Exception, secrets.token_hex(32) fallback lets those failures pass, and the random key can differ from the gateway secret already signed into pairing codes. Let unexpected failures propagate, or catch only documented storage/read/write failures with one shared cached fallback and an explicit warning.

🧰 Tools
πŸͺ› Ruff (0.16.1)

[warning] 32-32: Do not catch blind exception: Exception

(BLE001)

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-bot/praisonai_bot/bots/_pairing_ui.py` around lines 28 - 35,
Update _get_secret to remove the secrets.token_hex(32) fallback and broad
Exception suppression, allowing failures from _load_or_create_secret and
secret-store permission operations to propagate. If documented storage failures
must be handled, catch only those specific exceptions, reuse one shared cached
fallback, and emit an explicit warning so the key remains consistent with
gateway pairing codes.

Source: Linters/SAST tools

use) so pairing codes survive process restarts with zero operator action.
"""
return os.environ.get("PRAISONAI_GATEWAY_SECRET", "") or secrets.token_hex(32)
return _load_or_create_secret(_DEFAULT_STORE_DIR).decode()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate file:"
fd -a 'pairing.py$' . || true

echo
echo "Inspect pairing.py outline/imports and relevant functions:"
FILE="$(fd 'pairing.py$' . | head -n 1 || true)"
if [ -n "${FILE:-}" ]; then
  wc -l "$FILE"
  ast-grep outline "$FILE" || true
  sed -n '1,140p' "$FILE"
fi

echo
echo "Search _load_or_create_secret and signing usages:"
rg -n "_load_or_create_secret|_DEFAULT_STORE_DIR|pairing|callback|sign|hmac|key" -S .

Repository: MervinPraison/PraisonAI

Length of output: 50380


Fail closed on empty or unreadable persisted pairing state.

_load_or_create_secret accepts whitespace-only files after strip(), allowing an empty HMAC key. On read failures, create a new secret only for missing paths; do not overwrite an existing persisted secret. Reject empty content before creation so pairing/callback signatures cannot be generated or verified with an empty key.

[security_and_privACY]

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-bot/praisonai_bot/gateway/pairing.py` at line 41, Update
_load_or_create_secret to reject empty or whitespace-only persisted content
before decoding or using it. Only create and persist a new secret when the path
is missing; propagate read failures for existing paths without overwriting them,
ensuring pairing and callback signing never use an empty key.

πŸ—„οΈ Data Integrity & Integration | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching pairing.py:"
fd -a 'pairing\.py$' . || true

echo
echo "Target file snippet:"
if [ -f src/praisonai-bot/praisonai_bot/gateway/pairing.py ]; then
  nl -ba src/praisonai-bot/praisonai_bot/gateway/pairing.py | sed -n '1,140p'
else
  echo "target file not found"
fi

echo
echo "Search secret resolver usage:"
rg -n "_load_or_create_secret|gateway_secret|secret" -S --glob '*.py' .

Repository: MervinPraison/PraisonAI

Length of output: 463


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import os, tempfile, time

def both_like_current(path):
    if not os.path.exists(path):
        fd = os.open(path, os.O_CREAT|os.O_TRUNC, 0o600)
        with os.fdopen(fd, 'w') as f:
            f.write('secret\n')
        time.sleep(0.1)
        return open(path).read().rstrip()
    return open(path).read().rstrip()

with tempfile.TemporaryDirectory() as d:
    path = os.path.join(d, 'file')
    # first checks, second checks, both open/write via os.open/trunc
    t1 = os.dup(os.open(path, os.O_CREAT|os.O_TRUNC|os.O_EXCL, 0o600))
    os.write(t1, b'winner\n')
    os.close(t1)
    # now show race with O_CREAT|O_TRUNC equivalent: second opens and truncates
    t2 = os.dup(os.open(path, os.O_CREAT|os.O_TRUNC, 0o600))
    os.write(t2, b'loser\n')
    os.close(t2)
    print(os.path.exists(path))
    with open(path) as f:
        print(f.read().rstrip())
    # show O_EXCL behavior
    path2 = os.path.join(d, 'unique')
    fd = os.dup(os.open(path2, os.O_CREAT|os.O_TRUNC|os.O_EXCL, 0o600))
</PY>

Repository: MervinPraison/PraisonAI

Length of output: 318


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Target file excerpt:"
awk '{printf "%5d  %s\n", NR, $0}' src/praisonai-bot/praisonai_bot/gateway/pairing.py | sed -n '1,180p'

echo
echo "Related files excerpt:"
for f in src/praisonai-bot/praisonai_bot/cli/commands/pairing.py src/praisonai/praisonai/cli/commands/pairing.py; do
  if [ -f "$f" ]; then
    echo "--- $f"
    awk '{printf "%5d  %s\n", NR, $0}' "$f" | sed -n '1,180p'
  fi
done

echo
echo "Secret resolver references:"
python3 - <<'PY'
import os, re
for root in ["."]:
    for dirpath, _, filenames in os.walk(root):
        if ".git" in dirpath or dirpath == "./.git":
            continue
        for name in filenames:
            if not name.endswith(".py"):
                continue
            path = os.path.join(dirpath, name)
            try:
                text = open(path, "r", encoding="utf-8").read()
            except Exception:
                continue
            if re.search(r"_load_or_create_secret|O_CREAT|O_TRUNC|O_EXCL|flock|fcntl\.lock|lockf|gateway_secret", text):
                print(path)
                for i, line in enumerate(text.splitlines(), 1):
                    if re.search(r"_load_or_create_secret|O_CREAT|O_TRUNC|O_EXCL|flock|fcntl\.lock|lockf|gateway_secret", line):
                        print(f"{i}: {line}")
PY

echo
echo "OS open race probe:"
python3 - <<'PY'
import os, tempfile, tempfile, threading, time

path = None
def waiter(path):
    time.sleep(0.02)
    fd = os.open(path, os.O_CREAT | os.O_TRUNC, 0o600)
    os.write(fd, b"loser\n")
    os.close(fd)
def opener(path):
    time.sleep(0.02)
    fd = os.open(path, os.O_CREAT | os.O_TRUNC, 0o600)
    os.write(fd, b"winner\n")
    os.close(fd)

with tempfile.TemporaryDirectory() as td:
    path = os.path.join(td, "racefile")
    t1 = threading.Thread(target=opener, args=(path,))
    t2 = threading.Thread(target=waiter, args=(path,))
    t1.start(); t2.start(); t1.join(); t2.join()
    print(path, open(path).read().rstrip())

with tempfile.TemporaryDirectory() as td:
    path = os.path.join(td, "exclfile")
    fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_TRUNC, 0o600)
    try:
        fd2 = os.open(path, os.O_CREAT | os.O_EXCL | os.O_TRUNC, 0o600)
        os.write(fd2, b"second\n")
        os.close(fd2)
        print("excl-second-open", open(path).read().rstrip())
    except OSError as exc:
        print("excl-second-open-fail", type(exc).__name__, exc.errno, open(path).read().rstrip())
PY

Repository: MervinPraison/PraisonAI

Length of output: 28002


Make first-time secret creation atomic.

_load_or_create_secret() checks .gateway_secrets existence, then another process can create it before the current process opens it with O_CREAT | O_TRUNC. That path writes one secret but both processes can return different secrets, so one can issue pairing codes that fail under the receiver’s HMAC key. Use exclusive creation with O_CREAT | O_EXCL or a file lock, then reread the winner’s .gateway_secret.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-bot/praisonai_bot/gateway/pairing.py` at line 41, Update
_load_or_create_secret to make initial .gateway_secret creation atomic by using
exclusive file creation (O_CREAT | O_EXCL) or an equivalent lock. When another
process wins the race, reread the existing secret instead of generating or
returning a different value, while preserving the normal load behavior.

Comment on lines +161 to +165
global_warnings.append(
"PRAISONAI_GATEWAY_SECRET not set - using auto-provisioned "
"persisted secret (~/.praisonai/gateway/.gateway_secret); set "
"the env-var to share one secret across hosts"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ—„οΈ Data Integrity & Integration | 🟑 Minor | ⚑ Quick win

Document the callback override in cross-host guidance.

_get_callback_secret() uses PRAISONAI_CALLBACK_SECRET before PRAISONAI_GATEWAY_SECRET. Setting only the variable named here does not align callback signatures when PRAISONAI_CALLBACK_SECRET has a different value. State that the callback override must be unset or identical on every host.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-code/praisonai_code/cli/features/doctor/checks/bot_checks.py`
around lines 161 - 165, Update the gateway secret warning in the global_warnings
flow to document the PRAISONAI_CALLBACK_SECRET override: instruct users to unset
it or set it to the same value on every host so callback signatures align, while
retaining the existing PRAISONAI_GATEWAY_SECRET guidance.

@MervinPraison MervinPraison added pipeline/awaiting-merge-gate FINAL done; waiting for merge gate / CI pipeline/blocked:cooldown Blocked: post-push or @claude cooldown claude-ci-fix-pending and removed pipeline/final-claude-pending Reviews done; waiting for FINAL @claude pipeline/blocked:no-final Blocked: no FINAL @claude trigger yet labels Aug 7, 2026
@MervinPraison

Copy link
Copy Markdown
Owner

@claude CI failed on HEAD fa702ec2. Please fix the failures below and push to this branch.

Failed checks

Failures (extracted)

  1. tests/unit/cli/test_interactive_surfaces_parity.py::test_async_tui_expands_at_mentions β€” KeyError: 'prompt'
    • Job: test-core (cli)

Critical review first

Before changing code or tests, decide which side is wrong:

  1. Legitimate feature change β€” the PR intent is correct but implementation or tests need updating. Preserve SDK guarantees; update tests only when behaviour intentionally changed and document why.
  2. Regression / bug in this PR β€” the failure exposes a real breakage introduced here. Fix the implementation; do not weaken, skip, or delete tests just to go green.
  3. Pre-existing flake or unrelated failure β€” say so explicitly; prefer fixing the root cause over masking it.

SDK guardrails (AGENTS.md):

  • Agent-callable tools β†’ PraisonAI-Tools; lifecycle plugins β†’ PraisonAI-Plugins; sandbox backends β†’ PraisonAI-Plugins (praisonai.sandbox) β€” not praisonaiagents/.
  • Do not disturb core SDK contracts to accommodate a wrapper/feature change.
  • Tests must continue to guard backward compatibility and hot-path behaviour β€” passing CI by lowering test standards is not acceptable.
  • If the feature does not genuinely add SDK value, recommend reverting or narrowing scope instead of patching around failures.

What to do

  1. State your verdict: legitimate fix, regression fix, or needs human review β€” and why (1–3 sentences).
  2. Fix root cause with minimal changes; never bloat the Agent class with extra params.
  3. Run failing tests locally, e.g. pytest tests/unit/cli/test_interactive_surfaces_parity.py::test_async_tui_expands_at_mentions -q, plus any related SDK tests touched by the PR.
  4. Push to this branch and comment: files changed, review verdict, and why tests still protect SDK behaviour.

@praisonai-triage-agent

praisonai-triage-agent Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Claude finished @MervinPraison's task β€”β€” View job


Final Architecture Review β€” PR #3781 βœ…

Verdict: APPROVE (after fixes pushed to this branch).

  • Read changed files & scope
  • Investigate CI failure (KeyError: 'prompt')
  • Assess CodeRabbit findings
  • Implement valid fixes
  • Test & push
  • Final verdict

Phase 1 β€” Architecture review

  • Layer placement correct. All changes are wrapper-only gateway operational state (praisonai-bot, praisonai-code). No core SDK (praisonaiagents/) touched, no new Agent params, no new deps. βœ…
  • SDK value. Genuine security/robustness fix: unifies callback + pairing signing onto one persisted, auto-provisioned 0600 secret so approve/deny buttons and pairing codes survive restarts with zero operator action. Not scope creep. βœ…
  • Backward compatible. Env overrides (PRAISONAI_CALLBACK_SECRET, PRAISONAI_GATEWAY_SECRET) still win; no hot-path regressions. βœ…

Phase 2 β€” CI failure (root cause) + reviewer fixes

CI KeyError: 'prompt' β€” verdict: pre-existing merge collision, NOT caused by this PR.
_execute_prompt gained a read_only kwarg in #3758 (e6eb0d099), while the parity test's fake _execute_prompt(prompt) (from #3744) was never updated. _execute_in_background calls self._execute_prompt(processed_prompt, read_only=...); the fake raised TypeError, swallowed into error[0], so _execute_prompt never set captured["prompt"]. Root-cause fix updates the fake signature β€” assertions unchanged (expansion happened + ran exactly once), so it still guards behaviour.

Valid CodeRabbit findings β€” fixed:

  1. 🟠 _pairing_ui.py: narrowed the broad except Exception random fallback to storage errors + explicit warning (no silent diverging key).
  2. 🟠 pairing.py: reject empty/whitespace-only secret; fail closed on read failure of an existing secret (no silent HMAC rotation invalidating outstanding codes).
  3. 🟠 pairing.py: atomic first-time creation via O_CREAT|O_EXCL; race loser re-reads the winner's secret.
  4. 🟑 bot_checks.py: doctor guidance now notes PRAISONAI_CALLBACK_SECRET must be unset/identical across hosts.

Files modified

  • src/praisonai-bot/praisonai_bot/gateway/pairing.py
  • src/praisonai-bot/praisonai_bot/bots/_pairing_ui.py
  • src/praisonai-code/praisonai_code/cli/features/doctor/checks/bot_checks.py
  • src/praisonai/tests/unit/cli/test_interactive_surfaces_parity.py

Skipped: nothing material β€” all 4 reviewer findings addressed.

Phase 3 β€” Validation

  • test_interactive_surfaces_parity.py β€” 4 passed (CI failure resolved)
  • Pairing e2e + UI-approval + gateway-approval β€” 24 passed (incl. test_cli_approve_survives_restart)
  • Doctor TestBotSecurityCheck β€” 6 passed
  • Functional: empty-secret rejected, 0600 enforced, stable across calls, race adoption verified

All issues resolved. Ready to merge.

Job run β€’ Branch claude/issue-3779-20260807-0915

- pairing.py: reject empty/whitespace secrets, fail closed on read errors of
  an existing secret (no silent HMAC rotation), and create the secret atomically
  with O_CREAT|O_EXCL so racing processes converge on one key.
- _pairing_ui.py: narrow the callback-secret fallback to storage errors and warn
  explicitly instead of silently substituting a diverging random key.
- bot_checks.py: doctor guidance now notes PRAISONAI_CALLBACK_SECRET must be
  unset/identical across hosts for button signatures to align.
- test_interactive_surfaces_parity.py: update the _execute_prompt fake to accept
  read_only (matching the real signature added in #3758), fixing KeyError:'prompt'.

πŸ€– Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
@MervinPraison MervinPraison added pipeline/blocked:stale-final Blocked: FINAL stale after new commits and removed pipeline/blocked:stale-final Blocked: FINAL stale after new commits labels Aug 7, 2026
@praisonai-triage-agent

praisonai-triage-agent Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Claude finished @MervinPraison's task β€”β€” View job


CI Fix β€” PR #3781

Verdict: pre-existing merge collision, already fixed on current HEAD β€” no code changes needed.

The CI report targeted the stale HEAD fa702ec2. The fix commit ef9719418 already sits on top of it on this branch (both are pushed to remote). CI simply needs to re-run against the current HEAD.

  • Investigate CI failure (KeyError: 'prompt')
  • Verify current state of files
  • Confirm root-cause fix already present
  • Run failing test locally β€” passes
  • Validate CodeRabbit findings β€” all addressed

Root cause of KeyError: 'prompt'

_execute_prompt gained a read_only kwarg (from #3758, e6eb0d099); _execute_in_background calls self._execute_prompt(processed_prompt, read_only=...). The parity test's fake _execute_prompt(prompt) didn't accept it, so it raised TypeError, captured["prompt"] was never set β†’ KeyError. Fixed by widening the fake's signature to _fake_execute(prompt, read_only=False) (test_interactive_surfaces_parity.py:58). Assertions unchanged β€” still guards that expansion runs and runs exactly once.

Validation (local)

  • test_async_tui_expands_at_mentions β€” passed (whole parity file: 2 passed, 2 skipped for optional import)
  • test_pairing_agent_e2e.py β€” 3 passed
  • Secret-store functional checks β€” all pass: stable across calls, 0600 enforced, empty/whitespace-only rejected & regenerated, PRAISONAI_GATEWAY_SECRET override wins
  • Callback secret β€” stable across calls, PRAISONAI_CALLBACK_SECRET override wins

Remaining local failures are environmental only (missing optional deps typer, starlette, itsdangerous), unrelated to this PR β€” they are collection/import errors, not logic failures.

CodeRabbit findings β€” already resolved on ef9719418

# File Fix
🟠 _pairing_ui.py Narrowed broad except Exception β†’ (OSError, IOError); single cached fallback via lru_cache; explicit warning (no silent key rotation)
🟠 pairing.py Rejects empty/whitespace-only secret; fails closed on read failure of an existing secret
🟠 pairing.py Atomic first-time creation via O_CREAT|O_EXCL; race loser re-reads winner's secret
🟑 bot_checks.py Doctor guidance now notes PRAISONAI_CALLBACK_SECRET must be unset/identical across hosts

Architecture (AGENTS.md)

Wrapper-only (praisonai-bot, praisonai-code). No praisonaiagents/ core changes, no new Agent params, no new deps. Env overrides preserved β†’ backward compatible. βœ…

Files modified this branch (vs fa702ec2): gateway/pairing.py, bots/_pairing_ui.py, doctor/checks/bot_checks.py, tests/unit/cli/test_interactive_surfaces_parity.py. Skipped: nothing material.

Working tree clean; all fixes are on remote HEAD ef9719418. Ready to merge once CI re-runs.


Branch claude/issue-3779-20260807-0915

@praisonai-triage-agent praisonai-triage-agent Bot removed the pipeline/blocked:cooldown Blocked: post-push or @claude cooldown label Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-ci-fix-pending pipeline/awaiting-merge-gate FINAL done; waiting for merge gate / CI pipeline/blocked:ci Blocked: CI not green on HEAD

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gateway signing secrets are ephemeral by default: pairing codes and approval buttons silently break after a restart

1 participant