Skip to content

feat(integration): adopt verb to bind an existing Nango connection#141

Merged
khaliqgant merged 2 commits into
mainfrom
agent-relay/integration-adopt-cli
May 12, 2026
Merged

feat(integration): adopt verb to bind an existing Nango connection#141
khaliqgant merged 2 commits into
mainfrom
agent-relay/integration-adopt-cli

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

@khaliqgant khaliqgant commented May 12, 2026

Summary

  • Adds relayfile integration adopt PROVIDER --connection-id ID [--workspace NAME] [--provider-config-key KEY] [--yes]. Mirrors the disconnect command's flag parsing and confirmation prompt. Posts to Cloud's new POST .../integrations/{provider}/adopt route and on success persists the new connection in the local mirror and clears any prior disconnect marker so the status probe stops reporting the workspace as disconnected.
  • Adds adoptIntegration(provider, connectionId, options) to the TypeScript SDK next to disconnectIntegration. Returns { connectionId, replacedConnectionId? } so callers can surface whether a stale prior row was migrated.
  • Teaches apiClient.do to read Cloud's { error } field (alongside { message }) so 4xx bodies surface the operator-facing message instead of a raw JSON blob.
  • Updates the top-level CLI usage block to advertise integration adopt.

Cloud dependency

The Cloud route is AgentWorkforce/cloud#537 (agent-relay/integration-adopt-endpoint, draft pending merge of #536). This PR can be reviewed in parallel but should land after the Cloud endpoint is deployed.

Test plan

  • go test ./cmd/relayfile-cli/ — full Go CLI suite green, 5 new adopt-specific tests (fresh-insert success, replacement reporting, 409 refusal preserves local state, --connection-id required, disconnect-marker cleared on adopt)
  • npm test --workspace packages/sdk/typescript — 138 tests green, 3 new SDK tests (request shape, replacedConnectionId presence/absence, 409 surfaces as CloudApiError)
  • tsc --noEmit in packages/sdk/typescript — clean
  • go build ./cmd/relayfile-cli/ — clean
  • Manual: run relayfile integration adopt github --connection-id <real conn> against a staging workspace

`relayfile integration adopt PROVIDER --connection-id ID` mirrors the
disconnect command's flag shape and confirmation prompt, then POSTs to the
new Cloud route POST .../integrations/{provider}/adopt. The SDK gains
`adoptIntegration(provider, connectionId, options)` next to
`disconnectIntegration`. On success the CLI saves the new connection
locally and clears any prior disconnect marker; on 4xx the local state is
left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 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: 43d51087-f825-4345-b589-49f4c5d8c1cc

📥 Commits

Reviewing files that changed from the base of the PR and between 570846d and e3d21df.

📒 Files selected for processing (2)
  • cmd/relayfile-cli/main.go
  • cmd/relayfile-cli/main_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • cmd/relayfile-cli/main.go
  • cmd/relayfile-cli/main_test.go

📝 Walkthrough

Walkthrough

This PR adds relayfile integration adopt to bind an existing provider connection to a workspace/provider slot without re-running OAuth; it adds SDK support, CLI wiring including provider validation and local state persistence, enhanced Cloud error parsing, and tests covering happy and error flows.

Changes

Integration Adopt Feature

Layer / File(s) Summary
TypeScript SDK adoption endpoint
packages/sdk/typescript/src/setup.ts, packages/sdk/typescript/src/setup.test.ts
WorkspaceHandle.adoptIntegration validates inputs, posts to Cloud /integrations/{provider}/adopt with connectionId and optional providerConfigKey, normalizes connectionId and optional replacedConnectionId, clears pending connection state, and returns the identifiers. Tests verify successful adoption, omitted replacedConnectionId, and 409 conflict error conversion.
CLI adoption command and error handling
cmd/relayfile-cli/main.go
Adds integration adopt subcommand and runIntegrationAdopt handler: parses flags (--connection-id, --provider-config-key, --workspace, --yes), validates provider id, prompts for confirmation unless --yes, calls Cloud via SDK, persists adopted connection state only after Cloud success, clears any prior disconnect marker, and enhances API error parsing to map Next.js-style error payloads into the API error message.
CLI adoption tests
cmd/relayfile-cli/main_test.go
Adds test server helpers and workspace setup to mock token refresh, join, and adopt endpoints. Tests assert: request body includes connectionId and providerConfigKey, uses refreshed token, prints adoption confirmation, surfaces replacedConnectionId, preserves local state on 409, validates required --connection-id, rejects unsafe provider strings, and clears disconnect marker on success.

Sequence Diagram

sequenceDiagram
  participant User
  participant CLI
  participant SDK
  participant Cloud
  User->>CLI: integration adopt --connection-id=conn-123
  CLI->>CLI: parse & validate flags
  CLI->>User: confirm adoption?
  User-->>CLI: yes
  CLI->>SDK: adoptIntegration(provider, conn-123, { providerConfigKey? })
  SDK->>Cloud: POST /api/v1/workspaces/{workspaceId}/integrations/{provider}/adopt
  Cloud-->>SDK: { connectionId, replacedConnectionId? }
  SDK-->>CLI: adoption result
  CLI->>CLI: persist local connection state
  CLI->>CLI: clear disconnect marker
  CLI-->>User: adopted successfully
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit binds connections with a cheerful nod,
"Adopt" links providers without OAuth fraud,
Cloud signs and replies, local state then stays,
Tests hop around to prove all the ways,
A tiny CLI triumph — hooray! 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% 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 directly and accurately reflects the primary change: adding an 'adopt' verb to bind existing integration connections, which is the main feature across all modified files.
Description check ✅ Passed The description comprehensively covers the changeset, detailing the new CLI command, SDK method, error handling improvements, and test coverage across all modified files.
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 agent-relay/integration-adopt-cli

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

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

🧹 Nitpick comments (2)
cmd/relayfile-cli/main_test.go (2)

2264-2266: ⚡ Quick win

Don’t ignore credential setup errors in these tests.

Line 2264, Line 2308, and Line 2373 swallow loadCloudCredentials/saveCloudCredentials failures; this can mask setup breakage and make failures misleading.

Proposed explicit error checks
-	creds, _ := loadCloudCredentials()
+	creds, err := loadCloudCredentials()
+	if err != nil {
+		t.Fatalf("loadCloudCredentials failed: %v", err)
+	}
 	creds.APIURL = server.URL
-	_ = saveCloudCredentials(creds)
+	if err := saveCloudCredentials(creds); err != nil {
+		t.Fatalf("saveCloudCredentials(update) failed: %v", err)
+	}

Also applies to: 2308-2310, 2373-2375

🤖 Prompt for 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.

In `@cmd/relayfile-cli/main_test.go` around lines 2264 - 2266, The test currently
ignores errors from loadCloudCredentials/saveCloudCredentials which can hide
setup failures; update each call site (where loadCloudCredentials() and
saveCloudCredentials(creds) are invoked in main_test.go — the occurrences around
the blocks currently at the three locations) to check the returned error and
fail the test on error (e.g., use t.Fatalf or t.Helper()+t.Fatalf or
require.NoError) so any credential load/save failure aborts the test and
surfaces the real cause.

2145-2150: ⚡ Quick win

Strengthen bootstrap endpoint assertions to avoid false-positive adopt tests.

Line 2145 and Line 2148 currently accept refresh/join requests without validating method/auth/body. That lets refresh-flow regressions pass undetected (e.g., stale token accepted on join).

Proposed tightening in the test helper
 		switch {
 		case r.URL.Path == "/api/v1/auth/token/refresh":
 			seen["refresh"]++
+			if r.Method != http.MethodPost {
+				t.Fatalf("expected refresh POST, got %s", r.Method)
+			}
+			if got := r.Header.Get("Authorization"); got != "Bearer cld_old" {
+				t.Fatalf("unexpected refresh Authorization: %q", got)
+			}
+			var body cloudTokenRefreshRequest
+			if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
+				t.Fatalf("decode refresh body failed: %v", err)
+			}
+			if body.RefreshToken != "refresh_123" {
+				t.Fatalf("unexpected refresh token: %q", body.RefreshToken)
+			}
 			_, _ = w.Write([]byte(`{"apiUrl":"` + server.URL + `","accessToken":"cld_new","refreshToken":"refresh_123","accessTokenExpiresAt":"2030-05-01T00:00:00Z"}`))
 		case r.URL.Path == "/api/v1/workspaces/ws_123/join":
 			seen["join"]++
+			if r.Method != http.MethodPost {
+				t.Fatalf("expected join POST, got %s", r.Method)
+			}
+			if got := r.Header.Get("Authorization"); got != "Bearer cld_new" {
+				t.Fatalf("unexpected join Authorization: %q", got)
+			}
 			_, _ = w.Write([]byte(`{"workspaceId":"ws_123","token":"rf_join","relayfileUrl":"` + server.URL + `"}`))
🤖 Prompt for 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.

In `@cmd/relayfile-cli/main_test.go` around lines 2145 - 2150, The test HTTP
handler currently accepts any request to "/api/v1/auth/token/refresh" and
"/api/v1/workspaces/ws_123/join" without validating method, headers or body
which allows false-positive adopt tests; update the handler for those cases (the
switch arms matching those paths) to assert the expected HTTP method (e.g.,
POST), validate the Authorization header or request body contains the expected
refresh token/credentials for "/api/v1/auth/token/refresh" and expected join
token/body for "/api/v1/workspaces/ws_123/join", increment seen["refresh"] or
seen["join"] only after validation, and return a 400/401 when validation fails
so regressions (stale tokens/method mismatches) fail the test.
🤖 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 `@cmd/relayfile-cli/main.go`:
- Around line 1808-1810: The os.Remove call that clears the disconnect marker
for record.LocalDir is currently ignoring all errors; change this to capture the
returned error from os.Remove(filepath.Join(record.LocalDir, ".relay",
"disconnected", provider+".json")) and only ignore it when the error is
os.IsNotExist(err); for any other error, surface it (e.g., return it or log it
via the existing logger) so unexpected filesystem failures aren’t silently
swallowed—update the code around record.LocalDir and provider to handle and
propagate/log non-ENOENT errors.
- Around line 1729-1810: The provider string from normalizeProviderID(fs.Arg(0))
must be validated/sanitized before any local filesystem operations
(saveIntegrationConnection, filepath.Join/ os.Remove) to prevent path traversal;
add a strict check right after provider is set (e.g., ensure no path separators
or "..", and match a safe regexp like allowed chars [a-z0-9_-] with a non-empty
value) and return an error if it fails, then proceed to use provider for
saving/removing files only when it passes validation.

---

Nitpick comments:
In `@cmd/relayfile-cli/main_test.go`:
- Around line 2264-2266: The test currently ignores errors from
loadCloudCredentials/saveCloudCredentials which can hide setup failures; update
each call site (where loadCloudCredentials() and saveCloudCredentials(creds) are
invoked in main_test.go — the occurrences around the blocks currently at the
three locations) to check the returned error and fail the test on error (e.g.,
use t.Fatalf or t.Helper()+t.Fatalf or require.NoError) so any credential
load/save failure aborts the test and surfaces the real cause.
- Around line 2145-2150: The test HTTP handler currently accepts any request to
"/api/v1/auth/token/refresh" and "/api/v1/workspaces/ws_123/join" without
validating method, headers or body which allows false-positive adopt tests;
update the handler for those cases (the switch arms matching those paths) to
assert the expected HTTP method (e.g., POST), validate the Authorization header
or request body contains the expected refresh token/credentials for
"/api/v1/auth/token/refresh" and expected join token/body for
"/api/v1/workspaces/ws_123/join", increment seen["refresh"] or seen["join"] only
after validation, and return a 400/401 when validation fails so regressions
(stale tokens/method mismatches) fail the test.
🪄 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: 35600739-b5fc-4a80-a9ab-3d5f23210c1c

📥 Commits

Reviewing files that changed from the base of the PR and between 2b3eb18 and 570846d.

📒 Files selected for processing (4)
  • cmd/relayfile-cli/main.go
  • cmd/relayfile-cli/main_test.go
  • packages/sdk/typescript/src/setup.test.ts
  • packages/sdk/typescript/src/setup.ts

Comment thread cmd/relayfile-cli/main.go
Comment thread cmd/relayfile-cli/main.go
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

@khaliqgant khaliqgant merged commit 9e64fb9 into main May 12, 2026
7 checks passed
@khaliqgant khaliqgant deleted the agent-relay/integration-adopt-cli branch May 12, 2026 09:15
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