Skip to content

Commit 2c57ff7

Browse files
committed
improvement(tools): route internal API calls through one declarative primitive
Calls to Sim's own API were identified by inspecting the resolved URL string for an /api/ prefix, in two separate transports. Both now take an internalRoute template: the route comes from a source literal, interpolated ids are encoded, and query params go through withQuery. Static config strings stay as they are.
1 parent e6485f5 commit 2c57ff7

152 files changed

Lines changed: 904 additions & 276 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-integration/SKILL.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -872,7 +872,12 @@ export const {service}UploadTool: ToolConfig<Params, Response> = {
872872
fileContent: { type: 'string', required: false, visibility: 'hidden' }, // Legacy
873873
},
874874
request: {
875-
url: '/api/tools/{service}/upload', // Internal route
875+
// Internal route. A static string is a source literal, so the transport trusts it. When the
876+
// path is dynamic, use `internalRoute` from '@/tools/internal-route' instead of a template
877+
// string — a builder's plain `/api/...` string is treated as external, because a caller- or
878+
// model-supplied param can produce one:
879+
// url: (params) => internalRoute`/api/tools/{service}/upload/${params.folderId}`
880+
url: '/api/tools/{service}/upload',
876881
method: 'POST',
877882
body: (params) => ({
878883
accessToken: params.accessToken,

.agents/skills/add-tools/SKILL.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,37 @@ export const {serviceName}{Action}Tool: ToolConfig<
145145
- Always explicitly set `required: true` or `required: false`
146146
- Optional params should have `required: false`
147147

148+
## Internal Routes (calling Sim's own API)
149+
150+
Most tools call a third-party service and `request.url` returns an absolute `https://...` URL. A
151+
tool that instead calls Sim's own API — a `/api/tools/{service}/{action}` proxy route, or a platform
152+
route like `/api/table/...` — must SAY SO, because the transport resolves those against the internal
153+
base URL and signs them with an internal token for the executing user.
154+
155+
That declaration comes from the tool's source, never from the resolved string: a `user-or-llm` param
156+
can make any tool emit `/api/...` (the HTTP Request tool passes its `url` through verbatim, and a
157+
self-hosted integration with a blank host param collapses `${host}/api/v2/x` to `/api/v2/x`).
158+
159+
```typescript
160+
import { internalRoute } from '@/tools/internal-route'
161+
162+
// ✓ Static route — a source literal no param can influence
163+
url: '/api/tools/{service}/{action}',
164+
165+
// ✓ Dynamic route — branded, and every interpolated id is percent-encoded for you
166+
url: (params) => internalRoute`/api/table/${params.tableId}/rows`,
167+
168+
// ✓ Query params via withQuery (accepts an object or URLSearchParams; skips undefined/null)
169+
url: (params) => internalRoute`/api/logs`.withQuery({ workspaceId, limit: params.limit }),
170+
171+
// ✗ Treated as EXTERNAL and will fail — a builder's plain string carries no provenance
172+
url: (params) => `/api/table/${params.tableId}/rows`,
173+
```
174+
175+
`internalRoute` throws on a path outside `/api/` and on a query string inside the template. Never
176+
write `encodeURIComponent` inside the template — the tag already encodes each `${...}`, so doing it
177+
yourself double-encodes the value.
178+
148179
## Resolved Secrets and Provenance Boundaries
149180

150181
- Leave ordinary external API inputs and third-party results unchanged. Add provenance handling only

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ For **every** tool file, check:
7171
- [ ] Tool `description` is a concise one-liner describing what it does
7272
- [ ] Tool `version` is set (`'1.0.0'` or `'2.0.0'` for V2)
7373

74+
### Request URL
75+
- [ ] A tool calling a third-party service returns an ABSOLUTE `https://...` URL
76+
- [ ] A tool calling Sim's own API declares it: a static `/api/...` string, or `internalRoute` from
77+
`@/tools/internal-route` when the path is dynamic (query params via `.withQuery({...})`)
78+
- [ ] No builder returns a bare `` `/api/...` `` template string — that is treated as external and
79+
will fail, because a `user-or-llm` param can produce the same string
80+
- [ ] No `encodeURIComponent` inside an `internalRoute` template (the tag already encodes, so this
81+
double-encodes the value)
82+
7483
### Params
7584
- [ ] All required API params are marked `required: true`
7685
- [ ] All optional API params are marked `required: false`

.claude/commands/add-integration.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -871,7 +871,12 @@ export const {service}UploadTool: ToolConfig<Params, Response> = {
871871
fileContent: { type: 'string', required: false, visibility: 'hidden' }, // Legacy
872872
},
873873
request: {
874-
url: '/api/tools/{service}/upload', // Internal route
874+
// Internal route. A static string is a source literal, so the transport trusts it. When the
875+
// path is dynamic, use `internalRoute` from '@/tools/internal-route' instead of a template
876+
// string — a builder's plain `/api/...` string is treated as external, because a caller- or
877+
// model-supplied param can produce one:
878+
// url: (params) => internalRoute`/api/tools/{service}/upload/${params.folderId}`
879+
url: '/api/tools/{service}/upload',
875880
method: 'POST',
876881
body: (params) => ({
877882
accessToken: params.accessToken,

.claude/commands/add-tools.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,37 @@ export const {serviceName}{Action}Tool: ToolConfig<
144144
- Always explicitly set `required: true` or `required: false`
145145
- Optional params should have `required: false`
146146

147+
## Internal Routes (calling Sim's own API)
148+
149+
Most tools call a third-party service and `request.url` returns an absolute `https://...` URL. A
150+
tool that instead calls Sim's own API — a `/api/tools/{service}/{action}` proxy route, or a platform
151+
route like `/api/table/...` — must SAY SO, because the transport resolves those against the internal
152+
base URL and signs them with an internal token for the executing user.
153+
154+
That declaration comes from the tool's source, never from the resolved string: a `user-or-llm` param
155+
can make any tool emit `/api/...` (the HTTP Request tool passes its `url` through verbatim, and a
156+
self-hosted integration with a blank host param collapses `${host}/api/v2/x` to `/api/v2/x`).
157+
158+
```typescript
159+
import { internalRoute } from '@/tools/internal-route'
160+
161+
// ✓ Static route — a source literal no param can influence
162+
url: '/api/tools/{service}/{action}',
163+
164+
// ✓ Dynamic route — branded, and every interpolated id is percent-encoded for you
165+
url: (params) => internalRoute`/api/table/${params.tableId}/rows`,
166+
167+
// ✓ Query params via withQuery (accepts an object or URLSearchParams; skips undefined/null)
168+
url: (params) => internalRoute`/api/logs`.withQuery({ workspaceId, limit: params.limit }),
169+
170+
// ✗ Treated as EXTERNAL and will fail — a builder's plain string carries no provenance
171+
url: (params) => `/api/table/${params.tableId}/rows`,
172+
```
173+
174+
`internalRoute` throws on a path outside `/api/` and on a query string inside the template. Never
175+
write `encodeURIComponent` inside the template — the tag already encodes each `${...}`, so doing it
176+
yourself double-encodes the value.
177+
147178
## Resolved Secrets and Provenance Boundaries
148179

149180
- Leave ordinary external API inputs and third-party results unchanged. Add provenance handling only

.claude/commands/validate-integration.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ For **every** tool file, check:
7070
- [ ] Tool `description` is a concise one-liner describing what it does
7171
- [ ] Tool `version` is set (`'1.0.0'` or `'2.0.0'` for V2)
7272

73+
### Request URL
74+
- [ ] A tool calling a third-party service returns an ABSOLUTE `https://...` URL
75+
- [ ] A tool calling Sim's own API declares it: a static `/api/...` string, or `internalRoute` from
76+
`@/tools/internal-route` when the path is dynamic (query params via `.withQuery({...})`)
77+
- [ ] No builder returns a bare `` `/api/...` `` template string — that is treated as external and
78+
will fail, because a `user-or-llm` param can produce the same string
79+
- [ ] No `encodeURIComponent` inside an `internalRoute` template (the tag already encodes, so this
80+
double-encodes the value)
81+
7382
### Params
7483
- [ ] All required API params are marked `required: true`
7584
- [ ] All optional API params are marked `required: false`

.claude/rules/sim-integrations.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The full authoring instructions — tool/block/icon/trigger scaffolding, SubBloc
1414
## Hard rules (don't get these wrong)
1515

1616
- Tool IDs are `snake_case` (`service_action`). Register tools in `tools/registry.ts`, blocks in `blocks/registry-maps.ts` (the `BLOCK_REGISTRY` config map + `BLOCK_META_REGISTRY` catalog-meta map, alphabetically — `blocks/registry.ts` holds only the accessor functions), triggers in `triggers/registry.ts`.
17+
- A tool that calls Sim's own API declares it: a static `request.url` string (`'/api/tools/{service}/{action}'`), or `` internalRoute`/api/table/${params.tableId}/rows` `` from `@/tools/internal-route` when the path is dynamic (query params via `.withQuery({...})`). The transport signs internal requests with the executing user's token, so that decision follows the tool's source, never the resolved string — a builder returning a bare `/api/...` string is treated as EXTERNAL, because a `user-or-llm` param can produce one. `internalRoute` encodes every `${...}`; never add `encodeURIComponent` inside the template.
1718
- Type coercions (`Number()`, etc.) belong in `tools.config.params` (runs at execution, after variable resolution) — never in `tools.config.tool` (runs at serialization; coercing there destroys dynamic `<Block.output>` references).
1819
- `canonicalParamId` must NOT match any subblock's `id`, must be unique **block-wide** (groups are keyed by canonical id across every subblock and hold exactly one `basicId`, so two operations that each need a pair need two different canonical ids), and all subblocks in a canonical group must share the same `required` status. The `inputs` section and the params function reference canonical IDs, not raw subblock IDs — the serializer deletes the subblock IDs and republishes the active member's value under the canonical ID.
1920
- A canonical pair carries ONE concept. For files that is upload (basic) + file reference (advanced), as in Gmail attachments (`blocks/blocks/gmail.ts`). Never overload the advanced side with alternate identifiers (URL, provider asset ID) — give those their own subblocks, mark mutually exclusive sources `required: false`, and enforce "exactly one" at execution.

.cursor/commands/add-integration.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -866,7 +866,12 @@ export const {service}UploadTool: ToolConfig<Params, Response> = {
866866
fileContent: { type: 'string', required: false, visibility: 'hidden' }, // Legacy
867867
},
868868
request: {
869-
url: '/api/tools/{service}/upload', // Internal route
869+
// Internal route. A static string is a source literal, so the transport trusts it. When the
870+
// path is dynamic, use `internalRoute` from '@/tools/internal-route' instead of a template
871+
// string — a builder's plain `/api/...` string is treated as external, because a caller- or
872+
// model-supplied param can produce one:
873+
// url: (params) => internalRoute`/api/tools/{service}/upload/${params.folderId}`
874+
url: '/api/tools/{service}/upload',
870875
method: 'POST',
871876
body: (params) => ({
872877
accessToken: params.accessToken,

.cursor/commands/add-tools.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,37 @@ export const {serviceName}{Action}Tool: ToolConfig<
139139
- Always explicitly set `required: true` or `required: false`
140140
- Optional params should have `required: false`
141141

142+
## Internal Routes (calling Sim's own API)
143+
144+
Most tools call a third-party service and `request.url` returns an absolute `https://...` URL. A
145+
tool that instead calls Sim's own API — a `/api/tools/{service}/{action}` proxy route, or a platform
146+
route like `/api/table/...` — must SAY SO, because the transport resolves those against the internal
147+
base URL and signs them with an internal token for the executing user.
148+
149+
That declaration comes from the tool's source, never from the resolved string: a `user-or-llm` param
150+
can make any tool emit `/api/...` (the HTTP Request tool passes its `url` through verbatim, and a
151+
self-hosted integration with a blank host param collapses `${host}/api/v2/x` to `/api/v2/x`).
152+
153+
```typescript
154+
import { internalRoute } from '@/tools/internal-route'
155+
156+
// ✓ Static route — a source literal no param can influence
157+
url: '/api/tools/{service}/{action}',
158+
159+
// ✓ Dynamic route — branded, and every interpolated id is percent-encoded for you
160+
url: (params) => internalRoute`/api/table/${params.tableId}/rows`,
161+
162+
// ✓ Query params via withQuery (accepts an object or URLSearchParams; skips undefined/null)
163+
url: (params) => internalRoute`/api/logs`.withQuery({ workspaceId, limit: params.limit }),
164+
165+
// ✗ Treated as EXTERNAL and will fail — a builder's plain string carries no provenance
166+
url: (params) => `/api/table/${params.tableId}/rows`,
167+
```
168+
169+
`internalRoute` throws on a path outside `/api/` and on a query string inside the template. Never
170+
write `encodeURIComponent` inside the template — the tag already encodes each `${...}`, so doing it
171+
yourself double-encodes the value.
172+
142173
## Resolved Secrets and Provenance Boundaries
143174

144175
- Leave ordinary external API inputs and third-party results unchanged. Add provenance handling only

.cursor/commands/validate-integration.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ For **every** tool file, check:
6565
- [ ] Tool `description` is a concise one-liner describing what it does
6666
- [ ] Tool `version` is set (`'1.0.0'` or `'2.0.0'` for V2)
6767

68+
### Request URL
69+
- [ ] A tool calling a third-party service returns an ABSOLUTE `https://...` URL
70+
- [ ] A tool calling Sim's own API declares it: a static `/api/...` string, or `internalRoute` from
71+
`@/tools/internal-route` when the path is dynamic (query params via `.withQuery({...})`)
72+
- [ ] No builder returns a bare `` `/api/...` `` template string — that is treated as external and
73+
will fail, because a `user-or-llm` param can produce the same string
74+
- [ ] No `encodeURIComponent` inside an `internalRoute` template (the tag already encodes, so this
75+
double-encodes the value)
76+
6877
### Params
6978
- [ ] All required API params are marked `required: true`
7079
- [ ] All optional API params are marked `required: false`

0 commit comments

Comments
 (0)