Skip to content

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

Merged
os-zhuang merged 2 commits into
mainfrom
claude/project-thread-qkb7g0
Oct 31, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/project-thread-qkb7g0

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

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 真正跑通要等 runtime(mcp): an OAuth-token agent's api action forward carries a bearer the target door does not accept (acceptOAuthAccessToken matches /mcp paths only), so the target likely resolves the caller as anonymous #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 platform-objects: 16 more actions declare both confirmText and params, so one click opens two sequential dialogs (same shape as #7278) #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 platform-objects: 16 more actions declare both confirmText and params, so one click opens two sequential dialogs (same shape as #7278) #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

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Not touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 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.ai/code/session_016WV4czk9umSFBVoz5cijzX

…re it, by risk

Bucket E actions declare ai.exposed with a model-facing description;
bucket C actions also declare requiresConfirmation: true; human-only
actions carry no ai block. ADR-0011 gains a dated addendum recording the
three buckets and the H rule. The MCP dogfood drives one action per
bucket with an OAuth-connected agent, and two tests whose premise was
"no platform identity action declares ai" follow the declarations.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4czk9umSFBVoz5cijzX
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ba5c754de6e79e62bef45cf7195c279b915730d6 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract-review-claim: session_016WV4czuTWNgKR22fKv7jia · head 0bd149117a4f4cfd65467dd9a4f78ce396373492 · 2026-10-30T22:30Z

The lane seat runs the CONTRACT_REVIEW_TIER review this PR owes (Tier H: docs/adr/**; ruling 6523087116, letter A+). An authorised approval follows the review.

…C override set aside

leave_organization now declares ai.requiresConfirmation: true (ADR-0011's
2026-10-30 addendum, bucket C), and the declared override is read before the
variant, so "still destructive off variant alone" passed without reading the
variant. The case now asserts the declared flag on its own and classifies the
real declaration with the override set aside; dropping the variant there
turns it red.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4czk9umSFBVoz5cijzX
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract-review-claim: session_016WV4czuTWNgKR22fKv7jia · head a00df3047558fb9f740574badd992320ee7cb20b · 2026-10-30T22:41Z

Delta over 0bd14911 (reviewed in full): one test-only commit addressing the review's should-fix.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a00df3047558fb9f740574badd992320ee7cb20b
Review-basis: the earlier head 0bd149117a4f4cfd65467dd9a4f78ce396373492 was reviewed in full; this head is one commit on top of it (a00df30475, test(runtime): read leave_organization's variant leg with its bucket-C override set aside), reviewed as a delta — the diff between the two heads is exactly packages/runtime/src/action-execution-destructive.test.ts (+6, no other file).
Local-runs: none
Scope: docs/adr/0011-actions-as-ai-tools.md + packages/platform-objects/src/identity/*.object.ts (12), packages/platform-objects/src/identity/action-confirm-one-dialog.test.ts, packages/runtime/src/action-execution-destructive.test.ts, packages/qa/dogfood/test/mcp-system-object-action-exposure.dogfood.test.ts, packages/runtime/src/action-execution.ts (cited: systemObjectActionExposureError, actionAiExposureError, actionConfirmationRefusal, actionLooksDestructive, isIdentityRouteTarget), packages/runtime/src/http-dispatcher.ts (cited: acceptOAuthAccessToken), docs/adr/0066-unified-authorization-model.md (D4 2026-10-30 note), content/docs/ai/connect-mcp.mdx, .changeset/27900-identity-actions-ai-exposure.md

① Derived judgments

  • Bucket fidelity (scripted): parsed the card's three NAME LISTS (H 20 / C 17 / E 21 = 58, no cross-bucket duplicate) and brace-parsed the 12 identity object files at head (59 named actions). Result: every E action has ai: { exposed: true, description } and no requiresConfirmation key; every C action has exposed: true, description, requiresConfirmation: true; every H action has no ai block; no extra ai keys anywhere. 38 ai blocks. Mismatches by object.action: none. The one action outside the card, sys_invitation.copy_invitation_link (type script, added by invite-member answers outcome by mail delivery and sys_invitation gains a server copy_invitation_link action that answers the accept URL (ruling E on #27797) #27843), carries no ai block, as the seat ruled.
  • Descriptions: all 38 measured 92–267 chars (≥40). Each names what the action does, the params the model must pass (e.g. client_id, providerId, managerId, teamId/userId), and when to call it; every C description ends "Ask the human to confirm before calling", which is model-facing instruction, not the dialog question — the dialog test now asserts ai.description !== description for the 13 platform-objects: 16 more actions declare both confirmText and params, so one click opens two sequential dialogs (same shape as #7278) #7309 sites. Spot-checked wording against the actions' targets/params: no misdescription found. The two enable_two_factor descriptions say the param is the current password and that backup codes are answered — accurate, and that is the open question the PR flags to the maintainer (not ruled here). Nothing leaks a secret value; request_domain_verification names the DNS TXT record as the answer (its result view already does).
  • Rewritten tests, loosened only as far as needed?
    • action-execution-destructive.test.ts (at a00df30475: the only file the delta commit touches, +6 lines in the leave_organization sibling case — see ④): the 13-site describe now splits 7 exposed (ban_user, enable/disable/delete_oauth_application, unlink_account, delete_sso_provider, remove_team_member — all E/C per the lists) from 6 human-only (delete_my_account, disable_two_factor ×2, generate_backup_codes, rotate_client_secret, regenerate_backup_codes — all H). The six keep BOTH exclusion pins (gate 1 systemObjectActionExposureError, gate 3 actionAiExposureError) verbatim; gate 2 still runs on all 13; the seven get a new admitted-at-1-and-3 pin plus summarizeAction().requiresConfirmation === declared flag. The "5 flipped" describe moves disable_oauth_application out of the heuristic-only set (it now declares requiresConfirmation: true) and adds a case pinning that the declared override wins — consistent with actionLooksDestructive (override read first). No pin for an unchanged action was dropped.
    • action-confirm-one-dialog.test.ts: only the ai.description assertion changed (undefined → "truthy dialog description, and ai.description is not it"). Intent preserved for all 13; nothing else in the file changed.
  • Automatic check over the table: none. The dogfood's beforeAll premise reads one action per bucket (ban_user C, update_my_profile E, impersonate_user H) to drive its cases and says it is not a pin over the table; the destructive test's six-H exclusion pins pre-existed (platform-objects: 16 more actions declare both confirmText and params, so one click opens two sequential dialogs (same shape as #7278) #7309/mcp: run_action refuses every action on a sys_* object, so an agent cannot reach the identity actions (ban_user, impersonate_user, set_user_password) that REST POST /actions serves #27681). No new key, gate, script, schema or CI step; no runtime/spec/plugin source touched (file list: changeset, mdx, ADR, 12 objects, 3 tests).
  • ADR-0011 addendum: heading ## Addendum (2026-10-30): … matches the repo's dated-note convention (ADR-0018/0048 style); placed after Decision, before References. Counts (58 / 38; H 20, C 17, E 21) and all three name lists match the card's name lists exactly; the header-count correction is stated; the H rule is verbatim in spirit ("no ai.exposed … without a maintainer ruling that names the action"); copy_invitation_link recorded as not assessed; the struck automatic check is recorded with the maintainer's words; the ADR-0066 D4 2026-10-30 note and the three runtime symbols it cites exist on origin/main; "37 to /api/v1/auth/, 1 (revoke_api_key) to the REST data route" verified by target tally; the runtime(mcp): an OAuth-token agent's api action forward carries a bearer the target door does not accept (acceptOAuthAccessToken matches /mcp paths only), so the target likely resolves the caller as anonymous #28178 dependency is stated as an open decision, not pre-empted.
  • Dogfood: the agent token is minted by the embedded authorization server (DCR → authorize with resource = MCP URL + PKCE S256 → consent → token) — a real OAuth agent, not a session bearer relabelled. The E run pins today's 401 and the session contrast; the C case pins 428 unconfirmed then 401 confirmed with nobody banned; H absent and refused by name. The mcp: run_action refuses every action on a sys_* object, so an agent cannot reach the identity actions (ban_user, impersonate_user, set_user_password) that REST POST /actions serves #27681 session pins (listing attestation, 428, ADR-0126 switch 409, member refused, confirmed ban) are kept; the control moved from unban_user (now C) to impersonate_user (H). No change to which credentials an identity route or the REST face accepts: no auth/runtime source in the diff.

② Semver level

patch on @objectstack/platform-objects (published, 18.2.0). Clause-②: no — no export, key, status, error code or door code added; values written on the existing ai key of platform-owned metadata. Metadata-protocol: none — nothing an author can newly write, nothing refused, no meaning or default changed. Per Post-Task §3, none takes patch; the changeset carries both declaration lines line-initial and matches the PR body. Tests and docs publish nothing.

③ Boundary flags

④ Design judgment

  • The addendum holds against its first consumer (the 38 declarations in the same diff) and against the mechanism it cites (ADR-0066 D4 note; systemObjectActionExposureError per action, gates unchanged). Its claim that an OAuth agent's forward is answered 401 at every one of the 38 targets is measured by the dogfood for the better-auth routes only; for the one REST-data target (sys_api_key.revoke_api_key → PATCH /api/v1/data/sys_api_key/{id}) it is derived from http-dispatcher.ts (acceptOAuthAccessToken is true only on /mcp paths, so the forwarded OAuth bearer is refused there too), not measured. Accurate as written; the PR body cannot be edited, so this record carries the distinction.
  • Resolved at a00df30475 (was a should-fix at 0bd14911): the sibling case "leave_organization → destructive off variant alone" now asserts action.ai?.requiresConfirmation === true on its own and classifies { ...action, ai: undefined } as destructive, which exercises the mode/variant leg of actionLooksDestructive (the override is read first only when ai.requiresConfirmation !== undefined); the full declaration is still asserted destructive after it. Ablating the variant would turn the case red, as the thread reports. The guard is honest; no other line changed.
  • Note only: in the dialog test, the six H actions' former "no ai.description" pin now passes vacuously; the generic assertion covers the intent, so no table pin is being asked for.

Implemented-by: claude/project-thread-qkb7g0
Reviewed-by: session_016WV4czuTWNgKR22fKv7jia (seat subagent)

VERDICT: PASS

1 similar comment
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a00df3047558fb9f740574badd992320ee7cb20b
Review-basis: the earlier head 0bd149117a4f4cfd65467dd9a4f78ce396373492 was reviewed in full; this head is one commit on top of it (a00df30475, test(runtime): read leave_organization's variant leg with its bucket-C override set aside), reviewed as a delta — the diff between the two heads is exactly packages/runtime/src/action-execution-destructive.test.ts (+6, no other file).
Local-runs: none
Scope: docs/adr/0011-actions-as-ai-tools.md + packages/platform-objects/src/identity/*.object.ts (12), packages/platform-objects/src/identity/action-confirm-one-dialog.test.ts, packages/runtime/src/action-execution-destructive.test.ts, packages/qa/dogfood/test/mcp-system-object-action-exposure.dogfood.test.ts, packages/runtime/src/action-execution.ts (cited: systemObjectActionExposureError, actionAiExposureError, actionConfirmationRefusal, actionLooksDestructive, isIdentityRouteTarget), packages/runtime/src/http-dispatcher.ts (cited: acceptOAuthAccessToken), docs/adr/0066-unified-authorization-model.md (D4 2026-10-30 note), content/docs/ai/connect-mcp.mdx, .changeset/27900-identity-actions-ai-exposure.md

① Derived judgments

  • Bucket fidelity (scripted): parsed the card's three NAME LISTS (H 20 / C 17 / E 21 = 58, no cross-bucket duplicate) and brace-parsed the 12 identity object files at head (59 named actions). Result: every E action has ai: { exposed: true, description } and no requiresConfirmation key; every C action has exposed: true, description, requiresConfirmation: true; every H action has no ai block; no extra ai keys anywhere. 38 ai blocks. Mismatches by object.action: none. The one action outside the card, sys_invitation.copy_invitation_link (type script, added by invite-member answers outcome by mail delivery and sys_invitation gains a server copy_invitation_link action that answers the accept URL (ruling E on #27797) #27843), carries no ai block, as the seat ruled.
  • Descriptions: all 38 measured 92–267 chars (≥40). Each names what the action does, the params the model must pass (e.g. client_id, providerId, managerId, teamId/userId), and when to call it; every C description ends "Ask the human to confirm before calling", which is model-facing instruction, not the dialog question — the dialog test now asserts ai.description !== description for the 13 platform-objects: 16 more actions declare both confirmText and params, so one click opens two sequential dialogs (same shape as #7278) #7309 sites. Spot-checked wording against the actions' targets/params: no misdescription found. The two enable_two_factor descriptions say the param is the current password and that backup codes are answered — accurate, and that is the open question the PR flags to the maintainer (not ruled here). Nothing leaks a secret value; request_domain_verification names the DNS TXT record as the answer (its result view already does).
  • Rewritten tests, loosened only as far as needed?
    • action-execution-destructive.test.ts (at a00df30475: the only file the delta commit touches, +6 lines in the leave_organization sibling case — see ④): the 13-site describe now splits 7 exposed (ban_user, enable/disable/delete_oauth_application, unlink_account, delete_sso_provider, remove_team_member — all E/C per the lists) from 6 human-only (delete_my_account, disable_two_factor ×2, generate_backup_codes, rotate_client_secret, regenerate_backup_codes — all H). The six keep BOTH exclusion pins (gate 1 systemObjectActionExposureError, gate 3 actionAiExposureError) verbatim; gate 2 still runs on all 13; the seven get a new admitted-at-1-and-3 pin plus summarizeAction().requiresConfirmation === declared flag. The "5 flipped" describe moves disable_oauth_application out of the heuristic-only set (it now declares requiresConfirmation: true) and adds a case pinning that the declared override wins — consistent with actionLooksDestructive (override read first). No pin for an unchanged action was dropped.
    • action-confirm-one-dialog.test.ts: only the ai.description assertion changed (undefined → "truthy dialog description, and ai.description is not it"). Intent preserved for all 13; nothing else in the file changed.
  • Automatic check over the table: none. The dogfood's beforeAll premise reads one action per bucket (ban_user C, update_my_profile E, impersonate_user H) to drive its cases and says it is not a pin over the table; the destructive test's six-H exclusion pins pre-existed (platform-objects: 16 more actions declare both confirmText and params, so one click opens two sequential dialogs (same shape as #7278) #7309/mcp: run_action refuses every action on a sys_* object, so an agent cannot reach the identity actions (ban_user, impersonate_user, set_user_password) that REST POST /actions serves #27681). No new key, gate, script, schema or CI step; no runtime/spec/plugin source touched (file list: changeset, mdx, ADR, 12 objects, 3 tests).
  • ADR-0011 addendum: heading ## Addendum (2026-10-30): … matches the repo's dated-note convention (ADR-0018/0048 style); placed after Decision, before References. Counts (58 / 38; H 20, C 17, E 21) and all three name lists match the card's name lists exactly; the header-count correction is stated; the H rule is verbatim in spirit ("no ai.exposed … without a maintainer ruling that names the action"); copy_invitation_link recorded as not assessed; the struck automatic check is recorded with the maintainer's words; the ADR-0066 D4 2026-10-30 note and the three runtime symbols it cites exist on origin/main; "37 to /api/v1/auth/, 1 (revoke_api_key) to the REST data route" verified by target tally; the runtime(mcp): an OAuth-token agent's api action forward carries a bearer the target door does not accept (acceptOAuthAccessToken matches /mcp paths only), so the target likely resolves the caller as anonymous #28178 dependency is stated as an open decision, not pre-empted.
  • Dogfood: the agent token is minted by the embedded authorization server (DCR → authorize with resource = MCP URL + PKCE S256 → consent → token) — a real OAuth agent, not a session bearer relabelled. The E run pins today's 401 and the session contrast; the C case pins 428 unconfirmed then 401 confirmed with nobody banned; H absent and refused by name. The mcp: run_action refuses every action on a sys_* object, so an agent cannot reach the identity actions (ban_user, impersonate_user, set_user_password) that REST POST /actions serves #27681 session pins (listing attestation, 428, ADR-0126 switch 409, member refused, confirmed ban) are kept; the control moved from unban_user (now C) to impersonate_user (H). No change to which credentials an identity route or the REST face accepts: no auth/runtime source in the diff.

② Semver level

patch on @objectstack/platform-objects (published, 18.2.0). Clause-②: no — no export, key, status, error code or door code added; values written on the existing ai key of platform-owned metadata. Metadata-protocol: none — nothing an author can newly write, nothing refused, no meaning or default changed. Per Post-Task §3, none takes patch; the changeset carries both declaration lines line-initial and matches the PR body. Tests and docs publish nothing.

③ Boundary flags

④ Design judgment

  • The addendum holds against its first consumer (the 38 declarations in the same diff) and against the mechanism it cites (ADR-0066 D4 note; systemObjectActionExposureError per action, gates unchanged). Its claim that an OAuth agent's forward is answered 401 at every one of the 38 targets is measured by the dogfood for the better-auth routes only; for the one REST-data target (sys_api_key.revoke_api_key → PATCH /api/v1/data/sys_api_key/{id}) it is derived from http-dispatcher.ts (acceptOAuthAccessToken is true only on /mcp paths, so the forwarded OAuth bearer is refused there too), not measured. Accurate as written; the PR body cannot be edited, so this record carries the distinction.
  • Resolved at a00df30475 (was a should-fix at 0bd14911): the sibling case "leave_organization → destructive off variant alone" now asserts action.ai?.requiresConfirmation === true on its own and classifies { ...action, ai: undefined } as destructive, which exercises the mode/variant leg of actionLooksDestructive (the override is read first only when ai.requiresConfirmation !== undefined); the full declaration is still asserted destructive after it. Ablating the variant would turn the case red, as the thread reports. The guard is honest; no other line changed.
  • Note only: in the dialog test, the six H actions' former "no ai.description" pin now passes vacuously; the generic assertion covers the intent, so no table pin is being asked for.

Implemented-by: claude/project-thread-qkb7g0
Reviewed-by: session_016WV4czuTWNgKR22fKv7jia (seat subagent)

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Seat record · PR #28270 · head a00df3047558fb9f740574badd992320ee7cb20b · 2026-10-30T23:04Z

维护者速读

  • 改了什么: 按裁决 A+(6523087116),12 个身份对象上 58 个具名动作分三档写声明:E 档 21 个写 ai: { exposed: true, description };C 档 17 个另加 requiresConfirmation: true(MCP 不带确认即 428,什么都不执行);H 档 20 个不写 ai 块(不在列表里,按名拒绝)。ADR-0011 新增 2026-10-30 附记,列全三档名单与 H 档规则。没有新增任何自动检查、新键或门禁。
  • 为什么改: 执行您对「系统内核对象统一评估」的裁决:允许放开的动作把声明加上,一次到位,不再逐卡。
  • 风险与回滚: 只写既有键的值(Clause-②: no,Metadata-protocol: none)。计数按卡里列的名字为准(H 20 / C 17 / E 21,38 个 ai 块;卡的档头写 19/17/22,已在 ADR 附记更正)。sys_invitation.copy_invitation_link 是评估之后才加的,保持不声明、标「未评估」。今天 OAuth agent 能看到 E/C 动作,但执行到身份路由会 401、不改任何东西,真正跑通要等 runtime(mcp): an OAuth-token agent's api action forward carries a bearer the target door does not accept (acceptOAuthAccessToken matches /mcp paths only), so the target likely resolves the caller as anonymous #28178 的裁决,本 PR 不碰凭据边界。回滚即还原 12 个对象文件的 ai 块与 ADR 附记。
  • 先看: docs/adr/0011-actions-as-ai-tools.md 的 2026-10-30 附记(三档名单),再看 sys-user.object.ts 里各档各一例。
  • 两分钟验证: 跑 mcp-system-object-action-exposure.dogfood.test.ts(真实 OAuth agent:E 列出、C 未确认 428、H 不在列表且按名拒绝)。
  • 需要你做的: Tier H(docs/adr/**):请 os-zhuang 或 hotlong 审批(APPROVED)。契约复核 PASS 6535057166,Head-sha a00df3047558fb9f740574badd992320ee7cb20b。另有三个不阻塞本 PR 的问题已提给您:enable_two_factor 两行(要密码、返回登录因子)是否移到 H;copy_invitation_link 归哪档;sys_approval_request 的 9 个动作是否另卡评估。

席位意见: 按裁决原样落地,偏差(计数以名单为准、新增动作不声明)都向保守一侧,且都写进了 ADR 附记。建议批准。

席位核对

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

3 participants