Skip to content

ci: publish container image to GHCR + stdio auth overhaul#12

Open
Criptic wants to merge 8 commits into
sassoftware:mainfrom
Criptic:feat/ghcr-publish
Open

ci: publish container image to GHCR + stdio auth overhaul#12
Criptic wants to merge 8 commits into
sassoftware:mainfrom
Criptic:feat/ghcr-publish

Conversation

@Criptic
Copy link
Copy Markdown
Member

@Criptic Criptic commented May 12, 2026

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 at ghcr.io/sassoftware/sas-mcp-server on 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 returns invalid_client. The remaining commits address that:

  • Replace password-grant stdio auth with a chain of OAuth 2.0 paths, tried in order: sas-viya auth loginCode cache → built-in sas-mcp-login helper cache → native RFC 8628 device code. Password grant was deprecated by OAuth 2.1 and didn't work for confidential clients.
  • Add sas-mcp-login as a zero-prereq bootstrap helper using the built-in vscode Viya OAuth client (Viya 2022.11+). Operators who can't install the sas-viya CLI and don't have admin rights to register a custom OAuth client can now use stdio mode anyway.
  • Add ALLOW_RAW_BEARER as an additive HTTP-mode flag — when enabled, the server accepts raw upstream Viya JWTs in Authorization: Bearer alongside 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.toml and CHANGELOG.md — that's a deliberate marker for the auth-model overhaul and the first published container image. Happy to bump down to 0.2.0 if maintainers prefer.

Commits

Hash Scope
730a7c2 ci: publish container image to GHCR — closes #1
702109b fix(stdio): replace password grant with OAuth 2.0 device-code flow
35dc5e5 feat(auth): zero-prereq sas-mcp-login helper for stdio mode
f05f8e1 feat(auth): accept raw upstream bearer tokens via ALLOW_RAW_BEARER
321f9cd docs: unified auth-modes overview + CHANGELOG entries
7ef803e docs: troubleshooting entries for auth modes; cut 1.0.0 release
07ac992 chore(deps): sync uv.lock to package version 1.0.0

What this PR does NOT change

  • Existing HTTP/OAuth2 PKCE flow is unchanged when ALLOW_RAW_BEARER is unset (default).
  • VIYA_USERNAME / VIYA_PASSWORD remain in .env.sample because the integration test suite uses them. They're no longer read by the running stdio server.
  • No tag is cut — the workflow waits for a maintainer-pushed v1.0.0 tag to publish :latest and the semver tags. Until then, merging to main only publishes :edge + :sha-<short>.

Test plan — all validated against a live Viya instance

  • GHCR workflow — Dockerfile + workflow YAML reviewed; the action versions match GitHub's documented patterns. The image with the new OCI labels was built locally with podman and confirmed to carry all four labels (maintainer, image.source, image.description, image.licenses) on the runner stage.
  • HTTP mode + OAuth2 PKCE in the locally-built container — full browser flow, 26 tools registered, list_cas_servers / list_caslibs / execute_sas_code succeeded.
  • Stdio mode + sas-viya CLI cache — token loaded from mounted ~/.sas/credentials.json, real Viya calls succeeded.
  • Stdio mode + sas-mcp-login cache — helper completed PKCE flow with the built-in vscode client, wrote ~/.sas-mcp-server/credentials.json, container stdio mode then loaded and used the token.
  • HTTP mode + raw bearer passthroughALLOW_RAW_BEARER=true accepts arbitrary Viya-signed JWTs (verified with both an sas-mcp-login-issued token and an externally-issued token for the sas.launcher client). With ALLOW_RAW_BEARER=false, the same call returns 401 — confirming the flag is the gate.
  • PR-time Dockerfile build checkdocker-build.yml is path-filtered to Dockerfile-relevant changes; will run on this PR.

Pre-flight for maintainers

For first-time publish on sassoftware/sas-mcp-server:

  1. Workflow permissionsSettings → Actions → General → Workflow permissions set to Read and write permissions. The workflow declares permissions: { contents: read, packages: write, id-token: write, attestations: write } but org-level settings can still restrict it.
  2. Package visibility — GHCR makes new packages private on first push. After the first merge to main, flip visibility to public at https://github.com/orgs/sassoftware/packages/container/sas-mcp-server/settings. The org.opencontainers.image.source label is in place so repo permissions inherit cleanly.
  3. Cutting v1.0.0 — once the package is public, push a v1.0.0 git tag to publish :latest, :1.0.0, :1.0, :1 with provenance + SBOM. Or use workflow_dispatch from the Actions tab for an ad-hoc publish.

Follow-ups (intentionally out of scope)

  • Docker Hub mirror (issue Request: Provide a Public Container Image #1 mentions it as an alternative; OSPO doc only covers GHCR).
  • cosign signing on top of the existing build-provenance attestation.
  • Make uvicorn auto-reload opt-in via env var. Attempted earlier in this branch and reverted — the change exposed a separate environment-specific issue (VS Code integrated terminal signaling the foreground process) that's better handled in its own PR after the root cause is understood.

🤖 Generated with Claude Code

gerdaw added 7 commits May 12, 2026 13:31
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>
@Criptic Criptic requested a review from harrykeen18 May 12, 2026 14:18
@Criptic Criptic added the enhancement New feature or request label May 12, 2026
@semioticrobotic semioticrobotic self-requested a review May 12, 2026 14:33
Signed-off-by: Bryan Behrenshausen <bryan.behrenshausen@sas.com>
Copy link
Copy Markdown
Member

@semioticrobotic semioticrobotic left a comment

Choose a reason for hiding this comment

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

@Criptic: Please see the few minor, compliance-related additions from me. That's all I needed!

@Criptic
Copy link
Copy Markdown
Member Author

Criptic commented May 12, 2026

@semioticrobotic looks good, thank you!

@harrykeen18 ready for your review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Request: Provide a Public Container Image

3 participants