Skip to content

fix(cli): os package publish --install can use the environment you just switched to - #18268

Merged
hotlong merged 7 commits into
mainfrom
claude/issue-18265-active-env-in-cloud-config
Sep 16, 2026
Merged

hotlong merged 7 commits into
mainfrom
claude/issue-18265-active-env-in-cloud-config

Conversation

@hotlong

@hotlong hotlong commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Closes #18265

Clause-②: no — this diff adds an optional field to a local CLI credential file (CloudConfig in packages/cli/src/utils/cloud-config.ts) and a flag fallback. No new key reaches a published payload, and no contract accepts or refuses anything it did not before.

What was wrong

os environments switch — and os environments create --activate, the other writer — persisted activeEnvironmentId into ~/.objectstack/credentials.json only, the runtime credential store. os package publish reads ~/.objectstack/cloud.json — the cloud store — and never opened the other file. So the environment the CLI had just called active, and that os environments list marks with a ★ on the very next line, was invisible to the one command that installs into it:

os cloud login
os environments switch <env-id>       # ✓ Active environment: Dev
os package publish --install
# → `--install` requires `--env <id>`. Skipping auto-install.

# …and the path that does not even have a `switch` in it — the card's own
# scenario, "I created my own cloud dev environment, now publish to it":
os environments create --org $ORG --name Dev   # --activate is the default
os package publish --install
# → `--install` requires `--env <id>`. Skipping auto-install.

Why the obvious fix is the wrong one

Letting publish read credentials.json is not merely impure — it is wrong at runtime. The two files carry different servers: credentials.json's url falls back to http://localhost:3000 (utils/api-client.ts), cloud.json's default is https://cloud.objectos.ai (utils/cloud-config.ts), and the publish POSTs to the latter. An activeEnvironmentId read out of the runtime store therefore names an environment on a possibly different control plane, which the server resolves by bare id with no name or short-id rescue.

So the id now lives in cloud.json, next to the activeOrgId that is already there for the same kind of control-plane scope selector, and the url gate — not the file name — is the invariant. It lives in exactly one place, packages/cli/src/utils/active-environment.ts:

  • os environments switch records the id in cloud.json when the control plane it just talked to is cloud.json's url, and keeps writing credentials.json unchanged.
  • os environments create --activate (the default) records it through the same helper. It is the other writer of an active environment id and the first half of the flow this PR exists for, so fixing only switch would have left the most natural path refusing while reading like a fix. It is one call, not a second implementation: two copies of a url gate is how one of them stops gating. Its existing failure posture is untouched — creation succeeding while the activation or the record fails stays a warning, never an exit 1.
  • os package publish's --env falls back to that value when neither the flag nor $OS_ENVIRONMENT_ID is present, and only when cloud.json's url is the plane being published to.
  • A value written by an older CLI into credentials.json is migrated across once, and only when both files' urls agree.

Guards — three legs, each measured red-before / green-after

A guard that only pinned "a fallback exists" would go green on the wrong fix, which is the whole reason this card is not the one-liner it looks like. So the read side has two legs and the second is the real invariant; the write side has a third, for the second writer. Every leg restores from HEAD (the implementation was committed first) and proves the mutation reached disk before the run is read.

Leg A — remove the fallback

Mutation: delete the resolveCloudActiveEnvironmentId call in packages/cli/src/commands/package/publish.ts.

HEAD blob for packages/cli/src/commands/package/publish.ts: 09c3b3072467d8d26f05bd3877d291e87cc69b41
--- dirty lines BEFORE mutation: 1
anchor occurrences before mutation: 1
anchor occurrences after mutation:  0
injected marker occurrences:        1
--- dirty lines AFTER mutation: 2
--- on-disk hash now: 643257b2dcb66bc5175251d7e96094b526033640  (HEAD: 09c3b3072467d8d26f05bd3877d291e87cc69b41)
--- ABLATION-18265-A marker occurrences on disk: 1
=== RED LEG exit: 1
 Test Files  1 failed (1)
      Tests  3 failed | 6 passed (9)

 FAIL  test/publish-active-environment-store.test.ts > installs into the active cloud environment when --install carries no --env
AssertionError: expected undefined to be 'env_cloud_active'
 FAIL  test/publish-active-environment-store.test.ts > reads the CLOUD store, not the runtime store, even when both name this same server
AssertionError: expected undefined to be 'env_from_cloud_json'
 FAIL  test/publish-active-environment-store.test.ts > migrates a pre-existing runtime value into cloud.json once, only when the urls agree
AssertionError: expected undefined to be 'env_switched_before_upgrade'

--- restored; git diff HEAD is empty? []
--- dirty lines AFTER restore: 1
--- on-disk hash after restore: 09c3b3072467d8d26f05bd3877d291e87cc69b41  (HEAD: 09c3b3072467d8d26f05bd3877d291e87cc69b41)
=== GREEN LEG exit: 0
 Test Files  1 passed (1)
      Tests  9 passed (9)

Leg B — point the fallback at credentials.json (the tempting wrong fix)

Mutation: in packages/cli/src/utils/active-environment.ts, read readAuthConfig()'s activeEnvironmentId ahead of cloud.json's.

HEAD blob for packages/cli/src/utils/active-environment.ts: 2e9ec18e3cb8d12bc6c714152f5758b9f5c29e03
--- dirty lines BEFORE mutation: 1
anchor occurrences before mutation: 1
anchor occurrences after mutation:  1
injected marker occurrences:        1
--- dirty lines AFTER mutation: 2
--- on-disk hash now: 6c3def2ad6dae867a0f81c3e8dc79e398af05f54  (HEAD: 2e9ec18e3cb8d12bc6c714152f5758b9f5c29e03)
--- ABLATION-18265-B marker occurrences on disk: 1
=== RED LEG exit: 1
 Test Files  1 failed (1)
      Tests  3 failed | 6 passed (9)

 FAIL  test/publish-active-environment-store.test.ts > reads the CLOUD store, not the runtime store, even when both name this same server
AssertionError: expected 'env_from_credentials_json' to be 'env_from_cloud_json'
 FAIL  test/publish-active-environment-store.test.ts > refuses to install across control planes: an active environment recorded elsewhere is not used
AssertionError: expected 'env_on_other_plane' to be undefined
 FAIL  test/publish-active-environment-store.test.ts > migrates a pre-existing runtime value into cloud.json once, only when the urls agree
AssertionError: expected undefined to be 'env_switched_before_upgrade'

--- restored; git diff HEAD is empty? []
--- dirty lines AFTER restore: 1
--- on-disk hash after restore: 2e9ec18e3cb8d12bc6c714152f5758b9f5c29e03  (HEAD: 2e9ec18e3cb8d12bc6c714152f5758b9f5c29e03)
=== GREEN LEG exit: 0
 Test Files  1 passed (1)
      Tests  9 passed (9)

The asymmetry is the finding. Under leg B the first guard — "installs into the active cloud environment" — stays green: only the second case, which seeds both stores with different ids under the same url, can tell a correct fallback from the runtime-store one. expected 'env_from_credentials_json' to be 'env_from_cloud_json' is the line that refuses the wrong fix, and expected 'env_on_other_plane' to be undefined is the cross-plane leak the card describes, caught in the same leg.

Leg C — os environments create --activate stops recording for the cloud plane

Mutation: in packages/cli/src/commands/environments/create.ts, replace the recordCloudActiveEnvironmentId call with recordedForCloud = false — the runtime write stays, exactly as create behaved before this PR.

HEAD blob for packages/cli/src/commands/environments/create.ts: 8385130ed810b51292e54f7502f36fa75d2ff313
--- dirty lines BEFORE mutation: 1
anchor occurrences before mutation: 1
anchor occurrences after mutation:  0
injected marker occurrences:        1
--- dirty lines AFTER mutation: 2
--- on-disk hash now: b42e66f2fc6fb3b3b2b07bc962a83480e2bc8d1f  (HEAD: 8385130ed810b51292e54f7502f36fa75d2ff313)
--- ABLATION-18265-C marker occurrences on disk: 1
=== RED LEG exit: 1
 Test Files  1 failed (1)
      Tests  1 failed | 10 passed (11)

 FAIL  |unit| test/publish-active-environment-store.test.ts > os environments create --activate records the id for the cloud plane too > a freshly created environment is immediately a publish target, with no switch in between
AssertionError: create --activate recorded the new environment in the runtime store only, so the very next `os package publish --install` cannot see it. That is the same defect as the one this file guards on `switch`, one command over.: expected undefined to be 'env_created'

--- restored; git diff HEAD is empty? []
--- dirty lines AFTER restore: 1
--- on-disk hash after restore: 8385130ed810b51292e54f7502f36fa75d2ff313  (HEAD: 8385130ed810b51292e54f7502f36fa75d2ff313)
=== GREEN LEG exit: 0
 Test Files  1 passed (1)
      Tests  11 passed (11)

Leg C reds exactly one case, and that is the reading. It is the only leg the new create --activate case can distinguish, and legs A and B were re-measured after it was folded in rather than assumed: A now reds 4 cases (the new one included — it asserts the publish end to end) and B still reds the same 3 as before, with the new case staying green under B because both stores hold the same id there. So the "same url, different ids" case remains the only thing in the file that refuses the credentials.json fix; adding a second writer did not quietly make some other assertion carry that weight.

Gates

Command Verdict
pnpm --filter @objectstack/cli test ⚠️ exit 1 — 11 failed · 3384 passed (256 files). The failing set is a strict subset of the one measured before this change, and every file in it reproduces on an unmodified origin/main control checkout; none is a file this diff touches. Table below
pnpm --filter @objectstack/cli build ✅ exit 0
pnpm --filter @objectstack/cli typecheck ✅ exit 0 — tsc --noEmit + check:test-typecheck: "@objectstack/cli's test layer compiles under packages/cli/tsconfig.test.json"
pnpm check:cli-command-ids ✅ exit 0 — 500 literals across 145 files resolve; 63 command modules examined
pnpm check:cli-examples-parity ✅ exit 0 — "6 os package publish invocation(s) … == 6 in the block at content/docs/deployment/cli.mdx:1850"
pnpm check:empty-changeset ✅ exit 0 — "1 declaring changeset(s) added"
pnpm lint ✅ exit 0 — repo-wide eslint . --no-inline-config
pnpm check:doc-anchors ✅ exit 0 — 361 fragment links across 408 files resolve
pnpm check:doc-authoring ✅ exit 0

Also run, from dispatch-gates.mjs's derived family for these paths — every one exit 0:

check:nul-bytes · check:cross-package-test-inputs · check:test-source-alias · check:tier-file-adoption · check:type-check-coverage · check:type-check-debt · check:published-files · check:cli-test-child-env · check:docs-single-h1 · check-doc-frontmatter.mjs · check-closing-keyword-parity.mjs · check-docs-section-name.mjs · check-undeclared-dep-imports.mjs · docs-audit/check-affected-docs.mjs

The full @objectstack/cli suite is red on this machine, and it is red without this change

pnpm --filter @objectstack/cli test ran both tiers on the final head: 11 failed · 3384 passed over 256 files. Not one failure is in a file this diff touches, and each was controlled against an unmodified checkout rather than argued away.

The comparison that matters is the failing set, not the count — and after the second writer was folded in, that set shrank: src/commands/datasource/envelope-unwrap.test.ts and 4 of the 5 published-entry-node-env-source-reroute cases (all load-dependent timeouts) passed this time, and 11 previously skipped cases ran and passed. No file entered the set. The arithmetic closes exactly: 3367 + 11 recovered skips + 4 recovered timeouts + 2 new cases = 3384.

A detached worktree at origin/main (fd1247142a, installed and built the same way) reproduces them. The counts below are the earlier, larger run — the superset; the final run is those same files minus envelope-unwrap, with 4 of the 5 reroute cases recovered:

Failing file On origin/main Signature
test/published-subpath-console.pin.test.ts (2) same 2 fail expected '/private/var/folders/…' to be '/var/folders/…' — macOS symlinks /var; this is a realpath artefact of the box, not of the tree
test/published-subpath-hook-body.pin.test.ts (3) same 3 fail same
test/serve-runtime-state-project-key.test.ts (5) same 5 fail spawned-child state files; no child processes were driven at all: expected 2 to be 3
test/published-entry-node-env-source-reroute.test.ts (5) fails there too (1 of 5 in isolation, the other 4 are Test timed out in 5000ms under full-suite load) spawns the built entry
src/commands/datasource/envelope-unwrap.test.ts (1) passes in isolation on origin/main and on this branch (11 passed) Hook timed out in 10000ms in Config.load — contention during the 256-file run, not a failure

The targeted run of the file this card adds is green on its own: Test Files 1 passed · Tests 9 passed (9).

Declared deviation — the shared verify lock was never taken. scripts/pm/os-verify-lock.sh printed UNLOCKED (declared) · no usable flock on this host, so the shared verify lock was NEVER taken and NOTHING was serialized on every run above that went through it (build, typecheck, test). Nothing was serialized against parallel seats on this container.

Declared narrowing. This is the targeted local set. The repo-wide gate farm is CI's run: node scripts/pm/dispatch-gates.mjs --commands derives 94 commands for this change set, plus 48 artifact-roster families, 11 wide-population families and 6 path-scheduled CI jobs it explicitly marks NOT MEASURED locally.

Acceptance notes

  • The card body was verified line by line against origin/main before anything was written, and every line held. cloud-config.ts:42 (activeOrgId), cloud-config.ts:30 (DEFAULT_CLOUD_URL), cloud-config.ts:50 (the path), auth-config.ts:40/:47, switch.ts:58, api-client.ts:68/:78, publish.ts:501 and publish.ts:661 are all exactly where and what the card says.
  • One clause of the card was read toward "no regression" where it was ambiguous. "os environments switch writes to cloud.json … otherwise it keeps writing credentials.json" could be read as exclusive-or. It is implemented as additive: the runtime store is written exactly as before, and cloud.json is written as well when the planes agree. An exclusive-or reading would silently stop createApiClient from finding an active environment for the data / meta / environments families — the very families the card's Out-of-scope section says keep authenticating as the runtime identity.
  • One condition was added beyond the card's wording, for the card's own reason. The publish-side fallback is gated on cloud.json's url being the control plane being POSTed to, not only on the value being present. Without it, OS_CLOUD_URL=http://localhost:4000 os package publish --install would send an id recorded for cloud.objectos.ai to a different plane — the exact failure the card rejects the credentials.json fix for. It costs nothing in the card's own scenario (login, switch and publish all on one plane) and is measured by the third test case.
  • os environments switch's write to credentials.json became best-effort. It was an unguarded readAuthConfig(), which throws when the file does not exist — so a user who only ran os cloud login got an exit 1 after the server-side activation had already succeeded. The cloud write now happens first, the runtime write matches environments/create.ts's existing .catch(() => null) shape, and the command says so when neither store could record it.
  • Not done, deliberately: whether the whole os environments family should move to the cloud identity. It authenticates from credentials.json / $OS_TOKEN today and changing that changes which token existing users send — the card puts it out of scope and it stays out.
  • The second writer was folded in rather than left as a follow-up, on a maintainer decision. An earlier revision of this PR noted os environments create --activate (environments/create.ts) as the other writer of activeEnvironmentId and held it back because the card names switch. It is in now: on the day this ships, os environments create --org $ORG --name Dev --activate followed by os package publish --install is still the most natural path to the card's own scenario, and a half-fix that reads as a fix is worse than an obvious gap. The remedy is one call to the helper that already exists here, it adds no new verification surface, and leg C measures it.
  • Two user-visible strings in package publish named only os environments switch as the source of the install target — the Installing into the active environment … step line and the --install refusal. With two writers they were half-true, so both name both writers now, as do the --env rows and the install-target prose on cli.mdx and publish-and-preview.mdx.

Generated by Claude Code

hotlong and others added 4 commits September 15, 2026 13:08
…publish --install` can use it

`os environments switch` persisted `activeEnvironmentId` into the RUNTIME
credential store (`credentials.json`); `os package publish` reads the CLOUD
store (`cloud.json`) and never opens the other one, so `--install` refused
with "`--install` requires `--env <id>`" right after a successful switch.

The two files carry different servers, so publish must not read the runtime
copy: that id names an environment on a possibly different control plane.
Instead the id now lives in `cloud.json` next to `activeOrgId`, written by
`switch` when the control plane it just talked to IS `cloud.json`'s `url`,
and read back by publish under the same gate. `utils/active-environment.ts`
owns that gate; a one-time, url-guarded migration carries an existing value
across for users who switched before this change.

Claude-Session: https://claude.ai/code/session_82286b62-3514-46f6-8d53-ca6fbb6df3c6
Co-authored-by: Claude <noreply@anthropic.com>
… store

Two guards, because the wrong fix (publish reads credentials.json) looks
exactly like the right one from the outside. The second case seeds both
credential files with DIFFERENT active environment ids under the SAME url,
so only the SOURCE of the value can distinguish them; a fallback pointed at
the runtime store reds there while still passing the first case.

Claude-Session: https://claude.ai/code/session_82286b62-3514-46f6-8d53-ca6fbb6df3c6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 15, 2026
@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 15 documentable anchor(s).

15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via baseUrl (symbol, a field of interface ApiClientResult))
  • content/docs/api/declarative-endpoints.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/api/environment-routing.mdx (via baseUrl (symbol, a field of interface ApiClientResult))
  • content/docs/automation/connectors.mdx (via baseUrl (symbol, a field of interface ApiClientResult))
  • content/docs/concepts/metadata-lifecycle.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/deployment/cli.mdx (via os environments create (command, read off packages/cli/src/commands/environments/create.ts), os environments switch (command, read off packages/cli/src/commands/environments/switch.ts), os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/deployment/index.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/deployment/publish-and-preview.mdx (via os environments create (command, read off packages/cli/src/commands/environments/create.ts), os environments switch (command, read off packages/cli/src/commands/environments/switch.ts), os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/permissions/authentication.mdx (via baseUrl (symbol, a field of interface ApiClientResult))
  • content/docs/plugins/packages.mdx (via baseUrl (symbol, a field of interface ApiClientResult))
  • content/docs/protocol/kernel/index.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/kernel/lifecycle.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/kernel/metadata-service.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/objectql/index.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/objectql/schema.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via os environments create (command, read off packages/cli/src/commands/environments/create.ts))
  • content/docs/releases/v17/17-3.mdx (via os environments create (command, read off packages/cli/src/commands/environments/create.ts))
  • content/docs/releases/v9.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c053f748b68c6b877b0462d491b47dff27915692packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1ea8f4a2e585b9e36f4fb5b03d74661e2eaa2f07 — the merge of head a841e177ff6b9ed502e35ab223a64dbf4a588d91 into base c053f748b68c6b877b0462d491b47dff27915692, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1ea8f4a2e585b9e36f4fb5b03d74661e2eaa2f07 && git checkout 1ea8f4a2e585b9e36f4fb5b03d74661e2eaa2f07
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c053f748b68c6b877b0462d491b47dff27915692 a841e177ff6b9ed502e35ab223a64dbf4a588d91 && git checkout -B drift-repro c053f748b68c6b877b0462d491b47dff27915692 && git merge --no-ff a841e177ff6b9ed502e35ab223a64dbf4a588d91

node scripts/docs-audit/affected-docs.mjs --json c053f748b68c6b877b0462d491b47dff27915692

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c053f748b68c6b877b0462d491b47dff27915692 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ironment for the cloud plane too

`switch` was not the only writer of an active environment id. `create
--activate` (the default) names the environment it just provisioned, and
it is the first half of the flow this work exists for -- create your own
cloud dev environment, then publish into it, with no `switch` anywhere.
It wrote `credentials.json` only, so the most natural path still answered
"`--install` requires `--env <id>`".

It now records through the same `utils/active-environment.ts` helper, so
the url gate -- an environment id is used only on the control plane whose
`url` recorded it -- has exactly one implementation across both writers.
The existing failure posture is unchanged: creation succeeding while the
activation or the record fails is a warning, never an exit 1.

Both user-visible strings in `package publish` named only `os environments
switch` as the source of the value; they name both writers now.
…sh target

Two cases beside the `switch` pair, mirroring it exactly: create against
the cloud plane records into `cloud.json` and the very next
`publish --install` resolves it end to end; create against a different
control plane leaves `cloud.json` untouched while the runtime store still
takes its copy.

The first case is the one an ablation that skips the cloud write in
`create.ts` has to turn red -- without it, a fix that reaches only
`switch` reads as a fix while the card's own scenario still refuses.
…ges say so

`os environments create --activate` is as much a source of the
`--install` target as `os environments switch`, so the CLI reference's
environments table, the `--env` flag rows on both pages, the install-target
prose and the changeset all name it. The url gate they describe is
unchanged.
@hotlong

hotlong commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

End-to-end verification — real binaries, real HTTP, before/after

The unit tests and ablations pin the logic; nothing so far had run the actual command. This does: two real os binaries — one built from origin/main, one from this branch — driven against a stub control plane that records every request, each run in an isolated HOME with real ~/.objectstack/{cloud,credentials}.json files. The assertion is what lands on the wire (install_env_id in POST /cloud/packages/:id/versions), not what a fixture says.

Artifact under test: examples/app-todo/dist/objectstack.json (com.example.todo, 65.8 KB). Stub positive control run first — it records a hand-rolled POST — so an empty capture cannot be mistaken for a pass.

The headline: switchpublish --install, no --env anywhere

origin/main this branch
os environments switch <id> cloud.json<ABSENT>, credentials.json → id both written, and it says so: (also recorded in cloud.json — os package publish --install will use it)
os package publish --install ✗ `--install` requires `--env <id>`. Skipping auto-install. → Installing into the active environment env-…0001 (os environments switch / os environments create --activate)
on the wire install_env_id = <ABSENT> install_env_id = 'env-…0001'

create --activatepublish --install, the folded-in half

origin/main this branch
cloud.json after create <ABSENT> env-…0001
publish refuses installs
on the wire <ABSENT> 'env-…0001'

The two guards, measured on the wire

Cross-plane refusalcloud.json has no id (url :4711), credentials.json has one under a different url (:9999):

✗ `--install` requires `--env <id>`, $OS_ENVIRONMENT_ID, or an active environment
  (`os environments switch <id>` or `os environments create --activate` against this control plane).
on the wire: install_env_id = <ABSENT>
cloud.json after: unchanged — no migration happened

One-time migration — same fixture but credentials.json carries the same url:

→ Installing into the active environment env-…0001
on the wire: install_env_id = 'env-…0001'
cloud.json after: activeEnvironmentId = env-…0001   ← written by the CLI, not by the fixture

That pair is the card's whole argument, executed rather than asserted: the id crosses only when both files name the plane being published to.

What this does NOT cover

  • The stub is not the real control plane. It answers the two publish routes and the environment routes in the ok() envelope shape; it does not exercise installPackageIntoEnvironment, the sys_package_installation UPSERT, or kernel eviction. Those are unchanged by this PR — install_env_id is a pre-existing server contract, and the same two routes were exercised against the real production control plane earlier today by an unrelated publish.
  • No run against cloud.objectos.ai with a real account. This machine has no ~/.objectstack/cloud.json.
  • No browser evidence for Console → Marketplace → Install.

Rig teardown: stub killed, the throwaway origin/main worktree removed, a parallel session's stack left alone.

@hotlong

hotlong commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Re-done on a real rig — the earlier comment used a stub; this one does not

The previous comment's stub was a workaround for a broken local rig. The rig is fixed (the cause is written up at the end), so this is the same matrix against a real two-process stack: apps/cloud control plane on :4000 + apps/objectos tenant runtime on :5050, framework at the declared pin 6ff5b562cd, seeded org/env, both processes confirmed by lsof to be this worktree's.

Two real binaries as before: one built from origin/main, one from this branch (a841e177ff).

os cloud loginos environments switchos package publish --install

step origin/main this branch
environments switch exit 1, ✗ No stored credentials found. Please run os login first. ✓ Active environment + (also recorded in cloud.json — os package publish --install will use it)
cloud.json activeEnvironmentId <ABSENT> the env id
publish --install ✗ `--install` requires `--env <id>`. → Installing into the active environment …
sys_package_installation in the control DB no row pkgi_2e2221b9-…, enabled=1

The main failure is worse than the card described, and the rig is what showed it. readAuthConfig() throws when credentials.json is absent, and it runs after the server-side activate has already succeeded — so a user who ran only os cloud login gets a server-side switch that did happen, a client-side exit 1, and an error telling them to run a different login. This branch's .catch(() => null) is what makes that path survivable; it was flagged in the round-1 report and is now reproduced on a live control plane.

os environments create --activatepublish --install → does the app actually run?

Against a freshly created environment 08e35e94-… (created through the CLI, so this exercises the second writer too):

✓ Environment created: 08e35e94-…
  active environment set to 08e35e94-…
  (also recorded in cloud.json — `os package publish --install` will use it)

→ Installing into the active environment 08e35e94-… (os environments switch / os environments create --activate)
✓ Installed into environment

Tenant runtime log for that environment:

Plugin registered: plugin.app.com.example.todo@4.0.0

and zero kernel build failed / kernel_build_failing / Invalid semantic lines for 08e35e94. The app is loaded and the kernel is healthy — the half a stub cannot answer.

Authentication was NOT changed — the out-of-scope boundary held

Worth stating because the main run's failure looks like an auth difference. git diff origin/main...a841e177ff -- packages/cli/src/utils/api-client.ts adds exactly one thing: baseUrl on ApiClientResult. Token resolution is untouched, and switch still calls requireAuth(token) identically. The data / meta / environments families send the same token they always did.

Two defects this run surfaced, unrelated to this PR

Filed separately so they do not ride on this branch: a non-semver --version is accepted by the publish route and wedges the whole tenant environment into 503 (cloud#2305), and installing a corrected version does not recover it — the runtime keeps rebuilding a version the installation row no longer points at (cloud#2306). Both have timestamped control-DB evidence on their cards.

Still not covered

No run against cloud.objectos.ai with a real account (this machine has no ~/.objectstack/cloud.json), and no browser evidence for Console → Marketplace → Install.

The rig repair, for whoever hits it next

.claude/worktrees/objectstack in the cloud repo is a symlink that every cloud worktree's link:../../../objectstack/... dependency resolves through. It pointed at objectstack-cloudpin-eaba72e, a pinned worktree that had been deleted — so every @objectstack/* symlink under packages/*/node_modules/ was dangling and the stack died with Cannot find package '@objectstack/objectql' imported from …/service-cloud/dist/chunk-*.js, which reads like a stale-dist problem and is not one. Repointed at a fresh worktree of the declared pin (.objectstack-sha = 6ff5b562cd), then pnpm install + build in it, then pnpm install in the cloud worktree. Two further traps: turbo run build fails in @objectstack/service-cloud's dts step (Cannot find module 'zod') unless OS_SKIP_DTS=1, and the first rebuild after the repair reported FULL TURBO — replaying the cache built while the links were still broken, so it needs --force.

@hotlong
hotlong marked this pull request as ready for review September 16, 2026 09:51
@hotlong
hotlong enabled auto-merge September 16, 2026 09:51
@hotlong
hotlong added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 282d0eb Sep 16, 2026
44 checks passed
@hotlong
hotlong deleted the claude/issue-18265-active-env-in-cloud-config branch September 16, 2026 10:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

1 participant