From f2693727784607145dc12a3a0b8a50f2a6a0c0f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 04:58:25 +0000 Subject: [PATCH 1/3] fix(runtime,metadata): stamp env_local as the default local environment id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finish the v5.0 `project` to `environment` rename at the two sites it never reached. `packages/cli/CHANGELOG.md` records "Default local env id: `proj_local` -> `env_local`" and `content/docs/deployment/cli.mdx` documents `env_local`, while `createStandaloneStack` and `MetadataPlugin` still stamped `proj_local` — declared != enforced on a published default. - `packages/runtime/src/standalone-stack.ts`: the boot fallback, plus the two in-file comments that narrate what this stack stamps (one of them also carried a stale line anchor, ~515 -> ~567). - `packages/metadata/src/plugin.ts`: the placeholder that fills the `EnvironmentArtifactSchema` validation envelope for a bare definition. Adds the pin the value never had: the id is asserted where it LANDS, on the `MetadataPlugin` and `ObjectQLPlugin` instances the stack composes, together with the precedence above it (`cfg.environmentId` > `OS_ENVIRONMENT_ID` > default). `cloud-connection`'s dual accept and `package-state-store`'s separate `'default'` sanitiser fallback are deliberately untouched. Part of #13366 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --- packages/metadata/src/plugin.ts | 2 +- ...alone-stack-default-environment-id.test.ts | 111 ++++++++++++++++++ packages/runtime/src/standalone-stack.ts | 8 +- 3 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 packages/runtime/src/standalone-stack-default-environment-id.test.ts diff --git a/packages/metadata/src/plugin.ts b/packages/metadata/src/plugin.ts index 07f30140b1..84af55bd2c 100644 --- a/packages/metadata/src/plugin.ts +++ b/packages/metadata/src/plugin.ts @@ -915,7 +915,7 @@ export class MetadataPlugin implements Plugin { const def = ObjectStackDefinitionSchema.parse(this._convertArtifactForward(ctx, obj, label)); const canonical = JSON.stringify(def, Object.keys(def).sort()); const checksum = createHash('sha256').update(canonical).digest('hex'); - const environmentId = this.options.environmentId ?? 'proj_local'; + const environmentId = this.options.environmentId ?? 'env_local'; EnvironmentArtifactSchema.parse({ schemaVersion: '0.1', environmentId, diff --git a/packages/runtime/src/standalone-stack-default-environment-id.test.ts b/packages/runtime/src/standalone-stack-default-environment-id.test.ts new file mode 100644 index 0000000000..7ee57159d3 --- /dev/null +++ b/packages/runtime/src/standalone-stack-default-environment-id.test.ts @@ -0,0 +1,111 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// [#13366] The default environment id a standalone boot stamps, pinned at the +// place it is OBSERVABLE: the two plugins `createStandaloneStack` hands it to. +// +// Why this file exists at all. The v5.0 `project` to `environment` rename +// shipped the CLI default `env_local` — `packages/cli/CHANGELOG.md` records +// "Default local env id: `proj_local` -> `env_local`" and +// `content/docs/deployment/cli.mdx` documents `env_local` — but the runtime's +// own fallback kept stamping `proj_local`. Nothing pinned it, in either +// spelling, so `declared != enforced` held on a published default for a whole +// major line without one test going red. That is the gap this closes: the +// literal now has an assertion attached to the code path that emits it. +// +// It reads the id off `result.plugins` rather than off a copy of the constant, +// because the value is only interesting where it LANDS. `MetadataPlugin` takes +// it as `options.environmentId` and `ObjectQLPlugin` as a row-scope key; a +// pin that re-declared the string would stay green through a change that +// stopped passing it to either. +// +// ⛔ These cases must NOT be read as "the CLI default". `os dev` / `os start` +// export `OS_ENVIRONMENT_ID` into the child boot, so a CLI-spawned kernel never +// reaches this fallback — the CLI's own default is pinned separately (the +// `runtime.env_local.json` publication tests in packages/cli). What this file +// owns is the DIRECT-EMBEDDER path: `createStandaloneStack()` with no config +// and no env var, which is the surface a `createStandaloneStack` host observes. + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { createStandaloneStack } from './standalone-stack.js'; + +const BOOT_TIMEOUT = 60_000; + +// The two plugin ids the stack composes. Matched by the plugin's own declared +// `name`, not by array position: the composition order is documented as a +// dependency-graph outcome elsewhere in this package, and an index would pin +// that instead of this. +const METADATA_PLUGIN = 'com.objectstack.metadata'; +const OBJECTQL_PLUGIN = 'com.objectstack.engine.objectql'; + +/** + * The id as each plugin actually received it. + * + * `MetadataPlugin` keeps it under `options.environmentId`; `ObjectQLPlugin` + * copies it to its own `environmentId` field. Both are TypeScript-private — + * hence the casts — and reading them is deliberate: they are the last point at + * which the stamped value is still identifiable before it dissolves into row + * scoping and an artifact-validation envelope. + */ +function stampedIds(plugins: any[]): { metadata: unknown; objectql: unknown } { + const metadata = plugins.find((p) => p?.name === METADATA_PLUGIN); + const objectql = plugins.find((p) => p?.name === OBJECTQL_PLUGIN); + expect(metadata, `stack must carry ${METADATA_PLUGIN}`).toBeDefined(); + expect(objectql, `stack must carry ${OBJECTQL_PLUGIN}`).toBeDefined(); + return { + metadata: (metadata as any).options?.environmentId, + objectql: (objectql as any).environmentId, + }; +} + +describe('[#13366] createStandaloneStack — default environment id', () => { + let dir: string; + let savedEnvId: string | undefined; + let savedHome: string | undefined; + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'os-standalone-envid-')); + savedEnvId = process.env.OS_ENVIRONMENT_ID; + savedHome = process.env.OS_HOME; + delete process.env.OS_ENVIRONMENT_ID; + process.env.OS_HOME = dir; + }); + + afterEach(() => { + if (savedEnvId === undefined) delete process.env.OS_ENVIRONMENT_ID; + else process.env.OS_ENVIRONMENT_ID = savedEnvId; + if (savedHome === undefined) delete process.env.OS_HOME; + else process.env.OS_HOME = savedHome; + try { rmSync(dir, { recursive: true, force: true }); } catch { /* noop */ } + }); + + it('stamps `env_local` when neither the config nor OS_ENVIRONMENT_ID names one', async () => { + const stack = await createStandaloneStack({ databaseUrl: 'memory://standalone-envid-default' }); + // The literal, at both landing sites. `proj_local` here is the pre-#13366 + // value and is what this case exists to keep from coming back. + expect(stampedIds(stack.plugins)).toEqual({ metadata: 'env_local', objectql: 'env_local' }); + }, BOOT_TIMEOUT); + + it('OS_ENVIRONMENT_ID still overrides the default', async () => { + process.env.OS_ENVIRONMENT_ID = 'env_from_the_environment'; + const stack = await createStandaloneStack({ databaseUrl: 'memory://standalone-envid-env' }); + expect(stampedIds(stack.plugins)).toEqual({ + metadata: 'env_from_the_environment', + objectql: 'env_from_the_environment', + }); + }, BOOT_TIMEOUT); + + it('an explicit `cfg.environmentId` still outranks OS_ENVIRONMENT_ID', async () => { + process.env.OS_ENVIRONMENT_ID = 'env_from_the_environment'; + const stack = await createStandaloneStack({ + environmentId: 'env_from_the_config', + databaseUrl: 'memory://standalone-envid-cfg', + }); + expect(stampedIds(stack.plugins)).toEqual({ + metadata: 'env_from_the_config', + objectql: 'env_from_the_config', + }); + }, BOOT_TIMEOUT); +}); diff --git a/packages/runtime/src/standalone-stack.ts b/packages/runtime/src/standalone-stack.ts index e6a29257ef..a1645f87c3 100644 --- a/packages/runtime/src/standalone-stack.ts +++ b/packages/runtime/src/standalone-stack.ts @@ -222,8 +222,8 @@ export const StandaloneStackConfigSchema = z.object({ * Defaults to `true`, and that default is the fix: a standalone kernel * OWNS its local platform tables, which is what the gate in * `assembleMetadataProtocol` always meant to say. It used to deduce that - * from `environmentId === undefined`, and line ~515 below stamps - * `'proj_local'` on every boot — so the block never ran and #8686's + * from `environmentId === undefined`, and line ~567 below stamps + * `'env_local'` on every boot — so the block never ran and #8686's * "covers every existing deployment" half covered no self-hosted install * at all. * @@ -564,7 +564,7 @@ export async function createStandaloneStack(config?: StandaloneStackConfig): Pro const { DefaultDatasourcePlugin } = await import('./default-datasource-plugin.js'); const { AppPlugin } = await import('./app-plugin.js'); - const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? 'proj_local'; + const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? 'env_local'; const artifactPath = resolveArtifactPathInput(cfg); // `databaseAuthToken` / `OS_DATABASE_AUTH_TOKEN` / `TURSO_AUTH_TOKEN` are @@ -747,7 +747,7 @@ export async function createStandaloneStack(config?: StandaloneStackConfig): Pro ...(cfg.projectRoot ? { rootDir: cfg.projectRoot } : {}), }), // [#9380] `runPlatformMigrations` is declared here, not deduced from - // `environmentId`: this stack stamps `'proj_local'` above, and the + // `environmentId`: this stack stamps `'env_local'` above, and the // assembly's old `environmentId === undefined` gate read that as "a // per-project cloud kernel" and disarmed the three boot repairs on // every self-hosted install. A standalone kernel owns its local From ea5256d95e5d9a3d80fafaccb28a9898a1be48ea Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 05:23:00 +0000 Subject: [PATCH 2/3] chore: add changeset for the env_local default stamp Part of #13366 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --- .../standalone-stack-stamps-env-local.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .changeset/standalone-stack-stamps-env-local.md diff --git a/.changeset/standalone-stack-stamps-env-local.md b/.changeset/standalone-stack-stamps-env-local.md new file mode 100644 index 0000000000..906afde8e4 --- /dev/null +++ b/.changeset/standalone-stack-stamps-env-local.md @@ -0,0 +1,38 @@ +--- +"@objectstack/runtime": patch +"@objectstack/metadata": patch +--- + +fix(runtime,metadata): the default local environment id is `env_local`, not `proj_local` (#13366) + +The v5.0 `project` to `environment` rename changed the default local environment +id and shipped that change on the surfaces most people meet: `packages/cli`'s +`CHANGELOG.md` records "Default local env id: `proj_local` -> `env_local`", the +`os dev` / `os start` / `os serve` commands emit `env_local`, and +`content/docs/deployment/cli.mdx` documents `env_local` as the default. Two +sites never received it and kept stamping `proj_local`. + +FROM: `createStandaloneStack()` — with no `environmentId` in its config and no +`OS_ENVIRONMENT_ID` in the environment — stamped `proj_local` on the kernel it +composed, and `MetadataPlugin` used `proj_local` to fill the environment-artifact +validation envelope for a bare definition. + +TO: both stamp `env_local`. + +WHO SEES IT. Only a host that calls `createStandaloneStack` (or +`createDefaultHostConfig`) **directly** and lets the id default. A boot started +by `os dev` / `os start` never reached the changed line: those commands export +`OS_ENVIRONMENT_ID=env_local` into the child process, which the fallback yields +to. Where the id is observable — row scoping in `ObjectQLPlugin`, the +`X-Environment-Id` header, `sys_metadata.environment_id` — such an embedder now +sees `env_local` where it saw `proj_local`, so an install with rows already +written under the old id should set `environmentId: 'proj_local'` (or +`OS_ENVIRONMENT_ID=proj_local`) explicitly to keep them addressed. That escape +hatch is unchanged and still wins over the default. + +NOT CHANGED, deliberately: `@objectstack/cloud-connection` still treats BOTH +spellings as the local sentinel, so a persisted `OS_ENVIRONMENT_ID=proj_local` +config keeps being recognised as local rather than presented to the control +plane as a cloud environment id; and `package-state-store`'s separate `'default'` +fallback keeps its own spelling, because renaming it would re-key persisted +package-disable state files. From 389a485293befeb7e122cc784575d9ca473e1297 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 05:44:53 +0000 Subject: [PATCH 3/3] docs(changeset): name the bare `os serve` boot as a second affected audience MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `os serve` sets no OS_ENVIRONMENT_ID for its own boot — it only reads one to name the runtime state file — so a bare `os serve` reaches the changed default too, not only a direct `createStandaloneStack` embedder. Measured at `serve.ts:524` (read-only) against `dev.ts:380` / `start.ts:409` (which export). Part of #13366 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N --- .../standalone-stack-stamps-env-local.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.changeset/standalone-stack-stamps-env-local.md b/.changeset/standalone-stack-stamps-env-local.md index 906afde8e4..ff2dac16a4 100644 --- a/.changeset/standalone-stack-stamps-env-local.md +++ b/.changeset/standalone-stack-stamps-env-local.md @@ -19,11 +19,20 @@ validation envelope for a bare definition. TO: both stamp `env_local`. -WHO SEES IT. Only a host that calls `createStandaloneStack` (or -`createDefaultHostConfig`) **directly** and lets the id default. A boot started -by `os dev` / `os start` never reached the changed line: those commands export -`OS_ENVIRONMENT_ID=env_local` into the child process, which the fallback yields -to. Where the id is observable — row scoping in `ObjectQLPlugin`, the +WHO SEES IT. Two audiences, both on the DEFAULT path — no `environmentId` in +the config and no `OS_ENVIRONMENT_ID` in the environment: + +1. a host that calls `createStandaloneStack` / `createDefaultHostConfig` + **directly**; +2. a **bare `os serve`** — one not spawned by `os dev` / `os start`. Those two + commands export `OS_ENVIRONMENT_ID=env_local` into the child process, which + the fallback yields to, so a boot they start never reached the changed line. + `os serve` sets no such variable for its own boot: it only READS one to name + the runtime state file. So a bare `os serve` used to run a kernel stamped + `proj_local` while publishing `runtime.env_local.json` beside it; the two now + agree. + +Where the id is observable — row scoping in `ObjectQLPlugin`, the `X-Environment-Id` header, `sys_metadata.environment_id` — such an embedder now sees `env_local` where it saw `proj_local`, so an install with rows already written under the old id should set `environmentId: 'proj_local'` (or