Skip to content

actionLooksDestructive keys destructiveness off confirmText — the exact signal #7278/#7309 are migrating away, and 6 of 14 actions flip #7828

Closed

Description

@os-zhuang

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

  1. Does any ai.exposed === true object today rely on the confirmText signal to be classified destructive? If none does, this is confirmed latent and can be fixed calmly on its merits.
  2. 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 explicit ai.requiresConfirmation, or description presence — noting that the last one would classify every migrated action as destructive, which is the opposite over-correction.
  3. 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

Activity

  1. added
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Aug 11, 2026
  2. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    ContributorAuthor

    Findings-triage route A: appended domain:cli — routing only; the finding grade is untouched and no ownership is taken.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. added theissue type on Aug 12, 2026
  4. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Findings-round escalation: needs-user-decision (domain:cli, type Bug). The hold's timing objection is gone and the erosion is now live on origin/main: #7309 closed via merged PR #7827, the 14 identity actions no longer carry confirmText — so actionLooksDestructive (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

    Recommendation: A now; B only on measured need.

    • Platform long-term coherence: A removes a stale coupling; B adds a declared key that every authoring surface must then maintain; C rebuilds the bug.
    • Measured business pull: the 6/14 flip is measured and live today — A repairs it with a deletion. Nobody has yet asked for per-action declared destructiveness — B has zero pull.
    • AI-agent error-resistance: a safety property keyed off UI copy is precisely what AI authors get wrong; declared semantics (mode/variant) are closed enumerations.
    • Startup scope discipline: A is a two-token diff plus pins; B is a spec key owed maintenance forever.

    Veto/confirm on A is sufficient; B can be re-proposed later with a measured victim.

    本评论来自分诊座位(scheduled session session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。


    Generated by Claude Code

  5. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    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: actionLooksDestructive classifies on declared semantics only — drop the confirmText leg; mode === 'delete' || variant === 'danger' remain. A UI-copy heuristic must not decide an AI-facing safety property. Option B (an explicit declared destructive key in packages/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; :622 today). Deliverable includes pins on both directions: a confirmText-only action no longer classifies destructive; mode: 'delete' and variant: 'danger' still do. Check the 6-of-14 flipped identity actions read correctly after the change. needs-user-decision → pm:queue swapped with this record.

    本评论来自分诊座位(scheduled session session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。


    Generated by Claude Code

  6. self-assigned this
    on Aug 12, 2026
  7. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Claimed and dispatched — domain:cli PM seat (#6024), session session_01B3Kurx8qufrDzNjk4rag7V, branch claude/issue-7828-destructive-declared-semantics.

    Executing the binding ruling recorded in comment 5265943521: Option A — actionLooksDestructive classifies on declared semantics only; the confirmText leg is dropped, mode === 'delete' || variant === 'danger' remain. Option B (a declared destructive key in packages/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 :622 and 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, and mode: '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

  8. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    <!-- 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

  9. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    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

  10. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor
    {
      "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

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

Metadata

Metadata

Assignees

Labels

domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions