id: agent_notification_configs
version: "1.0.0"
title: "Agent notification config lifecycle"
category: schema_validation
summary: "Validates the 3.2 agent-level capabilities.changed registration surface: advertised revision fencing, NO_AUTH rejection, and dry-run clear semantics."
track: core
required_tools:
  - sync_agent_notification_configs

narrative: |
  `sync_agent_notification_configs` registers agent-level webhook subscribers
  such as `capabilities.changed`. Unlike account-level `notification_configs[]`,
  this surface is scoped to the authenticated caller principal rather than an
  account. Implementations must therefore reject anonymous registration, keep
  declarative replacement caller-scoped, and expose a durable
  `capabilities_version` fence so receivers can re-read
  `get_adcp_capabilities` after a webhook and know whether the notified
  revision is observable.

  This storyboard covers the portable vectors that do not require an outbound
  webhook receiver: capability declaration, unauthenticated rejection, and
  dry-run clear semantics. Endpoint proof, signed delivery, multi-principal
  caller isolation, and missed-webhook repair should be exercised by
  implementation-specific suites or future controller-backed storyboards that
  can simulate receiver endpoints and multiple principals.

agent:
  interaction_model: any
  capabilities: []
  examples:
    - "Any AdCP agent that advertises sync_agent_notification_configs"

caller:
  role: buyer_agent
  example: "Compliance test harness"

prerequisites:
  description: |
    The agent under test advertises `sync_agent_notification_configs` and
    `adcp.capability_changes.notifications.supported: true` in
    `get_adcp_capabilities`.
  test_kit: "test-kits/acme-outdoor.yaml"
  controller_seeding: false

phases:
  - id: capability_declaration
    title: "Advertise revision-fenced capability notifications"
    narrative: |
      An agent that supports capability-change notifications must publish an
      opaque `capabilities_version` and bounded `cache_ttl_seconds` in the same
      capability block. `last_modified` may be present for humans, but it is
      not sufficient as the webhook revision fence.

    steps:
      - id: get_capabilities_declares_agent_notifications
        title: "Read capability notification declaration"
        task: get_adcp_capabilities
        schema_ref: "protocol/get-adcp-capabilities-request.json"
        response_schema_ref: "protocol/get-adcp-capabilities-response.json"
        doc_ref: "/protocol/get_adcp_capabilities"
        stateful: false
        expected: |
          Return `adcp.capability_changes.notifications.supported: true` with:
          - capabilities_version present
          - cache_ttl_seconds present
          - registration_task: sync_agent_notification_configs
          - event_types[] containing capabilities.changed

        sample_request:
          context:
            correlation_id: "agent_notification_configs--get_capabilities"

        validations:
          - check: response_schema
            description: "Response matches get-adcp-capabilities-response.json schema"
          - check: field_value
            path: "adcp.capability_changes.notifications.supported"
            value: true
            description: "Agent advertises capability-change notifications"
          - check: field_present
            path: "adcp.capability_changes.capabilities_version"
            description: "Opaque revision token is present for read-after-notify fencing"
          - check: field_present
            path: "adcp.capability_changes.cache_ttl_seconds"
            description: "Bounded polling repair TTL is present"
          - check: field_value
            path: "adcp.capability_changes.notifications.registration_task"
            value: "sync_agent_notification_configs"
            description: "Registration task is the agent-level notification surface"
          - check: field_contains
            path: "adcp.capability_changes.notifications.event_types[*]"
            value: "capabilities.changed"
            description: "Agent supports capabilities.changed invalidation fires"

  - id: unauthenticated_registration
    title: "Reject NO_AUTH registration"
    narrative: |
      Registration replacement is keyed by the authenticated principal. If the
      transport request has no stable principal, the seller cannot isolate one
      caller's declarative subscriber set from another's and must reject before
      any persistence or endpoint proof.

    steps:
      - id: sync_agent_notification_configs_rejects_no_auth
        title: "Submit agent notification config with no credentials"
        task: sync_agent_notification_configs
        schema_ref: "protocol/sync-agent-notification-configs-request.json"
        response_schema_ref: "protocol/sync-agent-notification-configs-response.json"
        doc_ref: "/protocol/sync_agent_notification_configs"
        stateful: false
        auth: none
        expect_error: true
        negative_path: payload_well_formed
        expected: |
          Reject the unauthenticated registration with:
          - HTTP 401 (preferred) or 403
          - no persisted subscriber changes

        sample_request:
          idempotency_key: "$generate:uuid_v4#agent_notification_configs_no_auth"
          notification_configs:
            - subscriber_id: "registry-cache"
              url: "https://example.com/webhooks/adcp/capabilities"
              event_types:
                - "capabilities.changed"
              active: false
          context:
            correlation_id: "agent_notification_configs--no_auth"

        validations:
          - check: http_status_in
            allowed_values: [401, 403]
            description: "Anonymous registration must be rejected before any caller-scoped replacement can occur"

  - id: dry_run_clear
    title: "Validate dry-run clear without mutation"
    narrative: |
      `notification_configs: []` clears only the authenticated caller's
      subscriber set. With `dry_run: true`, the seller validates the operation
      and reports the would-be action without changing persisted subscribers or
      sending endpoint proof challenges.

    steps:
      - id: sync_agent_notification_configs_dry_run_clear
        title: "Dry-run clear caller-scoped subscribers"
        task: sync_agent_notification_configs
        schema_ref: "protocol/sync-agent-notification-configs-request.json"
        response_schema_ref: "protocol/sync-agent-notification-configs-response.json"
        doc_ref: "/protocol/sync_agent_notification_configs"
        stateful: false
        expected: |
          Return a schema-valid response with action `cleared` or `unchanged`
          and echo the correlation context. No endpoint proof challenge is sent
          during dry-run.

        sample_request:
          idempotency_key: "$generate:uuid_v4#agent_notification_configs_dry_run_clear"
          notification_configs: []
          dry_run: true
          context:
            correlation_id: "agent_notification_configs--dry_run_clear"

        validations:
          - check: response_schema
            description: "Response matches sync-agent-notification-configs-response.json schema"
          - check: field_value
            path: "action"
            allowed_values: ["cleared", "unchanged"]
            description: "Dry-run clear reports the would-be caller-scoped replacement outcome"
          - check: field_value
            path: "dry_run"
            value: true
            description: "Response confirms no mutation was applied"
          - check: field_value
            path: "context.correlation_id"
            value: "agent_notification_configs--dry_run_clear"
            description: "Context correlation_id returned unchanged"
