Skip to content

Commit b094718

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
feat(selectors): execute dynamic selectors server-side
1 parent 2795922 commit b094718

291 files changed

Lines changed: 10636 additions & 20858 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: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ silently available.
203203
id: 'channel',
204204
title: 'Channel',
205205
type: 'channel-selector',
206+
selectorKey: '{service}.channels',
206207
serviceId: '{service}',
207208
placeholder: 'Select channel',
208209
dependsOn: ['credential'],
@@ -213,6 +214,7 @@ silently available.
213214
id: 'project',
214215
title: 'Project',
215216
type: 'project-selector',
217+
selectorKey: '{service}.projects',
216218
serviceId: '{service}',
217219
dependsOn: ['credential'],
218220
}
@@ -222,6 +224,7 @@ silently available.
222224
id: 'file',
223225
title: 'File',
224226
type: 'file-selector',
227+
selectorKey: '{service}.files',
225228
serviceId: '{service}',
226229
mimeType: 'application/pdf',
227230
dependsOn: ['credential'],
@@ -232,6 +235,7 @@ silently available.
232235
id: 'user',
233236
title: 'User',
234237
type: 'user-selector',
238+
selectorKey: '{service}.users',
235239
serviceId: '{service}',
236240
dependsOn: ['credential'],
237241
}
@@ -1057,7 +1061,10 @@ After creating the block, you MUST validate it against every tool it references:
10571061

10581062
A sub-block gets its choices from exactly one of two places. There is no third.
10591063

1060-
**`selectorKey` — every remote list.** Register the list in `hooks/selectors/providers/<service>/selectors.ts`, add its key to `SelectorKey`, and point the sub-block at it. A selector is parameterized by an explicit `SelectorContext`, so the same definition serves the canvas, the workspace-fork sync modal, and anything added later.
1064+
**`selectorKey` — every remote list.** Use the `add-selector` skill to add browser-safe metadata in
1065+
`apps/sim/lib/selectors/manifest.ts` and a server attachment under
1066+
`apps/sim/lib/selectors/server/providers/`. Point the sub-block at that key. All remote selectors
1067+
execute through `selectors.execute`; never add a client provider module or selector-only fetch route.
10611068

10621069
```ts
10631070
{ id: 'triggerCredentials', type: 'oauth-input', canonicalParamId: 'oauthCredential', mode: 'trigger' },
@@ -1066,7 +1073,11 @@ A sub-block gets its choices from exactly one of two places. There is no third.
10661073
{ id: 'manualLabelIds', type: 'short-input', mode: 'trigger-advanced' },
10671074
```
10681075

1069-
`canonicalParamId: 'oauthCredential'` on the credential sub-block is the line people forget. `buildSelectorContextFromBlock` keys the context on a sub-block's CANONICAL id, so without it `context.oauthCredential` is never set and the picker looks unfixable without reading the store. (A credential field is also recognised by its `oauth-input` TYPE as a fallback, so a block whose shipped param is already named something else does not have to rename it.)
1076+
`canonicalParamId: 'oauthCredential'` on the credential sub-block is the line people forget. The
1077+
shared context builder projects only active `dependsOn` values and keys canonical pairs by their
1078+
canonical id. Exact environment references such as `{{GMAIL_CREDENTIAL_ID}}` stay unresolved in the
1079+
browser and are resolved only by the authorized server executor. A credential field is also
1080+
recognized by its `oauth-input` type as a compatibility fallback.
10701081

10711082
**`options` — everything else.** A static array, or a pure function of the block's own values for a list that narrows to a sibling's selection. No I/O.
10721083

@@ -1081,5 +1092,6 @@ options: (params) => {
10811092

10821093
Two rules the checks enforce:
10831094

1084-
- **A secret never enters a selector's `getQueryKey`.** A query key identifies a resource; a credential authorizes access to it. A credential *id* is fine; a typed password is not (see `imap.mailboxes`).
1095+
- **Selector query keys contain no context values.** This includes credential IDs, raw secrets,
1096+
unresolved references, and hashes of those values; the shared facade uses an opaque local revision.
10851097
- **A sub-block that `dependsOn` a credential / knowledge-base / table selector must be reconfigurable at fork-sync time** — a `selectorKey`, a canonical pair whose basic member is a selector, or a `short-input`/`long-input`. `bun run check:fork-dependent-coverage` fails otherwise, because a fork sync clears those fields on every push and an unofferable one can never be set anywhere that sticks.

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,14 @@ Three field types are supported: `short-input`, `dropdown`, and `selector`.
197197

198198
## Dynamic Selectors (Canonical Pairs)
199199

200-
Use `type: 'selector'` to fetch options dynamically from the existing selector registry (`hooks/selectors/registry.ts`). Selectors are always paired with a manual fallback input using the **canonical pair** pattern — a `selector` field (basic mode) and a `short-input` field (advanced mode) linked by `canonicalParamId`.
200+
Use `type: 'selector'` for a key declared in the browser-safe selector manifest at
201+
`apps/sim/lib/selectors/manifest.ts`. Remote selectors execute through the authorized
202+
`selectors.execute` server operation and a server attachment; connectors never call providers or
203+
resolve credentials in the browser. Apply the `add-selector` skill when the key does not exist.
204+
205+
Selectors are paired with a manual fallback input using the **canonical pair** pattern — a
206+
`selector` field (basic mode) and a `short-input` field (advanced mode) linked by
207+
`canonicalParamId`.
201208

202209
The user sees a toggle button (ArrowLeftRight) to switch between the selector dropdown and manual text input. On submit, the modal resolves each canonical pair to the active mode's value, keyed by `canonicalParamId`.
203210

@@ -217,7 +224,7 @@ configFields: [
217224
id: 'baseSelector',
218225
title: 'Base',
219226
type: 'selector',
220-
selectorKey: 'airtable.bases', // Must exist in hooks/selectors/registry.ts
227+
selectorKey: 'airtable.bases', // Must exist in lib/selectors/manifest.ts
221228
canonicalParamId: 'baseId',
222229
mode: 'basic',
223230
placeholder: 'Select a base',
@@ -260,7 +267,9 @@ configFields: [
260267

261268
### Selector with domain dependency (Jira/Confluence pattern)
262269

263-
When a selector depends on a plain `short-input` field (no canonical pair), `dependsOn` references that field's `id` directly. The `domain` field's value maps to `SelectorContext.domain` automatically via `SELECTOR_CONTEXT_FIELDS`.
270+
When a selector depends on a plain `short-input` field (no canonical pair), `dependsOn` references
271+
that field's `id` directly. Exact references such as `{{JIRA_DOMAIN}}` remain unresolved in the
272+
browser and are resolved only after workspace authorization on the server.
264273

265274
```typescript
266275
configFields: [
@@ -296,16 +305,16 @@ configFields: [
296305

297306
### How `dependsOn` maps to `SelectorContext`
298307

299-
The connector selector field builds a `SelectorContext` from dependency values. For the mapping to work, each dependency's `canonicalParamId` (or field `id` for non-canonical fields) must exist in `SELECTOR_CONTEXT_FIELDS` (`lib/workflows/subblocks/context.ts`):
300-
301-
```
302-
oauthCredential, domain, teamId, projectId, knowledgeBaseId, planId,
303-
siteId, collectionId, spreadsheetId, fileId, baseId, datasetId, serviceDeskId
304-
```
308+
The shared connector context builder projects only active dependencies. A canonical dependency uses
309+
its active basic or advanced value under `canonicalParamId`; a non-canonical dependency uses its
310+
field `id`. The resulting key must be a `SelectorContextKey` in
311+
`apps/sim/lib/selectors/types.ts` and must be explicitly allowed by that selector's manifest entry.
312+
The browser sends the connector's workspace scope, not the complete connector configuration.
305313

306314
### Available selector keys
307315

308-
Check `hooks/selectors/types.ts` for the full `SelectorKey` union. Common ones for connectors:
316+
Check `apps/sim/lib/selectors/manifest.ts` for the exhaustive selector keys. Common ones for
317+
connectors:
309318

310319
| SelectorKey | Context Deps | Returns |
311320
|-------------|-------------|---------|
@@ -607,9 +616,13 @@ export const CONNECTOR_META_REGISTRY: ConnectorMetaRegistry = {
607616
- [ ] **Selector fields configured correctly (if applicable):**
608617
- Every `type: 'selector'` field has a canonical pair (`short-input` or `dropdown` with same `canonicalParamId` and `mode: 'advanced'`)
609618
- `required` is identical on both fields in each canonical pair
610-
- `selectorKey` exists in `hooks/selectors/registry.ts`
619+
- `selectorKey` exists in `apps/sim/lib/selectors/manifest.ts`
611620
- `dependsOn` references selector field IDs (not `canonicalParamId`)
612-
- Dependency `canonicalParamId` values exist in `SELECTOR_CONTEXT_FIELDS`
621+
- Each projected dependency key is a `SelectorContextKey` allowed by the selector manifest
622+
- Every remote key has one server attachment with credential provider binding and a reviewed
623+
`fixed`, `credential-bound`, or `user-controlled` destination policy
624+
- No connector selector adds a client provider module, browser token request, or selector-only
625+
API route
613626
- [ ] `listDocuments` handles pagination with metadata-based content hashes
614627
- [ ] `syncContext.listingCapped = true` set whenever the listing is truncated (max-items cap or transient per-item error) — required to prevent the engine's deletion reconciliation from removing unseen documents
615628
- [ ] `contentDeferred: true` used if content requires per-doc API calls (file download, export, blocks fetch)

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,15 +262,24 @@ export const {Service}Block: BlockConfig = {
262262
{
263263
id: 'project',
264264
type: 'project-selector',
265+
selectorKey: '{service}.projects',
265266
dependsOn: ['credential'],
266267
},
267268
{
268269
id: 'issue',
269270
type: 'file-selector',
271+
selectorKey: '{service}.issues',
270272
dependsOn: ['credential', 'project'],
271273
}
272274
```
273275

276+
Every remote `selectorKey` must use the unified server selector path. Apply the `add-selector` skill:
277+
add browser-safe metadata to `apps/sim/lib/selectors/manifest.ts`, reuse or extract a server-only
278+
provider listing primitive, and add a credential- and destination-bound server attachment. Do not
279+
add code under `hooks/selectors/providers`, a provider-specific query key, browser token acquisition,
280+
or a selector-only API route. The shared context builder sends only active `dependsOn` values and
281+
preserves exact `{{KEY}}` environment references for server-side resolution.
282+
274283
**Basic/Advanced mode for dual UX:**
275284
```typescript
276285
// Basic: Visual selector
@@ -616,6 +625,10 @@ If creating V2 versions (API-aligned outputs):
616625
- [ ] Added credential field with `requiredScopes: getScopesForService('{service}')`
617626
- [ ] Added conditional fields per operation
618627
- [ ] Set up dependsOn for cascading selectors
628+
- [ ] Every remote `selectorKey` exists in the shared manifest and has one server attachment with
629+
trusted credential provider binding and a fixed, credential-bound, or explicitly reviewed
630+
user-controlled destination policy
631+
- [ ] No selector provider logic, credential resolution, or provider route call runs in the browser
619632
- [ ] Configured tools.access with all tool IDs
620633
- [ ] Configured tools.config.tool selector
621634
- [ ] Defined outputs matching tool outputs
@@ -1001,7 +1014,8 @@ requiredScopes: getScopesForService('{service}'),
10011014
3. **Block type is snake_case** - `type: 'stripe'`, not `type: 'Stripe'`
10021015
4. **Alphabetical ordering** - Keep imports and registry entries alphabetically sorted
10031016
5. **Required can be conditional** - Use `required: { field: 'op', value: 'create' }` instead of always true
1004-
6. **DependsOn clears options** - When a dependency changes, selector options are refetched
1017+
6. **DependsOn clears options** - When an active dependency changes, the shared selector facade
1018+
refetches with an opaque query revision; dependency values and references never enter query keys
10051019
7. **Never pass Buffer directly to fetch** - Convert to `new Uint8Array(buffer)` for TypeScript compatibility
10061020
8. **Always handle legacy file params** - Keep hidden `fileContent` params for backwards compatibility
10071021
9. **Optional fields use advanced mode** - Set `mode: 'advanced'` on rarely-used optional fields
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
---
2+
name: add-selector
3+
description: Add or update a Sim dynamic selector using the shared manifest, server attachment, and selectors.execute path. Use for provider-backed, internal, or local option lists referenced by block, trigger, or connector selectorKey fields.
4+
argument-hint: <selector-key>
5+
---
6+
7+
# Add Selector
8+
9+
Dynamic selectors expose option metadata while a workflow or connector is being configured. Every
10+
remote selector executes through the authorized `selectors.execute` application operation; the
11+
browser never resolves credentials or calls a provider directly.
12+
13+
## Read the shared boundary
14+
15+
Before editing, read:
16+
17+
- `apps/sim/lib/selectors/types.ts`
18+
- `apps/sim/lib/selectors/manifest.ts`
19+
- `apps/sim/lib/selectors/context.ts`
20+
- `apps/sim/lib/selectors/server/types.ts`
21+
- `apps/sim/lib/selectors/server/registry.ts`
22+
- `apps/sim/hooks/queries/selectors.ts`
23+
24+
Then read the nearest existing selector attachment and the block, trigger, or connector declaration
25+
that will consume the key.
26+
27+
## Classify the selector
28+
29+
- `provider-server`: contacts an external provider or uses provider credentials.
30+
- `internal-server`: reads protected Sim data through an existing authorized application use case.
31+
- `local`: pure browser-safe data with no protected data, credentials, references, or network I/O.
32+
33+
Add every key to the browser-safe manifest in `lib/selectors/manifest.ts`. `SelectorKey` derives from
34+
that manifest; do not maintain a second union. Manifest entries contain data only: allowed context,
35+
readiness, scope kinds, list/search/detail capabilities, and stale time. Do not import provider SDKs,
36+
credentials, server helpers, or attachment functions into the manifest.
37+
38+
## Build context from active values
39+
40+
Declare `dependsOn` on the consuming sub-block or connector field. The shared context builder sends
41+
only declared, active dependencies:
42+
43+
- Canonical basic/advanced pairs contribute the active value under their canonical key.
44+
- Action and trigger modes contribute only fields active on that surface.
45+
- Exact environment references such as `{{GMAIL_CREDENTIAL_ID}}` remain unresolved in the browser.
46+
- Runtime block-output references are not selector context.
47+
- Embedded environment interpolation such as `https://{{HOST}}/path` is unsupported.
48+
49+
Add a new `SelectorContextKey` only when the value is a real, reusable selector dependency. Allow it
50+
explicitly on each relevant manifest entry. Never send a full block or connector configuration.
51+
52+
## Add the server attachment
53+
54+
Add the service's attachment map under `apps/sim/lib/selectors/server/providers/` and include it in
55+
the exhaustive server registry. A provider attachment declares:
56+
57+
- Credential policy, including the exact context field and trusted `serviceIds`.
58+
- Destination policy: `fixed`, `credential-bound`, or `user-controlled`.
59+
- A list/detail adapter that explicitly projects `id`, `label`, and allowlisted scalar `meta`.
60+
61+
Stored credentials must pass actor-use, workspace, and provider/service binding checks. Do not trust
62+
a provider, service, operation kind, origin, or module name supplied by the browser.
63+
64+
Choose the destination policy deliberately:
65+
66+
- `fixed`: provider origin is code-defined.
67+
- `credential-bound`: origin/account/site comes from, or is verified against, the authorized
68+
credential.
69+
- `user-controlled`: the user selects the destination. Hidden use-only authentication requires an
70+
explicit security policy; do not combine it with an arbitrary destination by default.
71+
72+
Reuse or extract a server-only provider listing primitive. If an existing provider route has
73+
non-selector callers, keep the route as a thin caller of that primitive. If it is selector-only,
74+
move the logic and remove the obsolete route and contract. Never import a route handler or make an
75+
internal HTTP request from an attachment.
76+
77+
The attachment must return normalized selector results only. It must not return provider payloads,
78+
resolved context, credential IDs, tokens, or secrets. Let the shared executor own scope
79+
authorization, exact-reference resolution, credential authorization, error projection, output
80+
sanitization, and abort propagation.
81+
82+
## Wire the UI declaration
83+
84+
Point the block, trigger, or connector field at `selectorKey` and declare its `dependsOn` fields.
85+
Keep connector selector/manual canonical pairs and fork reconfiguration behavior intact. Static
86+
`options` stay local and need no selector.
87+
88+
Do not add:
89+
90+
- A module under `hooks/selectors/providers` or any client provider fetcher.
91+
- A provider-specific React Query key.
92+
- A selector-specific OAuth-token request.
93+
- A selector-only API route when the provider primitive can be called directly.
94+
95+
All server selectors use the shared POST contract and React Query facade. Query identities must stay
96+
opaque and must not include context values, references, credential IDs, secrets, or their hashes.
97+
98+
## Focused validation
99+
100+
Follow nearby Vitest and route-test style. Do not add an authorization matrix for every ordinary
101+
provider attachment; the shared executor tests own shared security behavior.
102+
103+
Add a focused adapter test when behavior is special, such as pagination, nontrivial destination
104+
binding, provider-specific projection, or a raw-connection policy. For an ordinary fixed-origin OAuth
105+
list, manifest/registry exhaustiveness plus an existing provider primitive test is usually enough.
106+
107+
Run the smallest relevant set, then:
108+
109+
```bash
110+
bunx vitest run <focused selector tests>
111+
bun run --cwd apps/sim type-check
112+
bun run check:fork-dependent-coverage
113+
bun run check:client-boundary
114+
git diff --check
115+
```
116+
117+
Confirm there is no browser-side provider call, every server key has one attachment, and every
118+
returned option is explicitly projected.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
interface:
2+
display_name: "Add Selector"
3+
short_description: "Build a secure dynamic selector"
4+
brand_color: "#2563EB"
5+
default_prompt: "Use $add-selector to add or update a Sim dynamic selector through the unified server execution path."

0 commit comments

Comments
 (0)