Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

cloud-connection panel: Disconnect lands on "Not connected" even when the server answers revoked:false, so the cloud-side connection stays active with no warning #12860

Closed as not planned

Description

@objectstack-fleet

Filing gate: ① a user-visible defect with a named landing and a measured reach. Filed by objectui domain:devx seat 1 (#5748), held by the same session as objectstack domain:devx seat 2 · os-sales · session_016PVjjuVPte5XEJvaQx1E4t. It comes from the out-of-scope findings of objectstack-ai/objectstack#24066 (PR objectstack-ai/objectstack#24147).

Reach

The objectstack#24066 dev measured the server half on a dev boot of examples/app-showcase at objectstack main 9ffe8af09, with the control plane unreachable:

  • POST /api/v1/cloud-connection/unbind answered {revoked:false, cleared:true}.
  • The server dropped its credential, but the cloud never heard the revoke, so the cloud-side connection stays active.

The panel half is read from code. No console was built for a browser check.

  • disconnect() ignores revoked.
  • The panel then lands on "Not connected" with no sign that the cloud account still lists the connection.

Done when

  • The panel tells the operator when the revoke did not reach the cloud. When unbind answers revoked:false, the panel shows the server as disconnected and says the cloud-side connection may still be active and should be removed in the cloud account. The message uses the existing i18n and alert blocks, with no new widget.
  • revoked:true is unchanged.
  • A test pins both answers.

The operator page for this flow is /docs/deployment/cloud-connection#disconnect (objectstack-ai/objectstack#24066), which already tells operators to check the cloud account.


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Oct 17, 2026
  2. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, priority:p3 · area:devpath · pm:queue · the filer's domain:ui stands · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-17T06:55Z

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 (priority:p3 bug, area:devpath; filed by objectui domain:devx seat 1 from objectstack-ai/objectstack#24066's findings and graded by the triage seat, 6230920138: the filer's domain:ui stands, and both unbind answers are covered now that objectstack-ai/objectstack#24148 has landed)
    Session: session_01SJn1GEkrbbBQkhGC26wEDw
    Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-12860-unbind-revoke-notice
    Worktree: objectui-issue-12860 (in the cloud session's own container)
    Domain: domain:ui
    Seat: domain:ui#4
    File surface:

    Stop on breach; explain in the report.
    Container & model: S, mode:cloud, model: opus (cloud on the maintainer's stance 「可以的话,ui 后续开发尽量用云 session,并且要求截图给我看」; lane scope on 「你也可以接ui车道的卡」; batch 3 on 「当前任务处理完,后续并发降到3」, this session's chat, 2026-10-16)
    Clause-②: yes
    Clause-② reason: one or two new @object-ui/i18n keys under cloudConnection in all ten packs. No exported type or prop moves.
    Responsibility: n/a — after a Disconnect whose revoke never reached the cloud, the panel says "Not connected" and nothing warns that the cloud still lists the connection
    Thread-read: 6230920138
    Serial constraints cleared:

  4. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 12860,
      "status": "needs_decision",
      "branch": "claude/issue-12860-unbind-revoke-notice",
      "pr": null,
      "session": "session_01B9twfw7wdL98RoK5XCyGXZ (dispatched by PM seat domain:ui#4, session_01SJn1GEkrbbBQkhGC26wEDw)",
      "premise_still_valid": true,
      "summary": "The code for rulings 1-3 and 5 is done and pushed (ada4643): disconnect() reads the unbind answer. revoked:false lands on Not connected with a warning notice. revoked:true is unchanged. A 403 PERMISSION_DENIED keeps the bound card and shows a localized 'needs Setup access' line. I am not handing it over as done, because ruling 4 (put the four new strings under cloudConnection) cannot pass a repo rule. ADR-0060 lists cloudConnection as a frozen namespace, and AGENTS.md section 1 says no key may be added under it in any locale. packages/app-shell/src/__tests__/product-neutral-renderer.ratchet.test.ts fails all 10 per-locale cases ('no key is added under a frozen namespace unless the live list carries it'). AGENTS.md bans appending to product-specific.allowlist.json to make it pass, and raising the ceiling is an ADR-0060 amendment (a governed file) that the maintainer must approve. That file is also outside the claimed file surface. I checked all four PM mechanism assumptions on main b279016 and all four hold. On main, a 403 already shows the error phase with the server's English message, not 'Not connected'.",
      "tests": "At HEAD ada4643. New file packages/app-shell/src/console/cloud-connection/__tests__/CloudConnectionPanel.unbindAnswer-12860.test.tsx, 7 cases, all passing: revoked:false shows the notice (en), revoked:false shows the notice (zh), 403 setup.access is not Not connected and is localized (en), the same (zh), revoked:true shows no notice, the notice survives an unbound refresh and clears on the next Connect, and CONTROL: a held-session 403 (PASSWORD_EXPIRED, a real AuthGateCodeSchema member) keeps the error phase and the server's message. Full run 'pnpm exec vitest run packages/app-shell/src/console/cloud-connection/ packages/i18n/ --maxWorkers=2' under os-verify-lock: VERDICT command-exit 0, Test Files 88 passed (88), Tests 1347 passed | 13 skipped (1360). CONTROL: the bind-flow pins in bindError, bindErrorLocale, displayLocale-10331 and unboundBody-12655 pass unchanged inside that run. Ablation: I committed the fix first, then used a Python replacer that asserts its anchor appears exactly once, with a trap that restores via 'git checkout HEAD --'. The packs and panel resolve from source through the vitest alias (@object-ui/i18n points to packages/i18n/src), so no dist rebuild was needed. A1: revoked===false changed to a constant false; 3 cases went red (notice en, notice zh, survives/clears). A2: constant true; revoked:true went red. A3: SETUP_ACCESS_REFUSED changed to '__never__'; 403 en and zh went red. A4: removed the setRevokeNotReached(false) call in connect(); survives/clears went red. A5: matched any code instead of PERMISSION_DENIED; CONTROL went red. Each case went red exactly as predicted. Afterwards 'git diff HEAD' was empty (diff-empty=yes). For A4 the grep -cF count reads 2/2 because the anchor spans two lines; the proof for A4 is the assert-once anchor plus the red result. Not passing: product-neutral-renderer.ratchet.test.ts exits 1 with 10 failures. That is the blocker described in open_questions.",
      "gates": "Each exit code captured before any pipe. pnpm exec vitest run (cloud-connection/ + packages/i18n/) = 0. vitest product-neutral-renderer.ratchet.test.ts = 1 (10 failed, BLOCKER). pnpm --filter @object-ui/i18n type-check = 0. @object-ui/app-shell type-check = 0 (echoed 'tsc --noEmit && tsc -p tsconfig.test.json'). @object-ui/i18n build = 0. @object-ui/app-shell build = 0. Both builds ran after the dependency closure built: 'turbo run build --filter=@object-ui/app-shell^... --concurrency=2' under the lock, VERDICT command-exit 0, 28/28. A first i18n build attempt before the closure existed exited 2 with TS6305 (core dist not built); that is not a measurement. Lint, normal mode: app-shell 'eslint src/console/cloud-connection/' = 0, 6 warnings, all no-explicit-any on existing catch (err: any) sites or the existing mount-effect setState, none on added lines. 'pnpm --filter @object-ui/i18n lint' = 0 (33 warnings). 'eslint packages/i18n/src/locales/' = 0. pnpm check:i18n-keys = 0. pnpm check:i18n-drift = 0 ('No en value changed in this range'). node scripts/check-changeset-presence.mjs = 0. Narrowed, declared: I linted app-shell over the touched directory only, not the whole package; repo-wide 'pnpm lint' is CI's. CI check-runs: NOT MEASURED, because no PR is open (degraded channel).",
      "mcp_calls": "3: claude-code-remote add_repo (objectstack, access read; answered read_available, nothing attached); github issue_read method=get #12860; github issue_read method=get_comments #12860. No write tools.",
      "api_writes": "0 REST writes. git push only: feature branch twice (the empty branch, then ada4643), evidence branch once (fa619dc).",
      "open_questions": [
        {
          "question": "Ruling 4 (four new keys under cloudConnection) conflicts with ADR-0060's frozen namespace, and product-neutral-renderer.ratchet.test.ts fails in all ten locales. Which way should the copy land?",
          "options": [
            "A. The maintainer approves an ADR-0060 frozen-baseline amendment that admits exactly cloudConnection.bound.disconnectNeedsSetupAccess and cloudConnection.unbound.revokeNotReached.{disconnected,cloudMayList,removeInCloud} (a governed docs/adr PR the seat drafts). Then this branch adds the same four entries to product-specific.allowlist.json; the PM widens the file surface by that one file. Real business need: measured, see live evidence (the real showcase /unbind answered revoked:false). Long-term: grows a frozen product surface by four leaves, against ADR rule 3 ('may only shrink'), but through the ADR's own approval path, not a workaround. AI-error axis: best of the options, because the ratchet stays strict and the exception needs a named human approval. Startup focus: smallest change that delivers the card. Cost: one governed ADR edit, plus one allowlist edit here.",
            "B. Move the copy into a new neutral namespace (for example a generic connection-revoke block) and rephrase it to drop the product name. The ratchet goes green with no ADR edit. Real business need: same as A. Long-term: this is the loophole ADR-0060 names ('product copy filed under a neutral name ... is review's to catch'), and the panel itself stays product code. AI-error axis: worst, because it teaches routing around a gate by renaming. Startup focus: no added scope, but it is a workaround. Not recommended.",
            "C. The producer composes it: the cloud-connection plugin's page metadata carries the copy (ADR-0060 rule 1), which needs a contract door for a conditional, answer-driven notice in objectstack (needs:contract-review). Long-term: the north-star shape. Startup focus: far larger than a p3 needs. Not now.",
            "D. Close as won't-do-in-UI: the operator page /docs/deployment/cloud-connection#disconnect already tells operators to check the cloud account, and a 403 already avoids 'Not connected' on main. Startup focus: strongest. Real business need: weakest, because the panel keeps showing a misleading 'Not connected' with no hint."
          ],
          "recommendation": "A, because the defect is a measured, user-visible false state ('Not connected' while the cloud still lists the connection). ADR-0060's ratchet exists to make exactly this kind of growth a named, human-approved exception, which is stricter than B's rename and much smaller than C. Under A the branch needs one more commit (four allowlist entries), and the pr_body_draft first line changes to 'Fixes #12860'."
        },
        {
          "question": "Choices I made under the rulings, recorded for review (not blocking).",
          "options": [
            "403: I built the preferred shape. The bound card stays and an AlertCircle row in destructive tokens sits above Disconnect. The match is on error.code === 'PERMISSION_DENIED', not on status 403, because ADR-0069's held-session refusal is also a 403 with its own code and an actionable message; that refusal keeps today's error phase (the CONTROL case pins this).",
            "Notice: an AlertCircle row in border-warning/40 bg-warning/10 with the icon in text-warning (the --color-warning theme token from apps/console/src/index.css; the body text stays foreground for contrast), role=status. The notice state is held outside Phase, so it survives any refresh that finds the server unbound and clears on Connect or on a bound status.",
            "No docs link: I measured zero docs URLs in the panel and in app-shell (no docs base exists), so I invented none.",
            "Three sentences, one key each, joined with a space. In zh this leaves a space after the full stop ('。 '), visible in the after screenshot; the maintainer may prefer different joining for CJK.",
            "One changeset file lists both @object-ui/app-shell and @object-ui/i18n as patch."
          ],
          "recommendation": "Keep as built unless the maintainer wants the CJK join changed."
        }
      ],
      "out_of_scope_findings": [],
      "deviations": [
        "Ruling 4 cannot pass ADR-0060 / AGENTS.md section 1 (frozen namespace cloudConnection); hence needs_decision. I did not append to the allowlist (banned, and outside the file surface).",
        "Contract conflict, file vs dispatch: os-dev.md says the dev opens the draft PR and posts os-dev-report on the card first. I followed the dispatch's degraded channel (no PR, no card comment, body below), because the fleet-write relay it names is refused (403) in this container. I did not probe the relay myself.",
        "Worktree: the shared checkout had the branch name checked out with no commits of its own. I detached the shared checkout and deleted the empty local branch, then ran the dispatch's 'git worktree add' command as given.",
        "Lock path: AGENTS.md names /home/user/objectstack/scripts/pm/os-verify-lock.sh, but in this container the read clone lives at /home/user/objectstack-ai/objectstack, so I used that path. objectui has no scripts/ablation-replace.mjs or ablation-dist-preflight, so I used an inline assert-once replacer with a trap; no dist was involved because tests resolve from source.",
        "Live run: start-backend.sh boots with OS_CLOUD_URL=off, which does not mount CloudConnectionPlugin (/status answered 404 ENDPOINT_NOT_FOUND). I stopped it and restarted the same prepared showcase (published @objectstack 18.0.0-next.2) on :4610 with OS_CLOUD_URL=http://127.0.0.1:9, an unreachable control plane, so the plugin mounts."
      ],
      "files_changed": [
        "packages/app-shell/src/console/cloud-connection/CloudConnectionPanel.tsx",
        "packages/app-shell/src/console/cloud-connection/__tests__/CloudConnectionPanel.unbindAnswer-12860.test.tsx (new)",
        "packages/i18n/src/locales/{en,zh,ja,ko,de,fr,es,pt,ru,ar}.ts (+4 keys each under cloudConnection)",
        ".changeset/12860-cloud-connection-unbind-answer.md (app-shell + i18n, patch)"
      ],
      "screenshots": [
        "before (main b279016 panel, real unbind answered revoked:false, zh-CN light): https://github.com/objectstack-ai/objectui/blob/fa619dc88b8efe6aaedafb47504fa8b1eb9ddd19/docs/design/findings/12860/before-main-revoked-false-zh.png?raw=true",
        "after (branch, same real answer, notice shown): https://github.com/objectstack-ai/objectui/blob/fa619dc88b8efe6aaedafb47504fa8b1eb9ddd19/docs/design/findings/12860/after-branch-revoked-false-zh.png?raw=true",
        "after (branch, 403 PERMISSION_DENIED fulfilled, bound card plus localized line): https://github.com/objectstack-ai/objectui/blob/fa619dc88b8efe6aaedafb47504fa8b1eb9ddd19/docs/design/findings/12860/after-branch-403-zh.png?raw=true",
        "bound card before clicking Disconnect (status fulfilled): https://github.com/objectstack-ai/objectui/blob/fa619dc88b8efe6aaedafb47504fa8b1eb9ddd19/docs/design/findings/12860/bound-before-disconnect-zh.png?raw=true"
      ],
      "line_budget": "n/a",
      "pr_title_draft": "fix(console): Cloud Connection panel reads the unbind answer on Disconnect (objectui#12860)",
      "pr_body_draft": "(held by the seat with the branch until the decision below is ruled; the dev's draft is kept for the PR the ruling opens)",
      "head": "ada4643"
    }

    Relayed by seat domain:ui#4 (session_01SJn1GEkrbbBQkhGC26wEDw) from the cloud session's final report. This is the degraded channel: the session pushed claude/issue-12860-unbind-revoke-notice at ada4643 and returned status: needs_decision, so no PR is opened yet. The conflict is the seat's own ruling 4 (the new copy under cloudConnection), which ADR-0060 freezes. The seat measured it before writing the decision that follows this comment.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Decision — Cloud 连接面板的断开提示:新文案放哪 · 2026-10-17T13:33Z

    维护者速读: 这张卡的代码已经写完并推上分支(ada4643),7 个新测试全过。卡住的是文案放哪,这是席位派发时犯的错:

    • 我让开发把 4 句新文案放进 cloudConnection 这个 i18n 命名空间。
    • 但 ADR-0060(渲染器保持产品中立)把 cloudConnection 列为冻结命名空间,规则 3 写明冻结面"只能缩小"。所以 product-neutral-renderer.ratchet.test.ts 在 10 个语言里全红。
    • AGENTS.md 禁止往白名单追加条目来让测试变绿。唯一合规的路是你批准一次 ADR-0060 修订,而 ADR 是受治理文件。

    所以要你定:这 4 句文案怎么进来,还是不做。

    做出来的效果(showcase 后端真实返回 revoked:false,zh-CN;证据分支,不合入):

    • 改前:点断开后直接显示「未连接」,没有任何提示。截图
    • 改后:「未连接」上方多一条琥珀色提示:「此服务器已断开连接。未能通知 ObjectStack Cloud,因此你的云账户中可能仍列有此连接。请在云账户中移除该连接。」截图
    • 无权限(403):连接卡片保持「已连接」,下面一行红字:「断开此服务器的连接需要 Setup 访问权限。」截图。这一行目前用的 key 也在 cloudConnection 下,同样被冻结。

    What was measured (dev report relayed above, objectui ada4643):

    • docs/adr/0060-product-neutral-renderer.md rule 3 says the frozen surface "may only shrink". Its frozen-baseline block lists cloudConnection with 26 keys.
    • AGENTS.md: "⛔ Never append to that list to turn the test green … growing it is an ADR amendment the maintainer approves."
    • docs/adr/** is a governed surface (check-governed-queue-guard).
    • The four new keys:
      • cloudConnection.unbound.revokeNotReached.disconnected
      • …cloudMayList
      • …removeInCloud
      • cloudConnection.bound.disconnectNeedsSetupAccess
    • The ratchet test is red in all ten locales. Every other gate is green (88 files / 1347 tests, type-check, build, lint, the i18n checks).
    选项 做什么 用户看到什么 工作量
    A 你批准一次 ADR-0060 修订:冻结基线里给 cloudConnection 加这 4 个 key,同一个 PR 往白名单加同样 4 条 上面截图的效果:断开没通知到 Cloud 时有提示,无权限时不再误显示「未连接」 小。分支再加一个提交(ADR 基线 + 白名单各 4 行)。PR 因为碰 ADR 变成受治理,要你批准合入
    B 换一个中立的命名空间,改写文案去掉产品名 提示还在,但说不清是哪个云 小。但这正是 ADR-0060 点名的漏洞:给产品文案换个中立名字绕开门禁
    C 由 cloud-connection 插件在页面元数据里提供文案(ADR-0060 规则 1 的终局形态) 同 A 大。objectstack 要先开一个新的契约口子,要做契约审查。对一张 p3 太重
    D 不做,关卡 照旧:断开没通知到 Cloud 时只显示「未连接」。操作文档里已经写了"去云账户检查" 零

    业务含义:

    • A 等于为一个真实的误导状态,按 ADR 自己的审批流程开一个小口子,加 4 句话。
    • B 等于换个名字绕过门禁。
    • C 等于为 4 句提示先去改平台契约。
    • D 等于接受这个误导,让用户靠文档自己发现。

    四轴(业务立场):

    • 实际业务需求(读数领起): 实测 showcase 真实返回了 revoked:false:服务器丢了凭据,但 Cloud 不知道,面板却说「未连接」。这是用户看得到的误导状态,不是猜测。触发条件是断开那一刻控制面不可达,所以定 p3。
    • 项目长远合理性: C 是终局,但太重。A 让冻结面多 4 个叶子,违背规则 3 的"只缩不增",但走的是 ADR 规定的审批路,不是绕路;等这块面板迁到元数据驱动,这 4 条会随迁移一起删掉。B 是绕路。
    • 防 AI 犯错: A 让门禁保持严格,例外有你的名字。B 会教 AI"换个名字就能过门禁",最差。
    • 创业阶段不扩散: D 最省。A 是交付这张卡的最小改动。C 最大。

    推荐: A;回退: D。 置信缺口:A 会让 ADR-0060 的冻结面第一次增长。如果你更看重"冻结面只缩不增"这条原则,就选 D,它是诚实的选择:提示没了,但操作文档仍然告诉运维去云账户检查。
    自检:只看①选 A;②③④是否翻转:②在意冻结面原则时翻向 D,④在意成本时翻向 D,③不翻。

    os-decision-facets

    • ① 实际业务需求:实测 revoked:false 时面板误显示「未连接」,用户看得到。
    • ② 项目长远合理性:A 走 ADR 审批路给冻结面加 4 条,迁移时随之删除;C 是终局但重;B 是绕路。
    • ③ 防 AI 犯错:A 让门禁严格、例外具名;B 教 AI 改名绕门禁。
    • ④ 创业阶段不扩散:D 零成本;A 最小交付;C 最大。

    After the ruling:

    • A: the seat sends the same dev branch one more commit, and the PR then carries Fixes #12860 and the ADR edit, so it is governed and waits for your approval to merge:
      • the ADR-0060 frozen-baseline block admits exactly the four keys, citing this ruling;
      • product-specific.allowlist.json gets the same four entries;
      • the zh join drops the space after 「。」, which the dev flagged.
    • D: the seat closes this card as not planned with this record, and the branch ada4643 stays unmerged.
    • B or C: the seat records the ruling and re-scopes the card. C files the objectstack contract card first.
    • In every case this card leaves needs-user-decision in the same write as the ruling record.

    The claim 6234200417 is released in this act: the card is back on needs-user-decision, with no assignee. The feature branch claude/issue-12860-unbind-revoke-notice (ada4643) holds the finished code. The evidence branch claude/issue-12860-evidence (fa619dc) is never merged.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling — D · close as not planned · 2026-10-17T16:04Z

    The maintainer, in seat domain:ui#4's session chat (session_01SJn1GEkrbbBQkhGC26wEDw), answering decision 6234491015 verbatim: 「D」.

    What D means here:

    • Nothing ships for this card. The four new cloudConnection.* keys would have grown ADR-0060's frozen surface, which rule 3 says "may only shrink". The maintainer kept that rule over the notice.
    • ADR-0060, its frozen baseline and product-specific.allowlist.json are unchanged.
    • The misleading state stays as it is on main. When unbind answers revoked:false, the panel lands on 「未连接」 with no notice. When it answers 403, the panel also lands on 「未连接」. The operator page /docs/deployment/cloud-connection#disconnect (docs: no page tells a self-hosted operator how to connect their ObjectOS server to ObjectStack Cloud (Setup → Cloud Connection → Connect, then approve in the cloud account) objectstack#24066) still tells operators to check the cloud account after a disconnect.
    • The finished code stays unmerged on claude/issue-12860-unbind-revoke-notice (ada4643), and the screenshots stay on claude/issue-12860-evidence (fa619dc). No PR is opened. The dev session session_01B9twfw7wdL98RoK5XCyGXZ stays archived.
    • If this surface is ever re-opened, the route is the one ADR-0060 rule 1 names: the cloud-connection plugin supplies its copy through page metadata (decision row C). It is not a renamed neutral namespace (row B).

    State change, in this one write:

    • this record;
    • needs-user-decision removed;
    • the card closed as not_planned.

    No assignee and no pm-state are left on the card.


    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

No one assigned

    Labels

    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions