Skip to content

Commit db1c7cc

Browse files
feat(core): isCloudManaged() is the one reader of OS_CLOUD_MANAGED, with the production fuse at first read and at kernel boot (#25306)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgX3AnP5C73ugJYnWGR6GGcn)_ Fixes #25294 Part of #25083 (the engine half, item 1; the services half in plugin-auth stays open on #25083). Clause-②: yes (widening) Metadata-protocol: none ## What this lands The engine half of ruling 6341690661 on #25083, **Q2 → D**: `OS_CLOUD_MANAGED=1` is the managed-mode switch on every boot, read by one accessor in `@objectstack/core` beside `isHostKernelComposition`, with the production fuse. ### The exports (`@objectstack/core`, `packages/core/src/stack-auth.ts`; names are final) - **`isCloudManaged(): boolean`** is synchronous and has no arguments. It returns `true` when `OS_CLOUD_MANAGED` is exactly `1` and `false` when the variable is unset or empty. Any other value (`true`, `0`, `yes`) throws an error naming the value. The answer is memoized for the process, and a refusal is memoized too: every later read throws the same error. - **`resetCloudManagedForTesting(): void`** forgets the memo, so the next read decides again from `process.env`. It is for tests only, and no boot calls it. Usage in a test: set `process.env.OS_CLOUD_MANAGED = '1'`, then call `resetCloudManagedForTesting()`. At the end, delete the variable and call `resetCloudManagedForTesting()` again. Under vitest `NODE_ENV` is `test`, so the fuse does not fire and the variable alone reads managed. - **`resolveControlPlaneApiKey(configured?: string): string`** returns the configured value if one is given, else `OS_CLOUD_API_KEY`, trimmed, or an empty string when neither is set. It is the one derivation of "a control plane is bound". ### The fuse A production boot that finds `OS_CLOUD_MANAGED=1` and `resolveControlPlaneApiKey() === ''` refuses. Production means not a development boot and not `NODE_ENV=test`. The development check reuses the existing reading, `isDevelopmentBoot()` (`stack-auth.ts`); `os serve --dev` sets `NODE_ENV=development` before anything is imported — only when `NODE_ENV` is unset, so `NODE_ENV=production os serve --dev` reads as a production boot and is refused the same way. The refusal message names `OS_CLOUD_MANAGED`, the missing `OS_CLOUD_API_KEY`, and both remedies. It fires: - at `isCloudManaged()`'s first read, and - at the top of `ObjectKernel.bootstrap()` and `LiteKernel.bootstrap()`, before any plugin's `init()`. Every kernel in the process reads the same memoized fact, including per-environment kernels that carry no reader. ⛔ It is not in `serve.ts`. ### One source for "control plane bound" cloud-connection depends on core, so core cannot read cloud-connection's `platformManaged`. The derivation therefore moves into core. `packages/cloud-connection/src/cloud-connection-plugin.ts` `start()` now computes `cloudApiKey` as `resolveControlPlaneApiKey(this.cfg.controlPlaneApiKey)`, which is the same precedence and trim as the inline expression it replaces. The seat widened the claim's file surface to that line in claim amendment 6352016263. The cross-lane declaration on the `domain:cli` seat post is comment 6352046857 on #6024. The diff touches two lines there: the import and the derivation. **The known cost (accepted by the seat):** a process-wide accessor cannot see any plugin's options, so the fuse reads only the environment arm. A production runtime that binds its control plane only through `CloudConnectionPlugin`'s `controlPlaneApiKey` option and also sets `OS_CLOUD_MANAGED=1` is refused, and the remedy is to set `OS_CLOUD_API_KEY`. This is the loud, safe direction. In this repository, no production construction passes `controlPlaneApiKey`. cloud-connection documents `OS_CLOUD_API_KEY` as "injected by the cloud into the runtimes it hosts". The cloud repository is private and was not read. ### Measured before the edit (origin/main `fb7501bc6`) - Current readers of `OS_CLOUD_MANAGED`: **0**. `git grep -n OS_CLOUD_MANAGED` has no hits outside changelogs, so nothing was folded onto the accessor. After this PR, `decideCloudManaged()` in `stack-auth.ts` is the only reader. - `platformManaged` was derived inline at `cloud-connection-plugin.ts:316/335`. - `--dev` is read once, by `isDevelopmentBoot()`. ## Tests (`packages/core/src/cloud-managed.test.ts`, head `569f28a5c`) - Pin 9, one case each: a `--dev` boot (`NODE_ENV=development`) with the variable runs managed; a production boot with the variable and `OS_CLOUD_API_KEY` runs managed; a production boot with the variable and no key refuses, and the test asserts the message's first sentence; without the variable nothing is managed. - Edges: `NODE_ENV=test` with the variable runs managed. An unset `NODE_ENV` counts as production. A blank key is no binding. An empty variable is off, and other values are refused. The memo holds, including a refusal, until reset. `resolveControlPlaneApiKey` precedence is pinned. - Kernel boot: for `ObjectKernel` and for `LiteKernel`, `bootstrap()` rejects before the probe plugin's `init()` runs, and boots once a key is set. - `pnpm --filter @objectstack/core test`: 111 files, 2649 tests passed. `typecheck` passed (tsc, examples, check:test-typecheck). - `pnpm --filter @objectstack/cloud-connection test`: 54 files, 670 tests passed. `typecheck` passed. **Ablation** (`scripts/ablation-replace.mjs`, which restores the file and verifies its blob equals HEAD): - Fuse removed (`if (false && production && …)`): 5 failed and 6 passed. The refusal pin and the four other refusal cases (unset `NODE_ENV`, memo, both kernels) go red. The three pin-9 run cases stay green. - `ObjectKernel.bootstrap()`'s call removed: only the ObjectKernel boot pin goes red (1 failed, 10 passed). The same holds for `LiteKernel`. ## Gates `dispatch-gates.mjs --commands` derived 69 families on this diff. All 69 were run locally on `569f28a5c`, and `--ran` reconciles them: 68 run, 1 NOT MEASURED. - `pnpm check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET: it needs the whole workspace built). It is left to CI. - `check-plugin-teardown-shape.mjs --self-test` exited 3 on the shallow clone. After `git fetch` of the fixture commit it was re-run and exited 0. The record carries the re-run. - The other 66 exited 0. ## Acceptance notes - `OS_CLOUD_MANAGED` keeps the maintainer's name: no `_ENABLED` suffix, the recorded exception to Prime Directive 9 (ruling 6341690661). - The strict value check (only `1` turns it on; other non-empty values are refused on every boot) is this PR's reading of the ruling's "`OS_CLOUD_MANAGED=1` is the switch". It is there so a mistyped switch cannot run a hosted runtime unmanaged. The reviewer may narrow it to the ruling's literal reading. - Not in scope: plugin-auth and every judgement site (the services half of #25083), `serve.ts`, the platform-objects predicates (#25295), and the spec features registry (#25292). --- _Generated by [Claude Code](https://claude.ai/code/session_01LCvB7huwegHimuqR4iTjxX)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 258f4e6 commit db1c7cc

6 files changed

Lines changed: 277 additions & 1 deletion

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/core': patch
3+
'@objectstack/cloud-connection': patch
4+
---
5+
6+
feat(core): `isCloudManaged()` is the one reader of `OS_CLOUD_MANAGED`, and a production boot that sets it without a bound control plane refuses to start
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
- **New exports of `@objectstack/core`:** `isCloudManaged()`, `resolveControlPlaneApiKey(configured?)` and `resetCloudManagedForTesting()`. `isCloudManaged()` is synchronous and answers `true` when `OS_CLOUD_MANAGED` is exactly `1`, `false` when it is unset or empty; any other value is refused with an error that names it. The answer is memoized for the process; tests that set the variable call `resetCloudManagedForTesting()` before and after.
12+
- **The fuse.** On a production boot (not `--dev`, which sets `NODE_ENV=development` only when `NODE_ENV` is unset, and not `NODE_ENV=test`; so `NODE_ENV=production os serve --dev` counts as a production boot and is refused the same way), `OS_CLOUD_MANAGED=1` with `OS_CLOUD_API_KEY` unset or empty refuses: `ObjectKernel.bootstrap()` and `LiteKernel.bootstrap()` reject before any plugin initialises, and `isCloudManaged()` throws the same error. The cloud sets both variables on the runtimes it hosts. A self-hosted deployment that has `OS_CLOUD_MANAGED=1` in a copied env file now fails to boot; remove the variable, which a self-hosted deployment never needs.
13+
- **One caveat:** the fuse reads the environment only. A runtime that binds its control plane solely through `CloudConnectionPlugin`'s `controlPlaneApiKey` option, and also sets `OS_CLOUD_MANAGED=1` in production, is refused. Set `OS_CLOUD_API_KEY` instead.
14+
- **`@objectstack/cloud-connection`:** the plugin resolves its control-plane credential through `resolveControlPlaneApiKey(controlPlaneApiKey)`, so the plugin's "platform-managed" answer and the fuse share one derivation. Its behaviour is unchanged: the option wins, then `OS_CLOUD_API_KEY`, trimmed.

‎packages/cloud-connection/src/cloud-connection-plugin.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ import {
141141
userAuthGateSourceOf,
142142
apiKeyLifecyclePolicySourceOf,
143143
classifyAdmissionTenancyPosture,
144+
resolveControlPlaneApiKey,
144145
type TenancyPostureSource,
145146
} from '@objectstack/core';
146147
import type { AuthSessionApi, IAuthService, IObjectQLEngine } from '@objectstack/spec/contracts';
@@ -313,7 +314,7 @@ export class CloudConnectionPlugin implements Plugin {
313314
// them between config load and kernel boot — preserves the
314315
// behavior of the original app-level wiring.
315316
const cloudUrl = (this.cfg.controlPlaneUrl ?? process.env.OS_CLOUD_URL ?? '').trim().replace(/\/+$/, '');
316-
const cloudApiKey = (this.cfg.controlPlaneApiKey ?? process.env.OS_CLOUD_API_KEY ?? '').trim();
317+
const cloudApiKey = resolveControlPlaneApiKey(this.cfg.controlPlaneApiKey);
317318
const deviceClientId = (this.cfg.deviceClientId ?? process.env.OS_CLI_CLIENT_ID ?? 'objectstack-cli').trim();
318319
const deviceScope = 'openid profile email';
319320

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* #25294 — `isCloudManaged()`, the one reader of `OS_CLOUD_MANAGED`, and its
5+
* production fuse (ruling 6341690661 on #25083, Q2 → D). Pin 9 as the ruling
6+
* reads it, one case each: a `--dev` boot with the variable runs managed; a
7+
* production boot with the variable and a bound control plane runs managed; a
8+
* production boot with the variable and no control plane refuses loudly;
9+
* without the variable nothing is managed. Then the fuse at both kernels'
10+
* `bootstrap()`, before any plugin initialises. The environment is driven
11+
* through `process.env` and restored; the memo is reset around every case.
12+
*/
13+
14+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
15+
import { ObjectKernel } from './kernel.js';
16+
import { LiteKernel } from './lite-kernel.js';
17+
import { isCloudManaged, resetCloudManagedForTesting, resolveControlPlaneApiKey } from './stack-auth.js';
18+
import type { Plugin } from './types.js';
19+
20+
const ENV_VARS = ['OS_CLOUD_MANAGED', 'OS_CLOUD_API_KEY', 'NODE_ENV'] as const;
21+
const saved = Object.fromEntries(ENV_VARS.map((k) => [k, process.env[k]]));
22+
beforeEach(() => {
23+
for (const k of ENV_VARS) delete process.env[k];
24+
resetCloudManagedForTesting();
25+
});
26+
afterEach(() => {
27+
for (const k of ENV_VARS) {
28+
if (saved[k] === undefined) delete process.env[k];
29+
else process.env[k] = saved[k];
30+
}
31+
resetCloudManagedForTesting();
32+
});
33+
34+
/** A production boot: `NODE_ENV` as `os serve` defaults it without `--dev`. */
35+
const production = () => { process.env.NODE_ENV = 'production'; };
36+
37+
describe('isCloudManaged — pin 9, as ruling 6341690661 (Q2 → D) reads it', () => {
38+
it('a `--dev` boot with the variable runs managed, with no control plane bound', () => {
39+
process.env.NODE_ENV = 'development'; // what `os serve --dev` sets
40+
process.env.OS_CLOUD_MANAGED = '1';
41+
expect(isCloudManaged()).toBe(true);
42+
});
43+
44+
it('a production boot with the variable and a bound control plane runs managed', () => {
45+
production();
46+
process.env.OS_CLOUD_MANAGED = '1';
47+
process.env.OS_CLOUD_API_KEY = 'svc-key';
48+
expect(isCloudManaged()).toBe(true);
49+
});
50+
51+
it('a production boot with the variable and no control plane refuses loudly, naming the variable and the binding', () => {
52+
production();
53+
process.env.OS_CLOUD_MANAGED = '1';
54+
expect(() => isCloudManaged()).toThrow(/^\[Kernel\] Refusing to start: OS_CLOUD_MANAGED=1 is set on a production boot with no bound control plane \(OS_CLOUD_API_KEY is unset or empty\)/);
55+
});
56+
57+
it('without the variable nothing is managed, on any boot', () => {
58+
production();
59+
expect(isCloudManaged()).toBe(false);
60+
resetCloudManagedForTesting();
61+
process.env.OS_CLOUD_API_KEY = 'svc-key';
62+
expect(isCloudManaged()).toBe(false);
63+
resetCloudManagedForTesting();
64+
process.env.NODE_ENV = 'development';
65+
expect(isCloudManaged()).toBe(false);
66+
});
67+
});
68+
69+
describe('isCloudManaged — the edges of the switch and of "production"', () => {
70+
it('NODE_ENV=test is not production: the variable alone runs managed (the services half sets it under vitest)', () => {
71+
process.env.NODE_ENV = 'test';
72+
process.env.OS_CLOUD_MANAGED = '1';
73+
expect(isCloudManaged()).toBe(true);
74+
});
75+
76+
it('an unset NODE_ENV is production (`os serve` defaults it so), and a blank key is no binding', () => {
77+
process.env.OS_CLOUD_MANAGED = '1';
78+
process.env.OS_CLOUD_API_KEY = ' ';
79+
expect(() => isCloudManaged()).toThrow(/no bound control plane/);
80+
});
81+
82+
it('an empty variable is off; any value but `1` is refused on every boot, naming the value', () => {
83+
process.env.NODE_ENV = 'development';
84+
process.env.OS_CLOUD_MANAGED = '';
85+
expect(isCloudManaged()).toBe(false);
86+
for (const value of ['true', '0', 'yes']) {
87+
resetCloudManagedForTesting();
88+
process.env.OS_CLOUD_MANAGED = value;
89+
expect(() => isCloudManaged()).toThrow(`OS_CLOUD_MANAGED is set to ${JSON.stringify(value)}`);
90+
}
91+
});
92+
93+
it('memoized for the process, the refusal included, until reset', () => {
94+
process.env.NODE_ENV = 'test';
95+
expect(isCloudManaged()).toBe(false);
96+
process.env.OS_CLOUD_MANAGED = '1';
97+
expect(isCloudManaged()).toBe(false);
98+
resetCloudManagedForTesting();
99+
expect(isCloudManaged()).toBe(true);
100+
101+
resetCloudManagedForTesting();
102+
production();
103+
expect(() => isCloudManaged()).toThrow(/no bound control plane/);
104+
process.env.OS_CLOUD_API_KEY = 'svc-key';
105+
expect(() => isCloudManaged()).toThrow(/no bound control plane/);
106+
resetCloudManagedForTesting();
107+
expect(isCloudManaged()).toBe(true);
108+
});
109+
});
110+
111+
describe('resolveControlPlaneApiKey — the one derivation of "a control plane is bound"', () => {
112+
it('the configured value wins, even empty; else OS_CLOUD_API_KEY; trimmed; `` when neither', () => {
113+
expect(resolveControlPlaneApiKey()).toBe('');
114+
process.env.OS_CLOUD_API_KEY = ' env-key ';
115+
expect(resolveControlPlaneApiKey()).toBe('env-key');
116+
expect(resolveControlPlaneApiKey(' cfg-key ')).toBe('cfg-key');
117+
expect(resolveControlPlaneApiKey('')).toBe('');
118+
});
119+
});
120+
121+
describe('the fuse at kernel boot — refused before any plugin initialises', () => {
122+
const probe = () => {
123+
const calls: string[] = [];
124+
const plugin: Plugin = {
125+
name: 'test.cloud-managed-probe',
126+
init: async () => { calls.push('init'); },
127+
start: async () => { calls.push('start'); },
128+
};
129+
return { calls, plugin };
130+
};
131+
132+
for (const [label, make] of [
133+
['ObjectKernel', () => new ObjectKernel({ logger: { level: 'error' }, gracefulShutdown: false, skipSystemValidation: true })],
134+
['LiteKernel', () => new LiteKernel({ logger: { level: 'error' } })],
135+
] as const) {
136+
it(`${label}: a production boot with the variable and no control plane refuses; with a binding it boots`, async () => {
137+
production();
138+
process.env.OS_CLOUD_MANAGED = '1';
139+
const refused = probe();
140+
const kernel = make();
141+
await kernel.use(refused.plugin);
142+
await expect(kernel.bootstrap()).rejects.toThrow(/no bound control plane/);
143+
expect(refused.calls).toEqual([]);
144+
145+
resetCloudManagedForTesting();
146+
process.env.OS_CLOUD_API_KEY = 'svc-key';
147+
const booted = probe();
148+
const second = make();
149+
await second.use(booted.plugin);
150+
await second.bootstrap();
151+
expect(booted.calls).toEqual(['init', 'start']);
152+
expect(isCloudManaged()).toBe(true);
153+
await second.shutdown();
154+
});
155+
}
156+
});

‎packages/core/src/kernel.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { dispatchHookIsolating, dispatchHookPropagating } from './hook-dispatch.js';
1717
import { registerPluginByName } from './plugin-registration.js';
1818
import { raceWithTimeout } from './timeout-guard.js';
19+
import { isCloudManaged } from './stack-auth.js';
1920

2021
/**
2122
* The kernel service a DEGRADED boot's conclusion is published on (#16630).
@@ -527,6 +528,12 @@ export class ObjectKernel {
527528
throw new Error('[Kernel] Kernel already bootstrapped');
528529
}
529530

531+
// The cloud-managed fuse (ruling 6341690661 on #25083, Q2 → D): a
532+
// production boot with `OS_CLOUD_MANAGED=1` and no bound control plane
533+
// refuses here, before any plugin initialises — every kernel in the
534+
// process reads the one memoized fact. See `isCloudManaged()`.
535+
isCloudManaged();
536+
530537
this.state = 'initializing';
531538
this.logger.info('Bootstrap started');
532539

‎packages/core/src/lite-kernel.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { LoggerConfig } from '@objectstack/spec/system';
66
import { ObjectKernelBase } from './kernel-base.js';
77
import { registerPluginByName } from './plugin-registration.js';
88
import { assertPluginContract } from './plugin-contract.js';
9+
import { isCloudManaged } from './stack-auth.js';
910

1011
/**
1112
* ObjectKernel - MiniKernel Architecture
@@ -87,6 +88,11 @@ export class LiteKernel extends ObjectKernelBase {
8788
async bootstrap(): Promise<void> {
8889
this.validateState('idle');
8990

91+
// The cloud-managed fuse, as in `ObjectKernel.bootstrap()`: every
92+
// kernel in the process reads the one memoized fact before any plugin
93+
// initialises. See `isCloudManaged()`.
94+
isCloudManaged();
95+
9096
this.state = 'initializing';
9197
this.logger.info('Bootstrap started');
9298

‎packages/core/src/stack-auth.ts‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { readEnvWithDeprecation } from '@objectstack/types';
4+
import { getEnv } from './utils/env.js';
45

56
/**
67
* The ONE rule that decides whether a boot composes the platform's auth family
@@ -118,6 +119,97 @@ export function isHostKernelComposition(plugins: readonly unknown[]): boolean {
118119
});
119120
}
120121

122+
/**
123+
* The control-plane credential this process is bound with: the configured
124+
* value when one is given, else `OS_CLOUD_API_KEY` (which the cloud injects
125+
* into the runtimes it hosts), trimmed; `''` when neither. The ONE derivation
126+
* of "a control plane is bound" — `CloudConnectionPlugin` (`@objectstack/cloud-connection`)
127+
* resolves its `platformManaged` through it with its `controlPlaneApiKey`
128+
* option, and {@link isCloudManaged}'s fuse reads it with none, because a
129+
* process-wide accessor sees no plugin's options. Ruling 6341690661 (#25083),
130+
* Q2 → D: the fuse reads the fact cloud-connection already computes, ⛔ never
131+
* a second spelling of it beside a reader.
132+
*/
133+
export function resolveControlPlaneApiKey(configured?: string): string {
134+
return (configured ?? getEnv('OS_CLOUD_API_KEY') ?? '').trim();
135+
}
136+
137+
/**
138+
* The memoized answer of {@link isCloudManaged} — its refusal included, so a
139+
* boot the fuse refused stays refused on every later read in the process.
140+
*/
141+
let cloudManagedVerdict: { readonly managed: boolean } | { readonly refusal: Error } | undefined;
142+
143+
/**
144+
* Is this process the cloud-managed runtime? `OS_CLOUD_MANAGED=1` is the
145+
* switch on EVERY boot, and this is its ONE reader (ruling 6341690661 on
146+
* #25083, Q2 → D): every judgement site — plugin-auth's sign-in surfaces, the
147+
* admin user doors, the audience posture, invitations, the just-in-time path —
148+
* calls it and nothing else, and every kernel in the process reads the same
149+
* fact, the per-environment kernels the cloud builds beside its host kernel
150+
* included. The name is the maintainer's: no `_ENABLED` suffix (the recorded
151+
* exception to Prime Directive 9), and not `isManaged()`, which already names
152+
* `idp_provisioned` identities and `managedBy` provenance in this tree.
153+
*
154+
* Exactly `1` turns it on; unset or empty leaves it off; any other value is
155+
* refused, so a mistyped switch cannot run a hosted runtime unmanaged.
156+
*
157+
* **The fuse.** A production boot — not a development boot
158+
* ({@link isDevelopmentBoot}, which `--dev` sets through `NODE_ENV`) and not
159+
* `NODE_ENV=test` — that finds the switch on with no bound control plane
160+
* ({@link resolveControlPlaneApiKey}) refuses: the cloud injects
161+
* `OS_CLOUD_API_KEY` into every runtime it hosts, so a runtime without it is
162+
* a self-hosted deployment carrying a copied env file, and it is refused
163+
* instead of silently narrowed. It fires at the first read here, and at
164+
* `ObjectKernel` / `LiteKernel` `bootstrap()`, which read it before any plugin
165+
* initialises — ⛔ never in `os serve`, which the hosted process never runs.
166+
* A control plane bound only through `CloudConnectionPlugin`'s
167+
* `controlPlaneApiKey` option is invisible to the fuse and is refused.
168+
*
169+
* Synchronous and memoized for the process; tests reset it with
170+
* {@link resetCloudManagedForTesting}.
171+
*/
172+
export function isCloudManaged(): boolean {
173+
cloudManagedVerdict ??= decideCloudManaged();
174+
if ('refusal' in cloudManagedVerdict) throw cloudManagedVerdict.refusal;
175+
return cloudManagedVerdict.managed;
176+
}
177+
178+
/**
179+
* Forget {@link isCloudManaged}'s memoized answer, so the next read decides
180+
* again from the environment. For tests that set `OS_CLOUD_MANAGED` and remove
181+
* it afterwards; ⛔ never called by a boot.
182+
*/
183+
export function resetCloudManagedForTesting(): void {
184+
cloudManagedVerdict = undefined;
185+
}
186+
187+
function decideCloudManaged(): { readonly managed: boolean } | { readonly refusal: Error } {
188+
const raw = getEnv('OS_CLOUD_MANAGED');
189+
if (raw === undefined) return { managed: false };
190+
if (raw !== '1') {
191+
return {
192+
refusal: new Error(
193+
`[Kernel] OS_CLOUD_MANAGED is set to ${JSON.stringify(raw)}. The managed-mode switch takes exactly 1, `
194+
+ 'or is left unset: set OS_CLOUD_MANAGED=1 on a cloud-hosted runtime, and remove it everywhere else.',
195+
),
196+
};
197+
}
198+
const production = !isDevelopmentBoot(undefined) && getEnv('NODE_ENV') !== 'test';
199+
if (production && resolveControlPlaneApiKey() === '') {
200+
return {
201+
refusal: new Error(
202+
'[Kernel] Refusing to start: OS_CLOUD_MANAGED=1 is set on a production boot with no bound control plane '
203+
+ '(OS_CLOUD_API_KEY is unset or empty). The cloud sets both on every runtime it hosts, so this runtime is '
204+
+ 'not cloud-hosted: remove OS_CLOUD_MANAGED (a self-hosted deployment never sets it), or set '
205+
+ "OS_CLOUD_API_KEY to the environment's control-plane service key. A key passed only as "
206+
+ "CloudConnectionPlugin's controlPlaneApiKey option does not count.",
207+
),
208+
};
209+
}
210+
return { managed: true };
211+
}
212+
121213
/**
122214
* The auth secret a development boot falls back to, so trying the login flow
123215
* locally does not need `OS_AUTH_SECRET`. Never used outside development.

0 commit comments

Comments
 (0)