Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions docs/cli/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,19 @@ Every `PIPEFY_*` env var is validated against a semantically meaningful regex at

### `Login succeeded but the session could not be stored in your keychain (<backend>)`

The login worked but `keyring` couldn't write the entry. On macOS / Windows this is rare. On headless Linux it usually means no Secret Service daemon is running — install `gnome-keyring` or `kwallet`, set `PIPEFY_KEYCHAIN_BACKEND=file` to use a plaintext file backend under the Pipefy config directory, or fall back to a static `PIPEFY_TOKEN`.
The login worked but `keyring` couldn't write the entry.

When `PIPEFY_KEYCHAIN_BACKEND=file` is active the backend reports as `PlaintextKeyring` and the hint switches to a config-directory writability check (the file backend writes to `keyring.cfg` under the resolved config directory).
**macOS (Keychain / `Keyring` backend).** OAuth can succeed while persistence fails with `Can't store password on keychain: (-25244, 'Unknown Error')`. That code is `errSecParam` from Security.framework — common when the calling process cannot surface the new-item ACL dialog (IDE slash commands, agent hosts, and other non-TTY subprocesses that do not share an interactive Aqua session). The remediation procedure is the same regardless of install method. macOS binds the keychain ACL to the calling Python binary's path, so `uv tool install` and a wheel install give you a stable grant; with `uvx`, the cached environment's binary path can change (for example after `uvx --refresh` or a uv version bump), and macOS will prompt again from Terminal.app.

1. Run `pipefy auth login` **once** from a regular **Terminal.app** session (not from inside the IDE).
2. When macOS prompts for keychain access for the Python binary running `pipefy`, click **Always Allow**.
3. Retry login from the IDE or agent integration — refresh writes should succeed without another prompt.

**Linux (headless / CI).** Usually no Secret Service daemon — install `gnome-keyring` or `kwallet`, set `PIPEFY_KEYCHAIN_BACKEND=file` for a plaintext file backend under the Pipefy config directory, or use a static `PIPEFY_TOKEN`.

**Windows.** Credential Manager may block non-interactive callers; run `pipefy auth login` once from an interactive Command Prompt or PowerShell window, or use `PIPEFY_KEYCHAIN_BACKEND=file` / `PIPEFY_TOKEN`.

When `PIPEFY_KEYCHAIN_BACKEND=file` is active the backend reports as `PlaintextKeyring` and the CLI hint switches to a config-directory writability check (the file backend writes to `keyring.cfg` under the resolved config directory).

### `Missing Pipefy authentication. Set PIPEFY_TOKEN, configure PIPEFY_SERVICE_ACCOUNT_*, or run \`pipefy auth login\`.`

Expand Down
60 changes: 60 additions & 0 deletions packages/cli/src/pipefy_cli/commands/_auth_keychain_hints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Platform-specific hints when OS keychain session storage fails after OAuth."""
Comment thread
adriannoes marked this conversation as resolved.

from __future__ import annotations

import platform

from pipefy_infra.config import config_dir

from pipefy_cli._docs import DOCS_CLI_AUTH_REF

_LINUX_HINT = (
"On headless Linux, ensure a Secret Service daemon "
"(gnome-keyring, kwallet) is running, set "
"PIPEFY_KEYCHAIN_BACKEND=file to use a plaintext file backend, "
"or use a static PIPEFY_TOKEN."
)

_MACOS_HINT = (
"This is usually macOS denying the calling subprocess access to the "
"keychain ACL prompt (errSecParam, -25244). Run `pipefy auth login` once "
"from a regular Terminal.app session and click Always Allow when macOS "
"prompts; subsequent runs (including agent / IDE integrations) will write "
"without prompting."
)

_WINDOWS_HINT = (
"On Windows, Credential Manager may block writes from non-interactive "
"callers. Run `pipefy auth login` once from an interactive Command Prompt "
"or PowerShell window, or set PIPEFY_KEYCHAIN_BACKEND=file, or use a "
"static PIPEFY_TOKEN."
)

_GENERIC_HINT = (
f"Run `pipefy auth status` to inspect the active backend, or see "
f"{DOCS_CLI_AUTH_REF} for keychain troubleshooting."
)


def keychain_store_failure_hint(*, backend: str) -> str:
"""Return a remediation hint after ``store_session`` fails post-login."""
if backend == "PlaintextKeyring":
return (
f"Ensure the config directory is writable ({config_dir()}), "
"or use a static PIPEFY_TOKEN."
)
return _keychain_hint_for_platform()


def _keychain_hint_for_platform() -> str:
system = platform.system()
if system == "Darwin":
return _MACOS_HINT
if system == "Linux":
return _LINUX_HINT
if system == "Windows":
return _WINDOWS_HINT
return _GENERIC_HINT


__all__ = ["keychain_store_failure_hint"]
16 changes: 2 additions & 14 deletions packages/cli/src/pipefy_cli/commands/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
get_authenticated_client,
to_display_source,
)
from pipefy_cli.commands._auth_keychain_hints import keychain_store_failure_hint
from pipefy_cli.commands._common import settings_and_auth_from_ctx
from pipefy_cli.output import render_json

Expand Down Expand Up @@ -193,20 +194,7 @@ def _open(url: str) -> bool:
# without ownership) surfaces as ``PermissionError`` from
# ``os.makedirs`` inside the backend, which is not a ``KeyringError``.
backend = keychain_backend_name()
if backend == "PlaintextKeyring":
from pipefy_infra.config import config_dir

hint = (
f"Ensure the config directory is writable ({config_dir()}), "
"or use a static PIPEFY_TOKEN."
)
else:
hint = (
"On headless Linux, ensure a Secret Service daemon "
"(gnome-keyring, kwallet) is running, set "
"PIPEFY_KEYCHAIN_BACKEND=file to use a plaintext file backend, "
"or use a static PIPEFY_TOKEN."
)
hint = keychain_store_failure_hint(backend=backend)
typer.echo(
f"Login succeeded but the session could not be stored in your "
f"keychain ({backend}): {exc}. {hint}",
Expand Down
39 changes: 39 additions & 0 deletions packages/cli/tests/test_auth_keychain_hints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from __future__ import annotations

import platform

import pytest

from pipefy_cli.commands import _auth_keychain_hints as hints


@pytest.mark.parametrize(
("system", "required", "forbidden"),
[
("Darwin", ["errSecParam", "Terminal.app", "Always Allow"], ["Secret Service"]),
("Linux", ["Secret Service"], ["Terminal.app", "Credential Manager"]),
("Windows", ["Credential Manager"], ["Secret Service", "Terminal.app"]),
("FreeBSD", ["pipefy auth status"], []),
],
)
def test_keychain_hint_for_platform(
monkeypatch: pytest.MonkeyPatch,
system: str,
required: list[str],
forbidden: list[str],
) -> None:
monkeypatch.setattr(platform, "system", lambda: system)
hint = hints.keychain_store_failure_hint(backend="Keyring")
for fragment in required:
assert fragment in hint
for fragment in forbidden:
assert fragment not in hint


def test_plaintext_backend_hint_ignores_platform(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(platform, "system", lambda: "Darwin")
hint = hints.keychain_store_failure_hint(backend="PlaintextKeyring")
assert "config directory is writable" in hint
assert "errSecParam" not in hint
26 changes: 21 additions & 5 deletions packages/cli/tests/test_auth_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,17 +750,31 @@ def _boom(**_kwargs: object) -> flow.LoginResult:
assert "Login failed" in result.stderr
assert "invalid_grant" in result.stderr

def test_store_failure_default_backend_hints_at_secret_service(
@pytest.mark.parametrize(
("system", "expected_fragment", "forbidden_fragment"),
[
("Linux", "Secret Service", "Terminal.app"),
("Darwin", "errSecParam", "Secret Service"),
("Windows", "Credential Manager", "Secret Service"),
],
)
def test_store_failure_default_backend_hint_matches_platform(
self,
cli_runner,
monkeypatch: pytest.MonkeyPatch,
fake_keyring: InMemoryKeyring,
clean_pipefy_env,
saved_cwd,
system: str,
expected_fragment: str,
forbidden_fragment: str,
) -> None:
"""A keychain write failure on the default OS backend surfaces the
Secret Service hint and the file-backend escape hatch."""
"""A keychain write failure on the default OS backend surfaces a
platform-appropriate remediation hint."""
import platform

monkeypatch.setenv("PIPEFY_AUTH_URL", "https://x.test/realms/foo")
monkeypatch.setattr(platform, "system", lambda: system)

from keyring.errors import KeyringError

Expand All @@ -783,8 +797,10 @@ def _boom(**_kwargs: object) -> None:
result = cli_runner.invoke(cli_app, ["auth", "login"])
assert result.exit_code == 1
assert "could not be stored in your keychain" in result.stderr
assert "Secret Service" in result.stderr
assert "PIPEFY_KEYCHAIN_BACKEND=file" in result.stderr
assert expected_fragment in result.stderr
assert forbidden_fragment not in result.stderr
if system == "Linux":
assert "PIPEFY_KEYCHAIN_BACKEND=file" in result.stderr

def test_store_failure_file_backend_hints_at_config_dir(
self,
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Full reference (every `PIPEFY_*` variable, validation rules, TOML schema, preced

### macOS keychain `errSecParam (-25244)`

`pipefy auth login` may exit with `errSecParam (-25244)` at the final keychain-write step even though OAuth itself succeeded. The cause is not yet reliably diagnosed — direct `keyring.set_password` calls from the same uv-tool-installed Python succeed under repro testing, so this is likely a transient `Security.framework` condition rather than a deterministic per-binary ACL problem. If it occurs, retry the slash command (Claude Code) or `pipefy auth login` (terminal) first; as a fallback, run `pipefy auth login` once from a regular Terminal.app session and approve any macOS keychain dialog that appears. [Issue #235](https://github.com/pipefy/ai-toolkit/issues/235) tracks platform-aware error messaging.
`pipefy auth login` may exit with `errSecParam (-25244)` at the final keychain-write step even though OAuth itself succeeded. That usually means the calling process could not surface the macOS keychain ACL dialog (IDE slash commands, agent hosts, and other non-TTY subprocesses). Run `pipefy auth login` once from a regular Terminal.app session and click **Always Allow** when prompted; refresh writes from the IDE or agent should then succeed. macOS binds the ACL to the calling Python binary's path, so a stable install (`uv tool install` / wheel) keeps the grant; with `uvx`, a path change (for example after `uvx --refresh`) can prompt again. See [`docs/cli/auth.md`](../../docs/cli/auth.md) for the full platform-specific troubleshooting.

### Claude Code: `claude mcp add` (per-project terminal flow)

Expand Down
Loading