# Secure Sub-flow Patterns
# Demonstrates the unified capability model: every flow declares requires:,
# effective capabilities = requires(sub-flow) ∩ capabilities(caller) ∩ cap(run-step).
#
# Key principles:
#   - Every flow MUST declare requires: (even if empty: requires: {})
#   - No capDrop: — just declare fewer capabilities in the sub-flow's requires:
#   - No ALL/NONE string shorthands — use explicit arrays/booleans
#   - cap: on run: steps further restricts (intersection with caller + sub-flow requires:)
#   - cap: INHERIT passes all caller capabilities (sub-flow still bounded by its own requires:)

flowmarkup:
  title: Secure Sub-flow Demo
  version: 1
  description: "Shows capability restriction for sub-flow invocation using explicit requires: and cap:"

  requires:
    ENV: [REGION, DB_URL]
    CONTEXT: [correlation_id, status]
    SERVICES: [claude]
    SUBFLOWS: true
    REQUEST: ["api.example.com", "api.partner.com"]
    SECRET: [api_token, anthropic_api_key, partner_api_key]

  input:
    user_feedback: TEXT
    order_id: STRING
  output:
    sentiment_score: NUMBER
    payment_receipt: JSON
  throws:
    - PaymentTimeoutError
  examples:
    positive_feedback:
      _notes_: Positive feedback results in high sentiment score and successful payment
      input:
        user_feedback: The product was excellent and arrived on time!
        order_id: ord-5001
      output:
        sentiment_score: 0.92
        payment_receipt:
          receipt_id: rcpt-8821
          amount: 99.99
          status: success
    negative_feedback:
      _notes_: Negative feedback with low sentiment score
      input:
        user_feedback: Very disappointed with the quality.
        order_id: ord-5002
      output:
        sentiment_score: 0.12
        payment_receipt:
          receipt_id: rcpt-8822
          amount: 49.99
          status: success

  vars:
    payload:

  do:
    # --- Pattern 1: Remote flow with explicit cap: allowlist ---
    # Sub-flow declares its own requires:. cap: further restricts what we pass.
  - run:
      _label_: Analyze sentiment (remote)
      flow: https://flows.example.com/sentiment.flowmarkup.yaml
      integrity: sha256-abc123def456
      cap:
        SECRET: [anthropic_api_key]
        CONTEXT: { read: [correlation_id] }
        SERVICES: [claude]
      params:
        text: =user_feedback
      result:
        sentiment_score: =RESULT.score
      timeout: 30s

    # --- Pattern 2: Local flow — cap: restricts to exactly what's needed ---
    # No capDrop: needed. The sub-flow's own requires: limits it; cap: further restricts.
  - run:
      _label_: Charge customer
      flow: billing/charge-customer.flowmarkup.yaml
      cap:
        CONTEXT: [correlation_id]
        SECRET: [api_token]
      params:
        order_id: =order_id
        amount: 99.99
      result:
        payment_receipt: =RESULT.receipt

    # --- Pattern 3: Complete sandbox (cap: with empty object = NONE for all) ---
  - run:
      _label_: Pure computation
      flow: plugins/untrusted-plugin.flowmarkup.yaml
      cap: {}
      params:
        data: =user_feedback
      result:
        payload: =RESULT.output

    # --- Pattern 4: Async fire-and-forget with handle for cancellation ---
  - run:
      _label_: Send notification
      flow: notifications/send-email.flowmarkup.yaml
      handle: notification_proc
      async: true
      cap:
        CONTEXT: { read: [correlation_id] }
      params:
        to: customer@example.com
        subject: Order Confirmed

    # --- Pattern 5: Parallel sub-flows with different restriction levels ---
  - parallel:
      _label_: "Parallel: trusted local + restricted remote"
      internal_branch:
        # Local sub-flow — cap: INHERIT passes all caller capabilities.
        #
        # SECURITY RISK: cap: INHERIT forwards EVERY capability the caller
        # holds (ENV, REQUEST, SECRET, SERVICES, CONTEXT, MAIL, SUBFLOWS)
        # to the sub-flow.  This means the sub-flow receives access to ALL
        # secrets, ALL allowed hosts, ALL services, etc. — which is almost
        # certainly more than it needs.  If the sub-flow is compromised or
        # has a bug, the blast radius is the caller's full permission set.
        #
        # RECOMMENDATION: Replace cap: INHERIT with an explicit, minimal
        # grant listing only the capabilities the sub-flow actually needs:
        #   cap:
        #     REQUEST: ["api.example.com"]
        #     CONTEXT: { read: [correlation_id] }
        #
        # Use cap: INHERIT ONLY when:
        #   1. The sub-flow is fully trusted (e.g., same repository, same team).
        #   2. The sub-flow's own requires: is itself minimal and well-audited.
        #   3. Listing capabilities explicitly is impractical (dynamic sets).
      - run:
          flow: internal/core-logic.flowmarkup.yaml
          cap: INHERIT
          params:
            data: =user_feedback
          result:
            payload: =RESULT.output
      partner_branch:
        # Remote — grant only what's needed
      - run:
          flow: https://partner.com/enrich.flowmarkup.yaml
          integrity: sha256-def456ghi789
          cap:
            SECRET: [partner_api_key]
            REQUEST: ["api.partner.com"]
          params:
            data: =user_feedback
          result:
            payload: =RESULT.output

    # --- Pattern 6: Fine-grained context access (per-key read/write) ---
  - run:
      _label_: Dynamic processor
      flow: dynamic/processor.flowmarkup.yaml
      cap:
        ENV: "=['REGION_' + 'us', 'DB_URL']"
        CONTEXT:
          read: [correlation_id]
          write: [status]
        REQUEST: ["api.example.com"]
      params:
        data: =payload

    # --- Pattern 7: Handle + source + cancel orchestration ---
  - run:
      _label_: Long-running payment
      flow: payments/process.flowmarkup.yaml
      handle: payment_proc
      async: true
      cap:
        SECRET: [api_token]
      params:
        order_id: =order_id

  - race:
      wait_result:
      - waitFor:
          event: payment_completed
          source: =payment_proc
          timeout: 5m
          capture:
            payment_receipt: =EVENT.DATA.receipt
      timeout_guard:
      - wait: 4m
      - cancel: =payment_proc
      - throw:
          error: PaymentTimeoutError
          message: "Payment timed out for order {{order_id}}"
