|
| 1 | +--- |
| 2 | +'@objectstack/mcp': patch |
| 3 | +--- |
| 4 | + |
| 5 | +Connect an Agent is reachable from the Account app, so a non-admin can mint their own key |
| 6 | + |
| 7 | +`POST /api/v1/keys` mints a `sys_api_key` bound to the **caller**, and the |
| 8 | +Connect-an-Agent page says the key "acts as you". But the page's only navigation |
| 9 | +entry sat in the Setup app, which declares `requiredPermissions: |
| 10 | +['setup.access']` — so every non-admin following the shipped two-step guide, and |
| 11 | +every reader of the runtime's own error text (`packages/mcp/src/plugin.ts`: |
| 12 | +*"mint an API key (Setup → Connect an Agent, or POST /api/v1/keys)"*, and |
| 13 | +`README.md`), stopped at step 1 while the endpoint behind the button had accepted |
| 14 | +them all along. Measured before: a principal with no system permissions gets |
| 15 | +`403 PERMISSION_DENIED` on `GET /api/v1/meta/apps/setup` and `nav_connect_agent` |
| 16 | +is absent from the wire. |
| 17 | + |
| 18 | +`CONNECT_AGENT_UI_BUNDLE` now carries a **second** `navigationContributions` |
| 19 | +entry, targeting the `account` app's `grp_account_developer` group beside the |
| 20 | +`nav_account_api_keys` entry already shipping there. Measured after, over the |
| 21 | +real composition (real `SETUP_APP` / `ACCOUNT_APP` / `SETUP_NAV_CONTRIBUTIONS`, |
| 22 | +the real fold and the real RBAC-by-route filter): the same permissionless |
| 23 | +principal gets `200` on `GET /api/v1/meta/apps/account` with |
| 24 | +`grp_account_developer` carrying `['nav_account_api_keys', |
| 25 | +'nav_account_oauth_apps', 'nav_connect_agent']`, while `apps/setup` still |
| 26 | +answers `403 PERMISSION_DENIED` with `connect_agent` absent from that body. |
| 27 | + |
| 28 | +**Nothing else moves.** No backend change, no authorization change, no change to |
| 29 | +which permissions exist, and the published "acts as you" promise is unchanged — |
| 30 | +it simply becomes keepable for the users it was written for. The Setup entry |
| 31 | +stays exactly as it was, so admins keep the page where the guide points, and no |
| 32 | +gate is added or removed anywhere: a navigation contribution registers exactly |
| 33 | +when the page registers, so an opted-out deployment |
| 34 | +(`OS_MCP_SERVER_ENABLED=false`) still gets no page and neither entry. |
| 35 | + |
| 36 | +⛔ Ungating Setup was **not** the fix, and was measured rather than assumed: the |
| 37 | +app-level `setup.access` gate fires before the group gate, so dropping the group |
| 38 | +gate alone changes nothing, and dropping both serves 14+ unrelated Setup |
| 39 | +surfaces (Users, Organization, Business Units, Branding, Feature Flags, …) to |
| 40 | +every signed-in user. ⛔ Nor was a `requiresService: 'mcp'` gate on an |
| 41 | +`account.app.ts` entry: the `mcp` service registers unconditionally in `init()` |
| 42 | +while this bundle registers behind `isMcpServerEnabled()`, so such an entry |
| 43 | +would outlive its page and 404 for every signed-in user on an opted-out |
| 44 | +deployment. |
| 45 | + |
| 46 | +Both entries deliberately share the item id `nav_connect_agent` — one |
| 47 | +destination, one identity. That is scoped, not a collision: `SchemaRegistry` |
| 48 | +keys contributions by target app and `applyNavContributions(app)` consults only |
| 49 | +that app's bucket, so a nav item id is unique within one app's navigation tree, |
| 50 | +and the translation bundles are keyed `apps.<app>.navigation.<id>`. |
0 commit comments