Skip to content

Commit 9b4d193

Browse files
fix(platform-objects): enabling two-factor and copying an invitation link are human-only identity actions (#28386)
<!-- claude-projects-attribution: {"github_login":"os-zhuang"} --> _Requested by **Sam** · [project thread](https://claude.ai/code/project/chan_01LjZwNRFiG2fkp8SNf6rHNx?thread=cmsg_01LjZwNRFiG2fkp8SNf6rHNxA5aNWp4A2SV4FZHR1jhLti)_ Fixes #28368 Clause-②: no Metadata-protocol: none domain:server#4 · os-zhuang · os-dev for claim 6541181717 · Ruling-ref: 6540309323 (#27900, 「其他同意」) Tier: H (`docs/adr/0011-actions-as-ai-tools.md`): a contract review, then an authorised approval by a GOVERNED_APPROVER, before the queue. ## 维护者速读 - 改了什么:两个 `enable_two_factor` 动作(`sys_user` 与 `sys_two_factor`)删掉 `ai` 块,从 E 档移到 H 档;`sys_invitation.copy_invitation_link` 评为 H 档,保持不写 `ai` 块。ADR-0011 附记更新三档名单(H23 / C17 / E19,共 36 个 `ai` 块、59 个动作),并加一行:`sys_approval_request` 的 9 个动作未评估,评估前一律仅限人工。 - 为什么改:执行您在 #27900 上的裁决「其他同意」。启用双因素要用户密码、返回新的登录因子;邀请链接本身就是凭据。两者都不该让 agent 拿到。 - 风险与回滚:只收窄 agent 能看到的动作,Console 里登录用户的操作不变。回滚即还原这一个提交。 - 先看:`docs/adr/0011-actions-as-ai-tools.md` 的修订注记与名单;`packages/platform-objects/src/identity/sys-user.object.ts` 的 `enable_two_factor`。 - 两分钟验证:`grep -c "ai: {" packages/platform-objects/src/identity/*.object.ts` 合计应为 36,且 `enable_two_factor` 下不再有 `ai:`。 - 需要你做的:本 PR 触及 ADR(Tier H),合同复审通过后需 os-zhuang 或 hotlong 批准。 ## What changes - **`sys_user.enable_two_factor`, `sys_two_factor.enable_two_factor` (E → H).** Each drops its `ai: { exposed: true, description }` block; a one-line comment names bucket H in its place. Over MCP neither is listed any more, and `run_action` refuses both by name (the existing per-action opt-in, `systemObjectActionExposureError`). A signed-in person runs both exactly as before. - **`sys_invitation.copy_invitation_link` (assessed H).** No `ai` block, as before; a one-line comment names bucket H and why (the accept link is a bearer credential). No behaviour change. - **ADR-0011, the 2026-10-30 identity addendum.** A dated amendment note (ruling of record 6540309323), the H list gains the three names (23), the E list loses two (19), the count paragraph reads 59 actions / 36 `ai` blocks, the "Not assessed" paragraph now records `sys_approval_request`'s 9 actions as unassessed and human-only, and the two "38 / 37" counts in the MCP paragraphs read 36 / 35. - **Changesets.** New `28368-identity-two-factor-invite-link-human-only.md` (`@objectstack/platform-objects` patch). The unreleased `27900-identity-actions-ai-exposure.md` is corrected to the final lists (19 / 17 / 23, 59 assessed, 36 declared) so the release does not ship a sentence saying two-factor enabling is exposed. ### Reading of the card's item 1 The card says `copy_invitation_link` "gets an `ai` block, tier H". The ruling of record (6540309323) says "→ H (human-only)", the ADR's H row says "no `ai` block", and the ruled total, 36 `ai` blocks = C17 + E19, leaves no room for a 37th. So H here means no `ai` block; the action keeps today's declaration, and the change is its entry in the H list plus the code comment. ## Tests - `pnpm --filter @objectstack/platform-objects test`: 104 files, 1474 / 1474 passed. - `pnpm --filter @objectstack/platform-objects typecheck`: exit 0. - Build of the dependency closure (`pnpm --filter '@objectstack/platform-objects...' build`): exit 0. - `git grep` of `enable_two_factor` and `copy_invitation_link` across `packages/runtime`, `packages/qa`, `packages/services` and `content/docs` found no test or doc pinning either action as agent-exposed. #28270 added no per-tier name-list pin (the maintainer struck the automatic check), so no pin moves. ## Acceptance notes - **`Metadata-protocol: none`.** The claim first read `narrowed`; the seat amended it to `none`, since the kind is defined by what a metadata author can write and these are values on an existing key of platform-owned metadata (the same kind #28270 declared). So the changeset is `patch` and carries no ADR-0087 marker. - **`check-empty-changeset` is red on purpose.** This PR corrects the still-unreleased `.changeset/27900-identity-actions-ai-exposure.md` (#28270's note) to the final lists: exposed 19 (enabling two-factor removed), human-only 23 (enabling two-factor and copying an invitation link added), 59 assessed / 36 declared. Without it, the release would ship a sentence saying enabling two-factor is agent-exposed. The gate puts this DELIBERATE CORRECTION in front of a person by staying red; please confirm it at landing. - The ruling also asked for one note line on `sys_approval_request`; its 9 actions (`plugin-approvals`) were measured to declare no `ai` block, so the note matches the code. ## Verification Head `cbd23194`, merge base `fc5d5093` (`origin/main`). <details><summary>Local gates: 61 derived, 61 run, 60 exit 0, 1 red by design</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 61 commands from 6 paths (69 changed lines). - All 61 run on head `cbd23194` after a full `pnpm build` (116/116, exit 0). 60 exit 0. - `node scripts/check-empty-changeset.mjs --base origin/main`: exit 1, the DELIBERATE CORRECTION of `.changeset/27900-identity-actions-ai-exposure.md` described under Acceptance notes. It stays red until a person confirms. - `dispatch-gates --ran` with exit codes recorded: 61 derived, 61 run, 0 NOT-MEASURED (a derived zero), exit 0. - `pnpm lint`: exit 0. - The first pass, before the full build, gave exit 3 (PREREQUISITE NOT MET) on `check:dual-build-cjs-loads`, `check:i18n`, `check:lean-entry-closure` and `check-plugin-teardown-shape.mjs --self-test` (shallow clone). All four exit 0 on the final pass. </details> <details><summary>Not measured locally</summary> - The CI-only families `dispatch-gates` names outside its derived total (artifact rosters, wide-population families, the Build Core and Type Check lanes) are left to this PR's check-runs. - The dogfood battery is not owed: there is no `!` in the title and no `(narrowing)` arm. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QUs3BrhuPB4AGHyXk3rvfh)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ba0084e commit 9b4d193

6 files changed

Lines changed: 40 additions & 28 deletions

File tree

‎.changeset/27900-identity-actions-ai-exposure.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ feat(platform-objects): the platform identity actions an agent may reach declare
77
Clause-②: no
88
Metadata-protocol: none
99

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):
10+
The `sys_*` identity objects' actions now carry the ADR-0011 `ai` block by risk, as ADR-0011's 2026-10-30 addendum records (59 actions assessed, 36 declared):
1111

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.
12+
- **Exposed (19).** 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, updating your own profile, resending the verification email, and requesting SSO domain verification.
1313
- **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.
14+
- **Human-only (23).** 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, enabling two-factor authentication, creating a user or an API key, copying an invitation's accept link, 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.
1515

1616
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.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): enabling two-factor authentication and copying an invitation's accept link are human-only — an agent cannot reach them over MCP
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`sys_user.enable_two_factor` and `sys_two_factor.enable_two_factor` no longer declare an ADR-0011 `ai` block. Each takes the user's current password and answers a new sign-in factor (the authenticator URI and one-time backup codes), so an MCP agent no longer sees them in `list_actions` and `run_action` refuses them by name. `sys_invitation.copy_invitation_link`, which answers the invitation's accept link (a bearer credential), is assessed human-only and keeps declaring no `ai` block. A person signed in to the Console runs all three exactly as before.
11+
12+
ADR-0011's identity-actions addendum records the amended lists (human-only 23, exposed with confirmation 17, exposed 19: 36 `ai` blocks over 59 actions) and notes that `sys_approval_request`'s 9 actions are unassessed and human-only until assessed.

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

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -324,15 +324,21 @@ three buckets, and the declarations were written in the same change as this adde
324324
| **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. |
325325
| **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). |
326326

327-
**H — human-only (20):** `sys_user.impersonate_user` · `sys_user.set_user_password` ·
327+
> Amended (2026-10-31, ruling of record `6540309323` on #27900, the maintainer 「其他同意」): both
328+
> `enable_two_factor` actions move from E to H, since each takes the user's password and answers a new
329+
> sign-in factor, and `sys_invitation.copy_invitation_link` is assessed H, since it answers the
330+
> invitation's accept link, a bearer credential. The lists below are the amended record.
331+
332+
**H — human-only (23):** `sys_user.impersonate_user` · `sys_user.set_user_password` ·
328333
`sys_user.create_user` · `sys_user.delete_my_account` · `sys_user.change_my_password` ·
329334
`sys_user.change_my_email` · `sys_user.disable_two_factor` · `sys_user.generate_backup_codes` ·
330335
`sys_two_factor.disable_two_factor` · `sys_two_factor.regenerate_backup_codes` ·
331336
`sys_api_key.create_api_key` · `sys_api_key.restore_api_key` ·
332337
`sys_oauth_application.create_oauth_application` · `sys_oauth_application.rotate_client_secret` ·
333338
`sys_sso_provider.register_sso_provider` · `sys_sso_provider.register_saml_provider` ·
334339
`sys_sso_provider.verify_domain` · `sys_member.transfer_ownership` · `sys_member.update_member_role` ·
335-
`sys_organization.delete_organization`.
340+
`sys_organization.delete_organization` · `sys_user.enable_two_factor` ·
341+
`sys_two_factor.enable_two_factor` · `sys_invitation.copy_invitation_link`.
336342

337343
**C — exposed, confirmed (17):** `sys_user.ban_user` · `sys_user.unban_user` · `sys_user.unlock_user` ·
338344
`sys_user.set_user_manager` · `sys_api_key.revoke_api_key` · `sys_session.revoke_session` ·
@@ -342,22 +348,22 @@ three buckets, and the declarations were written in the same change as this adde
342348
`sys_oauth_application.delete_oauth_application` · `sys_sso_provider.delete_sso_provider` ·
343349
`sys_organization.leave_organization` · `sys_account.unlink_account`.
344350

345-
**E — exposed (21):** `sys_user.invite_user` · `sys_invitation.invite_user` ·
351+
**E — exposed (19):** `sys_user.invite_user` · `sys_invitation.invite_user` ·
346352
`sys_organization.invite_user` · `sys_member.invite_user` · `sys_invitation.resend_invitation` ·
347353
`sys_invitation.accept_invitation` · `sys_member.add_member` · `sys_team_member.add_team_member` ·
348354
`sys_team.add_team_member` · `sys_team.create_team` · `sys_team.update_team` ·
349355
`sys_organization.create_organization` · `sys_organization.update_organization` ·
350356
`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` ·
357+
`sys_oauth_application.enable_oauth_application` · `sys_user.update_my_profile` ·
353358
`sys_user.resend_verification_email` · `sys_sso_provider.request_domain_verification`.
354359

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.
360+
That is 59 actions and 36 `ai` blocks (H 23 / C 17 / E 19). The 2026-10-30 assessment covered 58
361+
actions in 38 blocks: the card's bucket headers read H 19 and E 22, its name lists, which the ruling
362+
adopted, held 20 and 21, and the names are the record. `sys_invitation.copy_invitation_link`, added
363+
after that assessment, and the two `enable_two_factor` moves came with the 2026-10-31 amendment.
357364

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.
365+
**Not assessed.** `sys_approval_request`'s 9 actions (`plugin-approvals`) are outside this assessment:
366+
they are unassessed and human-only, with no `ai` block, until a maintainer ruling assesses them.
361367

362368
**No automatic check.** The maintainer struck the pin test the assessment proposed:
363369
「自动检查 有必要吗?每一个元数据应用都有类似的问题,后续都要检查吗」. This table lives only here; a reviewer
@@ -368,14 +374,14 @@ activation switch.
368374
**How the declarations reach an agent.** Over MCP, a `sys_*` action is listed and run only when the
369375
action itself declares `ai.exposed: true` — ADR-0066 D4's 2026-10-30 note records that mechanism
370376
(`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
377+
36 E and C actions above are the platform `sys_*` actions that declare it. Every gate after admission
372378
still runs: ADR-0066 D4's capability gate, the ADR-0126 §8 activation switch, the
373379
`ai.requiresConfirmation` gate (a C action is refused `428 ACTION_CONFIRMATION_REQUIRED`, nothing run,
374380
until the call carries `confirm: true`; `packages/runtime/src/action-execution.ts#actionConfirmationRefusal`),
375381
and the target's own authorization.
376382

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
383+
**Declared is not yet runnable for an OAuth-connected agent.** All 36 are `type: 'api'` actions that
384+
forward to an identity route: 35 to the better-auth routes under `/api/v1/auth/`, and
379385
`sys_api_key.revoke_api_key` to the REST data route. Those targets accept a signed-in session, and the
380386
forward of an OAuth-connected agent's call is answered `401` there. Which credentials a forward
381387
target accepts is not decided here; it is the open decision on #28178. Until that lands, an

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ export const SysInvitation = ObjectSchema.create({
205205
fields: [{ path: 'acceptUrl', label: 'Invite link' }],
206206
acknowledge: 'Done',
207207
},
208+
// ADR-0011 identity-actions note, bucket H: human-only, no `ai` block.
209+
// It answers the accept link, a bearer credential.
208210
},
209211

210212
// ── Recipient-side actions (the invited user) ────────────────────

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,8 @@ export const SysTwoFactor = ObjectSchema.create({
9393
{ path: 'backupCodes', label: 'Backup Codes', format: 'code-list' },
9494
],
9595
},
96-
// ADR-0011 identity-actions note, bucket E: exposed by plain declaration.
97-
ai: {
98-
exposed: true,
99-
description:
100-
'Turn on two-factor authentication for the caller\'s own account (param: the current password). Answers the authenticator URI and one-time backup codes; the user must save the codes, which are shown only once.',
101-
},
96+
// ADR-0011 identity-actions note, bucket H: human-only, no `ai` block.
97+
// It takes the user's password and answers a new sign-in factor.
10298
},
10399
{
104100
name: 'disable_two_factor',

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -733,12 +733,8 @@ export const SysUser = ObjectSchema.create({
733733
{ path: 'backupCodes', label: 'Backup Codes', format: 'code-list' },
734734
],
735735
},
736-
// ADR-0011 identity-actions note, bucket E: exposed by plain declaration.
737-
ai: {
738-
exposed: true,
739-
description:
740-
'Turn on two-factor authentication for the caller\'s own account (param: the current password). Answers the authenticator URI and one-time backup codes; the user must save the codes, which are shown only once.',
741-
},
736+
// ADR-0011 identity-actions note, bucket H: human-only, no `ai` block.
737+
// It takes the user's password and answers a new sign-in factor.
742738
},
743739
{
744740
name: 'disable_two_factor',

0 commit comments

Comments
 (0)