Skip to content

Commit f3d2845

Browse files
feat(platform-objects): the identity actions an agent may reach declare it — additive acts exposed, destructive lifecycle acts behind confirmation, credential and ownership acts human-only (#28270)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DHbeHcgBQuozCHjdGsnhPGD)_ domain:server#3 · os-elon-musk · session_016WV4czk9umSFBVoz5cijzX Fixes #27900 Ruling-ref: 6523087116 (letter A+) Tier: H (`docs/adr/0011-actions-as-ai-tools.md`): a contract review, then an authorised approval by a GOVERNED_APPROVER, before the queue. Clause-②: no (values on an existing metadata key, platform-owned; no export, answer member, status name or error code is added, and the MCP doors' code is unchanged: their existing per-action opt-in now admits the actions that declare it) Metadata-protocol: none Skills-docs: updated here — `content/docs/ai/connect-mcp.mdx` gains one bullet (which identity actions an MCP agent sees, and what an OAuth-connected agent gets today); `grep -rniE "sys_(user|member|invitation|team|organization|session|api_key|oauth|sso|two_factor|account)\b.{0,80}(ai|agent|mcp|exposed)|(ai\.exposed|exposed).{0,120}sys_" skills content/docs` found no skill or doc naming which platform identity actions are agent-exposed. ## 维护者速读 - 改了什么:12 个身份对象上的 58 个具名动作按裁决 A+ 分三档写声明。E 档 21 个写 `ai: { exposed: true, description }`(给模型看的说明);C 档 17 个另加 `requiresConfirmation: true`,MCP 不带 `confirm: true` 就 428 拒绝、什么都不执行;H 档 20 个不写 `ai` 块,不在列表里,按名拒绝。ADR-0011 新增 2026-10-30 附记,列全三档名单和 H 档规则。没有新增任何自动检查。 - 计数更正:卡里档头写 H 19 / E 22,按卡里列的名字数是 H 20 / E 21,所以是 38 个 `ai` 块而不是 39 个。名字为准。 - 未评估:`sys_invitation.copy_invitation_link` 是评估之后才加的(复制邀请链接,链接本身就是凭据),保持不声明,等您点名裁。 - 请您看一眼:两个 E 档动作 `sys_user.enable_two_factor` / `sys_two_factor.enable_two_factor` 要用户当前密码作参数,返回认证器 URI 和一次性备用码。暴露给 agent 就意味着 agent 向用户要密码、并拿到新的登录因子,这接近 H 档的「铸造凭据」。按卡原样落了;您一句话可以把它们移到 H(删两个块 + ADR 一行)。 - OAuth agent 今天的实际情况:列表能看到 E/C 动作,C 未确认照样 428;但执行一到身份路由就 401、什么都没改(这些路由只认登录会话)。会话调用同一个动作是成功的。让 OAuth agent 真正跑通要等 #28178 的裁决,本 PR 不碰凭据边界。 - 需要你做的:Tier H,合同复审通过后需 os-zhuang 或 hotlong 批准。 ## What changes **Declarations (`packages/platform-objects/src/identity/*.object.ts`, 12 files).** Each action the ruling places in bucket E gains `ai: { exposed: true, description }`, with a description written for the model (what it does, its params, when to call it). Each bucket C action gains the same plus `requiresConfirmation: true`, and its description ends by asking the model to confirm with the human first. Bucket H actions are untouched and carry no `ai` block. Each block sits last in its action, under a one-line comment naming the bucket. | Bucket | Count | Actions | |:--|:--|:--| | E | 21 | the four `invite_user` mirrors (`sys_user`, `sys_invitation`, `sys_organization`, `sys_member`), `sys_invitation.resend_invitation`, `sys_invitation.accept_invitation`, `sys_member.add_member`, `sys_team_member.add_team_member`, `sys_team.add_team_member`, `sys_team.create_team`, `sys_team.update_team`, `sys_organization.create_organization`, `sys_organization.update_organization`, `sys_organization.rename_organization`, `sys_organization.set_active_organization`, `sys_oauth_application.enable_oauth_application`, `sys_user.enable_two_factor`, `sys_two_factor.enable_two_factor`, `sys_user.update_my_profile`, `sys_user.resend_verification_email`, `sys_sso_provider.request_domain_verification` | | C | 17 | `sys_user.ban_user`, `sys_user.unban_user`, `sys_user.unlock_user`, `sys_user.set_user_manager`, `sys_api_key.revoke_api_key`, `sys_session.revoke_session`, `sys_session.revoke_my_other_sessions`, `sys_invitation.cancel_invitation`, `sys_invitation.reject_invitation`, `sys_member.remove_member`, `sys_team_member.remove_team_member`, `sys_team.remove_team`, `sys_oauth_application.disable_oauth_application`, `sys_oauth_application.delete_oauth_application`, `sys_sso_provider.delete_sso_provider`, `sys_organization.leave_organization`, `sys_account.unlink_account` | | H | 20 | no `ai` block (listed in the ADR addendum) | **ADR-0011 addendum (2026-10-30).** Records the ruling, the rubric, all three name lists, the H rule (no `ai.exposed` on an H action without a maintainer ruling naming it), the count correction, `copy_invitation_link` as not assessed, the struck automatic check, how the declarations reach an agent (pointing at ADR-0066 D4's 2026-10-30 note for the mechanism and naming these 38 as the platform `sys_*` actions that declare it), and what an OAuth-connected agent gets today. This keeps the two ADR notes consistent, per the skills note on the card (6523989016): ADR-0066's note states the per-action mechanism; this addendum says which actions use it. **Docs.** One bullet in `content/docs/ai/connect-mcp.mdx` under the exposure policy. **Changeset.** `@objectstack/platform-objects` patch. ## Why Ruling 6523087116 (letter A+) on #27900 executes parameter 2 of ruling 6513869839 on #27681 as one assessment of the platform's kernel actions, as the maintainer asked (「但是对于系统内核对象,是否应该统一评估一下。」), and settles that the actions allowed to open get their declarations now: 「所以我说评估标准对象的意思,就是允许放开的动作,是不是就应该把申明加上」. The maintainer struck the automatic check (「自动检查 有必要吗?…」), so the table lives only in the ADR. ## Premise re-read (dispatch step 1) Read on `main` @ `be646ede`, then rebased onto `744ad0ad`. All 58 names the card lists exist on the object and under the name the card gives. Two differences from the card, reported to the seat before any code, and the seat ruled the way on: - `sys_invitation.copy_invitation_link` (type `script`) exists on `main` and in no bucket; it is absent at the card's read sha `ded67bfb`. It keeps no `ai` block, today's state; the addendum names it as not assessed. - The card's bucket headers (H 19, E 22) disagree with its own name lists (H 20, E 21). The names are the record: 38 blocks, not 39. ## The MCP dogfood, one case per bucket `packages/qa/dogfood/test/mcp-system-object-action-exposure.dogfood.test.ts` (rewritten; booted showcase, live MCP transport, better-auth admin plugin on, embedded OAuth authorization server on). The test-only `ai` block it used to set on `ban_user` is gone: it reads the platform's own declarations, and a premise check asserts one action per bucket (`ban_user` C, `update_my_profile` E, `impersonate_user` H). It is not a pin over the table. The **OAuth-connected agent** is the caller the ruling exposes these actions to. Its token is minted the way an MCP client connects: dynamic client registration, authorize with `resource` = the MCP URL and PKCE S256, consent, token (scopes `data:read data:write actions:execute`). | Case | Caller | Answer on this branch | |:--|:--|:--| | E listed | OAuth agent | `update_my_profile` listed, `aiExposed: true`, `requiresConfirmation: false` | | E run | OAuth agent | **today:** `401 UNAUTHORIZED` from the identity route (`/api/v1/auth/update-user`); the name is unchanged | | E run, contrast | admin session | served: `ok: true`, the name changes | | C unconfirmed | OAuth agent | `428 ACTION_CONFIRMATION_REQUIRED`, nothing run | | C confirmed | OAuth agent | **today:** `401 UNAUTHENTICATED "Sign in first"` from `/api/v1/auth/admin/ban-user`; nobody banned | | H | OAuth agent | `impersonate_user` absent from the listing, refused by name | The #27681 pins stay on the administrator's session, now on the platform declaration: `ban_user` listed and attested, `428` unconfirmed, `409 ACTION_DISABLED` with the ADR-0126 switch off, a plain member's confirmed call refused by the target, the confirmed admin call bans. The control moved from `unban_user` (now C, exposed) to `impersonate_user` (H). **What waits on #28178.** The ruled outcome of the E run (an OAuth agent's call served) cannot be shown without changing which credentials an identity route or the REST face accepts, which #28178 governs. Per the dispatch, this PR does not build around it: the run case pins today's `401` and says it waits on #28178, and the session contrast shows the declaration itself is live. When #28178 lands, flip the two "today" expectations. For the record, the agent's listing on `sys_user` is exactly the eight E and C actions there (`invite_user`, `ban_user`, `unban_user`, `unlock_user`, `set_user_manager`, `update_my_profile`, `resend_verification_email`, `enable_two_factor`). ## Two tests whose premise the declarations changed (test-only edits, surface amended by the seat) - `packages/platform-objects/src/identity/action-confirm-one-dialog.test.ts`: it asserted that the #7309-converted actions have no `ai.description`, so that the dialog question was never moved into the tool description. Seven of them now carry a model-facing `ai.description` by the ruling. The assertion now checks the intent directly: each still has its dialog `description`, and its `ai.description` is not that question. Nothing else in the file changed. - `packages/runtime/src/action-execution-destructive.test.ts`: it asserted that all 13 #7309 identity actions are excluded at the MCP gates because none declares `ai.exposed`. Seven of the 13 are now exposed (C: `ban_user`, `disable_oauth_application`, `delete_oauth_application`, `unlink_account`, `delete_sso_provider`, `remove_team_member`; E: `enable_oauth_application`). The six human-only ones keep both exclusion pins unchanged. The seven are pinned admitted at gates 1 and 3, with the listing's `requiresConfirmation` equal to the declared flag. `disable_oauth_application` left the "no `ai` override, not destructive" list because it now declares `requiresConfirmation: true`, and a new case pins that the declared override wins (the precedence the file already pins). No assertion was dropped for an action whose declaration did not change. ## Verification - The dogfood above at the final head `0bd14911`: 11/11 (five of the file's six prior cases, plus the session contrast and the four OAuth cases; the old `unban_user` control case became the `impersonate_user` one). Run beside it, `api-action-door-forward.dogfood.test.ts` and `two-factor-backup-code-reveal.dogfood.test.ts` stay green: 3 files, 25/25. - Ablation (committed state, rebuilt `@objectstack/platform-objects` dist, then restored and rebuilt): with `ban_user`'s `requiresConfirmation` removed and an `ai` block added to `impersonate_user` (premise checks taken out so the cases speak), 8 of 11 red: the session listing, the session `impersonate_user` refusal, both `428` cases, the agent's H case, and three cascades (the switch, the confirmed ban and the session contrast, after the unguarded calls had run). Restored: 11/11. - `@objectstack/platform-objects` suite: 104 files, 1474/1474. - `packages/runtime/src/action-execution-destructive.test.ts`: 57/57. Runtime suite: 490 files, 8854 passed, 19 skipped. - `@objectstack/mcp` suite: 47 files, 603/603. - Typecheck: `pnpm --filter @objectstack/platform-objects typecheck` and `pnpm --filter @objectstack/runtime typecheck` exit 0; the dogfood package's `tsc --noEmit` (8 GB heap) exits 0. - Every family `dispatch-gates --commands` derives (94) ran after a whole-tree build (`pnpm build`, 115/115) and exited 0, reconciled with `--ran`: 94 run, 0 NOT-MEASURED. One of them, `check-plugin-teardown-shape.mjs --self-test`, first exited 3 because the shallow clone could not read its pinned fixture commit; after fetching that commit it exited 0 (48 cases). `pnpm lint` exits 0. ## Acceptance notes - The two `enable_two_factor` rows (E) take the user's current password as a param and answer the authenticator URI and one-time backup codes. Exposed to an agent, the agent asks for the password and receives fresh sign-in factors, close to the rubric's H test. Landed as the card lists them; the seat has taken the question to the maintainer. - Every description says what the model must pass. The MCP door does not fill `defaultFromRow` params from the record, so a C action like `delete_sso_provider` names its `providerId` param in the description. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (Not touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the lane's maintainer summary; quoted rulings stay verbatim). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016WV4czk9umSFBVoz5cijzX --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 91fa06d commit f3d2845

18 files changed

Lines changed: 669 additions & 78 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
feat(platform-objects): the platform identity actions an agent may reach declare it — additive acts exposed, destructive lifecycle acts exposed behind confirmation, credential and ownership acts human-only
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The `sys_*` identity objects' actions now carry the ADR-0011 `ai` block by risk, as ADR-0011's 2026-10-30 addendum records (58 actions assessed, 38 declared):
11+
12+
- **Exposed (21).** Additive or self-scoped acts declare `ai: { exposed: true, description }`: the four `invite_user` mirrors, resending and accepting an invitation, adding a member to an organization or a team, creating, editing and renaming a team or an organization, switching the active organization, re-enabling an OAuth application, enabling two-factor authentication, updating your own profile, resending the verification email, and requesting SSO domain verification.
13+
- **Exposed, confirmation required (17).** Destructive or privilege-changing lifecycle acts also declare `requiresConfirmation: true`, so MCP `run_action` refuses them `428 ACTION_CONFIRMATION_REQUIRED`, with nothing run, until the call carries `confirm: true`: banning, unbanning and unlocking a user, setting a user's manager, revoking an API key or a session, signing out other devices, cancelling or declining an invitation, removing a member or a team member, deleting a team, an OAuth application or an SSO provider, disabling an OAuth application, leaving an organization, and unlinking a sign-in provider.
14+
- **Human-only (20).** Acts that mint a credential or a session, change ownership, a role or a sign-in factor, or destroy an account or an organization (impersonating a user, setting a password, creating a user or an API key, transferring ownership, changing a member's role, deleting an organization, and the rest the addendum names) declare no `ai` block and are not listed over MCP. `sys_invitation.copy_invitation_link` declares none either; it was not part of the assessment.
15+
16+
Over MCP these actions are listed and run behind every existing gate: the capability gate, the installation's action switch, the param contract, the confirmation gate and the identity route's own authorization. They are `type: 'api'` actions forwarded to identity routes that accept a signed-in session only, so an agent connected over OAuth sees them listed and is refused at the route when it runs one.

‎content/docs/ai/connect-mcp.mdx‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,20 @@ can reach is the **default exposure policy (v1)**:
261261
The stored-metadata tables (`sys_metadata`, `sys_metadata_history`) stay
262262
refused even when flagged. Each action `list_actions` returns carries
263263
`aiExposed: true`, the server's attestation of that opt-in.
264+
- **The platform's own identity actions declare it by risk.** Additive acts
265+
(inviting someone, adding a member to a team, creating or renaming a team or
266+
an organization, re-enabling an OAuth application) are exposed. Destructive
267+
lifecycle acts (banning a user, revoking a session or an API key, removing a
268+
member, deleting a team, leaving an organization) are exposed with
269+
`ai.requiresConfirmation: true`, so `run_action` refuses them until the call
270+
carries `confirm: true`. Acts that mint a credential or a session, change
271+
ownership or a sign-in factor, or destroy an account or an organization
272+
(impersonating a user, setting a password, creating an API key, transferring
273+
ownership, deleting an organization) declare nothing and are not listed. The
274+
full list is ADR-0011's 2026-10-30 addendum. These actions forward to
275+
identity routes that accept a signed-in session only, so an agent connected
276+
over OAuth sees them listed, but a call that reaches the identity route is
277+
refused there (`401`) and changes nothing.
264278

265279
<Callout type="info">
266280
This is the default policy, not a ceiling. A finer, **metadata-declared** exposure

‎docs/adr/0011-actions-as-ai-tools.md‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,86 @@ ships next.
305305

306306
---
307307

308+
## Addendum (2026-10-30): the platform identity actions an agent may reach
309+
310+
> Status of this addendum: **ruled** (ruling of record `6523087116` on #27900, letter A+), executing
311+
> parameter 2 of ruling `6513869839` on #27681. The maintainer, verbatim:
312+
> 「所以我说评估标准对象的意思,就是允许放开的动作,是不是就应该把申明加上」.
313+
> It changes no rule of this ADR: it records how the platform's own `sys_*` identity actions apply the
314+
> opt-in, and the declarations that follow from it.
315+
316+
The opt-in binds the platform's `sys_*` actions exactly as it binds an app's: an action reaches an
317+
agent only when it declares `ai: { exposed: true, description }`. The named actions on the twelve
318+
identity objects in `packages/platform-objects/src/identity/` were assessed once, as one set, into
319+
three buckets, and the declarations were written in the same change as this addendum.
320+
321+
| Bucket | Test | What the action declares |
322+
|:--|:--|:--|
323+
| **H** human-only | it mints a credential or a session, changes who owns or who can sign in, or destroys an account or an organization | no `ai` block. ⛔ No `ai.exposed` on one of these without a maintainer ruling that names the action. |
324+
| **C** exposed, confirmed | a destructive or privilege-changing lifecycle act that is reversible or bounded | `ai: { exposed: true, description, requiresConfirmation: true }`. ⛔ The confirmation is never relaxed. |
325+
| **E** exposed | an additive or self-scoped act with no credential and no privilege escalation | `ai: { exposed: true, description }`; confirmation is the author's choice (none is declared today). |
326+
327+
**H — human-only (20):** `sys_user.impersonate_user` · `sys_user.set_user_password` ·
328+
`sys_user.create_user` · `sys_user.delete_my_account` · `sys_user.change_my_password` ·
329+
`sys_user.change_my_email` · `sys_user.disable_two_factor` · `sys_user.generate_backup_codes` ·
330+
`sys_two_factor.disable_two_factor` · `sys_two_factor.regenerate_backup_codes` ·
331+
`sys_api_key.create_api_key` · `sys_api_key.restore_api_key` ·
332+
`sys_oauth_application.create_oauth_application` · `sys_oauth_application.rotate_client_secret` ·
333+
`sys_sso_provider.register_sso_provider` · `sys_sso_provider.register_saml_provider` ·
334+
`sys_sso_provider.verify_domain` · `sys_member.transfer_ownership` · `sys_member.update_member_role` ·
335+
`sys_organization.delete_organization`.
336+
337+
**C — exposed, confirmed (17):** `sys_user.ban_user` · `sys_user.unban_user` · `sys_user.unlock_user` ·
338+
`sys_user.set_user_manager` · `sys_api_key.revoke_api_key` · `sys_session.revoke_session` ·
339+
`sys_session.revoke_my_other_sessions` · `sys_invitation.cancel_invitation` ·
340+
`sys_invitation.reject_invitation` · `sys_member.remove_member` · `sys_team_member.remove_team_member` ·
341+
`sys_team.remove_team` · `sys_oauth_application.disable_oauth_application` ·
342+
`sys_oauth_application.delete_oauth_application` · `sys_sso_provider.delete_sso_provider` ·
343+
`sys_organization.leave_organization` · `sys_account.unlink_account`.
344+
345+
**E — exposed (21):** `sys_user.invite_user` · `sys_invitation.invite_user` ·
346+
`sys_organization.invite_user` · `sys_member.invite_user` · `sys_invitation.resend_invitation` ·
347+
`sys_invitation.accept_invitation` · `sys_member.add_member` · `sys_team_member.add_team_member` ·
348+
`sys_team.add_team_member` · `sys_team.create_team` · `sys_team.update_team` ·
349+
`sys_organization.create_organization` · `sys_organization.update_organization` ·
350+
`sys_organization.rename_organization` · `sys_organization.set_active_organization` ·
351+
`sys_oauth_application.enable_oauth_application` · `sys_user.enable_two_factor` ·
352+
`sys_two_factor.enable_two_factor` · `sys_user.update_my_profile` ·
353+
`sys_user.resend_verification_email` · `sys_sso_provider.request_domain_verification`.
354+
355+
That is 58 actions and 38 `ai` blocks. The card's bucket headers read H 19 and E 22; its name lists,
356+
which the ruling adopted, hold 20 and 21, and the names are the record.
357+
358+
**Not assessed.** `sys_invitation.copy_invitation_link` was added after the assessment (the link-only
359+
invitation delivery). It declares no `ai` block, which is this ADR's default, and it is not assessed
360+
until a maintainer ruling names it.
361+
362+
**No automatic check.** The maintainer struck the pin test the assessment proposed:
363+
「自动检查 有必要吗?每一个元数据应用都有类似的问题,后续都要检查吗」. This table lives only here; a reviewer
364+
of `packages/platform-objects/src/identity/**` reads it. A customer app governs its own actions with
365+
the same key and the platform's defaults: the opt-in, the confirmation default, and the ADR-0126
366+
activation switch.
367+
368+
**How the declarations reach an agent.** Over MCP, a `sys_*` action is listed and run only when the
369+
action itself declares `ai.exposed: true` — ADR-0066 D4's 2026-10-30 note records that mechanism
370+
(`packages/runtime/src/action-execution.ts#systemObjectActionExposureError`). As of this addendum the
371+
38 E and C actions above are the platform `sys_*` actions that declare it. Every gate after admission
372+
still runs: ADR-0066 D4's capability gate, the ADR-0126 §8 activation switch, the
373+
`ai.requiresConfirmation` gate (a C action is refused `428 ACTION_CONFIRMATION_REQUIRED`, nothing run,
374+
until the call carries `confirm: true`; `packages/runtime/src/action-execution.ts#actionConfirmationRefusal`),
375+
and the target's own authorization.
376+
377+
**Declared is not yet runnable for an OAuth-connected agent.** All 38 are `type: 'api'` actions that
378+
forward to an identity route: 37 to the better-auth routes under `/api/v1/auth/`, and
379+
`sys_api_key.revoke_api_key` to the REST data route. Those targets accept a signed-in session, and the
380+
forward of an OAuth-connected agent's call is answered `401` there. Which credentials a forward
381+
target accepts is not decided here; it is the open decision on #28178. Until that lands, an
382+
OAuth-connected agent lists these actions, a C action is still refused `428` until the call is
383+
confirmed, and a call that reaches the identity route is answered `401` there, with nothing changed.
384+
A signed-in session's same call is served.
385+
386+
---
387+
308388
## References
309389

310390
* ADR-0003 — Package as first-class citizen (where actions live)

‎packages/platform-objects/src/identity/action-confirm-one-dialog.test.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,13 @@ describe('#7309 — an action that collects params opens ONE dialog', () => {
105105
// `ai.description` is the LLM-facing contract (≥40 chars, required when
106106
// `ai.exposed`) — the same word one level down. Putting the question there
107107
// would arm a tool description while the dialog fell back to its generic line.
108+
// An action ADR-0011's 2026-10-30 addendum exposes carries its own
109+
// `ai.description`, written for the model — never the dialog question.
108110
for (const [name, obj] of OBJECTS) {
109111
for (const action of CONVERTED[name] ?? []) {
110-
expect(byName(obj, action).ai?.description, `${name}.${action}.ai.description`).toBeUndefined();
112+
const a = byName(obj, action);
113+
expect(a.description, `${name}.${action}.description`).toBeTruthy();
114+
expect(a.ai?.description, `${name}.${action}.ai.description`).not.toBe(a.description);
111115
}
112116
}
113117
});

‎packages/platform-objects/src/identity/sys-account.object.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ export const SysAccount = ObjectSchema.create({
7474
params: [
7575
{ name: 'accountId', field: 'id', defaultFromRow: true, required: true },
7676
],
77+
// ADR-0011 identity-actions note, bucket C: exposed, and the
78+
// confirmation is never relaxed.
79+
ai: {
80+
exposed: true,
81+
description:
82+
'Unlink an external sign-in provider from the user\'s account (the record; param: its accountId): they can no longer sign in with that provider until they link it again. Ask the human to confirm before calling.',
83+
requiresConfirmation: true,
84+
},
7785
},
7886
],
7987

‎packages/platform-objects/src/identity/sys-api-key.object.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ export const SysApiKey = ObjectSchema.create({
160160
confirmText: 'Revoke this API key? Any clients using it will immediately lose access.',
161161
successMessage: 'API key "{{ record.name }}" revoked',
162162
refreshAfter: true,
163+
// ADR-0011 identity-actions note, bucket C: exposed, and the
164+
// confirmation is never relaxed.
165+
ai: {
166+
exposed: true,
167+
description:
168+
'Revoke an API key (the record): every client using it loses access immediately. It can be restored later. Ask the human to confirm before calling.',
169+
requiresConfirmation: true,
170+
},
163171
},
164172
{
165173
name: 'restore_api_key',

‎packages/platform-objects/src/identity/sys-invitation.object.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ export const SysInvitation = ObjectSchema.create({
106106
// field, so the gate narrows this dialog and no other picker.
107107
{ field: 'role', required: true, options: [...INVITATION_ROLE_OPTIONS], helpText: MEMBERSHIP_ROLE_PARAM_HELP },
108108
],
109+
// ADR-0011 identity-actions note, bucket E: exposed by plain declaration.
110+
ai: {
111+
exposed: true,
112+
description:
113+
'Invite a person by email address to join the current organization with a role (params: email, role). They join when they accept the invitation. Use when the user asks to invite someone who is not a member yet; the role options are only the roles the caller may grant.',
114+
},
109115
},
110116
{
111117
name: 'cancel_invitation',
@@ -122,6 +128,14 @@ export const SysInvitation = ObjectSchema.create({
122128
confirmText: 'Cancel this invitation? The recipient will no longer be able to accept it.',
123129
successMessage: 'Invitation canceled',
124130
refreshAfter: true,
131+
// ADR-0011 identity-actions note, bucket C: exposed, and the
132+
// confirmation is never relaxed.
133+
ai: {
134+
exposed: true,
135+
description:
136+
'Cancel a pending invitation (the record), so its recipient can no longer accept it. Ask the human to confirm before calling.',
137+
requiresConfirmation: true,
138+
},
125139
},
126140
{
127141
name: 'resend_invitation',
@@ -152,6 +166,12 @@ export const SysInvitation = ObjectSchema.create({
152166
{ field: 'email', required: true, defaultFromRow: true, carryOver: true },
153167
{ field: 'role', required: true, defaultFromRow: true, carryOver: true },
154168
],
169+
// ADR-0011 identity-actions note, bucket E: exposed by plain declaration.
170+
ai: {
171+
exposed: true,
172+
description:
173+
'Send a pending invitation (the record) again (params: its email and role, copied from the invitation; the stored role is kept). Its expiry date starts over. Use when the invitee lost the invitation or never received it.',
174+
},
155175
},
156176
{
157177
// [#27843, ruling E on #27797] The pending invitation's accept link, for
@@ -216,6 +236,12 @@ export const SysInvitation = ObjectSchema.create({
216236
visible: "has(record.email) && record.email == ctx.user.email && has(record.status) && record.status == 'pending'",
217237
successMessage: 'Invitation accepted',
218238
refreshAfter: true,
239+
// ADR-0011 identity-actions note, bucket E: exposed by plain declaration.
240+
ai: {
241+
exposed: true,
242+
description:
243+
'Accept an invitation addressed to the current user (the record), joining its organization with the invited role. Works only on a pending invitation sent to the caller\'s own email address.',
244+
},
219245
},
220246
{
221247
name: 'reject_invitation',
@@ -230,6 +256,14 @@ export const SysInvitation = ObjectSchema.create({
230256
confirmText: 'Decline this invitation? The inviter will be notified and you will need a new invitation to join.',
231257
successMessage: 'Invitation declined',
232258
refreshAfter: true,
259+
// ADR-0011 identity-actions note, bucket C: exposed, and the
260+
// confirmation is never relaxed.
261+
ai: {
262+
exposed: true,
263+
description:
264+
'Decline an invitation addressed to the current user (the record). The inviter is notified, and joining later needs a new invitation. Ask the human to confirm before calling.',
265+
requiresConfirmation: true,
266+
},
233267
},
234268
],
235269

‎packages/platform-objects/src/identity/sys-member.object.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ export const SysMember = ObjectSchema.create({
130130
// field, so the gate narrows this dialog and no other picker.
131131
{ field: 'role', required: true, options: [...INVITATION_ROLE_OPTIONS], helpText: MEMBERSHIP_ROLE_PARAM_HELP },
132132
],
133+
// ADR-0011 identity-actions note, bucket E: exposed by plain declaration.
134+
ai: {
135+
exposed: true,
136+
description:
137+
'Invite a person by email address to join the current organization with a role (params: email, role). They join when they accept the invitation. Use when the user asks to invite someone who is not a member yet; the role options are only the roles the caller may grant.',
138+
},
133139
},
134140
{
135141
// Admin-only: directly attach an existing user to the active org,
@@ -200,6 +206,12 @@ export const SysMember = ObjectSchema.create({
200206
{ field: 'role', required: true, helpText: MEMBERSHIP_ROLE_PARAM_HELP },
201207
{ name: 'organizationId', field: 'organization_id' },
202208
],
209+
// ADR-0011 identity-actions note, bucket E: exposed by plain declaration.
210+
ai: {
211+
exposed: true,
212+
description:
213+
'Add an existing user straight into an organization with a role, without an invitation (params: userId, role, and optionally organizationId, which defaults to the active organization). Platform administrators only.',
214+
},
203215
},
204216
{
205217
name: 'update_member_role',
@@ -245,6 +257,14 @@ export const SysMember = ObjectSchema.create({
245257
confirmText: 'Remove this member from the organization? They will lose access to all org resources.',
246258
successMessage: 'Member removed',
247259
refreshAfter: true,
260+
// ADR-0011 identity-actions note, bucket C: exposed, and the
261+
// confirmation is never relaxed.
262+
ai: {
263+
exposed: true,
264+
description:
265+
'Remove a member (the record) from the organization: they lose access to all of its resources. Ask the human to confirm before calling.',
266+
requiresConfirmation: true,
267+
},
248268
},
249269
// Make Owner is `update-member-role` with role=owner, and better-auth's
250270
// `updateMemberRole` writes the TARGET's role and nothing else: the caller

‎packages/platform-objects/src/identity/sys-oauth-application.object.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ export const SysOauthApplication = ObjectSchema.create({
113113
params: [
114114
{ name: 'client_id', field: 'client_id', defaultFromRow: true, required: true },
115115
],
116+
// ADR-0011 identity-actions note, bucket C: exposed, and the
117+
// confirmation is never relaxed.
118+
ai: {
119+
exposed: true,
120+
description:
121+
'Disable an OAuth application (the record; param: its client_id): its tokens are rejected and integrations using it stop working until it is re-enabled. Platform administrators only. Ask the human to confirm before calling.',
122+
requiresConfirmation: true,
123+
},
116124
},
117125
{
118126
name: 'enable_oauth_application',
@@ -134,6 +142,12 @@ export const SysOauthApplication = ObjectSchema.create({
134142
params: [
135143
{ name: 'client_id', field: 'client_id', defaultFromRow: true, required: true },
136144
],
145+
// ADR-0011 identity-actions note, bucket E: exposed by plain declaration.
146+
ai: {
147+
exposed: true,
148+
description:
149+
'Re-enable a disabled OAuth application (the record; param: its client_id), so token issuance, authorization and introspection resume for that client. Platform administrators only.',
150+
},
137151
},
138152
{
139153
name: 'create_oauth_application',
@@ -246,6 +260,14 @@ export const SysOauthApplication = ObjectSchema.create({
246260
params: [
247261
{ name: 'client_id', field: 'client_id', defaultFromRow: true, required: true },
248262
],
263+
// ADR-0011 identity-actions note, bucket C: exposed, and the
264+
// confirmation is never relaxed.
265+
ai: {
266+
exposed: true,
267+
description:
268+
'Permanently delete an OAuth application (the record; param: its client_id): all tokens and consents issued to it are invalidated and its integrations stop working. This cannot be undone. Ask the human to confirm before calling.',
269+
requiresConfirmation: true,
270+
},
249271
},
250272
],
251273

0 commit comments

Comments
 (0)