fix(cli): os environments runs on the os cloud login session through one shared resolver - #21400
Conversation
Five subcommands against a local echo control plane: only cloud.json, only credentials.json (the control), and both. Red on 5155093: with cloud.json alone every subcommand refuses before sending a request. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…one resolver createControlPlaneApiClient (utils/api-client.ts) chooses the stored session for list, show, create, bind and switch: credentials.json's session where it targets the server (with no --url it names it), else cloud.json's on the same terms; an explicit url neither file names keeps credentials.json's session as before and never gets the cloud token. The active environment comes from the chosen file, and switch / create --activate no longer write a cloud environment id into credentials.json when they ran on the cloud session. The refusal with no session names os cloud login too. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…oth stores share Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fc4cbe6a7676852d6f1ce129b7e986b5db2e486f && git checkout fc4cbe6a7676852d6f1ce129b7e986b5db2e486f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 85986144c2ef6f379955137677c5cbfb00e194d2 c97a04430400ad4991dc8f530a52a182af2f1ffc && git checkout -B drift-repro 85986144c2ef6f379955137677c5cbfb00e194d2 && git merge --no-ff c97a04430400ad4991dc8f530a52a182af2f1ffc
node scripts/docs-audit/affected-docs.mjs --json 85986144c2ef6f379955137677c5cbfb00e194d2
|
…e active-environment header The five subcommands now accept the cloud.json session they used to refuse, so the changeset declares Clause-② yes (widening) at minor. The active-environment.ts header no longer says os environments authenticates only as the runtime identity. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Read for this record: card #21360 body and its three comments (triage ruling ① Derived judgments(a) Shape 1 as ruled — RIGHT. One resolver: (b) The switch/create write gate — RIGHT. (c) (d) Published text — RIGHT, nothing false is left. README Cloud: the table row, the paragraph and the flow comment are rewritten to the new order and match the resolver branch for branch; the lead sentence "do not share one session or one flag spelling" stays literally true (no single session covers all of them; (e) Pins — no weakened assertion. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #21360
Clause-②: yes (widening)
What changes
The five
os environmentssubcommands (list,show,create,bind,switch) built their client withcreateApiClient, which reads only~/.objectstack/credentials.json(theos loginsession). With only~/.objectstack/cloud.json, which is the state afteros cloud login, all five exited 1 withAuthentication requiredbefore sending a request. Meanwhileos login --helpsends hosted users toos cloud login, so the documented hosted flow looped.This follows triage's ruling
5947754514(shape 1). All five now choose their session in one shared resolver,createControlPlaneApiClientinpackages/cli/src/utils/api-client.ts, which picks in this order:credentials.json's session where it targets the server the command talks to. With no--url/OS_CLOUD_URL, it names the server itself, so a user with anos loginsession sees no change.cloud.json's session, on the same terms. Its server is its recorded url, orhttps://cloud.objectos.aiwhen it records none.credentials.json's session as before.cloud.json's token is never sent to a url other than its own.Explicit flags and env vars (
--url/OS_CLOUD_URL,--token/OS_TOKEN,OS_ENVIRONMENT_ID) still win field by field, as increateApiClient. Other details:X-Environment-Idcomes from the chosen session's file.session: 'credentials' | 'cloud'.switchandcreate --activateread it and skip thecredentials.jsonwrite when they ran on the cloud session. Without that skip, an id from cloud.json's server would land in a file that names a different server, and every lateros data/os metacall would send it there. Thecloud.jsonwrite still goes through the existing url gate inactive-environment.ts.requireControlPlaneAuth) namesos cloud loginas well asos login. TheAuthentication requiredprefix is unchanged.createApiClient,requireAuth, and theirdata/meta/datasource/whoamicallers.Where the resolver lives, and why
os package publishdoes not use itapi-client.ts, besidecreateApiClient. The resolver needs both stores and the url gateisSameControlPlanefromactive-environment.ts. Putting it incloud-config.tswould create an import cycle, becauseactive-environment.tsimportscloud-config.ts.os package publishkeeps its own lane. It reads onlycloud.json, by design: its source says it deliberately does not fall back tocredentials.json, andcontent/docs/deployment/cli.mdxdocuments that. The ruling's order putscredentials.jsonfirst. Moving publish onto this resolver would change which token publish sends whenevercredentials.jsonnames the same server, sopublish.tsis untouched.Measured: the five subcommands, spawned from source
Setup: HOME holds only
cloud.json, pointed at a local echo control plane. The CLI runs throughbin/run-dev.js, withpackages/cli/distabsent so the source is what runs.51550933db)97c52b5ce7)listAuthentication required. Please run os login ...GET /api/v1/cloud/environmentswithBearer cloud_tokshow env_1GET .../environments/env_1withBearer cloud_tokcreate --org org_1 --name DevPOST .../environments+POST .../env_new/activatewithBearer cloud_tok; id recorded incloud.jsonbind env_1 --artifact ...GET+PATCH .../environments/env_1withBearer cloud_tokswitch env_1GET+POST .../env_1/activatewithBearer cloud_tok; id recorded incloud.jsonPins:
packages/cli/src/commands/environments/cloud-session.test.tsThe file has 49 cases. They run in-process through
Command.runagainst two realnode:httpecho control planes on 127.0.0.1, with HOME redirected to a temp directory. Every group runs over all five subcommands:cloud.json: the cloud bearer and the cloud active environment go to the cloud url. A--urlthatcloud.jsondoes not name gets exit 1 and zero requests.credentials.json(the control): the runtime bearer and active environment go to the runtime url, including with an explicit--url, as before.credentials.jsonwins where both name the same server, with and without--url.--url,credentials.jsonstill picks the server.--urlnaming cloud.json's server selects the cloud session.switchandcreateon the cloud session leavecredentials.jsonalone. With onlycloud.json, they record the id incloud.json.os cloud login.Red before the fix, measured with the pin commit
eb9dcdaeb4on top of51550933db: 19 failed, 25 passed. The failures were the cloud-only bearer (5), the--urlnaming cloud.json's server (5), the four write cases, and the no-session remedy (5). The 25 passing cases were the controls, the both-stores ordering cases and the foreign-url guard. These hold before and after the change by design.Ablations
All 15 ablations ran at
97c52b5ce7throughscripts/ablation-replace.mjsin WRAP mode, with the fix already committed. The subject is reached by relative import intopackages/cli/src, so no dist is on the path. Every leg's anchor hit exactly once, and the mutation was proven on disk by anchor count and blob change. Every restore was proven by blob == HEAD and an emptygit diff HEAD. Each leg went red exactly where predicted:--urlcloud x5, the 4 write cases--urlserver--url--url--urlcloud x5switch/createruntime write ungatedos cloud logincreateApiClientTests and gates, at
97c52b5ce7pnpm --filter @objectstack/cli typecheck: exit 0, which coverstsc --noEmitoversrc(the new pin is undersrc) pluscheck:test-typecheckOK.pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2:published-subpath-console.pinandpublished-subpath-hook-body.pin, refused becausepackages/cli/distwas absent, a build prerequisite.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 65 commands, and every one was run with its exit code recorded before any pipe.check:dual-build-cjs-loads,check:i18n,check:i18n-coverageandcheck:i18n-walk-parity. Afterturbo run buildthey exit 0.--ran:65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN.pnpm lint. ESLint ran with--no-inline-config --format jsonon the 10 changed.tsfiles.eslint.config.mjsnever enables type-aware linting (noparserOptions.project, no typed rules; it says so itself). Its only cross-file inputs are two baseline JSON files this diff does not touch, so no untouched file's verdict can move.Acceptance notes
packages/cli/src/utils/active-environment.tsheader says "os environmentsauthenticating as the runtime identity is deliberate". That sentence is now false. The file is outside this card's claimed surface, so it is not edited here. It is a one-sentence comment change.package/publish.tsandplugin/publish.tseach carry their own copy of thecloud.json-only resolution. That duplication predates this card, and this card does not touch it.content/docs/deployment/cli.mdxhas noos environmentssentence that this change makes false, so it is not edited.credentials.json's session) is kept so the control holds byte for byte. Narrowing it would be its own decision.origin/main. Its one stale input isscripts/sdui-manifest.record.json, which this diff does not touch. Upstream, onlypackages/cli/test/json-stdout-purity.e2e.test.tschanged underpackages/cli.Generated by Claude Code