@@ -11,11 +11,11 @@ import {
1111import { BoundarySafeError } from '@/executor/errors/boundary'
1212
1313/**
14- * The source workspace's payer has no headroom for this custom-block child run.
14+ * The payer has no headroom for this custom-block child run.
1515 *
16- * Boundary-safe: publishing and invocation are both gated on a single
17- * organization, so the exhausted limit always belongs to the consumer's own org.
18- * Surfacing it lets them act on it instead of seeing an opaque failure .
16+ * Boundary-safe only as far as the message allows: see
17+ * { @link admitCustomBlockChildExecution} for why actor-scoped denials are
18+ * collapsed to { @link GENERIC_USAGE_LIMIT_MESSAGE} before reaching here .
1919 */
2020export class CustomBlockAdmissionError extends BoundarySafeError {
2121 constructor ( message : string ) {
@@ -24,6 +24,13 @@ export class CustomBlockAdmissionError extends BoundarySafeError {
2424 }
2525}
2626
27+ /**
28+ * Consumer-safe stand-in for a denial whose real message describes the SOURCE
29+ * workflow owner's personal billing state rather than the shared organization.
30+ */
31+ const GENERIC_USAGE_LIMIT_MESSAGE =
32+ 'This custom block is unavailable because a usage limit was reached. Ask an organization admin to review it.'
33+
2734/**
2835 * Admits one custom-block child run against the SOURCE workspace's payer.
2936 *
@@ -39,9 +46,23 @@ export async function admitCustomBlockChildExecution(
3946 attribution : BillingAttributionSnapshot
4047) : Promise < void > {
4148 const usage = await checkAttributedUsageLimits ( attribution )
42- if ( usage . isExceeded ) {
43- throw new CustomBlockAdmissionError ( usage . message ?? 'Workspace usage limit exceeded' )
44- }
49+ if ( ! usage . isExceeded ) return
50+
51+ // Only the payer-scoped denial describes the shared organization ("Organization
52+ // usage limit exceeded: $X pooled of $Y organization limit"), which both sides
53+ // genuinely share and which the consumer can act on.
54+ //
55+ // The other gates are evaluated against `actorUserId` — the SOURCE workflow's
56+ // owner — and their text is addressed to that person: their account-frozen /
57+ // payment-method state, or "Ask an organization admin to raise YOUR credit
58+ // limit" against their individual member cap. Forwarding those verbatim would
59+ // show one org member another's private billing state. Being in the same
60+ // organization makes the *payer* shared; it does not make personal quotas
61+ // shared. The `usage_limit` type still tells the consumer what kind of failure
62+ // this was.
63+ const message =
64+ usage . scope === 'payer' && usage . message ? usage . message : GENERIC_USAGE_LIMIT_MESSAGE
65+ throw new CustomBlockAdmissionError ( message )
4566}
4667
4768/**
0 commit comments