Skip to content

feat: Google Drive access for agents (provider connection) + connection auth helpers - #160

Merged
linyiru merged 7 commits into
mainfrom
feat/google-drive-agent-capability
Aug 12, 2026
Merged

feat: Google Drive access for agents (provider connection) + connection auth helpers#160
linyiru merged 7 commits into
mainfrom
feat/google-drive-agent-capability

Conversation

@linyiru

@linyiru linyiru commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What & why

Gives June-built agents the ability to read from and write to Google Drive — an agent can save a file it produced, or read a file/path from Drive. Scenarios: "agent generated a report → store it in Drive", "read a reference doc from a known path".

Rather than a one-off tools module, Drive lands in the connections family (an agent's outbound edges), with a small, principled extension so it fits cleanly.

How it's built

1. New provider connection kind (@junejs/core/connections)

The existing mcp/openapi kinds wire a generic remote. Drive's REST API has quirks neither can express — multipart content upload, alt=media downloads, export for Google-native docs, and path→id resolution over the folder graph (save_file is a compound resolve→find→create/update). So a third kind, defineProviderConnection({ name, connect, requiresPrincipal? }), lets a remote bring its own transport while still joining the connection lifecycle: connectAll reports it, isolates its failures, the durable/edge target wires it lazily, and requiresPrincipal stamps every exposed tool.

2. Google Drive integration (@junejs/core/google-drive)

googleDriveConnection(config) is the first provider — tools: list_files, find_file, read_file, create_file, update_file, save_file (upsert-by-path), create_folder, delete_file. Each is a defineAction, so it's simultaneously an agent tool, a UI server action, and an /mcp tool. googleDriveTools(config) is also exported for programmatic defineAgent. Pure + fetch-only (edge-safe, no node:*). The OAuth2 token is resolved per call, server-side via auth(ctx) — it never reaches the model. MCP annotations: readOnlyHint on reads, idempotentHint on save_file, destructiveHint on delete_file.

3. Directory convention: array tool exports

A tools/*.ts file may now default-export one tool or an array; defineAgent/assembleDurable flatten arrays (native discovery + edge-compiled module).

4. Connection auth helpers (@junejs/server)

Turn an authenticated principal into a per-call token for any connection:

  • linkedAccountAuth({ providerId, store }) — generic, auth-library-agnostic, fail closed (no principal / unlinked account throws).
  • betterAuthAccessToken(auth, { providerId }) / betterAuthAccountTokenStore(auth) — the blessed Better Auth convenience, structural (BetterAuthLike) so it adds no better-auth dependency. Service Account / raw-token users pay nothing (pure opt-in exports).

This leans on June's web nature: the awkward OAuth redirect/callback/token-storage dance is owned by the (blessed) Better Auth integration, so auth(ctx) just reads the caller's linked Google token.

Tests & coverage

  • google-drive.ts: 100% lines/funcs (in-memory Drive fake: query subset, multipart/media upload, export/alt=media).
  • connections.ts: provider kind fully covered (sync/async connect, report, error isolation, requiresPrincipal, synthetic url, mixed kinds).
  • connection-auth.ts: 100% (fail-closed paths, structural Better Auth adapter, SA path bypass).
  • tsc clean (core + server); core 476 pass, server 501 pass, 0 fail.

Docs & changesets

  • docs/google-drive-integration.md — full guide: provider-kind rationale, tools, usage (connection + programmatic), authorization models (OAuth vs Service Account), how to obtain credentials, the web-native Better Auth flow, and the shipped helper.
  • Changesets: @junejs/core minor + @junejs/server minor.

Note: bun run build fails in this environment at config-load due to a missing unrun toolchain module (pre-existing, unrelated to this change; fails identically on main). Type gate and full test suites pass.

linyiru and others added 4 commits August 11, 2026 22:22
Add @junejs/core/google-drive: googleDriveTools(config) returns a set of
defineActions (list/find/read/create/update/save/create_folder/delete) that give
a June agent the ability to read from and write to Google Drive. Because they're
ordinary actions they're simultaneously an agent tool, a UI server action, and
an /mcp tool. Pure + fetch-only (edge-safe), so Drive access works on the native
host and in a Durable Object alike.

Identity mirrors connections: the OAuth2 access token is resolved per call,
server-side via auth(ctx) — it never reaches the model — so a multi-tenant app
mints the caller's short-lived token. requiresPrincipal hides the tools from
anonymous turns. Drive's quirks are handled honestly: multipart content upload,
alt=media downloads, export for Google-native docs, and slash-path resolution
over the folder graph (mkdir -p on save).

A tools/*.ts file may now default-export one tool OR an array of tools;
defineAgent/assembleDurable flatten arrays (native discovery + edge-compiled
module), so the whole integration drops into agent/tools/ as a single
`export default googleDriveTools({ ... })`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reframe the Drive integration from a standalone tools module into the
connections family, where an agent's outbound edges belong. Add a third
connection kind alongside mcp/openapi: defineProviderConnection({ name, connect,
requiresPrincipal? }). A provider brings its own transport — connect(ctx?)
returns its tools as defineActions — for remotes the generic mcp/openapi clients
can't express (multipart uploads, alt=media downloads, compound path→id ops). It
still joins the lifecycle: connectAll reports it (kind "provider"), isolates its
failures, the durable/edge target wires it lazily, and requiresPrincipal stamps
every exposed tool.

googleDriveConnection(config) is the first provider; googleDriveTools(config)
stays exported for programmatic defineAgent use. The Drive client is unchanged.

Coverage: google-drive.ts 100% lines/funcs, connections.ts 100% funcs. New
tests cover the provider kind (sync/async connect, report, error isolation,
requiresPrincipal, synthetic url, mixed kinds) and Drive-through-connectAll
(shape, end-to-end save/read, custom name, requiresPrincipal, per-call auth),
plus previously-uncovered Drive tools (create_file via folderPath, create_folder,
update_file) and the non-JSON error fallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document how the auth token is obtained: OAuth 2.0 (user) vs Service Account,
which to pick, and the simplest way to get credentials (OAuth Playground for dev,
a Service Account JSON key for a single shared account, an OAuth client for
multi-user). Add the web-native path — because June is a web framework with a
resolved request principal, the Better Auth integration can own the Google OAuth
consent/callback/token-storage flow, so auth(ctx) just reads the caller's linked
Google token. Add a design note: the auth seam stays overridable; a blessed
helper belongs at the @junejs/server layer (not pure core), connection-agnostic
and fail-closed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add host-layer helpers that turn an authenticated principal into a per-call
bearer token for an outbound connection (provider/mcp/openapi), resolved
server-side so the token never reaches the model:

- linkedAccountAuth({ providerId, store }): the generic, auth-library-agnostic
  core — inject an AccountTokenStore and get a fail-closed auth(ctx) that mints
  the caller's token. Connection-agnostic (Notion/Slack/GitHub/Drive alike).
- betterAuthAccessToken / betterAuthAccountTokenStore: the Better Auth
  convenience, kept structural (BetterAuthLike) so it adds NO better-auth
  dependency. A Service Account / raw-token user pays nothing (opt-in exports).

Lives in @junejs/server by design (the auth integration is not core's job); pure
logic, no node:*. 100% covered; server suite green (501 pass). Updated the Drive
doc to use the shipped helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds Google Drive read/write capabilities through a new provider connection model and introduces per-principal connection authentication helpers.

Changes:

  • Adds provider connections and Google Drive actions.
  • Supports array exports from tool modules.
  • Adds Better Auth helpers, tests, documentation, and release metadata.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/june/test/connection-auth.test.ts Tests connection authentication helpers.
packages/june/src/index.ts Exports authentication APIs.
packages/june/src/connection-auth.ts Implements linked-account token resolution.
packages/june/src/agent-discover.ts Discovers array tool exports.
packages/core/test/purity.test.ts Verifies Google Drive subpath resolution.
packages/core/test/google-drive.test.ts Tests Drive tools and connection behavior.
packages/core/test/connections.test.ts Tests provider connections.
packages/core/test/agent-config.test.ts Tests tool-array flattening.
packages/core/src/google-drive.ts Implements Google Drive actions and transport.
packages/core/src/connections.ts Adds the provider connection kind.
packages/core/src/agent-config.ts Flattens tool-array entries during assembly.
packages/core/package.json Exports the Google Drive module.
docs/google-drive-integration.md Documents configuration and authorization.
.changeset/connection-auth-helpers.md Records the server authentication API release.
.changeset/agent-google-drive.md Records Drive and provider connection releases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/core/src/connections.ts Outdated
// Uniform gate: `requiresPrincipal` on the connection stamps every exposed
// action — the same guarantee connectMcp/connectOpenapi give. Idempotent if a
// provider already stamped its own actions.
if (c.requiresPrincipal) for (const a of actions) a.requiresPrincipal = true;
Comment on lines +45 to +50
// Build an `auth(ctx)` for ANY June connection that mints the CALLER's
// linked-account token. FAIL CLOSED: a missing principal or an unlinked account
// throws — so an authenticated-but-unlinked user gets a clear error, and
// (together with the connection's `requiresPrincipal`, which hides the tools from
// anonymous turns) the capability is never reachable without a real credential.
export function linkedAccountAuth(opts: LinkedAccountAuthOptions): ConnectionAuth {
Comment thread packages/core/src/google-drive.ts Outdated
Comment on lines +181 to +188
let parent = "root";
if (segments.length > 1) {
try {
parent = await resolveFolderPath(segments.slice(0, -1), { create: false }, ctx);
} catch {
return null; // an intermediate folder doesn't exist ⇒ the file can't
}
}
Comment thread packages/june/src/connection-auth.ts Outdated
// Adapt a Better-Auth-shaped instance into an AccountTokenStore.
export function betterAuthAccountTokenStore(auth: BetterAuthLike): AccountTokenStore {
return async ({ userId, providerId }) => {
const res = await auth.api.getAccessToken({ providerId, userId });
Adopt all four reviewer findings:

1. Provider requiresPrincipal is now applied at REGISTRATION, not by post-hoc
   mutation. defineAction snapshots the gate into the Flight server-reference
   wrapper, so retro-setting action.requiresPrincipal left that path ungated.
   ProviderConnection.connect now receives { requiresPrincipal } and must thread
   it into its defineActions; connectAll fail-fast verifies every tool is gated
   (never mutates). googleDriveConnection threads it through to googleDriveTools.

2. linkedAccountAuth's compatibility claim narrowed: it is for connections whose
   auth resolves per call with identity (providers). MCP/OpenAPI call auth at
   discovery with ctx=undefined, which this fail-closed helper rejects — documented
   in code + docs.

3. resolvePathToFile no longer masks HTTP errors. It walks with findChild (null
   only for a genuinely absent segment); 401/403/429/5xx now propagate instead of
   surfacing as { found: false } / a misleading missing-file error. Dead
   create:false branch of resolveFolderPath removed.

4. betterAuthAccountTokenStore uses Better Auth's real endpoint shape
   { body: { providerId, userId } } (was passing fields top-level); BetterAuthLike,
   the call, and the fake test updated.

Coverage: google-drive.ts & connection-auth.ts 100%/100%. New tests: provider
gate-at-registration + fail-fast on a non-compliant provider, HTTP-error
propagation through path resolution, and the getAccessToken body shape. Full
suites green (core 483, server 502).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Suppressed comments (7)

packages/core/src/google-drive.ts:150

  • Drive permits multiple children with the same name, so taking files[0] makes path resolution arbitrary. In particular, save_file can overwrite an unrelated duplicate. Fetch two matches and fail on ambiguity rather than silently selecting one.
    const { files } = await listFiles({ q: clauses.join(" and "), pageSize: 1 }, ctx);
    return files[0] ?? null;

packages/core/src/google-drive.ts:420

  • save_file uses a non-atomic find-then-create sequence. Two concurrent identical calls can both see no file and create duplicates (Drive does not enforce unique sibling names), so advertising idempotentHint can cause clients to retry this operation as though it were safe. Remove the hint unless the implementation gains an idempotency/serialization mechanism.
    annotations: { title: "Save Drive file (upsert)", idempotentHint: true },

packages/core/src/google-drive.ts:39

  • There is no configurable base folder for path operations; both path walkers hard-code Drive's root. This makes the documented service-account setup (share a target folder or Shared Drive) unable to use save_file/find_file/path reads, because that shared location is not the service account's My Drive root. Add a root folder/drive ID to the config and use it as the initial parent for both readers and writers.
export type GoogleDriveConfig = {
  // Tool id prefix — the tools are `<name>__read_file`, `<name>__save_file`, …
  // mirroring the connection naming convention. Default "gdrive".
  name?: string;
  // Resolves an OAuth2 access token (scope drive / drive.file) for the call.

packages/core/src/google-drive.ts:324

  • The tool description says the return value is metadata or null, but the implementation returns { found, file }. Since this description is shown to the model, callers are given the wrong result contract. Describe the wrapper shape used by the implementation.
    description:
      "Find a file or folder by its slash-separated path from the Drive root, e.g. 'Reports/2024/summary.txt'. Returns the file's metadata (id, name, mimeType, ...) or null when it does not exist.",

docs/google-drive-integration.md:93

  • This assembleAgent(mod) example does not type-check because AgentModule requires surfaceInstructions, but the object omits it. Include an empty map as the other optional convention fields are already doing.
const mod = {
  config: { name: "archivist" },
  instructions: "Save your outputs to Drive and read reference docs from it.",
  tools: [], skills: [], channels: {}, channelInstructions: {},
  connections: [googleDriveConnection({ auth: () => ({ token: process.env.GOOGLE_DRIVE_TOKEN! }) })],
};
const agent = await assembleAgent(mod);

.changeset/agent-google-drive.md:11

  • The release note documents connect(ctx?), but the new API actually calls connect({ requiresPrincipal }); no action context is passed. Update the signature so consumers do not implement the provider contract incorrectly.
  `defineProviderConnection({ name, connect, requiresPrincipal? })`. A provider
  brings its OWN transport — `connect(ctx?)` returns the provider's tools as
  `defineAction`s — for remotes the generic mcp/openapi clients can't express

docs/google-drive-integration.md:45

  • The implementation returns { found, file }, not metadata or null directly. Update this table entry to match the tool's actual result contract.
| `gdrive__find_file`     | Resolve a slash path (`A/B/file.txt`) → metadata, or `null`.        |

Comment on lines +213 to +217
const actions = await c.connect({ requiresPrincipal: c.requiresPrincipal });
// Fail fast (never retro-mutate): if the connection demands a principal, every
// tool must already be gated. A non-compliant provider is a bug, and mutating
// here would leave the Flight path ungated while looking safe.
if (c.requiresPrincipal) {
…ess)

Main finding (transactional registration): connection tools self-register in the
global ACTION_REGISTRY (defineAction), which /mcp lists and invokeAction
dispatches. A connection that failed partway — a provider throwing after
registering, or the gate-check rejecting an ungated tool — left those actions
reachable even though the connection was "skipped".
- connectAll now snapshots the registry per connection and ROLLS BACK everything
  a failed connection added (all kinds: mcp/openapi/provider).
- The Flight server-reference wrapper resolves the action from ACTION_REGISTRY at
  call time and gates off the LIVE action, so a rolled-back action's reference is
  inert (deleting the map entry is now sufficient) and the gate can never be
  stale.

Google Drive correctness (from suppressed comments):
- rootFolderId config: path walkers + create default their base to it, so the
  documented service-account "share a folder / Shared Drive" setup actually works
  (a service account's My Drive root is empty/unwritable).
- findChild fails on ambiguous same-name siblings (Drive allows them) instead of
  arbitrarily taking files[0] — save_file can no longer overwrite an unrelated
  duplicate.
- save_file drops idempotentHint (non-atomic find-then-create can race).
- find_file description now states its real { found, file } contract.

Docs/changeset: connect({ requiresPrincipal }) signature, find_file contract,
rootFolderId for the SA setup, complete assembleAgent example.

Coverage: google-drive.ts 100%, connections.ts 100% funcs. Suites green
(core 490, server 502).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Suppressed comments (6)

packages/core/src/connections.ts:263

  • This global before/after diff spans an awaited connection attempt. If another connectAll/agent assembly registers tools while this attempt is pending, a later failure deletes those unrelated tools because their IDs were absent from this snapshot. Rollback needs connection-owned registration tracking or global transaction serialization rather than deleting every concurrently added registry entry.
      for (const key of ACTION_REGISTRY.keys()) if (!before.has(key)) ACTION_REGISTRY.delete(key);

packages/core/src/connections.ts:255

  • The rollback snapshots only IDs, so it cannot restore an existing action overwritten by the failed connection. A provider can register an action under a pre-existing ID and then throw; because that ID is in before, rollback leaves the failed provider's action globally reachable instead of restoring the original. Snapshot the entries and restore prior values as well as deleting newly added IDs.
    const before = new Set(ACTION_REGISTRY.keys());

.changeset/agent-google-drive.md:27

  • The release note advertises idempotentHint, but save_file deliberately does not set it and the new test asserts that it is undefined. Correct the changeset so the published changelog matches the shipped API.
  mints the caller's short-lived token. Read/list/find carry `readOnlyHint`,
  `save_file` carries `idempotentHint`, `delete_file` carries `destructiveHint`.

.changeset/connection-auth-helpers.md:15

  • “Reusable across every connection kind” is unsafe for MCP/OpenAPI remotes that authenticate discovery: those paths call auth(undefined), while this helper always rejects without a principal (as connection-auth.ts:15-19 documents). Qualify the release note so users do not wire this helper into discovery-authenticated connections and silently lose all tools.
  unreachable without a real credential). Reusable across every connection kind,
  not just Google Drive.

docs/google-drive-integration.md:54

  • This guide says save_file advertises idempotentHint, but the implementation and test intentionally omit it because the upsert is non-atomic (packages/core/src/google-drive.ts:439-442). Update the permission-UX documentation so clients are not told to expect an annotation that is absent.
Read/list/find carry `readOnlyHint`; `save_file` carries `idempotentHint`;
`delete_file` carries `destructiveHint` — so MCP clients can drive permission UX.

packages/core/src/google-drive.ts:219

  • The prefix also matches non-exportable Drive resources such as folders and shortcuts, but they are not in EXPORT_MIME; the fallback consequently sends an invalid files.export(..., mimeType=text/plain) request. Export only explicitly supported native types and reject unsupported Google-native resources before attempting alt=media.
    if (file.mimeType.startsWith("application/vnd.google-apps.")) {
      const exportMime = EXPORT_MIME[file.mimeType] ?? "text/plain";
      const search = new URLSearchParams({ mimeType: exportMime });
      const res = await request(`${api}/files/${encodeURIComponent(file.id)}/export?${search}`, { method: "GET" }, ctx);

Comment on lines +180 to +188
const flightRun = (...args: unknown[]) => {
const current = ACTION_REGISTRY.get(def.id);
if (!current) throw new Error(`Action "${def.id}" is not registered`);
if (current.requiresPrincipal) {
const ctx = args[1] as ActionContext | undefined;
if (!ctx?.user) throw new Error(`Action "${def.id}" requires an authenticated principal (ctx.user)`);
}
return (current.run as (...a: unknown[]) => unknown)(...args);
};
…entity)

- Flight reference identity binding (agent.ts): the server reference is bound to
  the EXACT action instance — it runs only while ACTION_REGISTRY still maps its id
  to that action. A rollback (delete) OR a same-id overwrite by a different action
  both make a stale reference inert, so it can never alias onto a replacement and
  run the wrong semantics.

- Concurrency- and overwrite-safe connection rollback (connections.ts):
  * connectAll is now serialized globally (per isolate). The registry is shared,
    so overlapping agent-assembly runs could otherwise interleave registrations
    and a failure's snapshot-diff would delete another run's tools. Serializing
    the boot-time/first-turn wiring makes each run's snapshot a faithful baseline
    (cheap; separate DO isolates have separate registries).
  * rollback now snapshots ENTRIES and both deletes ids the failed connection
    added AND restores any pre-existing entry it overwrote — a failed connection
    that clobbered an id no longer leaves its action reachable under that id.

- Drive read guards non-exportable Google-native resources (folders, shortcuts,
  …): reject with a clear error instead of firing a bogus files.export.

- Docs/changesets: save_file has no idempotentHint; linkedAccountAuth is for
  per-call-identity connections, not discovery-authenticated MCP/OpenAPI.

Tests: Flight inert on delete AND overwrite; rollback restores an overwritten
pre-existing action; connectAll serialization; read of a folder rejected.
google-drive.ts 100%. Suites green (core 493, server 502).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.

Suppressed comments (5)

packages/core/src/google-drive.ts:223

  • Every non-Google-native object is decoded with Response.text(), including PDFs, images, archives, and arbitrarily large files. read_file therefore returns corrupted replacement-character text for binary files and can exhaust an edge isolate while buffering a large Drive object. Restrict this path to supported textual MIME types and enforce a configurable byte limit, or expose an explicit bounded binary/base64 mode.
    const search = new URLSearchParams({ alt: "media", supportsAllDrives: "true" });
    const res = await request(`${api}/files/${encodeURIComponent(file.id)}?${search}`, { method: "GET" }, ctx);
    return res.text();

packages/core/src/google-drive.ts:447

  • The PR description still promises idempotentHint on save_file, while this implementation deliberately omits it because the upsert is non-atomic. The omission is safer; update the PR description so consumers are not told this operation is retry-safe.
    // No idempotentHint: the upsert is a non-atomic find-then-create/update, so
    // two concurrent identical saves can race into duplicate files (Drive does
    // not enforce unique sibling names). Don't advertise safe-to-retry.
    annotations: { title: "Save Drive file (upsert)" },

packages/june/src/connection-auth.ts:90

  • The real Better Auth getAccessToken endpoint throws ACCOUNT_NOT_FOUND when this user/provider has no account; it does not return null as the fake does. Consequently betterAuthAccessToken bypasses linkedAccountAuth's documented “no linked account” path and leaks a Better Auth API error instead. Normalize only the account-not-found error to null here (while rethrowing refresh/network failures), and cover that behavior with a throwing fake.
    const res = await auth.api.getAccessToken({ body: { providerId, userId } });

packages/core/src/connections.ts:263

  • This module-global queue is held while MCP/OpenAPI discovery performs network fetches with no timeout. If one endpoint never settles, every later connectAll call remains queued, preventing unrelated agents or durable instances in the same isolate from initializing. Add bounded cancellation and/or redesign the registry transaction so network discovery does not hold a global lock.
  const result = connectAllQueue.then(run, run); // chain regardless of the prior run's outcome

packages/core/src/google-drive.ts:109

  • authHeader is resolved for every Drive HTTP request, not once per tool call as the API contract states. Compound operations such as save_file perform several path lookups/creates, so the Better Auth adapter can repeat the account lookup/refresh many times and even switch tokens mid-operation. Resolve the credential once at action entry and reuse it for all requests in that invocation.

This issue also appears in the following locations of the same file:

  • line 221
  • line 444
    headers.set("authorization", await authHeader(ctx));

@linyiru
linyiru merged commit ec9d7af into main Aug 12, 2026
5 checks passed
linyiru added a commit that referenced this pull request Aug 12, 2026
Adopt all four reviewer findings:

1. Provider requiresPrincipal is now applied at REGISTRATION, not by post-hoc
   mutation. defineAction snapshots the gate into the Flight server-reference
   wrapper, so retro-setting action.requiresPrincipal left that path ungated.
   ProviderConnection.connect now receives { requiresPrincipal } and must thread
   it into its defineActions; connectAll fail-fast verifies every tool is gated
   (never mutates). googleDriveConnection threads it through to googleDriveTools.

2. linkedAccountAuth's compatibility claim narrowed: it is for connections whose
   auth resolves per call with identity (providers). MCP/OpenAPI call auth at
   discovery with ctx=undefined, which this fail-closed helper rejects — documented
   in code + docs.

3. resolvePathToFile no longer masks HTTP errors. It walks with findChild (null
   only for a genuinely absent segment); 401/403/429/5xx now propagate instead of
   surfacing as { found: false } / a misleading missing-file error. Dead
   create:false branch of resolveFolderPath removed.

4. betterAuthAccountTokenStore uses Better Auth's real endpoint shape
   { body: { providerId, userId } } (was passing fields top-level); BetterAuthLike,
   the call, and the fake test updated.

Coverage: google-drive.ts & connection-auth.ts 100%/100%. New tests: provider
gate-at-registration + fail-fast on a non-compliant provider, HTTP-error
propagation through path resolution, and the getAccessToken body shape. Full
suites green (core 483, server 502).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
linyiru added a commit that referenced this pull request Aug 12, 2026
…ess)

Main finding (transactional registration): connection tools self-register in the
global ACTION_REGISTRY (defineAction), which /mcp lists and invokeAction
dispatches. A connection that failed partway — a provider throwing after
registering, or the gate-check rejecting an ungated tool — left those actions
reachable even though the connection was "skipped".
- connectAll now snapshots the registry per connection and ROLLS BACK everything
  a failed connection added (all kinds: mcp/openapi/provider).
- The Flight server-reference wrapper resolves the action from ACTION_REGISTRY at
  call time and gates off the LIVE action, so a rolled-back action's reference is
  inert (deleting the map entry is now sufficient) and the gate can never be
  stale.

Google Drive correctness (from suppressed comments):
- rootFolderId config: path walkers + create default their base to it, so the
  documented service-account "share a folder / Shared Drive" setup actually works
  (a service account's My Drive root is empty/unwritable).
- findChild fails on ambiguous same-name siblings (Drive allows them) instead of
  arbitrarily taking files[0] — save_file can no longer overwrite an unrelated
  duplicate.
- save_file drops idempotentHint (non-atomic find-then-create can race).
- find_file description now states its real { found, file } contract.

Docs/changeset: connect({ requiresPrincipal }) signature, find_file contract,
rootFolderId for the SA setup, complete assembleAgent example.

Coverage: google-drive.ts 100%, connections.ts 100% funcs. Suites green
(core 490, server 502).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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