Skip to content

Snowflake: add connectionName config field and session-token auth in SnowflakeClient#6

Draft
costrouc wants to merge 9 commits into
mainfrom
snowflake-connection-support
Draft

Snowflake: add connectionName config field and session-token auth in SnowflakeClient#6
costrouc wants to merge 9 commits into
mainfrom
snowflake-connection-support

Conversation

@costrouc

@costrouc costrouc commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two small, backward-compatible changes that let Posit Assistant drive Snowflake
Cortex from a user's connections.toml (the file the Python connector / Snowflake
CLI use):

  1. ai-config — a non-secret connectionName field on the snowflake
    connection sub-section, so a consumer can record which named connection to
    use.
  2. ai-provider-bridgeSnowflakeClient support for the session-token
    auth scheme, needed for external-browser (SSO) sign-in.

All the discovery/parsing, token acquisition, and UI live in the consumer
(posit-dev/assistant, companion PR); this PR is only the enabling ai-lib
primitives.

Changes

ai-config

  • snowflakeConfigSchema gains an optional connectionName: z.string().
  • ResolvedConnection.snowflake gains connectionName?: string.
  • Regenerated providers.schema.json.

The field is optional and non-secret; it flows through the existing
connection-resolution/merge path with no other changes. It is not part of the
provider/protocol/client-kind vocabulary, so the cross-package shape guard is
unaffected
.

ai-provider-bridge

  • SnowflakeClient now recognizes an internal sentinel value (SESSION) on the
    X-Snowflake-Authorization-Token-Type header. When present, the client
    authenticates with Authorization: Snowflake Token="<token>" (the scheme
    Snowflake session tokens use) instead of a Bearer token, via a fetch wrapper
    applied on both the Anthropic (Claude) and OpenAI paths. The sentinel
    header is stripped before the request is sent.
  • The existing Bearer paths (keypair JWT, PAT, OAuth) are unchanged — the new
    branch only activates when a caller opts in with the sentinel.

Why a fetch wrapper: authorization is on the SDK-managed header filter, so a
session token can't be injected via customHeaders; wrapping fetch is the
reliable way to set the final Authorization header after the SDK builds the
request.

Why

Snowflake Cortex's REST API accepts different auth schemes depending on the
credential:

  • keypair JWT / PAT / OAuth → Authorization: Bearer <token> + a token-type
    header (already supported), and
  • external-browser SSO → a session token sent as
    Authorization: Snowflake Token="<token>" (this PR).

Recording the selected connectionName (non-secret) in providers.json lets the
consumer resolve the right connection on each request without persisting secrets.

Testing

  • npm run build (regenerates providers.schema.json)
  • npm run check-types — per-workspace typechecks + the cross-package shape
    guard
    all pass
  • npm testai-config (173) and ai-provider-bridge (210) suites pass

Compatibility / risk

  • Backward compatible. connectionName is optional; the SESSION path is
    inert unless a caller sends the sentinel. No behavior change for existing
    providers or Bearer-based Snowflake auth.
  • No new dependencies.

Companion PR

Consumed by posit-dev/assistant (Snowflake connections.toml discovery,
per-authenticator token acquisition, connection-selection UI). That PR pins the
submodule to this commit.

@costrouc
costrouc marked this pull request as draft July 9, 2026 20:58
@costrouc
costrouc force-pushed the snowflake-connection-support branch from 4de3123 to f947855 Compare July 24, 2026 19:29
@wch

wch commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

I’ve pushed three follow-up commits:

  • Added request-level regression coverage for both Snowflake routes:

    • Bearer auth through the Anthropic SDK.
    • Session-token auth through the Anthropic API.
    • Session-token auth through the OpenAI-compatible API.
    • Preservation of additive custom gateway headers and removal of SDK-generated x-api- key headers.
  • Replaced the internal magic-header sentinel with explicit runtime auth intent. ApiKeyCredentials.snowflakeSessionToken tells the provider factory that the credential is a session token; the factory maps that to the exported SnowflakeAuthScheme union ("bearer" | "session") passed directly to SnowflakeClient.

  • As a result, customHeaders now contains only headers intended for the request. No internal sentinel is added, inspected, stripped, or sent over the wire. The runtime-only credential flag is not persisted and does not change the credential-store format.

  • Added factory-level tests through ProviderRegistry to cover the production mapping from flagged credentials to session auth, as well as the default mapping to bearer auth. These complement the direct SnowflakeClient tests so a factory-wiring regression cannot leave the suite green.

The first follow-up also made the original sentinel lookup case-insensitive; the subsequent typed-scheme change superseded that fix by removing header-based signaling altogether. Existing bearer behavior remains unchanged.

costrouc and others added 9 commits July 24, 2026 22:14
Adds an optional, non-secret `connectionName` to the snowflake connection
sub-section so a consumer can record which connections.toml connection to use
for Snowflake Cortex. Regenerates providers.schema.json. Non-vocabulary change;
the shape guard is unaffected.
…rowser SSO

External-browser SSO yields a Snowflake session token that must be sent as
`Authorization: Snowflake Token="<token>"` rather than a Bearer token. When the
credential's `X-Snowflake-Authorization-Token-Type` header is the internal
`SESSION` sentinel, the client installs that header via a fetch wrapper on both
the Anthropic (Claude) and OpenAI paths and strips the sentinel before sending.
Keypair/PAT/OAuth Bearer paths are unchanged.
Match the X-Snowflake-Authorization-Token-Type sentinel case-insensitively,
since HTTP header names are case-insensitive: a caller supplying any mixed-case
spelling (e.g. lowercase x-snowflake-...) previously stayed on the Bearer path
and leaked the internal sentinel to Snowflake instead of using the
'Snowflake Token=' scheme.

Add interface-level regression tests covering both routes (Anthropic Messages
and OpenAI Chat): final Authorization value, removal of x-api-key and the
sentinel, preservation of additive gateway headers, and case-insensitive
sentinel recognition.
…agic header

Replace the in-band `X-Snowflake-Authorization-Token-Type: SESSION` sentinel
smuggled through customHeaders with an explicit typed credential field
(`ApiKeyCredentials.snowflakeSessionToken`) mapped to a `SnowflakeAuthScheme`
("bearer" | "session") constructor argument.

This makes the bridge the single source of truth for the auth scheme: callers
no longer duplicate a magic header spelling, customHeaders reverts to purely
additive gateway headers (no sentinel detection/stripping), and the
case-sensitivity surface addressed in the previous commit disappears entirely.
The typed field is synthesized at credential-resolution time and never
persisted, so the on-disk credential format (defined by an independent Zod
schema) is unaffected.

Tests updated to construct with the explicit scheme; the now-obsolete
case-insensitive-sentinel test is removed along with the mechanism it covered.
…sertion

Add registry-level tests that go through getClientForProvider -> the registered
client factory, exercising the credentials.snowflakeSessionToken -> auth-scheme
mapping. Constructing SnowflakeClient directly bypasses that seam, so a factory
regression could otherwise leave the client tests green while the product uses
Bearer auth. Verified the session test fails if the factory mapping is broken.

Remove the X-Snowflake-Authorization-Token-Type absence assertion and its
constant: the typed-scheme refactor deleted the sentinel mechanism, so the
header is never supplied and the assertion can no longer catch a regression.
The authorization and x-api-key assertions retain the useful coverage.
Replace the flat `ApiKeyCredentials.snowflakeSessionToken` boolean with a
grouped, runtime-only `snowflake?: { sessionConnectionIdentity: string }`. The
group's presence is the session-auth discriminant (avoiding invalid
flag-without-identity / identity-without-flag states), and it carries the
opaque, client-bound connection identity so a reauthentication hook can refresh
that exact connection on expiry.

Factory maps group presence to the session auth scheme. Runtime-only; the
on-disk StoredProviderCredentials format is unaffected.
Session tokens (external-browser SSO) expire; Cortex signals this with an
HTTP 200 body carrying error code 390112 and does not perform the operation.
Without refresh+retry an SSO conversation fails mid-stream even though the HTTP
layer reports success.

Add a session-only reauth seam:
- SnowflakeSessionReauth / SnowflakeSessionRefresh on the client: the session
  fetch wrapper detects a pre-stream 390112, reauthenticates *this client's*
  connection (client-bound identity, not the current selection), swaps the token
  and retries once. On reauth failure the original error is surfaced; success
  streams pass through untouched via a first-chunk peek that replays the body.
- SnowflakeProviderCallbacks.reauthenticateSession, threaded through
  ProviderRegistrationConfig -> registerAllProviders -> the Snowflake factory,
  which reads credentials.snowflake.sessionConnectionIdentity to bind refresh.
  The bridge never constructs the hook (Node builds it).

Policy per the documented pre-stream behavior; a mid-SSE 390112 after emitted
output is left to surface as a retryable failure (defined, empirical capture
deferred). Tests cover retry, passthrough, reauth-failure, and the client-bound
identity via the registered factory.
peekSessionExpiry inspected only the first physical response chunk, so a
390112 error body split on a chunk boundary ({"code":"390 / 112"})
slipped through as a bogus success stream and was handed to the SDK
instead of triggering reauth. Buffer the leading body bytes (bounded)
into a parallel scan view until expiry can be ruled in or out, replaying
the buffered raw chunks losslessly on the success path.

Also clarify that sessionConnectionIdentity is an opaque, resolver-minted
client-bound token (name + endpoint snapshot), not merely the connection
name — the consumer verification lives on the resolver side.

Tests: deliberately-split 390112 code, and a multi-chunk success stream
replayed byte-for-byte. Both fail on the single-chunk peek.
…tream

Replace peekSessionExpiry's 1 KiB bounded-scan of every session response with a
content-type branch: text/event-stream responses pass through untouched (the
wrapper never reads the body, so no first-token latency and no scanning model
output for a code that could appear in it), while non-stream JSON envelopes are
buffered whole and checked for 390112. Removes the passthrough ReadableStream and
scan-limit machinery.
@wch
wch force-pushed the snowflake-connection-support branch from 41ea3d3 to 5529273 Compare July 26, 2026 01:31
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