Skip to content

Commit a4bbaf7

Browse files
authored
fix(credentials): correct keyless setup instructions and enforce the saved-credential contract on every credential block (#9878)
* fix(credentials): enforce the saved-credential contract on every credential block, reopen key-based reconnects on their mode, and tighten fixtures and skills - check-block-registry runs the saved-credential contract for every block with a saved-credential picker, not only successors; the per-block contract moves to saved-credential-contract.ts with tests. The host probe gains a path-shaped id and a per-param shape so mixed-validation builders (Jama, Higgsfield) are probed, not guessed. - Blocks the widened check caught: Coda, Harmonic, and Oracle Database tools declare credentialKind 'service-account'; NetSuite pins instanceUrl in authoritativeParams; Vanta's assetExternalAccountId is a filter, not a host. - prepareWorkloadIdentity covers every keyless provider (AWS, Databricks added) and reuses audiences.ts for the provider-id mapping; the AWS and Databricks dialogs reopen a reconnect on the stored mode. - Snowflake keyless verification maps an egress refusal to host_not_allowed. - Neutral token fixtures in new credential tests; subject uniqueness TSDoc; skills describe native token blocks, the picker shape, keyless setup, and the successor naming exception. * docs(credentials): fix keyless setup instructions and copy - AWS: enter the Role ARN before setting the trust policy, so it never carries the <AWS_ACCOUNT_ID> placeholder IAM rejects (dialog hint and docs). - Databricks: the federation-policy command needs an account-level CLI login. - Google: a valid sample pool ID, ID format hints, user-managed service accounts only, skip the pool command when the pool exists, and the API list from Google's guide (adds Resource Manager). - Bitbucket: workspace and project access tokens need Premium; repository tokens do not. - Snowflake: rename SIM_WORKFLOWS if it exists; USERADMIN runs the statement. - Microsoft keyless dialog says where the federated credential goes. - Server errors name the trust values for API and CLI callers too; Google Workspace and Account ID wording; AWS catalog hint mentions stsRegion; the CLI keyless example uses aws-service-account throughout. - Self-hosting env vars list every keyless provider. * fix(credentials): reopen every keyless-capable reconnect on its stored auth mode and reject native credential tools without a policy - prepareWorkloadIdentity reports the reconnected credential's stored authType (reconnectAuthType), for an admin of a credential in the same workspace, whether or not this deployment can sign. reconnectsKeyless is removed from the contract and hook. The AWS, Databricks, Snowflake, Microsoft and Google dialogs reopen on that mode, so an assume_role, personal_access_token or certificate credential keeps its auth. - The saved-credential contract fails a native credential block whose tool has no oauth policy: the executor would never fill its token. - The Snowflake hint and docs say where to change SIM_WORKFLOWS; the DocuSign account_not_found message covers a credential with no Account ID; the docs baseline's reviewed CLI example entries carry their new byte length; the contract test fixtures use a real OAuth service id.
1 parent 219b39c commit a4bbaf7

61 files changed

Lines changed: 853 additions & 416 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/skills/add-block/SKILL.md‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ export const {ServiceName}Block: BlockConfig = {
8383
// Multi-line input
8484
{ id: 'field', title: 'Label', type: 'long-input', placeholder: '...', rows: 6 }
8585

86-
// Password input
86+
// Password input: only for legacy blocks or a secret with no saved-credential fit
87+
// (model-provider keys, hosted-key pairs); a service token uses a saved credential
8788
{ id: 'apiKey', title: 'API Key', type: 'short-input', password: true }
8889
```
8990

@@ -136,6 +137,32 @@ export const {ServiceName}Block: BlockConfig = {
136137
}
137138
```
138139

140+
A service-account-only picker (a saved token or key credential) instead sets
141+
`credentialKind: 'service-account'` and `mode: 'basic'` (no scopes), and pairs with an advanced
142+
twin; `isSavedCredentialPicker` matches only this shape:
143+
```typescript
144+
{
145+
id: 'credential',
146+
title: '{Service} Account',
147+
type: 'oauth-input',
148+
serviceId: '{service}',
149+
credentialKind: 'service-account',
150+
canonicalParamId: 'oauthCredential',
151+
mode: 'basic',
152+
placeholder: 'Select {Service} credential',
153+
required: true,
154+
},
155+
{
156+
id: 'manualCredential',
157+
title: '{Service} Account',
158+
type: 'short-input',
159+
canonicalParamId: 'oauthCredential',
160+
mode: 'advanced',
161+
placeholder: 'Enter credential ID',
162+
required: true,
163+
}
164+
```
165+
139166
**Scopes:** Always use `getScopesForService(serviceId)` from `@/lib/oauth/utils` for `requiredScopes`. Never hardcode scope arrays — the single source of truth is `OAUTH_PROVIDERS` in `lib/oauth/oauth.ts`.
140167

141168
**Scope descriptions:** When adding a new OAuth provider, also add human-readable descriptions for all scopes in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`.
@@ -695,7 +722,8 @@ outputs: {
695722
When creating V2 blocks (alongside legacy V1):
696723

697724
```typescript
698-
// V1 Block - mark as legacy
725+
// V1 Block - mark as legacy. A credential-only successor is the exception: the
726+
// successor keeps the legacy block's name, so neither block is renamed.
699727
export const ServiceBlock: BlockConfig = {
700728
type: 'service',
701729
name: 'Service (Legacy)',

‎.agents/skills/add-integration/SKILL.md‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ supported paths and choose the simplest supported setup for the intended user:
4343
| Customer-owned client credentials | Saved `service_account` credential with a client ID and secret | Internal-app eligibility, grant activation, scope syntax, token lifetime and revocation behavior |
4444
| API token or personal access token | Saved token service account when reusable connections are useful | Token permissions, identity verification, expiry and rotation |
4545
| Private key, certificate, or service-account JSON | Existing key-based service-account framework | Signing algorithm, audience, subject, tenant binding and key rotation |
46+
| Cloud workload identity (AWS role, Databricks/Snowflake/Entra/GCP federation) | Keyless saved credential: Sim signs a short-lived OIDC token the customer's cloud trusts. Add the provider to every table in `lib/credentials/workload-identity/audiences.ts` (`WORKLOAD_IDENTITY_PROVIDERS`, its service-account provider ID map, `WORKLOAD_IDENTITY_KEYLESS_CONFIGS`, `WORKLOAD_IDENTITY_AUDIENCES`) and to `MIXED_MODE_PROVIDERS` in `lib/credentials/application/prepare-workload-identity.ts` | Issuer and audience the provider accepts, how the trust pins the subject, token lifetime |
47+
48+
API, CLI, and MCP callers set up keyless in two steps: `POST /api/v2/credentials/workload-identity/subjects`
49+
(or `sim credentials keyless-subject <providerId> --output json`) returns the subject to trust and a
50+
`keylessConfig`; they add its `requiredConfigFields` and create the credential with it. The provider
51+
catalog advertises `keyless` only when the deployment can sign tokens.
4652

4753
Do not present customer-owned credentials as a way around a provider's approval requirements
4854
for a shared public integration. Explain the supported account/app type in the setup docs. Keep
@@ -86,15 +92,23 @@ service id:
8692
answers), and `errorBodyEchoesToken` when an error body echoes the token.
8793
3. `apps/sim/lib/oauth/oauth.ts` and `types.ts`: the service entry (`authType: 'service_account'`,
8894
`serviceAccountProviderId`).
89-
4. `apps/sim/tools/shared/saved-credentials.ts`: `tokenCredentialOAuth('<service>', { <toolParam>:
90-
'accessToken' | 'domain' | 'providerWorkspaceId' })`, listed in `SAVED_CREDENTIAL_OAUTH`, and set
91-
as `oauth` on every tool. Map every param that decides the request host; the executor overwrites
92-
mapped params after authorizing the credential, and `check-block-registry` probes the tools' real
93-
URL builders and fails on a host-steering param left unmapped.
95+
4. The tools' credential policy. A NEW integration's tools take a hidden `accessToken` and declare
96+
`oauth: { required: true, provider: '<service>', credentialKind: 'service-account' }`, pinning
97+
any host or account param with `authoritativeParams` (Flow's and Snowflake's tools are the
98+
pattern). `tokenCredentialOAuth('<service>', { <toolParam>: 'accessToken' | 'domain' |
99+
'providerWorkspaceId' })` in `apps/sim/tools/shared/saved-credentials.ts`, listed in
100+
`SAVED_CREDENTIAL_OAUTH`, is for existing tools that already take the token inline (successors).
101+
Either way, the executor overwrites mapped and pinned params after authorizing the credential,
102+
and `check-block-registry` probes the real URL builders of every block with a saved-credential
103+
picker and fails on a host-steering param left unmapped or unpinned.
94104
5. A credential-only block (including a successor) uses `authMode: AuthMode.OAuth`, so the
95105
generated catalog maps the service; a token-only block that keeps `AuthMode.ApiKey`, as Coda
96106
does, registers in `tokenCredentialIntegrationTypes` instead (see below). The credential picker
97-
sits directly under the operation dropdown (`add-block` → OAuth/Credentials). An existing block that already takes the secret inline is never edited in
107+
sits directly under the operation dropdown (`add-block` → OAuth/Credentials). A new block
108+
writes the picker inline, as Higgsfield does: `{ id: 'credential', type: 'oauth-input', serviceId,
109+
credentialKind: 'service-account', canonicalParamId: 'oauthCredential', mode: 'basic',
110+
required: true }`, plus a `manualCredential` `short-input` with the same `canonicalParamId` in
111+
`mode: 'advanced'`. An existing block that already takes the secret inline is never edited in
98112
place: add a credential-only successor (`add-block` → Credential-only successor).
99113

100114
Then run `generate-docs`, `generate:block-successors`, `generate:deployment-config`, and
@@ -461,7 +475,8 @@ If creating V2 versions (API-aligned outputs):
461475

462476
1. **V2 Tools** - Add `_v2` suffix, version `2.0.0`, flat outputs
463477
2. **V2 Block** - Add `_v2` type, use `createVersionedToolSelector`
464-
3. **V1 Block** - Add `(Legacy)` to name, set `hideFromToolbar: true`, and add
478+
3. **V1 Block** - Add `(Legacy)` to name (a credential-only successor is the exception: it keeps
479+
the legacy block's name), set `hideFromToolbar: true`, and add
465480
`sunset: { status: 'legacy', replacedBy: '{service}_v2' }` — `check-block-registry`
466481
fails a legacy block with no `replacedBy`, and the amber legacy badge plus its
467482
click-to-upgrade action read from that field.

‎.agents/skills/validate-integration/SKILL.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,13 @@ For **each tool** in `tools.access`:
226226
`tokenCredentialOAuth` policy, every
227227
param that decides the request host is mapped, and the stored domain is in the form the tools
228228
build URLs from (`check-block-registry.ts origin/staging` passes)
229+
- [ ] A non-successor token block's tools declare `oauth: { required: true, provider,
230+
credentialKind: 'service-account' }` and pin every host or account param in
231+
`authoritativeParams` (`check-block-registry` enforces this for every block with a
232+
saved-credential picker)
233+
- [ ] A keyless provider appears in every table in `lib/credentials/workload-identity/audiences.ts`
234+
and in `MIXED_MODE_PROVIDERS` (`prepare-workload-identity.ts`), and the provider catalog
235+
shows `keyless` only when the deployment can sign tokens
229236
- [ ] The `tokenCheckValidator` request uses the tools' host and auth, its normalizers refuse
230237
anything that could steer the host or path, and a fake key is rejected without a valid
231238
scoped key being refused

‎apps/docs/content/docs/cli/credentials.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,9 @@ The proof expires after 24 hours and works only for the user who requested it, w
100100
**Examples**
101101

102102
```bash
103-
sim credentials keyless-subject <providerId> --output json > subject.json
103+
sim credentials keyless-subject aws-service-account --output json > subject.json
104104
jq '.keylessConfig + {"roleArn":"arn:aws:iam::123456789012:role/SimWorkflows"}' subject.json > keyless.json
105-
sim credentials create <providerId> --name "Keyless" --credentials @keyless.json
105+
sim credentials create aws-service-account --name "Keyless" --credentials @keyless.json
106106
```
107107

108108
API request and response schemas: [createServiceAccountCredential](/api-reference/credentials/createServiceAccountCredential).
@@ -373,9 +373,9 @@ The proof expires after 24 hours and works only for the user who requested it, w
373373
**Examples**
374374

375375
```bash
376-
sim credentials keyless-subject <providerId> --output json > subject.json
376+
sim credentials keyless-subject aws-service-account --output json > subject.json
377377
jq '.keylessConfig + {"roleArn":"arn:aws:iam::123456789012:role/SimWorkflows"}' subject.json > keyless.json
378-
sim credentials create <providerId> --name "Keyless" --credentials @keyless.json
378+
sim credentials create aws-service-account --name "Keyless" --credentials @keyless.json
379379
```
380380

381381
API request and response schemas: [createCredentialWorkloadIdentitySubject](/api-reference/credentials/createCredentialWorkloadIdentitySubject).

‎apps/docs/content/docs/cli/reference.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -786,9 +786,9 @@ The proof expires after 24 hours and works only for the user who requested it, w
786786
**Examples**
787787

788788
```bash
789-
sim credentials keyless-subject <providerId> --output json > subject.json
789+
sim credentials keyless-subject aws-service-account --output json > subject.json
790790
jq '.keylessConfig + {"roleArn":"arn:aws:iam::123456789012:role/SimWorkflows"}' subject.json > keyless.json
791-
sim credentials create <providerId> --name "Keyless" --credentials @keyless.json
791+
sim credentials create aws-service-account --name "Keyless" --credentials @keyless.json
792792
```
793793

794794
API request and response schemas: [createServiceAccountCredential](/api-reference/credentials/createServiceAccountCredential).
@@ -822,9 +822,9 @@ The proof expires after 24 hours and works only for the user who requested it, w
822822
**Examples**
823823

824824
```bash
825-
sim credentials keyless-subject <providerId> --output json > subject.json
825+
sim credentials keyless-subject aws-service-account --output json > subject.json
826826
jq '.keylessConfig + {"roleArn":"arn:aws:iam::123456789012:role/SimWorkflows"}' subject.json > keyless.json
827-
sim credentials create <providerId> --name "Keyless" --credentials @keyless.json
827+
sim credentials create aws-service-account --name "Keyless" --credentials @keyless.json
828828
```
829829

830830
API request and response schemas: [createCredentialWorkloadIdentitySubject](/api-reference/credentials/createCredentialWorkloadIdentitySubject).

‎apps/docs/content/docs/integrations/aws-credentials.mdx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ STS **Get Session Token** works only with an **Access keys** credential: AWS ref
3636
In the AWS IAM console, open **Identity providers → Add provider**, choose **OpenID Connect**, and enter the dialog's **Issuer** as the **Provider URL**, and its **Audience**. You only do this once per AWS account.
3737
</Step>
3838
<Step>
39-
Create a role (or edit an existing one) and set its trust policy to the **Role trust policy** shown in the dialog. It trusts only this credential's **Subject**, so no other Sim credential, workspace, or customer can assume the role.
39+
Enter the role's ARN (`arn:aws:iam::<account ID>:role/<role name>`) in the dialog first. The **Role trust policy** fills in its account ID; until then it shows `<AWS_ACCOUNT_ID>`, which IAM rejects.
4040
</Step>
4141
<Step>
42-
Attach the permissions your workflows need to the role, then paste its ARN into the dialog and save. Sim verifies the connection by assuming the role before saving it, and refuses a role that would also trust a different subject.
42+
Create the role with that name (or edit the existing one) and set its trust policy to the **Role trust policy** shown in the dialog. It trusts only this credential's **Subject**, so no other Sim credential, workspace, or customer can assume the role.
43+
</Step>
44+
<Step>
45+
Attach the permissions your workflows need to the role, then save. Sim verifies the connection by assuming the role before saving it, and refuses a role that would also trust a different subject.
4346
</Step>
4447
</Steps>
4548

‎apps/docs/content/docs/integrations/bitbucket-service-account.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Bitbucket shows actions taken with the token (comments, merges, pipeline runs) u
1212

1313
## Prerequisites
1414

15-
Admin access to the workspace, project, or repository the token is for. Workspace access tokens require a Bitbucket Cloud Premium plan; project and repository access tokens do not.
15+
Admin access to the workspace, project, or repository the token is for. Workspace and project access tokens require a Bitbucket Cloud Premium plan; repository access tokens do not.
1616

1717
## Create the access token [#creating-the-access-token]
1818

‎apps/docs/content/docs/integrations/databricks-credentials.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Every option resolves on Sim's servers at run time; tokens never appear in block
2929
In Sim, add a **Databricks connection**, choose **Service principal (keyless, via Sim OIDC)**, and enter the workspace host and the service principal's **application ID**.
3030
</Step>
3131
<Step>
32-
A Databricks account admin runs the **Federation policy** command from the dialog with the service principal's numeric ID (find it with `databricks account service-principals list --filter 'applicationId eq "<application ID>"'`, or on the service principal's page in the account console). The policy trusts Sim's issuer, the Sim audience, and this connection's subject only.
32+
A Databricks account admin runs the **Federation policy** command from the dialog with the service principal's numeric ID. First sign in to the account with `databricks auth login --host <account console URL> --account-id <account ID>` (not a workspace profile or the workspace web terminal). Find the numeric ID with `databricks account service-principals list --filter 'applicationId eq "<application ID>"'`, or on the service principal's page in the account console. The policy trusts Sim's issuer, the Sim audience, and this connection's subject only.
3333
</Step>
3434
<Step>
3535
Make sure the service principal can access the workspace and the resources your workflows use, then save. Sim verifies the connection by exchanging a token and reading the service principal's identity, and refuses a policy that would also trust a different subject, since every Sim workspace shares the issuer and audience.

‎apps/docs/content/docs/integrations/google-service-account.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,16 @@ Keyless credentials work with Google Cloud APIs that the service account calls a
185185

186186
<Steps>
187187
<Step>
188-
In the [Google Cloud Console](https://console.cloud.google.com/), enable the IAM, Service Account Credentials, and Security Token Service APIs in the project that will hold the workload identity pool, and note its **project number** (on the project's Dashboard; it's numeric, not the project ID).
188+
In the [Google Cloud Console](https://console.cloud.google.com/), enable the IAM, Resource Manager, Service Account Credentials, and Security Token Service APIs in the project that will hold the workload identity pool, and note its **project number** (on the project's Dashboard; it's numeric, not the project ID).
189189
</Step>
190190
<Step>
191191
Create or pick a service account, and grant it the roles your workflows need (for example, BigQuery Data Viewer and BigQuery Job User, or Vertex AI User). No key is needed.
192192
</Step>
193193
<Step>
194-
In Sim, open the **Google BigQuery** integration, choose **Add service account**, and set **Authentication** to **Workload Identity Federation (keyless, via Sim OIDC)**. Enter the project number, a pool ID and provider ID (4 to 32 lowercase letters, digits, or hyphens, not starting with `gcp-`), and the service account email (`name@project-id.iam.gserviceaccount.com`).
194+
In Sim, open the **Google BigQuery** integration, choose **Add service account**, and set **Authentication** to **Workload Identity Federation (keyless, via Sim OIDC)**. Enter the project number, a pool ID and provider ID (4 to 32 lowercase letters, digits, or hyphens, not starting with `gcp-`), and the service account email (`name@project-id.iam.gserviceaccount.com`; user-managed service account only, so default accounts such as `…@developer.gserviceaccount.com` are refused).
195195
</Step>
196196
<Step>
197-
The dialog shows the **Issuer**, **Audience**, and **Subject**, and three `gcloud` commands with your values filled in. Run them in that project. They run against the project number you entered and create the pool, an OIDC provider for Sim's issuer with the attribute mapping `google.subject=assertion.sub` and an attribute condition that accepts only this credential's subject, and grant that subject `roles/iam.workloadIdentityUser` on the service account:
197+
The dialog shows the **Issuer**, **Audience**, and **Subject**, and three `gcloud` commands with your values filled in. Run them in that project. They run against the project number you entered and create the pool, an OIDC provider for Sim's issuer with the attribute mapping `google.subject=assertion.sub` and an attribute condition that accepts only this credential's subject, and grant that subject `roles/iam.workloadIdentityUser` on the service account. If the pool already exists, skip the first command:
198198

199199
```bash
200200
gcloud iam workload-identity-pools create POOL_ID \

‎apps/docs/content/docs/integrations/snowflake-service-account.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Snowflake's [workload identity federation](https://docs.snowflake.com/en/user-gu
2424
In Sim, add a **Snowflake** connection, choose **Service user (keyless, via Sim OIDC)**, and enter the account host (`myorg-myaccount.snowflakecomputing.com`). The dialog shows the issuer, audience, and subject for this connection, and the statement to run.
2525
</Step>
2626
<Step>
27-
A user whose role can create users (and has `OWNERSHIP` or `MODIFY PROGRAMMATIC AUTHENTICATION METHODS` on the user) runs the statement from the dialog. It looks like this:
27+
A user whose role can create users (for example USERADMIN) runs the statement from the dialog. If SIM_WORKFLOWS already exists, change the name in the statement you run and in the follow-up GRANT and ALTER commands below. It looks like this:
2828

2929
```sql
3030
CREATE USER SIM_WORKFLOWS

0 commit comments

Comments
 (0)