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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ The `changes` job runs Turborepo 2.9.14's `affectedTasks` query before installin

A PR compares the checked-out GitHub merge commit with its first parent: the actual target revision tested by that merge. The PR event's base SHA can become stale when the target advances. Scoping requires a matching GitHub merge ref and SHA, exactly two merge parents, and a usable target parent; otherwise every check runs. This works with the existing depth-two checkout without fetching older ancestry. Main/staging pushes execute all checks; the parent `ci` workflow skips checks on `dev` pushes, while PRs targeting `dev` still run checks. A manual `checks` run executes all checks unless supplied a full base commit SHA to reproduce a scoped plan. Missing history, an empty diff, unknown files outside the workspace graph, root script/CI changes, or a planner error run every check. Missing selection outputs fail the aggregate gate. The gate also rejects an omitted dependency, any failed/cancelled job, and any skip that its explicit selection flag did not authorize.

Dedicated docs verification uses the same planner, comparing pushes with their previous commit and PRs with the tested merge's first parent. Site acceptance follows `docs#build` and its transitive package dependencies. The app's docs-consumer task depends on server runtime and that docs build: backend-only changes run the platform consumer against the docs app, while site changes also run corpus and browser checks. Independent SDK and unit-only edits skip those production jobs. The required `docs HTTP` gate verifies explicit site/platform selections before accepting any skip.
Dedicated docs verification uses the same planner, comparing pushes with their previous commit and PRs with the tested merge's first parent. Site acceptance follows `docs#build` and its transitive package dependencies. The app's docs-consumer task depends on server runtime and that docs build: backend-only changes run the platform consumer against the docs app, while site changes also run corpus and browser checks. Standalone package and unit-only edits skip those production jobs. The required `docs HTTP` gate verifies explicit site/platform selections before accepting any skip.

Docs production builds run once and publish an artifact to isolated HTTP, corpus, Chromium, and WebKit jobs. Source checks keep their own checkout because generator verification mutates inputs. Content caches use separate lint/source/build namespaces; generation validates every restore, repairs missing declarations, and removes obsolete modules. The Next build cache keeps its supported incremental behavior. Content and Next caches have size limits, and Next cache files expire after seven days; successful content eviction happens after all consumers finish.

| Change | Expensive checks selected |
| --- | --- |
| Independent SDK | That workspace's tests and type-check; no app build, services, or browsers |
| Standalone package | That workspace's tests and type-check; no app build, services, or browsers |
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
| App unit test | App test shards and test type check |
| Integration test | Both provision paths; no HTTP/browser job merely for the test edit |
| Workspace UI | Database integration, mobile, SEO and live Electron, plus app tests/build |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,14 @@ export const RESOURCES_MENU: NavMenu = {
items: [
{
title: 'Docs',
description: 'Guides, SDKs, and API reference',
description: 'Guides, CLI, and API reference',
href: 'https://docs.sim.ai',
external: true,
preview: {
kind: 'docs',
eyebrow: 'Documentation',
title: 'Build with Sim',
details: ['Quickstart', 'SDKs', 'API reference'],
details: ['Quickstart', 'CLI', 'API reference'],
},
},
{
Expand Down
6 changes: 5 additions & 1 deletion apps/sim/lib/core/security/redaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ describe('isSensitiveKey', () => {
'exa_api_key',
'secretAccessKey',
'awsSecretAccessKey',
'secretKey',
'bedrockSecretKey',
'deployKey',
`${'a'.repeat(10000)}password`,
])('treats %s as sensitive', (key) => {
expect(isSensitiveKey(key)).toBe(true)
Expand All @@ -39,7 +42,8 @@ describe('isSensitiveKey', () => {
it.concurrent.each([
'tokenCount',
'tokenizer',
'secretKey',
'secretKeyId',
'deployKeyName',
'passwordStrength',
'authMethod',
'name',
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/lib/core/security/redaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ const SENSITIVE_KEY_PATTERNS: RegExp[] = [
/^.*api[_-]?key$/i,
// AWS's `secretAccessKey` / `awsSecretAccessKey`, which no suffix above covers.
/^.*secret[_-]?access[_-]?key$/i,
// Credential fields named for the key itself: Clerk's `secretKey`, Bedrock's
// `bedrockSecretKey`, and Convex's `deployKey`.
/^.*secret[_-]?key$/i,
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
/^.*deploy[_-]?key$/i,
/^passphrase$/i,
/^authorization$/i,
/^proxy[_-]?authorization$/i,
Expand Down
34 changes: 33 additions & 1 deletion apps/sim/scripts/saved-credential-contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { hostSteeringParams } from '@/scripts/saved-credential-contract'
import type { ToolConfig } from '@/tools/types'
import type { ExecutableToolConfig, ToolConfig } from '@/tools/types'

const required = { type: 'string', required: true }
const optional = { type: 'string', required: false }
Expand All @@ -25,7 +25,39 @@ function requestTool(
} as ToolConfig
}

/** An operation-backed tool: its server handler, not a URL builder, picks the destination. */
function operationTool(id: string, params: Record<string, { type: string }>): ExecutableToolConfig {
return {
id,
name: 'Operation fixture',
description: 'Fixture for an operation-backed tool',
version: '1.0.0',
params: Object.fromEntries(
Object.entries(params).map(([name, param]) => [
name,
{ ...param, required: false, visibility: 'user-only' },
])
),
operation: { input: (values: Record<string, unknown>) => values },
} as ExecutableToolConfig
}

describe('hostSteeringParams', () => {
it('treats a destination-named param of an operation-backed tool as steering', () => {
const tool = operationTool('probe_operation_fixture', {
serverUrl: optional,
query: optional,
})

expect(hostSteeringParams(tool)).toEqual({ params: ['serverUrl'], probed: false })
})

it('leaves a known payload param of an operation-backed tool unpinned', () => {
const tool = operationTool('browser_use_run_task', { startUrl: optional, task: optional })

expect(hostSteeringParams(tool).params).toEqual([])
})

it('finds an unvalidated host param that becomes the request origin', () => {
const tool = requestTool(
{ apiKey: required, indexHost: required, id: required },
Expand Down
35 changes: 31 additions & 4 deletions apps/sim/scripts/saved-credential-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@ export interface HostSteeringResult {
const DESTINATION_PARAM =
/(host|hostname|domain|endpoint|url|uri|origin|server|region|instance|shop|account|site|org|tenant|workspace|team|subdomain|space|cluster|environment|location|project)(name|slug|id)?$/i

/**
* Destination-named params of operation-backed tools that are request data, not the host
* the credential goes to: each operation sends its token only to the vendor's fixed API.
*/
const OPERATION_PAYLOAD_PARAMS: Readonly<Record<string, readonly string[]>> = {
// The page the remote browser opens; the token goes to api.browser-use.com.
browser_use_run_task: ['startUrl'],
// A guild id in the discord.com API path.
discord_send_message: ['serverId'],
// The audio Fireflies downloads; the token goes to api.fireflies.ai.
fireflies_upload_audio: ['audioUrl'],
// The status page's own domain, sent as a form field to the UptimeRobot API.
uptimerobot_create_psp: ['customDomain'],
uptimerobot_update_psp: ['customDomain'],
}

const PROBE_HOST = 'hostprobe.example.net'
const PROBE_VALUES = [PROBE_HOST, `https://${PROBE_HOST}`]

Expand Down Expand Up @@ -78,11 +94,18 @@ function alternateValues(value: unknown): string[] {
}

export function hostSteeringParams(tool: ExecutableToolConfig): HostSteeringResult {
// An internal operation has no request URL to probe: its server handler picks the
// destination, and the AWS families' handlers reach only AWS endpoints.
if (isInternalToolConfig(tool)) return { params: [], probed: true }
const params: [string, ProbeableParam][] = Object.entries(tool.params ?? {})
const stringParams = params.filter(([, param]) => param.type === 'string').map(([name]) => name)
// An internal operation has no request URL to probe: its server handler picks the
// destination from its input, so a destination-named param counts as steering.
// AWS families resolve their credential in the operation and are skipped by the caller.
if (isInternalToolConfig(tool)) {
const payload = new Set(OPERATION_PAYLOAD_PARAMS[tool.id] ?? [])
return {
params: stringParams.filter((name) => DESTINATION_PARAM.test(name) && !payload.has(name)),
probed: false,
}
}

// The URL the shared transport would request, so the probe sees exactly what runs.
const hostOf = (values: Record<string, unknown>): string | null => {
Expand Down Expand Up @@ -143,7 +166,11 @@ export function hostSteeringParams(tool: ExecutableToolConfig): HostSteeringResu
}
}
if (!probed) {
return { params: stringParams.filter((name) => DESTINATION_PARAM.test(name)), probed: false }
const payload = new Set(OPERATION_PAYLOAD_PARAMS[tool.id] ?? [])
return {
params: stringParams.filter((name) => DESTINATION_PARAM.test(name) && !payload.has(name)),
probed: false,
}
}
return { params: [...steering], probed: true }
}
5 changes: 5 additions & 0 deletions apps/sim/tools/shared/saved-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ export const LAUNCHDARKLY_CREDENTIAL_OAUTH = tokenCredentialOAuth('launchdarkly'
})
export const LEMLIST_CREDENTIAL_OAUTH = tokenCredentialOAuth('lemlist', { apiKey: 'accessToken' })
export const LINQ_CREDENTIAL_OAUTH = tokenCredentialOAuth('linq', { apiKey: 'accessToken' })
/**
* The stored `domain` is always the resolved base URL, which fills `host` and wins in
* `getLogfireBaseUrl`. `region` is mapped too only so the credential, not the block or
* the model, owns it; its value is never read while `host` is set.
*/
export const LOGFIRE_CREDENTIAL_OAUTH = tokenCredentialOAuth('logfire', {
apiKey: 'accessToken',
host: 'domain',
Expand Down
Loading