Skip to content

feat: add the sasviya runtime - #462

Merged
allanbowe merged 5 commits into
mainfrom
feat/viya-runtime
Oct 3, 2026
Merged

allanbowe merged 5 commits into
mainfrom
feat/viya-runtime

Conversation

@sasjs-dev

@sasjs-dev sasjs-dev Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Closes #461.

What

RUN_TIMES=sasviya executes stored programs on a remote SAS Viya compute server instead of a local SAS binary. The server mints an access token from VIYA_USER / VIYA_PASSWORD against the built-in sas.cli public client, submits the program to a compute session on VIYA_CONTEXT, and relays the session log and the program's _webout content back into the server's session folder - so ExecutionController and the STP response contract are unchanged.

The transport is @sasjs/adapter/node, which already implements the hot-session pool, code submission, log relay and _webout retrieval.

Why

On an estate where Viya accepts connections only from the server's IP, the server is the only host with a network path to Viya. This makes it the single egress point, so browsers and CI pipelines need only a server token - no direct Viya network access and no Viya credentials in a pipeline. Everything the server already provides (auth, groups, the Drive, session-state polling, streaming, the webout contract) then applies unchanged to Viya execution.

Changes

  • RunTimeType.SASVIYA and startup validation of VIYA_URL, VIYA_USER, VIYA_PASSWORD and optional VIYA_CONTEXT (default Compute Reusable) in verifyEnvVariables.ts.
  • viyaClient.ts - token mint, compute execution, log and webout relay, token rotation.
  • createViyaSASProgram.ts - a preamble that does not inject the local _webout fileref, SASAUTOS or header paths.
  • A SASVIYA branch in processProgram.ts.
  • @sasjs/adapter added to api/package.json; lockfile regenerated with npm 10.
  • api/public/swagger.yaml regenerated - RunTimeType is part of the generated contract (ExecuteCodePayload.runTime), and swagger.spec.ts caught the drift.
  • Session.ts - the session lifecycle (deleteSession / scheduleSessionDestroy) moves from SASSessionController to the base SessionController, and the base createSession schedules the destroy. Only the local sas runtime ever destroyed a session, so a triggered program on js, py, r or sasviya left its session folder behind permanently and expiresAfterMins had no effect on it. Found while testing the trigger path: 15 session folders had accumulated and a session triggered with expiresAfterMins=1 was still on disk minutes after completing.
  • docs/viya-runtime.md - the design note (also at https://paste.4gl.io/?e78a7d66e2e85825#DypKnyHz6poHsMViN1DwfqLJyFcjNHRzgNe1LhNsCDQi).

Verification

Unit: api/src/controllers/internal/spec/viyaRuntime.spec.ts, sessionLifecycle.spec.ts, plus the Viya cases in api/src/utils/specs/verifyEnvVariables.spec.ts. Full API suite: 39 suites / 497 tests green. tsc --noEmit and prettier 3.9.9 clean.

CLI end to end: sasjs compile -t viya -> sasjs build -> sasjs build -t server -> sasjs deploy -t server against a local server running RUN_TIMES=js,sasviya.

Live Viya, through the server:

GET /SASjsApi/stp/execute?_program=/Public/viya-e2e/services/common/hello.sasviya
-> 200 {"hello":[{"SOURCE":"viya","STATUS":"e2e-ok"}]}

GET /SASjsApi/stp/execute?_program=/Public/viya-e2e/services/common/minimal.sasviya
-> 200 {"minimal":[{"X":1}]}

The _debug response carries the Viya session log, proving execution happened on the compute server and was relayed back.

Async trigger, on a 12-second program on the js runtime:

POST /SASjsApi/code/trigger -> {"sessionId":"20261003085606-83511-1791017766804"}   returned in 64 ms
GET  /SASjsApi/session/<id>/state  -> running (1-12s) -> completed (13s)
POST /SASjsApi/code/execute (same program) -> waited 3151 ms
GET  /SASjsApi/session/does-not-exist/state -> 404

CI status

All jobs pass except security, which fails on the braces advisory GHSA-vfj7-8cjw-p6xm (stack-exhaustion DoS). That advisory now covers every published braces version - the registry reports first_patched_version: None - so no bump fixes it.

  • api: was failing on braces via nodemon -> chokidar. Commit 098e333 removes that path (dev-only; the shipped image runs the built server) and replaces it with node --watch. npm audit --audit-level=low in api now reports 0 vulnerabilities.
  • web: still fails with 20 vulnerabilities (2 moderate, 18 high), all the same braces advisory, reached through micromatch (eslint-webpack-plugin, ts-loader, http-proxy-middleware, fast-glob) and chokidar (sass). web/ is byte-identical to origin/main in this PR (git diff origin/main HEAD -- web/ is empty), and npm audit --audit-level=high in web reports the same 20 locally. This is repo-wide and pre-existing, not introduced here.

braces is build-time tooling only - it is not in the deployed image - but there is no patched release to move to, so unblocking the security job needs a decision: wait for a braces release, add a documented dated exception for that one advisory, or migrate the web toolchain off the CRA-era stack.

Known limits

  • File uploads are not relayed to Viya in this release.
  • %mv_webout must exist on the estate (it ships with a SASjs deployment). The server does not inject the macros.
  • The runtime is selected by the .sasviya extension; .sas still means the local runtime.
  • Per-user Viya identities are out of scope - every run uses one service account, so Drive write access is the Viya privilege boundary.
  • The adapter is reached through sasViyaApiClient because the public SASjs.executeScript does not return the _webout fileref; a public option there is a candidate follow-up in sasjs/adapter.

sasjs added 4 commits October 2, 2026 17:41
RUN_TIMES=sasviya executes stored programs on a remote SAS Viya compute
server instead of a local SAS binary. The server mints an access token
from VIYA_USER/VIYA_PASSWORD against the sas.cli public client, submits
the program to a compute session on VIYA_CONTEXT, and relays the session
log and the program's _webout content back into the session folder, so
the STP response contract is unchanged.

The transport is @sasjs/adapter/node, which already implements the hot
session pool, code submission, log relay and _webout retrieval.

Every run uses one service account in this release; per-user Viya
identities need a token per user and a session pool that cannot be
shared across identities.

Programs respond with %mv_webout, which must be present on the Viya
estate (it ships with a SASjs deployment). The runtime is selected by
the .sasviya extension.
RunTimeType is part of the generated API contract (ExecuteCodePayload.runTime),
so adding the sasviya member changes public/swagger.yaml. swagger.spec.ts
guards the committed file against the controllers and caught the drift.
One sentence per line, so a renderer wraps the text rather than the file
carrying hard line breaks.
The session lifecycle lived on SASSessionController, so only the local sas
runtime ever destroyed a session. A triggered program on js, py, r or the new
sasviya runtime left its session folder behind permanently and the documented
expiresAfterMins had no effect on it.

Move deleteSession and scheduleSessionDestroy onto the base SessionController
and schedule the destroy from the base createSession, so every runtime gets the
same lifecycle the local sas runtime already had.

Found by triggering a long-running job: 15 session folders had accumulated, and
a session triggered with expiresAfterMins=1 was still on disk minutes after it
completed.
@sasjs-dev

sasjs-dev Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up commit 5aff50e adds the session lifecycle fix found while testing the trigger path.

Triggering a long-running job (POST /SASjsApi/code/trigger, 12-second js program) returned in 64 ms with a sessionId, and the state polled running -> completed at 13s - the async contract works. But the session folder for a job triggered with expiresAfterMins=1 was still on disk minutes after it completed, and 15 folders had accumulated over a short test run.

Cause: deleteSession and scheduleSessionDestroy lived on SASSessionController, so only the local sas runtime ever destroyed a session. Every other runtime - including the new sasviya one - leaked its folder permanently and ignored expiresAfterMins. Both methods now live on the base SessionController and the base createSession schedules the destroy, so all runtimes share the lifecycle the sas runtime already had.

Covered by api/src/controllers/internal/spec/sessionLifecycle.spec.ts: an idle session is destroyed at its death time (folder gone, id stops resolving), a session still running at its death time survives and has its death time pushed out, and the base controller schedules the destroy.

Full API suite on 5aff50e: 39 suites / 497 tests green.

npm audit now fails on braces (GHSA-vfj7-8cjw-p6xm), which has no patched
release - the advisory covers every published version. The only reachable path
is dev-only: the watcher -> chokidar -> braces. Nothing in the shipped image
uses the watcher, so the path goes rather than the audit being narrowed.

node --watch replaces it for the development start script, and ts-node/register
is already a devDependency. The watcher's config block is no longer needed:
watch mode only restarts on files in the module graph, and sasjs_root is not
one of them.

Verified: the dev server boots on the new command, a change to a loaded source
file restarts it, and npm audit --audit-level=low reports 0 vulnerabilities
(18 packages removed).
@sasjs-dev

sasjs-dev Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

Two follow-up commits, and the remaining CI picture.

5aff50e fix: schedule session destruction for every runtime - the session lifecycle (deleteSession / scheduleSessionDestroy) lived on SASSessionController, so only the local sas runtime ever destroyed a session. A triggered program on js, py, r or sasviya leaked its folder permanently and expiresAfterMins did nothing. Found by triggering a long-running job: 15 session folders had accumulated and a session triggered with expiresAfterMins=1 was still on disk minutes after completing. Both methods now live on the base controller. Covered by sessionLifecycle.spec.ts.

098e333 chore(deps): drop the file watcher in favour of node --watch - npm audit --audit-level=low in api was failing on braces via the watcher -> chokidar -> braces. That advisory (GHSA-vfj7-8cjw-p6xm) has no patched release, so the path had to go rather than the audit being narrowed. The watcher is dev-only (the image runs build/src/server.js) and ts-node was already a devDependency. Verified: the dev server boots on node --watch -r ts-node/register ./src/server.ts, a change to a loaded file restarts it, and the api audit is now 0 vulnerabilities.

security is still red, and cannot be made green here. The remaining failures are the same braces advisory reached through the web tree - micromatch (eslint-webpack-plugin, ts-loader, http-proxy-middleware, fast-glob) and chokidar (sass). web/ is byte-identical to origin/main in this PR, and npm audit --audit-level=high in web reports the same 20 (2 moderate, 18 high) locally, so this is repo-wide and pre-existing.

Every other job is green on 098e333: build-api, build-web, lint, Container validate, Container smoke test. Local full suite: 39 suites / 497 tests.

Async trigger evidence, on a 12-second js program:

POST /SASjsApi/code/trigger -> {"sessionId":"20261003085606-83511-1791017766804"}  returned in 64 ms
GET  /SASjsApi/session/<id>/state -> running (1-12s) -> completed (13s)
POST /SASjsApi/code/execute (same program) -> waited 3151 ms
GET  /SASjsApi/session/does-not-exist/state -> 404

@sasjs-dev sasjs-dev Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewed the full base...head diff (13 files: viyaClient, createViyaSASProgram, processProgram sasviya branch, session-destroy move to the base controller, env validation, swagger enum, docs, api lockfile) plus the check status at this head.

  • .github/workflows/build.yml:66 - the security job is red at this head. The web audit (npm audit --audit-level=high) reports 18 high advisories (braces GHSA-vfj7-8cjw-p6xm reached through sass, ts-loader, webpack-dev-server and http-proxy-middleware) plus the 2 known moderate react-router ones. None of it comes from this PR: it touches api/ only, the root and api audits pass, and the same job was green at commit 7a16ac4 and red at 5aff50e with no web dependency change in between, so a newly published advisory is what flipped it. The gate stays red on every push until the web dev-dependency bumps land; sequence that work (or a documented override) before merging so the merge commit is not born red.
  • api/src/controllers/internal/viyaClient.ts:33 - resetViyaAuth is exported but nothing calls it outside the jest mock. If the cached token set ever becomes unusable (access token expired and the single-use refresh already spent), every subsequent request fails until the process restarts. Either wire it into the failure path (catch the adapter's auth errors, reset the cache, retry once) or drop the export.

2 findings above for review.

@allanbowe
allanbowe merged commit 61120ba into main Oct 3, 2026
6 of 7 checks passed
@allanbowe
allanbowe deleted the feat/viya-runtime branch October 3, 2026 18:17
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.20.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a sasviya runtime: execute stored programs on a Viya compute server

1 participant