Skip to content

fix(deploy): existing-persona lookup uses /deployments list (not phantom /agents)#120

Merged
khaliqgant merged 3 commits into
mainfrom
fix-cli/existing-persona-uses-deployments-list
May 13, 2026
Merged

fix(deploy): existing-persona lookup uses /deployments list (not phantom /agents)#120
khaliqgant merged 3 commits into
mainfrom
fix-cli/existing-persona-uses-deployments-list

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

Summary

findExistingAgent was calling GET /workspaces/{ws}/agents?persona_slug=<slug>. That route is a dashboard proxy to an external gateway — it requires session-cookie auth and 403s for the cli:auth Bearer tokens the CLI uses. The real persona-deploy listing endpoint, added in cloud#580, is GET /workspaces/{ws}/deployments. Accepts cli:auth, returns {agents:[{agentId, personaId, deployedName, status, ...}], nextCursor}.

Why no ?personaId= server-side filter

agents.personaId on the cloud schema is a UUID FK to personas.id. The CLI only knows the persona's slug (the local persona JSON's id field, e.g. notion-essay-pr). Sending the slug as personaId= makes cloud's drizzle predicate eq(agents.personaId, slug) throw on the UUID cast → 500. The list is workspace-scoped (dozens of rows in practice, not thousands), so the right tradeoff is fetching unfiltered and matching client-side against deployedName (which cloud derives from persona.slug || persona.name || persona.id), with personaSlug and personaId as fallbacks.

Filed as a follow-up: cloud's /deployments ?personaId= filter should accept slugs too (currently UUID-only → 500 on slug input).

Changes

  • findExistingAgent: switched to /deployments (no query string), updated 401 hint to reference agentworkforce login (not the legacy workforce login).
  • parseAgentLike: accepts both {agentId} (new) and {id} (legacy preview). When expectedPersonaId is supplied, matches against any persona-identifying field on the row (deployedName, personaSlug, personaId); rows with no persona info pass through (legacy preview shape that pre-filtered server-side).
  • Skip status === 'destroyed' rows so a re-deploy with the same slug doesn't trip on-exists against a tombstone.

Tests

88 deploy tests pass. Updates:

  • Existing tests rewritten to disambiguate listing GET from deploy POST via init.method (both share the same URL now).
  • Two new cases:
    1. Full /deployments shape parsing{agents:[{agentId, personaId, deployedName, status}]} with destroyed tombstones and wrong-persona rows correctly skipped, only the active match returned.
    2. Ordering guarantee — listing GET fires before deploy POST.

Smoke

Built locally and ran against https://agentrelay.com/cloud with the actual user workspace 50587328-...:

$ agentworkforce deploy ./.agentworkforce/notion-essay-pr/persona.json --mode cloud --no-prompt --harness-source oauth
workforce deploy → ...persona.json
persona notion-essay-pr: 2 integration(s), 0 schedule(s)
workspace: 50587328-441d-4acb-b8f3-dbe1b3c5de99
integrations.notion: already connected
integrations.github: already connected
bundle: staged to ...runner.mjs (14.5KB)
mode: cloud
cloud: claude credentials already connected
cloud: deploying persona bundle to https://agentrelay.com/cloud   ← was: 403 on /agents, then 500 on /deployments?personaId=slug

Next failure surfaces server-side: cloud's Lambda bundle is missing @parcel/watcher-linux-x64-glibc@agentworkforce/persona-kit can't load on cloud → 500 persona_kit_unavailable. That's cloud packaging; separate PR.

Test plan

  • pnpm -F @agentworkforce/deploy run lint clean
  • pnpm -F @agentworkforce/deploy run test — 88/88 pass
  • Smoke against agentrelay.com/cloud with real workspace — existing-persona check passes, bundle upload reaches the deploy handler

🤖 Generated with Claude Code

…tom /agents)

`findExistingAgent` was calling
`GET /workspaces/{ws}/agents?persona_slug=<slug>`. That route is a
dashboard proxy to an external gateway: it requires session-cookie auth
and 403s for the `cli:auth` Bearer tokens the CLI uses. The actual list
of deployed personas — the `agents` table reader added in cloud#580 —
lives at `GET /workspaces/{ws}/deployments`, accepts `cli:auth`, and
returns `{agents:[{agentId, personaId, deployedName, status, ...}]}`.

Why no `?personaId=` server-side filter: `agents.personaId` on the
cloud schema is a UUID FK to `personas.id`. The CLI only knows the
persona's slug (the local persona JSON's `id` field). Sending the slug
as `personaId=` makes cloud's drizzle predicate throw on the UUID cast
→ 500. The list is workspace-scoped (dozens of rows, not thousands),
so the right tradeoff is fetching unfiltered and matching client-side
against `deployedName` (which cloud derives from
`persona.slug || persona.name || persona.id`), `personaSlug`, or
`personaId` as a fallback.

Changes:
* `findExistingAgent` → call `/deployments` (no query string).
* `parseAgentLike` accepts both `{agentId}` (new) and `{id}` (legacy
  preview shape). When `expectedPersonaId` is supplied, match it
  against any persona-identifying field on the row (deployedName,
  personaSlug, personaId). Rows with NO persona info at all (legacy
  preview shape that pre-filtered server-side) still pass through.
* Treat `status === 'destroyed'` as "not present" so a re-deploy with
  the same slug doesn't trip on-exists against a tombstone.
* Rewrite test mocks to disambiguate the listing GET from the deploy
  POST via `init.method` (both now share the same URL).
* Added two new tests: full /deployments shape parsing (with
  tombstone + wrong-persona rows skipped), and ordering guarantee
  (listing GET fires before deploy POST).

Smoke against production cloud with the user's actual Anthropic-
connected workspace: no more 403 on the existing-persona check, no
more 500 on the personaId filter. Bundle upload now reaches cloud's
deploy handler. (Next failure: cloud Lambda missing
`@parcel/watcher-linux-x64-glibc` native binary — server packaging
issue, separate PR.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7a490ad4-df69-4aab-8d9c-f4e18003f4cc

📥 Commits

Reviewing files that changed from the base of the PR and between 9bdae07 and 6532ee6.

📒 Files selected for processing (1)
  • packages/deploy/src/modes/cloud.test.ts

📝 Walkthrough

Walkthrough

This PR migrates cloud deployment's "existing agent" detection from a persona-slug query parameter endpoint to the workspace /deployments listing endpoint. Response parsing is updated to match against persona identifiers, skip destroyed entries, and handle unauthorized responses with login guidance. Test mocks and assertions are updated consistently across the suite.

Changes

Cloud Deploy Mode Endpoint Migration

Layer / File(s) Summary
Endpoint contract and response parsing
packages/deploy/src/modes/cloud.ts
Replaced /agents?persona_slug=... with workspace /deployments GET request. Added doc comment explaining route choice and slug-vs-UUID filtering semantics. Updated findExistingAgent / parsing to accept expectedPersonaId, match persona fields (deployedName, slug/personaSlug, UUIDs), skip status === 'destroyed' rows, prefer newest active rows, and report authorization errors with login instructions.
Test mock updates and preflight handling
packages/deploy/src/modes/cloud.test.ts
Updated multiple tests to mock GET .../deployments preflight (often { agents: [] }) and standardized fetch mocks to be init.method-aware. Adjusted retry and counting assertions to consider only POST .../deployments deploy attempts so preflight GETs don't affect counts.
Existing-persona and findExistingAgent tests
packages/deploy/src/modes/cloud.test.ts
Added and updated detailed tests for findExistingAgent, verifying destroyed tombstone skipping, persona-id mismatches, empty agents array behavior (deploy proceeds), refusing workspace rows missing persona identifiers, selecting newest active among multiples, preferring active over newer failed, and skipping malformed entries without throwing. Also updated existing-persona destroy/cancel subtests to use the new response shape and assert no deploy POST on cancel.
Input values test update
packages/deploy/src/modes/input-values.test.ts
Distinguished deploy POST .../deployments from preflight GET by requiring the deploy call predicate to have a defined JSON body, ensuring the test identifies the actual deploy POST.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • AgentWorkforce/workforce#102: Previous PR also migrating the cloud deploy mode's "existing persona/agent" flow to use the workspace /deployments contract instead of the older /agents?persona_slug=... endpoint.

Poem

🐰 The deployments path now shines so bright,
No more persona slugs in the night!
Destroyed agents fade, ID matches bloom,
Login guidance clears the auth gloom. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: switching existing-persona lookup from a legacy /agents endpoint to the /deployments list.
Description check ✅ Passed The description is detailed and directly related to the changeset, explaining the rationale, implementation details, test coverage, and smoke testing results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix-cli/existing-persona-uses-deployments-list

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

coderabbitai[bot]

This comment was marked as resolved.

Addressing review findings on PR #120:

* Workspace-scoped list rows without persona-identifying fields are no
  longer treated as matches. The "no persona fields → pass through"
  fallback was justified by legacy `{agent: {...}}` envelopes where the
  URL path implied persona-scoping, but cloud's new `/deployments` list
  is workspace-scoped — a row missing `deployedName`/`personaSlug`/
  `personaId` could belong to any persona in the workspace, and acting
  on it would let on-exists destroy the wrong agent. The legacy envelope
  keeps its back-compat via a `requirePersonaMatch: false` opt-in;
  every array element from the new endpoint sets `requirePersonaMatch: true`.

* When multiple rows match (destroy+redeploy race window, soft-delete
  windows), prefer the newest `active` row instead of whichever cloud
  returns first. Sort by status tier (active first) then `createdAt`
  descending. Previously the first parsed match won, which on most SQL
  backends meant insertion order — the opposite of what users expect.

Tests:
* `workspace-scoped list rows without persona-identifying fields are
  NOT matched` — proves the tightened filter ignores ambiguous rows.
* `multiple active rows for the same persona — newest wins` — proves
  the newest-active tiebreaker.
* `active row wins over an older active and over inactive rows` —
  proves status tier outranks createdAt.
* `malformed array entries (null/empty) are skipped without throwing` —
  defensive parser regression guard.

101/101 deploy tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@khaliqgant
Copy link
Copy Markdown
Member Author

Fresh-eyes review feedback addressed in 9bdae07:

  1. Tightened persona-match for workspace-scoped list rows. The "no persona fields → pass through" fallback now only applies to the legacy {agent: {...}} envelope where the URL path implied persona-scoping. Array elements from the new workspace-scoped /deployments list MUST identify their persona via deployedName, personaSlug, or personaId — otherwise they're skipped. This closes the hole where an on-exists destroy could have acted on the wrong agent.

  2. Multi-match winner now deterministic. When >1 row matches (destroy+redeploy race, soft-delete window), the resolver sorts by status tier (active first) then createdAt desc and returns the newest active row. Previously the first parsed match won, which on most SQL backends meant insertion order — the wrong choice for race recovery.

Four new tests:

  • workspace-scoped list rows without persona-identifying fields are NOT matched
  • multiple active rows for the same persona — newest wins
  • active row wins over an older active and over inactive rows
  • malformed array entries (null/empty) are skipped without throwing

101/101 deploy tests pass.

The review's lower-priority follow-ups (pagination, res.json() exception handling, cross-workspace defense) are tracked separately — they're real but pre-existing and not load-bearing for the immediate unblock.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@packages/deploy/src/modes/cloud.test.ts`:
- Around line 684-686: The fetch mock in the test (the fetch(url, init) stub)
returns the old legacy envelope { agent: { id: 'agent-old' } } for GET
/deployments; update this mock to return the new workspace-scoped list shape so
the cancel-path test exercises persona matching: return { agents: [{ agentId:
'agent-old', deployedName: '<name>', status: '<status>' }] } (use realistic
deployedName/status values used elsewhere in the test) so later ambiguity
assertions are valid, or alternatively move the legacy-envelope assertion into a
dedicated parser compatibility test; adjust any downstream expectations in this
test that read agent.id to instead read agents[0].agentId (or adapt to helpers
that parse the new shape).
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: edb640be-245d-43a5-a3d4-9b52e69c208f

📥 Commits

Reviewing files that changed from the base of the PR and between eb2b77b and 9bdae07.

📒 Files selected for processing (2)
  • packages/deploy/src/modes/cloud.test.ts
  • packages/deploy/src/modes/cloud.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/deploy/src/modes/cloud.ts

Comment thread packages/deploy/src/modes/cloud.test.ts
@khaliqgant khaliqgant merged commit 33436ba into main May 13, 2026
2 checks passed
@khaliqgant khaliqgant deleted the fix-cli/existing-persona-uses-deployments-list branch May 13, 2026 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant