Skip to content

Commit 104face

Browse files
committed
fix(release): redact secret-key fields, check operation-backed tools in the saved-credential contract, and drop SDK copy
1 parent 03bf275 commit 104face

7 files changed

Lines changed: 82 additions & 10 deletions

File tree

‎.github/CI.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ The `changes` job runs Turborepo 2.9.14's `affectedTasks` query before installin
44

55
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.
66

7-
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.
7+
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.
88

99
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.
1010

1111
| Change | Expensive checks selected |
1212
| --- | --- |
13-
| Independent SDK | That workspace's tests and type-check; no app build, services, or browsers |
13+
| Standalone package | That workspace's tests and type-check; no app build, services, or browsers |
1414
| App unit test | App test shards and test type check |
1515
| Integration test | Both provision paths; no HTTP/browser job merely for the test edit |
1616
| Workspace UI | Database integration, mobile, SEO and live Electron, plus app tests/build |

‎apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/constants.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,14 @@ export const RESOURCES_MENU: NavMenu = {
128128
items: [
129129
{
130130
title: 'Docs',
131-
description: 'Guides, SDKs, and API reference',
131+
description: 'Guides, CLI, and API reference',
132132
href: 'https://docs.sim.ai',
133133
external: true,
134134
preview: {
135135
kind: 'docs',
136136
eyebrow: 'Documentation',
137137
title: 'Build with Sim',
138-
details: ['Quickstart', 'SDKs', 'API reference'],
138+
details: ['Quickstart', 'CLI', 'API reference'],
139139
},
140140
},
141141
{

‎apps/sim/lib/core/security/redaction.test.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ describe('isSensitiveKey', () => {
3131
'exa_api_key',
3232
'secretAccessKey',
3333
'awsSecretAccessKey',
34+
'secretKey',
35+
'bedrockSecretKey',
36+
'deployKey',
3437
`${'a'.repeat(10000)}password`,
3538
])('treats %s as sensitive', (key) => {
3639
expect(isSensitiveKey(key)).toBe(true)
@@ -39,7 +42,8 @@ describe('isSensitiveKey', () => {
3942
it.concurrent.each([
4043
'tokenCount',
4144
'tokenizer',
42-
'secretKey',
45+
'secretKeyId',
46+
'deployKeyName',
4347
'passwordStrength',
4448
'authMethod',
4549
'name',

‎apps/sim/lib/core/security/redaction.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ const SENSITIVE_KEY_PATTERNS: RegExp[] = [
2929
/^.*api[_-]?key$/i,
3030
// AWS's `secretAccessKey` / `awsSecretAccessKey`, which no suffix above covers.
3131
/^.*secret[_-]?access[_-]?key$/i,
32+
// Credential fields named for the key itself: Clerk's `secretKey`, Bedrock's
33+
// `bedrockSecretKey`, and Convex's `deployKey`.
34+
/^.*secret[_-]?key$/i,
35+
/^.*deploy[_-]?key$/i,
3236
/^passphrase$/i,
3337
/^authorization$/i,
3438
/^proxy[_-]?authorization$/i,

‎apps/sim/scripts/saved-credential-contract.test.ts‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest'
22
import { hostSteeringParams } from '@/scripts/saved-credential-contract'
3-
import type { ToolConfig } from '@/tools/types'
3+
import type { ExecutableToolConfig, ToolConfig } from '@/tools/types'
44

55
const required = { type: 'string', required: true }
66
const optional = { type: 'string', required: false }
@@ -25,7 +25,39 @@ function requestTool(
2525
} as ToolConfig
2626
}
2727

28+
/** An operation-backed tool: its server handler, not a URL builder, picks the destination. */
29+
function operationTool(id: string, params: Record<string, { type: string }>): ExecutableToolConfig {
30+
return {
31+
id,
32+
name: 'Operation fixture',
33+
description: 'Fixture for an operation-backed tool',
34+
version: '1.0.0',
35+
params: Object.fromEntries(
36+
Object.entries(params).map(([name, param]) => [
37+
name,
38+
{ ...param, required: false, visibility: 'user-only' },
39+
])
40+
),
41+
operation: { input: (values: Record<string, unknown>) => values },
42+
} as ExecutableToolConfig
43+
}
44+
2845
describe('hostSteeringParams', () => {
46+
it('treats a destination-named param of an operation-backed tool as steering', () => {
47+
const tool = operationTool('probe_operation_fixture', {
48+
serverUrl: optional,
49+
query: optional,
50+
})
51+
52+
expect(hostSteeringParams(tool)).toEqual({ params: ['serverUrl'], probed: false })
53+
})
54+
55+
it('leaves a known payload param of an operation-backed tool unpinned', () => {
56+
const tool = operationTool('browser_use_run_task', { startUrl: optional, task: optional })
57+
58+
expect(hostSteeringParams(tool).params).toEqual([])
59+
})
60+
2961
it('finds an unvalidated host param that becomes the request origin', () => {
3062
const tool = requestTool(
3163
{ apiKey: required, indexHost: required, id: required },

‎apps/sim/scripts/saved-credential-contract.ts‎

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ export interface HostSteeringResult {
3030
const DESTINATION_PARAM =
3131
/(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
3232

33+
/**
34+
* Destination-named params of operation-backed tools that are request data, not the host
35+
* the credential goes to: each operation sends its token only to the vendor's fixed API.
36+
*/
37+
const OPERATION_PAYLOAD_PARAMS: Readonly<Record<string, readonly string[]>> = {
38+
// The page the remote browser opens; the token goes to api.browser-use.com.
39+
browser_use_run_task: ['startUrl'],
40+
// A guild id in the discord.com API path.
41+
discord_send_message: ['serverId'],
42+
// The audio Fireflies downloads; the token goes to api.fireflies.ai.
43+
fireflies_upload_audio: ['audioUrl'],
44+
// The status page's own domain, sent as a form field to the UptimeRobot API.
45+
uptimerobot_create_psp: ['customDomain'],
46+
uptimerobot_update_psp: ['customDomain'],
47+
}
48+
3349
const PROBE_HOST = 'hostprobe.example.net'
3450
const PROBE_VALUES = [PROBE_HOST, `https://${PROBE_HOST}`]
3551

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

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

87110
// The URL the shared transport would request, so the probe sees exactly what runs.
88111
const hostOf = (values: Record<string, unknown>): string | null => {
@@ -143,7 +166,11 @@ export function hostSteeringParams(tool: ExecutableToolConfig): HostSteeringResu
143166
}
144167
}
145168
if (!probed) {
146-
return { params: stringParams.filter((name) => DESTINATION_PARAM.test(name)), probed: false }
169+
const payload = new Set(OPERATION_PAYLOAD_PARAMS[tool.id] ?? [])
170+
return {
171+
params: stringParams.filter((name) => DESTINATION_PARAM.test(name) && !payload.has(name)),
172+
probed: false,
173+
}
147174
}
148175
return { params: [...steering], probed: true }
149176
}

‎apps/sim/tools/shared/saved-credentials.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ export const LAUNCHDARKLY_CREDENTIAL_OAUTH = tokenCredentialOAuth('launchdarkly'
210210
})
211211
export const LEMLIST_CREDENTIAL_OAUTH = tokenCredentialOAuth('lemlist', { apiKey: 'accessToken' })
212212
export const LINQ_CREDENTIAL_OAUTH = tokenCredentialOAuth('linq', { apiKey: 'accessToken' })
213+
/**
214+
* The stored `domain` is always the resolved base URL, which fills `host` and wins in
215+
* `getLogfireBaseUrl`. `region` is mapped too only so the credential, not the block or
216+
* the model, owns it; its value is never read while `host` is set.
217+
*/
213218
export const LOGFIRE_CREDENTIAL_OAUTH = tokenCredentialOAuth('logfire', {
214219
apiKey: 'accessToken',
215220
host: 'domain',

0 commit comments

Comments
 (0)