Skip to content

docs(planning): SaaS connectors & outbound integration hardening plan - #669

Open
ginccc wants to merge 2 commits into
mainfrom
docs/saas-connectors-plan
Open

docs(planning): SaaS connectors & outbound integration hardening plan#669
ginccc wants to merge 2 commits into
mainfrom
docs/saas-connectors-plan

Conversation

@ginccc

@ginccc ginccc commented Aug 11, 2026

Copy link
Copy Markdown
Member

Planning doc only — no code changes, no behavior change.

What this is

The design record for connecting EDDI agents to third-party SaaS platforms (Jira, Amplitude, Google Drive, …) over REST, MCP, or A2A with each platform's real auth model, including per-end-user OAuth.

What the investigation established

All file:line claims were spot-checked against source (see the plan's Verified header), not taken from a summary:

  • Eleven outbound integration paths with five independent credential-resolution implementations, all static-credential only. Adding OAuth to each separately would be a five-times problem — hence the unification phase.
  • Zero OAuth client machinery anywhere. No callback, no PKCE, no token exchange, no refresh storage. A repo-wide search for authorization_code / grant_type / refresh_token / redirect_uri / token_endpoint in src/main/java returns nothing.
  • No per-user credential dimension. SecretReference is (tenantId, keyName); PropertySetterTask.autoVaultSecret keys per agent, so two end users of one agent overwrite each other's secret.
  • Vendor-hosted MCP servers are unreachable (Atlassian, Amplitude): they require OAuth 2.1, and a 401 currently just trips the circuit breaker rather than being read as an auth challenge. The MCP client is streamable-HTTP-only; stdio is hard-rejected, though StdioMcpTransport ships in the pinned langchain4j-mcp jar.

Exposure findings that gate the connector work: /mcp (33 tools) is unauthenticated under the shipped authorization.enabled=false default while AuthStartupGuard effectively never fires; two REST endpoints take credentials as query parameters, one echoing the credential back in its response body; tool results reach the LLM verbatim while only tool descriptions are sanitized; SecretScrubber misses array elements and URL-embedded credentials on export.

Plan shape

Six phases, sequenced so each is independently shippable:

  1. Close the exposure gap (McpStartupGuard, credential params → body, console redaction, scrubber holes)
  2. Govern tool results + MCP/A2A approval request-pinning
  3. Unify the five credential paths behind a ConnectionConfiguration resource + ${connection:name} reference with SERVICE | PER_USER binding — the gate for everything below
  4. stdio via sidecar bridge (docs, no code)
  5. OAuth client-credentials + MCP RFC 9728 discovery
  6. Per-user authorization-code + PKCE + Manager "Connect" flow
  7. Deferred (native stdio behind an admin template catalog)

Decisions worth reviewing

  • Connection resolution deliberately does not live in SecretResolver — that would break the two properties the vault grant-enforcement design depends on (no identity in the resolver; ChatModelRegistry caches on unresolved params).
  • The OAuth callback must be a permit path (application-type=service means no bearer on a browser redirect); a single-use, DB-persisted state is its only guard; PKCE mandatory.
  • PER_USER connections refuse to deploy when authorization.enabled=false. Without a verified identity, any caller claiming userId=alice resolves Alice's tokens — the /v1 adapter's trust-user-headers caveat documents exactly this failure mode. The same guard refuses OAuth connections when the vault is inert, so refresh tokens can never hit the autoVaultSecret plaintext-fallback path.
  • The global authorization.enabled default stays untouched — per-surface guard + path policy, per the precedent in openai-api-adapter-plan.md and mcp-hitl-surface-plan.md.
  • The SSRF default flip is flagged as needing product sign-off, not done silently: the false default is documented intent (application.properties:341-343), not an oversight.

Rebase note

Rebased onto #668, which required two corrections: the MCP resource bridge (exposeResources) is a second untrusted-content surface on the existing MCP connection (same credential, so not a new connector path — but resource text is verbatim and listing metadata is ungoverned, now folded into G5); and strict task-level toolApprovals. The plan's C11 constraint was re-checked against ToolApprovalGate.classify and still holds.

Open questions (plan §13)

§13.1 is the one that must be answered before Phase 5: in a group conversation, whose token does a member agent use? GroupConversationService.discuss() creates per-member conversations, so the principal may not be a human at all.

Summary by CodeRabbit

  • Documentation
    • Added a changelog entry covering SaaS connector and outbound integration updates, including implementation phases, authentication decisions, security considerations, and initial priorities.
    • Added a comprehensive implementation plan documenting connection models, OAuth flows, transport support, security controls, testing considerations, rollout phases, non-goals, and open design questions.
    • No public API or exported entity changes were introduced.

Design record for connecting agents to third-party SaaS (Jira, Amplitude,
Google Drive) over REST/MCP/A2A with real auth, including per-user OAuth.

Establishes: eleven outbound paths with five independent credential
implementations, zero OAuth machinery, no per-user credential dimension.
Plans six phases: exposure hardening, tool-result governance, a unifying
ConnectionConfiguration resource with ${connection:} references and
SERVICE|PER_USER binding, stdio-via-sidecar, OAuth client-credentials +
RFC 9728 MCP discovery, and per-user authorization-code + PKCE.

Key guards: PER_USER connections refuse to deploy under
authorization.enabled=false (unverifiable principal = token theft);
OAuth connections require an active vault; connection resolution stays
out of SecretResolver to preserve the grant-enforcement design's caching
assumptions.
@ginccc
ginccc requested a review from rolandpickl as a code owner August 11, 2026 18:36
@github-actions

Copy link
Copy Markdown

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ginccc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 688e96dc-45af-4117-bfb1-a12059f3df77

📥 Commits

Reviewing files that changed from the base of the PR and between 1c7b697 and b43c3da.

📒 Files selected for processing (2)
  • docs/changelog.md
  • planning/saas-connectors-plan.md
📝 Walkthrough

Walkthrough

The PR adds a changelog entry and a detailed plan for SaaS connectors and outbound integration hardening. It defines connection configuration, encrypted grants, OAuth flows, MCP transport work, security controls, phased delivery, metrics, tests, and unresolved design questions.

Changes

SaaS connectors and outbound hardening

Layer / File(s) Summary
Scope and outbound inventory
planning/saas-connectors-plan.md
Defines the plan scope, acceptance scenarios, outbound integration inventory, credential-resolution fragmentation, and identified security gaps.
Connection model and constraints
planning/saas-connectors-plan.md
Documents inherited constraints and proposes tenant-scoped ConnectionConfiguration resources with encrypted grants, binding rules, validation, and export restrictions.
Resolution, OAuth, and transport flows
planning/saas-connectors-plan.md
Specifies per-request connection resolution, principal and origin checks, token refresh behavior, OAuth flows, MCP discovery, and transport support.
Delivery and verification plan
planning/saas-connectors-plan.md, docs/changelog.md
Lists phased implementation work, anticipated files, metrics, behavioral verification, implementation boundaries, unresolved questions, and the changelog entry.

Estimated code review effort: 2 (Simple) | ~15 minutes

Suggested reviewers: rolandpickl

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the SaaS connectors and outbound integration hardening plan added by the pull request.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/saas-connectors-plan

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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 12

🧹 Nitpick comments (2)
planning/saas-connectors-plan.md (2)

575-582: 🔒 Security & Privacy | 🔵 Trivial | 🏗️ Heavy lift

Qualify the sidecar isolation claim.

A sidecar still executes the MCP server binary and communicates with EDDI over a network channel. Container separation reduces the blast radius; it does not eliminate process-execution or supply-chain risk.

Document image provenance, non-root execution, resource limits, network policy, and bridge authentication.

🤖 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 `@planning/saas-connectors-plan.md` around lines 575 - 582, Qualify the
isolation claims in the “Phase 3a — sidecar bridge” section to state that
container separation reduces blast radius but does not eliminate
process-execution or supply-chain risks. Document requirements for image
provenance, non-root execution, resource limits, network policy, and
authentication between EDDI and the bridge.

734-745: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Keep metric tags bounded.

connection and server are user-configurable. Raw connection names and server URLs can create unbounded Micrometer series and expose tenant or provider details.

Use bounded tags such as auth type, binding, source, and outcome. Keep specific identifiers in sanitized logs or exemplars.

🤖 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 `@planning/saas-connectors-plan.md` around lines 734 - 745, Update the metric
definitions in the observability metrics table to remove user-configurable
connection names and server URLs from tags. Use only bounded categorical values
such as auth type, binding, source, and outcome for connection and MCP metrics,
while retaining specific identifiers only in sanitized logs or exemplars.
🤖 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 `@docs/changelog.md`:
- Line 19: Update the phase-count wording in the “Plan shape” entry to match the
listed phases 0 through 6: either call them seven phases or explicitly exclude
the deferred Phase 6 from the six delivery phases.

In `@planning/saas-connectors-plan.md`:
- Around line 468-480: Update the OAuth callback flow described in the state
validation/exchange step to atomically claim the persisted state with a
conditional database update requiring consumedAt IS NULL before exchanging the
code. Continue only when the update claims exactly one row; reject already
consumed or expired states, and retain the single-use guarantee under concurrent
callbacks.
- Around line 428-443: Update the “single-flight per grant, with optimistic
locking” design to acquire a cross-replica refresh lease or atomic
refresh_in_progress claim before calling the token endpoint. Ensure only the
claimant refreshes, while other replicas wait and re-read the resulting token;
retain the existing version CAS as the final write guard and preserve the stated
failure semantics.
- Around line 327-355: Update OAuthConfig validation and the baseUrlAllowlist
documentation to use one canonical origin format, including schemes as required.
Validate tokenUrl, authorizationUrl, and discoveryUrl separately against the
trusted provider or authentication-endpoint allowlist, while keeping API target
validation based on baseUrlAllowlist.
- Around line 759-790: Extend the behavioral-test checklist with coverage for
the declared storage and authorization boundaries: verify grants are excluded
from exports, connection references are traversed by VaultGrantChecker,
connection and grant writes cannot use operator scope, and tenant isolation
holds for both Mongo and Postgres. Add one test for each boundary before marking
Phase 2 and Phase 5 complete.
- Around line 507-519: Update the ConnectionStartupGuard validation to parse
publicBaseUrl and normalize it before constructing the redirect URI. Require a
valid HTTPS origin with an authority and no userinfo, path, query, or fragment
(except an allowable trailing root slash), while preserving the dev/test
behavior and fail-closed startup errors; use the normalized origin when
appending /connections/callback.
- Around line 115-120: Update the connector inventory and the referenced MCP,
A2A, and httpcalls flows so every connection or discovery request uses
SafeHttpClient, including RFC 9728 resource_metadata retrieval. Add URL
validation, safe redirect handling, and cross-origin credential stripping;
restrict discovered metadata to the configured server or trusted allowlist, and
validate the RFC 9728 resource value before consuming it. Apply the same
requirements to the related sections at the other referenced locations.
- Around line 323-345: Define StaticAuth so every credential-bearing field uses
a ${vault:...} reference rather than an inline secret, and update
OAuthConfig.extraAuthParams to represent only non-secret protocol parameters.
Ensure connection validation rejects secret-bearing extra parameters and
preserves C4, export scrubbing, and VaultGrantChecker enforcement.
- Around line 623-631: Remove the credential-bearing GET compatibility forms for
discover-tools and discover-endpoints in the discovery endpoints and Manager
callers. Retain only a credential-free migration response, or remove the GET
routes entirely when the POST APIs are released; do not accept or process
apiKey/apiAuth query parameters because rejection occurs after they have already
entered URL-based logging and tracing.
- Around line 642-655: Update the C6 operator write-scope exclusion coverage for
connection capabilities to include both ConnectionConfiguration writes and the
Phase 5 authorization, connect, and disconnect grant-lifecycle routes. Add an
authorization test for each excluded route, verifying operators cannot invoke
them through write scope.
- Around line 453-460: Update the service-account client_credentials plan
section to specify grant_type=client_credentials, the supported
client-authentication method, and application/x-www-form-urlencoded request
encoding. Add behavioral tests covering the complete token request contract.
- Around line 619-631: Add a dedicated /secretstore/* authorization path policy
requiring eddi-admin, and update the startup validation to reject
authorization-disabled launches when the secret store endpoint is reachable,
unless an explicit opt-out is provided. Implement this before Phase 2, using the
existing startup-guard and path-policy patterns rather than relying on
IRestSecretStore’s `@RolesAllowed` annotation or the catch-all policy.

---

Nitpick comments:
In `@planning/saas-connectors-plan.md`:
- Around line 575-582: Qualify the isolation claims in the “Phase 3a — sidecar
bridge” section to state that container separation reduces blast radius but does
not eliminate process-execution or supply-chain risks. Document requirements for
image provenance, non-root execution, resource limits, network policy, and
authentication between EDDI and the bridge.
- Around line 734-745: Update the metric definitions in the observability
metrics table to remove user-configurable connection names and server URLs from
tags. Use only bounded categorical values such as auth type, binding, source,
and outcome for connection and MCP metrics, while retaining specific identifiers
only in sanitized logs or exemplars.
🪄 Autofix

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

Review profile: CHILL

Plan: Pro Plus

Run ID: 7826954a-a111-44f2-9483-c8262ffc006c

📥 Commits

Reviewing files that changed from the base of the PR and between 00420da and 1c7b697.

📒 Files selected for processing (2)
  • docs/changelog.md
  • planning/saas-connectors-plan.md

Comment thread docs/changelog.md Outdated
Comment thread planning/saas-connectors-plan.md
Comment thread planning/saas-connectors-plan.md
Comment thread planning/saas-connectors-plan.md Outdated
Comment thread planning/saas-connectors-plan.md
Comment thread planning/saas-connectors-plan.md
Comment thread planning/saas-connectors-plan.md
Comment thread planning/saas-connectors-plan.md
Comment thread planning/saas-connectors-plan.md
Comment thread planning/saas-connectors-plan.md
…es, tokenUrl allowlisting

CodeRabbit review on #669: 14 findings, all valid, all applied. Three
changed the design rather than the prose.

/secretstore has no path-specific policy, so it falls to the catch-all
authenticated policy — and DisabledAuthController disables that AND
@RolesAllowed under the shipped authorization.enabled=false. Vault
writes, DEK rotation and reset are unauthenticated out of the box. The
draft flagged /mcp and missed the credential store itself. New Phase 0.8.

Two TOCTOU races: token refresh relied on a version CAS checked at write
time, after both replicas had already called the token endpoint and the
provider had rotated the refresh token — now an atomic cross-replica
claim before the network call. Same class in the OAuth callback, where
validate-then-consume let two callbacks redeem one code.

tokenUrl was excluded from allowlisting while receiving the vault-resolved
clientSecret — a client-secret exfiltration path. Credential endpoints now
validate against their own allowlist with one canonical origin format.

Also: StaticAuth defined with reference-only secret fields; extraAuthParams
restricted to non-secret params; deprecated GET routes drop the credential
param entirely rather than rejecting it post-arrival; RFC 9728 discovery
constrained to same-origin metadata and pre-approved authorization servers;
grant-lifecycle routes excluded from operator write scope; client_credentials
request contract specified; publicBaseUrl parsed not prefix-matched; sidecar
isolation claim qualified; metric tags reduced to bounded categoricals; six
storage/authorization boundary tests added.
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.

2 participants