Skip to content

fix(deploy): detect workspace-scoped integration fallback#157

Merged
khaliqgant merged 3 commits into
mainfrom
fix/deploy-slack-typed-connect
May 28, 2026
Merged

fix(deploy): detect workspace-scoped integration fallback#157
khaliqgant merged 3 commits into
mainfrom
fix/deploy-slack-typed-connect

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

Summary

  • treat ready workspace-scoped integration rows as a compatibility fallback for bare/default deployer-user persona integrations
  • apply the same fallback during OAuth polling so a connect flow can resolve when cloud reports the completed row at workspace scope
  • keep explicit workspace and workspace_service_account source checks scoped to their exact table

This addresses the typed persona deploy symptom where slack: {} missed an already-connected workspace Slack row and then hung after opening the Nango connect URL. Picker prompts are expected to proceed once Slack is counted connected because connectedIntegrations now includes the provider.

Verification

  • corepack pnpm --filter @agentworkforce/persona-kit build
  • corepack pnpm --filter @agentworkforce/runtime build
  • corepack pnpm --filter @agentworkforce/deploy test -- connect.test.ts (142/142 passing)

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Review Change Stack

Warning

Review limit reached

@khaliqgant, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 41 minutes and 17 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b6d12ed9-5b17-40cd-b0ba-e09294ef1354

📥 Commits

Reviewing files that changed from the base of the PR and between 527d93f and 8e70be8.

📒 Files selected for processing (4)
  • packages/deploy/src/connect.test.ts
  • packages/deploy/src/connect.ts
  • packages/persona-kit/src/parse.test.ts
  • packages/persona-kit/src/parse.ts
📝 Walkthrough

Walkthrough

Updated integration connectivity resolution to support scope-aware checks with fallback logic. The isConnected and connect methods now delegate readiness decisions to centralized helpers and retry under workspace scope when deployer_user scope returns non-ready, with optional connectionId parameter passing through status queries.

Changes

Integration connectivity with scope fallback

Layer / File(s) Summary
Connectivity helpers and status API
packages/deploy/src/connect.ts
Introduces statusIsConnectedForSource, workspaceFallbackSource, and readConnectionId helpers to centralize scope-aware connected checks and fallback logic. Updates fetchIntegrationStatusForScope to accept optional connectionId query parameter instead of ad-hoc query building.
isConnected legacy workspace fallback
packages/deploy/src/connect.ts, packages/deploy/src/connect.test.ts
Refactored isConnected to use new helpers for scope-aware connectivity; when primary deployer_user scope is not connected, computes and retries with workspace fallback scope. Test verifies two-step fallback: initial non-ready deployer_user status triggers retry with workspace scope, returning connected once workspace is ready.
OAuth polling with workspace fallback
packages/deploy/src/connect.ts, packages/deploy/src/connect.test.ts
Refactored connect OAuth polling to fetch status via centralized helper with optional connectionId, use status readiness helper to decide polling completion, and optionally retry under fallback source. Test validates connect-session request body structure, asserts polling sequence across deployer_user then workspace scopes, and verifies workspace-scoped connectionId is returned.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • AgentWorkforce/workforce#131: Modifies the same isConnected and connect resolver methods to make integration checks scope-aware with fallback adjustments between deployer_user and workspace endpoints.
  • AgentWorkforce/workforce#109: Updates relayfile integration auto-connect flow and resolver "connected"/polling behavior in the same packages/deploy/src/connect.ts file.
  • AgentWorkforce/workforce#155: Modifies the same packages/deploy/src/connect.ts resolver to rely on ready integration status via scoped status calls with fallback support.

Poem

🐰 Scope and fallback, a tale of care,
First deployer_user, then workspace there,
When one scope sleeps, another awakes,
Connected at last, no integration breaks! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(deploy): detect workspace-scoped integration fallback' directly and specifically describes the main change—adding workspace-scoped integration fallback detection.
Description check ✅ Passed The description provides relevant context about treating workspace-scoped integrations as fallback and applies this during OAuth polling, directly relating to the changeset modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/deploy-slack-typed-connect

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 and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a fallback mechanism to the workspace scope for legacy default personas when checking or establishing integration connections. It refactors status checks and polling logic in connect.ts to utilize helper functions and fall back to the workspace scope if the initial deployer_user scope is not ready. Corresponding unit tests have been added to verify this fallback behavior. Feedback was provided to optimize the polling loop by resolving the workspace token once per iteration instead of multiple times.

Comment on lines +232 to +260
const status = await fetchIntegrationStatusForScope({
fetchImpl,
statusUrl.toString(),
await resolveWorkspaceToken(opts.workspaceToken)
);
if (isConnectedStatus(status)) {
const connectionId = readString(status, 'connectionId')
?? readString(status, 'currentConnectionId')
apiUrl,
token: await resolveWorkspaceToken(opts.workspaceToken),
workspaceId,
provider,
source: effectiveSource,
...(sessionId ? { connectionId: sessionId } : {}),
io
});
if (statusIsConnectedForSource(status, provider, effectiveSource)) {
const connectionId = readConnectionId(status)
?? sessionId
?? provider;
io?.info(`${provider} connected.`);
return { connectionId };
}

const fallbackSource = workspaceFallbackSource(effectiveSource);
if (fallbackSource) {
const fallbackStatus = await fetchIntegrationStatusForScope({
fetchImpl,
apiUrl,
token: await resolveWorkspaceToken(opts.workspaceToken),
workspaceId,
provider,
source: fallbackSource,
io
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In the polling loop, resolveWorkspaceToken(opts.workspaceToken) is called twice per iteration (once for the primary status check and once for the fallback status check). Since resolving the workspace token can be an expensive asynchronous operation (e.g., performing network requests or I/O to refresh/retrieve credentials), it should be resolved once at the start of each loop iteration and reused.

        const token = await resolveWorkspaceToken(opts.workspaceToken);
        const status = await fetchIntegrationStatusForScope({
          fetchImpl,
          apiUrl,
          token,
          workspaceId,
          provider,
          source: effectiveSource,
          ...(sessionId ? { connectionId: sessionId } : {}),
          io
        });
        if (statusIsConnectedForSource(status, provider, effectiveSource)) {
          const connectionId = readConnectionId(status)
            ?? sessionId
            ?? provider;
          io?.info(provider + " connected.");
          return { connectionId };
        }

        const fallbackSource = workspaceFallbackSource(effectiveSource);
        if (fallbackSource) {
          const fallbackStatus = await fetchIntegrationStatusForScope({
            fetchImpl,
            apiUrl,
            token,
            workspaceId,
            provider,
            source: fallbackSource,
            io
          });

@khaliqgant
Copy link
Copy Markdown
Member Author

Cross-reference: sibling workspace-identity work across the stack.

This bug is the third surface of a workspace-identity-resolution gap that's been chased today. All three are independent bugs but share the same root pattern: lookup keys / scope filters miss legitimate matches because the resolver expects an exact shape it doesn't always get.

Surface Bug Fix
cloud-web (integration route edges) App workspace UUID not resolving to bound Relayfile rw_* workspace at integration status / connect-session / list / runtime mounts cloud#1297 — MERGED
cloud-web (slack-proxy) /api/v1/proxy/slack resolved workspaceId: '(unknown)' and 401'd before any upstream call cloud#1322 — open
relayfile-worker (ACL matcher) Token had relayfile:fs:write:/github/* scope but ACL rule required exact scope-string membership before semantic path matching cloud#1319 — open
workforce CLI deploy resolver (this PR) Workspace-scoped integration rows not used as fallback for bare/default-deployer-user persona integrations workforce#157

The pattern is: a resolver that should fall back semantically instead enforces exact-key matching, so legitimate state (Slack connected, GitHub scope authorized, etc.) reads as "missing" to the caller.

Worth knowing for future workspace-identity-resolver work — if anyone reports a similar "X is connected but doesn't show as connected" or "I have permission but I'm getting 401/403" → check whether the lookup path has the same exact-vs-semantic-match gap.

Copy link
Copy Markdown
Contributor

@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: 2

🤖 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/connect.ts`:
- Around line 250-267: The fallback poll is currently unscoped and can pick up a
different ready row for the same provider; update the fallback logic in
connect() so the workspace-scope poll is pinned to the original row by passing
and enforcing the same discriminator used by the primary poll (e.g.,
expectedConfigKey or the original connectionId/sessionId) into
fetchIntegrationStatusForScope and in the subsequent check. Concretely: have
connect() carry the expectedConfigKey (from preflight) or the original sessionId
into the call to fetchIntegrationStatusForScope, then change the success check
around statusIsConnectedForSource and readConnectionId to verify the returned
status's configKey/connectionId matches the expectedConfigKey/sessionId before
returning { connectionId } so we never resolve against a different integration
row.
- Around line 882-888: The helper workspaceFallbackSource currently broadens any
deployer_user source to a workspace fallback; change it to only do that when the
source was implicit/default by adding a flag (e.g., wasImplicit: boolean) or
similar sentinel parameter and return { kind: 'workspace' } only if source.kind
=== 'deployer_user' && wasImplicit; update callers that previously normalized
omitted/explicit sources to pass true when the caller derived a default/implicit
deployer_user and false for explicitly authored sources so explicit user-scoped
integrations are not widened to workspace scope.
🪄 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: 3e8a8b05-7eaa-4e27-9591-c6d200c020a3

📥 Commits

Reviewing files that changed from the base of the PR and between 8f2e0a9 and 527d93f.

📒 Files selected for processing (2)
  • packages/deploy/src/connect.test.ts
  • packages/deploy/src/connect.ts

Comment thread packages/deploy/src/connect.ts Outdated
Comment thread packages/deploy/src/connect.ts Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/deploy/src/connect.ts">

<violation number="1" location="packages/deploy/src/connect.ts:235">
P2: `resolveWorkspaceToken` is awaited twice per poll iteration—once for the primary status check and again for the fallback. If the resolver performs I/O (e.g., a token refresh), this is wasteful and may hit rate limits. Resolve it once at the top of the loop and reuse the value.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/deploy/src/connect.ts Outdated
Comment thread packages/deploy/src/connect.ts Outdated
Comment thread packages/deploy/src/connect.ts Outdated
const connectionId = readString(status, 'connectionId')
?? readString(status, 'currentConnectionId')
apiUrl,
token: await resolveWorkspaceToken(opts.workspaceToken),
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.

P2: resolveWorkspaceToken is awaited twice per poll iteration—once for the primary status check and again for the fallback. If the resolver performs I/O (e.g., a token refresh), this is wasteful and may hit rate limits. Resolve it once at the top of the loop and reuse the value.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/deploy/src/connect.ts, line 235:

<comment>`resolveWorkspaceToken` is awaited twice per poll iteration—once for the primary status check and again for the fallback. If the resolver performs I/O (e.g., a token refresh), this is wasteful and may hit rate limits. Resolve it once at the top of the loop and reuse the value.</comment>

<file context>
@@ -217,31 +229,43 @@ export function relayfileIntegrationResolver(opts: {
-          const connectionId = readString(status, 'connectionId')
-            ?? readString(status, 'currentConnectionId')
+          apiUrl,
+          token: await resolveWorkspaceToken(opts.workspaceToken),
+          workspaceId,
+          provider,
</file context>

@khaliqgant
Copy link
Copy Markdown
Member Author

Pushed review-feedback patch 8e70be8 addressing the three outstanding bot comments:

  • Workspace fallback polling now preserves the OAuth session discriminator by polling the fallback scope with the same connectionId, checking returned connectionId when present, and carrying the expected provider config key through the fallback readiness check. Added a regression test that rejects a ready workspace row with a different connectionId.
  • Workspace fallback is now gated to implicit/default integration sources only. parseIntegrationConfig marks omitted source with a non-enumerable internal flag, and explicit source: { kind: "deployer_user" } no longer widens to workspace fallback. Added parser + deploy resolver coverage.
  • Double token resolution per poll iteration was already fixed in 750952b; verified it remains in the current diff.

Verification in clean #157 worktree:

  • corepack pnpm --filter @agentworkforce/persona-kit test -> 228/228
  • corepack pnpm --filter @agentworkforce/deploy test -- connect.test.ts -> 144/144

Holding merge pending CI and reviewer re-check; no self-merge while these comments are still pending.

@khaliqgant khaliqgant merged commit e80a40b into main May 28, 2026
3 checks passed
@khaliqgant khaliqgant deleted the fix/deploy-slack-typed-connect branch May 28, 2026 10: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