Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
34 changes: 34 additions & 0 deletions .agents/skills/add-block/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,40 @@ export const {ServiceName}Block: BlockConfig = {

Optional companions: `credentialLabels` (override the picker's section/connect-row copy) and `allowServiceAccounts: true` (trigger-mode only — list service accounts, which triggers otherwise exclude; set only when the trigger's polling path can resolve a service-account token). The connect modal, provider families (Google JSON key, Atlassian token, token-paste, client-credential, Slack bot), and the preview gate are all resolved from `serviceAccountProviderId` — you don't wire them per block.

### Credential-only successor (replacing inline secrets)

A block that takes a secret inline keeps running for existing workflows; the saved-credential
version is a new block type that spreads it. Keep the successor an object literal with the picker
written inline, because the docs generator reads the source text:

```typescript
export const ServiceV2Block: BlockConfig = {
...ServiceBlock,
type: 'service_v2',
authMode: AuthMode.OAuth,
hideFromToolbar: false,
sunset: undefined,
...withSavedCredential(ServiceBlock, {
id: 'credential',
type: 'oauth-input',
serviceId: 'service',
title: 'Service Credential',
}),
}
```

- The legacy block changes only by `hideFromToolbar: true` and `sunset: { status: 'legacy',
replacedBy: 'service_v2' }` (an older `_v2` points at the new `_v3`). Register both types.
- `withSavedCredential` (`blocks/utils.ts`) swaps exactly the fields its policy in
`SAVED_CREDENTIAL_OAUTH` supplies for the picker, places it directly under the operation
dropdown, inherits the replaced fields' `condition` and `required`, and leaves trigger-mode
fields alone. Pass `required` when the replaced fields disagree (an optional host), and `params`
only when the legacy params builder validates the secret: extract that builder so the legacy
block calls it unchanged (`blocks/legacy-builders.test.ts`) and the successor skips the check.
- Add the pair to `serializer/saved-credentials.test.ts` (`optionalIds` for replaced fields the
legacy block never required), and grep for code that hard-codes the legacy type (`getBlock`,
`integration.type ===`, availability lookups), which now resolves to the successor.

### OAuth deployment availability (required for integration blocks)

A visible tools-category block with OAuth is deployment-gated. Its `oauth-input.serviceId` is
Expand Down
36 changes: 36 additions & 0 deletions .agents/skills/add-integration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,42 @@ Use explicit permission choices when the supported operations have different acc
region and permission selections on reconnect unless the user changes them. Verify documented
identity endpoints rather than guessing which person a client-credentials token represents.

### Token credentials (API key, personal access token, token + host)

A service authenticated by a pasted key or token takes it through a saved token credential, never an
inline `password` field, so one shared, verified credential serves every block. Wire it by the
service id:

1. `apps/sim/lib/credentials/token-service-accounts/descriptors.ts`: a provider id and descriptor.
Fields are `apiToken`, plus `domain` for a host, instance URL, or region and
`providerWorkspaceId` for an account or org id; every listed field is required. Set `url: true`
on a `domain` that is a URL whose scheme or path matters, so the connect form sends it as typed.
2. `validators/token-checks.ts`: one `tokenCheckValidator` declaration (register it in
`server.ts`). Make one cheap, documented read on the tools' own host with the tools' own auth,
reusing their header and URL builders. `domain` and `workspaceId` normalize to exactly the form
the tools build URLs from and throw for anything that could steer the host or path
(`normalizeHttpBaseUrl`, `allowedDomain`, `refuseUrlCredentials`); the normalized domain is what
the credential stores. Set `egress` when the user chooses the host. Add `forbiddenMeansScoped`,
`rejectsKey`, or `provesKey` only for documented replies (probe a fake key to see how a bad one
answers), and `errorBodyEchoesToken` when an error body echoes the token.
3. `apps/sim/lib/oauth/oauth.ts` and `types.ts`: the service entry (`authType: 'service_account'`,
`serviceAccountProviderId`).
4. `apps/sim/tools/shared/saved-credentials.ts`: `tokenCredentialOAuth('<service>', { <toolParam>:
'accessToken' | 'domain' | 'providerWorkspaceId' })`, listed in `SAVED_CREDENTIAL_OAUTH`, and set
as `oauth` on every tool. Map every param that decides the request host; the executor overwrites
mapped params after authorizing the credential, and `check-block-registry` probes the tools' real
URL builders and fails on a host-steering param left unmapped.
5. A credential-only block (including a successor) uses `authMode: AuthMode.OAuth`, so the
generated catalog maps the service; a token-only block that keeps `AuthMode.ApiKey`, as Coda
does, registers in `tokenCredentialIntegrationTypes` instead (see below). The credential picker
sits directly under the operation dropdown (`add-block` → OAuth/Credentials). An existing block that already takes the secret inline is never edited in
place: add a credential-only successor (`add-block` → Credential-only successor).

Then run `generate-docs`, `generate:block-successors`, `generate:deployment-config`, and
`generate:tool-metadata`, and `check-block-registry.ts origin/staging`. This pattern does not fit
hosted-key tools, model-provider keys, two-secret or username/password logins, or a host chosen
per call.

### Pair saved credentials with block and resource selectors

`oauth-input` is the shared saved-credential picker, including for service accounts that never
Expand Down
4 changes: 4 additions & 0 deletions .agents/skills/add-tools/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ export const {serviceName}{Action}Tool: ToolConfig<
provider: '{service}', // Must match the service id in OAUTH_PROVIDERS
// credentialKind: 'service-account', // service-account-only integrations
},
// A token credential (API key, PAT, token + host) instead uses the service's shared policy from
// tools/shared/saved-credentials.ts, built with tokenCredentialOAuth('{service}', { apiKey:
// 'accessToken' }). Its keys are the tool's own token/host/account params, which the executor
// fills after authorizing the credential.

params: {
// Hidden params (system-injected, e.g. the OAuth accessToken)
Expand Down
11 changes: 11 additions & 0 deletions .agents/skills/validate-integration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,17 @@ For **each tool** in `tools.access`:
- True/false → `switch` (a Yes/No `dropdown` only when the tool needs a third "unset" state)
- Credentials → `oauth-input` with correct `serviceId`
- Every tool with a `credential`/`oauthCredential` param declares `oauth: { required: true, provider }` matching that `serviceId`, plus `credentialKind: 'service-account'` when only service accounts are supported
- [ ] In a new block or credential-only successor, a pasted key, token, or token + host comes from a
saved token credential (add-integration → Token credentials), not an inline `password` field
(legacy blocks keep theirs); a successor changes its legacy block only by `hideFromToolbar`
and `sunset`, and its picker sits under the operation
- [ ] For a token-credential integration, every tool in `tools.access` declares the service's
`tokenCredentialOAuth` policy, every
param that decides the request host is mapped, and the stored domain is in the form the tools
build URLs from (`check-block-registry.ts origin/staging` passes)
- [ ] The `tokenCheckValidator` request uses the tools' host and auth, its normalizers refuse
anything that could steer the host or path, and a fake key is rejected without a valid
scoped key being refused
- [ ] Dropdown `value: () => 'default'` is set for dropdowns with a sensible default
- [ ] Every `short-input`, `long-input`, `code`, and selector subBlock has a `placeholder` — including
password fields (`Enter your API key`). Formatted values show the shape
Expand Down
21 changes: 21 additions & 0 deletions apps/docs/components/ui/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
ahrefs_v2: AhrefsIcon,
airtable: AirtableIcon,
algolia: AlgoliaIcon,
algolia_v2: AlgoliaIcon,
amplitude: AmplitudeIcon,
api: ApiIcon,
api_trigger: ApiIcon,
Expand All @@ -361,6 +362,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
attio: AttioIcon,
azure_data_explorer: AzureDataExplorerIcon,
azure_devops: AzureIcon,
azure_devops_v2: AzureIcon,
bitbucket: BitbucketIcon,
box: BoxCompanyIcon,
box_v2: BoxCompanyIcon,
Expand Down Expand Up @@ -403,6 +405,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
confluence_v2: ConfluenceIcon,
context_dev: ContextDevIcon,
convex: ConvexIcon,
convex_v2: ConvexIcon,
credential: CredentialIcon,
credential_group: GridOffset,
crowdstrike: CrowdStrikeIcon,
Expand All @@ -420,6 +423,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
daytona_v2: DaytonaIcon,
deployments: Rocket,
devin: DevinIcon,
devin_v2: DevinIcon,
discord: DiscordIcon,
discord_v2: DiscordIcon,
docusign: DocuSignIcon,
Expand All @@ -435,12 +439,14 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
dynamodb: DynamoDBIcon,
dynamodb_v2: DynamoDBIcon,
dynatrace: DynatraceIcon,
dynatrace_v2: DynatraceIcon,
ec2: EC2Icon,
ec2_v2: EC2Icon,
elasticsearch: ElasticsearchIcon,
elevenlabs: ElevenLabsIcon,
elevenlabs_v2: ElevenLabsIcon,
emailbison: EmailBisonIcon,
emailbison_v2: EmailBisonIcon,
embeddings: EmbeddingsIcon,
enrich: EnrichSoIcon,
enrich_v2: EnrichSoIcon,
Expand Down Expand Up @@ -506,6 +512,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
'google-drive': GoogleDriveIcon,
'google-sheets': GoogleSheetsIcon,
grafana: GrafanaIcon,
grafana_v2: GrafanaIcon,
grain: GrainIcon,
grain_v2: GrainIcon,
grain_v3: GrainIcon,
Expand Down Expand Up @@ -537,6 +544,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
incidentio: IncidentioIcon,
incidentio_v2: IncidentioIcon,
infisical: InfisicalIcon,
infisical_v2: InfisicalIcon,
input_trigger: FormInput,
instantly: InstantlyIcon,
instantly_v2: InstantlyIcon,
Expand All @@ -549,9 +557,11 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
jira: JiraIcon,
jira_service_management: JiraServiceManagementIcon,
jotform: JotformIcon,
jotform_v2: JotformIcon,
jsm: JiraServiceManagementIcon,
jupyter: JupyterIcon,
jupyter_v2: JupyterIcon,
jupyter_v3: JupyterIcon,
kalshi: KalshiIcon,
kalshi_v2: KalshiIcon,
ketch: KetchIcon,
Expand All @@ -573,6 +583,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
linq: LinqIcon,
linq_v2: LinqIcon,
logfire: LogfireIcon,
logfire_v2: LogfireIcon,
logrocket: LogRocketIcon,
logs: Library,
logs_v2: Library,
Expand All @@ -583,6 +594,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
mailchimp: MailchimpIcon,
mailchimp_v2: MailchimpIcon,
mailgun: MailgunIcon,
mailgun_v2: MailgunIcon,
managed_agent: ClaudeIcon,
manageengine_sdp: ManageEngineIcon,
manual_trigger: Play,
Expand Down Expand Up @@ -615,12 +627,14 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
netsuite: NetSuiteIcon,
neverbounce: NeverBounceIcon,
new_relic: NewRelicIcon,
new_relic_v2: NewRelicIcon,
note: NoteIcon,
notion: NotionIcon,
notion_v2: NotionIcon,
obsidian: ObsidianIcon,
oci: OracleIcon,
okta: OktaIcon,
okta_v2: OktaIcon,
onedrive: MicrosoftOneDriveIcon,
onepassword: OnePasswordIcon,
openai: OpenAIIcon,
Expand All @@ -641,6 +655,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
pipedrive: PipedriveIcon,
pitchbook: PitchBookIcon,
plane: PlaneIcon,
plane_v2: PlaneIcon,
planetscale: PlanetScaleIcon,
polymarket: PolymarketIcon,
postgresql: PostgresIcon,
Expand Down Expand Up @@ -743,6 +758,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
stt: STTIcon,
stt_v2: STTIcon,
supabase: SupabaseIcon,
supabase_v2: SupabaseIcon,
table: Table,
table_v2: Table,
tailscale: TailscaleIcon,
Expand All @@ -757,8 +773,10 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
textract_v3: TextractIcon,
thinking: BrainIcon,
thrive: ThriveIcon,
thrive_v2: ThriveIcon,
tiktok: TikTokIcon,
tinybird: TinybirdIcon,
tinybird_v2: TinybirdIcon,
tinyfish: TinyFishIcon,
translate: TranslateIcon,
trello: TrelloIcon,
Expand All @@ -767,10 +785,13 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
tts: TTSIcon,
twilio: TwilioIcon,
twilio_sms: TwilioIcon,
twilio_sms_v2: TwilioIcon,
twilio_voice: TwilioIcon,
twilio_voice_v2: TwilioIcon,
typeform: TypeformIcon,
typeform_v2: TypeformIcon,
upstash: UpstashIcon,
upstash_v2: UpstashIcon,
uptimerobot: UptimeRobotIcon,
uptimerobot_v2: UptimeRobotIcon,
vanta: VantaIcon,
Expand Down
Loading
Loading