ci: publish container image to GHCR + stdio auth overhaul#12
Open
Criptic wants to merge 8 commits into
Open
Conversation
Add a GitHub Actions workflow that builds and pushes a multi-arch (linux/amd64, linux/arm64) container image to ghcr.io on push to main (edge + sha tags), on push of v* tags (latest + semver tags), and on workflow_dispatch. Images are pushed with build provenance and SBOM attestations. Adds the four OCI labels required by the SAS OSPO publishing guidelines (maintainer, image.source, image.description, image.licenses) to the Dockerfile runner stage; image.source ties the package to the repository so visibility and permissions inherit automatically. Adds a PR-time Dockerfile build check so Dockerfile regressions surface before merge. Updates README and examples/docker/setup.md with the pull snippet and the tag-to-image-version mapping. Closes sassoftware#1 Signed-off-by: David Weik <david.weik@sas.com>
The stdio server's password-grant authentication
(`auth=(CLIENT_ID, "")` against /SASLogon/oauth/token) fails with
401 invalid_client for OAuth clients registered as confidential.
RFC 6749 §4.3 and OAuth 2.1 also deprecate password grant.
Replace it with OAuth 2.0 Device Authorization Grant (RFC 8628):
1. Primary path: read the access token cached by `sas-viya auth
loginCode` from ~/.sas/credentials.json (override the parent
directory with SAS_CLI_CONFIG). This is the recommended path
because SAS Logon Manager typically CSRF-protects the device
endpoint, so the CLI's browser-driven flow is the path of
least resistance.
2. Fallback: native RFC 8628 flow against SAS Logon. Used only
when no cached credentials exist. Works on Viya instances
whose admins have not enabled CSRF protection on the device
endpoint and whose OAuth client is registered with the
urn:ietf:params:oauth:grant-type:device_code grant type.
Remove VIYA_USERNAME / VIYA_PASSWORD from the stdio config surface
(they remain available for the integration test suite, which uses
the legacy sas.cli password grant). Update README, configuration.md
(including the Gemini CLI section), and .env.sample to reflect the
new auth model.
Reuses the design contributed by a SAS colleague.
Signed-off-by: David Weik <david.weik@sas.com>
Add `sas-mcp-login`, an OAuth 2.0 Authorization Code + PKCE helper
that runs against the built-in `vscode` Viya OAuth client shipped
with SAS Viya 2022.11+. This catches the operator who has neither
the sas-viya CLI nor admin rights to register a custom OAuth client
on Viya — neither was previously supported for stdio mode.
The flow:
1. `uv run sas-mcp-login` prints a SAS Logon authorization URL,
opens the browser, and (because `vscode` has no registered
redirect URI on most deployments) instructs the user to copy
the code SAS Logon displays on the results page.
2. The interactive variant prompts on stdin and exchanges the
code in one go. When stdin is not a TTY (e.g., wrappers that
run shell commands non-interactively), PKCE state is saved to
~/.sas-mcp-server/login-state.json and the second invocation
`uv run sas-mcp-login --code <CODE>` completes the exchange.
3. The resulting access token is written to
~/.sas-mcp-server/credentials.json in the same shape as
~/.sas/credentials.json (Default.access-token /
Default.refresh-token / Default.expiry).
Extend stdio_server.py to check both cache locations in order
(sas-viya CLI cache first, sas-mcp-login cache second, native
device-code fallback last). Update README and configuration.md
to document both bootstrap paths.
Wire `sas-mcp-login` as a console script entry point in
pyproject.toml.
Signed-off-by: David Weik <david.weik@sas.com>
Add an additive auth path for HTTP mode: when ALLOW_RAW_BEARER=true, the server accepts a raw Viya access token in the Authorization header alongside the default OAuth2 PKCE flow. Useful for automation/CI clients that already hold a Viya token (for example from `sas-viya auth loginCode` on the same host) and want to call MCP tools without going through the PKCE dance per session. Implementation is additive: PermissiveOAuthProxy subclasses fastmcp's OAuthProxy and overrides load_access_token to fall through to the configured token_verifier (the Viya JWKS verifier already wired in for PKCE) only after the standard MCP JWT swap has returned None. Existing PKCE clients are unaffected — their tokens still take the same code path as before; the fallthrough only fires for tokens the proxy itself didn't issue. Verified end-to-end against real Viya: same raw token returns 200 with ALLOW_RAW_BEARER=true and 401 with ALLOW_RAW_BEARER=false. Document the new env var in .env.sample and configuration.md, and add a README snippet showing the curl invocation for programmatic clients. Signed-off-by: David Weik <david.weik@sas.com>
Add a single "Authentication modes — at a glance" table at the top of examples/configuration.md so a new operator can see all five auth paths (HTTP/PKCE, HTTP/raw-bearer, stdio/sas-viya-CLI-cache, stdio/sas-mcp-login-cache, stdio/native-device-code) side by side with when-to-use guidance, before drilling into individual sections. Populate the [Unreleased] section of CHANGELOG.md (Keep-a-Changelog format) with the GHCR publishing, stdio auth-model overhaul, sas-mcp-login helper, ALLOW_RAW_BEARER passthrough, and related doc moves; add a Removed section noting password-grant stdio auth is gone. Signed-off-by: David Weik <david.weik@sas.com>
Append six troubleshooting entries to TROUBLESHOOTING.md covering the new auth surface: missing stdio cache files, the device-code CSRF fallback failure mode, the sas-mcp-login non-TTY abort, the "invalid redirect_uri" failure, ALLOW_RAW_BEARER 401 diagnosis, and the volume-mount gotcha when running stdio mode inside a container. Retitle the CHANGELOG.md [Unreleased] section to [1.0.0] dated 2026-05-12, bump pyproject.toml from 0.1.0 to 1.0.0, and update the tag-mapping examples in README.md and examples/docker/setup.md to reflect the new release line. After this merges, tag `v1.0.0` to trigger the publish-ghcr workflow and ship the first release image. Signed-off-by: David Weik <david.weik@sas.com>
uv.lock was missing the version bump that the previous commit applied to pyproject.toml. Signed-off-by: David Weik <david.weik@sas.com>
Signed-off-by: Bryan Behrenshausen <bryan.behrenshausen@sas.com>
semioticrobotic
approved these changes
May 12, 2026
Member
semioticrobotic
left a comment
There was a problem hiding this comment.
@Criptic: Please see the few minor, compliance-related additions from me. That's all I needed!
Member
Author
|
@semioticrobotic looks good, thank you! @harrykeen18 ready for your review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #1 by adding a multi-arch (
linux/amd64,linux/arm64) GHCR publishing workflow that meets the SAS OSPO container-publishing guidelines (maintainer + four OCI labels on the runtime stage, signed build provenance, SBOM attestations). The image will land atghcr.io/sassoftware/sas-mcp-serveron the first tagged release.While verifying the resulting image worked end-to-end against a real Viya instance, the existing stdio auth surface turned out to be broken for confidential OAuth clients (the common case for custom Viya client registrations) — the
auth=(CLIENT_ID, "")call returnsinvalid_client. The remaining commits address that:sas-viya auth loginCodecache → built-insas-mcp-loginhelper cache → native RFC 8628 device code. Password grant was deprecated by OAuth 2.1 and didn't work for confidential clients.sas-mcp-loginas a zero-prereq bootstrap helper using the built-invscodeViya OAuth client (Viya 2022.11+). Operators who can't install thesas-viyaCLI and don't have admin rights to register a custom OAuth client can now use stdio mode anyway.ALLOW_RAW_BEARERas an additive HTTP-mode flag — when enabled, the server accepts raw upstream Viya JWTs inAuthorization: Beareralongside the existing OAuth2 PKCE flow. Useful for automation/CI clients that already hold a Viya token. PKCE clients are unaffected.Each commit is small, scoped, and reviewable on its own. Bumps version to 1.0.0 in
pyproject.tomlandCHANGELOG.md— that's a deliberate marker for the auth-model overhaul and the first published container image. Happy to bump down to0.2.0if maintainers prefer.Commits
730a7c2702109b35dc5e5sas-mcp-loginhelper for stdio modef05f8e1ALLOW_RAW_BEARER321f9cd7ef803e07ac992What this PR does NOT change
ALLOW_RAW_BEARERis unset (default).VIYA_USERNAME/VIYA_PASSWORDremain in.env.samplebecause the integration test suite uses them. They're no longer read by the running stdio server.v1.0.0tag to publish:latestand the semver tags. Until then, merging tomainonly publishes:edge+:sha-<short>.Test plan — all validated against a live Viya instance
maintainer,image.source,image.description,image.licenses) on the runner stage.list_cas_servers/list_caslibs/execute_sas_codesucceeded.sas-viyaCLI cache — token loaded from mounted~/.sas/credentials.json, real Viya calls succeeded.sas-mcp-logincache — helper completed PKCE flow with the built-invscodeclient, wrote~/.sas-mcp-server/credentials.json, container stdio mode then loaded and used the token.ALLOW_RAW_BEARER=trueaccepts arbitrary Viya-signed JWTs (verified with both ansas-mcp-login-issued token and an externally-issued token for thesas.launcherclient). WithALLOW_RAW_BEARER=false, the same call returns 401 — confirming the flag is the gate.docker-build.ymlis path-filtered to Dockerfile-relevant changes; will run on this PR.Pre-flight for maintainers
For first-time publish on
sassoftware/sas-mcp-server:Settings → Actions → General → Workflow permissionsset to Read and write permissions. The workflow declarespermissions: { contents: read, packages: write, id-token: write, attestations: write }but org-level settings can still restrict it.main, flip visibility to public athttps://github.com/orgs/sassoftware/packages/container/sas-mcp-server/settings. Theorg.opencontainers.image.sourcelabel is in place so repo permissions inherit cleanly.v1.0.0— once the package is public, push av1.0.0git tag to publish:latest,:1.0.0,:1.0,:1with provenance + SBOM. Or useworkflow_dispatchfrom the Actions tab for an ad-hoc publish.Follow-ups (intentionally out of scope)
🤖 Generated with Claude Code