Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b094718
feat(selectors): execute dynamic selectors server-side
Aug 28, 2026
a343f97
fix(selectors): expose route verb to contract audit
Aug 28, 2026
db0ffc0
test(selectors): align migration expectations
Aug 28, 2026
855ce1f
fix(selectors): address review findings
Aug 28, 2026
a135092
Merge origin/staging into feat/unified-server-selector-execution
Aug 28, 2026
8075e62
Merge origin/staging into feat/unified-server-selector-execution
Aug 28, 2026
d328092
fix(selectors): harden exact reference handling
Aug 28, 2026
7e0caab
Merge remote-tracking branch 'origin/staging' into feat/unified-serve…
Aug 28, 2026
96df0b7
fix(selectors): address Cubic review findings
Aug 28, 2026
f78fc4e
fix(selectors): address Cubic rerun findings
Aug 28, 2026
316c2f0
fix(selectors): close final Cubic review gaps
Aug 28, 2026
589a326
Merge remote-tracking branch 'origin/staging' into feat/unified-serve…
Aug 28, 2026
5558e19
fix(selectors): close final review follow-ups
Aug 28, 2026
8720e07
Merge remote-tracking branch 'origin/staging' into feat/unified-serve…
Aug 28, 2026
d78e567
fix(imap): restore deployment binding on rollback
Aug 28, 2026
46e31a3
Merge remote-tracking branch 'origin/staging' into feat/unified-serve…
Aug 28, 2026
d8db8ac
Merge remote-tracking branch 'origin/staging' into feat/unified-serve…
Aug 28, 2026
a5ddb7f
fix(imap): scope webhook updates to active deployment
Aug 28, 2026
677f3ea
Merge remote-tracking branch 'origin/staging' into feat/unified-serve…
Aug 28, 2026
c4365b4
test(selectors): consolidate migration regression coverage
Aug 28, 2026
7fe8c90
Merge remote-tracking branch 'origin/staging' into feat/unified-serve…
Aug 28, 2026
845acaa
refactor(selectors): isolate unified selector contract
Aug 28, 2026
8a68b31
Merge remote-tracking branch 'origin/staging' into feat/unified-serve…
Aug 28, 2026
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
  •  
  •  
  •  
21 changes: 18 additions & 3 deletions .agents/skills/add-block/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ silently available.
id: 'channel',
title: 'Channel',
type: 'channel-selector',
selectorKey: '{service}.channels',
serviceId: '{service}',
placeholder: 'Select channel',
dependsOn: ['credential'],
Expand All @@ -217,6 +218,7 @@ silently available.
id: 'project',
title: 'Project',
type: 'project-selector',
selectorKey: '{service}.projects',
serviceId: '{service}',
dependsOn: ['credential'],
}
Expand All @@ -226,6 +228,7 @@ silently available.
id: 'file',
title: 'File',
type: 'file-selector',
selectorKey: '{service}.files',
serviceId: '{service}',
mimeType: 'application/pdf',
dependsOn: ['credential'],
Expand All @@ -236,6 +239,7 @@ silently available.
id: 'user',
title: 'User',
type: 'user-selector',
selectorKey: '{service}.users',
serviceId: '{service}',
dependsOn: ['credential'],
}
Expand Down Expand Up @@ -1065,7 +1069,11 @@ After creating the block, you MUST validate it against every tool it references:

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

**`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.
**`selectorKey` — every remote list.** Use the `add-selector` skill to add browser-safe metadata in
`apps/sim/lib/selectors/manifest.ts`. Attach `provider-server` selectors under
`apps/sim/lib/selectors/server/providers/` and `internal-server` selectors in
`apps/sim/lib/selectors/server/internal.ts`. Point the sub-block at that key. All remote selectors
execute through `selectors.execute`; never add a client provider module or selector-only fetch route.

```ts
{ id: 'triggerCredentials', type: 'oauth-input', canonicalParamId: 'oauthCredential', mode: 'trigger' },
Expand All @@ -1074,7 +1082,13 @@ A sub-block gets its choices from exactly one of two places. There is no third.
{ id: 'manualLabelIds', type: 'short-input', mode: 'trigger-advanced' },
```

`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.)
`canonicalParamId: 'oauthCredential'` on the credential sub-block is the line people forget. The
shared context builder projects only active `dependsOn` values and keys canonical pairs by their
canonical id. Exact environment references such as `{{GMAIL_CREDENTIAL_ID}}` stay unresolved in the
browser and are resolved only by the authorized server executor. The builder does not infer a
nonstandard credential id from `type: 'oauth-input'`; give it
`canonicalParamId: 'oauthCredential'`, or declare an explicit manifest `sourceFields` alias when a
legacy source id must be retained.

**`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.

Expand All @@ -1089,5 +1103,6 @@ options: (params) => {

Two rules the checks enforce:

- **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`).
- **Selector query keys contain no context values.** This includes credential IDs, raw secrets,
unresolved references, and hashes of those values; the shared facade uses an opaque local revision.
- **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.
37 changes: 25 additions & 12 deletions .agents/skills/add-connector/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,14 @@ Three field types are supported: `short-input`, `dropdown`, and `selector`.

## Dynamic Selectors (Canonical Pairs)

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`.
Use `type: 'selector'` for a key declared in the browser-safe selector manifest at
`apps/sim/lib/selectors/manifest.ts`. Remote selectors execute through the authorized
`selectors.execute` server operation and a server attachment; connectors never call providers or
resolve credentials in the browser. Apply the `add-selector` skill when the key does not exist.

Selectors are 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`.

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`.

Expand All @@ -217,7 +224,7 @@ configFields: [
id: 'baseSelector',
title: 'Base',
type: 'selector',
selectorKey: 'airtable.bases', // Must exist in hooks/selectors/registry.ts
selectorKey: 'airtable.bases', // Must exist in lib/selectors/manifest.ts
canonicalParamId: 'baseId',
mode: 'basic',
placeholder: 'Select a base',
Expand Down Expand Up @@ -260,7 +267,9 @@ configFields: [

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

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`.
When a selector depends on a plain `short-input` field (no canonical pair), `dependsOn` references
that field's `id` directly. Exact references such as `{{JIRA_DOMAIN}}` remain unresolved in the
browser and are resolved only after workspace authorization on the server.

```typescript
configFields: [
Expand Down Expand Up @@ -296,16 +305,16 @@ configFields: [

### How `dependsOn` maps to `SelectorContext`

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`):

```
oauthCredential, domain, teamId, projectId, knowledgeBaseId, planId,
siteId, collectionId, spreadsheetId, fileId, baseId, datasetId, serviceDeskId
```
The shared connector context builder projects only active dependencies. A canonical dependency uses
its active basic or advanced value under `canonicalParamId`; a non-canonical dependency uses its
field `id`. The resulting key must be a `SelectorContextKey` in
`apps/sim/lib/selectors/types.ts` and must be explicitly allowed by that selector's manifest entry.
The browser sends the connector's workspace scope, not the complete connector configuration.

### Available selector keys

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

| SelectorKey | Context Deps | Returns |
|-------------|-------------|---------|
Expand Down Expand Up @@ -607,9 +616,13 @@ export const CONNECTOR_META_REGISTRY: ConnectorMetaRegistry = {
- [ ] **Selector fields configured correctly (if applicable):**
- Every `type: 'selector'` field has a canonical pair (`short-input` or `dropdown` with same `canonicalParamId` and `mode: 'advanced'`)
- `required` is identical on both fields in each canonical pair
- `selectorKey` exists in `hooks/selectors/registry.ts`
- `selectorKey` exists in `apps/sim/lib/selectors/manifest.ts`
- `dependsOn` references selector field IDs (not `canonicalParamId`)
- Dependency `canonicalParamId` values exist in `SELECTOR_CONTEXT_FIELDS`
- Each projected dependency key is a `SelectorContextKey` allowed by the selector manifest
- Every remote key has one server attachment with credential provider binding and a reviewed
`fixed`, `credential-bound`, or `user-controlled` destination policy
- No connector selector adds a client provider module, browser token request, or selector-only
API route
- [ ] `listDocuments` handles pagination with metadata-based content hashes
- [ ] `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
- [ ] `contentDeferred: true` used if content requires per-doc API calls (file download, export, blocks fetch)
Expand Down
16 changes: 15 additions & 1 deletion .agents/skills/add-integration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,15 +270,24 @@ export const {Service}Block: BlockConfig = {
{
id: 'project',
type: 'project-selector',
selectorKey: '{service}.projects',
dependsOn: ['credential'],
},
{
id: 'issue',
type: 'file-selector',
selectorKey: '{service}.issues',
dependsOn: ['credential', 'project'],
}
```

Every remote `selectorKey` must use the unified server selector path. Apply the `add-selector` skill:
add browser-safe metadata to `apps/sim/lib/selectors/manifest.ts`, reuse or extract a server-only
provider listing primitive, and add a credential- and destination-bound server attachment. Do not
add code under `hooks/selectors/providers`, a provider-specific query key, browser token acquisition,
or a selector-only API route. The shared context builder sends only active `dependsOn` values and
preserves exact `{{KEY}}` environment references for server-side resolution.
Comment thread
BillLeoutsakosvl346 marked this conversation as resolved.

**Basic/Advanced mode for dual UX:**
```typescript
// Basic: Visual selector
Expand Down Expand Up @@ -630,6 +639,10 @@ If creating V2 versions (API-aligned outputs):
- [ ] Added credential field with `requiredScopes: getScopesForService('{service}')`
- [ ] Added conditional fields per operation
- [ ] Set up dependsOn for cascading selectors
- [ ] Every remote `selectorKey` exists in the shared manifest and has one server attachment with
trusted credential provider binding and a fixed, credential-bound, or explicitly reviewed
user-controlled destination policy
- [ ] No selector provider logic, credential resolution, or provider route call runs in the browser
- [ ] Configured tools.access with all tool IDs
- [ ] Configured tools.config.tool selector
- [ ] Defined outputs matching tool outputs
Expand Down Expand Up @@ -922,7 +935,8 @@ requiredScopes: getScopesForService('{service}'),
3. **Block type is snake_case** - `type: 'stripe'`, not `type: 'Stripe'`
4. **Alphabetical ordering** - Keep imports and registry entries alphabetically sorted
5. **Required can be conditional** - Use `required: { field: 'op', value: 'create' }` instead of always true
6. **DependsOn clears options** - When a dependency changes, selector options are refetched
6. **DependsOn clears options** - When an active dependency changes, the shared selector facade
refetches with an opaque query revision; dependency values and references never enter query keys
7. **Never pass Buffer directly to fetch** - Convert to `new Uint8Array(buffer)` for TypeScript compatibility
8. **Always handle legacy file params** - Keep hidden `fileContent` params for backwards compatibility
9. **Optional fields use advanced mode** - Set `mode: 'advanced'` on rarely-used optional fields
Expand Down
121 changes: 121 additions & 0 deletions .agents/skills/add-selector/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
name: add-selector
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.
argument-hint: <selector-key>
---

# Add Selector

Dynamic selectors expose option metadata while a workflow or connector is being configured. Every
remote selector executes through the authorized `selectors.execute` application operation; the
browser never resolves credentials or calls a provider directly.

## Read the shared boundary

Before editing, read:

- `apps/sim/lib/selectors/types.ts`
- `apps/sim/lib/selectors/manifest.ts`
- `apps/sim/lib/selectors/context.ts`
- `apps/sim/lib/selectors/server/types.ts`
- `apps/sim/lib/selectors/server/registry.ts`
- `apps/sim/hooks/queries/selectors.ts`

Then read the nearest existing selector attachment and the block, trigger, or connector declaration
that will consume the key.

## Classify the selector

- `provider-server`: contacts an external provider or uses provider credentials.
- `internal-server`: reads protected Sim data through an existing authorized application use case.
- `local`: pure browser-safe data with no protected data, credentials, references, or network I/O.

Add every key to the browser-safe manifest in `lib/selectors/manifest.ts`. `SelectorKey` derives from
that manifest; do not maintain a second union. Manifest entries contain data only: allowed context,
readiness, scope kinds, list/search/detail capabilities, and stale time. Do not import provider SDKs,
credentials, server helpers, or attachment functions into the manifest.

## Build context from active values

Declare `dependsOn` on the consuming sub-block or connector field. The shared context builder sends
only declared, active dependencies:

- Canonical basic/advanced pairs contribute the active value under their canonical key.
- Action and trigger modes contribute only fields active on that surface.
- Exact environment references such as `{{GMAIL_CREDENTIAL_ID}}` remain unresolved in the browser.
- Runtime block-output references are not selector context.
- Embedded environment interpolation such as `https://{{HOST}}/path` is unsupported.

Add a new `SelectorContextKey` only when the value is a real, reusable selector dependency. Allow it
explicitly on each relevant manifest entry. Never send a full block or connector configuration.

## Add the server attachment

For `provider-server`, add the service's attachment map under
`apps/sim/lib/selectors/server/providers/` and include it in the exhaustive server registry. For
`internal-server`, add the attachment in `apps/sim/lib/selectors/server/internal.ts`. Local keys use
the exhaustive browser-safe registry in `apps/sim/lib/selectors/client/local.ts` and never enter the
server registry. A provider attachment declares:

- Credential policy, including the exact context field and trusted `serviceIds`.
- Destination policy: `fixed`, `credential-bound`, or `user-controlled`.
- A list/detail adapter that explicitly projects `id`, `label`, and allowlisted scalar `meta`.

Stored credentials must pass actor-use, workspace, and provider/service binding checks. Do not trust
a provider, service, operation kind, origin, or module name supplied by the browser.

Choose the destination policy deliberately:

- `fixed`: provider origin is code-defined.
- `credential-bound`: origin/account/site comes from, or is verified against, the authorized
credential.
- `user-controlled`: the user selects the destination. Hidden use-only authentication requires an
explicit security policy; do not combine it with an arbitrary destination by default.

Reuse or extract a server-only provider listing primitive. If an existing provider route has
non-selector callers, keep the route as a thin caller of that primitive. If it is selector-only,
move the logic and remove the obsolete route and contract. Never import a route handler or make an
internal HTTP request from an attachment.

The attachment must return normalized selector results only. It must not return provider payloads,
resolved context, credential IDs, tokens, or secrets. Let the shared executor own scope
authorization, exact-reference resolution, credential authorization, error projection, output
sanitization, and abort propagation.

## Wire the UI declaration

Point the block, trigger, or connector field at `selectorKey` and declare its `dependsOn` fields.
Keep connector selector/manual canonical pairs and fork reconfiguration behavior intact. Static
`options` stay local and need no selector.

Do not add:

- A module under `hooks/selectors/providers` or any client provider fetcher.
- A provider-specific React Query key.
- A selector-specific OAuth-token request.
- A selector-only API route when the provider primitive can be called directly.

All server selectors use the shared POST contract and React Query facade. Query identities must stay
opaque and must not include context values, references, credential IDs, secrets, or their hashes.

## Focused validation

Follow nearby Vitest and route-test style. Do not add an authorization matrix for every ordinary
provider attachment; the shared executor tests own shared security behavior.

Add a focused adapter test when behavior is special, such as pagination, nontrivial destination
binding, provider-specific projection, or a raw-connection policy. For an ordinary fixed-origin OAuth
list, manifest/registry exhaustiveness plus an existing provider primitive test is usually enough.

Run the smallest relevant set, then:

```bash
bunx vitest run <focused selector tests>
bun run --cwd apps/sim type-check
bun run check:fork-dependent-coverage
bun run check:client-boundary
git diff --check
```

Confirm there is no browser-side provider call, every server key has one attachment, and every
returned option is explicitly projected.
5 changes: 5 additions & 0 deletions .agents/skills/add-selector/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
interface:
display_name: "Add Selector"
short_description: "Build a secure dynamic selector"
brand_color: "#2563EB"
default_prompt: "Use $add-selector to add or update a Sim dynamic selector through the unified server execution path."
Loading
Loading