What happens
os cloud login
os environments switch <env-id> # ✓ Active environment: Dev
os build
os package publish --install
# → `--install` requires `--env <id>`. Skipping auto-install.
The environment the CLI just told you is active — and which os environments list
marks with a ★ on the very next line — is invisible to the one command that would
install into it. Every publish has to carry the UUID again, by flag or by
OS_ENVIRONMENT_ID. Nobody remembers a UUID.
Why it is invisible (measured on origin/main)
Two credential stores, two identities. packages/cli/src/utils/cloud-config.ts
states the split in its own header:
- Runtime identity — who you are inside your own ObjectOS instance. Stored in
credentials.json, written by os login.
- Cloud identity — who you are on the official ObjectStack Cloud package
registry. Stored in cloud.json, written by os cloud login. Used to publish &
install packages …
Keeping them in separate files … makes it impossible to accidentally publish a
package with a runtime-scoped token.
os environments switch writes cfg.activeEnvironmentId into credentials.json
(commands/environments/switch.ts:58, path at utils/auth-config.ts:47, field
declared at utils/auth-config.ts:40).
os package publish reads cloud.json (utils/cloud-config.ts:50) and resolves
its base URL from that file's url (commands/package/publish.ts:501). It never
opens credentials.json.
- The only reader of
activeEnvironmentId anywhere in the CLI is
utils/api-client.ts:68, which serves the data / meta / environments families.
- The refusal itself is
commands/package/publish.ts:661 —
const shouldInstall = flags.install && flags.env;
⛔ The fix is NOT "let publish read credentials.json"
That is precisely the mixing the file header says the split exists to prevent. It is
also wrong at runtime, not merely impure:
The two files carry different servers. credentials.json's URL falls back to
http://localhost:3000 (utils/api-client.ts:78); cloud.json's default is
https://cloud.objectos.ai (utils/cloud-config.ts:30), and that is the URL the
publish actually POSTs to. So credentials.json's activeEnvironmentId means "an
environment on whatever server I am currently pointed at". Handing it to a publish
aimed at cloud.objectos.ai names a UUID belonging to a different control plane —
a 404 at best, and the server resolves it with a bare
findOne('sys_environment', { where: { id } }), so there is no name or short-id
rescue on that path either.
Shape of the fix
- Add
activeEnvironmentId?: string to CloudConfig — next to activeOrgId
(utils/cloud-config.ts:42), which is already there and already documented as
"Active organization id chosen for publishing". An organization and an
environment are the same kind of thing: control-plane scope selectors, one level
apart. The precedent is in the same interface.
os environments switch writes to cloud.json when the control plane it just
talked to is cloud.json's url; otherwise it keeps writing
credentials.json. Two files, two control planes, an active environment for each
— that is the self-consistent model, and it preserves os environments working
against a self-hosted server with a runtime token, which is deliberate (see
Out of scope).
os package publish's --env falls back to cloud.json's activeEnvironmentId
when both the flag and OS_ENVIRONMENT_ID are absent. ⛔ It must never read
credentials.json.
- Migration: existing values live in
credentials.json. Copy across once, and
only when the two files' urls agree.
Guards — both must be measured red-before / green-after
- Remove the fallback ⇒ a publish with an active cloud environment and no
--env
fails.
- Point the fallback at
credentials.json ⇒ also fails. This is the real
invariant the card is about. A guard that only pins "a fallback exists" would
accept the wrong fix, which is the whole reason this card is not the one-line
change it looks like.
Out of scope — ⛔ do not bundle
Whether the whole os environments family should move to the cloud identity. It
authenticates today from credentials.json / OS_TOKEN against OS_CLOUD_URL
(defaulting to localhost:3000) — right for a self-hosted control plane, odd-looking
against cloud.objectos.ai. Changing it changes which token existing users' commands
send. Separate card, separate decision.
Found while answering "how do I deploy a metadata project into my own cloud dev
environment". The maintainer's words: 「没有人会记得 env_id」. The first fix proposed
in that conversation was the credentials.json one above; the maintainer rejected it
with "activeEnvironmentId 要写也应该是 cloud.json ?", and the server-split argument
above is what confirmed they were right.
What happens
The environment the CLI just told you is active — and which
os environments listmarks with a ★ on the very next line — is invisible to the one command that would
install into it. Every publish has to carry the UUID again, by flag or by
OS_ENVIRONMENT_ID. Nobody remembers a UUID.Why it is invisible (measured on
origin/main)Two credential stores, two identities.
packages/cli/src/utils/cloud-config.tsstates the split in its own header:
os environments switchwritescfg.activeEnvironmentIdintocredentials.json(
commands/environments/switch.ts:58, path atutils/auth-config.ts:47, fielddeclared at
utils/auth-config.ts:40).os package publishreadscloud.json(utils/cloud-config.ts:50) and resolvesits base URL from that file's
url(commands/package/publish.ts:501). It neveropens
credentials.json.activeEnvironmentIdanywhere in the CLI isutils/api-client.ts:68, which serves thedata/meta/environmentsfamilies.commands/package/publish.ts:661—const shouldInstall = flags.install && flags.env;⛔ The fix is NOT "let publish read credentials.json"
That is precisely the mixing the file header says the split exists to prevent. It is
also wrong at runtime, not merely impure:
The two files carry different servers.
credentials.json's URL falls back tohttp://localhost:3000(utils/api-client.ts:78);cloud.json's default ishttps://cloud.objectos.ai(utils/cloud-config.ts:30), and that is the URL thepublish actually POSTs to. So
credentials.json'sactiveEnvironmentIdmeans "anenvironment on whatever server I am currently pointed at". Handing it to a publish
aimed at cloud.objectos.ai names a UUID belonging to a different control plane —
a 404 at best, and the server resolves it with a bare
findOne('sys_environment', { where: { id } }), so there is no name or short-idrescue on that path either.
Shape of the fix
activeEnvironmentId?: stringtoCloudConfig— next toactiveOrgId(
utils/cloud-config.ts:42), which is already there and already documented as"Active organization id chosen for publishing". An organization and an
environment are the same kind of thing: control-plane scope selectors, one level
apart. The precedent is in the same interface.
os environments switchwrites tocloud.jsonwhen the control plane it justtalked to is
cloud.json'surl; otherwise it keeps writingcredentials.json. Two files, two control planes, an active environment for each— that is the self-consistent model, and it preserves
os environmentsworkingagainst a self-hosted server with a runtime token, which is deliberate (see
Out of scope).
os package publish's--envfalls back tocloud.json'sactiveEnvironmentIdwhen both the flag and
OS_ENVIRONMENT_IDare absent. ⛔ It must never readcredentials.json.credentials.json. Copy across once, andonly when the two files'
urls agree.Guards — both must be measured red-before / green-after
--envfails.
credentials.json⇒ also fails. This is the realinvariant the card is about. A guard that only pins "a fallback exists" would
accept the wrong fix, which is the whole reason this card is not the one-line
change it looks like.
Out of scope — ⛔ do not bundle
Whether the whole
os environmentsfamily should move to the cloud identity. Itauthenticates today from
credentials.json/OS_TOKENagainstOS_CLOUD_URL(defaulting to
localhost:3000) — right for a self-hosted control plane, odd-lookingagainst cloud.objectos.ai. Changing it changes which token existing users' commands
send. Separate card, separate decision.
Found while answering "how do I deploy a metadata project into my own cloud dev
environment". The maintainer's words: 「没有人会记得 env_id」. The first fix proposed
in that conversation was the credentials.json one above; the maintainer rejected it
with "activeEnvironmentId 要写也应该是 cloud.json ?", and the server-split argument
above is what confirmed they were right.