Skip to content

Add seller-side available_actions enforcement helpers #2011

@bokelley

Description

@bokelley

Problem

@adcp/sdk@8.1.0-beta.12 now gives us the buyer-side pieces for media-buy available_actions[]:

  • generated types/schemas for MediaBuyAvailableAction, SLAWindow, ActionNotAllowedDetails
  • getAvailableActions / findAvailableAction
  • boolean gates (canIncreaseBudget, canExtendFlight, etc.)
  • getActionForMutation
  • preflightUpdateMediaBuy
  • ActionNotAllowedError
  • ACTION_NOT_ALLOWED in the standard error-code runtime table

That is enough for buyer-side preflight, but seller/training-agent implementations still have to hand-roll the server-side enforcement path: preserve/emit available_actions[], map an incoming update_media_buy mutation to the canonical action, reject non-direct modes with ACTION_NOT_ALLOWED, and build the exact error details payload.

During adcontextprotocol/adcp#5018, the training agent still needed local logic equivalent to:

  • derive attempted action(s) from update_media_buy request
  • look up the buy's resolved available_actions[]
  • allow direct update only for self_serve / maybe conditional_self_serve
  • reject requires_proposal / requires_approval direct updates as mode_mismatch
  • reject absent actions as not_supported_on_buy
  • return payload error ACTION_NOT_ALLOWED with details:
    • attempted_action
    • reason
    • currently_available_actions

The SDK has most primitives, but not the seller-side composition that makes this hard to drift from the spec.

Ask

Please add a server-side helper that makes the common seller path “just work”. Rough shape:

const result = enforceAvailableActionForUpdate(currentBuy, updateMediaBuyRequest, {
  directModes: ['self_serve', 'conditional_self_serve'],
});

if (!result.ok) {
  throw result.error; // or return result.payloadError
}

The helper should use the SDK's canonical getActionForMutation mapping internally and produce a spec-shaped ACTION_NOT_ALLOWED result on denial.

Desired output options:

type EnforceAvailableActionResult =
  | { ok: true; actions: ResolvedAction[]; matched: MediaBuyAvailableAction[]; modes: MediaBuyActionMode[] }
  | {
      ok: false;
      code: 'ACTION_NOT_ALLOWED';
      recovery: 'correctable';
      details: ActionNotAllowedDetails;
      error: AdcpError; // server-side structured error, if importing from @adcp/sdk/server
    };

It would also help to expose a small builder for the payload shape when a seller has already determined the attempted action/reason:

buildActionNotAllowedError(details: ActionNotAllowedDetails, opts?: { message?: string }): AdcpError
buildActionNotAllowedPayload(details: ActionNotAllowedDetails, opts?: { message?: string }): { errors: [...] }

Why this belongs in the SDK

The attempted-action mapping and ACTION_NOT_ALLOWED details shape are protocol logic, not training-agent-specific behavior. If every seller adapter implements this independently, they will drift on:

  • update_media_buy field → MediaBuyValidAction mapping
  • rollup behavior for legacy coarse actions
  • mode_mismatch vs unsupported reasons
  • whether currently_available_actions[] is echoed
  • recovery classification and message text

The SDK already centralizes buyer preflight; seller enforcement should use the same source of truth.

Non-goals

The SDK does not need to own scenario seeding or the training agent's internal storage shape. Those can remain app-specific. The useful shared piece is the enforcement/error builder over SDK-shaped MediaBuyActionContext + UpdateMediaBuyRequestLike.

Repro context

Protocol repo branch for adcontextprotocol/adcp#5018 added the media_buy_seller/available_actions storyboard. With beta.12, the branch can use SDK types and buyer helpers, but still carries custom seller-side enforcement to pass the storyboard.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions