d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import { getErrorMessage } from '@sim/utils/errors'
|
|
import { LRUCache } from 'lru-cache'
|
|
import { isCustomBlocksEligible } from '@/lib/workflows/custom-blocks/operations'
|
|
|
|
const logger = createLogger('CopilotEntitlements')
|
|
|
|
/**
|
|
* Cross-repo contract: the mothership (Go) matches these exact strings against
|
|
* its `core.Entitlement*` constants to gate agent surfaces.
|
|
*/
|
|
export const CUSTOM_BLOCKS_ENTITLEMENT = 'custom-blocks'
|
|
|
|
/**
|
|
* Workspace entitlements — plan/flag-gated org capabilities sent to the
|
|
* mothership as the chat payload's `entitlements` array. The Go side hides the
|
|
* matching tools, skills, and prompt sections when an entitlement is absent, so
|
|
* a non-entitled org's agents never hear of the feature.
|
|
*
|
|
* Adding an entitlement:
|
|
* 1. Add the kebab-case name and a fail-closed evaluator here. Every payload
|
|
* site (interactive chat, headless execute, inbox) picks it up automatically.
|
|
* 2. Go repo: add the matching `Entitlement*` constant in `internal/core` and
|
|
* gate surfaces declaratively — `RequiredEntitlement` on tool definitions,
|
|
* `entitlement:` frontmatter on skills, a conditional section in
|
|
* `BuildAgentEnvelope`, or a variant swap in `Capabilities`.
|
|
* 3. Keep enforcement in sim: the Go gating is advertisement-only (the payload
|
|
* is forgeable), so the sim-side tool handler must re-check the same
|
|
* predicate at execution time.
|
|
*/
|
|
const ENTITLEMENT_EVALUATORS: Record<
|
|
string,
|
|
(workspaceId: string, userId?: string) => Promise<boolean>
|
|
> = {
|
|
[CUSTOM_BLOCKS_ENTITLEMENT]: isCustomBlocksEligible,
|
|
}
|
|
|
|
const entitlementsCache = new LRUCache<string, Promise<string[]>>({
|
|
max: 500,
|
|
ttl: 5_000,
|
|
})
|
|
|
|
/**
|
|
* The entitlements to send to the mothership for a request in this workspace.
|
|
* Each evaluator fails closed (an error means the entitlement is absent).
|
|
* Cached briefly so the several per-message callers collapse to one evaluation.
|
|
*/
|
|
export function computeWorkspaceEntitlements(
|
|
workspaceId: string,
|
|
userId?: string
|
|
): Promise<string[]> {
|
|
const cacheKey = `${workspaceId}:${userId ?? ''}`
|
|
const cached = entitlementsCache.get(cacheKey)
|
|
if (cached) return cached
|
|
|
|
const promise = Promise.all(
|
|
Object.entries(ENTITLEMENT_EVALUATORS).map(async ([name, evaluate]) => {
|
|
try {
|
|
return (await evaluate(workspaceId, userId)) ? name : null
|
|
} catch (error) {
|
|
logger.warn('Entitlement evaluation failed; treating as absent', {
|
|
entitlement: name,
|
|
workspaceId,
|
|
error: getErrorMessage(error),
|
|
})
|
|
return null
|
|
}
|
|
})
|
|
).then((names) => names.filter((name): name is string => name !== null))
|
|
entitlementsCache.set(cacheKey, promise)
|
|
return promise
|
|
}
|