Skip to content

fix(deploy): referenced secret sets are no longer rejected as not found (PCC-1021 follow-up) - #197

Merged
mattshep merged 3 commits into
mainfrom
jamsea/pcc-deploy-referenced-secret-gate
Aug 24, 2026
Merged

fix(deploy): referenced secret sets are no longer rejected as not found (PCC-1021 follow-up)#197
mattshep merged 3 commits into
mainfrom
jamsea/pcc-deploy-referenced-secret-gate

Conversation

@jamsea

@jamsea jamsea commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Found while running the full self-hosted secrets flow end to end on a kind gateway region (hush-0) against staging, ahead of showing it to customers.

The bug

pcc deploy cannot deploy an agent that uses a referenced secret set. Every one of them is rejected as missing:

[13:32:33] Verifying secret set pipecat-quickstart-secrets-hush-0 exists...
Error: Secret set 'pipecat-quickstart-secrets-hush-0' not found in organization 'following-antelope-aqua-372'

The set was healthy at the time: pipecat cloud secrets list showed it ready in hush-0, and secrets reference had just succeeded.

Why

The check in _deploy fetched the set through API.secrets_list(secret_set=...), which returns the set's array of key names, and then tested that array for truthiness.

Referenced sets carry no key-name rows. That is deliberate, from PCC-1020: "No Secret (key-name) rows — referenced sets are name+region+readiness only; contents are invisible by design." So the API does the right thing and the client misreads it.

Confirmed against staging, same org:

Set source status secrets length truthy
pipecat-quickstart-secrets-hush-0 referenced ready 0 False
pipecat-quickstart-secrets managed ready 6 True

The API response shape is identical in both cases (region, secrets, source, status); only the array length differs.

This blocks the phase-1 self-hosted secrets flow at the last step. PCC-1021 added secrets reference, the readiness display, and the secrets set guard, but the deploy-time existence check predates references and was not revisited.

The fix

Use API.secrets_get, which api.py already documents for exactly this ("Use this when you need readiness"), and abort only when nothing comes back. _secrets_get passes not_found_is_empty=True, so a genuinely missing set still returns None and still aborts.

Readiness deliberately stays server-side, where PCC-1020 put it: a fresh getSecretStatus at deploy time that blocks on unknown. This PR does not duplicate that client-side.

Tests

tests/test_deploy_referenced_secret_set.py, mirroring the structure of test_deploy_image_pull_secret.py:

  • a referenced set (secrets: [], source: referenced) passes the guard and reaches API.deploy
  • a managed set with key names still passes
  • a missing set (None) still aborts before API.deploy
  • an API error still aborts

Full suite: 433 passed. ruff format --diff and ruff check clean. pyright on deploy.py is unchanged at 3 pre-existing errors.

Note on releasing

secrets reference itself (#185) is also still unreleased: latest on PyPI is 1.1.0 from 2026-08-05, and #185 merged 2026-08-14. Anyone testing this flow from PyPI hits the missing subcommand first and this bug second, so the two want to ship together.

Draft because the release timing is Matt's call, and worth a check that keeping readiness purely server-side is the intended split.

The secret set existence check in `pcc deploy` called the per-set endpoint
through `API.secrets_list`, which returns the set's array of key NAMES, then
tested that array for truthiness.

Referenced secret sets (self-hosted regions, PCC-1020) deliberately carry no
key-name rows: Daily stores name + region + readiness and never the contents.
So the API correctly returns `secrets: []` for a healthy referenced set, the
check read that as "does not exist", and the deploy aborted with "Secret set
'X' not found in organization 'Y'". That made every referenced set
undeployable, which blocks the whole self-hosted secrets flow end to end.

Fetch the set itself with `API.secrets_get` and abort only when nothing comes
back. Readiness stays where PCC-1020 put it, in the server-side deploy gate
that does a fresh status check and blocks on `unknown`.

Verified against staging: a referenced set returns
`{"secrets": [], "region": "hush-0", "source": "referenced", "status": "ready"}`
while a managed set in the same org returns 6 key names.
@linear

linear Bot commented Aug 24, 2026

Copy link
Copy Markdown

PCC-1021

@jamsea jamsea self-assigned this Aug 24, 2026
@jamsea
jamsea marked this pull request as ready for review August 24, 2026 05:48
@jamsea
jamsea requested a review from mattshep August 24, 2026 06:28
@jamsea

jamsea commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Verified end to end on a live self-hosted region

Ran the full flow against staging with this branch installed as the CLI (pipecatcloud 1.1.1.dev21 from d7acd5c), deploying into a kind gateway region (hush-0) enrolled to region-gateway.staging.pipecat.daily.co.

The secret set under test is a real referenced set, not a fixture:

$ pipecat cloud secrets list
Secret Set Name                     Region   Status  Type
pipecat-quickstart-ecr-hush-0       hush-0   ready   Image Pull Secret
pipecat-quickstart-secrets          us-west  ready   Secret Set
pipecat-quickstart-secrets-hush-0   hush-0   ready   Secret Set

GET /v1/organizations/{org}/secrets/pipecat-quickstart-secrets-hush-0 returns {"secrets": [], "region": "hush-0", "source": "referenced", "status": "ready"}.

Before (released CLI and git main, same code)

[13:32:33] Verifying secret set pipecat-quickstart-secrets-hush-0 exists...
Error: Secret set 'pipecat-quickstart-secrets-hush-0' not found in organization 'following-antelope-aqua-372'

After (this branch)

[13:53:16] Verifying secret set pipecat-quickstart-secrets-hush-0 exists...
[13:53:17] Verifying image pull secret pipecat-quickstart-ecr-hush-0 exists...
[13:53:18] Updating agent manifest for 'pipecat-quickstart-hush-0'

The deploy went through, and the agent is serving:

$ pipecat cloud agent status pipecat-quickstart-hush-0
Ready: True
Deployment Phase: Active
Image: 034703319162.dkr.ecr.us-west-2.amazonaws.com/pipecat-quickstart:hush-0-test
Architecture: arm64
Resources: cpu=500m, memory=1Gi

Both pods reached 3/3 Running and the bot booted in-cluster (Pipecat 1.7.0, uvicorn listening on 8080), with env coming from the referenced secret set.

Two notes from the run:

  • The image-pull-secret guard (the secrets_list path this PR does not touch) already handled a referenced pull secret correctly, since it matches on name and type rather than key names. Only the secret set check was affected.
  • Nothing else in the deploy path needed changing for a referenced set. Everything after this guard worked first time.

…he output-mode tests

Two additions from review:

- `secrets set` had the identical existence-check misread: it fetched the
  set's key names and tested them for truthiness, so an existing
  referenced set read as nonexistent and the command said "Creating
  secret set" before hitting the server's cross-backend 409. It now
  fetches the set itself, and an existing referenced set gets a clear
  client-side refusal pointing at the cluster as where its contents are
  managed. Managed and missing sets behave as before. Tests verified red
  against the previous check.

- The output-mode tests failed in any shell exporting FORCE_COLOR (or
  TTY_COMPATIBLE): rich resolves is_terminal from those before asking
  the file, so consoles writing to plain buffers claimed to be
  terminals. conftest now scrubs both at module level, the same seam as
  the PIPECAT_CONFIG_PATH isolation, making detection depend only on the
  file the console is handed.

@mattshep mattshep 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.

Verified the whole chain independently, since this guard is the last step of the phase-1 self-hosted secrets flow:

  • The misread is real. The per-set GET (sandbox routes/secrets/controller.ts) returns {secrets, region, source, status}, and a referenced set's secrets is empty by design (PCC-1020: name + region + readiness only, never contents). The old client tested exactly that array. A genuine 404 also comes back falsy (not_found_is_empty), which is why the check ever looked correct — "missing" and "referenced" were indistinguishable to it.
  • The fix tests the right thing. secrets_get returns the set object or None on 404, so existence is decided on the set, not on key visibility.
  • On your open question: yes, readiness server-side is the intended split. validateSecretSetReady runs at all five deploy sites in the sandbox services controllers with a fresh reconcile at deploy time, so a client-side check would only add a stale second opinion. Good call not duplicating it.

Your 4 tests are well-shaped; I confirmed the positive case exercises the guard, and the fix-reverted run goes red.

I've pushed one commit onto the branch (e094dc5) with two additions found while reviewing — shout if you'd rather they travel separately:

  1. secrets set had the identical misread: an existing referenced set read as nonexistent, so the command said "Creating secret set" and then hit the server's cross-backend name-uniqueness 409. Same secrets_get fix, plus a clear client-side refusal for referenced sets (their contents are managed in the cluster, e.g. kubectl). Three tests, verified red against the old check.
  2. The output-mode tests were environment-sensitive: any shell exporting FORCE_COLOR (or TTY_COMPATIBLE) failed 5 of them locally while CI stayed green — rich resolves is_terminal from those env vars before ever asking the file. conftest now scrubs both at module level (the same seam as the PIPECAT_CONFIG_PATH isolation), so the suite result no longer depends on the developer's shell. Full suite: 436 passed.

Agreed on release coupling: secrets reference (#185) has never shipped (PyPI latest is 1.1.0, 08-05), so nobody outside the team can hit this yet — and the broken and fixed paths want to go out in the same release so the subcommand never exists without a working deploy.

Since we've now both contributed to the branch, handing the approval to fresh eyes.

@mattshep mattshep added the bug Something isn't working label Aug 24, 2026

@cbrianhill cbrianhill 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.

Approving. The fix is correct and the scope is exactly right.

I traced the central claim rather than taking it on faith, and it holds in a way that makes this even lower-risk than the description suggests: _secrets_list(secret_set=X) and _secrets_get build the identical URL (GET /secrets/{setName} → the sandbox's getSecretSet). So this changes only how the response is parsed — same endpoint, same server-side reconcile, no additional API calls. And getSecretSet returns secrets: set.secrets with Secret included as attributes: ['fieldName'], which independently confirms both the empty-array-for-referenced behavior and the object shape the secrets set overlap check consumes.

I also checked whether there was a third instance of the same misread, since two had already turned up. There isn't: unset and delete go straight to their delete endpoints with no key-name truthiness test, and the image-pull-secret guard matches on name + type off the org-wide list, which is a different response shape. The coverage here is complete.

One thing I specifically went looking for and did not find a problem with — the conftest.py env scrub. Scrubbing environment variables in conftest can quietly paper over real behavior, so I checked console.output_mode: it returns _explicit_output_mode before ever consulting is_terminal, so --output plain was never at risk from FORCE_COLOR. Only the auto-detection path was env-sensitive, and honoring FORCE_COLOR there is the desired production behavior. Right fix, and the comment's reasoning is accurate.

Two non-blocking comments inline: a missing CHANGELOG entry for the secrets set half, and a UX ordering nit where the new refusal lands after the confirmation prompt.

Agreed on the release coupling — secrets reference (#185) has never shipped, so these want to go out together or the subcommand exists without a working deploy. And nice work on the evidence: the live hush-0 before/after plus a fix-reverted red run is more than most PRs this size carry.

Comment thread CHANGELOG.md
for truthiness, but referenced sets carry no key names by design (Daily
stores name, region and readiness, never the contents), so every healthy
referenced set failed the check and no self-hosted deploy could use one. The
check now fetches the set itself; readiness is still enforced server-side.

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.

The entry covers the deploy guard, but not the secrets set fix that came in with e094dc5 — and that one is a user-visible behavior change too: pipecat cloud secrets set <referenced-set> now refuses client-side, where before it printed "Creating secret set" and then failed on the server's cross-backend 409.

This repo keeps a pretty thorough Keep-a-Changelog (secrets reference itself got a full Added entry), so a silently-undocumented command change stands out. Something like:

- `pipecat cloud secrets set` no longer treats an existing referenced secret
  set as nonexistent. It had the same key-names misread as the deploy guard,
  so it announced "Creating secret set" and then failed on the server's
  name-uniqueness check. It now refuses clearly, pointing at the cluster as
  the place a referenced set's contents are managed.

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.

Added in 752fd18, essentially your wording with one change: "refuses before asking for confirmation", since after the ordering fix below that is now also true.

Agreed it stood out. The Unreleased section documents the deploy guard in full, so leaving its sibling silent would have implied secrets set was untouched.

# deploy guard, cli#197): a referenced set carries no key-name rows by
# design, so testing the key array reads "exists but referenced" as
# "does not exist".
set_data, error = await API.secrets_get(org=org, secret_set=name)

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.

The refusal is right, but it fires late: this existence check sits after the if not skip_confirm: block, so a user running secrets set against a referenced set gets the full "Secrets to create / modify" panel, answers "Would you like to proceed with these secrets?", and only then is told the command doesn't apply to this set.

The ordering is pre-existing, but the new hard refusal is the first outcome where that confirmation is guaranteed wasted. Worth hoisting the secrets_get lookup above the confirmation block — as a side benefit, existing_set is already what decides the "Creating" vs "Modifying" wording, so the earlier fetch fits naturally there.

Non-blocking; happy for it to be a follow-up.

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.

Fixed in 752fd18 rather than deferred. Hoisted the whole secrets_get lookup above the if not skip_confirm: block; the diff is a pure block move, with the leading comment rewritten to record why the ordering matters so it does not drift back.

Your framing sharpened what the actual defect was. Every other check in create_set (name, file, key-value syntax, region) already refuses before prompting. The set lookup was the one validation that ran after, purely because it started life as the create-vs-modify decision rather than as a guard. Adding a hard refusal to it is what turned a latent ordering wart into a visible one, and it does read more naturally in the earlier position.

Added a test for the ordering, since none of the existing three would have caught a regression: all of them pass skip_confirm=True. It asserts questionary.confirm is never called, with API.properties never awaited as a second witness (that call exists only to render the panel's region line).

One detail worth recording. Verifying against the pre-hoist code, the test first failed with a TypeError because .ask_async() on a plain MagicMock is not awaitable. That proved the prompt was reached, but for the wrong reason. The mocked prompt now answers yes if it is reached, so the red is:

AssertionError: Expected 'confirm' to not have been called. Called 1 times.

Also worth flagging since it is the trade you are asking for: the lookup now happens before the panel, so a user who aborts at the prompt has made one extra read they previously would not have. Cheap, and the right way round.

One thing I did not take: you noted existing_set is what decides the "Creating" vs "Modifying" wording, which now means the panel title Secrets to create / modify could resolve to the exact verb. I read that as justifying the hoist rather than requesting the change, and it is a separate UX edit, so I left it. Happy to do it if you meant it as a suggestion.

…ngelog

Both non-blocking notes from Brian's review:

- The `secrets set` refusal fired after the confirmation block, so a user
  running it against a referenced set saw the full "Secrets to create /
  modify" panel and answered "proceed?" before being told the command does
  not apply to that set. The set lookup now runs ahead of the prompt, which
  also restores the invariant that every refusal in this command happens
  before any question. It already decided the create-vs-modify wording, so
  it reads naturally in the earlier position.

- Added the missing CHANGELOG entry for the `secrets set` half of e094dc5.
  It is a user-visible behavior change (it announced "Creating secret set"
  and then failed on the server's name-uniqueness check) and the Unreleased
  section documents the deploy guard already.

New test covers the ordering: the three existing cases all pass
skip_confirm=True, so none of them would have caught a regression here. It
asserts questionary.confirm is never called, with API.properties never
awaited as a second witness, since that call exists only to render the
panel. The mocked prompt answers yes if reached, so the pre-hoist run fails
on that assertion rather than on an unawaitable mock.

Full suite: 437 passed. ruff format --diff and ruff check clean. pyright on
secrets.py reports 0 errors.
@mattshep
mattshep merged commit ea7eb4b into main Aug 24, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants