Uh oh!
There was an error while loading. Please reload this page.
fix(runtime): actionLooksDestructive classifies on declared semantics only - #8128
Conversation
… only Drops the confirmText leg from actionLooksDestructive (packages/runtime/src/ action-execution.ts). mode === 'delete' || variant === 'danger' remain the signal -- closed, declared enumerations an author sets on purpose, not UI dialog copy a heuristic was never meant to read as an AI-facing safety property. confirmText is being withdrawn by design: #7278/#7309 moved identity-object confirm questions onto `description` instead, and measured on #7309's merged branch (PR #7827), 6 of its 14 migrated actions flipped from destructive to not-destructive the moment their confirmText was dropped, because none of them declares mode:'delete' or variant:'danger' to fall back on. Maintainer ruling: issue #7828, comment 5265943521 (Option A adopted). Fixes#7828 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also reference the affected code. These are read-only:
|
hotlong
commented
Aug 12, 2026
PM review — |
action-execution-destructive.test.ts reads the REAL sys_* identity declarations to prove today's platform objects are excluded before actionLooksDestructive ever runs on them. That import resolved through `exports` to platform-objects/dist -- a build artifact -- so all 66 pins were a verdict about build state rather than about the declarations in the checkout. `pnpm check:test-source-alias` (#7668/#7778) reported it as a NEW unaliased artifact import on @objectstack/runtime. Aliases platform-objects to source in packages/runtime/vitest.config.ts. resolve.alias becomes the ARRAY form because only that form accepts a RegExp find; the pre-existing string entries keep the prefix-match semantics they had as object keys (Vite normalizes an alias object into exactly this list, in this order), so no other resolution changes. The new entries are ANCHORED, one rule for every namespace rather than an enumeration of the ones reached today -- the PR #7778 constraint, same shape as @objectstack/spec in packages/qa/downstream-contract (PR #8129). `/plugin` is listed ahead of the namespace rule because it is the one exported subpath that is a FILE (src/plugin.ts) and not a directory. The registry entry in scripts/check-test-source-alias.mjs is untouched. Measured, both directions: - artifact-resolved (before): 66 passed - source-resolved (after): 66 passed - per-test diff of the two verbose runs: IDENTICAL, name for name. The 14-action pins read `type`/`ai.exposed` off the imported objects through actionByName(), which throws when an action is missing, so an identical name+verdict set means source and dist agree on every declaration these pins touch. No pin changed verdict; none modified. Reverse verification (the alias is live, not decorative): with sys_user.ban_user's `type` flipped 'api' -> 'script' in SOURCE only and no rebuild, the suite reports 1 failed / 65 passed -- `expected 'script' to be 'api'` at :309. dist/identity/index.mjs:81 still carries `type: "api"`, i.e. the identical tree read green through the pre-alias config. Injection reverted; no test was weakened. Part of #7828 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V
hotlong
commented
Aug 12, 2026
The red |
| resolution | result |
|---|---|
| artifact (before the alias) | 66 passed |
| source (after the alias) | 66 passed |
The per-test diff of the two --reporter=verbose runs is identical, name for name. That is stronger than "both green": the 14-action pins read type and ai.exposed off the imported objects via actionByName(), which throws when an action is missing, so an identical name-and-verdict set means source and dist agree on every declaration these pins touch. No pin changed verdict in either direction, and no artifact/source disagreement about a shipped declaration was found — so there is nothing to file here.
No dual-instance or circular-import problem appeared either (the kind PR #8129 hit on defineConnector for @objectstack/spec); these pins compare plain declaration properties and hold no cross-entry-point identity.
Reverse verification — the alias is live, not decorative
Direction predicted before running: the pin should go red, because the injected change exists only in source.
With sys_user.ban_user's type flipped 'api' to 'script' in source only, no rebuild:
FAIL src/action-execution-destructive.test.ts > ... > sys_user.ban_user is excluded by
isHeadlessInvokableAction (gate 2: type is not script/flow)
AssertionError: expected 'script' to be 'api'
Tests 1 failed | 65 passed (66)
packages/platform-objects/dist/identity/index.mjs:81 still carried type: "api" at that moment — i.e. the identical tree read green through the pre-alias config. Injection reverted; the tree pushed is clean.
Verification
| gate | result |
|---|---|
pnpm check:test-source-alias | OK — 72 packages with tests scanned; 63 registered |
pnpm check:test-source-alias --self-test | OK |
pnpm --filter @objectstack/runtime test | 143 files, 2242 passed |
pnpm --filter @objectstack/runtime typecheck | clean |
pnpm check:type-check-debt | OK — 33 entries re-measured, none above its recorded number; @objectstack/runtime measured at its recorded 227, not raised (it does not appear in the lowerable list) |
node scripts/check-nul-bytes.mjs | OK — 7367 files, no raw control bytes |
The debt gate was run with the full closure built first (turbo run build --filter=./packages/* --filter=./packages/*/*), since it compiles *.test.ts that the package typecheck script excludes.
Generated by Claude Code
hotlong
commented
Aug 12, 2026
PM review of the follow-up ( |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7828
What changed
actionLooksDestructive(packages/runtime/src/action-execution.ts) classified an action as destructive if it carriedconfirmText(UI dialog copy) ORmode: 'delete'ORvariant: 'danger'.confirmTextis being withdrawn by design — #7278/#7309 moved identity-object confirm questions ontodescriptioninstead of pairingconfirmTextwithparams(two dialogs for one decision) — so the heuristic's input is a signal actively being deleted from real metadata. Measured on #7309's merged branch (PR #7827): 6 of its 14 migrated identity actions flipped from destructive to not-destructive the moment theirconfirmTextwas dropped, because none of them declaresmode: 'delete'orvariant: 'danger'to fall back on.Maintainer ruling (issue #7828, comment 5265943521, Option A adopted, quoted verbatim):
This PR executes exactly that: drop the
confirmTextleg, keepmode === 'delete' || variant === 'danger'. Options B (a declareddestructive/impactkey inpackages/spec) and C (re-deriving the signal fromaction.descriptionor confirm presence) are explicitly out of scope per the ruling and are not introduced here.Is this observable on a live path today?
actionLooksDestructive's sole caller issummarizeAction, reached only from the MCPlistActionsbridge (packages/runtime/src/domains/mcp.ts, the sole production implementer ofpackages/mcp'sMcpActionBridge). That bridge gates every candidate action through three independent checks beforesummarizeActionever runs:sys_*objects (isSystemObjectName)isHeadlessInvokableAction— onlytype: 'script'ortype: 'flow'has a headless dispatch path at allactionAiExposureError— excluded unless the author setai.exposed: trueAll 14 of #7309's identity actions live on
sys_*objects, aretype: 'api'(notscript/flow, so gate 2 excludes them independently of gate 1), and none declaresai.exposed. So today's real declarations are excluded on three independent grounds beforeactionLooksDestructiveever runs on them — the #7828 triage comment named two (sys_*+ai.exposed); gate 2 (isHeadlessInvokableAction) is a third, confirmed in this PR's test suite against the real object declarations. This fix has zero observable effect on any request a caller can make today.That is a fact about today's platform objects, not about the heuristic — a future
ai.exposed, non-sys_*,script/flowaction carrying onlyconfirmTextwould have had its classification silently flip live, which is the erosion #7828 was filed to stop. The new test suite pins both the (currently dead) real identity-action declarations and a synthetic action shaped like a future live-reachable one, through the actualsummarizeActionboundary — not just the raw predicate — so "latent today" is pinned as a fact about today's objects rather than mistaken for the classifier being unreachable in principle.Tests
New file:
packages/runtime/src/action-execution-destructive.test.ts(66 tests, all green):confirmText-only action no longer classifies as destructive (several variants).mode: 'delete'andvariant: 'danger'still classify destructive, including combined withconfirmText, and the pre-existingai.requiresConfirmationoverride still wins over everything.@objectstack/platform-objects/identity, not hand-rolled fixtures):generate_backup_codes,regenerate_backup_codes,change_slug,enable_oauth_application,disable_oauth_application,rotate_client_secret— all now read not-destructive, each pin also guards its own fixture premise (noconfirmText, nomode:'delete'/variant:'danger').disable_two_factor,delete_organization,leave_organization) still read destructive.summarizeAction'srequiresConfirmationfield (the actual contract a caller reads) mirrors the same rule.ai.exposed, non-sys_*,script-type action proves the boundary is reachable in general, and that the erosion class this issue closes is real (would have flipped without the fix).confirmTextandparams, so one click opens two sequential dialogs (same shape as #7278) #7309's real identity-action declarations are independently confirmed excluded by each of the three MCP gates today.Reverse verification
Restored the
confirmTextleg via a temporary edit (notgit stash— a plainEditround-trip within this worktree), ran the new suite, and 5 of 66 tests went red for exactly the expected reason — every "confirmText alone" pin, and only those:(same failure text, same shape, for the 4 other confirmText-only pins: the "non-danger variant" and "non-delete mode" combinations,
summarizeAction's confirmText-only case, and the synthetic live-reachable-shape case)The other 61 tests (mode/variant pins, the 6 flipped identity actions, the still-destructive siblings, the 14-gate documentation) stayed green, confirming they test something independent of the reverted leg. Restored the fix and confirmed the restore is byte-identical to the intended change (
git diff --stat HEADonaction-execution.tsmatched the saved patch exactly).Full verification run
pnpm --filter @objectstack/runtime test— 143 test files, 2242 tests, all passing (includes the new file).pnpm --filter @objectstack/runtime typecheck— clean (tsc --noEmit, no output).pnpm check:type-check-debt— OK, 33 ledger entries re-measured, none above their recorded ceiling;@objectstack/runtime's entry (227) did not rise.node scripts/check-nul-bytes.mjs— OK.Out of scope
packages/spec(Option B, not adopted).action.descriptionor confirm presence (Option C, rebuilds the defect).packages/runtime/src/domains/automation.tsuntouched (concurrent dispatchPOST /api/v1/automationanswers 500 INTERNAL_ERROR for a malformed flow body — including the #4277 undeclared-config-key refusal the #7545 ruling leans on #8055 owns that file).Changeset
.changeset/action-destructive-declared-semantics.md— patch on@objectstack/runtime(this changes an AI-facing classification surface).Generated by Claude Code