Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions apps/sim/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
/**
* Container entrypoint. Hydrates `process.env` from the runtime secret and drops Next's route
* cache when an earlier build wrote it, before loading the Next.js standalone server, so
* application modules that read env at import time see the full configuration. See
* Container entrypoint. Hydrates `process.env` from the runtime secret, resolves an AWS
* Marketplace Enterprise license into `ENTERPRISE_ENABLED` on Marketplace installs, and drops
* Next's route cache when an earlier build wrote it, before loading the Next.js standalone server,
* so application modules that read env at import time see the full configuration. See
* `@sim/runtime-secrets`.
*/
import { fileURLToPath } from 'node:url'
import { loadRuntimeSecrets } from '@sim/runtime-secrets'
import { applyAwsMarketplaceContainerLicense } from '@/lib/billing/aws-marketplace/container-license'
import { clearRouteCacheFromOtherBuild } from '@/lib/core/utils/route-cache'

await loadRuntimeSecrets()
await applyAwsMarketplaceContainerLicense()
await clearRouteCacheFromOtherBuild(fileURLToPath(new URL('./.next', import.meta.url)))
// `server.js` is the Next standalone build artifact, a sibling of this file in
// the image; it does not exist at type-check time, so the specifier is held in a
Expand Down
141 changes: 141 additions & 0 deletions apps/sim/lib/billing/aws-marketplace/container-license.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import {
type GrantedLicense,
LicenseManagerClient,
ListReceivedLicensesCommand,
} from '@aws-sdk/client-license-manager'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { awsRequestHandler } from '@/lib/core/network/proxy-agent.server'

/**
* Enterprise entitlement for Sim installed from the AWS Marketplace container listing.
*
* The listing sells an annual contract whose one dimension is `enterprise_license`. AWS Marketplace
* grants the buyer a license in AWS License Manager, and the running product reads it. While the
* license is active, the Enterprise suite is on (`ENTERPRISE_ENABLED`). Without one, Sim runs as the
* open-source core, so a lapsed renewal or an unreachable License Manager never locks a buyer out
* of their deployment.
*
* The check reads the license rather than checking a unit out of it. A contract usually covers the
* whole deployment with one unit, so checking it out from every replica would make them compete
* for it, and a unit left checked out (a failed check-in holds it for an hour) would read exactly
* like an expired license.
*
* Opt-in: nothing here runs unless {@link PRODUCT_SKU_ENV} is set, which only the Marketplace
* Helm profile does. Enterprise flags are read once at startup by both the server and the
* browser, so instead of flipping them live, a re-check that sees the answer change asks the
* process to shut down and the orchestrator starts it again in the right mode.
*/

const logger = createLogger('AwsMarketplaceContainerLicense')

/** Set by the Marketplace Helm profile to the listing's product ID (the License Manager SKU). */
const PRODUCT_SKU_ENV = 'AWS_MARKETPLACE_LICENSE_PRODUCT_SKU'

/**
* Optional override for where the license is read. License Manager replicates a granted license to
* other Regions, so the default is the pod's own Region, the same chain AWS's EKS walkthrough uses.
*/
const LICENSE_REGION_ENV = 'AWS_MARKETPLACE_LICENSE_REGION'

/** The issuer every AWS Marketplace license is signed by (a fixed value, not a placeholder). */
const MARKETPLACE_KEY_FINGERPRINT = 'aws:294406891311:AWS/Marketplace:issuer-fingerprint'

/** The listing's contract dimension API name. */
const ENTERPRISE_ENTITLEMENT = 'enterprise_license'

/** AWS recommends re-checking every 15 minutes to pick up renewals, upgrades, and cancellations. */
const RECHECK_INTERVAL_MS = 15 * 60 * 1000

const ENTERPRISE_FLAGS = ['ENTERPRISE_ENABLED', 'NEXT_PUBLIC_ENTERPRISE_ENABLED'] as const
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved

/** `unknown` is a failed check (network, permissions, throttling), never a verdict on the license. */
type LicenseState = 'entitled' | 'not-entitled' | 'unknown'

function createClient(): LicenseManagerClient {
const region =
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
process.env[LICENSE_REGION_ENV] ||
process.env.AWS_REGION ||
process.env.AWS_DEFAULT_REGION ||
'us-east-1'
return new LicenseManagerClient({
region,
requestHandler: awsRequestHandler({ connectionTimeout: 3_000, requestTimeout: 10_000 }),
})
}

/**
* An AWS Marketplace license this product's buyer holds is current when License Manager reports it
* `AVAILABLE`, its issuer is AWS Marketplace, and it carries the Enterprise entitlement. License
* Manager moves an ended, cancelled, or suspended license out of `AVAILABLE` itself.
*/
function isActiveEnterpriseLicense(license: GrantedLicense): boolean {
return (
license.Status === 'AVAILABLE' &&
license.Issuer?.KeyFingerprint === MARKETPLACE_KEY_FINGERPRINT &&
(license.Entitlements ?? []).some((entitlement) => entitlement.Name === ENTERPRISE_ENTITLEMENT)
)
}

async function checkLicense(
client: LicenseManagerClient,
productSku: string
): Promise<LicenseState> {
try {
let nextToken: string | undefined
do {
const page = await client.send(
new ListReceivedLicensesCommand({
Filters: [
{ Name: 'ProductSKU', Values: [productSku] },
{ Name: 'Fingerprint', Values: [MARKETPLACE_KEY_FINGERPRINT] },
],
NextToken: nextToken,
})
)
if ((page.Licenses ?? []).some(isActiveEnterpriseLicense)) return 'entitled'
nextToken = page.NextToken
} while (nextToken)
return 'not-entitled'
} catch (error) {
logger.warn('Could not check the AWS Marketplace Enterprise license', {
message: getErrorMessage(error, 'Unknown error'),
})
return 'unknown'
}
}

function setEnterpriseFlags(enabled: boolean): void {
for (const flag of ENTERPRISE_FLAGS) process.env[flag] = enabled ? 'true' : 'false'
}

/**
* Resolves the Enterprise license before the server loads, so every Enterprise gate reads the
* answer through `ENTERPRISE_ENABLED`, then keeps re-checking. Returns without doing anything on a
* deployment that is not a Marketplace install.
*/
export async function applyAwsMarketplaceContainerLicense(): Promise<void> {
const productSku = process.env[PRODUCT_SKU_ENV]
if (!productSku) return

const client = createClient()
const bootState = await checkLicense(client, productSku)
const enterpriseAtBoot = bootState === 'entitled'
setEnterpriseFlags(enterpriseAtBoot)
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
logger.info(
enterpriseAtBoot
? 'AWS Marketplace Enterprise license active; Enterprise features on'
: 'No active AWS Marketplace Enterprise license; running without Enterprise features',
{ state: bootState }
)

const recheck = setInterval(async () => {
const state = await checkLicense(client, productSku)
const entitledNow = state === 'entitled'
if (state === 'unknown' || entitledNow === enterpriseAtBoot) return
logger.info('AWS Marketplace Enterprise license changed; restarting to apply it', { state })
clearInterval(recheck)
process.kill(process.pid, 'SIGTERM')
}, RECHECK_INTERVAL_MS)
recheck.unref()
}
1 change: 1 addition & 0 deletions apps/sim/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"@aws-sdk/client-iam": "3.1162.0",
"@aws-sdk/client-identitystore": "3.1162.0",
"@aws-sdk/client-lambda": "3.1162.0",
"@aws-sdk/client-license-manager": "3.1162.0",
"@aws-sdk/client-marketplace-entitlement-service": "3.1162.0",
"@aws-sdk/client-marketplace-metering": "3.1162.0",
"@aws-sdk/client-organizations": "3.1162.0",
Expand Down
7 changes: 7 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion helm/sim/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: v2
name: sim
description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents
type: application
version: 1.11.14
version: 1.11.15
appVersion: "v0.8.26"
kubeVersion: ">=1.25.0-0"
home: https://sim.ai
Expand Down
10 changes: 10 additions & 0 deletions helm/sim/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ Pre-built values files for common scenarios live in [`helm/sim/examples/`](https
| `values-development.yaml` | Local dev / `kind` / `minikube`. Minimal resources, no TLS. |
| `values-production.yaml` | Generic production: HA, network policy, autoscaling, monitoring. |
| `values-aws.yaml` | EKS — EBS GP3 storage, ALB ingress, IRSA-friendly. |
| `values-aws-marketplace.yaml` | EKS from the AWS Marketplace listing — Sim's images only, RDS + ElastiCache, Enterprise from the Marketplace license. |
| `values-gcp.yaml` | GKE — Persistent Disk storage, GCP managed certs, Workload Identity. |
| `values-azure.yaml` | AKS — managed-csi storage, NGINX ingress, GPU node pools. |
| `values-external-db.yaml` | Production with a managed Postgres (RDS, Cloud SQL, Azure DB). |
Expand All @@ -285,6 +286,15 @@ helm install sim oci://ghcr.io/simstudioai/charts/sim \
--set postgresql.auth.password="$POSTGRES_PASSWORD"
```

### AWS Marketplace

The AWS Marketplace listing ships this chart with `values-aws-marketplace.yaml` as its defaults
and every image pointed at the AWS Marketplace registry. It needs your own Amazon RDS for
PostgreSQL 17 and Amazon ElastiCache: [`aws-marketplace-prerequisites.yaml`](https://github.com/simstudioai/sim/tree/main/helm/sim/examples/aws-marketplace-prerequisites.yaml)
is a CloudFormation template that creates both, plus the IAM role the chart's ServiceAccount
uses to check your Marketplace license in AWS License Manager. While that license is active the
Enterprise suite is on; without it Sim runs as the open-source core.

---

## Parameters
Expand Down
Loading
Loading