Skip to content

feat: connect a person's own account for assistant turns that act for them - #8044

Merged
danielkov merged 16 commits into
mainfrom
daniel/aim-598-feat-assistant-user-connections
Oct 30, 2026
Merged

danielkov merged 16 commits into
mainfrom
daniel/aim-598-feat-assistant-user-connections

Conversation

@danielkov

@danielkov danielkov commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

AIM-598 (2 of 2)

Stacked on #8043.

Summary

  • Whose connection. A turn credential that acts for a person (an agent principal with AuthorizerUserID U) now resolves personal auth clients against U's own connection. Before, it used the agent subject, which owns no connection. connectionSubject feeds both the mandatory mcp:auth check and the token lookup, for direct servers and gateway members.
    • Admission already binds U as the represented user with U's own policy, so the existing connection_owner = $represented_user rule now matches U. The agent's, owner's and U's policies all still apply, and U's owner blocks keep working.
    • Turns that act for nobody (workload principals) are unchanged: no person's connection resolves.
  • Recovery link. On a direct server, when U's connection for the chosen personal client is missing or expired, the gate answers Connection required or Reconnect required (a 412 with a readable message) instead of the 401 that sent the runtime into the owner-only OAuth flow. The message carries a link and tells the assistant to deliver it privately or not at all.
    • The link is <site>/mcp/connection_recovery/{token}, always on the canonical dashboard origin. It is an opaque, single-use cache entry bound to U, the organization and the auth client (with its registration and server), expiring after 15 minutes. The message is not logged, because it carries the link.
    • Start:
      1. The browser's Speakeasy session must be U. A signed-out browser goes to login and comes back.
      2. Support and impersonated sessions are refused.
      3. U must still be an active member who passes the mcp:auth check for the client.
      4. Only then is the link used up and U's upstream login started, through the existing remote-login flow.
    • Completion: before the connection is stored, the callback rechecks U's membership and mcp:auth. If the browser presents a session, it must be U. If the callback lands on a host without the session cookie, the server-held, single-use state bound to U stands in. The login is consumed only after these checks, so a stray callback cannot spend it, and a recovery login whose entry is missing fails closed.
    • Someone else sees "This link is for someone else" and learns nothing more, and the link stays usable for U. A denied U sees "No access" with no way to connect. Neither page reveals the upstream URL.
    • Recovery never changes the assistant's choice or picks another client.
  • Delivery. The assistant instructions name the exact link prefix, so a link from any other origin (for example, one an MCP server's own error text makes up) is never relayed. The Slack and dashboard adapters route genuine links to the person the turn acts for (Slack: ephemeral, else DM; never the channel or the owner). Without a private route, the assistant gives non-sensitive "open Speakeasy and connect" instructions. The owner-only routing of assistant_mcp_auth_required is unchanged.
  • Scope
    • Links are issued for direct servers. A gateway member keeps pointing to the gateway's sign-in page, where the person signs in as themselves and connects their own account, which turns acting for them then use.
    • Links are not tied to a thread. The person asks the assistant to retry after connecting.

Motivation

RFC sections 7.4, 7.5 and 7.7: an agent uses only its authorizing user's connection and never borrows the owner's. When that connection is missing, the person gets a connect link bound to them, which completion verifies. A denial stays "No access".

@danielkov
danielkov requested a review from a team as a code owner October 24, 2026 17:08
@linear-code

linear-code Bot commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

AIM-598

@changeset-bot

changeset-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2a2cfc3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
server Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@danielkov

Copy link
Copy Markdown
Contributor Author

Demo

Connection link completion page

  1. The page a person lands on after connecting their own account through a connection link. The "expired", "for someone else" and "No access" pages use the same layout (covered by tests).

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 12 files

This PR changes authentication, authorization, or input validation. Ultrareviews find 2.4x more serious bugs than standard reviews. Comment @cubic-dev-ai ultrareview to run one.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread server/internal/assistants/service.go Resolved
Comment thread server/internal/assistants/service.go Resolved

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

Reviewed incremental diff against #8043 at f141723b220d5e2c51153b3cc35b17e3763f7df8; tested combined head in an isolated local stack with synthetic credentials/local OAuth stub. No novel confirmed defect to annotate inline; not repeating resolved comments. COMMENT, not blanket approval: private-delivery behavior remains unverified end to end.

Check PASS/FAIL
Recovery security: U/org/registration binding, membership removal, malformed/expired state, wrong-user/support/impersonation refusal, concurrent single-use completion PASS — 21 focused test entries; real PostgreSQL/Redis, mocked browser authentication and permission decisions
Personal subject resolution: direct/gateway select U, never owner/agent; attribution-only context cannot substitute for verified credential PASS — 8 final new test entries including HTTP checks below
Post-authentication HTTP gate: missing/expired U → 412 without challenge; permission denial → 403 without link; userless rejection → 401 PASS — production selection/rejection + HTTP serializer; injected selection/permitter, not full public-router admission
Existing credential/policy/resolver regressions PASS — separate focused runs: 17 and 88 entries (overlap; not a unique-test total)
Assistant adapter/composition/history regressions and outbound-tool characterization PASS — 49 entries, including fake-transport actual Slack tool probes; no Slack network calls
Standalone recovery routes: invalid/expired/future/zero-date state; signed-out redirect with exact return path; wrong user, missing membership, support, impersonation PASS — real server/session checks; refused starts preserve token
Standalone authorized start → local OAuth exchange → persisted session → completion; start/callback replay PASS — 302 start, 303 callback, exactly one token exchange and persisted session, 200 completion; replay 404/401
Recovery-route privacy headers PASS — all 11 tested recovery-route responses assert no-store/no-referrer
Browser invalid-link rendering and real local dev-IDP login PASS — 2 scenarios; no external requests attempted

Standalone total: 14 HTTP + 2 browser scenarios. Recovery cache entries were synthetically seeded, so this bypasses issuance; ordinary login and callback exchange/persistence were real. Support/impersonation sessions were synthetic clones. Mandatory enforcement mode was not independently confirmed in the standalone run. Browser login and HTTP return-path assertions were separate, not a continuous browser recovery journey.

Not verified (not counted as PASS): model-driven private Slack delivery, trusted-U recipient mapping, ephemeral→DM fallback, no-private-route suppression, dashboard stream/transcript access, full issuance-to-retry flow, or selected-profile immutability. Adapter changes are prompt instructions; actual outbound Slack tools pass supplied URLs/recipients unchanged. Direct arbitrary-tool-argument probes do not demonstrate that the revised model violates those instructions, so this is a coverage limitation, not a new reproduced finding.

No production edits, commits, pushes, external OAuth providers, or real Slack calls. Cleanup verified: isolated stack, volumes/networks, scoped Pub/Sub resources, browser session, disposable worktree, temporary tests/caches/logs, and local review refs removed. Shared emulator preserved; main checkout clean.

@blacksmith-sh

This comment has been minimized.

Base automatically changed from daniel/aim-598-feat-assistant-profile-choices to main October 30, 2026 10:23
@danielkov
danielkov requested a review from a team as a code owner October 30, 2026 10:23
… them

Person-backed turns resolve the authorizing user's connection. A missing or
expired one answers with a single-use link bound to that user, organization
and client; starting and completing it verify the signed-in user and their
access, and a denial shows No access.
Issue links on the canonical dashboard origin and relay only links with
that prefix, route them to the person the turn acts for, keep the link out
of logs, validate a login's callback before using it up, and accept a
callback on another host when the person's access still checks out.
@danielkov
danielkov force-pushed the daniel/aim-598-feat-assistant-user-connections branch from 654edfc to cff2976 Compare October 30, 2026 10:31

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 14 files

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

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread server/internal/assistants/service.go Resolved
Comment thread server/internal/mcp/impl.go Resolved
Require the starting browser cookie (via the remote login hop when the callback host differs) before storing a connection, re-resolve the server auth client at login start and completion, mount the link routes with browser error pages, and give worker turns the site URL.
@blacksmith-sh

This comment has been minimized.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 9 files (changes from recent commits).

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

View guided diff | Turn on auto-fix | Re-trigger cubic

…cp:auth check

Principal-backed API keys name their validated authorizer as the connection owner for typed auth clients and read that user connection; the mandatory check still decides with the agent grants. Also satisfy lint, use a generated query for the callback-origin fixture, bound browser hops in tests, and drop a stale-attachment case the check reports as an error.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 7 files (changes from recent commits).

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

View guided diff | Turn on auto-fix | Re-trigger cubic

ResolveAuthClientAccessTokens sent principal-backed keys to the attachment path before the authorizer branch; treat a key asking for its authorizer as unattached. Cover the token retrieval, assert the shadow decision, and reuse the consent registration helper for a callback origin.
@blacksmith-sh

This comment has been minimized.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 5 files (changes from recent commits).

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

View guided diff | Turn on auto-fix | Re-trigger cubic

…nnection

Keep the agent subject so an explicit attachment still binds its pinned connection; without one, a principal-backed agent key names its validated authorizer as the connection owner for the agent mcp:auth check and reads that user connection. Align the reconnect check with token retrieval.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 7 files (changes from recent commits).

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

View guided diff | Turn on auto-fix | Re-trigger cubic

… handback paths

Under enforcement, a legacy agent gateway member and a handback to the upstream client choice read an unattached agent key from its validated authorizer own connection when the agent mcp:auth check on a catalog client names that owner. Explicit attachments still serve first, pinned; before enforcement the key is still denied.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 12 files (changes from recent commits).

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

View guided diff | Turn on auto-fix | Re-trigger cubic

@danielkov
danielkov added this pull request to the merge queue Oct 30, 2026
@danielkov
danielkov removed this pull request from the merge queue due to a manual request Oct 30, 2026
…zer fallback

An agent key falls back to its authorizer own connection only when the agent was never bound for the sign-in and registration. A revoked binding or one pinned to a replaced source keeps failing closed. Also cover a connect-only credential ceiling, the policy agent sessions store.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 8 files (changes from recent commits).

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

View guided diff | Turn on auto-fix | Re-trigger cubic

…their authorizer

Remove the binding-history query and Bound flag: a revoked attachment never serves, and the authorizer path stands on its own mcp:auth decision. Admitted agent sessions take the same authorizer path as agent keys; issued v4 sessions keep their admission-derived auth ceiling.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 11 files (changes from recent commits).

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

View guided diff | Turn on auto-fix | Re-trigger cubic

@danielkov
danielkov added this pull request to the merge queue Oct 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 30, 2026
@danielkov
danielkov added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 0ae2b74 Oct 30, 2026
49 checks passed
@danielkov
danielkov deleted the daniel/aim-598-feat-assistant-user-connections branch October 30, 2026 17:15
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants