Repository navigation
actionLooksDestructive keys destructiveness off confirmText — the exact signal #7278/#7309 are migrating away, and 6 of 14 actions flip #7828
Description
Reported as an out_of_scope_findings item by the #7309 dev (report), deliberately not fixed there. Filed unassigned; assign at the moment you start.
No domain:* label applied — routing is the triage seat's call. For the record the site is packages/runtime.
The mechanism
actionLooksDestructive (packages/runtime/src/action-execution.ts:620) treats the presence of confirmText as a destructiveness signal.
#7278 and #7309 are deliberately removing confirmText from actions that also declare params, moving the question onto description — so the heuristic's input is being withdrawn by design, one card at a time. Measured on #7309's branch: 6 of its 14 migrated actions flip from destructive to not-destructive — generate_backup_codes, regenerate_backup_codes, change_slug, enable_oauth_application, disable_oauth_application, rotate_client_secret — and none of them carries mode: 'delete' or variant: 'danger' to fall back on.
Latent today — and the reason it is latent is not a property of the heuristic
The #7309 dev established this and it should not be re-derived: the only caller is summarizeAction, reachable only from MCP listActions, which is gated twice — fail-closed on sys_* objects (all 14 qualify) and requiring ai.exposed === true (false on all 14). Nothing changes today.
But both gates are facts about today's platform objects, not about the heuristic. An ai.exposed object — third-party or future first-party — declaring the same shape reaches the live path with a destructiveness signal that the platform's own authoring guidance is actively telling authors to stop writing. The erosion is also cumulative across cards: #7278 moved 2 actions, #7309 moves 14, and #7428 intends to make the pair a rule, at which point confirmText-beside-params stops existing in new metadata altogether.
⛔ What NOT to do
Do not fix this by declaring ai: { requiresConfirmation: true } on the affected system actions. That was considered and rejected on #7309 for the right reason: it adds dead metadata to a doubly-unreachable path, which is speculative widening. That reasoning is endorsed here — the fix belongs in the heuristic, not in per-action annotations papering over it.
Step one — measure before designing
- Does any
ai.exposed === trueobject today rely on theconfirmTextsignal to be classified destructive? If none does, this is confirmed latent and can be fixed calmly on its merits. - What should the signal be, now that the platform's own convention moves confirm questions to
description? Candidates worth measuring rather than presuming:mode: 'delete',variant: 'danger', an explicitai.requiresConfirmation, ordescriptionpresence — noting that the last one would classify every migrated action as destructive, which is the opposite over-correction. - Whether the heuristic should exist at all, versus requiring an explicit declaration. A heuristic keyed off UI dialog copy to decide an AI-facing safety property is a category confusion worth naming, and it is the reason this is filed rather than patched.
"Latent, no change needed yet" is a valid outcome if step 1 comes back empty and the design question is recorded — but it should be an outcome someone measured, not one inherited from this card.
Related
- approvals:
sys_approval_request.approval_reject/approval_recalldeclare bothconfirmTextandparams, so one decision opens two sequential dialogs #7278 (ruling: carry the confirm question ondescription; moved 2 actions) - platform-objects: 16 more actions declare both
confirmTextandparams, so one click opens two sequential dialogs (same shape as #7278) #7309 / PR fix(platform-objects): one decision, one dialog — carry identity confirm questions ondescription(#7309) #7827 (moved 14; where this was measured) - spec: guard the
confirmText+paramsPAIR at authoring time, once the 20 shipped sites have migrated toaction.description#7428 (the authoring-time guard that would make the pair a rule — note its guard must be scoped toActionSchema, per the correction filed on that card)
Activity
os-zhuang commentedon Aug 11, 2026
Findings-triage route A: appended
domain:cli— routing only; thefindinggrade is untouched and no ownership is taken.actionLooksDestructivelives inpackages/runtime/src/action-execution.ts(the card anchors:620), sole callersummarizeActionon the MCPlistActionspath —packages/runtimeandpackages/mcpare bothdomain:cliper the lanes table. The heuristic's input is being withdrawn bydomain:metadatacards (approvals:sys_approval_request.approval_reject/approval_recalldeclare bothconfirmTextandparams, so one decision opens two sequential dialogs #7278/platform-objects: 16 more actions declare bothconfirmTextandparams, so one click opens two sequential dialogs (same shape as #7278) #7309) and eventually spec: guard theconfirmText+paramsPAIR at authoring time, once the 20 shipped sites have migrated toaction.description#7428 (domain:spec), but the fix — re-basing the destructiveness signal offconfirmText— lands in runtime.finding, correctly: double-gated dead today (sys_*fail-closed +ai.exposed === falseon all 14), and the right fix likely wants spec: guard theconfirmText+paramsPAIR at authoring time, once the 20 shipped sites have migrated toaction.description#7428's authoring-rule shape settled first so the signal is redesigned once, not twice. Dispatching now would race the very migration eroding it.sys_approval_request.approval_reject/approval_recalldeclare bothconfirmTextandparams, so one decision opens two sequential dialogs #7278/platform-objects: 16 more actions declare bothconfirmTextandparams, so one click opens two sequential dialogs (same shape as #7278) #7309 (the migrations) and spec: guard theconfirmText+paramsPAIR at authoring time, once the 20 shipped sites have migrated toaction.description#7428 (the authoring guard); no card owns the heuristic itself.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
hotlong commentedon Aug 12, 2026
Findings-round escalation:
needs-user-decision(domain:cli, type Bug). The hold's timing objection is gone and the erosion is now live onorigin/main: #7309 closed via merged PR #7827, the 14 identity actions no longer carryconfirmText— soactionLooksDestructive(packages/runtime/src/action-execution.ts:622:confirmText || mode === 'delete' || variant === 'danger') has measurably flipped 6 of 14 actions to "not destructive" for AI callers. #7428's ruling (refuse severity, schema-boundary scoping) is settled with PR #7972 in draft, so the signal can now be redesigned once.Question
What signal should classify an action as destructive for AI callers — and should a UI-copy heuristic ever decide an AI-facing safety property?
Options
confirmTextleg now: classify on declared semantics only (mode === 'delete' || variant === 'danger'). Pure deletion of a now-wrong signal; no contract change.destructive/impactkey to the action contract inpackages/spec. Honest and closed, but new permanent authoring surface (Feature, fable clause).action.description/confirm presence post-spec: guard theconfirmText+paramsPAIR at authoring time, once the 20 shipped sites have migrated toaction.description#7428): re-couples safety to UI copy, the exact defect class again.Recommendation: A now; B only on measured need.
mode/variant) are closed enumerations.Veto/confirm on A is sufficient; B can be re-proposed later with a measured victim.
本评论来自分诊座位(scheduled session
session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。Generated by Claude Code
hotlong commentedon Aug 12, 2026
Maintainer ruling recorded — Option A adopted. Provenance: maintainer, 2026-08-12 (live, in the PM session that ran this findings round), replying to the round report that tabled this card's four-lens recommendation: 「同意你的决策」. The recommendation adopted is Option A of this round's escalation comment above.
Binding ruling:
actionLooksDestructiveclassifies on declared semantics only — drop theconfirmTextleg;mode === 'delete' || variant === 'danger'remain. A UI-copy heuristic must not decide an AI-facing safety property. Option B (an explicit declareddestructivekey inpackages/spec) is not adopted now — it may be re-proposed only with a measured need, as a separate card.Dispatch constraints (cli seat): landing
packages/runtime/src/action-execution.ts(locate by symbol;:622today). Deliverable includes pins on both directions: aconfirmText-only action no longer classifies destructive;mode: 'delete'andvariant: 'danger'still do. Check the 6-of-14 flipped identity actions read correctly after the change.needs-user-decision→pm:queueswapped with this record.本评论来自分诊座位(scheduled session
session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。Generated by Claude Code
hotlong commentedon Aug 12, 2026
Claimed and dispatched —
domain:cliPM seat (#6024), sessionsession_01B3Kurx8qufrDzNjk4rag7V, branchclaude/issue-7828-destructive-declared-semantics.Executing the binding ruling recorded in comment 5265943521: Option A —
actionLooksDestructiveclassifies on declared semantics only; theconfirmTextleg is dropped,mode === 'delete' || variant === 'danger'remain. Option B (a declareddestructivekey inpackages/spec) is explicitly not in scope and must not be introduced here.Landing
packages/runtime/src/action-execution.ts, located by symbol — the ruling records:622and the original card:620, which is itself the evidence that the line number is not the anchor.Both required pins carried into the brief: a
confirmText-only action no longer classifies destructive, andmode: 'delete'/variant: 'danger'still do — the second direction is what stops the fix from being "delete the function." Plus the check the ruling names: the 6-of-14 identity actions that flipped after #7309 (merged PR #7827) read correctly afterwards.No file conflict with the concurrent #8055 dispatch (
domains/automation.ts); same package, different file.Generated by Claude Code
hotlong commentedon Aug 12, 2026
<!-- os-dev-report -->
{ "issue": 7828, "status": "done", "branch": "claude/issue-7828-destructive-declared-semantics", "pr": "https://github.com/objectstack-ai/objectstack/pull/8128", "premise_still_valid": true, "summary": "Dropped the confirmText leg from actionLooksDestructive (packages/runtime/src/action-execution.ts, located by symbol) per the binding ruling (comment 5265943521, Option A): mode === 'delete' || variant === 'danger' remain the sole signal. Added packages/runtime/src/action-execution-destructive.test.ts (66 tests) pinning both required directions plus the 6-of-14 flipped identity actions read off their real @objectstack/platform-objects/identity declarations, sibling still-destructive identity actions, the summarizeAction boundary contract, and a synthetic live-reachable-shape case. Added a changeset (patch, @objectstack/runtime) since this changes an AI-facing classification.", "tests": "pnpm --filter @objectstack/runtime test: 143 files / 2242 tests passed (includes the new file, 66/66). pnpm --filter @objectstack/runtime typecheck: clean (tsc --noEmit, no output). pnpm check:type-check-debt: OK, 33 ledger entries re-measured against a full packages/* closure build, none above their recorded ceiling; @objectstack/runtime's entry stayed at 227 (not raised). node scripts/check-nul-bytes.mjs: OK. Reverse verification: restored the confirmText leg via a plain Edit round-trip (not git stash), re-ran the suite -- 5 of 66 tests went red, every 'confirmText alone' pin and only those, each with 'AssertionError: expected true to be false // Object.is equality' (received true because the reverted code re-added confirmText to the OR). The other 61 (mode/variant pins, the 6 flipped identity actions, still-destructive siblings, the 14-gate documentation) stayed green. Restored the fix; git diff --stat HEAD on action-execution.ts matched the saved patch byte-for-byte.", "open_questions": [], "out_of_scope_findings": [], "observability_finding": "actionLooksDestructive's sole caller is summarizeAction, reached only from the MCP listActions bridge in packages/runtime/src/domains/mcp.ts (the sole production implementer of packages/mcp's McpActionBridge). That bridge gates every candidate action through THREE independent checks before summarizeAction runs: (1) isSystemObjectName fail-closed on sys_*, (2) isHeadlessInvokableAction -- only type:'script' or type:'flow' has a headless dispatch path, (3) actionAiExposureError -- excluded unless ai.exposed===true. All 14 of #7309's identity actions are on sys_* objects, are type:'api' (so gate 2 excludes them independently of gate 1), and none declares ai.exposed. So today's real declarations are excluded on THREE independent grounds (the #7828 triage comment named two; gate 2/isHeadlessInvokableAction is a third I found and confirmed against the real object declarations in the test suite) before actionLooksDestructive ever runs on them. This fix has ZERO observable effect on any request a caller can make today -- the classifier's output is computed and discarded behind all three gates for every action that carries confirmText in the repo today (I also swept examples/app-todo and examples/app-showcase for any live-reachable ai.exposed+confirmText action that might flip: the one candidate, app-todo's delete_completed, already carries variant:'danger' too, so it does not flip). Unit pins on the real identity-action declarations are correct and are the check the ruling calls for, but they do not exercise the MCP domain's gating wiring (untouched by this PR) and must not be read as proving end-to-end behavior through that wiring. To still cover the boundary honestly, the suite also pins summarizeAction directly (the actual function that produces the requiresConfirmation field a caller reads) and a synthetic action shaped like a future ai.exposed/non-sys_*/script-type action carrying only confirmText -- proving the boundary IS reachable in general and that this fix closes a real (if not yet live) erosion path, without overclaiming coverage of domains/mcp.ts's gate wiring itself." }Generated by Claude Code
hotlong commentedon Aug 12, 2026
os-dev-report
(Correction: the previous comment's marker was HTML-entity-escaped by my own mistake and did not survive as a recognizable marker — this comment carries the same report using the documented plain-text fallback. No tool in this session's toolset can edit/delete the prior comment; the dev-report JSON below is the authoritative one.)
{ "issue": 7828, "status": "done", "branch": "claude/issue-7828-destructive-declared-semantics", "pr": "https://github.com/objectstack-ai/objectstack/pull/8128", "premise_still_valid": true, "summary": "Dropped the confirmText leg from actionLooksDestructive (packages/runtime/src/action-execution.ts, located by symbol) per the binding ruling (comment 5265943521, Option A): mode === 'delete' || variant === 'danger' remain the sole signal. Added packages/runtime/src/action-execution-destructive.test.ts (66 tests) pinning both required directions plus the 6-of-14 flipped identity actions read off their real @objectstack/platform-objects/identity declarations, sibling still-destructive identity actions, the summarizeAction boundary contract, and a synthetic live-reachable-shape case. Added a changeset (patch, @objectstack/runtime) since this changes an AI-facing classification.", "tests": "pnpm --filter @objectstack/runtime test: 143 files / 2242 tests passed (includes the new file, 66/66). pnpm --filter @objectstack/runtime typecheck: clean (tsc --noEmit, no output). pnpm check:type-check-debt: OK, 33 ledger entries re-measured against a full packages/* closure build, none above their recorded ceiling; @objectstack/runtime's entry stayed at 227 (not raised). node scripts/check-nul-bytes.mjs: OK. Reverse verification: restored the confirmText leg via a plain Edit round-trip (not git stash), re-ran the suite -- 5 of 66 tests went red, every 'confirmText alone' pin and only those, each with 'AssertionError: expected true to be false // Object.is equality' (received true because the reverted code re-added confirmText to the OR). The other 61 (mode/variant pins, the 6 flipped identity actions, still-destructive siblings, the 14-gate documentation) stayed green. Restored the fix; git diff --stat HEAD on action-execution.ts matched the saved patch byte-for-byte.", "open_questions": [], "out_of_scope_findings": [], "observability_finding": "actionLooksDestructive's sole caller is summarizeAction, reached only from the MCP listActions bridge in packages/runtime/src/domains/mcp.ts (the sole production implementer of packages/mcp's McpActionBridge). That bridge gates every candidate action through THREE independent checks before summarizeAction runs: (1) isSystemObjectName fail-closed on sys_*, (2) isHeadlessInvokableAction -- only type:'script' or type:'flow' has a headless dispatch path, (3) actionAiExposureError -- excluded unless ai.exposed===true. All 14 of #7309's identity actions are on sys_* objects, are type:'api' (so gate 2 excludes them independently of gate 1), and none declares ai.exposed. So today's real declarations are excluded on THREE independent grounds (the #7828 triage comment named two; gate 2/isHeadlessInvokableAction is a third I found and confirmed against the real object declarations in the test suite) before actionLooksDestructive ever runs on them. This fix has ZERO observable effect on any request a caller can make today -- the classifier's output is computed and discarded behind all three gates for every action that carries confirmText in the repo today (I also swept examples/app-todo and examples/app-showcase for any live-reachable ai.exposed+confirmText action that might flip: the one candidate, app-todo's delete_completed, already carries variant:'danger' too, so it does not flip). Unit pins on the real identity-action declarations are correct and are the check the ruling calls for, but they do not exercise the MCP domain's gating wiring (untouched by this PR) and must not be read as proving end-to-end behavior through that wiring. To still cover the boundary honestly, the suite also pins summarizeAction directly (the actual function that produces the requiresConfirmation field a caller reads) and a synthetic action shaped like a future ai.exposed/non-sys_*/script-type action carrying only confirmText -- proving the boundary IS reachable in general and that this fix closes a real (if not yet live) erosion path, without overclaiming coverage of domains/mcp.ts's gate wiring itself." }Generated by Claude Code
test(runtime): resolve @objectstack/platform-objects to source in vitest
hotlong commentedon Aug 12, 2026
{ "issue": 7828, "status": "done", "branch": "claude/issue-7828-destructive-declared-semantics", "pr": "https://github.com/objectstack-ai/objectstack/pull/8128", "premise_still_valid": true, "summary": "Follow-up commit bd0c95988 on the existing PR #8128 branch, fixing the red `ESLint` CI job, which was not a lint error at all: that job runs a series of check:* scripts and the failure was `pnpm check:test-source-alias` reporting @objectstack/platform-objects as a NEW unaliased artifact import on @objectstack/runtime. action-execution-destructive.test.ts imports the real sys_* identity declarations from @objectstack/platform-objects/identity, which resolved through `exports` to platform-objects/dist, so all 66 pins were a verdict about build state rather than about the checkout. Aliased the package to source in packages/runtime/vitest.config.ts: resolve.alias converted to the ARRAY form (only that form accepts a RegExp find; the pre-existing string entries keep the identical prefix-match semantics they had as object keys, so no other resolution changed), with ANCHORED entries and ONE subpath rule covering every namespace rather than an enumeration; `/plugin` is listed ahead of it because it is the one exported subpath that is a FILE (src/plugin.ts) and not a directory, where the namespace rule would produce src/plugin/index.ts, a path that does not exist and that the gate cannot catch because it still reads as pointing at source. The registry in scripts/check-test-source-alias.mjs was NOT widened, and none of the 66 pins was modified, weakened or skipped. NO PIN CHANGED VERDICT when resolution moved from artifact to source: 66 passed both ways and the per-test diff of the two verbose runs is identical name for name, so no artifact/source disagreement about a shipped declaration exists to file. No dual-instance or circular-import problem surfaced either.", "tests": "check:test-source-alias BEFORE: '✗ @objectstack/runtime: NEW unaliased artifact import(s) since this entry was measured: @objectstack/platform-objects.' AFTER: 'check-test-source-alias OK — 72 packages with tests scanned; 63 registered as still resolving a workspace dep through `dist/`.' Self-test leg: 'check-test-source-alias --self-test OK'. ARTIFACT-vs-SOURCE comparison (dep closure built first via pnpm --filter '@objectstack/runtime^...' build): artifact-resolved run of src/action-execution-destructive.test.ts --reporter=verbose = 'Tests 66 passed (66)'; source-resolved run after the alias = 'Tests 66 passed (66)'; normalized per-test diff of the two = IDENTICAL (66 lines each, zero differences), which also means the 14 it.each pins enumerated the same actions from both sources (their names are built from the declarations and actionByName() throws on a missing action). REVERSE VERIFICATION, direction predicted before running (red, since the injection exists only in source): with sys_user.ban_user `type` flipped 'api'->'script' in SOURCE ONLY and no rebuild, the suite reported 'Tests 1 failed | 65 passed (66)' with \"AssertionError: expected 'script' to be 'api'\" at action-execution-destructive.test.ts:309, while packages/platform-objects/dist/identity/index.mjs:81 still carried type: \"api\" — i.e. the identical tree read green through the pre-alias config. Injection reverted with git checkout (never git stash); git status shows only vitest.config.ts modified. FULL SUITE: pnpm --filter @objectstack/runtime test = 'Test Files 143 passed (143) / Tests 2242 passed (2242)'. pnpm --filter @objectstack/runtime typecheck = clean (tsc --noEmit, no output). pnpm check:type-check-debt: --self-test OK (23 semantic + 24 observation + 15 re-measure + 12 built-closure + 9 auto-lowering cases); --re-measure, run after `turbo run build --filter=./packages/* --filter=./packages/*/*` = 'OK — 33 ledger entr(ies) re-measured in 314.0s, 1966 raw tsc error(s) total, none above its recorded number'; @objectstack/runtime does NOT appear in the lowerable-entry list, so it measured exactly its recorded ceiling of 227 and was not raised. node scripts/check-nul-bytes.mjs = 'OK (scanned 7367 text file(s) ... no raw ASCII control bytes)', plus a targeted control-byte self-scan of the edited config (no hits).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
fix(runtime): actionLooksDestructive classifies on declared semantics…
test(spec): the first ui/ file group's test titles state each cited d…