Skip to content

fix(release): keyless subject uniqueness, NO_PROXY IPv6/CIDR, proxied boot secret, and remaining audit follow-ups - #9866

Merged
waleedlatif1 merged 16 commits into
stagingfrom
fix/release-audit-unique-1110
Nov 11, 2026
Merged

waleedlatif1 merged 16 commits into
stagingfrom
fix/release-audit-unique-1110

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

Release-audit follow-ups that no other open follow-up PR (#9857–#9865) covers; coordinated with those owners so nothing is duplicated.

  • Keyless subject uniqueness: creating or rebinding an AWS/Databricks keyless credential is refused (409) when another live credential of the same owner already holds that workload-identity subject, so a member removed from a credential can't replay its subject proof into a new one within the proof's TTL. Serialized per subject with acquireAdvisoryXactLock; a reconnect that keeps its own subject is unaffected; an unreadable sibling credential refuses with a clear 409 instead of a 500. Complements fix(security): harden saved-credential, proxy egress, and keyless-mint paths from the release audit #9865's subject-owner check at mint
  • NO_PROXY matching for the SSRF guard, AWS clients, and OTLP: bare IPv6 entries (::1) and CIDR ranges now match (bare ::1 was parsed as host : port 1); entries are parsed once per env value. Networking docs describe the rules and that Bun's default fetch follows its own
  • Boot secret fetch through the proxy: @sim/runtime-secrets uses the fetch handler under Bun when HTTP(S)_PROXY is set, so a proxy-only-egress deployment can load its startup secret
  • v2 key auth: a personal key whose owner row is missing returns 401 Invalid API key like every other key failure, instead of a 500
  • Pause context detail stops repeating inputFields on every listed point (only the selected point carries them, as with display)
  • NVIDIA Nemotron 3 Super is marked retired (NVIDIA stopped serving it 2026-10-19; Pi 1.5 dropped it), and the NVIDIA default moves to Nemotron 3 Ultra, Pi's own replacement. Saved selections are unchanged
  • credentialSuppliedParamsFor detects the saved-credential picker with isSavedCredentialPicker, the same predicate as the rest of the saved-credential code (no block changes behavior)
  • Docs: v2 execute says a full-access API key (read-only keys are refused there), MCP operations regenerated; the api-key-read-only flag description says to turn it off before rolling back below the release that added read-only keys

Type of Change

  • Bug fix
  • Security fix

Testing

  • Integration against PostgreSQL: workload-identity-subject-reuse.integration.ts (8: second credential and concurrent creates refused, reconnect onto a held subject refused, concurrent reconnects, own-subject reconnect allowed, unreadable siblings refuse with 409), pause-persistence.integration.ts (11); each guard reverted turns its test red
  • environment-proxy.server.test.ts (28; bare IPv6, long IPv6 spelling, IPv4/IPv6 CIDR fail without the change), runtime-secrets proxy-egress.test.ts (real Bun subprocess through a CONNECT recorder; no tunnel before the fix), v2-api-key-auth.test.ts, pi-providers.test.ts, models.test.ts, human-in-the-loop-manager.test.ts, saved-credential and params tests
  • tsc on apps/sim, runtime-secrets, and testing; root scripts and sim-cli suites; check:openapi, cli-api, mcp-operations, v2-route-table, boundaries, egress-boundary, realtime-prune, source-conventions, test-patterns, comment-hygiene; block registry check; biome

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing (new tests pass the test-audit authoring gate)
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…ready holds

A keyless subject proof stays valid for 24 hours and is not bound to one
credential, so a member removed from a keyless AWS or Databricks credential
could save a second credential with the same subject and proof and keep
assuming the customer's role.

Creating or reconnecting a credential onto a subject it did not already hold
now refuses (409 conflict) when another non-revoked credential of the same
owner holds it. The subject lives only in the encrypted secret, so the check
decrypts the owner's keyless credentials inside the write transaction, under a
per-subject advisory lock so concurrent saves serialize. A reconnect keeping
its stored subject binds nothing new and skips the check.
…ntialPicker

credentialSuppliedParamsFor matched any oauth-input feeding oauthCredential,
without the credentialKind === 'service-account' check isSavedCredentialPicker
applies. It now uses that predicate. No registered block changes: every block
whose loose-match picker lacks the service-account kind has no tool that
declares credentialSuppliedParams.
The Secrets Manager client used the SDK's Node handler, which ignores
HTTPS_PROXY, so a container whose only egress is a proxy could not load its
env and never booted. Under Bun with a proxy set, send through Bun's fetch,
which honors HTTPS_PROXY/HTTP_PROXY/NO_PROXY. apps/sim's awsRequestHandler
is out of reach for packages and the realtime bootstrap needs the same path.
… default

NVIDIA's model page states the nvidia/nemotron-3-super-120b-a12b API is no
longer supported after 2026-10-19, and Pi 1.0.1 dropped it as its NVIDIA
default for the same reason. Mark it deprecated (hidden from pickers, still a
known id for pinned workflows) and default the NVIDIA provider to Nemotron 3
Ultra, which the Pi catalog also serves.

The Mistral IDs Pi 1.5 dropped (mistral-large-2411, devstral-small-2507,
devstral-medium-2507) were retired by Mistral on 2026-05-31 and are already
deprecated here, so saved Pi blocks fail before credentials with the
choose-a-model error instead of being mapped onto a different model.
…ck helper and refuse on unreadable siblings

assertWorkloadIdentitySubjectUnclaimed now takes a DbTransaction and locks
through acquireAdvisoryXactLock, so the lock carries its SQLCommenter tag and
cannot be taken on the pool by mistake. A sibling keyless credential whose
secret does not decrypt or parse used to throw a generic error, failing every
keyless create or rotation for the owner with a 500. Since it might hold the
subject, the save is now refused as a conflict naming that credential, with a
warning logged, and stored subjects are read with a schema safeParse.
…nd CIDR

A bare IPv6 entry such as ::1 was read as host ':' port 1 and never matched,
IP entries only matched their exact spelling, and CIDR ranges were ignored.
Entries now follow curl: domains cover their subdomains, bracketed IPv6 may
carry a port, IP entries match any spelling, and CIDR ranges match IP-literal
destinations. NO_PROXY is parsed once per distinct value. The comments and
the self-hosting networking page describe these rules, and note that Bun's
default fetch applies its own.
@waleedlatif1
waleedlatif1 requested a review from a team as a code owner November 11, 2026 00:03
@vercel

vercel Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
docs Ready Ready Preview Nov 11, 2026 12:19am UTC

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 25 files

Turn on auto-fix | Re-trigger cubic

Comment thread apps/docs/openapi-v2-workflows.json Resolved
Comment thread packages/runtime-secrets/src/index.ts Resolved
@greptile-apps

greptile-apps Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge; no new actionable issue remains.

Summary

Adds checks that stop AWS and Databricks credentials from sharing a workload-identity subject.

  • Each live keyless credential keeps its own workload identity subject.
  • NO_PROXY entries now match IPv6 and CIDR destinations.
  • Boot-time secret fetches use the configured proxy under Bun.
  • Personal API keys with missing owners return the usual invalid-key response.
  • Pause details list input fields only on the selected point.
  • NVIDIA defaults to Nemotron 3 Ultra as Nemotron 3 Super retires.
  • Saved-credential checks and API guidance use consistent wording.

No new actionable issues were found.

Diagram

sequenceDiagram
  participant Save as Credential save
  participant Provider as Cloud provider
  participant DB as PostgreSQL
  Save->>Provider: Verify submitted credentials
  Provider-->>Save: Verification result
  Save->>DB: Begin transaction and lock subject
  opt Reconnect
    Save->>DB: Lock and read current credential
  end
  Save->>DB: Check other credentials when claiming subject
  alt Subject is available or already held by this credential
    Save->>DB: Write credential and commit
  else Another credential holds subject
    Save-->>Save: Return conflict
  end
Loading

Reviews (2) · Last reviewed commit: "Merge remote-tracking branch 'origin/sta..." · Reviewed by Greptile

…s reconnect

A reconnect that kept its subject skipped the uniqueness check, but whether
it kept the subject was decided from the secret read before provider
verification. If another reconnect moved the credential to a new subject and a
second credential claimed the old one meanwhile, the stale reconnect wrote the
old subject back unchecked and two credentials held it.

Every keyless reconnect now locks the subject it is writing, then the
credential row (FOR UPDATE), re-reads the row's current subject, and runs the
sibling check whenever the two differ, with the decision and the write in one
transaction.
Bun's fetch picks a proxy by scheme only, so with just HTTP_PROXY set the
https Secrets Manager request went direct and a proxy-only container still
could not boot. Move proxy selection (HTTPS_PROXY falling back to HTTP_PROXY,
curl-style NO_PROXY) from apps/sim into @sim/security/environment-proxy, and
have runtime-secrets pass the selected proxy to Bun's fetch explicitly, or
send through the Node handler when NO_PROXY excludes the endpoint.
Manual runs need api:write, which read-only personal keys lack, workspace
write access, and a personal key or OAuth token; workspace API keys are
refused. Regenerate the OpenAPI document and the MCP operation catalog.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 29 files

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

@waleedlatif1
waleedlatif1 merged commit 9b10389 into staging Nov 11, 2026
90 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/release-audit-unique-1110 branch November 11, 2026 00:23
waleedlatif1 added a commit that referenced this pull request Nov 11, 2026
…eyless, GitHub and Bitbucket tokens, DocuSign JWT (#9869)

* feat(credentials): keyless Snowflake connections through workload identity federation

A Snowflake connection can now trust Sim's OIDC issuer instead of holding a
programmatic access token. Sim signs a short-lived token (audience
snowflakecomputing.com) for the credential's own random subject; the customer
creates a SERVICE user with WORKLOAD_IDENTITY = (TYPE = OIDC ISSUER SUBJECT).

- Save verifies by minting a token and running the identity statement at the
  pinned account host through the guarded egress path; Snowflake's refusals
  keep the shared codes (invalid_credentials, site_not_found,
  provider_unavailable). Subjects bind to their creator like AWS/Databricks.
- Runtime: the resolver returns authStyle 'snowflake-workload-identity' for a
  keyless credential, and the tools and selectors send Bearer WIF.OIDC.<token>
  with the WORKLOAD_IDENTITY_FEDERATION token type. authStyle and domain are
  authoritative on every Snowflake tool, so a caller can no longer choose the
  token type or steer the account host of a saved credential.
- PAT credentials resolve and verify exactly as before.
- Connect dialog offers the keyless service user with the CREATE USER
  statement; it falls back to the token dialog where keyless is unavailable.
- Docs: Snowflake connections page with keyless setup, troubleshooting, and
  the 60-minute Snowflake key cache on signing-key rotation.

* fix(snowflake): run Cortex Analyst SQL with the resolved credential and reopen keyless reconnects as keyless

Cortex Analyst's "run the SQL" went through a nested snowflake_execute_sql
call carrying the already-resolved token and host. With authStyle and domain
now authoritative, the executor cleared both before the nested call, so it
failed for every credential (and a keyless token would have been sent as a
PAT). The generated SQL now runs as one guarded statement request built from
the parent call's credential-derived host, token, and token type, through the
same requestTarget egress the tool transport uses, with redirects refused.

The Snowflake connect dialog opened every reconnect on the programmatic
access token. The workload-identity prepare call now takes the reconnected
credential and reports whether it is keyless (only to an admin of that
credential in the same workspace), and the dialog opens on that mode.

Docs: the 60-minute wait applies before signing with a new key; removing the
old key needs 10 minutes with keyless Snowflake connections.

* fix(snowflake): cancel Cortex Analyst's generated SQL with the run

The generated SQL used to run as a nested tool call, which inherited the run's
abort signal; running it directly dropped that. postProcess now receives the
executor's effective abort signal as an optional fourth argument (the same
signal the tool's own request used), and Cortex Analyst forwards it to the DNS
check, the guarded fetch, and the response read. Other tools' postProcess
hooks are unchanged.

* feat(credentials): saved GitHub personal access tokens and Bitbucket access tokens

GitHub: a saved personal access token credential (fine-grained or classic),
verified against api.github.com/user with the tools' own headers. GitHub's
workflow block authenticates with an inline token rather than OAuth, so it
follows the token-block pattern: github_v3 is the credential-only successor
of github_v2, which stays exactly as it was, marked legacy and hidden from the
toolbar. Every GitHub tool declares the optional saved-token policy; inline
tokens keep working. The GitHub App installation credential (Search) is a
separate service and is untouched: the Assistant still projects GitHub tools
onto the person's connected GitHub account, and Search policy now follows the
visible GitHub block (github_v3).

Bitbucket: a workspace, project, or repository access token as a second
option next to OAuth (the Linear pattern), verified by listing one repository
of the typed workspace on api.bitbucket.org; the slug is validated so it
cannot steer the path. List Workspaces needs a signed-in user, so it now
refuses a saved access token before any request.

Also: isHostNotFound no longer lets an outer error code mask ENOTFOUND on its
cause.

* fix(credentials): close wave 2a review findings on GitHub and Bitbucket tokens

- The GitHub token service is `github-token`, so it no longer shares an id
  with the GitHub sign-in provider: parseProvider and getProviderIdFromServiceId
  answer for sign-in exactly as before, and a token-only service is never
  marked connected from a sign-in account whose id prefixes it.
- Suggested connect rows offer each integration once and count it connected
  when any of its services' OAuth or service-account credentials is saved
  (GitHub's Search App and saved tokens share one integration).
- Assistant GitHub calls bind through the connected-account policy the
  Assistant projects (the person's github-repositories OAuth account) in the
  token resolver too, on both the ordinary and managed paths; workflow runs
  and saved workspace tokens are unaffected.
- Bitbucket verification reads one repository (`workspace/repository`), the
  request Atlassian documents for workspace, project, and repository access
  tokens alike; listing a workspace is not documented for repository tokens.
- prepare-workload-identity test uses the central credentials-access mock.

* feat(credentials): DocuSign JWT Grant credentials next to OAuth

A saved DocuSign credential that authenticates with JWT Grant (an
integration key, an impersonated user ID, the app's RSA private key, and an
environment), offered as a second option beside the existing OAuth
connection, which is unchanged.

The minter signs an RS256 assertion (iss = integration key, sub = user,
aud = the environment's authentication host without a scheme, scope
"signature impersonation"), exchanges it at that host's /oauth/token, then
reads /oauth/userinfo on the same host to bind the pinned account (or the
user's default) and returns its eSignature REST base. Both hosts come only
from the stored environment; a base_uri outside that environment's
docusign.net shape is never trusted with the token. GUIDs and the key are
checked before any request, and tokens are re-minted about 15 minutes
before they expire, as DocuSign advises.

DocuSign tools take the account base as an authoritative credential param,
so a caller can never supply it, and DocuSignClient skips its
deployment-host userinfo lookup when one is bound. OAuth tokens resolve
exactly as before.

Save-time failures map to consent_required (the connect modal shows the
exact consent URL for the key and environment) and account_not_found (a
pinned account the user does not belong to), alongside the shared
invalid_credentials and provider_unavailable codes.

* fix(credentials): close wave 2b review findings on DocuSign JWT Grant

- The DocuSign knowledge connector now honours a JWT Grant credential's
  bound account. The resolved token's instanceUrl reaches the connector as
  credentialInstanceUrl in the sync context (seeded on every path through
  syncContextForToken). The connector re-validates it with the same parser
  as DocuSignClient, lists and fetches from that account in the
  credential's environment, links envelopes to that environment's web app,
  and validates without asking the deployment host. A seeded value of the
  wrong shape fails closed. OAuth syncs are unchanged.
- RSA keys under 2048 bits are refused as invalid_credentials with copy
  that names the size, instead of jose's raw TypeError.
- A short expires_in no longer makes every call re-mint: the cached
  lifetime floors at six minutes while staying a minute under the real
  expiry.
- The connect modal shows the consent URL as a link with a copy button,
  and account_not_found says "pinned account" only when one was entered.

* feat(credentials): Microsoft Entra app-only credentials for Entra ID, SharePoint, Dataverse, and Dynamics 365

A saved Microsoft app credential (OAuth 2.0 client credentials) as an
additional option next to Microsoft OAuth, which is unchanged. One app
registration serves the four services whose tools run without a signed-in
user; Outlook, OneDrive, Excel, Word, Planner, and Teams never offer it and
token resolution refuses it for them (provider mismatch).

- Modes: client secret; certificate (PS256 client assertion with the
  certificate's x5t#S256 thumbprint, aud = the tenant token endpoint, 5-minute
  lifetime; key/certificate pairing, RSA size, and expiry checked before any
  request); keyless federated credential (a Sim workload-identity token for the
  credential's own subject, audience api://AzureADTokenExchange, subject bound
  to the saving user, refused if the app also accepts an unheld subject).
- The token resource comes from the tool's service, passed from the
  server-validated tool policy: Graph for Entra ID and SharePoint, the stored
  environment for Dataverse and Dynamics 365. Any other service is refused.
- The environment URL is stored on the credential (public-cloud Dynamics
  hosts, https root only). Dataverse tools take credentialType and instanceUrl
  as authoritative params; an app credential's request goes only to its own
  environment and a block environment that names another is refused. OAuth
  requests keep the block environment exactly as before. Dynamics 365 already
  matched instanceUrl against the block environment.
- Save check: a Graph token reading the app's own service principal (no
  permission needed), plus Dataverse WhoAmI when an environment is set. Entra
  AADSTS codes map to actionable copy; runtime failures keep the shared
  invalid_credentials / provider_unavailable split. Tokens cache per
  credential, resource, and stored-secret version in an LRU.
- Azure AD Reset password refuses app-only credentials: Graph does not
  support application permissions for resetPassword.
- Connect dialog with tenant ID, client ID, environment URL, and the
  issuer/audience/subject to paste; docs page with per-mode setup.

* fix(credentials): close wave 3 review findings on Microsoft app credentials

- SharePoint selectors (workspace and organization paths) and knowledge
  connectors now pass the service they resolve for (the selector's own
  serviceId, the connector's declared auth provider), so a Microsoft app
  credential browses SharePoint with a Graph token instead of being refused.
  The resolution option is renamed from toolService to service; it is only
  ever set from server-side policy.
- AADSTS700211 and 700212 are no longer mapped: Microsoft documents only
  70021 and 700213 for federated credential mismatches. Other keyless
  refusals still name the mismatch.
- DocuSign minter test mocks carry account_name, and the connector
  access-token test narrows its tokens with a guard instead of non-null
  assertions.

* feat(credentials): keyless Google Cloud credentials via Workload Identity Federation

A keyless mode of the existing Google service account credential
(google-service-account) for Google Cloud APIs. JSON-key credentials, their
connect flow, and their token minting are unchanged.

- Flow: Sim signs a short-lived token for the credential's own subject with
  aud = the customer's provider resource name
  (https://iam.googleapis.com/projects/<number>/locations/global/
  workloadIdentityPools/<pool>/providers/<provider>), exchanges it at
  sts.googleapis.com/v1/token (token-exchange, jwt subject token,
  cloud-platform scope, audience //iam.googleapis.com/...), then calls
  iamcredentials generateAccessToken for the service account with the tool's
  scopes and a 1h lifetime. Impersonation rather than direct federated access:
  some Cloud APIs reject federated tokens, and the service account keeps the
  same identity and roles a JSON key uses.
- Inputs validated against Google's documented formats before any request:
  numeric project number, pool/provider IDs (4-32 [a-z0-9-], no gcp-), and a
  user-managed *.iam.gserviceaccount.com email, so nothing can steer the STS
  audience or IAM URL. Requests never follow redirects.
- Scope: refuses domain-wide delegation (an impersonated user) and any service
  but BigQuery and Vertex AI before reading further or contacting Google, with
  a new shared unsupported_use code and copy pointing to a JSON key. Workspace
  blocks never offer the mode.
- Save check: STS exchange, impersonation, and tokeninfo (no IAM role or
  enabled API needed), plus a probe that a subject no credential holds cannot
  impersonate the service account. Subject bound to the saving user with the
  signed proof; kept across reconnects.
- STS/IAM failures map to actionable copy (provider settings, issuer
  reachability, the exact workloadIdentityUser binding) without Google's
  descriptions or any token; runtime keeps the invalid_credentials /
  provider_unavailable split. Tokens cache per credential, secret version, and
  scopes in an LRU.
- Per-credential audiences: audiences.ts gains google without a fixed
  audience; the setup response omits audience for it and reports a keyless
  reconnect. Connect dialog shows the issuer, audience, subject, and filled-in
  gcloud commands (pool, OIDC provider with attribute condition on the
  subject, IAM binding); docs section added.

* fix(credentials): close wave 4 review findings on keyless Google Cloud credentials

- Drop the tokeninfo check: a token from generateAccessToken already proves
  the provider accepted Sim's token and the impersonation binding, and
  tokeninfo puts a live token in a query string.
- Test that saving fails closed when the stray-subject pin probe hits an STS
  outage or an unreachable IAM endpoint.
- Pass --project=<project number> on all three gcloud setup commands, drop
  the unused Resource Manager API from the docs, and raise the shared
  validation error for an empty scope list.

* fix(credentials): keep Snowflake, Microsoft app, and Google keyless subjects unique

The subject-uniqueness scan from #9866 covered only AWS and Databricks. It now
also scans Snowflake, Microsoft app, and Google service account credentials,
so no two live credentials of an owner hold one keyless subject. Their keyed
secrets that carry no config (a Google JSON key, a Snowflake programmatic
access token) hold no subject and no longer read as unreadable. The three
keyless save paths report a newly bound subject so create checks it.

* fix(credentials): close round-1 CI and review findings on alt auth

- Knowledge connector tests expect the service now passed with Google
  service-account token requests; a new test proves a JSON-key mint signs the
  same request with or without it.
- Re-record the tool-registry-boundary baseline: the keyless Snowflake,
  Microsoft app, and Google modules add 5 modules behind credential-service,
  which tipped credit-usage past its allowance on top of staging's drift.
- Docs compatibility baseline: record the new code examples in the Google and
  Snowflake service account pages.
- Generate the Microsoft certificate test fixture at run time instead of
  committing a PEM key and certificate.
- A Microsoft app credential reconnect opens on keyless only when the stored
  credential is federated (the prepare step now reports it), like Snowflake;
  new connections still default to keyless.
- TSDoc on the Google project number, pool/provider ID, and service account
  email validators.

* fix(credentials): offer keyless Google setup only in a Google Cloud context

Reconnecting a keyless Google service account from a Workspace integration
(Gmail, Drive, ...) offered the keyless form, saving a credential that
integration can never use. The dialog now offers keyless only for a Google
Cloud service (new or reconnect) or on the credential's own page; from a
Workspace integration it explains the credential works only with BigQuery and
Vertex AI and does not save. The chat connect control passes its service so
it gets the same gate.

* test(credentials): match the stable part of the subject-in-use refusal

This branch was successfully deployed

1 active deployment
Preview — f1d0b0f0 Deployed Nov 11, 2026 by vercel[bot]
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.

1 participant