发布

  • feat(core): surface accountType + per-user ACL on SHARED connected accounts (#3392)

    frostbyte_neo 发布于 2026-05-11 20:03:22 +00:00

    Summary

    SDK wrappers for the Apollo-side Shared Accounts feature shipped across
    four Hermes PRs:

    • hermes#9860
      account_type enum (PRIVATE / SHARED) + pin-only invariant
    • hermes#9882 — echo
      account_type in create-flow responses
    • hermes#9887 — echo
      account_type in internal link-session GET (no public docs surface)
    • hermes#9902
      per-user ACL for SHARED connections, nested under
      acl_config_for_shared

    Ready for review — Apollo, the OpenAPI spec, and
    @composio/client@0.1.0-alpha.71 (published from
    composio-base-ts#77)
    all carry the merged contract.

    Implemented

    Types (types/connectedAccounts.types.ts)

    • ConnectedAccountTypes constants + ConnectedAccountTypeSchema Zod
      enum + ConnectedAccountType TS type.
    • ConnectedAccountAclConfigSchema (input — all 3 fields optional) and
      ConnectedAccountAclConfigResponseSchema (output — fields populated
      when block visible). Per-list cap 1000 entries, per-userId length
      cap 1..256 chars — matches Apollo's aclConfig.ts.
    • Extended CreateConnectedAccountLinkOptionsSchema with accountType
    • aclConfigForShared (nested object).
    • Extended ConnectedAccountRetrieveResponseSchema with accountType +
      optional aclConfigForShared (undefined when caller can't see ACL —
      distinguishes hidden-ACL from default-state).
    • New UpdateConnectedAccountAclParamsSchema, refined to require ≥1
      field (rejects no-op {}). Inline comment notes the ZodEffects .shape
      quirk for future maintainers.

    Model (models/ConnectedAccounts.ts)

    • link(userId, authConfigId, options) — forwards accountType and
      serializes aclConfigForShared to the nested acl_config_for_shared
      wire block via a new internal serializeAclConfigForWire helper.
      Default behaviour (no options) creates a PRIVATE connection exactly as
      today.
    • New updateAcl(nanoid, params) — wraps
      client.connectedAccounts.patch (PATCH /connected_accounts/{id}),
      serialising flat camelCase params into the nested wire block. PATCH
      semantics: omit a field to leave unchanged; pass [] to clear an
      allow/deny list. JSDoc flags the empty-array footgun
      (notAllowedUserIds: [] silently re-grants previously-blocked users).
      Returns ConnectedAccountPatchResponse to match sibling-method
      ergonomics.
    • Clarifying JSDoc on existing update() pointing readers at
      updateAcl() for ACL and noting the historical "alias and/or
      credentials" doc is inaccurate (separate cleanup PR).

    Model (models/ToolRouterSession.ts)

    • authorize(toolkit, options) — options gain accountType +
      aclConfigForShared. The /tool_router/session/{session_id}/link
      endpoint accepts the same fields, so a SHARED connection with an ACL can
      be created in one call from inside a tool-router session.

    Transformer (utils/transformers/connectedAccounts.ts)

    • Maps account_typeaccountType.
    • Maps acl_config_for_shared (nested) → aclConfigForShared (nested
      camelCase). Forwards undefined when the block is absent so callers can
      distinguish "I can't see the ACL" (cookie caller, non-creator) from
      "ACL is the default deny-by-default state".

    Errors (errors/ConnectedAccountsErrors.ts)

    • ComposioAclOnlyForSharedError (400) — wired at three call sites:
      link(), updateAcl(), ToolRouterSession.authorize(). Substring
      match on 'acl_config_for_shared is only valid on SHARED' (verified
      against Apollo's createConnectedAccount.ts in hermes#9902).
    • ComposioSharedAccessDeniedError (403) — exported, not yet wrapped
      by SDK code
      . Will be wrapped when Tools.execute()/direct-execute
      error mapping lands in a follow-up. For now, callers seeing this from a
      raw BadRequestError/ForbiddenError can match on message substring.
    • ComposioSharedConnectionNotAccessibleError (400) — same status. Will
      be wrapped when ToolRouterSession.create()/update()
      session-validator error mapping lands.

    Tests (test/connectedAccounts/connectedAccounts.test.ts)

    • 12 new behaviour tests covering link() ACL forwarding (5),
      updateAcl() body construction + empty-object refine + AclOnlyForShared
      mapping (6), and link() error-mapping fallback (1). 911 tests pass (up
      from 899).

    Changeset

    • .changeset/connected-accounts-acl.md@composio/core: minor
      (additive surface, no breaking changes), describes the nested shape and
      resolution rule.

    Not implemented (deliberate scope cuts)

    Skipped Why Where it lands
    Python SDK mirror composio-client@1.38.0 (from
    composio-base-py#66)
    now exposes the same fields. Better as its own PR scoped to
    python/composio/ so Python's release cadence stays independent.
    Separate PR, same release train (called out in changeset).
    Docs cookbook entry ("share a connection with specific users")
    Needs @composio/core to be published before the TS code samples can
    twoslash-typecheck against the new methods. Separate PR under
    docs/content/cookbooks/.
    docs/public/openapi*.json spec refresh Auto-PR
    #3399 already opened
    against next with the post-9902 spec. Mergeable independently from
    this PR.
    update() → real PATCH endpoint Pre-existing — JSDoc says
    "alias and/or credentials" but implementation calls updateStatus. The
    new JSDoc explicitly disclaims it; the behaviour-change rewrite is
    unrelated to ACL. Separate cleanup PR.
    **ComposioSharedAccessDeniedError +
    ComposioSharedConnectionNotAccessibleError wiring** Need to map at
    Tools.execute() and ToolRouterSession.create()/update()/patch().
    Out of scope for a connected-accounts PR — the failure modes are in
    different SDK touchpoints. Exported now so the public-API contract is
    stable, JSDocs explicit about when they'll start being thrown.
    Follow-up PR scoped to execute/session error mapping.
    Internal link-session GET (hermes#9887) Endpoint is
    /api/v3/internal/... and filtered out of the public spec by
    docs/scripts/fetch-openapi.mjs. Dashboard UI consumes it, not the SDK.
    N/A — internal only.
    Cast tightening / shared wire-ACL fragment (review P3-13, P3-14)
    Casts and local body types are gone (commit 483af3607). Shared
    fragment was already extracted as AclConfigWireShape in commit
    752efd886 — now removed since it lives in the generated types. Done.
    Duplicate-account guard extraction (review P3-16) Reviewer
    explicitly says "parity-by-copy is fine for this PR" — link() and
    initiate() share a 14-line guard. Extracting
    assertNoActiveConnections() is mechanical and unrelated. Follow-up.

    Commits

    84a3a074
    — initial draft against the flat-field shape described in 9902's PR
    body.
    2.
    752efd88
    — reshape to the nested aclConfigForShared shape that actually merged
    in 9902. Verified against staging-backend.composio.dev's spec.
    3.
    483af3607
    — bump @composio/client to 0.1.0-alpha.71, drop the three casts +
    local body types, apply the 9 in-scope multi-agent review fixes, wire
    ComposioAclOnlyForSharedError at three call sites, add 12 behaviour
    tests.

    Test plan

    • pnpm --filter @composio/core typecheck — clean
    • pnpm --filter @composio/core test — 911/911 pass (12 new, 899
      pre-existing)
    • pnpm --filter @composio/core buildpublint + attw clean
    • Wire contract spot-checked against prod spec for all five
      endpoints (POST link, POST connected_accounts, PATCH, GET single, GET
      list, tool_router/session link)
    • All forward-compat casts removed; SDK now uses LinkCreateParams,
      ConnectedAccountPatchParams, SessionLinkParams from the generated
      client directly
    • Manual smoke (reviewer or merger): create SHARED →
      updateAcl({ allowAllUsers: true, notAllowedUserIds: ['user_bob'] })
      verify get() returns aclConfigForShared for creator, returns
      undefined for non-creator.

    🤖 Generated with Claude Code

    下载附件