edge:
  id: "edge-example"
  name: "Example Edge"

server:
  listen: "0.0.0.0:9090"
  # advertise_host is the host other components (Node bootstrap, OpenAI client)
  # use to reach this edge. Empty means resolve automatically from interfaces.
  advertise_host: ""

# bootstrap is the artifact/bootstrap channel iop-edge can render in
# bootstrap commands and `env` output. Leave artifact_base_url empty until a
# field channel is provisioned.
bootstrap:
  artifact_base_url: ""

tls:
  enabled: false
  cert: "/run/secrets/iop/edge.crt"
  key: "/run/secrets/iop/edge.key"
  ca: "/run/secrets/iop/credential-plane-ca.crt"
  peer_role: "node"

# Legacy source-selection example (active below): credential_plane is false,
# and the OpenAI section may opt into bearer_token, principal_tokens, or
# provider_auth without changing the IOP Authorization/provider-token boundary.
#
# Managed source-selection example (comment-only, secret-free): set this block
# to enabled: true, keep the OpenAI legacy auth fields empty/disabled, enable
# the edge-node TLS, Control Plane mTLS, and OpenAI HTTPS blocks in this file,
# then restart the Edge. Key fields remain deployment file paths, never values.
credential_plane:
  # Omitted or false keeps the legacy credential mode. In legacy mode,
  # openai.bearer_token, openai.principal_tokens, and openai.provider_auth may
  # be used. Set enabled to true only after the secure Control Plane path and
  # OpenAI HTTPS listener below are ready.
  #
  # Managed mode accepts only Control Plane principal projections and
  # credential leases. It rejects all configured or caller-supplied legacy
  # credential sources and never falls back to them while the projection or
  # lease path is unavailable. Enabling, disabling, or rolling back this mode
  # is restart-required; config refresh does not partially apply the change.
  enabled: false
  lease_ttl_seconds: 30
  lease_cache_size: 256

logging:
  level: "info"
  pretty: true
  # path is the edge log file. Empty falls back to <binary-dir>/logs/edge.log
  # for the bundled dev binary; explicit values win.
  path: ""

metrics:
  port: 19092

# control_plane connects this Edge to a Control Plane instance.
# Set enabled: true and wire_addr to the Control Plane TCP endpoint to
# activate the outbound connector. Leave enabled: false (default) for
# standalone / local-only Edge deployments.
control_plane:
  enabled: false
  wire_addr: ""
  reconnect_interval_sec: 5
  tls:
    enabled: false
    cert: "/run/secrets/iop/edge.crt"
    key: "/run/secrets/iop/edge.key"
    ca: "/run/secrets/iop/credential-plane-ca.crt"
    server_name: "control-plane.internal"
    peer_role: "control-plane"

# refresh enables the Edge-local admin API used by `iop-edge config refresh`.
# Keep it loopback-only unless an operator-specific access control layer is added.
refresh:
  enabled: false
  listen: "127.0.0.1:19093"

# Requests with an estimated input token count at or above this value are
# classified as long-context for admission policy.
long_context_threshold_tokens: 100000

# provider_pool is the canonical, shared queue policy owner for every provider
# candidate in the pool. Legacy per-provider max_queue/queue_timeout_ms are
# compatibility fields only; once provider_pool is set, those legacy values
# are ignored by effective policy resolution.
provider_pool:
  max_queue: 16
  queue_timeout_ms: 30000

a2a:
  enabled: false
  listen: "0.0.0.0:8081"
  path: "/a2a"
  node: ""
  adapter: "openai_compat"
  session_id: "a2a"
  timeout_sec: 120
  bearer_token: ""

openai:
  enabled: false
  # HTTPS is mandatory when credential_plane.enabled is true.
  tls:
    enabled: false
    cert: "/run/secrets/iop/edge-openai.crt"
    key: "/run/secrets/iop/edge-openai.key"
  listen: "0.0.0.0:18081"
  bearer_token: ""
  # principal_tokens maps IOP bearer-token-operation identities to hashed
  # OpenAI-compatible bearer tokens. Raw tokens are never stored here; only
  # a SHA-256 hash (lowercase hex, 64 chars) of each issued token is kept.
  # When set, callers authenticate via hash match against this list; the
  # legacy single bearer_token above still works as an unmapped fallback.
  # Example (values below are illustrative hashes, not real tokens):
  #   principal_tokens:
  #     - token_ref: "iop-tok-alice"
  #       token_hash_sha256: "<sha256 hex of the issued token>"
  #       principal_ref: "user:alice"
  #       principal_alias: "alice"
  principal_tokens: []
  # provider_auth forwards a caller-supplied raw provider token to the selected
  # OpenAI-compatible provider tunnel. The raw token is read only from the
  # request header and must not be stored in config, docs, or logs.
  provider_auth:
    enabled: false
    # Defaults when enabled:
    # from_header: "X-IOP-Provider-Authorization"
    # target_header: "Authorization"
    # scheme: "Bearer"
    # required: true
  # In managed credential mode, keep bearer_token, principal_tokens, and
  # provider_auth empty/disabled. The dedicated legacy provider credential
  # header is rejected before routing and is never forwarded upstream.
  # The selected provider determines the execution path: OpenAI-compatible
  # providers use passthrough, while native inference providers use normalized
  # execution. Caller metadata does not select the route or response shape.
  node: ""
  # Stable provider identity for direct/fallback OpenAI dispatch attribution.
  # Required when openai.enabled=true. A legacy model_routes entry can override
  # it with its own provider_id; adapter text is never used as this identity.
  provider_id: ""
  adapter: "ollama"
  target: ""
  models: []
  # Legacy/compatibility model_routes (discouraged for new deploys).
  # model_routes was the adapter/target route catalog that mapped external model
  # ids to adapter + target. It is superseded by the provider-pool approach:
  #   1. Define canonical routing keys with models[].
  #   2. Declare provider candidates with nodes[].providers[].
  #   3. Link them via models[].providers[provider_id] = served_model.
  # For details see the Provider-pool section below.
  # This block is a backward-compat fallback only. New deploys should use
  # top-level models[] + nodes[].providers[].
  session_id: "openai"
  timeout_sec: 120
  strict_output: true
  strict_stream_buffer: false
  # stream_evidence_gate configures request-local Recovery Coordinator, ingress snapshot limits,
  # and optional caller-neutral semantic output filters.
  # enabled: route supported Chat Completions, normalized Responses, and provider tunnel passthrough
  #   through the streamgate request runtime instead of the legacy eager-write path (default: false).
  # max_request_fault_recovery: request fault recovery cap (0..3, default: 3). Explicit 0 disables recovery.
  # max_strategy_fault_recovery: strategy fault recovery cap (0..max_request_fault_recovery, default: inherits request limit).
  # max_ingress_snapshot_bytes: ingress snapshot size limit in bytes (1..16777216 [16 MiB], default: 16777216).
  stream_evidence_gate:
    enabled: false
    environment: dev # dev | dev-corp; request-start selector snapshot
    max_request_fault_recovery: 3
    max_strategy_fault_recovery: 3
    max_ingress_snapshot_bytes: 16777216
    # filters are disabled by omission. Each policy has one unique filter kind:
    # repeat_guard (request-local history plus Unicode rolling/current-stream
    # inspection), schema_gate (only when metadata.scheme is present), or
    # provider_error (error-event lifecycle observation only; matching and
    # retry semantics belong to a follow-up task). Repeat evidence and
    # observations retain fingerprints/counts/offsets, never prompt, output,
    # reasoning, tool arguments, or results. A provider must advertise
    # the configured capability in lifecycle_capabilities only when a blocking
    # policy is enabled. Selectors may refine enablement/enforcement by
    # environment, model_group, model, or provider; caller/agent identity is not
    # a selector.
    # filters:
    #   - filter: repeat_guard
    #     enforcement: blocking # blocking | observe_only
    #     capability: output.repeat_guard
    #     hold_evidence_runes: 500
    #     timeout_ms: 5000
    #   - filter: schema_gate
    #     enforcement: blocking
    #     capability: output.schema_gate
    #   - filter: provider_error
    #     enforcement: blocking
    #     capability: output.provider_error
    #     selectors:
    #       - type: provider # environment | model_group | model | provider
    #         key: "provider-id"
    #         enabled: true

# === Protocol Profiles (optional) ===
# protocol_profiles is the top-level reusable catalog of protocol profile
# overlays. It combines immutable built-in profiles (openai, gemini,
# anthropic, glm, kimi, minimax_chat, minimax_messages, mimo_chat,
# mimo_messages, grok, seulgi_chat, seulgi_messages)
# with operator-defined custom overlays. Each entry is keyed by a stable
# profile id and may extend a built-in or another custom profile via the
# Base field. Resolution happens once at config load into immutable
# ConcreteProtocolProfile snapshots carried onto each provider.
#
# Built-in profile ids (selectable via nodes[].providers[].profile or
# normalized from the legacy provider type):
#   openai          — openai_chat driver, https://api.openai.com/v1
#   gemini          — openai_chat driver, https://generativelanguage.googleapis.com/v1beta/openai
#   anthropic       — anthropic_messages driver, https://api.anthropic.com + /v1/messages
#   glm             — openai_chat driver, https://api.z.ai/api/paas/v4
#   glm_coding      — openai_chat driver, https://api.z.ai/api/coding/paas/v4 (Coding Plan)
#                    Coding Plan uses the same Bearer contract as General API but a
#                    distinct endpoint and subscription quota. It is not enabled by
#                    default. See official usage policy:
#                    https://docs.z.ai/legal-agreement/subscription-terms
#                    https://docs.z.ai/devpack/usage-policy
#                    - Must be used only for supported tooling / personal use or with
#                      separate provider authorization.
#                    - Proxy, shared, or production redistribution without authorization
#                      is prohibited.
#                    - No automatic fallback exists between General API and Coding Plan;
#                      selecting one profile routes to one endpoint exclusively.
#   kimi            — openai_chat driver, https://api.moonshot.cn/v1
#   minimax_chat    — openai_chat driver, https://api.minimax.io/v1
#   minimax_messages — anthropic_messages driver, https://api.minimax.io/anthropic
#   mimo_chat       — openai_chat driver, https://api.xiaomimimo.com/v1 (pay-go)
#   mimo_messages   — anthropic_messages driver, https://api.xiaomimimo.com/anthropic (pay-go)
#   grok            — openai_chat driver, https://api.x.ai/v1
#   seulgi_chat     — openai_chat driver, Seulgi GPT OpenAI-compatible
#   seulgi_messages — anthropic_messages driver, Seulgi Claude native Messages
#
# === GLM General API / Coding Plan example (comment-only, not enabled) ===
# Z.AI Coding Plan uses the same Bearer contract as General API but a distinct
# endpoint and subscription quota. Endpoint selection is driven by external
# model IDs, not by an extra request field. Both profiles expose only
# models + chat_completions with Bearer auth and must never be mapped under
# the same external model ID when endpoint/quota isolation is required.
#
# No automatic fallback exists between General API and Coding Plan;
# selecting one profile routes to one endpoint exclusively.
# Coding Plan may only be enabled for use allowed by current Z.AI terms
# (https://docs.z.ai/legal-agreement/subscription-terms).
#
#   models:
#     - id: "glm-5.1-api"
#       providers:
#         glm-api: "glm-5.1"
#     - id: "glm-5.1-coding"
#       providers:
#         glm-coding: "glm-5.1"
#   nodes:
#     - id: "node-glm-example"
#       providers:
#         - id: "glm-api"
#           type: "openai_api"
#           category: "api"
#           profile: "glm"
#           models: ["glm-5.1"]
#           capacity: 1
#         - id: "glm-coding"
#           type: "openai_api"
#           category: "api"
#           profile: "glm_coding"
#           models: ["glm-5.1"]
#           capacity: 1
#
# When endpoint/quota isolation is required, keep glm-5.1-api and glm-5.1-coding
# as separate external model IDs mapping to distinct provider IDs.
# Do not place both providers under the same external model id.
# Do not put raw credential values here; use managed credential slots instead.
#
# Example custom overlay extending the openai built-in:
#   protocol_profiles:
#     my-openai-proxy:
#       base: "openai"
#       base_url: "https://my-proxy.example.invalid/v1"
#       auth:
#         header: "Authorization"
#         scheme: "Bearer"
#
# Each nodes[].providers[] entry may set profile to select a catalog entry.
# A provider endpoint overrides only that provider's cloned profile base URL;
# it never mutates the built-in catalog. MiMo plan-specific URLs should be set
# explicitly as provider endpoints instead of being guessed from the pay-go URL.
# When profile is empty, the provider type is used for legacy alias
# normalization (e.g. vllm/openai_api → openai, seulgivibe_claude →
# seulgi_messages).

# === Provider-pool (models[] / nodes[].providers[]) — recommended ===
# Top-level models[] defines canonical routing keys and their provider-pool mapping.
# Each entry id is the external model id; providers map maps provider id → served model.
#
# Example:
#   models:
#     - id: "qwen3.6:35b"
#       display_name: "Qwen 3.6 35B"
#       context_window_tokens: 262144
#       providers:
#         vllm-gpu: "nvidia/Qwen3.6-35B-A3B-NVFP4"
#         ollama-local: "qwen3.6:35b"
#
# nodes[].providers[] defines each provider candidate with catalog and execution fields (Provider-First).
# nodes[].providers[].id is the stable provider identity referenced by models[].providers keys.
# nodes[].providers[].type — runtime type (vllm, ollama, lemonade, sglang,
#   openai_api, seulgivibe_claude, seulgivibe_openai).
# nodes[].providers[].category — "api" or "local_inference".
# nodes[].providers[].endpoint / base_url — provider transport fields.
# nodes[].providers[].models — served model names this provider can serve.
# nodes[].providers[].health — observed health state string.
# nodes[].providers[].capacity — provider-pool max concurrent execution slots; 0 is not dispatchable.
# nodes[].providers[].total_context_tokens — provider runtime total KV/context budget used for
#   long-context admission. Must satisfy total_context_tokens >= context_window_tokens *
#   long_context_capacity for every model group referencing this provider. Live-apply on refresh.
# nodes[].providers[].long_context_capacity — concurrent long-context slots (context_window-sized
#   requests). Long requests occupy a normal slot and a long slot; 0 means no dedicated long limit.
#   Live-apply on refresh.
# nodes[].providers[].max_queue — legacy per-provider queue depth (compatibility only).
#   Effective policy is owned by the root provider_pool.max_queue.
#   When provider_pool is set, per-provider values are ignored.
# nodes[].providers[].queue_timeout_ms — legacy per-provider queue timeout (compatibility only).
#   Effective policy is owned by the root provider_pool.queue_timeout_ms.
#   When provider_pool is set, per-provider values are ignored.
# nodes[].providers[].lifecycle_capabilities — coarse lifecycle capabilities list.
# nodes[].providers[].enabled — on/off dispatch switch; omit or true = enabled, false = excluded from
#   dispatch pool. Disabled providers appear in status with status=disabled, capacity=0. Does not
#   stop adapter processes. Classified as live-apply (no restart required) on config refresh.

console:
  adapter: "ollama"
  target: "qwen3.6:35b"
  session_id: "default"
  background: false
  timeout_sec: 240

# Top-level models[] defines canonical routing keys and their provider-pool mapping.
# models[].id is the external model id; providers maps provider id → served model.
models:
  - id: "qwen3.6:35b"
    # Defaults to provider. Set model_group only when every candidate is
    # operator-approved as the same logical model for query-time rollup.
    usage_attribution: "model_group"
    display_name: "Qwen 3.6 35B"
    context_window_tokens: 262144
    default_max_tokens: 32768
    min_max_tokens: 32768
    default_thinking_token_budget: 1024
    providers:
      mac-mlx-vllm: "mlx-community/Qwen3.6-35B-A3B-4bit"
      # Same model group can keep a normalized-only Ollama provider as a low
      # capacity/high priority fallback candidate. Clients do not select this
      # execution path; capacity + priority + availability choose the provider.
      ollama-local: "qwen3.6:35b"
  - id: "local-llama3"
    # Omitted usage_attribution defaults to actual-provider attribution.
    display_name: "Local Llama 3"
    context_window_tokens: 8192
    providers:
      # Ollama-only model groups are valid and run through normalized execution.
      ollama-local: "llama3.1:8b"
  # Example: Seulgivibe static catalog. Endpoint and raw token values belong to
  # environment-specific private config/request headers, not tracked files.
  #   - id: "claude-sonnet-4-5"
  #     providers:
  #       seulgivibe-claude: "claude-sonnet-4-5"
  #   - id: "claude-opus-4-8"
  #     providers:
  #       seulgivibe-claude: "claude-opus-4-8"
  #   - id: "claude-fable-5"
  #     providers:
  #       seulgivibe-claude: "claude-fable-5"
  #   - id: "gpt-5.1"
  #     providers:
  #       seulgivibe-openai: "gpt-5.1"
  #   - id: "gpt-5.5"
  #     providers:
  #       seulgivibe-openai: "gpt-5.5"

nodes:
  # id is the stable node identity; omitting it falls back to an auto UUID (dev only).
  # Provider-First example (recommended):
  - id: "node-example-01"
    alias: "example-node"
    token: "<node-token>"
    providers:
      # Mac MLX vLLM provider (local GPU inference).
      # Tracked secrets (API key, etc.) are not stored in this file.
      - id: "mac-mlx-vllm"
        type: "openai_api"
        category: "local_inference"
        endpoint: "http://127.0.0.1:8002/v1"
        models:
          - "mlx-community/Qwen3.6-35B-A3B-4bit"
        health: "healthy"
        capacity: 2
        priority: 0
        # Long-context admission budget: total_context_tokens >= 262144 * 1.
        # mac-mlx-vllm runtime KV budget is 262144 (max_kv_size / max_request_tokens),
        # so only one full 262144-window long request fits at a time. long_context_capacity
        # is the long-slot count, not the normal capacity (2); do not conflate them.
        total_context_tokens: 262144
        long_context_capacity: 1
      # Ollama provider kept in the provider pool with explicit low concurrency.
      # It is not removed from mixed groups; operators weight it with capacity
      # and priority instead.
      - id: "ollama-local"
        type: "ollama"
        category: "local_inference"
        base_url: "http://127.0.0.1:11434"
        models:
          - "qwen3.6:35b"
          - "llama3.1:8b"
        health: "healthy"
        capacity: 1
        priority: 50
        # response_stall_timeout_ms: 300000  # omitted → uses documented default
      # Seulgivibe OpenAI-compatible provider examples. Keep endpoint values
      # illustrative and provide user tokens per request via openai.provider_auth.
      # - id: "seulgivibe-claude"
      #   type: "seulgivibe_claude"
      #   category: "api"
      #   endpoint: "https://seulgivibe.example.invalid/anthropic/v1"
      #   models:
      #     - "claude-sonnet-4-5"
      #     - "claude-opus-4-8"
      #     - "claude-fable-5"
      #   capacity: 4
      # - id: "seulgivibe-openai"
      #   type: "seulgivibe_openai"
      #   category: "api"
      #   endpoint: "https://seulgivibe.example.invalid/openai/v1"
      #   models:
      #     - "gpt-5.1"
      #     - "gpt-5.5"
      #   capacity: 4
    runtime:
      concurrency: 1

# === Provider-pool example (recommended) ===
# Full example node with provider pool (Provider-First).
# nodes[].providers[].id is the stable provider identity referenced by models[].providers keys.
# nodes:
#   - id: "node-gpu-01"
#     alias: "gpu-node"
#     providers:
#       - id: "vllm-gpu"
#         type: "vllm"
#         category: "api"
#         endpoint: "http://127.0.0.1:8000/v1"
#         models:
#           - "nvidia/Qwen3.6-35B-A3B-NVFP4"
#         health: "healthy"
#         capacity: 4
#         priority: 1
#         request_timeout_ms: 120000
#         lifecycle_capabilities: ["scale_up", "scale_down"]
#         # enabled: false  # exclude from dispatch pool (live-apply)
#       - id: "ollama-local"
#         type: "ollama"
#         category: "local_inference"
#         base_url: "http://127.0.0.1:11434"
#         models:
#           - "qwen3.6:35b"
#         health: "healthy"
#         capacity: 2
#         priority: 2
#
# === Legacy adapters (compat override example — discouraged for new deploys) ===
# Only use when explicit adapter instance override or compatibility before compilation is required.
#   - id: "node-vllm-01"
#     alias: "vllm-gpu-node"
#     token: "<node-token>"
#     adapters:
#       openai_compat_instances:
#         - name: "vllm-gpu"
#           enabled: true
#           provider: "vllm"
#           endpoint: "http://127.0.0.1:8000/v1"
#           capacity: 4
#           max_queue: 16
#           queue_timeout_ms: 30000
#     providers:
#       - id: "vllm-gpu-legacy"
#         type: "vllm"
#         category: "api"
#         adapter: "vllm-gpu"
#         models:
#           - "nvidia/Qwen3.6-35B-A3B-NVFP4"
#
# === Operator-owned workspace example (commented) ===
# workspaces[] is the operator-owned bounded capability catalog for this
# node. Each entry is keyed by a globally unique ref, declares allowed
# operations (read, list, write, delete, command), command templates, the
# environment variable allowlist, and byte/time limits. Each enabled read,
# write, list, and command operation requires its effective positive bound:
# max_read_bytes, max_write_bytes, max_output_bytes, and (for command)
# max_command_timeout_ms. Platform is the closed "darwin" or "linux" set;
# every non-empty catalog entry must match the selected Node host exactly
# before its root is opened. Windows and unknown hosts fail closed. Roots are
# absolute clean paths other than "/".
# Refs must be globally unique across all nodes. An empty workspaces slice
# is backward-compatible.
#
# workspace_ref in execution_presets[].single_request references one of
# these entries by ref. Raw roots and command templates never enter execution
# presets, caller-visible responses, provider requests, or public metadata.
# Edge delivers the catalog through the dedicated Node-private config payload.
# Catalog changes are restart-required and never alter an active request.
#
#   workspaces:
#     - ref: "ws-operator-project-root"
#       platform: "darwin"
#       root: "/Users/operator/projects/iop-workspace"
#       operations:
#         - "read"
#         - "list"
#         - "write"
#         - "delete"
#         - "command"
#       commands:
#         - id: "find-go-files"
#           executable: "/usr/bin/find"
#           args:
#             - "/Users/operator/projects/iop-workspace"
#             - "-name"
#             - "*.go"
#         - id: "read-file"
#           executable: "/usr/bin/cat"
#           args: []
#       environment_allowlist:
#         - "IOP_ENV"
#         - "HOME"
#       max_read_bytes: 1048576
#       max_write_bytes: 524288
#       max_output_bytes: 8388608
#       max_command_timeout_ms: 30000
#     - ref: "ws-operator-linux-root"
#       platform: "linux"
#       root: "/srv/iop/workspace"
#       operations:
#         - "read"
#         - "list"
#         - "write"
#       max_read_bytes: 1048576
#       max_write_bytes: 524288
#       max_output_bytes: 8388608
#
# === Fixed single-request preset example (commented) ===
# execution_presets[] entry with operator-owned fixed single-request policy.
# Live-apply on refresh; affects only new request snapshots.
# execution_presets:
#   - id: "preset-fixed-light"
#     selector:
#       model: "qwen3.6:35b"
#       options:
#         reasoning_effort: "high"
#     allowed_modes:
#       - "light"
#     routes:
#       light:
#         stages:
#           - role: "plan"
#             model: "qwen3.6:35b"
#             options:
#               reasoning_effort: "high"
#           - role: "work"
#             model: "qwen3.6:35b"
#           - role: "review"
#             model: "qwen3.6:35b"
#             options:
#               reasoning_effort: "high"
#     single_request:
#       workspace_ref: "<opaque-workspace-ref>"  # never a raw path or credential
#       limits:
#         wall_clock_ms: 1800000       # 30 minutes (max)
#         timeout_ms: 600000           # 10 minutes per stage (max)
#         max_tool_iterations: 64      # per stage
#         max_output_bytes: 16777216   # 16 MiB per stage
#       stages:
#         plan:
#           model: "qwen3.6:35b"
#           options:
#             reasoning_effort: "high"
#         work:
#           model: "qwen3.6:35b"
#         review:
#           model: "qwen3.6:35b"
#           options:
#             reasoning_effort: "high"
