pipecatcloud 0.4.3__tar.gz → 0.5.0__tar.gz

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 (66) hide show
  1. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/CHANGELOG.md +58 -0
  2. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/PKG-INFO +29 -3
  3. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/README.md +26 -0
  4. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/pyproject.toml +9 -9
  5. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/_utils/deploy_utils.py +1 -0
  6. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/api.py +5 -1
  7. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/cli/commands/auth.py +61 -7
  8. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/cli/commands/deploy.py +8 -3
  9. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/cli/commands/secrets.py +25 -21
  10. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/cli/config.py +29 -10
  11. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/cli/entry_point.py +13 -3
  12. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud.egg-info/PKG-INFO +29 -3
  13. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud.egg-info/SOURCES.txt +1 -0
  14. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud.egg-info/requires.txt +1 -1
  15. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/tests/test_api_client.py +48 -0
  16. pipecatcloud-0.5.0/tests/test_auth.py +303 -0
  17. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/uv.lock +345 -748
  18. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/.github/workflows/format.yml +0 -0
  19. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/.github/workflows/publish-pypi.yml +0 -0
  20. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/.github/workflows/publish-test.yml +0 -0
  21. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/.github/workflows/tests.yml +0 -0
  22. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/.gitignore +0 -0
  23. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/.pre-commit-config.yaml +0 -0
  24. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/LICENSE +0 -0
  25. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/pipecat-cloud.png +0 -0
  26. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/pytest.ini +0 -0
  27. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/scripts/fix-ruff.sh +0 -0
  28. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/scripts/pre-commit.sh +0 -0
  29. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/setup.cfg +0 -0
  30. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/__init__.py +0 -0
  31. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/__main__.py +0 -0
  32. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/__version__.py +0 -0
  33. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/_utils/__init__.py +0 -0
  34. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/_utils/async_utils.py +0 -0
  35. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/_utils/auth_utils.py +0 -0
  36. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/_utils/build_utils.py +0 -0
  37. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/_utils/console_utils.py +0 -0
  38. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/_utils/regions.py +0 -0
  39. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/agent.py +0 -0
  40. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/cli/__init__.py +0 -0
  41. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/cli/api.py +0 -0
  42. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/cli/commands/__init__.py +0 -0
  43. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/cli/commands/agent.py +0 -0
  44. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/cli/commands/build.py +0 -0
  45. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/cli/commands/docker.py +0 -0
  46. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/cli/commands/organizations.py +0 -0
  47. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/cli/commands/regions.py +0 -0
  48. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/config.py +0 -0
  49. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/constants.py +0 -0
  50. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/exception.py +0 -0
  51. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/py.typed +0 -0
  52. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/session.py +0 -0
  53. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
  54. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
  55. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
  56. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/src/pipecatcloud.egg-info/top_level.txt +0 -0
  57. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/tests/__init__.py +0 -0
  58. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/tests/conftest.py +0 -0
  59. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/tests/test_agent_sessions.py +0 -0
  60. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/tests/test_agent_stop.py +0 -0
  61. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/tests/test_config_file.py +0 -0
  62. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/tests/test_deployment_status.py +0 -0
  63. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/tests/test_docker_commands.py +0 -0
  64. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/tests/test_krisp_viva.py +0 -0
  65. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/tests/test_pat_auth.py +0 -0
  66. {pipecatcloud-0.4.3 → pipecatcloud-0.5.0}/tests/test_regions.py +0 -0
@@ -5,6 +5,64 @@ All notable changes to **Pipecat Cloud** will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.0] - 2026-04-17
9
+
10
+ ### Changed
11
+
12
+ - Minimum Python version bumped from 3.10 to 3.11, matching Pipecat 1.0's
13
+ supported range (3.11-3.14). Users on Python 3.10 will continue to receive
14
+ the previous release via pip/uv version resolution.
15
+
16
+ - The `pipecatcloud[pipecat]` optional dependency now requires `pipecat-ai>=1.0.0`
17
+ (previously `>=0.0.79`).
18
+
19
+ - **`pcc deploy --force` now forces a new deployment** even if your
20
+ configuration hasn't changed. Previously, `--force` only skipped the
21
+ confirmation prompt. Use this when you want to pick up a new image
22
+ behind the same tag (e.g. `:latest`) or restart your agent instances.
23
+ `--yes` remains available for non-interactive/CI usage.
24
+
25
+ ## [0.4.4] - 2026-04-10
26
+
27
+ ### Added
28
+
29
+ - `pcc secrets image-pull-secret` now supports updating existing image pull
30
+ secrets. Previously, updating required deleting and recreating the secret.
31
+ The command prompts for confirmation before overwriting; use `--skip` to
32
+ bypass the prompt in scripts.
33
+
34
+ ### Changed
35
+
36
+ - `--show-cli-config` now redacts `token` and `refresh_token` values in output.
37
+ A type prefix (e.g. `pcc_pat_****`) is shown for diagnostics. Users who need
38
+ raw values can read the config file directly.
39
+
40
+ - `pcc auth use-pat` now reads the token from a secure hidden prompt instead of
41
+ a positional argument. The positional form still works but emits a deprecation
42
+ warning (tokens passed as arguments are visible in shell history and process
43
+ listings).
44
+
45
+ - OAuth callback server now binds to `127.0.0.1` instead of `localhost` to
46
+ prevent DNS rebinding attacks.
47
+
48
+ ### Fixed
49
+
50
+ - `pcc auth logout` no longer claims "session revoked" when server-side
51
+ revocation was not confirmed. It now accurately reports what succeeded.
52
+
53
+ - `pcc auth logout` no longer crashes if the credentials file is already absent.
54
+
55
+ - Fixed `organizations_current()` fallback returning the wrong data shape when
56
+ no specific org is requested.
57
+
58
+ - OIDC discovery metadata is now validated before use: issuer must match
59
+ expected value, endpoints must use HTTPS, and PKCE S256 support is verified
60
+ when advertised.
61
+
62
+ - Credentials are now written atomically (write to temp file, fsync, rename)
63
+ to prevent corruption from interrupted writes. File permissions are
64
+ restrictive from creation.
65
+
8
66
  ## [0.4.3] - 2026-04-02
9
67
 
10
68
  ### Fixed
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 0.4.3
3
+ Version: 0.5.0
4
4
  Summary: Cloud hosting for Pipecat AI applications
5
5
  License: BSD 2-Clause License
6
6
  Project-URL: Website, https://docs.pipecat.daily.co/
7
7
  Keywords: pipecat,pipecat-ai,voice,ai,llm
8
- Requires-Python: >=3.10
8
+ Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: aiohttp<4.0.0,>=3.11.12
@@ -18,7 +18,7 @@ Requires-Dist: synchronicity<1.0.0,>=0.11
18
18
  Requires-Dist: toml~=0.10.2
19
19
  Requires-Dist: questionary~=2.1.0
20
20
  Provides-Extra: pipecat
21
- Requires-Dist: pipecat-ai>=0.0.79; extra == "pipecat"
21
+ Requires-Dist: pipecat-ai>=1.0.0; extra == "pipecat"
22
22
  Dynamic: license-file
23
23
 
24
24
  <h1><div align="center">
@@ -97,6 +97,32 @@ if __name__ == "__main__":
97
97
  asyncio.run(main())
98
98
  ```
99
99
 
100
+ ## Troubleshooting
101
+
102
+ ### SSL certificate errors on macOS
103
+
104
+ If `pcc auth login` fails with an SSL certificate verification error, your Python
105
+ installation may not have access to the macOS system certificate store. This is
106
+ common with Python installed via pyenv, conda, or the python.org installer.
107
+
108
+ To diagnose:
109
+
110
+ ```python
111
+ import ssl, sys, os
112
+ print(ssl.get_default_verify_paths())
113
+ print(os.path.realpath(sys.executable))
114
+ ```
115
+
116
+ To fix, install `certifi` and point Python to its certificates:
117
+
118
+ ```shell
119
+ pip install certifi
120
+ export SSL_CERT_FILE=$(python -c "import certifi; print(certifi.where())")
121
+ ```
122
+
123
+ For the python.org installer, you can also run the bundled
124
+ `Install Certificates.command` script found in `/Applications/Python X.Y/`.
125
+
100
126
  ## 🛠️ Contributing
101
127
 
102
128
  ### Setup Steps
@@ -74,6 +74,32 @@ if __name__ == "__main__":
74
74
  asyncio.run(main())
75
75
  ```
76
76
 
77
+ ## Troubleshooting
78
+
79
+ ### SSL certificate errors on macOS
80
+
81
+ If `pcc auth login` fails with an SSL certificate verification error, your Python
82
+ installation may not have access to the macOS system certificate store. This is
83
+ common with Python installed via pyenv, conda, or the python.org installer.
84
+
85
+ To diagnose:
86
+
87
+ ```python
88
+ import ssl, sys, os
89
+ print(ssl.get_default_verify_paths())
90
+ print(os.path.realpath(sys.executable))
91
+ ```
92
+
93
+ To fix, install `certifi` and point Python to its certificates:
94
+
95
+ ```shell
96
+ pip install certifi
97
+ export SSL_CERT_FILE=$(python -c "import certifi; print(certifi.where())")
98
+ ```
99
+
100
+ For the python.org installer, you can also run the bundled
101
+ `Install Certificates.command` script found in `/Applications/Python X.Y/`.
102
+
77
103
  ## 🛠️ Contributing
78
104
 
79
105
  ### Setup Steps
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "pipecatcloud"
7
7
  dynamic = ["version"]
8
- requires-python = ">=3.10"
8
+ requires-python = ">=3.11"
9
9
  description = "Cloud hosting for Pipecat AI applications"
10
10
  license = { text = "BSD 2-Clause License" }
11
11
  readme = "README.md"
@@ -23,17 +23,17 @@ dependencies = [
23
23
  ]
24
24
 
25
25
  [project.optional-dependencies]
26
- pipecat = ["pipecat-ai>=0.0.79"]
26
+ pipecat = ["pipecat-ai>=1.0.0"]
27
27
 
28
28
  [dependency-groups]
29
29
  dev = [
30
- "build~=1.2.2",
31
- "grpcio-tools~=1.67.1",
32
- "pre-commit~=4.2.0",
33
- "pyright~=1.1.402",
34
- "pytest~=8.4.1",
35
- "pytest-asyncio~=1.0.0",
36
- "ruff~=0.12.1",
30
+ "build~=1.4.0",
31
+ "grpcio-tools~=1.78.0",
32
+ "pre-commit~=4.5.1",
33
+ "pyright>=1.1.404,<1.2",
34
+ "pytest>=9.0.0,<10",
35
+ "pytest-asyncio>=1.0.0,<2",
36
+ "ruff>=0.12.11,<1",
37
37
  "setuptools~=78.1.1",
38
38
  "setuptools_scm~=8.3.1",
39
39
  ]
@@ -333,6 +333,7 @@ class DeployConfigParams:
333
333
  build_config: BuildConfig = field(factory=BuildConfig) # Cloud build configuration
334
334
  agent_profile: Optional[str] = None
335
335
  krisp_viva: KrispVivaConfig = field(factory=KrispVivaConfig)
336
+ force_redeploy: bool = False
336
337
 
337
338
  def __attrs_post_init__(self):
338
339
  if self.image is not None and ":" not in self.image:
@@ -233,7 +233,10 @@ class _API:
233
233
  return {"name": o["name"], "verbose_name": o["verboseName"]}
234
234
 
235
235
  # Default to first organization if active_org not found or not specified
236
- return {"name": results[0]["name"], "verbose_name": results[0]["verboseName"]}
236
+ return {
237
+ "name": results["organizations"][0]["name"],
238
+ "verbose_name": results["organizations"][0]["verboseName"],
239
+ }
237
240
 
238
241
  @property
239
242
  def organizations_current(self):
@@ -411,6 +414,7 @@ class _API:
411
414
  "krispViva": {
412
415
  "audioFilter": deploy_config.krisp_viva.audio_filter,
413
416
  },
417
+ "forceRedeploy": deploy_config.force_redeploy or None,
414
418
  }
415
419
 
416
420
  # Use either build_id (cloud build) or image (user-provided)
@@ -40,6 +40,7 @@ USER_AGENT = f"PipecatCloudCLI/{_cli_version}"
40
40
  # Ports to try for the localhost OAuth callback server.
41
41
  # Our audience is developers who may have services running on common ports.
42
42
  # We try a range and use the first available.
43
+ CALLBACK_HOST = "127.0.0.1"
43
44
  CALLBACK_PORTS = [8400, 8401, 8402, 8403, 8404]
44
45
  CALLBACK_PATH = "/oauth_callback"
45
46
 
@@ -63,17 +64,49 @@ async def _fetch_oauth_config() -> dict:
63
64
 
64
65
 
65
66
  async def _fetch_oidc_discovery(issuer: str) -> dict:
66
- """Fetch OIDC discovery document from the OAuth issuer (RFC 8414).
67
+ """Fetch and validate OIDC discovery document from the OAuth issuer (RFC 8414).
67
68
 
68
69
  This gives us the authorization and token endpoints without
69
70
  hardcoding any provider-specific URLs.
71
+
72
+ Validates the discovery metadata per RFC 8414 §3.1 and RFC 9207 §2.4:
73
+ - issuer must exactly match the expected value
74
+ - authorization_endpoint and token_endpoint must use HTTPS
75
+ - code_challenge_methods_supported must include S256 (when present)
76
+ - response_types_supported must include "code" (when present)
70
77
  """
71
78
  url = f"{issuer}/.well-known/openid-configuration"
72
79
  async with aiohttp.ClientSession() as session:
73
80
  async with session.get(url, headers={"User-Agent": USER_AGENT}) as resp:
74
81
  if resp.status != 200:
75
82
  raise RuntimeError(f"OIDC discovery failed ({resp.status}) at {url}")
76
- return await resp.json()
83
+ doc = await resp.json()
84
+
85
+ # RFC 8414 §3.1: issuer in metadata MUST exactly match the expected issuer.
86
+ if doc.get("issuer") != issuer:
87
+ raise RuntimeError(f"OIDC issuer mismatch: expected {issuer!r}, got {doc.get('issuer')!r}")
88
+
89
+ # Endpoints must use HTTPS to prevent credential interception.
90
+ for key in ("authorization_endpoint", "token_endpoint"):
91
+ endpoint = doc.get(key, "")
92
+ if not endpoint.startswith("https://"):
93
+ raise RuntimeError(f"OIDC {key} must use HTTPS, got {endpoint!r}")
94
+
95
+ # If the server advertises supported challenge methods, verify S256 is included.
96
+ challenge_methods = doc.get("code_challenge_methods_supported")
97
+ if challenge_methods is not None and "S256" not in challenge_methods:
98
+ raise RuntimeError(
99
+ f"OIDC server does not support S256 PKCE (supported: {challenge_methods})"
100
+ )
101
+
102
+ # If the server advertises supported response types, verify "code" is included.
103
+ response_types = doc.get("response_types_supported")
104
+ if response_types is not None and "code" not in response_types:
105
+ raise RuntimeError(
106
+ f"OIDC server does not support 'code' response type (supported: {response_types})"
107
+ )
108
+
109
+ return doc
77
110
 
78
111
 
79
112
  # ---- Helpers ----
@@ -241,7 +274,7 @@ async def _start_callback_server() -> Tuple[Any, int, "asyncio.Future"]:
241
274
  # so we use a less-common range and try multiple.
242
275
  for port in CALLBACK_PORTS:
243
276
  try:
244
- site = web.TCPSite(runner, "localhost", port)
277
+ site = web.TCPSite(runner, CALLBACK_HOST, port)
245
278
  await site.start()
246
279
  return runner, port, result_future
247
280
  except OSError:
@@ -343,7 +376,7 @@ async def login():
343
376
  console.error(str(e))
344
377
  return
345
378
 
346
- redirect_uri = f"http://localhost:{port}{CALLBACK_PATH}"
379
+ redirect_uri = f"http://{CALLBACK_HOST}:{port}{CALLBACK_PATH}"
347
380
 
348
381
  try:
349
382
  # Generate PKCE verifier + challenge
@@ -442,6 +475,7 @@ async def login():
442
475
  @requires_login
443
476
  async def logout():
444
477
  refresh_token = config.get("refresh_token")
478
+ revocation_succeeded = False
445
479
 
446
480
  # If this is an OAuth session, attempt server-side token revocation (PCC-678).
447
481
  # The /auth/logout endpoint proxies revocation to Clerk with the client_secret.
@@ -464,7 +498,7 @@ async def logout():
464
498
  "Token will expire within 24 hours.[/yellow]"
465
499
  )
466
500
  elif resp.ok:
467
- logger.debug("Server-side token revocation succeeded")
501
+ revocation_succeeded = True
468
502
  else:
469
503
  logger.debug(f"Logout revocation returned {resp.status}")
470
504
  except (aiohttp.ClientError, asyncio.TimeoutError) as e:
@@ -477,8 +511,13 @@ async def logout():
477
511
  with console.status("[dim]Removing credentials...[/dim]", spinner="dots"):
478
512
  remove_user_config()
479
513
 
480
- if refresh_token:
514
+ if revocation_succeeded:
481
515
  console.success("Logged out and session revoked.")
516
+ elif refresh_token:
517
+ console.success(
518
+ "Local credentials removed. Server-side revocation could not be confirmed.\n"
519
+ "[dim]The token will expire automatically within 24 hours.[/dim]"
520
+ )
482
521
  else:
483
522
  console.success(
484
523
  "User credentials for Pipecat Cloud removed. Please sign out via dashboard to fully revoke session.",
@@ -528,8 +567,23 @@ async def _use_pat_impl(token: str):
528
567
  @auth_cli.command(name="use-pat", help="Authenticate with a Personal Access Token")
529
568
  @synchronizer.create_blocking
530
569
  async def use_pat(
531
- token: str = typer.Argument(help="Personal Access Token (pcc_pat_...)"),
570
+ token: Optional[str] = typer.Argument(
571
+ None, help="[deprecated] PAT as argument (leaks to shell history)"
572
+ ),
532
573
  ):
574
+ if token is not None:
575
+ console.print(
576
+ "[yellow]Warning: Passing tokens as arguments is deprecated — they are visible in "
577
+ "shell history and process listings. Next time, omit the argument to use the "
578
+ "secure prompt.[/yellow]"
579
+ )
580
+ else:
581
+ import getpass
582
+
583
+ token = getpass.getpass("Personal Access Token: ")
584
+ if not token:
585
+ console.error("No token provided.")
586
+ return
533
587
  await _use_pat_impl(token)
534
588
 
535
589
 
@@ -346,6 +346,10 @@ async def _deploy(params: DeployConfigParams, org, force: bool = False):
346
346
  # Close the live display before starting the new polling phase
347
347
  live.stop()
348
348
 
349
+ # Surface API warning (e.g. when identical config was deployed without --force)
350
+ if result and result.get("warning"):
351
+ console.print(f"[yellow]Warning: {result['warning']}[/yellow]")
352
+
349
353
  """
350
354
  # 3. Poll status until healthy
351
355
  """
@@ -556,11 +560,11 @@ def create_deploy_command(app: typer.Typer):
556
560
  help="Region for service deployment",
557
561
  rich_help_panel="Deployment Configuration",
558
562
  ),
559
- skip_confirm: bool = typer.Option(
563
+ force: bool = typer.Option(
560
564
  False,
561
565
  "--force",
562
566
  "-f",
563
- help="Force deployment / skip confirmation",
567
+ help="Force a new deployment even if config hasn't changed",
564
568
  rich_help_panel="Additional Options",
565
569
  ),
566
570
  yes: bool = typer.Option(
@@ -632,7 +636,7 @@ def create_deploy_command(app: typer.Typer):
632
636
  org = organization or config.get("org")
633
637
 
634
638
  # Combine automation flags
635
- auto_yes = yes or skip_confirm
639
+ auto_yes = yes or force
636
640
 
637
641
  # Compose deployment config from CLI options and config file (if provided)
638
642
  # Order of precedence:
@@ -653,6 +657,7 @@ def create_deploy_command(app: typer.Typer):
653
657
  )
654
658
  partial_config.enable_krisp = krisp or partial_config.enable_krisp
655
659
  partial_config.agent_profile = profile or partial_config.agent_profile
660
+ partial_config.force_redeploy = force
656
661
 
657
662
  # Override build config from CLI args
658
663
  if build_dir:
@@ -15,7 +15,6 @@ import questionary
15
15
  import typer
16
16
  from loguru import logger
17
17
  from rich import box
18
- from rich.live import Live
19
18
  from rich.panel import Panel
20
19
  from rich.table import Table
21
20
 
@@ -492,7 +491,7 @@ async def delete(
492
491
 
493
492
 
494
493
  @secrets_cli.command(
495
- name="image-pull-secret", help="Create an image pull secret for active organization."
494
+ name="image-pull-secret", help="Create or update an image pull secret for active organization."
496
495
  )
497
496
  @synchronizer.create_blocking
498
497
  @requires_login
@@ -509,6 +508,12 @@ async def image_pull_secret(
509
508
  base64encode: bool = typer.Option(
510
509
  True, "--encode", "-e", help="base64 encode credentials for added security"
511
510
  ),
511
+ skip_confirm: boolean = typer.Option(
512
+ False,
513
+ "--skip",
514
+ "-s",
515
+ help="Skip confirmations / force creation or update",
516
+ ),
512
517
  organization: str = typer.Option(
513
518
  None,
514
519
  "--organization",
@@ -556,13 +561,10 @@ async def image_pull_secret(
556
561
  return typer.Exit(1)
557
562
 
558
563
  # Check if secret already exists
559
- used_region = None
560
- with Live(
561
- console.status(
562
- f"[dim]Checking if image pull secret '{name}' already exists[/dim]", spinner="dots"
563
- ),
564
- refresh_per_second=4,
565
- ) as live:
564
+ existing_secret = None
565
+ with console.status(
566
+ f"[dim]Checking if image pull secret '{name}' already exists[/dim]", spinner="dots"
567
+ ):
566
568
  data, error = await API.secrets_list(org=org)
567
569
 
568
570
  if error:
@@ -572,19 +574,20 @@ async def image_pull_secret(
572
574
  existing_secret = next(
573
575
  (s for s in data if s["name"] == name and s["type"] == "imagePullSecret"), None
574
576
  )
575
- if existing_secret:
576
- live.stop()
577
- console.error(
578
- f"Image pull secret '[bold]{name}'[/bold] already exists. Please choose a different name or delete the existing one first."
579
- )
580
- return typer.Exit(1)
581
577
 
582
- live.update(
583
- console.status(
584
- f"[dim]Creating image pull secret [bold]'{name}'[/bold][/dim]", spinner="dots"
585
- )
586
- )
578
+ if existing_secret and not skip_confirm:
579
+ confirm = await questionary.confirm(
580
+ f"Image pull secret '{name}' already exists. Update it?"
581
+ ).ask_async()
582
+ if not confirm:
583
+ console.print("[bold red]Update cancelled[/bold red]")
584
+ return typer.Exit(1)
587
585
 
586
+ used_region = None
587
+ with console.status(
588
+ f"[dim]{'Updating' if existing_secret else 'Creating'} image pull secret [bold]'{name}'[/bold][/dim]",
589
+ spinner="dots",
590
+ ):
588
591
  data, error = await API.secrets_upsert(
589
592
  data={"isImagePullSecret": True, "secretValue": credentials, "host": host},
590
593
  set_name=name,
@@ -600,6 +603,7 @@ async def image_pull_secret(
600
603
  used_region = data["region"]
601
604
 
602
605
  region_info = f" in [bold cyan]{used_region}[/bold cyan]" if used_region else ""
606
+ action = "updated" if existing_secret else "created"
603
607
  console.success(
604
- f"Image pull secret [bold green]'{name}'[/bold green] for [bold green]{host}[/bold green] created successfully{region_info}.",
608
+ f"Image pull secret [bold green]'{name}'[/bold green] for [bold green]{host}[/bold green] {action} successfully{region_info}.",
605
609
  )
@@ -7,6 +7,7 @@
7
7
  import os
8
8
  import stat
9
9
  import sys
10
+ import tempfile
10
11
  from typing import Optional
11
12
 
12
13
  import toml
@@ -80,20 +81,38 @@ user_config = _read_user_config()
80
81
 
81
82
  def _write_user_config(new_config):
82
83
  dir_path = os.path.dirname(user_config_path)
83
- os.makedirs(dir_path, exist_ok=True)
84
+ os.makedirs(dir_path, mode=0o700, exist_ok=True)
84
85
 
85
- with open(user_config_path, "w") as f:
86
- toml.dump(new_config, f)
87
-
88
- # Restrict permissions so only the file owner can access credentials.
89
- # On Windows os.chmod is limited but home directories are already
90
- # protected by ACLs, so this is effectively a Unix-only hardening.
91
- os.chmod(dir_path, stat.S_IRWXU) # 0o700
92
- os.chmod(user_config_path, stat.S_IRUSR | stat.S_IWUSR) # 0o600
86
+ # Atomic write: serialize to a temp file in the same directory, fsync,
87
+ # then rename into place. This prevents a crash mid-write from leaving
88
+ # a truncated or empty credentials file (RFC 6749 §10.3).
89
+ fd = None
90
+ tmp_path = None
91
+ try:
92
+ fd, tmp_path = tempfile.mkstemp(dir=dir_path, prefix=".pipecatcloud_", suffix=".tmp")
93
+ # mkstemp opens with 0o600 by default — restrictive from creation.
94
+ with os.fdopen(fd, "w") as f:
95
+ fd = None # os.fdopen takes ownership of the fd
96
+ toml.dump(new_config, f)
97
+ f.flush()
98
+ os.fsync(f.fileno())
99
+ os.replace(tmp_path, user_config_path)
100
+ tmp_path = None # rename succeeded, nothing to clean up
101
+ finally:
102
+ if fd is not None:
103
+ os.close(fd)
104
+ if tmp_path is not None:
105
+ try:
106
+ os.unlink(tmp_path)
107
+ except OSError:
108
+ pass
93
109
 
94
110
 
95
111
  def remove_user_config():
96
- os.remove(user_config_path)
112
+ try:
113
+ os.remove(user_config_path)
114
+ except FileNotFoundError:
115
+ pass
97
116
 
98
117
 
99
118
  def update_user_config(
@@ -42,8 +42,18 @@ def show_config_callback(value: bool):
42
42
  from pipecatcloud._utils.deploy_utils import load_deploy_config_file
43
43
  from pipecatcloud.cli.config import deploy_config_path
44
44
 
45
- # Print local config
46
- pprint(config.to_dict())
45
+ config_dict = config.to_dict()
46
+
47
+ # Redact credential values — users who need raw values can read the
48
+ # config file directly (~/.config/pipecatcloud/pipecatcloud.toml).
49
+ for key in ("token", "refresh_token"):
50
+ if config_dict.get(key):
51
+ val = str(config_dict[key])
52
+ # Show token type prefix (e.g. "pcc_pat_****") for diagnostics
53
+ prefix = val[:8] if len(val) > 12 else ""
54
+ config_dict[key] = f"{prefix}****"
55
+
56
+ pprint(config_dict)
47
57
 
48
58
  # Check for deploy config
49
59
  try:
@@ -76,7 +86,7 @@ def cli(
76
86
  None,
77
87
  "--show-cli-config",
78
88
  callback=show_config_callback,
79
- help="Show CLI internal configuration (credentials, active org)",
89
+ help="Show CLI internal configuration (credentials redacted)",
80
90
  ),
81
91
  ):
82
92
  pass
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 0.4.3
3
+ Version: 0.5.0
4
4
  Summary: Cloud hosting for Pipecat AI applications
5
5
  License: BSD 2-Clause License
6
6
  Project-URL: Website, https://docs.pipecat.daily.co/
7
7
  Keywords: pipecat,pipecat-ai,voice,ai,llm
8
- Requires-Python: >=3.10
8
+ Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: aiohttp<4.0.0,>=3.11.12
@@ -18,7 +18,7 @@ Requires-Dist: synchronicity<1.0.0,>=0.11
18
18
  Requires-Dist: toml~=0.10.2
19
19
  Requires-Dist: questionary~=2.1.0
20
20
  Provides-Extra: pipecat
21
- Requires-Dist: pipecat-ai>=0.0.79; extra == "pipecat"
21
+ Requires-Dist: pipecat-ai>=1.0.0; extra == "pipecat"
22
22
  Dynamic: license-file
23
23
 
24
24
  <h1><div align="center">
@@ -97,6 +97,32 @@ if __name__ == "__main__":
97
97
  asyncio.run(main())
98
98
  ```
99
99
 
100
+ ## Troubleshooting
101
+
102
+ ### SSL certificate errors on macOS
103
+
104
+ If `pcc auth login` fails with an SSL certificate verification error, your Python
105
+ installation may not have access to the macOS system certificate store. This is
106
+ common with Python installed via pyenv, conda, or the python.org installer.
107
+
108
+ To diagnose:
109
+
110
+ ```python
111
+ import ssl, sys, os
112
+ print(ssl.get_default_verify_paths())
113
+ print(os.path.realpath(sys.executable))
114
+ ```
115
+
116
+ To fix, install `certifi` and point Python to its certificates:
117
+
118
+ ```shell
119
+ pip install certifi
120
+ export SSL_CERT_FILE=$(python -c "import certifi; print(certifi.where())")
121
+ ```
122
+
123
+ For the python.org installer, you can also run the bundled
124
+ `Install Certificates.command` script found in `/Applications/Python X.Y/`.
125
+
100
126
  ## 🛠️ Contributing
101
127
 
102
128
  ### Setup Steps
@@ -55,6 +55,7 @@ tests/conftest.py
55
55
  tests/test_agent_sessions.py
56
56
  tests/test_agent_stop.py
57
57
  tests/test_api_client.py
58
+ tests/test_auth.py
58
59
  tests/test_config_file.py
59
60
  tests/test_deployment_status.py
60
61
  tests/test_docker_commands.py
@@ -9,4 +9,4 @@ toml~=0.10.2
9
9
  questionary~=2.1.0
10
10
 
11
11
  [pipecat]
12
- pipecat-ai>=0.0.79
12
+ pipecat-ai>=1.0.0