Skip to content

contract: a hook refusal has no way to mark its message user-facing — the console's 403 substitution (ruled in #3821) needs a producer-side opt-in channel #9934

Description

@os-elon

Filed by the maintainer-directed PM session (session_01KspKtTajJmy3J37UhH8tcH, 2026-08-19) under the cross-seat transfer rule — the producer half of a ruled cross-repo split. Provenance: the maintainer accepted the objectui decision-inbox review's recommendation for objectui#5210 verbatim (「全部接受」, 2026-08-19, PM session chat): option 1 — producer-side opt-in.

The problem this solves

objectui#5210 (external report by @baozhoutao, app developer on 17.0.0-rc.5): the console form deliberately discards the server message on 403 and substitutes a generic string — that substitution is the recorded fix for #3821 (raw platform diagnostics leaking to end users) and must not be silently reverted. But the platform gives an application hook no way to distinguish author-written user guidance from platform diagnostics, so 11 real hook guards' actionable, localized refusal messages never reach users, and authors are incentivized to return 400 instead of 403 (degrading status-code semantics — the reporter explicitly refuses that workaround).

Ruled shape (accepted 2026-08-19)

Add the distinction once, at the contract level, producer-side — a way for an application hook's refusal to mark its message as user-facing (flag, field, or error-code family; the concrete shape is this card's design work, spec-first). Consumers (console form first) render a marked message and keep the #3821 generic substitution for everything unmarked. Not a 403 special case — the marking should be status-agnostic.

Explicitly rejected in the same ruling: consumer-side raw-403 opt-in (re-exposes platform diagnostics, per-app all-or-nothing), message-copy-only improvements, and working-as-intended.

Executable acceptance criterion

A beforeUpdate hook throwing a refusal with the user-facing marking gets its exact text rendered by the console form on 403; the same throw without the marking still renders the generic form.noPermissionToSave; a pin test states both halves. #3821's protection is preserved by construction.

Named reader

objectui#5210 carries Blocked-by: objectstack-ai/objectstack#<this> and implements the consumer half (form renders marked messages) once this lands and the pin covers it.

Refs: objectui#5210 (destination card, full measurement) · #3821 (the ruling the current substitution implements — preserved, not reverted) · #7366 (original report thread, 3 comments) · #7307 (related but distinct, 409 message content).

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions