Skip to content
23 changes: 12 additions & 11 deletions docs/code-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ where Enterprise and admin gating apply again.
- **Per plan:** saved code tools are capped per plan — Community 3, Starter 5,
Team 20, Enterprise unlimited (same on Cloud and self-hosted; an Enterprise
contract can override the cap with the licence feature `maxCodeTools`). Inline
code (`code_execute` / `code_propose_tool`) is Team and above. A Cloud trial
with a card on file is treated as Team for code mode for the length of the
trial (inline code and the Team cap of 20), so a high-intent user can try
everything; a free, card-less trial stays tools-only with the cap of 3. A
self-hosted trial carries no billing and so is never promoted.
code (`code_execute` / `code_propose_tool`) is Team and above. Every Cloud trial
is treated as Team for code mode for the length of the trial (inline code and
the Team cap of 20), so a new workspace can try everything; only the run
quotas below differ for a trial without a card. A self-hosted trial is not
promoted.
- **Writes** happen only in an admin-approved code tool with *Allow write
tools* on; inline code and read-only tools can never write.
- **Audit:** every run is recorded (a `code_runs` row), and each tool call the
Expand Down Expand Up @@ -119,10 +119,11 @@ tool calls it may make, and limits on output size. A program that exceeds a
limit fails with a clear error; it never affects other runs or the backend.

On Cloud, where the runner is shared between tenants, two per-workspace quotas
apply on top of the per-run limits: a **monthly run cap** (trial 100, Starter
500, Team 5,000, Enterprise unlimited; an Enterprise contract can override it
with the licence feature `maxCodeRunsPerMonth`) and a **concurrency cap** on
simultaneous runs (trial/Starter 1, Team 3, Enterprise 10). Over a quota, a run
is refused with a clear message and nothing is executed. A card-backed trial
gets the Team quotas. Self-hosted is single-tenant and not metered by runs, so
apply on top of the per-run limits: a **monthly run cap** (trial without a card 500,
Starter 500, Team 5,000, Enterprise unlimited; an Enterprise contract can
override it with the licence feature `maxCodeRunsPerMonth`) and a **concurrency
cap** on simultaneous runs (trial without a card and Starter 1, Team 3,
Enterprise 10). Over a quota, a run is refused with a clear message and nothing
is executed. A trial with a card on file gets the Team quotas. Every Cloud trial
has the Team code-mode features: inline code and up to 20 saved code tools. Self-hosted is single-tenant and not metered by runs, so
neither quota applies there.
13 changes: 7 additions & 6 deletions docs/connectors/file-search.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,15 @@ AI clients, and search keeps working on the last index during a sync.

| | Trial (Cloud) | Starter | Team | Enterprise |
|---|---|---|---|---|
| File Search connectors | 1 | 1 | 3 | by contract (self-hosted: unlimited) |
| Indexed files, all connectors together | 500 | 500 | 2,000 | by contract (self-hosted: unlimited) |
| Largest file (Cloud) | 10 MB | 25 MB | 50 MB | 200 MB |
| File Search connectors | 3 | 1 | 3 | by contract (self-hosted: unlimited) |
| Indexed files, all connectors together | 2,000 | 500 | 2,000 | by contract (self-hosted: unlimited) |
| Largest file (Cloud) | 50 MB | 25 MB | 50 MB | 200 MB |
| Automatic sync | daily | daily | daily | down to every 15 minutes (self-hosted: 5) |
| Manual syncs per day | 1 | 1 | 3 | unlimited |
| Manual syncs per day | 3 | 1 | 3 | unlimited |
| OCR, folder permissions, workspace policy | – | – | – | ✓ |

Self-hosted: the self-hosted 30-day trial has the Team limits; file size is
The Cloud trial has the Team limits, so you can try everything before you
pick a plan. Self-hosted: the self-hosted 30-day trial has the Team limits too; file size is
set by `FILES_MAX_FILE_BYTES` (default 100 MB). Self-hosted Starter and Team
are no longer sold; existing Starter and Team keys keep the limits above, and
more comes with [Enterprise](https://anythingmcp.com/contact?plan=enterprise).
Expand Down Expand Up @@ -204,7 +205,7 @@ lets AI clients publish files they write.
deleted by the worker.
- Limits: Starter links last at most 24 hours, with 100 MB for the files of
all active links together and 10 MB per file; Team 7 days, 2 GB, 50 MB. The
Cloud trial has no File Share. On the Cloud, program files (`.exe`, `.msi`,
Cloud trial has the Team limits. On the Cloud, program files (`.exe`, `.msi`,
`.js`, …) cannot be shared.

## Permissions and governance
Expand Down
3 changes: 2 additions & 1 deletion docs/plans/file-search.md
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,8 @@ existing keys need no reissue.
| File Share | – | internal + public, ≤ 24 h | ≤ 7 days | no limit, internal links without expiry |
| OCR, folder permissions, org policy | – | – | – | ✓ |

The trial mirrors Starter (as it does for connectors) minus File Share.
The trial column is the original design. Since 2026-11-09 the Cloud trial has
the Team limits, File Share included (see `file-search-quotas.ts`).
Self-hosted Community shows File Search in the catalog with a lock and
**Start 30-day trial** / **Buy a licence**; the existing Business trial
(`EditionService.startTrial`, once per instance) unlocks it with Team limits.
Expand Down
36 changes: 19 additions & 17 deletions llms-full.txt

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

5 changes: 3 additions & 2 deletions packages/backend/src/file-search/file-search-quotas.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ describe('File Search quotas', () => {
expect(quotaForPlan('starter', { cloud: true })).toMatchObject({ maxConnectors: 1, maxFiles: 500, minIntervalMinutes: 1440, manualSyncsPerDay: 1 });
expect(quotaForPlan('team', { cloud: true })).toMatchObject({ maxConnectors: 3, maxFiles: 2000, minIntervalMinutes: 1440 });
expect(quotaForPlan('enterprise', { cloud: true })).toMatchObject({ minIntervalMinutes: 15, ocr: true, folderRules: true, policy: true });
expect(quotaForPlan('trial', { cloud: true })!.share).toBeNull();
expect(quotaForPlan('trial', { cloud: true })).toMatchObject({ plan: 'trial', maxConnectors: 3, maxFiles: 2000, maxFileBytes: 50 * 1024 * 1024, manualSyncsPerDay: 3, ocr: false });
expect(quotaForPlan('trial', { cloud: true })!.share).toMatchObject({ maxTtlHours: 7 * 24, maxStoredBytes: 2 * 1024 * 1024 * 1024 });
expect(quotaForPlan('starter', { cloud: true })!.share).toMatchObject({ maxTtlHours: 24, maxStoredBytes: 100 * 1024 * 1024, maxFileBytes: 10 * 1024 * 1024, publicLinks: true });
expect(quotaForPlan('business', { cloud: true })).toMatchObject({ maxFiles: 2000, plan: 'business' });
});
Expand All @@ -25,7 +26,7 @@ describe('File Search quotas', () => {

it('suggests the next plan and clamps the interval to the plan minimum', () => {
expect(nextPlan('starter', true)?.plan).toBe('team');
expect(nextPlan('trial', true)?.plan).toBe('team');
expect(nextPlan('trial', true)?.plan).toBe('enterprise');
expect(nextPlan('trial', false)?.plan).toBe('enterprise');
expect(nextPlan('enterprise', true)).toBeNull();
expect(clampInterval(60, quotaForPlan('team', { cloud: true })!)).toBe(1440);
Expand Down
22 changes: 6 additions & 16 deletions packages/backend/src/file-search/file-search-quotas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,6 @@ const CLOUD_SHARE_ENTERPRISE: FileShareQuota = {
};

const CLOUD: Record<string, FileSearchQuota> = {
trial: {
plan: 'trial',
maxConnectors: 1,
maxFiles: 500,
maxSourceBytes: 500 * MB,
maxTextChars: 25_000_000,
maxFileBytes: 10 * MB,
minIntervalMinutes: DAY_MIN,
manualSyncsPerDay: 1,
share: null,
ocr: false,
folderRules: false,
policy: false,
},
starter: {
plan: 'starter',
maxConnectors: 1,
Expand Down Expand Up @@ -99,6 +85,10 @@ const CLOUD: Record<string, FileSearchQuota> = {
},
};
CLOUD.business = { ...CLOUD.team, plan: 'business' };
// The Cloud trial has the Team limits, so a new workspace can try everything
// before it picks a plan (decided 2026-11-09). Before checkout, the paywall
// tells a workspace above Starter what Starter would not include.
CLOUD.trial = { ...CLOUD.team, share: { ...CLOUD_SHARE_TEAM }, plan: 'trial' };

function selfHosted(maxFileBytes: number): Record<string, FileSearchQuota> {
const share = (ttl: number | null, internalWithoutExpiry = false): FileShareQuota => ({
Expand Down Expand Up @@ -189,8 +179,8 @@ function applyOverrides(base: FileSearchQuota, overrides: unknown): FileSearchQu
/**
* The plan to suggest when a limit is reached (wizard and pause banner): the
* first plan after the current one that really includes more connectors or
* files. A Cloud trial (Starter limits) is pointed at Team, a self-hosted
* evaluation (Team limits) at Enterprise.
* files. A Cloud trial and a self-hosted evaluation (both with Team limits)
* are pointed at Enterprise.
*/
export function nextPlan(plan: string, cloud: boolean): { plan: string; quota: FileSearchQuota } | null {
const order = ['trial', 'starter', 'team', 'enterprise'];
Expand Down
51 changes: 40 additions & 11 deletions packages/backend/src/license/code-mode-gating.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
monthlyCodeRunCapForPlan,
concurrentCodeRunCapForPlan,
startOfUtcMonth,
codeRunQuotaPlan,
} from './code-run-quotas';

function guard({ cloud, license, codeToolCount }: { cloud: boolean; license: any; codeToolCount: number }) {
Expand Down Expand Up @@ -60,6 +61,10 @@ describe('code-mode-quotas', () => {
expect(effectiveCodeModePlan('trial', false)).toBe('trial');
expect(codeToolCapForPlan(effectiveCodeModePlan('trial', false))).toBe(3);
expect(inlineCodeAllowedForPlan(effectiveCodeModePlan('trial', false))).toBe(false);
// On Cloud every trial is Team for code mode, with or without a card.
expect(effectiveCodeModePlan('trial', false, true)).toBe('team');
expect(inlineCodeAllowedForPlan(effectiveCodeModePlan('trial', false, true))).toBe(true);
expect(effectiveCodeModePlan('starter', false, true)).toBe('starter');
// The card flag only lifts a trial, nothing else.
expect(effectiveCodeModePlan('starter', true)).toBe('starter');
expect(effectiveCodeModePlan('community', true)).toBe('community');
Expand All @@ -69,7 +74,7 @@ describe('code-mode-quotas', () => {

describe('code-run-quotas', () => {
it('caps monthly code runs per plan (null = unlimited)', () => {
expect(monthlyCodeRunCapForPlan('trial')).toBe(100);
expect(monthlyCodeRunCapForPlan('trial')).toBe(500);
expect(monthlyCodeRunCapForPlan('starter')).toBe(500);
expect(monthlyCodeRunCapForPlan('team')).toBe(5000);
expect(monthlyCodeRunCapForPlan('business')).toBe(5000);
Expand All @@ -93,20 +98,45 @@ describe('code-run-quotas', () => {
expect(concurrentCodeRunCapForPlan('mystery')).toBeNull();
});

it('runs a card trial on Team quotas and a trial without a card on the trial quotas', () => {
expect(codeRunQuotaPlan('trial', true)).toBe('team');
expect(codeRunQuotaPlan('trial', false)).toBe('trial');
expect(codeRunQuotaPlan('starter', true)).toBe('starter');
expect(codeRunQuotaPlan(null, false)).toBe('community');
expect(monthlyCodeRunCapForPlan(codeRunQuotaPlan('trial', false))).toBe(500);
expect(concurrentCodeRunCapForPlan(codeRunQuotaPlan('trial', false))).toBe(1);
expect(monthlyCodeRunCapForPlan(codeRunQuotaPlan('trial', true))).toBe(5000);
});

it('startOfUtcMonth is the first of the month at UTC midnight', () => {
const d = startOfUtcMonth(new Date('2026-11-17T09:30:00Z'));
expect(d.toISOString()).toBe('2026-11-01T00:00:00.000Z');
});
});

describe('LicenseGuardService.codeModePlanForOrg', () => {
it('resolves a card-backed trial to team, a free trial to trial', async () => {
describe('LicenseGuardService.codeModePlanForOrg / codeRunPlanForOrg', () => {
it('resolves every Cloud trial to team for features, a self-hosted trial to trial', async () => {
const cardTrial = { plan: 'trial', status: 'active', billing: { status: 'trialing' } };
await expect(guard({ cloud: true, license: cardTrial, codeToolCount: 0 }).codeModePlanForOrg('org1')).resolves.toBe('team');
await expect(guard({ cloud: true, license: { plan: 'trial', status: 'active' }, codeToolCount: 0 }).codeModePlanForOrg('org1')).resolves.toBe('trial');
await expect(guard({ cloud: true, license: { plan: 'trial', status: 'active' }, codeToolCount: 0 }).codeModePlanForOrg('org1')).resolves.toBe('team');
await expect(guard({ cloud: false, license: { plan: 'trial', status: 'active' }, codeToolCount: 0 }).codeModePlanForOrg('org1')).resolves.toBe('trial');
await expect(guard({ cloud: true, license: { plan: 'team' }, codeToolCount: 0 }).codeModePlanForOrg('org1')).resolves.toBe('team');
await expect(guard({ cloud: false, license: null, codeToolCount: 0 }).codeModePlanForOrg('org1')).resolves.toBe('community');
});

it('keeps the small run budget for a trial without a card', async () => {
const cardTrial = { plan: 'trial', status: 'active', billing: { status: 'trialing' } };
await expect(guard({ cloud: true, license: cardTrial, codeToolCount: 0 }).codeRunPlanForOrg('org1')).resolves.toBe('team');
await expect(guard({ cloud: true, license: { plan: 'trial', status: 'active' }, codeToolCount: 0 }).codeRunPlanForOrg('org1')).resolves.toBe('trial');
await expect(guard({ cloud: true, license: { plan: 'starter' }, codeToolCount: 0 }).codeRunPlanForOrg('org1')).resolves.toBe('starter');
});

it('gives a Cloud trial without a card inline code and the Team tool cap', async () => {
const free = { plan: 'trial', status: 'active', features: {} };
await expect(guard({ cloud: true, license: free, codeToolCount: 0 }).canUseInlineCode('org1')).resolves.toBe(true);
await expect(guard({ cloud: true, license: free, codeToolCount: 19 }).checkCanCreateCodeTool('org1')).resolves.toBeUndefined();
await expect(guard({ cloud: true, license: free, codeToolCount: 20 }).checkCanCreateCodeTool('org1')).rejects.toThrow(/20 code tools on the team plan/);
});
});

describe('LicenseGuardService.checkCanCreateCodeTool (per-plan cap, Cloud + self-hosted)', () => {
Expand Down Expand Up @@ -158,15 +188,14 @@ describe('LicenseGuardService.canUseInlineCode', () => {
await expect(guard({ cloud: false, license: null, codeToolCount: 0 }).canUseInlineCode('org1')).resolves.toBe(false);
});

it('is true for a card-backed trial (billing.status trialing), false for a free trial', async () => {
it('is true for every Cloud trial, with or without a card, and false for a self-hosted trial', async () => {
const cardTrial = { plan: 'trial', status: 'active', billing: { status: 'trialing' } };
await expect(guard({ cloud: true, license: cardTrial, codeToolCount: 0 }).canUseInlineCode('org1')).resolves.toBe(true);
// A free trial has no Stripe billing.
await expect(guard({ cloud: true, license: { plan: 'trial', status: 'active' }, codeToolCount: 0 }).canUseInlineCode('org1')).resolves.toBe(false);
// A trial whose subscription is not trialing (e.g. past_due) is not a live card trial.
await expect(guard({ cloud: true, license: { plan: 'trial', status: 'active' }, codeToolCount: 0 }).canUseInlineCode('org1')).resolves.toBe(true);
await expect(
guard({ cloud: true, license: { plan: 'trial', status: 'active', billing: { status: 'past_due' } }, codeToolCount: 0 }).canUseInlineCode('org1'),
).resolves.toBe(false);
).resolves.toBe(true);
await expect(guard({ cloud: false, license: { plan: 'trial', status: 'active' }, codeToolCount: 0 }).canUseInlineCode('org1')).resolves.toBe(false);
});
});

Expand All @@ -181,10 +210,10 @@ describe('LicenseGuardService.checkCanCreateCodeTool — card-backed trial cap',
).rejects.toThrow(/20 code tools on the team plan/);
});

it('a free trial still caps at 3', async () => {
it('a self-hosted trial still caps at 3', async () => {
const freeTrial = { plan: 'trial', status: 'active', features: {} };
await expect(
guard({ cloud: true, license: freeTrial, codeToolCount: 3 }).checkCanCreateCodeTool('org1'),
guard({ cloud: false, license: freeTrial, codeToolCount: 3 }).checkCanCreateCodeTool('org1'),
).rejects.toThrow(/3 code tools on the trial plan/);
});
});
Loading
Loading