Skip to content

cli: os environments * never read the os cloud login session, while os login --help sends hosted users to os cloud login — the documented cloud flow loops #21360

Description

@objectstack-fleet

Filing-gate class: ① a defect with a named landing site and a measured reach (class a, a public door).

  • Landing site: packages/cli/src/utils/api-client.ts:56-85. createApiClient reads only ~/.objectstack/credentials.json, the os login session. Its default server URL is http://localhost:3000.
  • reach: os environments list|show|create|bind|switch. They are the second step of the documented cloud flow, in packages/cli/README.md (Cloud) and content/docs/deployment/cli.mdx.

Measured by #21310's os-dev (round report 5947221232), with HOME holding only ~/.objectstack/cloud.json, the state after os cloud login, pointed at a local echo server:

  • All five os environments subcommands exit 1 with "Authentication required. Please run os login or set OS_TOKEN environment variable." before sending any request.
  • os login --help says "For the hosted package registry, use os cloud login instead". So a hosted user is sent from one command to the other and back.
  • os package publish, by contrast, reads cloud.json: with cloud.json alone its request carries that session's bearer.
  • environments/create.ts:15-30 writes the active environment into cloud.json when the control plane matches, so these commands are built to run against the cloud control plane.

Where things stand: PR #21354 (card #21310) makes the README state this behaviour truthfully, in a per-command "Credentials and server URL" table. It changes no code, so the gap remains.

Shapes (a product decision, not a ruling)

  1. os environments (through createApiClient or a cloud-specific client) falls back to the cloud.json session and URL, as os package publish already does.
  2. os login against the hosted control plane becomes the documented route for os environments, and os login --help stops redirecting hosted users to os cloud login.
  3. One stored session for both, which retires the split.

Each shape's reasoning belongs on the four axes. The choice of shape is the maintainer's.

Duplicate check: 464 objectstack issues and PRs listed over REST (open plus the most recently updated closed), titles and bodies grepped for credentials.json near cloud.json, os environments near cloud login / Authentication required, and createApiClient near cloud. No hit.

Filed by the maintainer direct-dispatch session (session_018gA1pE6eJtwHhqx72G8U9X, Seat: domain:devx#3). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.

Dedupe words: os environments cloud login session · Authentication required os login · credentials.json cloud.json · createApiClient cloud session


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: grade completed — priority:p2 · area:devpath. Ruling: shape 1, so os environments reads the cloud session as os package publish already does. The filer's bug · domain:cli · pm:queue stand

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-02T07:55Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p2. The documented hosted flow loops: os environments can't run on the session os cloud login stores. It is measured on all five subcommands.

    Ruling: shape 1 (triage's, by the in-repo precedent; overturnable by the maintainer).

    • os package publish already reads cloud.json's session and URL, and environments/create.ts already writes the active environment there. So the hosted commands read the cloud session through one shared resolver: credentials.json's session first where it targets the same server, then cloud.json. ⛔ No second copy of that resolution per command.
    • os login --help's redirect then becomes true.
    • Shape 3 (one stored session, retiring the split) is a product change beyond this defect, and goes to the maintainer as its own decision if wanted.
    • PR docs(cli): the README states the global flags and the os plugin group that the built os registers #21354's README table is updated in the same change to state the new behaviour.

    Pins: with only cloud.json, each os environments subcommand sends the cloud bearer to the cloud URL. With only credentials.json, the existing behaviour is unchanged (the control).


    Generated by Claude Code

  2. added
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    on Oct 2, 2026
  3. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 of the domain:cli seat's session session_01VvcEokUG1tvVxkceYfR5XB (batch 3): priority:p2, to triage's ruling 5947754514 (shape 1). Landed as Fixes #21360.
    Session: session_01VvcEokUG1tvVxkceYfR5XB
    Account: huangyiirene
    Branch: claude/issue-21360-environments-cloud-session
    Worktree: objectstack-issue-21360
    Domain: domain:cli
    Seat: domain:cli#1
    File surface, derived at origin/main 51550933db:

    • The ruling, as built: the hosted commands read the cloud session through one shared resolver: credentials.json's session first where it targets the same server, then cloud.json. os environments list|show|create|bind|switch use it. ⛔ No second copy of that resolution per command. os login --help's redirect then becomes true. Shape 3 (one stored session) is ⛔ not this card.
    • Read at 51550933db:
      • createApiClient (packages/cli/src/utils/api-client.ts:54) reads only credentials.json, and its default server is http://localhost:3000.
      • os package publish reads cloud.json through tryReadCloudConfig (packages/cli/src/utils/cloud-config.ts, imported at publish.ts:36).
      • environments/create.ts and switch.ts already write the active environment into cloud.json.
      • The dev decides whether the shared resolver lives in api-client.ts or cloud-config.ts, and whether publish.ts moves onto it, so that the hosted commands share one resolution. The dev reports the measured choice.
    • Expected files:
      • packages/cli/src/utils/api-client.ts;
      • packages/cli/src/utils/cloud-config.ts;
      • packages/cli/src/commands/environments/*.ts, only where a command builds its own client;
      • packages/cli/src/commands/package/publish.ts, only if it moves onto the one resolver;
      • pins beside them.
    • Docs: the README's per-command "Credentials and server URL" table (from PR docs(cli): the README states the global flags and the os plugin group that the built os registers #21354, landed) states the new behaviour, in packages/cli/README.md's Cloud section only. The os environments text in content/docs/deployment/cli.mdx is updated if the change falsifies it.
    • Pins:
      • With only cloud.json, each of the five os environments subcommands sends the cloud bearer to the cloud URL, measured red before the fix against a local echo server.
      • With only credentials.json, the existing behaviour is unchanged (the control).
      • With both, targeting the same server, credentials.json's session wins.
    • Changeset: one .changeset/21360-*.md for @objectstack/cli, at the level the real diff takes.
    • Stop clause: stop if shape 1 needs a change to the stored file formats, or to os cloud login / os login themselves.
      (stop on a breach outside these; explain in the report)
      Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable; the default).
      Clause-②: no. os environments starts accepting a session the platform already issues; no public member or stored shape moves. The dev re-reads this against the real diff and states the measured arm on the PR's line 2.
      Thread-read: 5947754514
      Serial constraints cleared: Of the 12 open PRs (all file lists read in this act), none touches api-client.ts, cloud-config.ts, active-environment.ts, the environments commands, package/publish.ts, the login commands or packages/cli/README.md. #21310 / PR #21354 (the README table) has landed. No in-flight claim names these files.
  4. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21360,
    "status": "done",
    "branch": "claude/issue-21360-environments-cloud-session",
    "pr": "#21400",
    "head": "97c52b5ce7",
    "session": "session_01VvcEokUG1tvVxkceYfR5XB",
    "premise_still_valid": true,
    "summary": "Premise re-measured at 5155093: with HOME holding only cloud.json, all five os environments subcommands exited 1 with 'Authentication required. Please run os login...' and sent 0 requests. Per triage ruling 5947754514 (shape 1), the five subcommands now go through one shared resolver, createControlPlaneApiClient in packages/cli/src/utils/api-client.ts. It picks credentials.json's session where it targets the server (with no --url it names the server, so os login users see no change), else cloud.json's session on the same terms. An explicit url that neither file names keeps credentials.json's session as before, and the cloud token never goes to a foreign url. The active environment comes from the chosen file. switch and create --activate skip the credentials.json write when they ran on the cloud session, because an id from cloud.json's server must not land in a file that names another server. The no-session refusal (requireControlPlaneAuth) names os cloud login too. createApiClient and requireAuth, with their data/meta/datasource/whoami callers, are unchanged; os package publish is unchanged. The README Cloud section table, paragraph and flow comment now state the new behaviour, and a patch changeset is added.",
    "resolver": {
    "location": "packages/cli/src/utils/api-client.ts: createControlPlaneApiClient + private chooseControlPlaneSession; result field session: 'credentials' | 'cloud'; requireControlPlaneAuth beside requireAuth. Not cloud-config.ts: active-environment.ts imports cloud-config.ts, so the url gate isSameControlPlane would close an import cycle.",
    "precedence": "url/token/environmentId: explicit option or env (OS_CLOUD_URL, OS_TOKEN, OS_ENVIRONMENT_ID) wins field by field. The chosen session then fills the rest. No explicit url: credentials.json if present, else cloud.json (url = its url or https://cloud.objectos.ai). Explicit url: credentials.json if its url names that server, else cloud.json if its url does, else credentials.json as before (never cloud.json). The final baseUrl fallback is http://localhost:3000.",
    "callers": "environments/list.ts, show.ts, create.ts, bind.ts, switch.ts (each previously called createApiClient). create.ts and switch.ts also read result.session to skip the credentials.json write on the cloud session.",
    "publish_ts": "Not moved. os package publish (and os plugin publish) read only cloud.json by design; the code and cli.mdx say they deliberately do not fall back to credentials.json. The ruling's order puts credentials.json first, so moving publish onto this resolver would change which token publish sends when credentials.json names the same server. That is a behaviour change outside the ruling. The hosted control-plane family has exactly one resolution; publish keeps its own documented cloud-only lane."
    },
    "repro": {
    "setup": "HOME holds only cloud.json {url: local echo server, token: cloud_tok}. The CLI is spawned from source via bin/run-dev.js through tsx, with packages/cli/dist absent (checked by the script).",
    "before_51550933db": "list/show/create/bind/switch: each exit 1, 0 requests, 'Authentication required. Please run os login or set OS_TOKEN environment variable.'",
    "after_97c52b5ce7": "list: exit 0, GET /api/v1/cloud/environments Bearer cloud_tok. show: exit 0, GET .../env_1 Bearer cloud_tok. create: exit 0, POST .../environments + POST .../env_new/activate Bearer cloud_tok, recorded in cloud.json. bind: exit 0, GET + PATCH .../env_1 Bearer cloud_tok, X-Environment-Id env_new (from cloud.json). switch: exit 0, GET + POST .../env_1/activate Bearer cloud_tok; cloud.json activeEnvironmentId=env_1."
    },
    "tests": "All at HEAD 97c52b5. (1) New pin packages/cli/src/commands/environments/cloud-session.test.ts, 49 cases, two real node:http echo control planes, HOME redirected to a temp dir. Red before the fix (pin commit eb9dcda over 5155093): 'Tests 19 failed | 25 passed (44)'; the 25 green were the controls, the both-stores ordering cases and the foreign-url guard. After: 'Tests 49 passed (49)'. (2) Related files: vitest run of src/commands/environments/ + test/publish-active-environment-store.test.ts + test/remote-api-utils.test.ts + src/utils/auth-config.test.ts: 'Test Files 6 passed (6) / Tests 157 passed (157)'. This was before the extra pin case; the cloud-session file alone was re-run afterwards, 49/49. (3) pnpm --filter @objectstack/cli typecheck: TC_EXIT=0 (tsc --noEmit over src, which includes the new pin; check:test-typecheck OK). (4) pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 'Test Files 2 failed | 245 passed (247) / Tests 3509 passed | 29 skipped'. The 2 failures were test/published-subpath-console.pin and test/published-subpath-hook-body.pin, which refuse with 'packages/cli is not built'. After turbo build (72 tasks, 71 cached), those two files: 'Test Files 2 passed (2) / Tests 29 passed (29)'. (5) Integration tier: NOT MEASURED locally, declared to CI, because the diff touches no integration-tier file and no spawn entry (os-dev rule). (6) Lint, a declared narrowing of pnpm lint: eslint --no-inline-config --format json over the 10 changed .ts files reported '10 files linted, 0 ignored, 0 errors, 0 warnings'. Population is read from eslint's own JSON output (no 'File ignored' entries). Invariance: eslint.config.mjs never enables type-aware linting (no parserOptions.project, no typed rules; stated at its line 327-329), and its only cross-file inputs are two baseline JSON files this diff does not touch. (7) Ablations: 15 legs, see ablations.",
    "ablations": "Each leg ran at 97c52b5 through scripts/ablation-replace.mjs WRAP mode, with the fix already committed. The subject is imported by relative path into packages/cli/src, so no dist is on the path. Every anchor hit exactly once (x1 → x0), the blob changed, and every restore was proven by blob == HEAD and an empty git diff HEAD; the tree was clean afterwards. Red counts matched the prediction: L1 drop the cloud.json candidate: 14 (cloud-only bearer x5, --url cloud x5, 4 write cases). L2 swap the no-url order to cloud first: 10 (both-same-server x5, no-url-picks-credentials x5). L3 delete the credentials url match: 5 (credentials wins on the shared --url server). L4 ungated cloud fallback for a foreign url: 5 (cloud token never sent to a foreign --url). L5 drop the legacy credentials leg: 5 (control with explicit --url). L6 drop the cloud active environment: 10. L7 drop the runtime active environment: 10. L8 switch write ungated: 1. L9 create write ungated: 1. L10 remedy reverted: 5. L11-L15 list/show/create/bind/switch back on createApiClient: 2/2/4/2/4, each that subcommand's cases only. Every one of the 49 cases is reddened by at least one leg.",
    "gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 65 commands at 97c52b5. Each was run with its exit captured before any pipe. 61 exited 0 on the first pass. 4 exited 3 (PREREQUISITE NOT MET, missing dist): check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity. After the workspace turbo build all 4 exited 0 ('check-i18n-bundles: OK (9 package(s)...)', 'check-i18n-coverage: OK (13 config(s), 621 baselined..., none new)', 'check-i18n-walk-parity: 11 declared group(s), 9 walked, 2 exempted', dual-build provenance line, exit 0). --ran: 'Run reconciliation — 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). The derivation warned the tree is 8 commits behind origin/main; its one stale input is scripts/sdui-manifest.record.json, which this diff does not touch. Upstream, packages/cli changed only in test/json-stdout-purity.e2e.test.ts, so no merge was taken.",
    "checks_after_push": "Read at head 97c52b5 when the report was written: 31 check runs, 10 success, 3 skipped, 18 in_progress, 0 failed. Required: Governed Surface Queue Guard success; Lint & Repo Gates, Build Core and Temporal Conformance in_progress; TypeScript Type Check, Test Core and Dogfood Regression Gate not yet listed. CI convergence is left to PM.",
    "files_changed": [
    ".changeset/21360-environments-cloud-session.md (new, @objectstack/cli patch, Clause-②: no)",
    "packages/cli/src/utils/api-client.ts (resolver, session field, requireControlPlaneAuth)",
    "packages/cli/src/utils/cloud-config.ts (doc comments only)",
    "packages/cli/src/commands/environments/list.ts, show.ts, bind.ts (client swap)",
    "packages/cli/src/commands/environments/create.ts, switch.ts (client swap + runtime-store write gate + docblocks)",
    "packages/cli/src/commands/environments/cloud-session.test.ts (new pin)",
    "packages/cli/src/commands/environments/create-clone-from.test.ts (vi.mock key createApiClient → createControlPlaneApiClient)",
    "packages/cli/test/publish-active-environment-store.test.ts (one comment made true)",
    "packages/cli/README.md (Cloud section: table row, paragraph, flow comment)"
    ],
    "deviations": [
    "Added beyond the literal ruling, both on files inside the claimed surface: (a) the switch/create runtime-store write gate (session === 'cloud'). Without it, this change newly makes reachable a cloud environment id written into credentials.json, which names another server. (b) requireControlPlaneAuth's remedy names os cloud login, the other door of the loop; the 'Authentication required' prefix is kept.",
    "Two pins next to the environments commands were edited: create-clone-from.test.ts mocked createApiClient, so its CONTROL went red when create.ts moved to the new resolver, and the mock key was renamed. test/publish-active-environment-store.test.ts had a comment the change made false. That file is the pin for switch/create's cloud.json write.",
    "README: besides the table, the paragraph and the flow-block comment PR #21354 added in the same Cloud section were rewritten, because they stated the old behaviour. content/docs/deployment/cli.mdx was not edited: no os environments sentence there is falsified.",
    "packages/cli/src/utils/active-environment.ts was NOT edited. Its header says 'os environments authenticating as the runtime identity is deliberate', which is now false. It is outside the claimed file surface. The fix is one comment sentence; the seat can authorize it as a patch round.",
    "Integration tier not run locally. The dispatch asked for it, and os-dev says packages/cli cards owe only the unit tier unless the diff touches integration-tier files or spawn entries; this diff touches neither, so os-dev wins and it is declared to CI.",
    "The harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. The AGENTS.md / dispatch model-free trailer pair and the session-URL footer were used instead, per their precedence.",
    "The verify batch ran typecheck and the unit tier joined by ';' under one lock call. That verdict line is 'batch-last-exit', so the two exits were read from the separately captured files (TC_EXIT=0, UNIT_EXIT=1, explained in tests)."
    ],
    "mcp_calls": "0",
    "api_writes": "3 — all through the fleet-write relay (each a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]): pr_create → POST /repos/objectstack-ai/objectstack/pulls (PR 21400, draft, body read back byte-identical); label-write assign → POST /repos//issues/21400/assignees (huangyiirene, read back MATCHES); this os-dev-report → POST /repos//issues/21360/comments. Plus 3 git pushes (not REST).",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: PR 21400 (this card's seat, as a patch round extending the file surface) · noted, not filed — packages/cli/src/utils/active-environment.ts header sentence 'os environments authenticating as the runtime identity is deliberate' is made false by this change; one comment line."
    ]
    }

  5. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #21400 at c97a0443 (os environments reads the os cloud login session through one resolver). Fixes #21360; landing through the queue

    domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-10-02T12:18Z

    • Contract review of record: 5952160148 on the PR, at CONTRACT_REVIEW_TIER, head c97a0443, PASS.
      • Shape 1 as ruled: one resolver (createControlPlaneApiClient), and no per-command copy. credentials.json comes first where it targets the same server, then cloud.json.
      • No token crosses servers: the cloud bearer never leaves for a server cloud.json does not name, which is pinned as five refusals with zero requests. The credentials.json token never goes to the cloud server while the two files name different servers.
      • The switch/create write gate keeps a cloud environment id out of a file that names another server, and nothing an os login user relied on is lost.
      • os package publish rightly stays on its documented cloud-only lane.
      • Published text: the README, the active-environment.ts header, cli.mdx, publish-and-preview.mdx and environment-routing.mdx hold nothing false.
    • Seat patch round (same claim, 5950327876):
      • Five subcommands that refused a cloud.json-only HOME now accept it, an accept-set widening. So the changeset moved to minor with Clause-②: yes (widening), and the seat corrected the PR body's line 2. No BREAKING banner and no ADR-0087 marker are owed; the review searched for a hidden narrowing and found none.
      • The active-environment.ts header sentence this change falsified was corrected: the file surface widened by that one sentence.
    • Seat verification:
      • Checks on c97a0443, collapsed latest-per-name: 34 names, 29 success, 5 skipped, 0 red.
      • The net diff is 13 files, +571 / −50. check-governed-merges --pr 21400 reads not governed. mergeable_state reads clean.
      • Fixes #21360 is the only closing keyword. Shape 3 is out of scope by the ruling.
    • Residuals, none blocking (Acceptance notes):
      • With cloud.json alone, OS_TOKEN set and no url, the target moves from http://localhost:3000 to cloud.json's url. This is the ruling's own consequence, and the README row names the --url remedy. Not pinned.
      • The ApiClientOptions.environmentId TSDoc still names only credentials.json.
      • switch's "(also recorded …)" wording on a cloud-only run is loose, not false.
    • Landing: not governed, so pr_ready and automerge_enable follow in this act.

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21400 → 4b20c84748 (os environments runs on the os cloud login session through one resolver). Fixes #21360: the card is complete

    domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-10-02T12:43Z

    • Landing reading: 4b20c84748 is on origin/main as a single-parent squash: 13 files, +571 / −50. Its git patch-id --stable is e33106d171b4…, equal to the reviewed net diff at c97a0443.
    • The card: closed as completed by this merge. pm:dispatched is removed in the same act as this note.
    • Not this card: shape 3 (one stored session) stays the maintainer's to raise, per triage's ruling 5947754514.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:clipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions