Skip to content

docs(skills, mcp): teach that an AI service account is refused a confirmation-gated action whatever it sends - #33057

Merged
os-zhuang merged 2 commits into
mainfrom
claude/project-thread-6mlhpm
Nov 13, 2026
Merged

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

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #32936

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:五处教学文字(objectstack-ai 技能表、MCP 下发的 SKILL.md 及控制台下载镜像、run_action/resume_run 工具描述、actions.mdx 姿态表、审批 dogfood 文件头)补上同一句话:AI 服务账号用自己的密钥调用需确认的动作时,无论带什么都会被 428 拒绝、什么都不执行,应交给人来执行,或由作者在可无人值守时声明 ai.requiresConfirmation: false。
  • 为什么改:运行时早已如此(ADR-0151 D3 第 3 条),但已发布的技能仍教所有调用方「带 confirm: true 重试」,服务账号照做永远过不去。
  • 风险与回滚:零运行时改动,只改文字与两条测试断言;回滚即 revert 本 PR。
  • 先看:skills/objectstack-ai/SKILL.md 第 204 行表格行;packages/mcp/src/mcp-http-tools.ts 的 run_action 描述;packages/mcp/src/skill-md.ts 的 run_action 条目。
  • 两分钟验证:pnpm --filter @objectstack/mcp exec vitest run src/skill-md.test.ts src/mcp-resume-run-tool.test.ts,预期全绿。
  • 需要你做的:本 PR 触及 skills/**(Tier H)。契约复审在现 head 已 PASS(评论 6846560754,Head-sha a7baa0d746269fc53b8a2e7d4cdb3294e58e23c6),请以 os-zhuang 批准;批准后席位落地。
  • 席位意见:同意落地。四个入口与服务账号判定(principalKind === 'agent' 且无 onBehalfOf)逐一对过代码;复审只有四个 nit,作为接受的偏差。卡评论要的 list_actions 标记属运行时改动,已另立 mcp: list_actions marks no action unavailable for an AI service account, though run_action refuses its confirmation-gated actions whatever it sends #33067,本 PR 不做。

What changes

One sentence, in the same terms everywhere: under an AI service account's own key (an agent acting for itself, for no person) the call is refused whatever it sends and nothing runs: hand the action to a person, or its author declares ai.requiresConfirmation: false when it is safe unattended. The existing teaching for a person and a delegated agent (confirm: true after the human approves) stays in every text.

  • skills/objectstack-ai/SKILL.md — the list_actions / run_action / resume_run row of the MCP tool table.
  • packages/mcp/src/skill-md.ts — the run_action entry carries the sentence; the resume_run entry names the same refusal. ui/apps/console/src/pages/developer/objectstack-skill.ts (the Console "Download skill" mirror) carries the same bytes: the byte-for-byte pin in packages/mcp/src/skill-md.test.ts goes red otherwise. This file is outside the claim's listed surface; it is the mirror of skill-md.ts, not a new text.
  • packages/mcp/src/mcp-http-tools.ts — the run_action description (comment 6839924935), the resume_run description, and both tools' confirm member descriptions ("Under an AI service account's own key it is not read: the call is refused whatever it sends").
  • content/docs/ui/actions.mdx (nit D) — the posture table: the 'rest' row now states the gate it asks for an account under either posture, and the 'ai' row points to it; the dispatchAction example's confirm comment says it is never read for an account.
  • packages/qa/dogfood/test/approval-decide-over-mcp.dogfood.test.ts (nit E) — the file header names the account confirmation rule for the AI service account case (header only; no test code changed).
  • Changesets: '@objectstack/skills': patch; '@objectstack/mcp': patch + '@object-ui/console': patch (the mirror).

The runtime rule, quoted from main (c54a23d), packages/runtime/src/action-execution.ts:

    if (action?.ai?.requiresConfirmation !== true) return undefined;
    // [ADR-0151 D3 rule 3] An AI service account never confirms for itself:
    // for one, the member is not read at all.
    const account = isAiServiceAccountCaller(caller);
    // Only the boolean `true` attests.
    if (!account && request?.[AI_ACTION_CONFIRMATION_MEMBER] === true) return undefined;

It is asked at every door: packages/runtime/src/action-dispatch.ts (if (posture === 'ai' || actionExec.isAiServiceAccountCaller(wiring.ec)), so REST /actions and a host dispatchAction on 'rest' refuse an account too) and packages/runtime/src/domains/mcp.ts resumeAdmissionRefusal (resume_run). isAiServiceAccountCaller is principalKind === 'agent' && !onBehalfOf.

Not changed: no runtime code; delete_record / delete_records teaching untouched (a different gate).

Tests

  • packages/mcp/src/mcp-resume-run-tool.test.ts — new case: both run_action and resume_run descriptions keep confirm: true and carry the account sentence, and both confirm members say the member is not read for an account.
  • packages/mcp/src/skill-md.test.ts — the action-preference case also pins the account sentence in the run_action and resume_run entries. The pinned sentence is absent from both source files on origin/main (grep -c 0), so neither case can pass on the base text.

Acceptance notes

  • Skill budget (skills/**). skills/objectstack-ai/SKILL.md: 422 → 421 lines, 5776 → 5813 tokens (ceiling 6801). Whole package (sum of every SKILL.md): 4597 → 4596 lines, 58697 → 58734 tokens. The row is one table line edited in place; the growth is funded by deleting the pitfall line's restatement of the tool-level requiresConfirmation retirement (no execution path read it; ToolSchema is strict), which ## Tool already states. No re-wrap is counted. Net tokens +37, inside the ceiling.
  • Out of scope, reported to the seat: comment 6839924935 also asks that an AI service account's list_actions row carry an unavailable marker. That is a behaviour change (today domains/mcp.ts sets unavailable only from apiForwardCredentialRefusal), so it is not built here.

Verification

Local, on 2126444e (the change) and re-read on a7baa0d7 (after merging main 64c426a):

  • pnpm --filter @objectstack/mcp test — 63/63 files, 1094/1094 tests on 2126444; 63/63, 1097/1097 on a7baa0d (lock VERDICT command-exit 0).
  • pnpm --filter @objectstack/mcp typecheck — exit 0 (check:test-typecheck: OK) on 2126444.
  • node scripts/check-skills-token-ratchet.mjs — exit 0 on a7baa0d: skills/objectstack-ai/SKILL.md is 5813 tokens (ceiling 6801; headroom 988).
  • node scripts/pm/dispatch-gates.mjs --ran — ✓ 108 derived famil(ies) accounted for — 106 run, 2 NOT-MEASURED. Every run family exited 0 (check:skill-examples, check:lean-entry-closure and pnpm --dir ui exec vitest run scripts/ after building their prerequisites: 344 prose examples type-check; 171/171 files).
NOT MEASURED locally (2), left to CI
  • pnpm check:dual-build-cjs-loads — exit 3, needs every published package's dist (a full workspace build); this diff changes text only.
  • node scripts/check-plugin-teardown-shape.mjs --self-test — exit 3, its pinned commit 621a487 cannot be fetched into this shallow clone.

CI check-runs are the verdict for every other family; the seat reads them.


Generated by Claude Code

…irmation-gated action whatever it sends (#32936)

The skill table row, the served SKILL.md (and its Console mirror), the
run_action / resume_run tool descriptions and confirm members, the
actions.mdx posture table and the approval-over-MCP dogfood header now say
the rule the runtime already enforces (ADR-0151 D3 rule 3): under an AI
service account's own key the call is refused whatever it sends and nothing
runs; hand the action to a person, or its author declares
ai.requiresConfirmation: false when it is safe unattended. The person /
delegated-agent teaching (confirm: true after the human approves) stays.

No runtime change.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxjTGv9SGUZ76PjPuG
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/mcp/src/skill-md.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/mcp/src/skill-md.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 12 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 64c426ab22aa1f13290240aff1cd02dc1bf56027 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 94c9ad9f7261277df073f13e7b04573027cbcf12 — the merge of head a7baa0d746269fc53b8a2e7d4cdb3294e58e23c6 into base 64c426ab22aa1f13290240aff1cd02dc1bf56027, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 94c9ad9f7261277df073f13e7b04573027cbcf12 && git checkout 94c9ad9f7261277df073f13e7b04573027cbcf12
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 64c426ab22aa1f13290240aff1cd02dc1bf56027 a7baa0d746269fc53b8a2e7d4cdb3294e58e23c6 && git checkout -B drift-repro 64c426ab22aa1f13290240aff1cd02dc1bf56027 && git merge --no-ff a7baa0d746269fc53b8a2e7d4cdb3294e58e23c6

node scripts/docs-audit/affected-docs.mjs --json 64c426ab22aa1f13290240aff1cd02dc1bf56027

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#2 · os-tesla · session_01L9qibvXzthnUYrxzphgFgW

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a7baa0d746269fc53b8a2e7d4cdb3294e58e23c6
Local-runs: none
Scope: skills/objectstack-ai/SKILL.md + packages/mcp/src/skill-md.ts, packages/mcp/src/mcp-http-tools.ts, ui/apps/console/src/pages/developer/objectstack-skill.ts, packages/mcp/src/skill-md.test.ts, packages/mcp/src/mcp-resume-run-tool.test.ts; cited, read only: packages/runtime/src/action-execution.ts, packages/runtime/src/action-dispatch.ts, packages/runtime/src/domains/mcp.ts, packages/runtime/src/domains/actions.ts, packages/runtime/src/domains/automation.ts, packages/core/src/security/assemble-execution-context.ts, packages/core/src/security/resolve-authz-context.ts, packages/spec/src/ai/tool.zod.ts, docs/adr/0151-ai-service-account-a-headless-agent-is-a-registered-principal-of-its-own.md, scripts/check-skills-token-ratchet.mjs, .changeset/config.json, .github/workflows/lint.yml

Inputs read: card #32936 (body and all 7 comments, among them the scope additions 6839924935, 6842642177 and 6844145580 and the dev report 6846244737); PR #32926 (body, merge commit 6739719e, its five contract-review records and four seat ACCEPTs); PR #33057 (body, 10 files, net diff against main 64c426ab, which is also the merge base); the check-runs on this head. The face is the rules face: reviewFace returns one hit, skills/objectstack-ai/SKILL.md. content/docs/ui/actions.mdx, the two changesets and the dogfood file header are the lane seat's ACCEPT at TIER_DEFAULT: noted on in ③, not graded. Every "on main" below is read at 64c426ab.

① Derived judgments

  • Schema accept sets: none moves, right. run_action and resume_run keep their keys, types and closed shapes. The four hunks in mcp-http-tools.ts are two tool descriptions, two confirm member descriptions and one code comment. No handler line, registration condition or annotation changes.
  • Listing behaviour: none moves, right. list_actions (description and handler) is untouched and no file under packages/runtime is in the diff. An AI service account's row for a gated action reads as it does on main: requiresConfirmation: true, no unavailable. That is the card's "No runtime change", and it leaves half of scope addition 6839924935 unbuilt (③).
  • Refusal behaviour: none moves, right. The gate, the admitted sequence and the resume door are main's, byte for byte.
  • Served text, four carriers, each a text-only change: right. (a) tools/list: the run_action and resume_run descriptions and their confirm member descriptions. (b) renderSkillMarkdown: the run_action and resume_run entries. (c) The Console "Download skill" mirror: the same bytes. (d) The published objectstack-ai skill: one table row, one pitfall line.
  • Package entry surface and error codes: none, right. No export, signature or code is added, removed or renamed.
  • Gate verdicts: the check-runs on this head, read at 2026-11-13T07:05Z. 55 runs, all concluded: 46 success, 9 skipped (by event or opt-in), none failed, none pending. The eleven required contexts are all success. Bearing on this diff:
    • Test Core (7/8) ran @objectstack/mcp (read from its job log): 63 files, 1097 tests passed, skill-md.test.ts (16) and mcp-resume-run-tool.test.ts (10) among them. That is the server half of the mirror pin and both text pins.
    • Test UI (2/16) ran the Console half of the mirror pin (same source), objectstack-skill.test.ts (13 tests), green.
    • Type Check · source gates carries check:skill-docs, check:skill-refs and the published-skills token ratchet; Type Check · consumer gates carries check:skill-examples. Both success, with Check Changeset, Build Docs, Check Documentation Links and the eight Lint · lanes.
    • The Dogfood Regression Gate shards are skipped on a pull request. The dogfood file's edit is a header comment, so no verdict is owed for it here.

② Semver level

  • '@objectstack/skills': patch: right. The package ships the skills/** tree; one skill file's teaching text changes.
  • '@objectstack/mcp': patch: right. skill-md.ts and mcp-http-tools.ts ship strings only; the two test files publish nothing.
  • '@object-ui/console': patch, riding the mcp changeset: right, and needed. The mirror file ships inside the Console. The package is in the root changeset config's fixed group, and naming it alone is how Console source edits are declared on main.
  • No changeset for content/docs/ui/actions.mdx or the dogfood header: right, neither publishes.
  • Clause-②: no, line-initial in the PR body and in both changesets: right. Nothing widens an accept set or enlarges the public surface, and nothing narrows one; the narrowing was fix(runtime)!: an AI service account does not confirm an action for itself #32926's and was declared there. No !, no BREAKING banner, no ADR-0087 marker owed.
  • Metadata-protocol: none: right. Nothing changes what a metadata author can write. No Skills-docs: line is owed on a none kind with no arm.

③ Boundary flags

  • The Console mirror edit, outside the claim's file surface: forced, accepted. packages/mcp/src/skill-md.test.ts (renders the served text for %s) holds renderObjectStackSkill(options) equal to renderSkillMarkdown(options) byte for byte on main, and objectstack-skill.test.ts holds the same from the Console side. Editing skill-md.ts alone reds Test Core. The two hunks are identical (same added and removed lines), and both halves of the pin are green on this head (①). The third changeset package follows from it.
  • resume_run description and both confirm members, beyond the one description comment 6839924935 names: accepted. Same file, same family; leaving them would keep a retry-only lesson beside the corrected one.
  • Out-of-scope finding, the list_actions unavailable marker: evidence holds; not built here is right; escalated. On main, listActions in packages/runtime/src/domains/mcp.ts sets unavailable from apiForwardCredentialRefusal alone (lines 1127 and 1148) and never reads the caller's kind. requiresConfirmation on a row is actionLooksDestructive. So under an AI service account's key a gated action is listed with requiresConfirmation: true and no marker, and run_action then refuses it whatever it sends. The exported JSDoc on McpActionSummary.unavailable ("Present when run_action would refuse this action for THIS caller's credential before running it") already reads as covering that case. Building it moves a listing answer for one caller class in @objectstack/runtime: a behaviour change, outside "No runtime change" and outside this lane. Escalated to the seat: the PR body says Fixes #32936, so the merge closes the card with this half of 6839924935 undelivered. The finding needs its own card before this lands (class b, the dev's dedupe words), or the closing keyword goes. No such card is named in this round's inputs.
  • Two texts routed here that never reached the card. Delta record 6843779833 on fix(runtime)!: an AI service account does not confirm an action for itself #32926 sent two more sentences "for docs(skills, mcp): teach that an AI service account is refused a confirmation-gated action whatever it sends #32936": the comment above the decide actions in packages/plugins/plugin-approvals/src/sys-approval-request.object.ts ("refuses until the call carries the confirmation") and the approvals bullet in content/docs/ai/connect-mcp.mdx ("decides only once the call carries confirm: true"). Card comment 6844145580 carried only nit E, and this diff touches neither. Both are prose (a source comment; a docs page that carries the account bullet further down). The seat's call: the follow-up card, or drop.
  • Nit D, content/docs/ui/actions.mdx (prose face, not graded): delivered and true to the code. The sequence asks the gate on posture === 'ai' || isAiServiceAccountCaller(wiring.ec), so the 'rest' row's sentence holds under both postures. Two wording notes for the sentence check: "It asks for a confirmation once" introduces a refusal no confirmation satisfies, and at a host door there is no key, the caller is the executionContext the host passes.
  • Nit E, the dogfood header (lane seat's remainder, not graded): half delivered. The bullet now names the rule, as comment 6844145580 asks. The sentence record 6843779833 called stale still stands three lines up ("Nothing in the MCP door or the action door adds a check of its own, so the negative pins below are the service's gate ..., not a second gate") and the new bullet contradicts it: for the account pin the action door answers first. Comment only.
  • "No runtime change": confirmed (①). delete_record / delete_records teaching: untouched (④).
  • Runtime rule "verified on main c54a23d": the PR body's quotation is byte-identical to main at 64c426ab.
  • Local numbers in the report (1097 tests at this head; token ratchet exit 0, 5813 against 6801): CI and a hand count agree (①, ④). The two NOT MEASURED families (check:dual-build-cjs-loads, the teardown-shape self-test) were left to CI, and every check-run on this head is success or an expected skip.
  • Dogfood battery n/a: right. No ! in the title, no (narrowing) arm, a comment-only dogfood edit.
  • open_questions: none. security_findings: none; this review adds none.
  • Author: objectstack-fleet[bot]; no separate-author note is owed.
  • Landing: skills/** is Tier H. The PR is a draft with no auto-merge, and this record lifts nothing by itself.

④ Design judgment

The hit, skills/objectstack-ai/SKILL.md: holds against the code on main.

The added sentence: under an AI service account's own key (an agent acting for itself, for no person) the call is refused whatever it sends and nothing runs; hand the action to a person, or its author declares ai.requiresConfirmation: false when it is safe unattended.

  • Who is an account. isAiServiceAccountCaller is principalKind === 'agent' with no onBehalfOf. The shared assembler stamps that when the admitted identity's own row is of kind agent (authz.principalKind === 'agent'), and only an API key admits such a row (resolve-authz-context.ts; ADR-0151 D2: it holds no session). That arm wins over the OAuth arm and never sets onBehalfOf. An OAuth client's token gives principalKind: 'agent' with onBehalfOf naming the consenting person: the delegated agent, not an account. A person's own API key gives human. A missing envelope and the system context are neither. "An AI service account's own key ... for no person" names the first case and no other.
  • Every door. MCP run_action: invokeBusinessAction runs the one admitted sequence under ai with the caller's envelope. REST /actions: domains/actions.ts runs it under rest. A host's dispatchAction: the same sequence under rest or ai with the envelope the host passes. In the sequence the gate is asked when posture === 'ai' || isAiServiceAccountCaller(wiring.ec). MCP resume_run: resumeAdmissionRefusal hands ec to the gate for each candidate action. The four dispatch arms are called from the sequence alone.
  • "Whatever it sends." actionConfirmationRefusal returns on ai.requiresConfirmation !== true, then reads the member only when the caller is not an account. For an account on a declared true, nothing in the request changes the answer.
  • "Nothing runs." The gate is the last pre-dispatch check, before the subject load; at resume every refusal is thrown before resume(), so the run stays parked.
  • "Its author declares false." A declared false, and an absent key, return from the gate's first line for every caller.
  • The person and delegated-agent teaching is kept. "refused with the same 428 unless the call sends confirm: true ... ask the human first, then retry" stays, and it is still true for both: the member is read for any caller that is not an account.
  • delete_record / delete_records: untouched. Rows 202 and 203 of the skill, the two served entries and the two delete tool descriptions are main's; every hunk in mcp-http-tools.ts sits inside registerActionTools. Theirs is a different gate, the tool's own in packages/mcp, which reads no caller kind.
  • The scenario the card was graded on (an AI client on a service-account key retrying confirm: true forever): the reader now meets the refusal rule and a next step in the skill row, in the served SKILL.md and in both tool descriptions. Who reads the row: the author's coding agent planning a headless client, who can now choose between a person's hands and an ungated declaration before shipping one.

The same thing in the same terms. One sentence, word for word, in three places: the skill row, the served run_action entry (and its mirror), the HTTP run_action description. The shorter carriers use the same three terms ("an AI service account's own key", "refused whatever it sends" or "whatever the call sends", "hand ... to a person"): the served resume_run entry, the HTTP resume_run description, both confirm members ("it is not read: the call is refused whatever it sends"). They match the runtime's own message for an account ("a person runs this action") and the spec's rejection text in tool.zod.ts ("an agent acting for itself, not on a person's behalf"). One deviation, finding 1.

Implementing files, against that contract.

  • packages/mcp/src/skill-md.ts: agrees. Both entries are true as written; the resume_run entry places the account refusal among "the same gates as run_action", which is where the resume door puts it.
  • ui/apps/console/src/pages/developer/objectstack-skill.ts: agrees, by identity with the file above.
  • packages/mcp/src/mcp-http-tools.ts: agrees. Text only (①). "It is not read" is exact for the member. "The run stays paused" is exact for a refused resume.
  • skill-md.test.ts, mcp-resume-run-tool.test.ts: agree, and can fail. Each pinned phrase is absent from both sources on main. The new case sits in the tool's existing harness file and reads both tools off a real tools/list.

The budget. Counted in the gate's convention (bytes less the stamp line, over 4, rounded up): on main 23123 bytes, 422 lines, 5776 tokens; on this head 23270 bytes, 421 lines, 5813 tokens. The row grows by 251 bytes on its one line. The pitfall loses 104 bytes: a clause is deleted ("no execution path read it, so it produced no pause; ToolSchema is strict, so authoring it now fails the parse") and the two lines left are that deletion's own reflow. A real deletion, not a re-wrap; no re-wrap is counted as room. It pays the line count (minus one) and about 26 of the 63 added tokens. The other 37 come out of the file's existing headroom: the ceiling is 6801, no ceilings row is in the diff, and the gate's check-run is green. No ceiling moves, so no ruling is owed. See finding 3 for the wording.

The deleted restatement has a home. ## Tool metadata, some forty lines up, keeps the rule: "ToolSchema is strict — an unknown key (a typo, or a retired one) is a parse error", and requiresConfirmation is named among the retired keys, "each rejection carries its replacement". The rationale that went ("no execution path read it") is in the rejection an author who writes the key is shown (TOOL_RETIRED_KEY_GUIDANCE.requiresConfirmation). The pitfall keeps the removal, its ADR and a pointer ("retired above") that resolves. No rule is lost.

Tried to break it.

  • A delegated agent reading the account sentence: "for no person" excludes it, and its retry still works.
  • An agent on a person's API key that takes itself for "an agent acting for itself": it hands off where confirm: true would have run. It errs toward not asserting an approval nobody gave.
  • An account sending confirm: true, a truthy string or nothing, at MCP, REST or a host door under either posture: one refusal.
  • Another gate answering first: the confirmation gate is last, so an account's malformed params get their 400 and a missing capability its 403 before any 428. "Refused ... nothing runs" holds; the account sentence in the skill row promises no code.
  • Resume with several candidate actions: the first that passes admits, so an account's run whose flow an ungated action also targets resumes. Each text ties the refusal to the gated action, so each holds.
  • The agent as its own author: "its author declares false" could read as the agent's own move. An account's key is served the data skill with no authoring section; an OAuth caller's metadata edits land as drafts a person publishes. A person decides either way.
  • stdio: it serves no action tools, as the row already says.
  • The listing: see finding 2.

Findings.

  1. nit. mcp-http-tools.ts, resume_run description: "the run stays paused: hand it to a person". The nearest noun is the run. The same description says only the user whose call started the run can resume it (the door answers anyone else its one not-found), and ADR-0151 D3 rule 3 names the door that hands a run to a person as not yet built. Every other carrier says "hand the action to a person". The reader's next step is the same under either reading (stop retrying, tell a person). Say "the action" when the file is next touched.
  2. nit. All three client-facing texts hang the rule on what list_actions reports. The listing answers actionLooksDestructive (the declared flag, else mode: 'delete' or variant: 'danger'); the gate reads a declared true alone. An account therefore reads "refused whatever it sends" for an undeclared destructive-looking action that would run for it. The over-read is older than this diff (the person's sentence has it, harmlessly: always confirming is always correct) and for an account it errs to the safe side. The sentence's own subject, an action its author gated, is the declared flag, so the rule as written is true. A per-row unavailable computed from the declared flag is what settles it: carry this into the follow-up card.
  3. nit (prose faces). The PR body and the skills changeset say the growth is "funded by" the pitfall trim. Measured, the trim funds the line count and under half the tokens; plus 37 is taken from headroom, as the body's own next sentence says. The card's acceptance names check:skill-line-ratchet, which does not price skills/**; the token ratchet does, and it is green under an unmoved ceiling. Accepted as it stands.
  4. nit (lane seat's remainder). The dogfood header's stale sentence, ③.

Four nits; no should-fix, no must-fix. No delta review is owed.

Implemented-by: claude/project-thread-6mlhpm
Reviewed-by: session_01L9qibvXzthnUYrxzphgFgW

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#2 · os-tesla · session_01L9qibvXzthnUYrxzphgFgW

席位核对 · ACCEPT · 2026-11-13T07:09Z

Head-sha: a7baa0d746269fc53b8a2e7d4cdb3294e58e23c6

  1. Shape. Draft PR to main, Fixes #32936, 10 files, +83/−16 ≤ 3000. GOVERNED via skills/objectstack-ai/SKILL.md ⇒ Tier H, a human approval. Changesets: '@objectstack/skills': patch; '@objectstack/mcp': patch with '@object-ui/console': patch (the Console mirror ships in the Console). Clause-②: no, Metadata-protocol: none. Files are within claim 6845484647 as amended 6846607454 (the Console mirror, forced by the byte-identity pin).
  2. Premise. Holds: fix(runtime)!: an AI service account does not confirm an action for itself #32926 merged; the runtime rule (ADR-0151 D3 rule 3, actionConfirmationRefusal, isAiServiceAccountCaller) is on main as quoted in the body.
  3. Review. Contract review at CONTRACT_REVIEW_TIER by an isolated sub-agent on this head: PASS, 0 must-fix, 0 should-fix, 4 nits (6846560754). Every door traced (MCP run_action, resume_run, REST /actions, host dispatchAction); the delegated-agent and person teaching is kept; delete_record(s) untouched; no runtime line moves. Prose faces I read against the diff: content/docs/ui/actions.mdx posture-table sentence (true under both postures), the two changeset bodies, the dogfood header bullet.
  4. Gates. Check-runs on this head: 46 success, 9 skipped (by event or opt-in), 0 failure; mergeable clean. @objectstack/mcp 1097/1097 in Test Core (7/8); both halves of the mirror pin green (Test Core, Test UI (2/16)); token ratchet 5813/6801, no ceiling moves.
  5. Accepted deviations. N1: the HTTP resume_run description says "hand it to a person" where the others say "the action". N2: the listing's requiresConfirmation is actionLooksDestructive, wider than the gate's declared flag; for an account it errs safe, and the fix belongs to mcp: list_actions marks no action unavailable for an AI service account, though run_action refuses its confirmation-gated actions whatever it sends #33067. N3: body/changeset say the growth is "funded by" the pitfall trim; it funds the line and part of the tokens, the rest is existing headroom (gate green, no ceiling moved). N4: the dogfood header still carries one older sentence the new bullet qualifies (comment only).
  6. Out of scope / landing.

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 13, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review November 13, 2026 08:37
@os-zhuang
os-zhuang requested a review from hotlong as a code owner November 13, 2026 08:37
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 13, 2026
Merged via the queue into main with commit 8e3d4a5 Nov 13, 2026
65 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-6mlhpm branch November 13, 2026 08:58
@objectstack-fleet objectstack-fleet Bot removed the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 13, 2026
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/s tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(skills, mcp): teach that an AI service account is refused a confirmation-gated action whatever it sends

3 participants