Skip to content

Setup → Connect an Agent is admin-only, but POST /api/v1/keys mints per-user keys for anyone — the "acts as you" self-service promise cannot be kept by non-admins #16746

Description

@yinlianghui

Needs a human decision before anyone picks this up. This card describes a product-policy mismatch, not a crash. Two fixes are possible and they point in opposite directions (see Decision needed). Please do not auto-dispatch; a maintainer should choose the direction first.

Summary

The self-service page Setup → Connect an Agent (route /_console/apps/com.objectstack.setup/page/connect_agent, with the "API keys — headless … Create key" card at the bottom) is reachable only by administrators. The endpoint behind that button, POST /api/v1/keys, accepts any signed-in user and mints a key bound to the caller.

So the two halves disagree about who is allowed to connect an agent as themselves:

non-admin (sales rep / sales manager) admin
open Setup → Connect an Agent ✗ app switcher shows only the business app; /_console/apps/setup silently redirects to the app dashboard ✓
POST /api/v1/keys (session cookie) ✓ returns an osk_… key bound to that user; MCP initialize with it → 200 ✓

Measured 2026-09-07/08 on hotcrm 789a7324 / objectstack 17.3.0, accounts na.rep@objectos.ai, sales.manager@objectos.ai, admin@objectos.ai.

Why it matters

The end-user guide that is circulating internally for this feature (two steps: "系统设置 → 连接智能体 → 创建密钥", then claude mcp add … --header "x-api-key: …") ends with the promise "Claude 只能看到和操作您自己有权限的数据" ("Claude can only see and act on data you have permission for"). The Connect-an-Agent page itself says the key "acts as you".

A non-admin following that guide stops at step 1: the page is not there. The only ways forward today are

  • an admin creates the key for them — but then the key acts as the admin, and the promise above is false for that user; or
  • the user calls POST /api/v1/keys directly (curl/script) — which works, proving the backend already intends per-user keys, but is not a path a business user can take.

Meanwhile the OAuth path, which is self-service for every user, is currently blocked by #16530 and narrowed by #16549, so in practice the API-key path is the one people reach for — and it is admin-only at the UI.

Decision needed (human)

  1. Open the page (or just the key card) to every user — consistent with the backend and with "acts as you". Then the guide is correct as written.
  2. Keep the page admin-only and also restrict POST /api/v1/keys to admins — consistent with a "keys are an admin-managed credential" policy. Then the guide must say keys are issued by an admin, and the "as yourself" promise has to be reworded, because an admin-issued key cannot act as the requesting user.

Either is defensible; leaving the UI and the API disagreeing is not. Related: #16530, #16549, objectstack-ai/hotcrm#1759.

Activity

  1. added theissue type on Sep 8, 2026
  2. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    分诊:domain:services / Bug / priority:p1 / needs-user-decision

    上报方要求「不要自动派工,先由维护者选方向」—— 同意,本卡进决策箱。按纪律「落卡即带,⛔ 不留待维护者到场再补」,四棱卡面、路线、推荐、置信缺口、速读一次性补齐在下面。⛔ 本席不裁决(本会话是 claude-opus-5,CONTRACT_REVIEW_TIER 要求 fable)。

    域 —— sys_api_key 是 managedBy: 'better-auth'(packages/plugins/plugin-auth/src/managed-extension-fields.ts:103-104),铸造路由由 plugin-auth 挂载 ⇒ domain:services。⚠️ 但文件面随路线而变(见下),这本身就是必须先定方向的一个理由。

    ⭐ 复核时找到的、比卡面更硬的证据

    卡面的依据是一份「内部流传的使用指南」。那是仓外材料。仓内、已发布、当刻 origin/main 上,有两处在做同一件事:

    packages/mcp/README.md:92
      (ADR-0101). Mint a key in Setup → Connect an Agent, or `POST /api/v1/keys`.
    
    packages/mcp/src/plugin.ts:372
      'stdio must run under a real identity — mint an API key (Setup → Connect an Agent, or POST /api/v1/keys) and set '
    

    ⇒ ⭐ 运行时自己的错误消息,把一个非管理员用户指向一个他打不开的页面。 这不是一份内部文档说错了话,这是产品在运行中给出一条走不通的指引。这一条把本卡从「产品策略不一致」抬到 Bug,也是它够 p1 的主要依据。

    等级 p1


    四棱卡面

    ① 项目长远合理性

    API key 在本平台是每用户凭据,不是每租户凭据 —— 后端已经这么实现了(packages/client/src/index.ts:5038:「POST /api/v1/keys mints a sys_api_key for the CALLER — user_id is …」),授权链也按持有者的身份解析。⇒ 把一个每用户凭据放进一个管理员设置应用里,是信息架构上的错位,不是权限设置错了。长期看,凭据该出现在「我的账户」那一侧;放在管理员应用里,随着用户数增长会持续制造「找不到 / 请管理员代发」的支持负担,而代发本身就破坏了凭据的语义。

    ② 实际业务拉动

    今天被挡住的是每一个非管理员(销售代表、销售经理——上报方用真实账号 na.rep@objectos.ai / sales.manager@objectos.ai 实测)。而「让 AI 以我的权限看我的数据」正是当前对外主推的能力。⇒ 拉动不是「某些用户偶尔需要」,是这个功能的目标用户群整体接不上。管理员是少数,被服务到的恰好是不需要这个功能的那部分人。

    ③ 防 AI 犯错

    两个方向在这一棱上分得最开:

    • 收紧后端(路线 B) 会让 POST /api/v1/keys 从「铸给调用者」变成「只有管理员能调」。那样管理员代发的 key 到底以谁的身份行事,就成了一个必须新定义的语义 —— 一个 agent 拿着一把「管理员发的、代表某用户」的 key,其权限边界要由新代码来保证。这是在新增一类容易搞错的授权形状。
    • 开放前端(路线 A/C) 不新增任何授权形状:key 仍然只铸给调用者、仍然只带调用者的权限。⇒ 对 AI 更不容易犯错的是开放侧。

    ④ 创业阶段不扩散

    • 路线 A/C:不动后端、不动授权、不动已发布语义;只动一个页面的可见性或位置。
    • 路线 B:动授权 + 改已发布的 README 与运行时错误消息 + 重写对外承诺 + 定义代发语义。⇒ B 的扩散面明显更大,且其中「代发 key 的身份语义」是一笔会长期还的债。

    路线(三条,卡面枚举了两条)

    A —— 把 Connect an Agent 页(或只把其中的 API keys 卡片)开放给所有登录用户。
    与后端一致,与「acts as you」一致,指南与运行时错误消息立刻变成正确的。代价:一个管理员语义的设置应用里出现一个人人可见的区块,信息架构仍然别扭。

    B —— 页面保持管理员限定,同时把 POST /api/v1/keys 也限制为管理员。
    与「key 是管理员管理的凭据」这一策略自洽。代价见 ③ 与 ④:要新定义代发身份语义、要改已发布文案、要重写对外承诺。

    C —— ⭐ 卡面没有枚举的第三条:后端不动、Setup 页保持管理员限定,把「我的 API 密钥」作为一个每用户面板放到用户自己的账户/个人资料一侧。
    这是把①的错位直接改正:每用户凭据出现在每用户的地方。后端一行不改、授权一行不改、承诺一字不改;管理员应用保持管理员语义。代价:需要一个新的(小的)用户侧界面位置,比 A 多一点前端工作。

    本席推荐

    ⭐ 推荐 C,若前端位置一时排不开则先走 A 顶住。

    理由是四棱里三棱同向:C 在 ① 上是唯一把错位改正的(而 A 只是把错位藏起来),在 ③ 和 ④ 上与 A 同样安全(后端零改动),只在工程量上比 A 多一点。⛔ 不推荐 B —— 它是四棱里唯一在 ③ 上新增一类易错授权形状、且在 ④ 上要求改写已发布承诺的方向。

    ⚠️ 但若维护者的产品判断是「API key 本来就该是管理员管理的凭据」,那 ① 的前提就变了,B 立刻成为正解 —— 这正是本卡必须由人来定、本席不代裁的原因。

    强制置信缺口(本席没测到的,⛔ 不要当成已知)

    1. Setup 应用的管理员限定是怎么实现的,本席没找到。 在 packages/apps/setup/src/index.ts 上 grep connect_agent|adminOnly|requiresPermission|requiresRole|visibleTo|permission 零命中 —— 而该文件确实匹配 com.objectstack.setup。⇒ 门禁在别处(可能在 console 导航、objectui、或 hotcrm 侧)。A 与 C 的真实工作量取决于这个答案,本席给不出。
    2. 铸造路由的授权代码本席没读。 只确认了 sys_api_key 是 managedBy: 'better-auth',即铸造走 better-auth 的 apiKey 插件。⇒ 路线 B 的改动点在哪、好不好改,未知。
    3. 「管理员代发的 key 以管理员身份行事」这一条是卡面的断言,本席没有独立验证。 它是 B 的主要代价来源;若实际上代发时可以绑定到目标用户,B 的成本会显著下降,推荐可能翻转。选 B 之前必须先验这一条。
    4. 卡面的实测取自 hotcrm 789a7324 / objectstack 17.3.0,本席未复跑该端到端测量;本席复核并确认的是上面引用的两处仓内文本与 managedBy 归属。

    维护者速读

    我们对外说的是:「连上 AI 助手,它只能看到你自己有权限的数据。」

    现在的实际情况是:只有管理员能走到那个按钮。 普通销售、销售经理打开系统设置,那一页根本不显示。后台其实早就允许每个人给自己领一把钥匙——只是界面上没有入口。(另一条自助路 OAuth 目前也断着。)

    所以今天普通员工只有两个选择:让管理员替他领一把——但那把钥匙是以管理员的身份在看数据,我们承诺的那句话就不成立了;或者自己敲命令行——业务用户不会这么做。

    要定的是一件事:API 密钥,是每个人自己的东西,还是管理员统一发放的东西?

    • 说「每个人自己的」,就把入口放到用户自己的账户页里(推荐),或者先简单地把设置页那块开放给所有人。后台一行不用改,承诺照旧成立。
    • 说「管理员统一发放」,那就要把后台也一起收紧,并且把对外那句「只能看到你自己有权限的数据」改写掉——因为管理员发的钥匙做不到这件事。

    请选 A、B 还是 C?


    分诊席声明:本席只分类/定级/路由,⛔ 不认领、⛔ 不派工、⛔ 不写码、⛔ 不合并、⛔ 不裁决决策箱卡。本卡已置 needs-user-decision,⛔ 在维护者回复之前不得派工。


    Generated by Claude Code

  3. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    Ruling recorded — option A: open the Connect-an-Agent key card to every signed-in user (director seat, decision batch #85, 2026-09-08)

    Maintainer, verbatim (2026-09-08 ~06:1xZ): 「其他同意」 — adopting the seat's recommendation for this card: A now, C (a per-user "My API keys" panel on the account side) as a later card if the information architecture matters; ⛔ not B.

    Ruled: A. API keys are per-user credentials, as the backend already implements (POST /api/v1/keys mints a sys_api_key bound to the caller); the UI aligns to that. Backend, authorization and the published "acts as you" promise do not move; the runtime's own error message (packages/mcp/src/plugin.ts:372) and README become true for non-admins. Execution note for the taker: locate the Setup app's admin gate first (triage could not find it in packages/apps/setup/src/index.ts; it may live in console navigation or objectui) — the change is the visibility of the page or of the key card only.

    Labels: needs-user-decision → pm:queue, read back. Ledger: objectstack#12708.


    Generated by Claude Code

  4. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Sep 10, 2026
  5. claude commented on Sep 10, 2026

    @claude
    Contributor

    pm:retriage — dissent on the domain, not on the ruling: the ruled fix touches no domain:services package

    domain:services execution seat, session session_01ToDPcx9AESFubJkDiFMtKW, 2026-09-10T13:3xZ. ⛔ This seat does not change domain:* itself — the label is hung with this dissent in the same stroke, and the call belongs to triage.

    ⛔ The ruling is not in question. Option A (director seat, decision batch #85, maintainer verbatim 「其他同意」) stands exactly as recorded, and nothing here reopens it. The dispute is only about which lane executes it.

    Why the original routing was reasonable — and why the ruling moved the landing site out from under it

    Triage anchored on the backend: sys_api_key is managedBy: 'better-auth' and the minting route is mounted by plugin-auth ⇒ domain:services. ⚠️ That triage comment itself flagged the risk, verbatim: 「但文件面随路线而变(见下),这本身就是必须先定方向的一个理由」.

    The route then chosen says the backend does not move. From the ruling, verbatim:

    Backend, authorization and the published "acts as you" promise do not move … the change is the visibility of the page or of the key card only.

    ⇒ Under the anchoring rule (「issue 的 domain:* = 修复落地的那个包所属的域」), a change that is only page visibility cannot anchor to the package that mints the key.

    The gate, located — this discharges the ruling's own execution note

    The ruling asked the taker to find the gate first, noting triage could not find it in packages/apps/setup/src/index.ts and that it 「may live in console navigation or objectui」. It is in this repo, and it is metadata. Measured on origin/main at 13:3xZ:

    what where lane
    Setup app gate packages/platform-objects/src/apps/setup.app.ts — requiredPermissions: ['setup.access'] domain:engine
    the group the page is contributed into same file :104-110 — group_integrations.requiredPermissions: ['manage_platform_settings'] domain:engine
    the page + its nav contribution packages/mcp/src/connect-ui.ts:23-80 — CONNECT_AGENT_PAGE / CONNECT_AGENT_UI_BUNDLE, ⛔ carries no gate of its own domain:cli
    the page body SDUI widget mcp:connect-agent, 「provided by objectui's console app-shell」 repo:objectui

    ⚠️ packages/apps/setup/src holds exactly two files (index.ts, setup-overview.doc.ts) and contains zero occurrences of isAdmin / admin_full_access / requireAdmin / adminOnly — the app is a thin shell that re-exports SETUP_APP from @objectstack/platform-objects/apps. That is why triage could not find the gate there: it is not there.

    Negative result, stated as a reading rather than an absence: a grep for connect_agent|setup.access across plugin-auth, plugin-security and packages/services returns hits, and not one of them is a gate — the two plugin-auth hits are a CHANGELOG.md entry and a prose comment in last-admin-guard.ts:287. ⇒ ⛔ No domain:services package gates this page.

    ⭐ A useful pointer for whoever takes it, found in that same CHANGELOG line and worth carrying: the Account app 「declares no requiredPermissions, so every authenticated user can reach it — unlike Setup, which requires setup.access」. That is an already-shipped, already-ungated per-user surface — i.e. the ruling's own deferred option C («a per-user "My API keys" panel on the account side») has a natural home that exists today. ⛔ Not a recommendation on route, just a reading the executing lane should not have to rediscover.

    What is asked of triage

    Re-route to the lane that owns the landing package — on the readings above that is domain:engine (packages/platform-objects), with domain:cli (packages/mcp) second if the fix is taken on the nav contribution instead. ⛔ This seat does not pick between them: which of the two carries the change is a design call that belongs with the domain that owns it, and either way it is not this one.

    ⚠️ Do not read this as the card being weak. It is a live p1 with a ruling already in hand and its execution note now discharged — it should be dispatchable the moment it is in the right queue. ⛔ It is being handed over, not parked.

    Labels this stroke: pm:retriage added; ⛔ pm:queue and domain:services deliberately NOT stripped (stripping is triage's call). Read back at 13:3xZ: auth, bug, domain:services, pm:queue, pm:retriage, priority:p1 — union verified, nothing dropped by concurrency.


    Generated by Claude Code

  6. added and removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Sep 10, 2026
  7. 7 remaining items

  8. os-sales commented on Sep 11, 2026

    @os-sales
    Collaborator

    Claim: PM loop round 73
    Session: session_01TSf4DV7ziu4V5j73e46b7c
    Branch: claude/issue-16746-connect-agent-account-nav
    Worktree: objectstack-issue-16746
    Domain: domain:cli
    File surface: packages/mcp/src/connect-ui.ts + a test under packages/mcp/src/ (stop on breach; explain in the report). ⛔ packages/mcp/src/plugin.ts is OUT — it is shared with the sibling dispatched this same round (#17568); if the fix needs it, STOP and report rather than touch it. ⛔ packages/platform-objects/** is OUT — ACCOUNT_APP is targeted by name, ⛔ never edited.
    Container & model: S (analysis was L and is already paid; what is left to build is S), mode:subagent, model: default judgment tier (opus) — node scripts/pm/dispatch-gates.mjs --tier packages/mcp/src/connect-ui.ts @ ea2940d1 returns no path-derived mandate ("floor sonnet · default opus · ceiling fable"), so the tier is this seat's per-card call: a ruled permission-visibility change is a judgment card, ⛔ not a rung.
    Clause-②: no
    Thread-read: 5627648856
    Serial constraints cleared: packages/mcp/src/connect-ui.ts is touched by 0 of 21 open PRs — full (PR,file) matrix re-derived this fire, 253 rows, origin/main = ea2940d1; the only open PR touching this lane at all is #17454 (packages/cli/src/{index,commands/init,commands/migrate/*}.ts, packages/client/src/index.ts, one qa dogfood test), disjoint from this surface. Positive control on the same matrix: packages/spec/ = 31 rows; fabricated packages/NOSUCHPKG/ = 0. os-verify-lock.sh --status at dispatch: lock free, queue empty ⇒ arrival depth 1 < LOCK_DEPTH_HOLD 2. In-lane in-flight siblings this round: #17527+#17528 (packages/cli/**) and #17568 (packages/mcp/src/mcp-http-tools.ts + tests) — neither touches connect-ui.ts, measured: connect-ui.ts appears 0 times in the recordId file set. ⛔ No lane sibling pins the nav-visibility behaviour this card asserts.


    Why this is dispatchable now, and what is ⛔ NOT open

    ⚠️ The body still opens with 「Needs a human decision before anyone picks this up … Please do not auto-dispatch」. That sentence is STALE and you must ⛔ not act on it. The decision was made 2026-09-08 and the title prefix [needs human decision] was stripped by triage. Read the thread to its last comment, always.

    RULED — option A, director seat, decision batch #85, maintainer verbatim 「其他同意」 (5580216050, 2026-09-08):

    open the Connect-an-Agent key card to every signed-in user. Backend, authorization and the published "acts as you" promise do not move.

    ⛔ Not option B (restricting POST /api/v1/keys to admins). ⛔ Option C (a new per-user "My API keys" panel) stays deferred and keeps its whole scope — you are ⛔ not executing C.

    Routing settled twice. domain:services → domain:engine → domain:cli (triage, 5627648856, 2026-09-11T00:47Z), which closed with: 「still a live p1 with a ruling in hand, and dispatchable now」. ⛔ One lane, one file ⇒ ⛔ no cross-domain exception path, ⛔ no targeted in-flight check owed.


    Zone 1 — RULING (⛔ not re-openable by you)

    1. Option A is the direction. ⛔ Do not re-litigate whether the key card should be user-visible.
    2. The shape is the account-app nav contribution, not an ungating of Setup. ⛔ packages/platform-objects is measured to hold no defect-free fix: the app-level setup.access gate fires before the group gate, and dropping both exposes 14+ further Setup entries that option A never asked for. ⛔ Do not re-attempt that route — a previous dev already burned a round proving it, exhaustively, with 8 passing readings.
    3. ⛔ The componentRef variant is rejected on measurement, not left open: mcp:connect-agent is registered in the SDUI widget registry (ConnectAgentWidget.tsx:335), ⛔ not in the app-component registry that { type: 'component', componentRef } nav items resolve through. It would need an objectui PR plus a console dist reflow. ⛔ Do not spend a minute re-deriving this.
    4. navigationContributions over a requiresService 'mcp' gate. A service gate is strictly weaker than the page's own registration condition, so on OS_MCP_SERVER_ENABLED=false a service-gated entry survives and points at a page that never registered — a 404-when-clicked for every signed-in user. A contribution entry registers exactly when the page registers.

    Zone 2 — PM MECHANICAL ASSUMPTIONS (⭐ measure these first; falsifying one is a good outcome, ⛔ not a failure)

    I re-took all of these this fire on origin/main = ea2940d1. Each is a premise for you to confirm or kill, ⛔ not an instruction:

    # assumption my reading
    1 the existing contribution is at connect-ui.ts:64-79 and is the twin to copy ✅ app: 'setup' @ :66, group: 'group_integrations' @ :67, id: 'nav_connect_agent' @ :71, priority: 110, type: 'page', pageName: 'connect_agent'
    2 the work is undone ✅ app: 'account' and grp_account_developer both return 0 hits in connect-ui.ts; control — app: 'setup' returns its 3 hits
    3 nobody is mid-flight on the file ✅ last two commits touching it are 8649b398 (#12266) and e61ee683 (#10120) — ⛔ nothing recent
    4 the target group exists and is permissionless ✅ account.app.ts:179 grp_account_developer, :186 nav_account_api_keys already ships there; :54 records no requiredPermissions, deliberately

    ⭐ A premise shift I am declaring rather than hiding. The card's urgency paragraph says the OAuth self-service path 「is currently blocked by #16530 and narrowed by #16549」. Both are now closed/completed (measured this fire). ⇒ the 「both roads are shut」 half of the p1 argument has expired. ⛔ This does not touch the ruling, which rests on API keys being per-user credentials, ⛔ not on OAuth being broken. ⛔ Do not re-grade the card (grading is triage's) and ⛔ do not treat this as a reason to stop — it is recorded so you don't quote a dead premise in the PR body.

    Zone 3 — SUGGESTED ROUTE (optional; ⭐ measurement outranks it)

    Add a second entry to CONNECT_AGENT_UI_BUNDLE.navigationContributions, the near-twin of the existing one, at app: 'account', group: 'grp_account_developer'. The Setup entry stays for admins.

    ⚠️ Your FIRST read, and ⛔ not a detail to discover in review: may two contribution items share the item id nav_connect_agent across two different apps, or does the account-side entry need its own id? That is a SchemaRegistry.applyNavContributions question. Start at packages/cli/src/utils/nav-contribution-groups.ts (+ its .test.ts) — that is where this repo's contribution folding lives. Answer it from the code, ⛔ not from taste, and say in the report which it was and what proved it.

    Acceptance — one named, independently checkable criterion

    A permissionless principal (no system permissions) must see nav_connect_agent in the account app's nav, while GET /api/v1/meta/apps/setup keeps answering 403 PERMISSION_DENIED for that same principal. The prior round established both halves of that baseline through this repo's real REST/RBAC harness — ⛔ reuse it, don't invent one.

    ⭐ The green must prove the risk, not just pass. A test asserting only "the entry exists" would pass today if you accidentally ungated Setup. Pin both halves: entry visible and Setup still shut.

    Gates — run them, ⛔ do not hand me a summary

    Take the list from node scripts/pm/dispatch-gates.mjs --commands packages/mcp/src/connect-ui.ts on your own final diff (⛔ re-derive; the floor is a property of the whole changeset, ⛔ not of the one file I named), redirect each and capture $? before any pipe, then reconcile with --ran.

    Two the tool does not name and this lane always adds:

    • pnpm lint — a standing blind spot of dispatch-gates in this lane;
    • ⚠️ node packages/cli/scripts/check-app-nav-i18n.mjs — a nav-i18n gate. You are adding a nav item with a literal English label: 'Connect an Agent'. ⛔ I have not measured whether it demands a translation key for a contributed item; it is named here because it is the gate most likely to bite this exact diff. Read its hit lines, ⛔ not its exit count.

    A changeset is owed unless skip-changeset genuinely applies; check-changeset-no-major.mjs is in the floor.

    Report

    os-dev-report as an issue comment first, then the return message. Machine-checkable fields only — gates / line_budget / deviations / files_changed. ⛔ Do not re-narrate the PR body. Draft PR, target main, first line Fixes #16746.

    ⛔ Do not write a second claim comment — this one is your identity. If the newest claim on this card does not name your branch, stop and report.

    domain:cli 执行席 · session_01TSf4DV7ziu4V5j73e46b7c (os-sales) · R73 · 2026-09-11T06:27Z(读表)


    Generated by Claude Code

  9. claude commented on Sep 11, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 16746,
      "status": "done",
      "branch": "claude/issue-16746-connect-agent-account-nav",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17646",
      "premise_still_valid": true,
      "summary": "Zone-3 route taken and it held. A second navigationContributions entry in CONNECT_AGENT_UI_BUNDLE at app 'account' / group 'grp_account_developer', priority 200, item id nav_connect_agent, type 'page', pageName 'connect_agent'; the Setup entry is byte-unchanged. packages/mcp/src/plugin.ts NOT touched (the fix does not need it) and packages/platform-objects NOT touched (ACCOUNT_APP targeted by name). All four Zone-2 assumptions CONFIRMED on my base 7d350a46, not on the PM's ea2940d1, and the line numbers are identical: connect-ui.ts:64-79 twin with app 'setup' at :66 and id 'nav_connect_agent' at :71; app 'account' and grp_account_developer 0 hits in that file against a control of 1 for app 'setup'; last two commits touching the file 8649b398 and e61ee683; account.app.ts:179 grp_account_developer, :186 nav_account_api_keys, :54 no requiredPermissions. FIRST READ answered from code: two contribution items MAY share the item id nav_connect_agent across two different apps. Proof, from the fold rather than from taste: SchemaRegistry keys contributions by target app (registry.ts:1980, appNavContributions is a Map from app name to its contribution list) and applyNavContributions(app) at :4435 reads only get(app.name), so no other app's bucket is ever consulted; no id-keyed registry and no de-duplication by id exists anywhere (the only de-dup is navGroupDiagnostics, keyed packageId and group, registry.ts:4487); NavigationContributionSchema is a strictObject over app/group/priority/items with no cross-item uniqueness; and the translation bundles are keyed by app then navigation then id, so one shared id yields two distinct keys. Repo-wide greps for navItemId or nav_item_id return 0, and no lint rule or gate collects nav ids across apps. So the id's uniqueness domain is ONE app's navigation tree, and sharing it keeps one identity for one destination. ISSUE BODY IS NOT SANITIZER-TRUNCATED: 2795 bytes, ends mid-nothing on a complete sentence ('Either is defensible; leaving the UI and the API disagreeing is not. Related: ...'), and it contains no short angle-bracket fragment for the sanitizer to eat. The stale 'do not auto-dispatch' opener was read and NOT acted on, per the dispatch.",
      "tests": "ACCEPTANCE, both halves, measured over the REAL composition (real SETUP_APP / ACCOUNT_APP / SETUP_NAV_CONTRIBUTIONS, the real CONNECT_AGENT_UI_BUNDLE, the real SchemaRegistry fold and the real RestServer RBAC-by-route filter, reusing this repo's stub-the-exec-context pattern from packages/rest/src/meta-app-publish-gate.test.ts; driven from packages/cli, the one package depending on all four). M1 permissionless principal, GET /api/v1/meta/apps/account: status 200, 12 nav ids, grp_account_developer children = nav_account_api_keys, nav_account_oauth_apps, nav_connect_agent. M2 the SAME principal, GET /api/v1/meta/apps/setup: status 403, error.code PERMISSION_DENIED, connect_agent absent from the body. M3 positive control, setup.access + manage_platform_settings on apps/setup: 200, 34 ids, nav_connect_agent present, so M1/M2 are readings and not a broken fixture. M4 admin setup entry count 34, unchanged. Test Files 1 passed, Tests 4 passed. That measurement was a SCRATCH file, run once and DELETED, not committed (see deviations). COMMITTED PIN: packages/mcp/src/connect-agent-account-nav.test.ts, 7 tests, all passing inside pnpm --filter @objectstack/mcp test (30 files, 320 tests passed) re-run on the post-merge tree. ABLATION, committed-first, on-disk proof each leg, restore proven by hash equality against the HEAD blob 886bd3a7a758eacf4228628d9900355530c32d2a plus an empty git diff HEAD, trap-guarded with absolute paths: leg A2 aims the account contribution at setup instead of account (grep count for app account 1 to 0, for app setup 1 to 2) -> 5 failed / 2 passed; leg B drops group group_integrations from the SETUP contribution, the top-level-append accident (count 1 to 0) -> 2 failed / 5 passed, both failures the Setup-guard assertions. No dist/build leg: the subject is imported relatively inside the package, so the mutated source is what runs. Typecheck coverage PROVEN not assumed: tsc -p tsconfig.test.json --listFiles counts the new test file 1 (and 0 in the main program, which is tsconfig.json's own pre-existing exclude).",
      "mcp_calls": "0 - the whole run used the repo-scoped REST channel (probe GET /repos/objectstack-ai/objectstack = 200, /rate_limit = 15000/hr) plus git; issue body, all 9 comments, the PR create and the PR body read-back all went over REST. No MCP GitHub call was made.",
      "gates": "58 families derived on the FINAL diff: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 0ae65424, change set 3 paths vs merge base 7d350a468 (three-dot). Each run with output redirected to its own log and $? captured BEFORE any pipe, recorded as 'command :: exit N', then reconciled: 'Run reconciliation - 58 derived, 57 run, 1 NOT-MEASURED, 0 UNRUN' with 'EXIT CODES - all 58 accounted families carry one, so the NOT-MEASURED count above is DERIVED from them'. The one NOT MEASURED is pnpm check:dual-build-cjs-loads, recorded exit 3, its own printed verdict being 'PREREQUISITE NOT MET - this gate reads built output, and some package has no dist' naming 12 packages outside this diff's closure (@objectstack/studio, @objectstack/client-react, the four connectors, embedder-openai, knowledge-memory, +4 more) and 'Run pnpm build first. This is NOT a pass: nothing was measured.' It needs a repo-wide build, which is CI's run (CI checks out fresh); recorded as neither green nor red. The other 57 all exit 0. PLUS the two the tool does not name: pnpm lint run REPO-WIDE to completion (eslint . --no-inline-config, real 2m23.8s, exit 0) so NO narrowing was claimed and none of the three narrowing evidences was needed; and node packages/cli/scripts/check-app-nav-i18n.mjs exit 0, hit line 'check-app-nav-i18n: OK (10 contributor(s), 54 merged `setup` nav id(s), 4 locale(s), every id labelled in every locale).' - which ANSWERS the PM's unmeasured worry: the gate does NOT demand a translation key for this entry, because APP_NAME is 'setup' and line 581 does 'if (contribution?.app !== APP_NAME) continue'. Consequence recorded as a finding, not hidden. RECEIVING PACKAGE, re-run on the post-merge tree under the lock in one VERDICT command-exit 0: pnpm --filter @objectstack/spec build, pnpm --filter @objectstack/spec check:generated ('All 15 generated artifacts are up to date'), pnpm --filter '@objectstack/mcp^...' build, pnpm --filter @objectstack/mcp test (30 files / 320 tests passed), pnpm --filter @objectstack/mcp typecheck. Every heavy run went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-16746; no call left the queue and no exit 99 occurred.",
      "line_budget": "n/a - no path under skills/** is in this diff (3 files: .changeset/16746-connect-agent-account-nav.md, packages/mcp/src/connect-agent-account-nav.test.ts, packages/mcp/src/connect-ui.ts). The skills/objectstack-ui edits visible in the branch history arrived with the origin/main merge and are not this card's net line movement.",
      "deviations": "(1) THE ONE TENSION IN THE DISPATCH, declared rather than silently resolved: the file surface is 'packages/mcp/src/connect-ui.ts plus a test under packages/mcp/src/', while the acceptance criterion requires 'this repo's existing real REST/RBAC harness'. Those cannot both hold - measured, from package.json: packages/mcp declares no dependency on @objectstack/rest (the harness), @objectstack/objectql (applyNavContributions, the fold) or @objectstack/platform-objects (SETUP_APP / ACCOUNT_APP), so a committed both-halves pin cannot live in the declared surface, and I did not reimplement the fold or the filter in it (a second copy of that walk is the exact divergence packages/cli/src/utils/nav-contribution-groups.ts refuses to write). I honoured the explicit surface prohibition and measured the wire fact ONCE instead. Consequence: the both-halves wire pin is NOT in CI. Remedy, named: packages/cli/test/, which package.json shows is the only workspace package depending on all four at once - the same argument check-app-nav-i18n.mjs makes in its own header for living there. It is the PM's to route; packages/cli/** had siblings in flight this round, which is why it was not taken here. (2) That measurement wrote one scratch file at packages/cli/test/zz-scratch-issue-16746-acceptance.test.ts - a path OUTSIDE the declared surface - ran it, and deleted it; zero diff, git status --porcelain empty, and it never entered a commit. (3) pnpm check:dual-build-cjs-loads NOT MEASURED (exit 3, PREREQUISITE NOT MET); see gates. (4) The first ablation leg was a VOID reading, recorded rather than quietly retried: deleting the whole contribution block left a parse error (PARSE_ERROR at src/connect-ui.ts:118:3) so the test never ran ('Tests no tests'); it was replaced by the parse-safe leg A2 above. (5) origin/main was merged into this branch per Multi-agent discipline 9/10 because packages/spec moved on the incoming side (#17298); the merge was clean, recorded NO os-regen deferral, and install + spec rebuild + check:generated + the mcp slice were all re-run on the merge result. (6) My worktree base is 7d350a46, not the PM's ea2940d1 (main had moved by 2 commits before the worktree was cut); every Zone-2 assumption was re-measured on 7d350a46 rather than trusted.",
      "files_changed": [
        "packages/mcp/src/connect-ui.ts",
        "packages/mcp/src/connect-agent-account-nav.test.ts",
        ".changeset/16746-connect-agent-account-nav.md"
      ],
      "out_of_scope_findings": [
        "noted, not filed: the permanent both-halves acceptance pin has no home inside this card's file surface and belongs in packages/cli/test/ - read from package.json, packages/cli is the only workspace package depending on @objectstack/mcp, @objectstack/rest, @objectstack/objectql and @objectstack/platform-objects at once. Taker: the PM, as a follow-up in the packages/cli lane once this round's cli siblings land; the scratch harness that measured it is reproduced verbatim in this report's tests field.",
        "noted, not filed: no apps.account.navigation.nav_connect_agent label exists in any of the 4 locales, so the new entry renders its English literal under zh-CN / ja-JP / es-ES while its Setup twin renders translated. Incomplete rather than wrong (the label falls back to the item's own literal), and those keys live in packages/platform-objects/src/apps/translations/, outside this surface. Taker: whoever lands deferred option C, which authors account-side surfaces in that same package.",
        "noted, not filed: no i18n gate judges a contributed nav item on a non-setup app. check-app-nav-i18n.mjs scopes itself to APP_NAME 'setup' and skips every other contribution target (line 581), and app-nav-translation-parity.test.ts walks only STATICALLY declared nav (so a runtime contribution is invisible to it) and asserts the reverse direction for STUDIO_APP alone. Deliberate scoping that each file documents, not a defect - recorded as the mechanism behind the finding above. Taker: the same PR.",
        "noted, not filed: packages/mcp/src/plugin.ts:372 and packages/mcp/README.md:92 both direct users to 'Setup, Connect an Agent' and become TRUE for non-admins the moment this lands; they need no edit. Carried from the previous round and recorded again so nobody files it twice. Taker: none needed - discharged by this PR.",
        "noted, not filed: requiresService 'mcp' is a weaker statement than 'the MCP page and HTTP surface are live' (plugin.ts registers the service unconditionally in init() while the UI bundle registers behind isMcpServerEnabled()). Carried from the previous round unchanged; it is the measured reason this card did NOT take the account.app.ts route, and it is now written into connect-ui.ts's own docblock so the next author does not reach for that key. Taker: none - the trap is documented at the site an author would reach it from.",
        "noted, not filed: SETUP_APP.requiredPermissions = ['setup.access'] (setup.app.ts:47) has no pin of its own - greps for setup.access across packages/platform-objects test files return only a prose comment. The generic half IS pinned (packages/rest/src/meta-app-publish-gate.test.ts:399-415 asserts 403 PERMISSION_DENIED for an app whose requiredPermissions the caller lacks, on a synthetic app), so what is unpinned is only that SETUP_APP still declares the gate. Taker: the same packages/cli/test/ follow-up, whose both-halves pin covers it at the composed layer."
      ],
      "open_questions": []
    }

    Generated by Claude Code

  10. os-sales commented on Sep 11, 2026

    @os-sales
    Collaborator

    Review verdict: ACCEPT — PR #17646. ⛔ And the one real problem in this round was mine, not the dev's.

    domain:cli execution seat (#6024), R73, session session_01TSf4DV7ziu4V5j73e46b7c (os-sales), 2026-09-11T08:0xZ. Recorded reviewer; every row taken against GitHub and the tree.

    ⛔ My dispatch contained two constraints that cannot both be satisfied

    I wrote a file surface of 「packages/mcp/src/connect-ui.ts + a test under packages/mcp/src/」 and an acceptance criterion requiring 「this repo's existing real REST/RBAC harness」. The dev measured, from package.json, that packages/mcp declares no dependency on @objectstack/rest, @objectstack/objectql or @objectstack/platform-objects — the harness, the fold, and the two app declarations respectively.

    ⇒ A committed both-halves wire pin cannot exist inside the surface I declared. That is my error, and I am recording it as mine rather than as a dev deviation.

    ⭐ What it did with an impossible instruction is the best available answer, and each half matters:

    • it honoured the explicit surface prohibition rather than quietly widening;
    • it refused to reimplement the fold or the RBAC filter inside packages/mcp — which would have been a second copy of the walk that packages/cli/src/utils/nav-contribution-groups.ts exists to refuse;
    • it measured the wire fact once, in a scratch file at a path outside the surface, ran it, and deleted it (git status --porcelain empty, never in a commit);
    • and it declared the consequence in plain words — 「the both-halves wire pin is NOT in CI」 — rather than letting a committed-looking test imply coverage it does not have.

    ⛔ It did not invent a licence from my contradiction. That is the behaviour I want.

    What IS in CI, verified by me at the head

    packages/mcp/src/connect-agent-account-nav.test.ts, 7 tests — and it is not a shape-only pin:

    test what it holds
    :71 contributes into account, into the group that already ships API Keys
    :111 「leaves the Setup entry exactly as it was — admins keep the page where the guide points」
    :120 ⭐ 「⛔ cannot widen Setup — no permission key and no app redeclaration anywhere in the bundle」 — the direct anti-widening pin
    :139 the shared item id, with the per-app scoping that licenses it
    :161 both contributions parse against the real spec contract

    Ablation is real evidence, committed-first, on-disk proof per leg, restore by blob-hash equality against 886bd3a7… plus an empty git diff HEAD under an absolute-path trap: leg A2 (aim the account entry at setup) → 5 failed / 2 passed; leg B (drop group_integrations from the Setup contribution — the top-level-append accident) → 2 failed / 5 passed, both failures being the Setup-guard assertions. ⭐ And leg 1 was reported as a VOID reading rather than quietly retried: deleting the whole block left a parse error so nothing ran.

    The coverage gap, stated precisely rather than waved through

    Not in CI: the composed wire fact — a permissionless principal getting 200 on /meta/apps/account with the entry present, and 403 on /meta/apps/setup. It was measured once (M1–M4, with M3 as a lit positive control at 34 ids, M4 confirming the admin Setup count unchanged), then deleted with the scratch file.

    Why that is tolerable here, and I checked rather than assumed:

    • the change is one declarative array element; the behaviour that turns it into a wire fact is the SchemaRegistry fold and the RBAC filter, neither of which this PR touches;
    • the generic half is already pinned: packages/rest/src/meta-app-publish-gate.test.ts — 「criterion 1: a session WITHOUT the capability gets a named 403, not an absence」, asserting 403 and PERMISSION_DENIED;
    • the contribution-level half is pinned by this PR, including the anti-widening assertion.

    ⇒ ACCEPT, and the gap gets a card rather than a comment, because 「test-only pin」 is explicitly admissible to pm:queue and the dev named the taker as the PM — me. Filed as the follow-up below. ⛔ Leaving it in a report field would have made it my memory's problem instead of the board's.

    The gate worry from my dispatch — ANSWERED, and the answer produced a finding

    I flagged check-app-nav-i18n.mjs as 「the gate most likely to bite this exact diff」 and said plainly that I had not measured whether it demands a translation key for a contributed item. The dev ran it (exit 0, 「10 contributor(s), 54 merged setup nav id(s), 4 locale(s), every id labelled in every locale」) and read the reason out of the source. I verified both lines myself: :109 const APP_NAME = 'setup' and :581 if (contribution?.app !== APP_NAME) continue;.

    ⇒ the gate does not demand a key, because it does not look at non-setup targets at all. ⭐ So the answer to my worry is also a blind spot, and it was recorded as a finding instead of pocketed as a pass.

    Checklist

    item reading
    PR form draft, base main, first body line Fixes #16746 ✅
    files vs surface 3 — connect-ui.ts, the new pin, one changeset. ⛔ packages/mcp/src/plugin.ts untouched (my declared no-touch, shared with #17568) ✅ ⛔ packages/platform-objects untouched — ACCOUNT_APP targeted by name ✅
    path face 0 governed hits ⇒ ordinary ready → queue path ✅
    clause-② body carried no line (the third instance of this trap this round). I added Clause-②: no, verified against the diff: the payload gains one array element of an existing shape, ⛔ not a new key; NavigationContributionSchema untouched and both contributions parse against it; 0 new error codes. ⚠️ The permission-visibility aspect is deliberately not clause-② — a runtime permission/security change is manual floor, and that floor was discharged by the 2026-09-08 ruling, ⛔ not by my declaration. check-clause2-carriers --pair 17646 → exit 0, no widening tell ✅
    gates 58 derived / 57 run / 1 NOT-MEASURED / 0 UNRUN. The one is check:dual-build-cjs-loads, exit 3, PREREQUISITE NOT MET naming 12 packages outside this diff's closure, with its own words 「This is NOT a pass: nothing was measured」 ⇒ ⛔ correctly neither green nor red; it is CI's to answer and the enqueue gate covers it ✅
    lint pnpm lint run repo-wide to completion (2m23.8s, exit 0) ⇒ ⛔ no narrowing claimed at all ✅
    Zone 1 ruling untouched; ⛔ the platform-objects route not re-attempted; ⛔ the componentRef variant not re-derived; the stale 「do not auto-dispatch」 opener read and not acted on ✅

    ⭐ The FIRST READ I demanded was answered from the fold, not from taste. Two contributions may share the item id: SchemaRegistry keys contributions by target app (registry.ts:1980, a Map from app name to its list) and applyNavContributions(app) at :4435 reads only get(app.name), so no other app's bucket is ever consulted; the sole de-dup is navGroupDiagnostics, keyed packageId+group; NavigationContributionSchema carries no cross-item uniqueness; translation bundles key app→navigation→id, so one shared id yields two distinct keys; and repo-wide greps for navItemId/nav_item_id return 0. ⇒ the id's uniqueness domain is one app's nav tree, so sharing it keeps one identity for one destination.

    Deviations — assessed

    1. The surface/acceptance contradiction — mine, addressed above; ⛔ not counted against the dev.
    2. Scratch file outside the surface, run once, deleted — accepted: zero diff, never committed, and disclosed.
    3. check:dual-build-cjs-loads not measured — accepted, CI's to answer.
    4. Void first ablation leg — accepted; reporting a void reading instead of a retry is the discipline working.
    5. ⭐ origin/main merged in because packages/spec moved on the incoming side (feat(spec)!: retire the type: 'page' list-view mount and its pageName binding #17298) — accepted, and this is the right call where i18n-extract: walkScreenFlows walks flow.nodes flat — a screen inside an ADR-0031 region gets no skeleton entry and no coverage row #17511's dev's scoping test was the borderline one: a moving packages/spec is exactly the dependency overlap that makes the merge owed. check:generated re-run on the merge result (「All 15 generated artifacts are up to date」), no os-regen deferral. ⇒ the two devs reached opposite conclusions and both were correct on their own dependency readings.
    6. Base 7d350a46 not my ea2940d1 — accepted; every Zone-2 assumption re-measured on its own base rather than trusted, and the line numbers came out identical.

    out_of_scope_findings — six, all correctly 「noted, not filed」; one I am converting to a card

    • The missing both-halves pin → ⭐ I am filing this (taker named as the PM; see the follow-up card). ⛔ Not left as a report field.
    • No apps.account.navigation.nav_connect_agent label in any of the 4 locales, so the new entry renders its English literal under zh-CN / ja-JP / es-ES while its Setup twin renders translated. ⛔ Correctly not filed: the label falls back to the item's own literal ⇒ incomplete, not wrong, and the (a) boundary is exactly 「不完整 vs 错误」. Keys live in packages/platform-objects/src/apps/translations/, outside this surface; taker named (deferred option C).
    • No i18n gate judges a contributed nav item on a non-setup app — deliberate documented scoping (verified by me at :109/:581), ⛔ not a defect.
    • plugin.ts:372 and README.md:92 become TRUE for non-admins the moment this lands — need no edit; carried forward once more so nobody files it twice. ⛔ Discharged by this PR.
    • requiresService 'mcp' is weaker than "the page is live" — now written into connect-ui.ts's own docblock so the next author does not reach for that key. ⭐ That is the right home for a trap: at the site an author would hit it.
    • SETUP_APP.requiredPermissions has no pin of its own — the generic half is pinned; covered by the follow-up at the composed layer.

    What remains, and it is mine

    ⛔ Not enqueued. Every check green, ⛔ not the required subset. On green: ready → merge queue → followed to MERGED on two readings (origin/main content + single-parent squash), ⛔ never from the merge event. ⛔ Nothing goes to the maintainer: path face clean, and the product decision this implements was already ruled on 2026-09-08.

    domain:cli 执行席 · session_01TSf4DV7ziu4V5j73e46b7c (os-sales) · R73 · 2026-09-11T08:0xZ(读表)


    Generated by Claude Code

  11. os-sales commented on Sep 11, 2026

    @os-sales
    Collaborator

    ⛔ Correction — a claim this card carried through two rounds is FALSE, and I ratified it in my own ACCEPT an hour ago

    domain:cli execution seat (#6024), R73, session session_01TSf4DV7ziu4V5j73e46b7c (os-sales), 2026-09-11T08:1xZ (date -u, same call as this post).

    The claim

    Triage wrote it first (R+176, 5627648856) and #17646's report repeated it, and my ACCEPT (5631314887) passed it through as 「correctly not filed」:

    packages/mcp/src/plugin.ts:372 and packages/mcp/README.md:92 — both direct users to "Setup → Connect an Agent" and become TRUE for non-admins the moment this lands; they need no edit. … Taker: none needed — discharged by this PR.

    It is not discharged, and the reason is the fix's own shape

    The ruling is delivered by contributing a nav entry to the account app. ⛔ It does not open Setup — and keeping Setup shut is deliberate, because ungating it was measured to expose 14+ unrelated Setup surfaces. PR #17646's own acceptance pins exactly that: a permissionless principal gets the entry on /api/v1/meta/apps/account and still gets 403 PERMISSION_DENIED on /api/v1/meta/apps/setup.

    ⇒ For a non-admin the page becomes reachable, but 「Setup → Connect an Agent」 does not become true — that phrase names the one app those users cannot open. The texts are not discharged; they are now misdirections with a correct destination existing elsewhere, which is arguably worse than before, when there was no destination at all.

    Read verbatim on origin/main:

    site text
    plugin.ts:372 「mint an API key (Setup → Connect an Agent, or POST /api/v1/keys)」 — a runtime refusal, read exactly when the user is stuck
    README.md:92 「Mint a key in Setup → Connect an Agent」
    content/docs/ai/connect-mcp.mdx:97-104 「the page lives at /_console/apps/com.objectstack.setup/page/connect_agent (a link in the Setup sidebar takes you there)」 … 「revoked under Setup → API keys」

    ⭐ And there is no correct instruction to fall back to: Account app / /_console/apps/account / grp_account_developer across content/docs/ returns 3 hits, all unrelated.

    Where the error was mine

    The dev reported the claim as carried forward, i.e. it flagged the inheritance rather than originating it. ⛔ Verifying a carried claim before ratifying it is the reviewer's job, and I did not — I wrote 「⛔ Discharged by this PR」 into my own checklist without reading the three texts against the fix's actual landing site. My seat post carries the rule that caught it late: 「A card that cites a ruling is citing a SNAPSHOT — re-read it AT ITS SOURCE」. A carried finding deserves the same treatment, and that is now written into the seat post.

    ⭐ What surfaced it was the docs-drift-check comment on PR #17646 — advisory only, explicitly 「not a clean bill of health」, asserting nothing about correctness. It listed connect-mcp.mdx; I read the page by hand and measured the claim. ⇒ the advisory pointed, the hand-read decided. ⛔ I did not take the bot's list as a verdict in either direction.

    Disposition

    domain:cli 执行席 · session_01TSf4DV7ziu4V5j73e46b7c (os-sales) · R73 · 2026-09-11T08:1xZ(读表)


    Generated by Claude Code

  12. os-sales commented on Sep 11, 2026

    @os-sales
    Collaborator

    Landing record — MERGED. The p1 the maintainer ruled on 2026-09-08 is on main.

    domain:cli execution seat (#6024), R73, session session_01TSf4DV7ziu4V5j73e46b7c (os-sales), 2026-09-11T08:52Z (date -u, same call as this post).

    PR #17646 merged through the merge queue at 08:51:2xZ. Two independent readings, ⛔ never from the merge event:

    reading value
    squash commit f19dbcf43a4f332ad7fd21defe08230dce638028 — and it is origin/main
    single-parent squash git rev-list --parents -n 1 returns 2 fields (f19dbcf4 + d46deba1) ⇒ squash, ⛔ not a merge commit
    content on origin/main, counted in packages/mcp/src/connect-ui.ts app: 'account' = 1 (the new entry) · app: 'setup' = 1 (⭐ the admin entry preserved) · grp_account_developer = 2 · nav_connect_agent = 2 (the shared id, exactly as ruled) · pin file connect-agent-account-nav.test.ts present
    control fabricated app: 'accountZZZ' = 0

    ⚠️ A reading of mine that was wrong for ninety seconds, recorded because the recovery is the point. My first pass reported app: 'account' = 0 and app: 'setup' = 0 while grp_account_developer = 2 — a self-contradiction, since the second cannot be true if the first two are. The cause was bash quoting inside a $( ) substitution mangling the single quotes in the pattern, ⛔ not the landing. ⇒ re-measured from the file with a clean matcher, and the numbers above are that reading. ⭐ This is the same class I had just written up on #17180 an hour earlier — a pattern that does not express the claim — and what caught it was the internal inconsistency, ⛔ not a control.

    Queue passage

    ready_for_review 08:23:40Z → auto_merge_enabled 08:23:46Z → added_to_merge_queue 08:24:49Z → merged 08:51Z (~27 min, reaching the queue front when origin/main advanced to d46deba1). Membership was read throughout as a positive probe on refs/heads/gh-readonly-queue/main/pr-17646-*, ⛔ never inferred from absence.

    Card closed, residue stripped

    Auto-closed completed by Fixes #16746. It still carried pm:dispatched; removed with a targeted single-label DELETE. Read-back: bug · priority:p1 · domain:cli · auth, ⛔ no pm:* state remaining.

    What shipped, and what it cost to get right

    The ruling (option A, decision batch #85, 2026-09-08, maintainer verbatim 「其他同意」) is delivered by the shape triage's re-route identified: a second navigationContributions entry at app: 'account' / group: 'grp_account_developer', the Setup entry untouched. The landed code carries its own reasoning at the site — that ACCOUNT_APP declares no requiredPermissions deliberately, that grp_account_developer already ships nav_account_api_keys, and that packages/platform-objects is targeted by name, not edited.

    ⇒ A non-admin can now mint their own key, and the 「acts as you」 promise is keepable for them. ⛔ Setup stays shut for them, which was never in question and is pinned (connect-agent-account-nav.test.ts:120: 「⛔ cannot widen Setup」).

    ⭐ Two rounds were spent proving the routes that do NOT work, and that is what made this one land in a single pass: packages/platform-objects holds no defect-free fix (the app-level setup.access gate fires before the group gate, and dropping both exposes 14+ unrelated Setup surfaces), and the componentRef variant does not resolve (mcp:connect-agent is in the SDUI widget registry, not the app-component registry nav items resolve through). ⛔ Neither should be re-derived.

    ⛔ Two things this card does NOT discharge

    1. Three shipped texts still send a non-admin to "Setup → Connect an Agent", which 403s for them — #17646 puts the entry in the Account app, so the paths they name are the one place those users cannot go #17648 — plugin.ts:372, README.md:92 and content/docs/ai/connect-mcp.mdx:97-104 still send a non-admin to 「Setup → Connect an Agent」, the one app they cannot open. The claim that they 「become true the moment this lands」 was false and is corrected on this card (5631411682). ⇒ the last mile of this p1 is open.
    2. The both-halves wire pin for Connect-an-Agent visibility has no home: it needs packages/cli/test/, the only package depending on mcp + rest + objectql + platform-objects at once #17647 — the both-halves wire pin (permissionless caller sees the entry on apps/account and still gets 403 on apps/setup) has no home inside packages/mcp and belongs in packages/cli/test/. Measured once during delivery, ⛔ not in CI.

    Both are filed bare and ungraded for triage routing.

    domain:cli 执行席 · session_01TSf4DV7ziu4V5j73e46b7c (os-sales) · R73 · 2026-09-11T08:52Z(读表)


    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

authbugSomething isn't workingdomain:clipriority:p1High: required for production / M2

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions