Skip to content

Commit 0f5110e

Browse files
committed
Merge remote-tracking branch 'origin/staging' into mship-file-improvements
2 parents 75e42ed + 29acfb1 commit 0f5110e

593 files changed

Lines changed: 114731 additions & 5340 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.

.claude/rules/sim-caching.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
---
2+
paths:
3+
- "apps/sim/lib/**/*.ts"
4+
- "apps/sim/providers/**/*.ts"
5+
- "apps/sim/executor/**/*.ts"
6+
- "apps/sim/tools/**/*.ts"
7+
---
8+
9+
# In-Process Caching
10+
11+
**Never hand-roll TTL arithmetic.** `lru-cache` is a direct dependency of `apps/sim` and owns
12+
expiry, the ceiling, and — through `fetchMethod` — request coalescing. A
13+
`Map` plus `Date.now() - entry.fetchedAt < TTL` re-implements all three, badly.
14+
15+
## First decide whether it is a cache at all
16+
17+
Most module-level `Map`s in this codebase are **not** caches, and forcing them into one is worse
18+
than leaving them alone.
19+
20+
| Shape | Key dies when | Right tool |
21+
| --- | --- | --- |
22+
| **Lifecycle map**`activeStreams`, `pendingChildRuns`, `memoryStreams`, `handlerRegistry` | the tracked thing ends, and the code deletes it there | plain `Map`. No TTL, no ceiling. |
23+
| **TTL cache** — a remote read keyed by tenant (org id, user id, workspace id) | time passes | `LRUCache` |
24+
25+
A lifecycle map's key space is unbounded and that is fine, because every key has a defined death.
26+
Adding a TTL to one introduces an expiry that races the lifecycle. Adding a ceiling silently drops
27+
live state.
28+
29+
## TTL caches: always set `max`
30+
31+
```ts
32+
const policyCache = new LRUCache<string, ResolvedSessionPolicy>({
33+
max: 20_000,
34+
ttl: SESSION_POLICY_CACHE_TTL_MS,
35+
})
36+
```
37+
38+
`ttl` alone does **not** bound memory. Without `ttlAutopurge` (itself expensive — one timer per
39+
entry) an expired entry lingers until something touches its key or the ceiling evicts it. `max` is
40+
what actually caps the process, which is why a tenant-keyed `Map` grew for the life of the process
41+
before this rule existed.
42+
43+
**The ceiling is a memory backstop, not an operating limit.** Exceeding it makes the LRU evict
44+
*inside* the TTL, so each miss becomes one more read — never a wrong answer, it degrades to exactly
45+
the pre-cache behavior, but it is a hit-rate cliff on whatever path the cache sits on. Entries are
46+
tens of bytes, so set the cap far above any plausible per-instance working set within the TTL
47+
window and let it stay a backstop.
48+
49+
**Reads test `!== undefined`, not truthiness**, whenever the value can be `false`, `0`, or `null`.
50+
`if (cached)` on a cached `false` re-queries on every single call, for exactly the tenants the
51+
cache exists to protect.
52+
53+
## Async read-through: prefer `fetchMethod`
54+
55+
`fetchMethod` + `cache.fetch(key)` gives TTL, coalescing (concurrent callers share one promise),
56+
and eviction-on-rejection (`noDeleteOnFetchRejection` defaults to `false`) in one primitive. Reach
57+
for it before composing anything yourself.
58+
59+
**The one reason to compose instead: a hung producer.** `fetchMethod` has no settle deadline, and
60+
the app pool sets no `statement_timeout` (`packages/db/db.ts` sets only `connect_timeout` /
61+
`idle_timeout`, neither of which bounds a query already in flight). Where a wedged read would hold
62+
every caller for the whole TTL, wrap `coalesceLocally` from `@/lib/concurrency/singleflight` around
63+
a read-through `LRUCache` instead — it evicts and rejects at its deadline. See
64+
`lib/api-key/byok-entitlement.ts`, and `lib/oauth/credential-service.ts` for the same shape.
65+
66+
Do **not** build a house wrapper over `lru-cache`. Call sites differ in ways a thin helper cannot
67+
hold (synchronous memoization with `updateAgeOnGet` in `providers/client-cache.ts`, per-entry TTLs
68+
in `lib/auth/security-policy.ts`), so a wrapper covering the common case just adds a fourth pattern.
69+
70+
## Cache the gate, never the credential
71+
72+
Entitlements, plans, and policies tolerate bounded staleness **in the safe direction** — a lapsed
73+
organization keeping its own provider key for another minute costs a little metering and charges
74+
nobody wrongly. Key material does not: revocation has to be immediate, so
75+
`getBYOKKey` reads key rows fresh on every call and caches only the entitlement around them.
76+
77+
An outage must not be cached as a negative answer. A resolver that maps a failed read to `false`
78+
makes an outage indistinguishable from a real lapse, so give it an `onError: 'throw'` option and
79+
write the cache only on the success path — see `resolveOrganizationPlan`.
80+
81+
**Where a human is waiting, read fresh.** Keep two entry points rather than one cached function:
82+
the settings surfaces and management use cases must not tell an organization that just upgraded
83+
that it still lacks a plan, while the execution path underneath can serve from cache
84+
(`isOrganizationBYOKEntitled` vs `isOrganizationBYOKEntitledCached`).
85+
86+
## React `cache()` does nothing in a worker
87+
88+
`cache()` is request-scoped. Workflows run in Trigger.dev workers, which have no React request
89+
scope, so a `cache()`-wrapped gate that looks free on a settings page is uncached and per-block on
90+
the execution path. Anything reached from the executor needs a real cache — see
91+
`.claude/rules/sim-architecture.md`'s app/worker runtime boundary.
92+
93+
## Invalidation
94+
95+
Add a per-key invalidator only when the code that mutates the value runs in the **same process**
96+
that reads it. `invalidateSessionPolicyCache` works because the route writing the policy is the one
97+
serving the reads. An entitlement change arriving on a Stripe webhook lands in one process while
98+
the readers are per-worker, so an invalidator there would imply an immediacy it cannot deliver —
99+
the TTL is the real mechanism, and the absence of an invalidator should say so.

CLAUDE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,22 @@ describe('my route', () => {
472472

473473
Use `@sim/testing` mocks/factories over local test data.
474474

475+
## Caching
476+
477+
Never hand-roll TTL arithmetic — a `Map` plus `Date.now() - fetchedAt < TTL` re-implements expiry,
478+
the ceiling, and coalescing badly. Use `lru-cache` (a direct dependency), and always set `max`:
479+
`ttl` alone does not bound memory, so a tenant-keyed cache without a ceiling grows for the life of
480+
the process. Prefer `fetchMethod` + `cache.fetch(key)` for async read-through — it gives TTL,
481+
coalescing, and eviction-on-rejection in one primitive — and compose `coalesceLocally` around an
482+
`LRUCache` only when a hung producer would otherwise wedge callers for the whole TTL.
483+
484+
First check the thing is a cache at all: a lifecycle map whose entry is deleted when the tracked
485+
thing ends (`activeStreams`, `pendingChildRuns`) is a plain `Map`, and giving it a TTL or a ceiling
486+
introduces an expiry that races the lifecycle. Cache the gate, never the credential — entitlements
487+
tolerate bounded staleness in the safe direction, key material must stay fresh so revocation is
488+
immediate. Full decision tree, sizing, `!== undefined` reads, and the invalidation rule are in
489+
`.claude/rules/sim-caching.md`.
490+
475491
## Utils Rules
476492

477493
- Never create `utils.ts` for single consumer - inline it

apps/docs/components/icons.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,28 @@ export function ChartBarIcon(props: SVGProps<SVGSVGElement>) {
563563
)
564564
}
565565

566+
export function HarmonicIcon(props: SVGProps<SVGSVGElement>) {
567+
return (
568+
<svg {...props} viewBox='0 1 26 30.1' fill='none' role='img' xmlns='http://www.w3.org/2000/svg'>
569+
{/** svg-path-precision-exception: Preserves Harmonic's supplied brand-mark coordinates. */}
570+
<path
571+
d='M6.49743 1.08252L12.9949 4.83381V12.3364L6.49743 16.0877L0 12.3364V4.83381L6.49743 1.08252Z'
572+
fill='#FE5D45'
573+
/>
574+
{/** svg-path-precision-exception: Preserves Harmonic's supplied brand-mark coordinates. */}
575+
<path
576+
d='M6.49743 16.0874L12.9949 19.8387V27.3413L6.49743 31.0926L0 27.3413V19.8387L6.49743 16.0874Z'
577+
fill='#FE5D45'
578+
/>
579+
{/** svg-path-precision-exception: Preserves Harmonic's supplied brand-mark coordinates. */}
580+
<path
581+
d='M19.5026 8.58496L26 12.3363V19.8388L19.5026 23.5901L13.0051 19.8388V12.3363L19.5026 8.58496Z'
582+
fill='#FE5D45'
583+
/>
584+
</svg>
585+
)
586+
}
587+
566588
export function HubspotIcon(props: SVGProps<SVGSVGElement>) {
567589
return (
568590
<svg
@@ -5129,6 +5151,30 @@ export function BasetenIcon(props: SVGProps<SVGSVGElement>) {
51295151
)
51305152
}
51315153

5154+
export function CohereIcon(props: SVGProps<SVGSVGElement>) {
5155+
return (
5156+
<svg {...props} height='1em' width='1em' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
5157+
<title>Cohere</title>
5158+
<path
5159+
d='M8.128 14.099c.592 0 1.77-.033 3.398-.703 1.897-.781 5.672-2.2 8.395-3.656 1.905-1.018 2.74-2.366 2.74-4.18A4.56 4.56 0 0018.1 1H7.549A6.55 6.55 0 001 7.55c0 3.617 2.745 6.549 7.128 6.549z'
5160+
clipRule='evenodd'
5161+
fill='#39594D'
5162+
fillRule='evenodd'
5163+
/>
5164+
<path
5165+
d='M9.912 18.61a4.387 4.387 0 012.705-4.052l3.323-1.38c3.361-1.394 7.06 1.076 7.06 4.715a5.104 5.104 0 01-5.105 5.104l-3.597-.001a4.386 4.386 0 01-4.386-4.387z'
5166+
clipRule='evenodd'
5167+
fill='#D18EE2'
5168+
fillRule='evenodd'
5169+
/>
5170+
<path
5171+
d='M4.776 14.962A3.775 3.775 0 001 18.738v.489a3.776 3.776 0 007.551 0v-.49a3.775 3.775 0 00-3.775-3.775z'
5172+
fill='#FF7759'
5173+
/>
5174+
</svg>
5175+
)
5176+
}
5177+
51325178
export function MondayIcon(props: SVGProps<SVGSVGElement>) {
51335179
return (
51345180
<svg
@@ -5606,6 +5652,19 @@ export function TrelloIcon(props: SVGProps<SVGSVGElement>) {
56065652
)
56075653
}
56085654

5655+
export function AffinityIcon(props: SVGProps<SVGSVGElement>) {
5656+
return (
5657+
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 22' fill='none'>
5658+
<path
5659+
fillRule='evenodd'
5660+
clipRule='evenodd'
5661+
d='M20.412 20.505C20.412 20.505 20.412 20.505 20.412 20.505C17.298 23.566 13.602 21.66 11.014 19.072C8.426 21.66 4.73 23.566 1.616 20.505C1.616 20.505 1.616 20.505 1.616 20.505C1.616 20.505 1.615 20.505 1.615 20.505C1.608 20.498 1.6 20.49 1.593 20.483C1.587 20.477 1.581 20.471 1.575 20.465C1.575 20.465 1.575 20.464 1.575 20.464C1.575 20.464 1.575 20.464 1.575 20.464C-1.486 17.35 0.42 13.654 3.008 11.066C0.42 8.478 -1.486 4.782 1.575 1.668C1.575 1.668 1.575 1.668 1.575 1.668C1.575 1.668 1.575 1.667 1.575 1.667C1.582 1.661 1.588 1.654 1.595 1.647C1.602 1.641 1.608 1.634 1.615 1.627C1.615 1.627 1.615 1.627 1.616 1.627C1.616 1.627 1.616 1.627 1.616 1.627C3.99 -0.706 7.381 -0.573 10.259 2.305L11.014 3.06L11.769 2.305C14.647 -0.573 18.038 -0.706 20.412 1.627C20.412 1.627 20.412 1.627 20.412 1.627C20.412 1.627 20.413 1.627 20.413 1.627C20.419 1.633 20.425 1.639 20.431 1.645C20.438 1.653 20.446 1.66 20.453 1.667C20.453 1.667 20.453 1.668 20.453 1.668C20.453 1.668 20.453 1.668 20.453 1.669C22.786 4.042 22.653 7.433 19.775 10.311L19.02 11.066C21.608 13.654 23.514 17.35 20.453 20.464C20.453 20.464 20.453 20.464 20.453 20.464C20.453 20.464 20.453 20.465 20.453 20.465C20.447 20.471 20.44 20.478 20.433 20.485C20.426 20.492 20.419 20.499 20.413 20.505C20.413 20.505 20.412 20.505 20.412 20.505ZM2.709 19.376C3.732 20.376 5.391 20.49 6.946 18.935L13.831 12.05C13.943 11.938 14.13 11.963 14.202 12.104C14.631 12.944 14.909 13.949 15.049 14.974C15.054 15.009 15.042 15.044 15.017 15.069L13.117 16.97V17.16L15.082 18.935C16.637 20.49 18.296 20.376 19.319 19.376C20.324 18.353 20.44 16.691 18.883 15.135L11.975 8.226C11.862 8.114 11.889 7.924 12.032 7.854C12.87 7.443 13.861 7.177 14.869 7.045C14.942 7.036 15.015 7.061 15.067 7.113L16.918 8.964L18.883 6.998C20.438 5.443 20.324 3.784 19.324 2.761C18.301 1.756 16.639 1.64 15.082 3.197L8.174 10.106C8.061 10.219 7.872 10.192 7.801 10.049C7.391 9.211 7.125 8.22 6.993 7.212C6.983 7.139 7.009 7.065 7.061 7.013L8.912 5.163L6.946 3.197C5.39 1.641 3.729 1.756 2.706 2.759C1.704 3.782 1.589 5.442 3.145 6.998L10.084 13.937C10.196 14.049 10.171 14.236 10.03 14.308C9.204 14.727 8.221 15.001 7.217 15.142C7.143 15.152 7.069 15.127 7.016 15.074L5.111 13.169L3.145 15.135C1.588 16.691 1.704 18.353 2.709 19.376Z'
5662+
fill='currentColor'
5663+
/>
5664+
</svg>
5665+
)
5666+
}
5667+
56095668
export function AttioIcon(props: SVGProps<SVGSVGElement>) {
56105669
return (
56115670
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60.9 50' fill='currentColor'>

apps/docs/components/ui/icon-mapping.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { ComponentType, SVGProps } from 'react'
66
import { Library, Rocket, Table } from '@sim/emcn/icons'
77
import {
88
A2AIcon,
9+
AffinityIcon,
910
AgentMailIcon,
1011
AgentPhoneIcon,
1112
AgiloftIcon,
@@ -112,6 +113,7 @@ import {
112113
GranolaIcon,
113114
GreenhouseIcon,
114115
GreptileIcon,
116+
HarmonicIcon,
115117
HexIcon,
116118
HubspotIcon,
117119
HuggingFaceIcon,
@@ -272,6 +274,7 @@ type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
272274

273275
export const blockTypeToIconMap: Record<string, IconComponent> = {
274276
a2a: A2AIcon,
277+
affinity: AffinityIcon,
275278
agentmail: AgentMailIcon,
276279
agentphone: AgentPhoneIcon,
277280
agiloft: AgiloftIcon,
@@ -393,6 +396,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
393396
granola: GranolaIcon,
394397
greenhouse: GreenhouseIcon,
395398
greptile: GreptileIcon,
399+
harmonic: HarmonicIcon,
396400
hex: HexIcon,
397401
hubspot: HubspotIcon,
398402
huggingface: HuggingFaceIcon,

apps/docs/content/docs/en/cli/authentication.mdx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ sim workflows list --workspace ws_other
5353
`sim login --workspace <id>` preselects a workspace in the picker, and
5454
re-logging into an existing profile preselects the one already configured.
5555

56+
To save another workspace without minting or copying another personal key, add
57+
a workspace profile:
58+
59+
```bash
60+
sim workspaces list
61+
sim profile add acme --workspace ws_acme
62+
sim --profile acme whoami
63+
```
64+
65+
The new profile stores `auth_profile = default` and its own workspace. Omit
66+
`--workspace` in an interactive terminal to choose from the workspaces the
67+
active key can access; scripts must provide the workspace ID explicitly. The
68+
picker is capped at 1,000 entries and asks for an explicit ID above that.
69+
5670
## Checking who you are
5771

5872
```bash
@@ -76,6 +90,12 @@ sim logout # remove the stored key
7690
sim logout --all # remove the profile entirely, including its settings
7791
```
7892

93+
A workspace profile that shares authentication cannot remove the shared key.
94+
Remove only that local profile with `sim logout --all --profile <name>`, or log
95+
out of the authentication profile named by the error message. Removing an
96+
authentication profile entirely is refused until its workspace profiles are
97+
removed, so it cannot leave dangling references.
98+
7999
<Callout type="warn">
80100
`sim logout` removes the key from disk but does **not** revoke it. Revoke keys in
81101
Sim under **Settings → API keys**.
@@ -116,9 +136,9 @@ jobs:
116136
SIM_WORKSPACE: ${{ vars.SIM_WORKSPACE }}
117137
```
118138
119-
## Several accounts at once
139+
## Several accounts and workspaces
120140
121-
Each profile holds one identity and one set of defaults:
141+
Use separate logins for separate identities or deployments:
122142
123143
```bash
124144
sim login --profile dev --endpoint http://localhost:3000
@@ -128,6 +148,16 @@ sim workflows list --profile dev
128148
sim workflows list --profile prod
129149
```
130150

151+
Use workspace profiles when one personal key should target several workspaces:
152+
153+
```bash
154+
sim profile add marketing --workspace ws_marketing
155+
sim profile add support --workspace ws_support
156+
157+
sim workflows list --profile marketing
158+
sim workflows list --profile support
159+
```
160+
131161
See [Configuration](/cli/configuration) for how profiles are stored and resolved.
132162

133163
## Self-hosted and non-production deployments

apps/docs/content/docs/en/cli/commands.mdx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ These apply to every command, and may be written before or after it.
3030

3131
| Group | Description |
3232
| --- | --- |
33+
| [`sim profiles`](/cli/profiles) | List profiles or add a workspace profile that shares a stored login |
3334
| [`sim audit-logs`](/cli/audit-logs) | Manage audit logs |
3435
| [`sim billing`](/cli/billing) | Manage billing |
3536
| [`sim credentials`](/cli/credentials) | Manage credentials |
@@ -94,14 +95,6 @@ sim whoami [options]
9495

9596
</CommandTable>
9697

97-
## List the profiles defined in the config and credentials files
98-
99-
```bash
100-
sim profiles
101-
```
102-
103-
Also available as `sim profile`.
104-
10598
## Set a profile's endpoint, default workspace, or output format
10699

107100
```bash

apps/docs/content/docs/en/cli/configuration.mdx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ for a single command.
99

1010
## Profiles
1111

12-
A profile is one identity plus one set of defaults, in the style of the AWS CLI.
13-
Select one with `-P`, `--profile`, or `SIM_PROFILE`:
12+
A profile selects one set of defaults, in the style of the AWS CLI. It normally
13+
uses its same-named stored identity, but a workspace profile can share another
14+
profile's identity through `auth_profile`. Select one with `-P`, `--profile`, or
15+
`SIM_PROFILE`:
1416

1517
```bash
1618
sim workflows list --profile dev
@@ -23,6 +25,12 @@ The profile is named `default` when you do not pick one.
2325
sim profiles # list them; * marks the active one
2426
```
2527

28+
Add a profile for another workspace without creating or copying an API key:
29+
30+
```bash
31+
sim profile add acme --workspace ws_acme
32+
```
33+
2634
## Setting defaults
2735

2836
```bash
@@ -51,7 +59,7 @@ Each setting resolves independently, and the first match wins:
5159
| --- | --- |
5260
| 1 | Command-line flag — `--endpoint`, `--workspace`, `--output` |
5361
| 2 | Environment — `SIM_ENDPOINT`, `SIM_API_KEY`, `SIM_WORKSPACE`, `SIM_OUTPUT` |
54-
| 3 | `~/.sim/config` and `~/.sim/credentials`, for the selected profile |
62+
| 3 | `~/.sim/config` for the selected profile and `~/.sim/credentials` for its `auth_profile`, when set |
5563
| 4 | Built-in default — `https://www.sim.ai` and `table` |
5664

5765
`sim whoami` prints the winning source for each setting:
@@ -74,6 +82,10 @@ output = table
7482
[profile dev]
7583
endpoint = http://localhost:3000
7684
workspace = ws_local
85+
86+
[profile acme]
87+
auth_profile = default
88+
workspace = ws_acme
7789
```
7890

7991
Keys live in `~/.sim/credentials`, written `0600`:
@@ -89,6 +101,10 @@ api_key = sim_…
89101
Section naming follows the AWS convention: `[profile dev]` in config, `[dev]` in
90102
credentials. The `default` profile is `[default]` in both.
91103

104+
`auth_profile` references one direct profile and shares only its endpoint and
105+
API key; workspace and output remain local. References cannot be chained, and a
106+
shared profile cannot also set its own endpoint or API key.
107+
92108
## Environment variables
93109

94110
| Variable | Effect |
@@ -121,6 +137,19 @@ sim configure --set-workspace ws_abc123
121137
export SIM_WORKSPACE=ws_abc123
122138
```
123139

140+
For a reusable selection, create a workspace profile backed by the current
141+
stored login:
142+
143+
```bash
144+
sim workspaces list
145+
sim profile add acme --workspace ws_acme
146+
sim --profile acme tables list
147+
```
148+
149+
When `--workspace` is omitted in a terminal, `profile add` presents an
150+
interactive picker, capped at 1,000 entries. It refuses environment-only keys
151+
and endpoint overrides because those values would disappear in another shell.
152+
124153
`sim billing status`, `sim billing logs`, and `sim audit-logs list` accept
125154
`--all-workspaces` to drop the filter instead. It cannot be combined with
126155
`--workspace`.

0 commit comments

Comments
 (0)