{
  "$defs": {
    "ActivityPanelBlock": {
      "additionalProperties": false,
      "description": "``ui.activity:`` block \u2014 opt-in observability pane for sub-agents.\n\nSurfaces the live sub-agent fan-out, background tasks, and recent\nterminal events as a dedicated workspace mode (any client). Pure display: the daemon never inspects this; both\nclients consume the field through the manifest summary and gate\nthe Activity mode entry on its presence.\n\n**Opt-in contract**: when this block is omitted (default), the\nActivity mode is HIDDEN from the workspace mode menu and the\npanel is not rendered. A simple chat app that never spawns\nsub-agents shouldn't see it. Apps that orchestrate fan-out\n(coordinator, dev assistant, multi-agent research) opt in.\n\nExample YAML::\n\n    ui:\n      activity:\n        enabled: true\n        position: right\n        title: \"Activity\"\n        show_running: true\n        show_recent: true\n        show_stats: true\n        show_bg_tasks: true\n        max_recent: 50\n        auto_open_on_spawn: false",
      "properties": {
        "auto_open_on_spawn": {
          "default": false,
          "description": "When true, the client auto-switches to the Activity pane the first time the agent spawns a sub-agent. Off by default \u2014 surface only when the user opens it explicitly.",
          "title": "Auto Open On Spawn",
          "type": "boolean"
        },
        "enabled": {
          "default": true,
          "description": "Master switch. When this block is present in YAML the client renders the Activity mode entry. Set ``enabled: false`` to disable the pane while keeping the rest of the config (useful for staged rollouts).",
          "title": "Enabled",
          "type": "boolean"
        },
        "max_recent": {
          "default": 50,
          "description": "Cap on the number of terminal events kept in the recent list. Older events are evicted FIFO. Tune up for long-running orchestration sessions; the panel's performance degrades past ~200.",
          "maximum": 500,
          "minimum": 5,
          "title": "Max Recent",
          "type": "integer"
        },
        "position": {
          "default": "right",
          "description": "Where the activity pane sits relative to the chat: right | bottom | overlay. Mirror of ``WorkspaceBlock.position`` semantics.",
          "title": "Position",
          "type": "string"
        },
        "show_bg_tasks": {
          "default": true,
          "description": "Interleave background shell tasks alongside sub-agents in the live + recent strips. Disable for apps that only spawn sub-agents (no shell.bash run_in_background).",
          "title": "Show Bg Tasks",
          "type": "boolean"
        },
        "show_recent": {
          "default": true,
          "description": "Render the recent-terminal-events scrollable list. Carries the last ``max_recent`` agents that completed / failed / cancelled, with one-line preview.",
          "title": "Show Recent",
          "type": "boolean"
        },
        "show_running": {
          "default": true,
          "description": "Render the live sub-agent strip at the top of the pane. Set false to suppress and keep only the recent + stats sections (useful for archival / read-only views).",
          "title": "Show Running",
          "type": "boolean"
        },
        "show_stats": {
          "default": true,
          "description": "Render the aggregate stats footer (total spawned / completed / failed, average duration, success rate). Pulls from ``/api/metrics`` digitorn_agent_* counters.",
          "title": "Show Stats",
          "type": "boolean"
        },
        "title": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Panel header label. Defaults to a localised ``Activity`` string when omitted.",
          "title": "Title"
        }
      },
      "title": "ActivityPanelBlock",
      "type": "object"
    },
    "AgentBrain": {
      "additionalProperties": false,
      "description": "LLM brain configuration for an agent.\n\nTwo modes:\n\n1. **Inline** - full provider config embedded in the agent::\n\n    brain:\n      provider: deepseek\n      model: deepseek-chat\n      temperature: 0.2\n      config:\n        api_key: \"{{secret.DEEPSEEK_API_KEY}}\"\n        base_url: \"https://api.deepseek.com/v1\"\n\n2. **Reference** - points to a named provider in ``modules.llm_provider``::\n\n    brain:\n      provider_id: deepseek_main\n      temperature: 0.2",
      "properties": {
        "backend": {
          "default": "openai_compat",
          "description": "Provider backend: 'anthropic', 'openai_compat', or 'github_copilot' (uses your Copilot subscription via api.githubcopilot.com).",
          "enum": [
            "openai_compat",
            "anthropic",
            "github_copilot"
          ],
          "title": "Backend",
          "type": "string"
        },
        "config": {
          "additionalProperties": true,
          "description": "Provider-specific config (api_key, base_url, etc.).",
          "title": "Config",
          "type": "object"
        },
        "context": {
          "anyOf": [
            {
              "$ref": "#/$defs/ContextConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Context window management for this brain. If not set, inherits from execution.context. Useful in multi-agent apps where each brain uses a different model."
        },
        "credential": {
          "default": null,
          "description": "Reference to a user-vault credential. Two YAML shapes:\n  - string (compact): `credential: openai_main`\n  - mapping (explicit): `credential: { ref: openai_main, scope: per_user }`\nThe runtime resolves the reference at activation time and injects the credential's fields into `config` (api_key, base_url, etc.) replacing any inline values. Recommended over inline `{{secret.X}}` templates.",
          "title": "Credential"
        },
        "fallback": {
          "anyOf": [
            {
              "$ref": "#/$defs/AgentBrain"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Fallback brain used when the primary provider returns a billing or credit error (402, insufficient balance). Lets apps gracefully degrade to a cheaper/free model instead of failing. Example:\n  fallback:\n    provider: anthropic\n    model: claude-haiku-4-5\n    config:\n      api_key: \"claude-code\""
        },
        "image_detail": {
          "default": "auto",
          "description": "Image resolution for vision. 'auto' = provider decides. 'low' = 512px (cheaper, faster). 'high' = native resolution (more accurate, more tokens).",
          "title": "Image Detail",
          "type": "string"
        },
        "image_generation": {
          "default": false,
          "description": "Whether this model can generate images. If true, the framework handles image output in tool results and SSE events. Models like DALL-E, Stable Diffusion via MCP.",
          "title": "Image Generation",
          "type": "boolean"
        },
        "max_images_per_turn": {
          "default": 5,
          "description": "Max images sent to the model per turn (0 = unlimited).",
          "maximum": 100,
          "minimum": 0,
          "title": "Max Images Per Turn",
          "type": "integer"
        },
        "max_tokens": {
          "anyOf": [
            {
              "type": "integer"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Max tokens to generate.",
          "title": "Max Tokens"
        },
        "model": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Model identifier (e.g. 'deepseek-chat', 'claude-sonnet-4-5').",
          "title": "Model"
        },
        "native_tool_use": {
          "anyOf": [
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Override native tool calling detection. By default, auto-detected from provider (e.g. Ollama defaults to text-based). Set to true to force native OpenAI-style tool_calls (e.g. qwen2.5-coder on Ollama). Set to false to force text-based tool calling.",
          "title": "Native Tool Use"
        },
        "provider": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Provider hint for auto-resolving base URL. Known values: anthropic, openai, deepseek, groq, mistral, together, ollama, lm_studio, vllm, google-gemini, gemini, xai, grok, cerebras, perplexity, fireworks, github_copilot.",
          "title": "Provider"
        },
        "provider_id": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Reference to a named provider declared in modules.llm_provider.config.providers. If set, provider/model/config are ignored.",
          "title": "Provider Id"
        },
        "temperature": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Sampling temperature.",
          "title": "Temperature"
        },
        "timeout": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Request timeout in seconds.",
          "title": "Timeout"
        },
        "top_p": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Nucleus sampling threshold.",
          "title": "Top P"
        },
        "vision": {
          "anyOf": [
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Whether this model supports image input (vision). null = auto-detect from model name. true = force enabled. false = convert images to text descriptions.",
          "title": "Vision"
        }
      },
      "title": "AgentBrain",
      "type": "object"
    },
    "AgentDefinition": {
      "additionalProperties": false,
      "description": "Definition of a single agent in the app YAML.\n\nOnly ``id`` and ``brain`` are required for now.\nOther fields (tools, signals, loop, watch) will be added\nwhen we implement the full agent runtime.\n\nExample::\n\n    agents:\n      - id: coordinator\n        role: coordinator\n        brain:\n          provider: deepseek\n          model: deepseek-chat\n          temperature: 0.2\n          config:\n            api_key: \"{{secret.DEEPSEEK_API_KEY}}\"\n            base_url: \"https://api.deepseek.com/v1\"\n        system_prompt: |\n          You are a coordinator agent.",
      "properties": {
        "brain": {
          "$ref": "#/$defs/AgentBrain",
          "description": "LLM provider configuration for this agent."
        },
        "capabilities": {
          "description": "List of skill names to auto-load from the bundle's ``skills/`` directory. The compiler reads ``skills/<name>.md`` for each entry and appends the content to this agent's ``system_prompt`` under an ``## Available capabilities`` section. Clean way to separate the agent's identity (system_prompt) from its skill definitions (individual markdown files).",
          "items": {
            "type": "string"
          },
          "title": "Capabilities",
          "type": "array"
        },
        "coordination": {
          "anyOf": [
            {
              "$ref": "#/$defs/CoordinationBlock"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Grouped orchestration block: delegate_to + pool. Replaces the historical scatter."
        },
        "delegate_to": {
          "description": "Agent IDs this coordinator can delegate to. The compiler verifies each entry references a declared agent id.",
          "items": {
            "type": "string"
          },
          "title": "Delegate To",
          "type": "array"
        },
        "hooks": {
          "description": "Per-agent hooks - merged with ``execution.hooks`` but only evaluated when this specific agent is active. Use for specialist-specific behavior (e.g. a `reviewer` agent that runs extra lint, a `writer` agent that logs every edit). App-wide hooks still fire for every agent; these add on top.",
          "items": {
            "$ref": "#/$defs/HookConfig"
          },
          "title": "Hooks",
          "type": "array"
        },
        "id": {
          "description": "Unique agent identifier within this app.",
          "title": "Id",
          "type": "string"
        },
        "instructions": {
          "anyOf": [
            {
              "$ref": "#/$defs/InstructionsBlock"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Grouped prompt-extension block: file + capabilities + specialty. Replaces the historical scatter."
        },
        "modules": {
          "description": "Modules this specialist can access. Empty = same as coordinator.\nSupports two formats (mix is OK):\n  - Simple: ['filesystem', 'shell'] - full module access\n  - Granular: [{filesystem: [read, grep]}, 'shell'] - restrict actions",
          "items": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "additionalProperties": {
                  "items": {
                    "type": "string"
                  },
                  "type": "array"
                },
                "type": "object"
              }
            ]
          },
          "title": "Modules",
          "type": "array"
        },
        "plan_first": {
          "default": true,
          "description": "When true, the agent must explain its plan in plain text before executing any tools on the first turn. Prevents silent tool calls.",
          "title": "Plan First",
          "type": "boolean"
        },
        "pool": {
          "$ref": "#/$defs/AgentPoolConfig",
          "description": "Agent pool config for coordinators (max_workers, progress, auto_retry)."
        },
        "role": {
          "default": "worker",
          "description": "Agent role hint. Functional roles: 'coordinator' (can spawn agents), 'specialist' (pre-configured expert), 'worker' (default). Descriptive roles like 'assistant', 'analyst', 'reviewer' are also accepted and used in the system prompt.",
          "title": "Role",
          "type": "string"
        },
        "skills": {
          "default": "",
          "description": "Path to a .md file with detailed methodology/instructions for this specialist.",
          "title": "Skills",
          "type": "string"
        },
        "specialty": {
          "default": "",
          "description": "Short description of this specialist's expertise (shown to coordinator).",
          "title": "Specialty",
          "type": "string"
        },
        "system_prompt": {
          "default": "",
          "description": "System prompt injected at conversation start.",
          "title": "System Prompt",
          "type": "string"
        }
      },
      "required": [
        "id",
        "brain"
      ],
      "title": "AgentDefinition",
      "type": "object"
    },
    "AgentNode": {
      "additionalProperties": false,
      "description": "Run an existing declared agent for one turn.\n\nThe agent is identified by the ``agent`` field which must reference\na declared ``agents[].id`` (validated in\n:func:`validate_flow_references`).",
      "properties": {
        "agent": {
          "description": "agents[].id to execute.",
          "title": "Agent",
          "type": "string"
        },
        "description": {
          "default": "",
          "description": "Free-form description, surfaced on the canvas tooltip.",
          "title": "Description",
          "type": "string"
        },
        "id": {
          "description": "Unique node identifier within the flow.",
          "title": "Id",
          "type": "string"
        },
        "input": {
          "additionalProperties": true,
          "description": "Static or templated input passed to the agent.",
          "title": "Input",
          "type": "object"
        },
        "on_error": {
          "description": "Error-handling edges. Catch-all (default: true) must be last.",
          "items": {
            "$ref": "#/$defs/FlowOnErrorRoute"
          },
          "title": "On Error",
          "type": "array"
        },
        "routes": {
          "description": "Outgoing edges. Top-to-bottom evaluation order.",
          "items": {
            "$ref": "#/$defs/FlowRoute"
          },
          "title": "Routes",
          "type": "array"
        },
        "type": {
          "const": "agent",
          "title": "Type",
          "type": "string"
        }
      },
      "required": [
        "id",
        "type",
        "agent"
      ],
      "title": "AgentNode",
      "type": "object"
    },
    "AgentPoolConfig": {
      "additionalProperties": false,
      "description": "Pool configuration for coordinator agents that spawn specialists.\n\nReplaces the old ``dict[str, Any]`` form which used to be validated\nonly by hand in the compiler. The compiler-side check is now a\nno-op - Pydantic enforces the shape and bounds.",
      "properties": {
        "auto_retry": {
          "default": 0,
          "description": "Number of automatic retries when a specialist fails.",
          "maximum": 5,
          "minimum": 0,
          "title": "Auto Retry",
          "type": "integer"
        },
        "max_workers": {
          "default": 3,
          "description": "Maximum concurrent specialist agents this coordinator can spawn.",
          "maximum": 100,
          "minimum": 1,
          "title": "Max Workers",
          "type": "integer"
        },
        "progress": {
          "default": false,
          "description": "Whether to relay progress events from spawned agents to the coordinator.",
          "title": "Progress",
          "type": "boolean"
        }
      },
      "title": "AgentPoolConfig",
      "type": "object"
    },
    "AppMeta": {
      "additionalProperties": false,
      "description": "Top-level application identity.",
      "properties": {
        "app_id": {
          "description": "Unique application identifier.",
          "title": "App Id",
          "type": "string"
        },
        "attachments": {
          "anyOf": [
            {
              "items": {
                "enum": [
                  "image",
                  "document",
                  "audio",
                  "video"
                ],
                "type": "string"
              },
              "type": "array"
            },
            {
              "const": "*",
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Attachment types the chat composer accepts.\n  - ``image``    PNG / JPG / GIF / WEBP / HEIC, routed to a vision-capable LLM\n  - ``document`` PDF / TXT / MD / CSV / XLSX / DOCX / code files, extracted as text before being handed to the model\n  - ``audio``    MP3 / WAV / M4A / OGG, transcribed via STT\n  - ``video``    MP4 / MOV / WEBM, accepted by a few vision models (Gemini, Sonnet)\n\nPass the string ``*`` to enable every supported type. Leaving the field empty (``null`` / unset) disables attachments entirely: the composer's ``+`` menu collapses to slash-commands + snippets only. Apps must opt in - the default is no attachments.",
          "title": "Attachments"
        },
        "author": {
          "default": "",
          "description": "Application author.",
          "title": "Author",
          "type": "string"
        },
        "category": {
          "default": "general",
          "description": "App category for grouping in the UI. Examples: 'coding', 'writing', 'research', 'data', 'devops', 'design', 'communication', 'automation', 'general'.",
          "title": "Category",
          "type": "string"
        },
        "color": {
          "default": "",
          "description": "Accent color for the app card/header. Hex format: '#8B5CF6'. If empty, auto-generated from app_id hash.",
          "title": "Color",
          "type": "string"
        },
        "description": {
          "default": "",
          "description": "Optional description.",
          "title": "Description",
          "type": "string"
        },
        "features": {
          "additionalProperties": {
            "type": "boolean"
          },
          "description": "Client-UI feature toggles (same contract as top-level features:). Keys: voice, attachments, tools_panel, snippets, tasks_panel, memory_panel, context_ring, markdown, slash_commands, message_actions, status_pills, token_badges. Unspecified keys default to true on the client.",
          "title": "Features",
          "type": "object"
        },
        "icon": {
          "default": "",
          "description": "App icon. Can be: emoji ('\ud83d\udcbb'), icon name ('code'), URL to an image ('https://...'), or base64 data URI. If empty, the client generates a colored circle from app_id.",
          "title": "Icon",
          "type": "string"
        },
        "name": {
          "description": "Human-readable application name.",
          "title": "Name",
          "type": "string"
        },
        "quick_prompts": {
          "description": "Suggested prompts shown as clickable buttons when the user opens the app. Each entry: {label: 'short text', message: 'full prompt', icon: 'emoji'}. If empty, the client shows just the input field.",
          "items": {
            "$ref": "#/$defs/QuickPrompt"
          },
          "title": "Quick Prompts",
          "type": "array"
        },
        "schema_version": {
          "default": "1",
          "description": "YAML schema version for forward compatibility.",
          "title": "Schema Version",
          "type": "string"
        },
        "short_name": {
          "default": "",
          "description": "Compact label used in tight UI slots (dashboard chip, tab header, mobile drawer). One word, or two SHORT words; longer names overflow the 68 px chip and overlap their neighbors. When empty, clients fall back to `name` (truncated with ellipsis).",
          "title": "Short Name",
          "type": "string"
        },
        "tags": {
          "description": "Searchable tags.",
          "items": {
            "type": "string"
          },
          "title": "Tags",
          "type": "array"
        },
        "theme": {
          "additionalProperties": {
            "type": "string"
          },
          "description": "Client theme overrides. Keys: accent (hex), background (hex). accent overrides app.color for fine-grained control.",
          "title": "Theme",
          "type": "object"
        },
        "version": {
          "default": "1.0",
          "description": "Application version string.",
          "title": "Version",
          "type": "string"
        }
      },
      "required": [
        "app_id",
        "name"
      ],
      "title": "AppMeta",
      "type": "object"
    },
    "ApprovalNode": {
      "additionalProperties": false,
      "description": "Human-in-the-loop gate. Pauses until a human chooses an option.\n\nThe decision becomes part of the flow context as\n``approvals.<node_id>`` so downstream routes can branch on it via\n``when: \"approvals.<id> == 'approve'\"``.",
      "properties": {
        "choices": {
          "description": "Selectable answers. The user picks one.",
          "items": {
            "type": "string"
          },
          "minItems": 2,
          "title": "Choices",
          "type": "array"
        },
        "description": {
          "default": "",
          "description": "Free-form description, surfaced on the canvas tooltip.",
          "title": "Description",
          "type": "string"
        },
        "id": {
          "description": "Unique node identifier within the flow.",
          "title": "Id",
          "type": "string"
        },
        "message": {
          "description": "Question shown to the user.",
          "minLength": 1,
          "title": "Message",
          "type": "string"
        },
        "on_error": {
          "description": "Error-handling edges. Catch-all (default: true) must be last.",
          "items": {
            "$ref": "#/$defs/FlowOnErrorRoute"
          },
          "title": "On Error",
          "type": "array"
        },
        "routes": {
          "description": "Outgoing edges. Top-to-bottom evaluation order.",
          "items": {
            "$ref": "#/$defs/FlowRoute"
          },
          "title": "Routes",
          "type": "array"
        },
        "type": {
          "const": "approval",
          "title": "Type",
          "type": "string"
        }
      },
      "required": [
        "id",
        "type",
        "message"
      ],
      "title": "ApprovalNode",
      "type": "object"
    },
    "BehaviorConfig": {
      "additionalProperties": false,
      "description": "Behavioral enforcement rules - actively monitored at runtime.\n\nDefine a profile preset and/or individual rules. All enabled rules\nare enforced by the behavior engine on every tool call.\n\nExample::\n\n    behavior:\n      profile: coding\n      classify_turns: true\n      classifier:\n        frequency: every_turn\n        timeout: 15\n        approaches: [direct, plan_and_confirm, delegate]\n      rules:\n        read_before_edit: true\n        test_after_changes: true\n      custom:\n        - id: protect_migrations\n          rule: \"Never modify migration files without asking\"\n          trigger: edit\n          action: block",
      "properties": {
        "brain": {
          "anyOf": [
            {
              "$ref": "#/$defs/AgentBrain"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "LLM for semantic classification. Uses the same AgentBrain format as agents.\nIf omitted, uses the coordinator's brain.\nTip: use a fast/cheap model (haiku, deepseek-chat) for minimal latency."
        },
        "classifier": {
          "$ref": "#/$defs/ClassifierConfig",
          "description": "Configuration for the semantic classifier LLM."
        },
        "classify_turns": {
          "default": false,
          "description": "Enable semantic classification. A small LLM analyzes each user message BEFORE the main agent acts and injects behavioral directives.",
          "title": "Classify Turns",
          "type": "boolean"
        },
        "custom": {
          "description": "Legacy custom rules (backward compat). Prefer rule_definitions.",
          "items": {
            "$ref": "#/$defs/BehaviorCustomRule"
          },
          "title": "Custom",
          "type": "array"
        },
        "profile": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Preset profile: 'dev', 'coding', 'research', 'data', 'creative', 'assistant', or '{{behavior.X}}'.",
          "title": "Profile"
        },
        "rule_definitions": {
          "description": "Fully declarative rules - works for ANY action. See BehaviorRuleDefinition.",
          "items": {
            "$ref": "#/$defs/BehaviorRuleDefinition"
          },
          "title": "Rule Definitions",
          "type": "array"
        },
        "rules": {
          "additionalProperties": true,
          "description": "Rule overrides. Keys are rule IDs (read_before_edit, test_after_changes, etc.), values are bool or int.",
          "title": "Rules",
          "type": "object"
        },
        "state_tracking": {
          "anyOf": [
            {
              "$ref": "#/$defs/StateTrackingConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "What the session state tracks. When null, uses defaults from profile."
        },
        "use_agent_brain": {
          "default": true,
          "description": "When brain is not set, reuse the coordinator's brain for classification. Set to false to disable classification when no brain is configured.",
          "title": "Use Agent Brain",
          "type": "boolean"
        }
      },
      "title": "BehaviorConfig",
      "type": "object"
    },
    "BehaviorCustomRule": {
      "additionalProperties": false,
      "description": "Legacy custom rule format. Kept for backward compatibility.\nPrefer ``rule_definitions`` for new apps.",
      "properties": {
        "action": {
          "default": "warn",
          "title": "Action",
          "type": "string"
        },
        "condition": {
          "additionalProperties": true,
          "title": "Condition",
          "type": "object"
        },
        "enforce": {
          "default": "pre_tool",
          "title": "Enforce",
          "type": "string"
        },
        "id": {
          "default": "custom",
          "title": "Id",
          "type": "string"
        },
        "message": {
          "default": "",
          "title": "Message",
          "type": "string"
        },
        "rule": {
          "title": "Rule",
          "type": "string"
        },
        "trigger": {
          "default": "",
          "title": "Trigger",
          "type": "string"
        }
      },
      "required": [
        "rule"
      ],
      "title": "BehaviorCustomRule",
      "type": "object"
    },
    "BehaviorRuleDefinition": {
      "additionalProperties": false,
      "description": "A fully declarative behavioral rule - works for ANY action.\n\nExample::\n\n    rule_definitions:\n      - id: read_before_edit\n        description: \"Must read a file before editing it\"\n        trigger: [edit]\n        when: pre_tool\n        action: warn\n        condition:\n          target_not_in_set: read_files\n        message: \"You are editing '{target}' without reading it first.\"\n\n      - id: no_sql_injection\n        description: \"Block raw SQL in user-facing queries\"\n        trigger: [database.execute]\n        when: pre_tool\n        action: block\n        condition:\n          param_matches:\n            param: query\n            pattern: \".*;\\s*(DROP|DELETE|TRUNCATE)\"\n        message: \"Dangerous SQL detected. Use parameterized queries.\"",
      "properties": {
        "action": {
          "default": "warn",
          "description": "What to do: 'block' (prevent), 'warn' (inject message), 'remind' (post-tool hint).",
          "title": "Action",
          "type": "string"
        },
        "condition": {
          "additionalProperties": true,
          "description": "When the rule fires. Condition types:\n  target_not_in_set: <set_name>    - target param NOT in tracked set\n  target_in_set: <set_name>         - target param IS in tracked set\n  counter_gte: {name, value}         - counter >= threshold\n  param_matches: {param, pattern}    - param matches regex\n  param_contains: {param, value}     - param contains string\n  flag_is: {name, value}             - flag equals value\n  no_text_before_tools: true         - agent didn't explain before tools\n  consecutive_gte: <N>               - same tool called N+ times\n  all: [conditions...]               - all must match\n  any: [conditions...]               - at least one matches\n  not: <condition>                   - negation",
          "title": "Condition",
          "type": "object"
        },
        "description": {
          "default": "",
          "description": "Human-readable description (shown in prompt).",
          "title": "Description",
          "type": "string"
        },
        "id": {
          "description": "Unique rule identifier.",
          "title": "Id",
          "type": "string"
        },
        "message": {
          "default": "",
          "description": "Message template. Placeholders:\n  {target}              - file_path or primary target param\n  {tool}                - current tool name\n  {param:<name>}        - any param value\n  {counter:<name>}      - counter value\n  {set_count:<name>}    - size of a tracked set\n  {turn}                - current turn number",
          "title": "Message",
          "type": "string"
        },
        "trigger": {
          "anyOf": [
            {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            {
              "type": "string"
            }
          ],
          "default": "*",
          "description": "Tool name(s) that trigger this rule. '*' = all tools.",
          "title": "Trigger"
        },
        "when": {
          "default": "pre_tool",
          "description": "When to check: 'pre_tool', 'post_tool', 'on_text' (agent text output).",
          "title": "When",
          "type": "string"
        }
      },
      "required": [
        "id"
      ],
      "title": "BehaviorRuleDefinition",
      "type": "object"
    },
    "CapabilitiesConfig": {
      "additionalProperties": false,
      "description": "Application-level security capabilities.",
      "properties": {
        "approval_timeout": {
          "default": 300,
          "description": "Seconds to wait for user approval before auto-denying (30\u20133600).",
          "maximum": 3600,
          "minimum": 30,
          "title": "Approval Timeout",
          "type": "integer"
        },
        "approve": {
          "description": "Actions requiring explicit user approval before execution.",
          "items": {
            "$ref": "#/$defs/CapabilityGrant"
          },
          "title": "Approve",
          "type": "array"
        },
        "default_policy": {
          "default": "approve",
          "description": "Default action policy: 'auto', 'approve', or 'block'.",
          "enum": [
            "auto",
            "approve",
            "block"
          ],
          "title": "Default Policy",
          "type": "string"
        },
        "deny": {
          "description": "Explicit action denies per module.",
          "items": {
            "$ref": "#/$defs/CapabilityGrant"
          },
          "title": "Deny",
          "type": "array"
        },
        "grant": {
          "description": "Explicit action grants per module.",
          "items": {
            "$ref": "#/$defs/CapabilityGrant"
          },
          "title": "Grant",
          "type": "array"
        },
        "hidden_actions": {
          "description": "Specific actions to hide from the agent's tool index. Unlike 'deny', hidden actions are invisible but still executable by setup steps, hooks, and channels. Use this to declutter the agent's toolset without breaking internal automation.",
          "items": {
            "$ref": "#/$defs/CapabilityGrant"
          },
          "title": "Hidden Actions",
          "type": "array"
        },
        "hidden_modules": {
          "description": "Module IDs to hide from the agent's tool index. Hidden modules are still loaded and can be used by setup steps, hooks, and channels - but the agent cannot see or call their tools. Example: ['filesystem'] to prevent the agent from accessing files.",
          "items": {
            "type": "string"
          },
          "title": "Hidden Modules",
          "type": "array"
        },
        "max_risk_level": {
          "default": "medium",
          "description": "Maximum allowed risk level: 'low', 'medium', or 'high'.",
          "enum": [
            "low",
            "medium",
            "high"
          ],
          "title": "Max Risk Level",
          "type": "string"
        }
      },
      "title": "CapabilitiesConfig",
      "type": "object"
    },
    "CapabilityGrant": {
      "additionalProperties": false,
      "description": "An explicit grant or deny for module actions.",
      "properties": {
        "actions": {
          "description": "Action names. Empty = all actions on the module.",
          "items": {
            "type": "string"
          },
          "title": "Actions",
          "type": "array"
        },
        "module": {
          "description": "Target module ID.",
          "title": "Module",
          "type": "string"
        },
        "reason": {
          "default": "",
          "description": "Human-readable reason (for deny).",
          "title": "Reason",
          "type": "string"
        }
      },
      "required": [
        "module"
      ],
      "title": "CapabilityGrant",
      "type": "object"
    },
    "ChannelInstanceConfig": {
      "additionalProperties": false,
      "description": "Configuration for a named output channel instance.\n\nEach entry in the ``channels:`` block defines a channel instance\nwith a user-chosen name, a channel type, and type-specific config.\n\nOptionally, a ``user_resolver`` auto-resolves per-user delivery targets\n(email, phone, chat_id) from a data source - no manual ``output_config``\nneeded.\n\nExample::\n\n    channels:\n      slack_alerts:\n        type: webhook\n        config:\n          url: \"{{secret.SLACK_WEBHOOK}}\"\n\n      sms_user:\n        type: sms\n        config:\n          account_sid: \"{{env.TWILIO_SID}}\"\n          from_number: \"+33600000000\"\n        user_resolver:\n          module: database\n          action: fetch_results\n          params:\n            query: \"SELECT phone FROM users WHERE session_id = :session_id\"\n          mapping:\n            to_number: phone",
      "properties": {
        "config": {
          "additionalProperties": true,
          "description": "Channel-specific configuration. Supports {{variables}} and {{secret.X}} / {{env.X}} for credentials. See 'digitorn channel schema <type>' for available fields.",
          "title": "Config",
          "type": "object"
        },
        "type": {
          "description": "Channel type ID. Built-in: 'llm_notification', 'webhook', 'log'. Plugins: 'slack', 'gmail', 'telegram', 'kafka', 'sms', etc. (via pip install digitorn-channel-<type>)",
          "title": "Type",
          "type": "string"
        },
        "user_resolver": {
          "anyOf": [
            {
              "$ref": "#/$defs/UserResolverConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Optional user resolver for auto-targeting notifications. When set, the channel automatically looks up the user's delivery address (email, phone, chat_id) from a data source using the session_id. No manual output_config needed."
        }
      },
      "required": [
        "type"
      ],
      "title": "ChannelInstanceConfig",
      "type": "object"
    },
    "ChatComposerBlock": {
      "additionalProperties": false,
      "description": "Composer toolbar configuration (file upload, voice, ...).\n\nMirrors the existing ``ui.features`` flags for backward\ncompatibility - if both are present, ``composer.X`` wins.",
      "properties": {
        "file_upload": {
          "default": true,
          "description": "Paperclip / drag-drop file attachment.",
          "title": "File Upload",
          "type": "boolean"
        },
        "quick_prompts_visible": {
          "default": true,
          "description": "Show suggested ``ui.quick_prompts`` chips above the composer when the conversation is empty.",
          "title": "Quick Prompts Visible",
          "type": "boolean"
        },
        "slash_commands": {
          "default": true,
          "description": "Slash command palette via ``/``.",
          "title": "Slash Commands",
          "type": "boolean"
        },
        "voice": {
          "default": true,
          "description": "Microphone button. Default ``true`` to match the legacy ``features.voice`` default - apps that want voice off should set ``composer.voice: false`` explicitly.",
          "title": "Voice",
          "type": "boolean"
        }
      },
      "title": "ChatComposerBlock",
      "type": "object"
    },
    "ChatSideWidget": {
      "additionalProperties": false,
      "description": "Z2 - companion side panel rendered next to the chat.",
      "properties": {
        "accent": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Accent"
        },
        "collapsible": {
          "default": true,
          "title": "Collapsible",
          "type": "boolean"
        },
        "data": {
          "additionalProperties": true,
          "title": "Data",
          "type": "object"
        },
        "default_open": {
          "default": true,
          "title": "Default Open",
          "type": "boolean"
        },
        "density": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Density"
        },
        "icon": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Icon"
        },
        "title": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Title"
        },
        "tree": {
          "$ref": "#/$defs/WidgetNode"
        },
        "width": {
          "default": 300,
          "maximum": 420,
          "minimum": 260,
          "title": "Width",
          "type": "integer"
        }
      },
      "required": [
        "tree"
      ],
      "title": "ChatSideWidget",
      "type": "object"
    },
    "ChatThinkingBlock": {
      "additionalProperties": false,
      "description": "Per-app visibility / collapse defaults for ``<thinking>`` blocks.",
      "properties": {
        "collapsed_default": {
          "default": true,
          "description": "Initial collapsed state of thinking blocks; the user can still toggle them when ``visible`` is true.",
          "title": "Collapsed Default",
          "type": "boolean"
        },
        "visible": {
          "default": true,
          "description": "When false, thinking blocks are hidden entirely.",
          "title": "Visible",
          "type": "boolean"
        }
      },
      "title": "ChatThinkingBlock",
      "type": "object"
    },
    "ChatToolCallsBlock": {
      "additionalProperties": false,
      "description": "Per-app visibility / collapse defaults for tool-call chips.",
      "properties": {
        "collapsed_default": {
          "default": true,
          "description": "Initial collapsed state of tool-call previews.",
          "title": "Collapsed Default",
          "type": "boolean"
        },
        "show_silent": {
          "default": false,
          "description": "When true, plumbing tools (memory ops, agent_spawn internals, discovery meta-tools) are rendered. Default false hides them.",
          "title": "Show Silent",
          "type": "boolean"
        }
      },
      "title": "ChatToolCallsBlock",
      "type": "object"
    },
    "ChatVisualBlock": {
      "additionalProperties": false,
      "description": "Visual styling for chat bubbles / accent / alignment.",
      "properties": {
        "accent": {
          "default": "",
          "description": "Hex accent colour, e.g. ``#3b82f6``. Falls back to ``ui.theme.accent`` then ``app.color`` when empty.",
          "title": "Accent",
          "type": "string"
        },
        "bubble_style": {
          "default": "card",
          "description": "Bubble visual treatment: card | flat | minimal.",
          "title": "Bubble Style",
          "type": "string"
        },
        "user_bubble_alignment": {
          "default": "right",
          "description": "User message alignment: right (default) | left.",
          "title": "User Bubble Alignment",
          "type": "string"
        }
      },
      "title": "ChatVisualBlock",
      "type": "object"
    },
    "ClassifierConfig": {
      "additionalProperties": false,
      "description": "Configuration for the semantic classifier LLM.\n\nThe classifier is a generic pre-turn analysis engine. Each app\nconfigures what it analyzes, when it runs, and what it produces.\n\nExample::\n\n    behavior:\n      classify_turns: true\n      classifier:\n        frequency: every_turn\n        timeout: 15\n        complexity_levels: [trivial, simple, moderate, complex, critical]\n        approaches: [direct, explore_first, plan_and_confirm, delegate, research_first]\n        risk_levels: [none, low, medium, high]\n        max_directives: 5\n        system_prompt: \"{{prompt.classifier}}\"",
      "properties": {
        "approaches": {
          "description": "Approach strategies. Each entry is either a plain string\nor a dict with {name, label, when, behavior} for full customization:\n\n  approaches:\n    - name: direct\n      label: 'Execute directly'\n      when: 'Task is trivial or simple, clear path'\n      behavior: 'Go straight to tool calls, minimal text'\n    - name: ask_expert\n      label: 'Needs human expertise'\n      when: 'Domain knowledge required that the agent lacks'\n      behavior: 'Ask the user with AskUser, explain what you need to know'",
          "items": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "additionalProperties": {
                  "type": "string"
                },
                "type": "object"
              }
            ]
          },
          "title": "Approaches",
          "type": "array"
        },
        "complexity_levels": {
          "description": "Ordered list of complexity levels. Each entry is either a plain string\nor a dict with {name, when, behavior} for full customization:\n\n  complexity_levels:\n    - name: trivial\n      when: '1 action, obvious answer'\n      behavior: 'Just do it, no planning'\n    - name: deep\n      when: 'Cross-cutting concern, 10+ files'\n      behavior: 'Full plan, user approval, phased execution'",
          "items": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "additionalProperties": {
                  "type": "string"
                },
                "type": "object"
              }
            ]
          },
          "title": "Complexity Levels",
          "type": "array"
        },
        "context": {
          "$ref": "#/$defs/ClassifierContextConfig",
          "description": "What context the classifier receives."
        },
        "directive_footer": {
          "default": "Follow these directives. They are based on your behavioral rules and the current session state. Violations are detected in real-time.",
          "description": "Text appended at the end of every directive message.",
          "title": "Directive Footer",
          "type": "string"
        },
        "directive_prefix": {
          "default": "[BEHAVIOR DIRECTIVE - {complexity} complexity, {risk} risk]",
          "description": "Format string for the directive header. Available placeholders:\n{complexity}, {approach}, {risk}, {approach_label}",
          "title": "Directive Prefix",
          "type": "string"
        },
        "frequency": {
          "default": "every_turn",
          "description": "When to run the classifier:\n  'every_turn'    - before every agent turn (classifier can skip via skip_reason)\n  'first_turn'    - only on the first turn of a session\n  'every_n_turns' - every N turns (set frequency_n)\n  'on_new_message'- only when the user sent a new message (skip tool-only turns)",
          "enum": [
            "every_turn",
            "first_turn",
            "every_n_turns",
            "on_new_message"
          ],
          "title": "Frequency",
          "type": "string"
        },
        "frequency_n": {
          "default": 3,
          "description": "For 'every_n_turns': run every N turns.",
          "title": "Frequency N",
          "type": "integer"
        },
        "high_risk_threshold": {
          "default": "medium",
          "description": "Risk level (from risk_levels) at or above which high_risk_warning is appended.",
          "title": "High Risk Threshold",
          "type": "string"
        },
        "high_risk_warning": {
          "default": "Risk level: {risk}. Confirm destructive or irreversible actions with the user before proceeding.",
          "description": "Warning appended when risk >= high_risk_threshold. Use {risk} placeholder.",
          "title": "High Risk Warning",
          "type": "string"
        },
        "max_directives": {
          "default": 5,
          "description": "Maximum number of directives the classifier should produce.",
          "title": "Max Directives",
          "type": "integer"
        },
        "risk_levels": {
          "description": "Risk levels. Same format as approaches - string or dict:\n\n  risk_levels:\n    - name: safe\n      when: 'Read-only, no side effects'\n    - name: destructive\n      when: 'Deletes data, drops tables, force-pushes'\n      behavior: 'MUST confirm with user, explain what will be lost'",
          "items": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "additionalProperties": {
                  "type": "string"
                },
                "type": "object"
              }
            ]
          },
          "title": "Risk Levels",
          "type": "array"
        },
        "skip_followups": {
          "default": true,
          "description": "Auto-skip classification for simple follow-ups: 'yes', 'ok', 'continue', 'go ahead', etc. Saves a classifier LLM call.",
          "title": "Skip Followups",
          "type": "boolean"
        },
        "system_prompt": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Custom system prompt for the classifier LLM.\nSupports {{prompt.X}} references to load from ./prompts/.\nWhen null, uses the built-in default behavioral model.\nThe prompt receives the output schema dynamically - you don't\nneed to hardcode complexity/approach values in your prompt.",
          "title": "System Prompt"
        },
        "timeout": {
          "default": 15,
          "description": "Max seconds to wait for the classifier LLM response.",
          "title": "Timeout",
          "type": "integer"
        }
      },
      "title": "ClassifierConfig",
      "type": "object"
    },
    "ClassifierContextConfig": {
      "additionalProperties": false,
      "description": "What context the classifier receives about the agent's state.",
      "properties": {
        "history_depth": {
          "default": 8,
          "description": "How many recent messages to include.",
          "title": "History Depth",
          "type": "integer"
        },
        "recent_history": {
          "default": true,
          "description": "Send recent messages with tool calls and results.",
          "title": "Recent History",
          "type": "boolean"
        },
        "session_state": {
          "default": true,
          "description": "Send session state: files read/edited, searches, violations, turn number.",
          "title": "Session State",
          "type": "boolean"
        },
        "tool_inventory": {
          "default": true,
          "description": "Send the agent's tool names + descriptions.",
          "title": "Tool Inventory",
          "type": "boolean"
        },
        "workspace_info": {
          "default": true,
          "description": "Send workspace metadata: project type, languages, file count.",
          "title": "Workspace Info",
          "type": "boolean"
        }
      },
      "title": "ClassifierContextConfig",
      "type": "object"
    },
    "ContextConfig": {
      "additionalProperties": false,
      "description": "Context management configuration for the agent loop.\n\nControls how the context window is managed to prevent overflow errors.\nWhen the context fills up, the runtime can automatically compact it\nusing the configured strategy.\n\nCan be set at two levels:\n- ``execution.context`` - default for all agents\n- ``agent.brain.context`` - per-brain override (multi-agent apps)\n\nExample::\n\n    brain:\n      provider: deepseek\n      model: deepseek-chat\n      context:\n        max_tokens: 131072\n        strategy: summarize\n        keep_recent: 30\n        compression_trigger: 0.70",
      "properties": {
        "auto_compact": {
          "default": true,
          "description": "Enable automatic compaction. When true, the runtime injects a context_pressure hook automatically if none is declared.",
          "title": "Auto Compact",
          "type": "boolean"
        },
        "compression_trigger": {
          "default": 0.75,
          "description": "Token pressure ratio (0.0-1.0) at which automatic compaction triggers.",
          "title": "Compression Trigger",
          "type": "number"
        },
        "keep_recent": {
          "default": 10,
          "description": "Number of recent messages to preserve during compaction.",
          "title": "Keep Recent",
          "type": "integer"
        },
        "max_tokens": {
          "default": 0,
          "description": "Context window size in tokens. 0 = auto-detect from provider. Override if the provider doesn't report it.",
          "maximum": 2000000,
          "minimum": 0,
          "title": "Max Tokens",
          "type": "integer"
        },
        "output_reserved": {
          "default": 4096,
          "description": "Tokens reserved for output generation. Subtracted from max_tokens for pressure calculation.",
          "title": "Output Reserved",
          "type": "integer"
        },
        "strategy": {
          "default": "summarize",
          "description": "Compaction strategy: 'truncate' or 'summarize'.",
          "enum": [
            "truncate",
            "summarize"
          ],
          "title": "Strategy",
          "type": "string"
        },
        "summary_brain": {
          "anyOf": [
            {
              "$ref": "#/$defs/AgentBrain"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Optional separate brain for summarization during compaction. Use a fast/cheap model for summaries instead of the main brain. If not set, the agent's main brain is used."
        },
        "summary_max_tokens": {
          "default": 1024,
          "description": "Maximum tokens for the summary when using 'summarize' strategy.",
          "title": "Summary Max Tokens",
          "type": "integer"
        }
      },
      "title": "ContextConfig",
      "type": "object"
    },
    "CoordinationBlock": {
      "additionalProperties": false,
      "description": "Phase 9 grouped orchestration concerns on an agent.\n\nReplaces the historical scatter (``delegate_to``, ``pool``) with\na single sub-block. Both shapes still compile - the legacy fields\nare aliased into this block at compile time.",
      "properties": {
        "delegate_to": {
          "description": "Agent ids this coordinator can dispatch to.",
          "items": {
            "type": "string"
          },
          "title": "Delegate To",
          "type": "array"
        },
        "pool": {
          "anyOf": [
            {
              "$ref": "#/$defs/AgentPoolConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Agent-pool config (max_workers, progress, auto_retry)."
        }
      },
      "title": "CoordinationBlock",
      "type": "object"
    },
    "CredentialFieldConfig": {
      "additionalProperties": false,
      "description": "One field inside a credential provider (e.g. ``api_key``, ``bot_token``).\n\nDirectly mapped to the form widget the client renders.",
      "properties": {
        "default": {
          "default": null,
          "description": "Pre-filled default value.",
          "title": "Default"
        },
        "description": {
          "default": "",
          "description": "Help text.",
          "title": "Description",
          "type": "string"
        },
        "help": {
          "default": "",
          "description": "Extra inline help shown below the input.",
          "title": "Help",
          "type": "string"
        },
        "label": {
          "default": "",
          "description": "Human label shown in the form.",
          "title": "Label",
          "type": "string"
        },
        "name": {
          "description": "Internal field name (identifier).",
          "title": "Name",
          "type": "string"
        },
        "options": {
          "description": "Allowed values for ``type: select``.",
          "items": {
            "type": "string"
          },
          "title": "Options",
          "type": "array"
        },
        "placeholder": {
          "default": "",
          "description": "Input placeholder.",
          "title": "Placeholder",
          "type": "string"
        },
        "required": {
          "default": false,
          "title": "Required",
          "type": "boolean"
        },
        "type": {
          "default": "secret",
          "description": "Form widget type. ``secret`` = masked password field, ``url`` = URL input with validation, ``select`` requires ``options``, ``connection_string`` = URL with scheme/host check.",
          "enum": [
            "secret",
            "string",
            "url",
            "select",
            "number",
            "boolean",
            "connection_string"
          ],
          "title": "Type",
          "type": "string"
        },
        "validation_regex": {
          "default": "",
          "description": "Optional regex the value must match. Validated both server-side (handler) and client-side (form).",
          "title": "Validation Regex",
          "type": "string"
        }
      },
      "required": [
        "name"
      ],
      "title": "CredentialFieldConfig",
      "type": "object"
    },
    "CredentialProviderConfig": {
      "additionalProperties": false,
      "description": "One provider entry inside ``credentials_schema.providers``.\n\nEach provider declares which fields are needed, which handler\nshould process them (``type``), and which scope rules apply\n(``per_user`` / ``per_app_shared`` / ``system_wide``).",
      "properties": {
        "command": {
          "description": "For stdio MCP servers: command + args to spawn.",
          "items": {
            "type": "string"
          },
          "title": "Command",
          "type": "array"
        },
        "docs_url": {
          "default": "",
          "description": "Link to the provider's docs / 'where do I get this?'",
          "title": "Docs Url",
          "type": "string"
        },
        "env_template": {
          "additionalProperties": {
            "type": "string"
          },
          "description": "For MCP servers: extra env vars to inject into the spawned process. Supports ``{{field.X}}`` substitution pulling from the filled credential fields.",
          "title": "Env Template",
          "type": "object"
        },
        "fields": {
          "description": "Fields the user must fill.",
          "items": {
            "$ref": "#/$defs/CredentialFieldConfig"
          },
          "title": "Fields",
          "type": "array"
        },
        "health_check": {
          "additionalProperties": true,
          "description": "For MCP servers: how to check the server is alive. e.g. ``{method: tools/list, timeout_s: 5}``.",
          "title": "Health Check",
          "type": "object"
        },
        "icon": {
          "default": "",
          "description": "Logo URL shown in the form.",
          "title": "Icon",
          "type": "string"
        },
        "label": {
          "default": "",
          "description": "Human label for the UI.",
          "title": "Label",
          "type": "string"
        },
        "name": {
          "description": "Internal provider id. Used as the path segment in ``/credentials/{app_id}/{provider_name}`` routes.",
          "title": "Name",
          "type": "string"
        },
        "oauth_provider": {
          "default": "",
          "description": "For ``type: oauth2``: the key of the OAuth provider registered on the daemon (notion, google, github, slack). The daemon's client_id / client_secret for this provider must be configured by the admin.",
          "title": "Oauth Provider",
          "type": "string"
        },
        "oauth_scopes": {
          "description": "OAuth scopes to request during the flow.",
          "items": {
            "type": "string"
          },
          "title": "Oauth Scopes",
          "type": "array"
        },
        "required": {
          "default": true,
          "description": "Whether the app refuses to run without this provider filled.",
          "title": "Required",
          "type": "boolean"
        },
        "scope": {
          "default": "per_user",
          "description": "Where the credential lives: ``per_user`` means each user has their own (default), ``per_app_shared`` means one credential for all users of this app, ``system_wide`` means daemon-level config (admin only).",
          "enum": [
            "per_user",
            "per_app_shared",
            "system_wide"
          ],
          "title": "Scope",
          "type": "string"
        },
        "test": {
          "additionalProperties": true,
          "description": "Optional live-connection test declaration. For api_key: ``{method, url, auth_header, expected_status}``. For connection_string: ``{test_query}``.",
          "title": "Test",
          "type": "object"
        },
        "transport": {
          "default": "",
          "description": "For ``type: mcp_server``: stdio / http / ws.",
          "enum": [
            "stdio",
            "http",
            "ws",
            ""
          ],
          "title": "Transport",
          "type": "string"
        },
        "type": {
          "default": "api_key",
          "description": "Handler type. Determines the form widget, validation rules, and lifecycle behaviour.",
          "enum": [
            "api_key",
            "multi_field",
            "oauth2",
            "connection_string",
            "mcp_server",
            "custom"
          ],
          "title": "Type",
          "type": "string"
        },
        "url": {
          "default": "",
          "description": "For http/ws MCP servers: the server URL.",
          "title": "Url",
          "type": "string"
        }
      },
      "required": [
        "name"
      ],
      "title": "CredentialProviderConfig",
      "type": "object"
    },
    "CredentialsSchemaConfig": {
      "additionalProperties": false,
      "description": "Declarative credentials schema for a Digitorn app.\n\nWhen set, the client fetches this from\n``GET /api/apps/{id}/credentials/schema`` and renders a typed\nform for each provider. The daemon's resolver also uses it to\nknow what's expected so it can fail with a clean \"credential\nmissing\" error rather than a cryptic compile-time secret miss.\n\nExample::\n\n    credentials_schema:\n      required: true\n      providers:\n        - name: openai\n          label: OpenAI\n          type: api_key\n          scope: per_user\n          fields:\n            - name: api_key\n              type: secret\n              required: true\n              validation_regex: \"^sk-[A-Za-z0-9_-]{20,}$\"\n        - name: notion\n          type: oauth2\n          oauth_provider: notion\n          scope: per_user\n          oauth_scopes: [read_content, update_content]\n        - name: notion_mcp\n          type: mcp_server\n          transport: stdio\n          command: [\"npx\", \"-y\", \"@modelcontextprotocol/server-notion\"]\n          env_template:\n            NOTION_API_KEY: \"{{field.api_key}}\"\n          fields:\n            - name: api_key\n              type: secret\n              required: true",
      "properties": {
        "providers": {
          "description": "Declared credential providers.",
          "items": {
            "$ref": "#/$defs/CredentialProviderConfig"
          },
          "title": "Providers",
          "type": "array"
        },
        "required": {
          "default": true,
          "description": "If true, the daemon blocks activation when any required provider is not filled for the user.",
          "title": "Required",
          "type": "boolean"
        }
      },
      "title": "CredentialsSchemaConfig",
      "type": "object"
    },
    "DecisionNode": {
      "additionalProperties": false,
      "description": "Pure routing decision - no LLM, no tool, just an expression.\n\n``expr`` is evaluated against the flow context. The result is\nmatched against ``routes[].when`` clauses to pick the next hop.",
      "properties": {
        "description": {
          "default": "",
          "description": "Free-form description, surfaced on the canvas tooltip.",
          "title": "Description",
          "type": "string"
        },
        "expr": {
          "description": "Expression that drives routing.",
          "minLength": 1,
          "title": "Expr",
          "type": "string"
        },
        "id": {
          "description": "Unique node identifier within the flow.",
          "title": "Id",
          "type": "string"
        },
        "on_error": {
          "description": "Error-handling edges. Catch-all (default: true) must be last.",
          "items": {
            "$ref": "#/$defs/FlowOnErrorRoute"
          },
          "title": "On Error",
          "type": "array"
        },
        "routes": {
          "description": "Outgoing edges. Top-to-bottom evaluation order.",
          "items": {
            "$ref": "#/$defs/FlowRoute"
          },
          "title": "Routes",
          "type": "array"
        },
        "type": {
          "const": "decision",
          "title": "Type",
          "type": "string"
        }
      },
      "required": [
        "id",
        "type",
        "expr"
      ],
      "title": "DecisionNode",
      "type": "object"
    },
    "DevBlock": {
      "additionalProperties": false,
      "description": "Developer affordances: skills, variables, fragmentation directives.\n\nThings authored at design time that don't fit cleanly into the\nother blocks. Skills are /command markdown files. Variables are\ntemplate substitutions. Include is the fragmentation directive.",
      "properties": {
        "include": {
          "anyOf": [
            {
              "$ref": "#/$defs/IncludeBlock"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Optional fragmentation block. Splits agents, hooks, and other list-shaped sections into separate files."
        },
        "skills": {
          "description": "App-level /command skill files the agent can invoke.",
          "items": {
            "$ref": "#/$defs/SkillEntry"
          },
          "title": "Skills",
          "type": "array"
        },
        "variables": {
          "additionalProperties": {
            "type": "string"
          },
          "description": "Template variables available as {{name}} in params and constraints.",
          "title": "Variables",
          "type": "object"
        }
      },
      "title": "DevBlock",
      "type": "object"
    },
    "FlowConfig": {
      "additionalProperties": false,
      "description": "Declarative orchestration graph for a Digitorn app.\n\nExample::\n\n    flow:\n      id: support_main\n      entry: triage\n      max_iterations: 100\n      nodes:\n        - id: triage\n          type: agent\n          agent: triage\n          routes:\n            - { when: \"category == 'refund'\", to: refund }\n            - { when: \"default\", to: end }\n        - id: refund\n          type: agent\n          agent: refund_specialist\n          routes:\n            - { to: gate }\n        - id: gate\n          type: approval\n          message: \"Confirm refund?\"\n          routes:\n            - { when: \"approvals.gate == 'approve'\", to: end }\n            - { when: \"default\", to: end }",
      "properties": {
        "description": {
          "default": "",
          "description": "Free-form summary of the flow.",
          "title": "Description",
          "type": "string"
        },
        "entry": {
          "description": "Starting node id.",
          "minLength": 1,
          "title": "Entry",
          "type": "string"
        },
        "id": {
          "description": "Flow identifier (unique within the app).",
          "minLength": 1,
          "title": "Id",
          "type": "string"
        },
        "max_iterations": {
          "default": 0,
          "description": "Per-flow cap on total node visits. 0 = no cap (only valid for acyclic flows). Required (>= 1) when the graph has any cycle to prevent infinite loops at runtime.",
          "minimum": 0,
          "title": "Max Iterations",
          "type": "integer"
        },
        "nodes": {
          "description": "Nodes that compose the graph.",
          "items": {
            "discriminator": {
              "mapping": {
                "agent": "#/$defs/AgentNode",
                "approval": "#/$defs/ApprovalNode",
                "decision": "#/$defs/DecisionNode",
                "parallel": "#/$defs/ParallelNode",
                "terminal": "#/$defs/TerminalNode",
                "tool": "#/$defs/ToolNode"
              },
              "propertyName": "type"
            },
            "oneOf": [
              {
                "$ref": "#/$defs/AgentNode"
              },
              {
                "$ref": "#/$defs/ToolNode"
              },
              {
                "$ref": "#/$defs/ParallelNode"
              },
              {
                "$ref": "#/$defs/ApprovalNode"
              },
              {
                "$ref": "#/$defs/DecisionNode"
              },
              {
                "$ref": "#/$defs/TerminalNode"
              }
            ]
          },
          "minItems": 1,
          "title": "Nodes",
          "type": "array"
        }
      },
      "required": [
        "id",
        "entry",
        "nodes"
      ],
      "title": "FlowConfig",
      "type": "object"
    },
    "FlowJoin": {
      "additionalProperties": false,
      "description": "Join policy for parallel fan-outs.\n\n- ``all`` (default): wait for every branch to complete.\n- ``any``: continue as soon as one branch returns.\n- ``first``: same as ``any``, alias for clarity.\n- ``count``: wait for exactly ``count`` branches.\n\n``timeout`` is the per-join wall-clock cap in seconds. Any branch\nstill running when it elapses is cancelled and treated as failed.",
      "properties": {
        "count": {
          "default": 0,
          "description": "Required only when type='count'. Number of branches to wait for.",
          "minimum": 0,
          "title": "Count",
          "type": "integer"
        },
        "timeout": {
          "default": 60.0,
          "description": "Per-join wall-clock cap in seconds.",
          "exclusiveMinimum": 0,
          "title": "Timeout",
          "type": "number"
        },
        "type": {
          "default": "all",
          "description": "How many branches must complete before joining.",
          "enum": [
            "all",
            "any",
            "first",
            "count"
          ],
          "title": "Type",
          "type": "string"
        }
      },
      "title": "FlowJoin",
      "type": "object"
    },
    "FlowOnErrorRoute": {
      "additionalProperties": false,
      "description": "Error-handling edge. Matched when the source node raises.\n\nEither ``match`` (regex on the error type/message) plus ``to``, or\n``default: True`` plus ``to`` for the catch-all branch. Listed in\norder; first match wins. ``default`` must be the last entry.",
      "properties": {
        "default": {
          "default": false,
          "description": "Catch-all clause. Must come last when present.",
          "title": "Default",
          "type": "boolean"
        },
        "match": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Regex matched against the runtime error type or message.",
          "title": "Match"
        },
        "to": {
          "description": "Target node id when this clause matches.",
          "title": "To",
          "type": "string"
        }
      },
      "required": [
        "to"
      ],
      "title": "FlowOnErrorRoute",
      "type": "object"
    },
    "FlowRoute": {
      "additionalProperties": false,
      "description": "A directed edge from the current node to ``to`` under condition ``when``.\n\n``when`` is either a literal expression (``\"input.kind == 'refund'\"``)\nor the sentinel ``\"default\"`` meaning \"match if no other route matched\nfirst\". Routes are evaluated top-to-bottom; the first matching route\nwins.\n\nThe expression syntax is intentionally NOT validated here - that\nhappens in the runtime's expression engine. The schema only checks\nthat ``when`` is a non-empty string and ``to`` references something.",
      "properties": {
        "to": {
          "description": "Target node id, or the literal sentinel 'end'.",
          "title": "To",
          "type": "string"
        },
        "when": {
          "default": "default",
          "description": "Condition expression or 'default'. First match wins.",
          "title": "When",
          "type": "string"
        }
      },
      "required": [
        "to"
      ],
      "title": "FlowRoute",
      "type": "object"
    },
    "HookActionConfig": {
      "additionalProperties": true,
      "description": "Action configuration for an internal hook.\n\nBuilt-in actions:\n- ``compact_context``: intelligently compact message history\n- ``inject_message``: inject a message into the conversation\n- ``module_action``: call any module action\n- ``log``: log a message (debugging)\n\nExample::\n\n    action:\n      type: compact_context\n      strategy: summarize\n      keep_last: 10",
      "properties": {
        "type": {
          "description": "Action type (registered name).",
          "title": "Type",
          "type": "string"
        }
      },
      "required": [
        "type"
      ],
      "title": "HookActionConfig",
      "type": "object"
    },
    "HookConditionConfig": {
      "additionalProperties": true,
      "description": "Condition configuration for an internal hook.\n\nBuilt-in conditions:\n- ``context_pressure``: fires when token usage exceeds threshold\n- ``turn_count``: fires at a specific turn number or every N turns\n- ``tool_calls``: fires when tool call count exceeds threshold\n- ``message_count``: fires when message count exceeds threshold\n- ``always``: fires every time (useful with cooldown)\n\nExample::\n\n    condition:\n      type: context_pressure\n      threshold: 0.75\n      max_tokens: 128000",
      "properties": {
        "type": {
          "description": "Condition type (registered name).",
          "title": "Type",
          "type": "string"
        }
      },
      "required": [
        "type"
      ],
      "title": "HookConditionConfig",
      "type": "object"
    },
    "HookConfig": {
      "additionalProperties": false,
      "description": "An internal hook: condition \u2192 action, evaluated during the agent loop.\n\nExample::\n\n    hooks:\n      - id: context_compaction\n        \"on\": turn_end\n        condition:\n          type: context_pressure\n          threshold: 0.75\n        action:\n          type: compact_context\n          strategy: summarize\n          keep_last: 10\n        cooldown: 30\n\nIMPORTANT: YAML 1.1 parses unquoted ``on`` as boolean ``True``.\nAlways quote it: ``\"on\": tool_end``. This schema rejects any\nnon-string value on that field.",
      "properties": {
        "action": {
          "$ref": "#/$defs/HookActionConfig",
          "description": "Action to execute when the condition is met."
        },
        "condition": {
          "$ref": "#/$defs/HookConditionConfig",
          "description": "Condition that must be true for the hook to fire."
        },
        "cooldown": {
          "default": 0.0,
          "description": "Minimum seconds between fires (0 = no cooldown).",
          "title": "Cooldown",
          "type": "number"
        },
        "enabled": {
          "default": true,
          "description": "Feature flag. When False the hook is loaded but never fires - lets apps A/B gate new behavior without YAML surgery.",
          "title": "Enabled",
          "type": "boolean"
        },
        "id": {
          "description": "Unique hook identifier.",
          "title": "Id",
          "type": "string"
        },
        "max_fires": {
          "default": 0,
          "description": "Max times this hook can fire per app lifetime. 0 = unlimited. Useful for one-shot setup hooks or for bounding runaway triggers.",
          "minimum": 0,
          "title": "Max Fires",
          "type": "integer"
        },
        "on": {
          "default": "turn_end",
          "description": "When to evaluate. One of: activation, agent_complete, agent_spawn, approval_request, error, post_tool_use, pre_compact, pre_tool_use, session_end, session_start, tool_end, tool_start, turn_end, turn_start, user_prompt. MUST be quoted in YAML ('on' is a YAML 1.1 boolean keyword).",
          "title": "On",
          "type": "string"
        },
        "priority": {
          "default": 100,
          "description": "Evaluation order among hooks on the same event. Lower runs first. Same priority \u2192 YAML order is preserved. Default 100.",
          "title": "Priority",
          "type": "integer"
        },
        "tags": {
          "description": "Free-form tags for grouping / querying hooks. Not used by the runtime - surfaced in /api/apps/{id}/hooks for introspection.",
          "items": {
            "type": "string"
          },
          "title": "Tags",
          "type": "array"
        },
        "timeout": {
          "default": 30.0,
          "description": "Max seconds the hook action is allowed to run before being cancelled. Protects the event loop from a runaway hook (catastrophic regex, infinite loop in transform_params, stuck shell, \u2026). Default 30s - enough for compaction. Lower for cheap hooks (log, notify, gate) so a misconfig can't stall the loop. Cancellation surfaces as an ``error`` event for the hook, the turn keeps going.",
          "exclusiveMinimum": 0,
          "title": "Timeout",
          "type": "number"
        }
      },
      "required": [
        "id",
        "condition",
        "action"
      ],
      "title": "HookConfig",
      "type": "object"
    },
    "IncludeBlock": {
      "additionalProperties": false,
      "description": "The optional ``include:`` block that drives fragmentation.\n\nThe compiler resolves these paths BEFORE Pydantic validation - by\nthe time AppDefinition.model_validate runs, the entries here have\nalready been merged into ``agents:`` / ``execution.hooks`` etc and\nthe block has been popped from the raw dict. We still declare it\non the schema so editors give autocomplete and the JSON Schema\ndocuments the feature.\n\nEach value is either a path string ('./agents/') or a list of\npaths (['./roster/triage.yaml', './roster/refund.yaml']).",
      "properties": {
        "agents": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Path to a directory of agent YAML files or a list of paths. Convention auto-loads ``./agents/*.yaml`` even without this entry.",
          "title": "Agents"
        },
        "hooks": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Path to a directory of hook YAML files or a list of paths. Convention auto-loads ``./hooks/*.yaml`` even without this entry.",
          "title": "Hooks"
        }
      },
      "title": "IncludeBlock",
      "type": "object"
    },
    "InlineWidget": {
      "additionalProperties": false,
      "description": "Named inline widget - referenceable by ``ref:`` from agent SSE.",
      "properties": {
        "data": {
          "additionalProperties": true,
          "title": "Data",
          "type": "object"
        },
        "tree": {
          "$ref": "#/$defs/WidgetNode"
        }
      },
      "required": [
        "tree"
      ],
      "title": "InlineWidget",
      "type": "object"
    },
    "InputConfig": {
      "additionalProperties": false,
      "description": "Input contract for one_shot mode.\n\nDefines what the application expects as input and how the CLI\nshould present it to the agent.\n\nExample::\n\n    input:\n      type: text\n      description: \"Code source to analyse\"\n      required: true",
      "properties": {
        "accept": {
          "description": "Accepted MIME types. Empty = infer from type. Examples: ['image/png', 'image/jpeg'], ['audio/wav', 'audio/mp3'], ['application/pdf'], ['video/mp4'].",
          "items": {
            "type": "string"
          },
          "title": "Accept",
          "type": "array"
        },
        "description": {
          "default": "",
          "description": "Human-readable description of the expected input.",
          "title": "Description",
          "type": "string"
        },
        "max_size": {
          "default": "",
          "description": "Maximum input size. Examples: '10MB', '500KB'. Empty = no limit.",
          "title": "Max Size",
          "type": "string"
        },
        "required": {
          "default": true,
          "description": "Whether input is mandatory.",
          "title": "Required",
          "type": "boolean"
        },
        "type": {
          "default": "text",
          "description": "Input type: 'text', 'image', 'audio', 'video', 'file', 'json', 'any'. Must be supported by the agent's brain model. For example, 'image' requires a vision-capable model (GPT-4o, Claude Sonnet, Gemini).",
          "title": "Type",
          "type": "string"
        }
      },
      "title": "InputConfig",
      "type": "object"
    },
    "InstructionsBlock": {
      "additionalProperties": false,
      "description": "Phase 9 grouped prompt-extension concerns on an agent.\n\nReplaces the historical scatter (``specialty``, ``skills``,\n``capabilities``) with a single sub-block whose role is clearer:\neverything here ENRICHES the agent's prompt at runtime.\n\nBoth shapes still compile - the legacy fields are aliased into this\nblock at compile time when the new shape is empty.",
      "properties": {
        "capabilities": {
          "description": "Names of skill files to auto-load from the bundle's ``./skills/`` directory.",
          "items": {
            "type": "string"
          },
          "title": "Capabilities",
          "type": "array"
        },
        "file": {
          "default": "",
          "description": "Path to a .md file with detailed methodology / instructions. Loaded at compile time and appended to the system prompt.",
          "title": "File",
          "type": "string"
        },
        "specialty": {
          "default": "",
          "description": "Short description of this specialist's expertise. Shown to the coordinator in the agent_spawn module's specialist catalogue.",
          "title": "Specialty",
          "type": "string"
        }
      },
      "title": "InstructionsBlock",
      "type": "object"
    },
    "MessageActionsBlock": {
      "additionalProperties": false,
      "description": "Top-level ``ui.message_actions`` block. Disabled by default\nso apps without the block keep their historical \"no extra row\nunder messages\" behaviour with zero behaviour change.",
      "properties": {
        "enabled": {
          "default": false,
          "description": "Master toggle. When false (default), the dispatcher is short-circuited and no message actions render. Useful for staged rollouts: ship the rules but keep ``enabled: false`` until QA signs off.",
          "title": "Enabled",
          "type": "boolean"
        },
        "fallback_on_error": {
          "default": true,
          "description": "When the matched widget throws, swallow the error and render nothing under the message. Set false during local renderer dev to surface the failure.",
          "title": "Fallback On Error",
          "type": "boolean"
        },
        "rules": {
          "description": "Ordered rules. First match wins. An empty array means the dispatcher runs but never matches - same as ``enabled: false`` for end users.",
          "items": {
            "$ref": "#/$defs/MessageActionsRule"
          },
          "title": "Rules",
          "type": "array"
        }
      },
      "title": "MessageActionsBlock",
      "type": "object"
    },
    "MessageActionsMatch": {
      "additionalProperties": false,
      "description": "Predicate evaluated against a message to decide if the rule\nfires. Every field is optional and ANDed - rules with no fields\nset always match.\n\nMatch criteria are intentionally narrow to keep the dispatcher\nfast (each message walks the rules array on every render). Use\n``content_regex`` sparingly - regex compile + test on every\nmessage can show up in profiles for chat panes with thousands\nof messages.",
      "properties": {
        "content_regex": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Regex pattern (re.search) tested against the message text. Use sparingly - regex compile fires per render per rule per message. Null = any.",
          "title": "Content Regex"
        },
        "has_tool_calls": {
          "anyOf": [
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Match only when the message has at least one tool call (true), or no tool calls at all (false). Null = ignore the count entirely.",
          "title": "Has Tool Calls"
        },
        "role": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Match the message role exactly. Common values: ``user``, ``assistant``, ``system``. Null = any.",
          "title": "Role"
        },
        "tool_pattern": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Regex pattern (re.search semantics) matched against every tool call name in the message. First match in the message decides. Null = any.",
          "title": "Tool Pattern"
        },
        "tool_used": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Match when the message contains a tool call with this EXACT name (e.g. ``WsWrite``). For pattern matching use ``tool_pattern`` instead. Null = any.",
          "title": "Tool Used"
        }
      },
      "title": "MessageActionsMatch",
      "type": "object"
    },
    "MessageActionsRule": {
      "additionalProperties": false,
      "description": "One ``match \u2192 render`` rule. The rules array is evaluated\ntop-to-bottom and the FIRST matching rule wins - put the most\nspecific rules first.",
      "properties": {
        "match": {
          "$ref": "#/$defs/MessageActionsMatch",
          "description": "Predicate. Empty match (no fields set) acts as a catch-all - useful at the bottom of the array."
        },
        "ref": {
          "description": "Name of an entry in ``ui.widgets.inline``. The widget tree there is rendered UNDER the matching message body, with ``{{message.*}}`` bindings substituted.",
          "title": "Ref",
          "type": "string"
        }
      },
      "required": [
        "ref"
      ],
      "title": "MessageActionsRule",
      "type": "object"
    },
    "ModalWidget": {
      "additionalProperties": false,
      "description": "Z4 - modal pushed by ``action: open_modal``.",
      "properties": {
        "data": {
          "additionalProperties": true,
          "title": "Data",
          "type": "object"
        },
        "dismissible": {
          "default": true,
          "title": "Dismissible",
          "type": "boolean"
        },
        "title": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Title"
        },
        "tree": {
          "$ref": "#/$defs/WidgetNode"
        },
        "width": {
          "anyOf": [
            {
              "type": "integer"
            },
            {
              "type": "string"
            }
          ],
          "default": 560,
          "description": "Modal width preset (one of 420|560|640|720|'full') or px int.",
          "title": "Width"
        }
      },
      "required": [
        "tree"
      ],
      "title": "ModalWidget",
      "type": "object"
    },
    "ModeDef": {
      "additionalProperties": false,
      "description": "Per-mode runtime configuration.\n\nThe chat composer surfaces a \"mode picker\" pill (Ask / Plan / Auto / \u2026)\nbacked by `runtime.modes`. Each entry is a *sparse* override:\nonly fields the user sets are applied on top of the app's normal\nruntime / agent / tool config when that mode is the active one.\nEmpty fields fall back to the app's defaults.\n\nConceptually a mode lets one app behave like several:\n  - tighten / loosen the agent's autonomy (`max_turns`, `timeout`)\n  - swap or amend the system prompt (`system_prompt`)\n  - gate which tools the agent can reach (`tool_grants`)\n  - flip the behavior engine profile (`behavior_profile`)\n\nPicker UX: the composer hides the picker entirely when only a\nsingle mode is declared (no choice = no menu). Apps that want the\npicker must declare at least two entries.",
      "properties": {
        "accent": {
          "default": "",
          "description": "Optional accent colour for the pill border + dropdown row tint. Known: `primary`, `secondary`, `cyan`, `purple`, `red`, `green`, `orange`. Empty falls back to the theme accent.",
          "title": "Accent",
          "type": "string"
        },
        "behavior_profile": {
          "default": "",
          "description": "Override the behavior module profile while this mode is active (e.g. `assistant` for Ask, `coding` for Auto). Empty inherits the app's normal `security.behavior.profile`.",
          "title": "Behavior Profile",
          "type": "string"
        },
        "description": {
          "default": "",
          "description": "One-line subtitle shown under the label in the picker dropdown. Keep it short (\u2264 30 chars) so the panel stays narrow.",
          "title": "Description",
          "type": "string"
        },
        "icon": {
          "default": "",
          "description": "Optional icon hint for the picker. Known: `lightbulb`, `map`, `sparkles`, `wrench`, `shield`. When empty the client picks a sensible default per mode id.",
          "title": "Icon",
          "type": "string"
        },
        "label": {
          "default": "",
          "description": "Display name shown in the picker pill + the dropdown row. Defaults to the mode id capitalised when empty.",
          "title": "Label",
          "type": "string"
        },
        "max_turns": {
          "anyOf": [
            {
              "minimum": 1,
              "type": "integer"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Override `runtime.max_turns` while this mode is active. Use 1 for a strict one-shot mode.",
          "title": "Max Turns"
        },
        "system_prompt": {
          "default": "",
          "description": "Suffix appended to the agent's existing system prompt when this mode is active. Use to nudge the agent towards the mode's intent (e.g. \"Reply concisely, no tools.\").",
          "title": "System Prompt",
          "type": "string"
        },
        "timeout": {
          "anyOf": [
            {
              "exclusiveMinimum": 0,
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Override `runtime.timeout` (seconds) for this mode.",
          "title": "Timeout"
        },
        "tool_grants": {
          "description": "Subset of the app's tools the agent may reach in this mode. Same shape as `tools.grant`. Empty = inherit everything from the app's normal grant list.",
          "items": {
            "$ref": "#/$defs/CapabilityGrant"
          },
          "title": "Tool Grants",
          "type": "array"
        },
        "workspace_mode": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Override `ui.workspace.mode` for this mode. e.g. `none` for an Ask mode that hides the workspace panel.",
          "title": "Workspace Mode"
        }
      },
      "title": "ModeDef",
      "type": "object"
    },
    "ModuleBlock": {
      "additionalProperties": false,
      "description": "Configuration block for a single module in the app YAML.\n\nThree sections:\n\n- ``config``: Static module configuration - pushed via\n  ``module.on_config_update(config)`` at bootstrap time.  Validated\n  against the module's ``CONFIG_MODEL`` (Pydantic) if declared.\n\n- ``setup``: Ordered list of action calls executed at bootstrap time.\n\n- ``constraints``: Runtime restrictions applied during the app's lifetime.\n\nExample::\n\n    perception:\n      config:\n        enabled: false\n        capture_after: true\n        ocr_enabled: false\n        timeout_seconds: 10\n        actions:\n          browser.take_screenshot:\n            capture_after: true\n            ocr_enabled: true\n      setup:\n        - action: register_handler\n          params: { ... }\n      constraints:\n        allowed_actions: [capture_screen, parse_screen]",
      "properties": {
        "config": {
          "additionalProperties": true,
          "description": "Static module configuration. Pushed to the module via on_config_update() at bootstrap time. Validated against the module's CONFIG_MODEL if declared.\n\nFor MCP servers and third-party modules, an optional 'sandbox' key declares OS-level permissions:\n  sandbox:\n    permissions: [fs.read, net.http]\n    paths:\n      read: ['{{workspace}}']\n      write: []\n    allowed_hosts: [api.github.com]",
          "title": "Config",
          "type": "object"
        },
        "constraints": {
          "additionalProperties": true,
          "description": "Runtime constraints. 'allowed_actions' and 'blocked_actions' are universal; other keys are validated against the module's ConstraintSpec declarations.",
          "title": "Constraints",
          "type": "object"
        },
        "credential": {
          "default": null,
          "description": "Reference to a user-vault credential bound to this module.\nTwo shapes (compact and explicit):\n  credential: openai_main\n  credential: { ref: openai_main, scope: per_user }\nResolved at activation time. The module's CredentialSlot declares which fields are injected and where in `config`.",
          "title": "Credential"
        },
        "middleware": {
          "description": "Module-level middleware pipeline. Each entry is a middleware name with optional config: [{audit: {log_params: true}}, {retry: {max_attempts: 3}}]",
          "items": {
            "additionalProperties": true,
            "type": "object"
          },
          "title": "Middleware",
          "type": "array"
        },
        "setup": {
          "description": "Ordered list of actions to execute at app bootstrap.",
          "items": {
            "$ref": "#/$defs/SetupStep"
          },
          "title": "Setup",
          "type": "array"
        }
      },
      "title": "ModuleBlock",
      "type": "object"
    },
    "OutputConfig": {
      "additionalProperties": false,
      "description": "Output contract for one_shot mode.\n\nDefines what the application produces and how the CLI should\nformat it.\n\nExample::\n\n    output:\n      type: json\n      description: \"Structured analysis report\"\n      schema:\n        type: object\n        properties:\n          bugs: { type: array }\n          score: { type: integer }",
      "properties": {
        "description": {
          "default": "",
          "description": "Human-readable description of the output.",
          "title": "Description",
          "type": "string"
        },
        "format": {
          "default": "",
          "description": "Output format hint. For 'json': a JSON Schema. For 'file': the file extension. For 'image': 'png', 'svg', etc.",
          "title": "Format",
          "type": "string"
        },
        "schema": {
          "additionalProperties": true,
          "description": "Optional JSON Schema for the expected output structure.",
          "title": "Schema",
          "type": "object"
        },
        "type": {
          "default": "text",
          "description": "Output type: 'text', 'json', 'markdown', 'file', 'image', 'audio'. Determines how the CLI and API format the response.",
          "title": "Type",
          "type": "string"
        }
      },
      "title": "OutputConfig",
      "type": "object"
    },
    "ParallelNode": {
      "additionalProperties": false,
      "description": "Fan-out into N parallel branches, join, then continue.\n\nEach branch is a ``FlowRoute`` whose ``to`` points to a node that\nruns concurrently with its siblings. The ``join`` field specifies\nhow many branches must complete before the flow continues via the\nparent's ``routes``.",
      "properties": {
        "branches": {
          "description": "Concurrent branches (>= 2 entries).",
          "items": {
            "$ref": "#/$defs/FlowRoute"
          },
          "minItems": 2,
          "title": "Branches",
          "type": "array"
        },
        "description": {
          "default": "",
          "description": "Free-form description, surfaced on the canvas tooltip.",
          "title": "Description",
          "type": "string"
        },
        "id": {
          "description": "Unique node identifier within the flow.",
          "title": "Id",
          "type": "string"
        },
        "join": {
          "$ref": "#/$defs/FlowJoin",
          "description": "Join policy after the branches complete."
        },
        "on_error": {
          "description": "Error-handling edges. Catch-all (default: true) must be last.",
          "items": {
            "$ref": "#/$defs/FlowOnErrorRoute"
          },
          "title": "On Error",
          "type": "array"
        },
        "routes": {
          "description": "Outgoing edges. Top-to-bottom evaluation order.",
          "items": {
            "$ref": "#/$defs/FlowRoute"
          },
          "title": "Routes",
          "type": "array"
        },
        "type": {
          "const": "parallel",
          "title": "Type",
          "type": "string"
        }
      },
      "required": [
        "id",
        "type",
        "branches"
      ],
      "title": "ParallelNode",
      "type": "object"
    },
    "PayloadFieldConfig": {
      "additionalProperties": false,
      "description": "One declared field on a background app's session payload metadata.\n\nThe list of these is what the client dashboard uses to render a\ntyped form for the user instead of a generic key/value editor.",
      "properties": {
        "default": {
          "default": null,
          "description": "Default value pre-filled in the form.",
          "title": "Default"
        },
        "description": {
          "default": "",
          "description": "Help text shown under the field.",
          "title": "Description",
          "type": "string"
        },
        "label": {
          "default": "",
          "description": "Human-friendly label shown in the form. Defaults to ``name``.",
          "title": "Label",
          "type": "string"
        },
        "max": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Max value for number/integer fields.",
          "title": "Max"
        },
        "min": {
          "anyOf": [
            {
              "type": "number"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Min value for number/integer fields.",
          "title": "Min"
        },
        "name": {
          "description": "Internal key used in payload.metadata. Must be a valid identifier.",
          "title": "Name",
          "type": "string"
        },
        "options": {
          "description": "Allowed values for ``type: select``.",
          "items": {
            "type": "string"
          },
          "title": "Options",
          "type": "array"
        },
        "placeholder": {
          "default": "",
          "description": "Input placeholder.",
          "title": "Placeholder",
          "type": "string"
        },
        "required": {
          "default": false,
          "description": "Whether this metadata field must be set before activation.",
          "title": "Required",
          "type": "boolean"
        },
        "type": {
          "default": "string",
          "description": "Form field type. ``text`` = multiline string. ``select`` requires ``options`` to be set.",
          "enum": [
            "string",
            "number",
            "integer",
            "boolean",
            "select",
            "text"
          ],
          "title": "Type",
          "type": "string"
        }
      },
      "required": [
        "name"
      ],
      "title": "PayloadFieldConfig",
      "type": "object"
    },
    "PayloadFileRuleConfig": {
      "additionalProperties": false,
      "description": "Constraint on the files a user can attach to a session payload.",
      "properties": {
        "description": {
          "default": "",
          "description": "Help text shown next to the upload zone.",
          "title": "Description",
          "type": "string"
        },
        "label": {
          "default": "",
          "description": "Human-friendly label.",
          "title": "Label",
          "type": "string"
        },
        "max_count": {
          "default": 1,
          "description": "Max number of files for this slot.",
          "minimum": 1,
          "title": "Max Count",
          "type": "integer"
        },
        "max_size_mb": {
          "default": 25.0,
          "description": "Per-file size cap in MB (server hard cap is 25 MB).",
          "exclusiveMinimum": 0,
          "title": "Max Size Mb",
          "type": "number"
        },
        "mime": {
          "description": "Accepted MIME types (e.g. ``['application/pdf']``). Empty = any. Wildcards like ``image/*`` are supported.",
          "items": {
            "type": "string"
          },
          "title": "Mime",
          "type": "array"
        },
        "name": {
          "description": "Logical slot name (e.g. ``cv``, ``cover_letter``). Free-form. When ``required: true``, the user must upload at least one file matching ``mime`` for this slot.",
          "title": "Name",
          "type": "string"
        },
        "required": {
          "default": false,
          "description": "Whether at least one matching file is mandatory.",
          "title": "Required",
          "type": "boolean"
        }
      },
      "required": [
        "name"
      ],
      "title": "PayloadFileRuleConfig",
      "type": "object"
    },
    "PayloadSchemaConfig": {
      "additionalProperties": false,
      "description": "Declarative description of the user-pre-filled session payload.\n\nWhen set on a background app, the client dashboard renders a typed\nform (instead of the generic key/value editor) and the daemon can\nenforce validation before letting the cron fire on an empty\nsession. See ``ExecutionConfig.payload_schema``.\n\nExample::\n\n    execution:\n      mode: background\n      payload_schema:\n        required: true\n        prompt:\n          required: true\n          label: \"What should I look for?\"\n          placeholder: \"Find me remote Python jobs paying 80k+\"\n          min_length: 20\n        metadata:\n          - name: location\n            type: string\n            required: true\n            label: \"City\"\n          - name: min_salary\n            type: integer\n            min: 0\n            default: 60000\n          - name: remote_only\n            type: boolean\n            default: true\n        files:\n          - name: cv\n            label: \"Your CV\"\n            required: true\n            mime: [application/pdf]\n            max_size_mb: 5\n          - name: portfolio\n            required: false\n            mime: [application/pdf, image/*]\n            max_count: 5",
      "properties": {
        "files": {
          "description": "File slots with mime/size/count constraints.",
          "items": {
            "$ref": "#/$defs/PayloadFileRuleConfig"
          },
          "title": "Files",
          "type": "array"
        },
        "metadata": {
          "description": "Typed metadata fields the user fills in via a form.",
          "items": {
            "$ref": "#/$defs/PayloadFieldConfig"
          },
          "title": "Metadata",
          "type": "array"
        },
        "prompt": {
          "additionalProperties": true,
          "description": "Prompt field config. Recognised keys: ``required`` (bool), ``label`` (str), ``placeholder`` (str), ``description`` (str), ``default`` (str), ``min_length`` (int), ``max_length`` (int).",
          "title": "Prompt",
          "type": "object"
        },
        "required": {
          "default": false,
          "description": "If true, the daemon refuses to fire triggers for a session whose payload doesn't satisfy the schema (missing required prompt / metadata field / file). The dashboard also blocks the 'Activate session' button until the user fills it in.",
          "title": "Required",
          "type": "boolean"
        }
      },
      "title": "PayloadSchemaConfig",
      "type": "object"
    },
    "PipelineStep": {
      "additionalProperties": false,
      "description": "A single step in a pipeline: call a deployed app with an input.",
      "properties": {
        "app": {
          "description": "Deployed app_id to invoke.",
          "title": "App",
          "type": "string"
        },
        "input": {
          "default": "",
          "description": "Input for this step. Supports {{variables}} including {{input}} (original pipeline input) and {{steps[N].output}} (output of step N).",
          "title": "Input",
          "type": "string"
        },
        "optional": {
          "default": false,
          "description": "If true, continue pipeline even if this step fails.",
          "title": "Optional",
          "type": "boolean"
        },
        "output_as": {
          "default": "",
          "description": "Optional name to reference this step's output in later steps.",
          "title": "Output As",
          "type": "string"
        }
      },
      "required": [
        "app"
      ],
      "title": "PipelineStep",
      "type": "object"
    },
    "QuickPrompt": {
      "additionalProperties": true,
      "description": "A one-click suggested prompt rendered by the client.\n\nFuture-proofed with ``extra: allow`` - the client may consume\nadditional fields (tooltip, accent, ...) without a schema bump.",
      "properties": {
        "icon": {
          "default": "",
          "description": "Optional emoji or icon name for the button.",
          "title": "Icon",
          "type": "string"
        },
        "label": {
          "description": "Short button label (e.g. 'Counter', 'New PR').",
          "minLength": 1,
          "title": "Label",
          "type": "string"
        },
        "message": {
          "description": "The full prompt sent to the agent when clicked.",
          "minLength": 1,
          "title": "Message",
          "type": "string"
        }
      },
      "required": [
        "label",
        "message"
      ],
      "title": "QuickPrompt",
      "type": "object"
    },
    "RuntimeBlock": {
      "additionalProperties": false,
      "description": "Lifecycle + execution policy: how the app actually runs.\n\nHolds every field that controls the daemon's per-turn behaviour:\nmode, max_turns, triggers, hooks, sandbox/security gates, context\nwindow, and the orchestration extensions (middleware, pipeline,\nflow).\n\nFields formerly named under ``execution:`` keep their semantics\nhere unchanged. Two renames for unambiguity:\n\n-  ``execution.workspace``      ->  ``runtime.workdir``\n-  ``execution.workspace_mode`` ->  ``runtime.workdir_mode``\n\nThe renames disambiguate from ``ui.workspace`` (the client
renderer block) which is a completely different concept.",
      "properties": {
        "context": {
          "$ref": "#/$defs/ContextConfig",
          "description": "Context window management configuration."
        },
        "default_channel": {
          "default": "llm_notification",
          "description": "Default output channel for scheduled jobs and watchers.",
          "title": "Default Channel",
          "type": "string"
        },
        "direct_modules": {
          "description": "Module IDs whose actions are always injected as direct tools.",
          "items": {
            "type": "string"
          },
          "title": "Direct Modules",
          "type": "array"
        },
        "entry_agent": {
          "default": "",
          "description": "Agent to start with. Default: first agent in list.",
          "title": "Entry Agent",
          "type": "string"
        },
        "hooks": {
          "description": "Internal hooks evaluated during the agent loop.",
          "items": {
            "$ref": "#/$defs/HookConfig"
          },
          "title": "Hooks",
          "type": "array"
        },
        "input": {
          "$ref": "#/$defs/InputConfig",
          "description": "Input contract (one_shot mode)."
        },
        "max_concurrent_activations": {
          "default": 20,
          "description": "Max concurrent LLM calls when a broadcast trigger fires. Prevents rate limit storms.",
          "minimum": 1,
          "title": "Max Concurrent Activations",
          "type": "integer"
        },
        "max_sessions_per_user": {
          "default": 10,
          "description": "Max background sessions per user in multi mode (0 = unlimited). Ignored in mono mode.",
          "minimum": 0,
          "title": "Max Sessions Per User",
          "type": "integer"
        },
        "max_turns": {
          "default": 50,
          "description": "Maximum agent loop iterations (per turn for conversation, per activation for background). Must be >= 1.",
          "minimum": 1,
          "title": "Max Turns",
          "type": "integer"
        },
        "middleware": {
          "description": "App-level middleware pipeline that wraps every LLM call. Built-in: mask_secrets, prompt_inject, content_filter, rag_inject, response_filter.",
          "items": {
            "additionalProperties": true,
            "type": "object"
          },
          "title": "Middleware",
          "type": "array"
        },
        "mode": {
          "default": "conversation",
          "description": "Execution mode: 'conversation' (default, multi-turn chat), 'one_shot' (single input then stop), 'background' (trigger-driven), 'pipeline' (multi-app sequencing).",
          "enum": [
            "one_shot",
            "conversation",
            "background",
            "pipeline"
          ],
          "title": "Mode",
          "type": "string"
        },
        "modes": {
          "additionalProperties": {
            "$ref": "#/$defs/ModeDef"
          },
          "description": "Composer mode picker \u2014 keyed by mode id (`ask`, `plan`, `auto`, or anything app-specific). Each value is a sparse `ModeDef` that overrides the app's runtime / agent / tools / behavior config when the user picks that mode in the chat composer. Empty dict = no picker (client hides the pill). One entry = no picker either (no choice). Two or more = pill shown with the listed entries; the chat-store's `selectedMode` indexes into this dict at dispatch time.",
          "title": "Modes",
          "type": "object"
        },
        "output": {
          "$ref": "#/$defs/OutputConfig",
          "description": "Output contract (one_shot mode)."
        },
        "payload_schema": {
          "anyOf": [
            {
              "$ref": "#/$defs/PayloadSchemaConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Optional declarative schema for the per-session user payload. Only meaningful in mode=background."
        },
        "pipeline": {
          "description": "Pipeline of apps executed in sequence (mode=pipeline only). Each step calls a deployed app and pipes its output to the next step.",
          "items": {
            "$ref": "#/$defs/PipelineStep"
          },
          "title": "Pipeline",
          "type": "array"
        },
        "project_memory": {
          "default": "auto",
          "description": "Path to a project memory file loaded into the system prompt at startup. 'auto' scans for .digitorn.md / CLAUDE.md / README.md.",
          "title": "Project Memory",
          "type": "string"
        },
        "scheduler": {
          "default": false,
          "description": "Enable scheduler capabilities. Requires watchers=true.",
          "title": "Scheduler",
          "type": "boolean"
        },
        "session_mode": {
          "default": "mono",
          "description": "Background session mode: 'mono' (1 session per user, auto-created) or 'multi' (N sessions per user, created via API with custom params).",
          "enum": [
            "mono",
            "multi"
          ],
          "title": "Session Mode",
          "type": "string"
        },
        "timeout": {
          "default": 300.0,
          "description": "Timeout in seconds (per turn for conversation, per activation for background). Must be > 0.",
          "exclusiveMinimum": 0,
          "title": "Timeout",
          "type": "number"
        },
        "tool_injection": {
          "anyOf": [
            {
              "enum": [
                "direct",
                "compact_direct",
                "discovery"
              ],
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Force a specific tool injection mode. Auto-detected when None.",
          "title": "Tool Injection"
        },
        "triggers": {
          "description": "Triggers for background mode.",
          "items": {
            "$ref": "#/$defs/TriggerConfig"
          },
          "title": "Triggers",
          "type": "array"
        },
        "watchers": {
          "default": false,
          "description": "Enable persistent watcher capabilities.",
          "title": "Watchers",
          "type": "boolean"
        },
        "workdir": {
          "default": "",
          "description": "Working directory (formerly ``execution.workspace``). The filesystem path the app's modules operate within. Auto-indexed at startup. Supports {{variables}} and {{env.PWD}}. Renamed from `workspace` to disambiguate from `ui.workspace` (renderer).",
          "title": "Workdir",
          "type": "string"
        },
        "workdir_mode": {
          "default": "auto",
          "description": "Working-directory mode (formerly ``execution.workspace_mode``): 'none', 'required', 'fixed', or 'auto'.",
          "enum": [
            "none",
            "required",
            "fixed",
            "auto"
          ],
          "title": "Workdir Mode",
          "type": "string"
        }
      },
      "title": "RuntimeBlock",
      "type": "object"
    },
    "SandboxConfig": {
      "additionalProperties": false,
      "description": "OS-level sandbox configuration for per-session isolation.\n\nLevels (presets):\n    - off: no sandbox (current non-sandbox path)\n    - standard: Landlock + seccomp + cgroups (single worker)\n    - strict: + warm pool + user/PID namespaces + capability drop + MDWE\n    - maximum: + network namespace + seccomp-notify audit + workspace snapshot\n\nExample::\n\n    execution:\n      sandbox:\n        level: strict\n        pool_size: 4\n        namespaces: [user, pid, net]",
      "properties": {
        "allow_paths": {
          "description": "Additional filesystem paths the sandbox may access, beyond the workspace. Each entry is 'path' (read-only) or 'path:rw' (read-write). Supports {{variables}} and ~ for home directory. Example: ['/data/models', '~/datasets:rw', '/etc/myapp']",
          "items": {
            "type": "string"
          },
          "title": "Allow Paths",
          "type": "array"
        },
        "audit": {
          "default": false,
          "description": "Enable per-session audit trail (security event log).",
          "title": "Audit",
          "type": "boolean"
        },
        "idle_timeout": {
          "default": 300,
          "description": "Idle timeout in seconds before worker recycling.",
          "minimum": 30,
          "title": "Idle Timeout",
          "type": "integer"
        },
        "level": {
          "default": "standard",
          "description": "Sandbox level preset: 'off', 'standard', 'strict', or 'maximum'.",
          "enum": [
            "off",
            "standard",
            "strict",
            "maximum"
          ],
          "title": "Level",
          "type": "string"
        },
        "namespaces": {
          "description": "Linux namespaces to create: 'user', 'pid', 'net', 'mount'.",
          "items": {
            "type": "string"
          },
          "title": "Namespaces",
          "type": "array"
        },
        "pool_max": {
          "default": 8,
          "description": "Maximum workers under load (pool_size \u2264 pool_max).",
          "maximum": 64,
          "minimum": 1,
          "title": "Pool Max",
          "type": "integer"
        },
        "pool_size": {
          "default": 2,
          "description": "Number of pre-warmed workers in the pool.",
          "maximum": 32,
          "minimum": 1,
          "title": "Pool Size",
          "type": "integer"
        },
        "resources": {
          "additionalProperties": true,
          "description": "Per-worker resource limits. Keys: 'memory' (e.g. '512MB'), 'cpu' (cores), 'processes' (max PIDs).",
          "title": "Resources",
          "type": "object"
        },
        "session_timeout": {
          "default": 3600,
          "description": "Maximum session duration in seconds before auto-termination.",
          "minimum": 60,
          "title": "Session Timeout",
          "type": "integer"
        },
        "workspace_snapshot": {
          "default": false,
          "description": "Enable CoW workspace snapshots per session.",
          "title": "Workspace Snapshot",
          "type": "boolean"
        }
      },
      "title": "SandboxConfig",
      "type": "object"
    },
    "SecurityBlock": {
      "additionalProperties": false,
      "description": "Runtime security boundaries: behavioral rules, OS sandbox, secret vault.\n\n`behavior` describes per-tool checks the daemon enforces at\nruntime. `sandbox` is the OS-level isolation layer (Landlock /\nseccomp / namespaces). `credentials_schema` declares every external\nsecret the app needs.",
      "properties": {
        "behavior": {
          "anyOf": [
            {
              "$ref": "#/$defs/BehaviorConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Behavioral enforcement rules (preset profile + custom). Actively monitored at runtime."
        },
        "credentials_schema": {
          "anyOf": [
            {
              "$ref": "#/$defs/CredentialsSchemaConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Declarative credentials schema. Declares every external service (OpenAI, Notion OAuth, Slack, MCP servers, ...) the app needs."
        },
        "sandbox": {
          "anyOf": [
            {
              "$ref": "#/$defs/SandboxConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "OS-level sandbox configuration (Landlock + seccomp + namespaces). Use 'level' presets or fine-tune."
        }
      },
      "title": "SecurityBlock",
      "type": "object"
    },
    "SetupStep": {
      "additionalProperties": false,
      "description": "A single action call to execute during app bootstrap.\n\nMaps directly to ``module.execute(action, params)``.\nThe ``params`` dict is validated at compile time against the action's\n``params_model`` (Pydantic JSON Schema).",
      "properties": {
        "action": {
          "description": "Action name on the target module.",
          "title": "Action",
          "type": "string"
        },
        "params": {
          "additionalProperties": true,
          "description": "Parameters for the action. May contain {{variables}}.",
          "title": "Params",
          "type": "object"
        }
      },
      "required": [
        "action"
      ],
      "title": "SetupStep",
      "type": "object"
    },
    "SkillEntry": {
      "additionalProperties": false,
      "description": "One entry in the app-level ``skills:`` list.\n\nSkills are reusable command files (.md) the agent can invoke via\na slash command. The compiler reads the file at compile time and\nsurfaces the content to the agent via the slash-command palette.",
      "properties": {
        "command": {
          "description": "Slash command id (e.g. '/commit', '/review').",
          "minLength": 1,
          "title": "Command",
          "type": "string"
        },
        "description": {
          "default": "",
          "description": "One-line description shown in the command palette.",
          "title": "Description",
          "type": "string"
        },
        "path": {
          "description": "Path to the .md file relative to the bundle dir.",
          "minLength": 1,
          "title": "Path",
          "type": "string"
        }
      },
      "required": [
        "command",
        "path"
      ],
      "title": "SkillEntry",
      "type": "object"
    },
    "SlashCommand": {
      "additionalProperties": true,
      "description": "One entry in the app-level ``slash_commands:`` list.\n\nDistinct from ``skills:`` in that slash commands are pure client-side\nUI: they render in the / palette but DO NOT load a markdown file.\nThe ``template`` is filled with form values and sent to the agent\nas a normal message.\n\nFuture-proofed with ``extra: allow`` to keep the contract evolvable.",
      "properties": {
        "command": {
          "description": "Slash command id (e.g. '/deploy', '/restart').",
          "minLength": 1,
          "title": "Command",
          "type": "string"
        },
        "description": {
          "default": "",
          "description": "One-line description shown in the / palette.",
          "title": "Description",
          "type": "string"
        },
        "template": {
          "default": "",
          "description": "Optional message template with {{var}} placeholders.",
          "title": "Template",
          "type": "string"
        }
      },
      "required": [
        "command"
      ],
      "title": "SlashCommand",
      "type": "object"
    },
    "SlotEntry": {
      "additionalProperties": true,
      "description": "One slot placement: which widget kind and what to render.\n\nPhase 1 (2026-05-04) supports a single ``kind: inline`` that\nreferences an entry from ``ui.widgets.inline`` by name. Phase 4\nwill add ``chart``, ``data_table``, ``iframe`` as native kinds\nso a slot can carry a primitive without going through\n``inline``. The ``extra: allow`` policy keeps the contract\nforward-compatible - unknown kind-specific fields stay on the\npayload and reach the client untouched.",
      "properties": {
        "kind": {
          "default": "inline",
          "description": "Renderer for this slot. Phase 1 supports ``inline`` (reference to ``ui.widgets.inline.<ref>``).",
          "title": "Kind",
          "type": "string"
        },
        "ref": {
          "default": "",
          "description": "Name of the inline widget to render (must exist in ``ui.widgets.inline``). Required when ``kind: inline``.",
          "title": "Ref",
          "type": "string"
        }
      },
      "title": "SlotEntry",
      "type": "object"
    },
    "SlotsConfig": {
      "additionalProperties": false,
      "description": "Per-app placement of inline widgets in the chat surface.\n\nGeneralises the legacy ``ui.widgets.chat_side`` (single\nright-side panel) to five named placements the YAML can fill\nindependently:\n\n- ``header``: floating overlay at the top of the chat panel\n  (top-right). Does NOT take vertical layout space.\n- ``sidebar_left`` / ``sidebar_right``: left/right of the\n  message list (inside the chat panel - distinct from the\n  global workspace splitter).\n- ``footer_left``: REPLACES the workspace-path chip in the\n  ``StatusLine`` row below the composer. Renders inline at\n  the left edge of that row, no extra vertical space.\n- ``footer_right``: REPLACES the model chip in the same\n  ``StatusLine`` row, pinned to the far-right edge.\n\nThe footer pair is the \"no-extra-row\" override mechanism:\ninstead of adding a new line below the composer (which users\nrejected as wasted vertical space), the YAML can hijack the\ntwo existing chips that already live there - workspace path\non the left, model name on the right - and substitute its\nown widget. Set neither and the StatusLine is unchanged.\n\nNo ``above_composer`` slot: action rows between the message\nlist and the composer were rejected as visually competing\nwith both the chat scroll area and the composer itself.\nApps that want pre-composer affordances should use\n``header`` (overlay) or the upcoming Phase-2\n``message_actions`` (per-message buttons) instead.\n\nEach slot is optional; omitted slots stay empty so existing\napps without a ``ui.slots`` block keep their historical\nlayout. The slot system is the architectural foundation that\nPhase 2 (``message_actions``) and Phase 3 (``tool_renderers``)\nbuild on.",
      "properties": {
        "footer_left": {
          "anyOf": [
            {
              "$ref": "#/$defs/SlotEntry"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "footer_right": {
          "anyOf": [
            {
              "$ref": "#/$defs/SlotEntry"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "header": {
          "anyOf": [
            {
              "$ref": "#/$defs/SlotEntry"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "sidebar_left": {
          "anyOf": [
            {
              "$ref": "#/$defs/SlotEntry"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "sidebar_right": {
          "anyOf": [
            {
              "$ref": "#/$defs/SlotEntry"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        }
      },
      "title": "SlotsConfig",
      "type": "object"
    },
    "StateTrackingConfig": {
      "additionalProperties": false,
      "description": "Configure what the session state tracks - fully declarative.\n\nExample::\n\n    state_tracking:\n      sets:\n        read_files:\n          add_on: [read, filesystem.read]\n          target: file_path\n        fetched_urls:\n          add_on: [web.fetch]\n          target: url\n      counters:\n        changes_since_test:\n          increment_on: [edit, write]\n          reset_on: [bash]\n          reset_when:\n            tool: bash\n            param: command\n            matches: \"pytest|npm test\"\n      flags:\n        has_web_searched:\n          set_on: [web.search, search]",
      "properties": {
        "counters": {
          "additionalProperties": {
            "$ref": "#/$defs/StateTrackingCounterConfig"
          },
          "title": "Counters",
          "type": "object"
        },
        "flags": {
          "additionalProperties": {
            "$ref": "#/$defs/StateTrackingFlagConfig"
          },
          "title": "Flags",
          "type": "object"
        },
        "sets": {
          "additionalProperties": {
            "$ref": "#/$defs/StateTrackingSetConfig"
          },
          "title": "Sets",
          "type": "object"
        }
      },
      "title": "StateTrackingConfig",
      "type": "object"
    },
    "StateTrackingCounterConfig": {
      "additionalProperties": false,
      "description": "Configure a named counter.",
      "properties": {
        "increment_on": {
          "description": "Tool names that increment this counter.",
          "items": {
            "type": "string"
          },
          "title": "Increment On",
          "type": "array"
        },
        "reset_on": {
          "description": "Tool names that reset this counter to 0.",
          "items": {
            "type": "string"
          },
          "title": "Reset On",
          "type": "array"
        },
        "reset_when": {
          "additionalProperties": {
            "type": "string"
          },
          "description": "Reset when a param matches: {tool, param, matches}.",
          "title": "Reset When",
          "type": "object"
        }
      },
      "title": "StateTrackingCounterConfig",
      "type": "object"
    },
    "StateTrackingFlagConfig": {
      "additionalProperties": false,
      "description": "Configure a named boolean flag.",
      "properties": {
        "set_on": {
          "description": "Tool names that set this flag to True.",
          "items": {
            "type": "string"
          },
          "title": "Set On",
          "type": "array"
        },
        "unset_on": {
          "description": "Tool names that set this flag to False.",
          "items": {
            "type": "string"
          },
          "title": "Unset On",
          "type": "array"
        }
      },
      "title": "StateTrackingFlagConfig",
      "type": "object"
    },
    "StateTrackingSetConfig": {
      "additionalProperties": false,
      "description": "Configure a named set that tracks targets per tool.",
      "properties": {
        "add_on": {
          "description": "Tool names that add to this set.",
          "items": {
            "type": "string"
          },
          "title": "Add On",
          "type": "array"
        },
        "aliases": {
          "description": "Alternative param names (path, filepath, etc.).",
          "items": {
            "type": "string"
          },
          "title": "Aliases",
          "type": "array"
        },
        "target": {
          "default": "file_path",
          "description": "Param name to extract as the target value.",
          "title": "Target",
          "type": "string"
        }
      },
      "required": [
        "add_on"
      ],
      "title": "StateTrackingSetConfig",
      "type": "object"
    },
    "TerminalNode": {
      "additionalProperties": false,
      "description": "End of a flow path. Carries an optional output payload.\n\nTerminal nodes typically have empty ``routes`` (the flow stops here).\nIf they do declare routes, they're treated as a sub-flow continuation\npoint useful for subflow node returns - but the runtime treats the\npath as ended for the caller's purposes.",
      "properties": {
        "description": {
          "default": "",
          "description": "Free-form description, surfaced on the canvas tooltip.",
          "title": "Description",
          "type": "string"
        },
        "id": {
          "description": "Unique node identifier within the flow.",
          "title": "Id",
          "type": "string"
        },
        "on_error": {
          "description": "Error-handling edges. Catch-all (default: true) must be last.",
          "items": {
            "$ref": "#/$defs/FlowOnErrorRoute"
          },
          "title": "On Error",
          "type": "array"
        },
        "output": {
          "additionalProperties": true,
          "description": "Final output payload returned by this flow path.",
          "title": "Output",
          "type": "object"
        },
        "routes": {
          "description": "Outgoing edges. Top-to-bottom evaluation order.",
          "items": {
            "$ref": "#/$defs/FlowRoute"
          },
          "title": "Routes",
          "type": "array"
        },
        "type": {
          "const": "terminal",
          "title": "Type",
          "type": "string"
        }
      },
      "required": [
        "id",
        "type"
      ],
      "title": "TerminalNode",
      "type": "object"
    },
    "ToolNode": {
      "additionalProperties": false,
      "description": "Direct tool invocation, no LLM in the loop.\n\nThe ``tool`` field must be a ``module.action`` FQN that resolves to\na real action of a declared module.",
      "properties": {
        "description": {
          "default": "",
          "description": "Free-form description, surfaced on the canvas tooltip.",
          "title": "Description",
          "type": "string"
        },
        "id": {
          "description": "Unique node identifier within the flow.",
          "title": "Id",
          "type": "string"
        },
        "on_error": {
          "description": "Error-handling edges. Catch-all (default: true) must be last.",
          "items": {
            "$ref": "#/$defs/FlowOnErrorRoute"
          },
          "title": "On Error",
          "type": "array"
        },
        "params": {
          "additionalProperties": true,
          "description": "Parameters passed to the tool. Supports {{templates}}.",
          "title": "Params",
          "type": "object"
        },
        "routes": {
          "description": "Outgoing edges. Top-to-bottom evaluation order.",
          "items": {
            "$ref": "#/$defs/FlowRoute"
          },
          "title": "Routes",
          "type": "array"
        },
        "tool": {
          "description": "Tool FQN, e.g. 'web.search' or 'http.post'.",
          "title": "Tool",
          "type": "string"
        },
        "type": {
          "const": "tool",
          "title": "Type",
          "type": "string"
        }
      },
      "required": [
        "id",
        "type",
        "tool"
      ],
      "title": "ToolNode",
      "type": "object"
    },
    "ToolRendererEntry": {
      "additionalProperties": false,
      "description": "A single ``tool_renderers`` mapping entry.\n\nTwo ways to bind the renderer to a tool:\n\n* ``ref``: name of an entry in ``ui.widgets.inline``. The widget\n  tree there is rendered with the ``tool.*`` bindings injected.\n\nFuture fields (Phase 3.1):\n\n* ``inline_tree``: literal widget tree inline at this site\n  instead of pointing at ``ui.widgets.inline``. Convenient when\n  the tree is small and doesn't need reuse.",
      "properties": {
        "ref": {
          "description": "Name of an entry in ``ui.widgets.inline`` to render for this tool. The widget tree receives ``{{tool.*}}`` bindings (name, params, result, error, duration_ms, status) at render time.",
          "title": "Ref",
          "type": "string"
        }
      },
      "required": [
        "ref"
      ],
      "title": "ToolRendererEntry",
      "type": "object"
    },
    "ToolRenderersBlock": {
      "additionalProperties": false,
      "description": "Top-level ``ui.tool_renderers`` block.\n\nMaps tool names (and optional regex patterns) to inline-widget\nrefs. The dispatcher in each client checks ``by_name`` first\n(exact match, fastest), then ``by_pattern`` (re.search), then\nfalls back to the legacy tool chip when neither matches.\n\nDisabled by default - the block being PRESENT and ``enabled``\nbeing truthy is what flips the dispatcher to consult these\nmappings. Apps without the block keep their historical chip\nrendering with zero behaviour change.",
      "properties": {
        "by_name": {
          "additionalProperties": {
            "$ref": "#/$defs/ToolRendererEntry"
          },
          "description": "Exact-match map from tool name (short, e.g. ``WsRead``) to a renderer entry. Checked first; an exact hit short-circuits the pattern lookup.",
          "title": "By Name",
          "type": "object"
        },
        "by_pattern": {
          "additionalProperties": {
            "$ref": "#/$defs/ToolRendererEntry"
          },
          "description": "Regex-match map. Each key is a Python re.search-style pattern (``^memory\\..+`` etc.) tested against the tool name in iteration order. The first match wins. Use this for grouping (``filesystem.*`` \u2192 one renderer for all filesystem tools) without listing each tool by name.",
          "title": "By Pattern",
          "type": "object"
        },
        "enabled": {
          "default": false,
          "description": "Master toggle. When false (default), every tool call renders with the legacy chip and ``by_name`` / ``by_pattern`` are ignored. Flipping the flag turns the client dispatcher on - good for staged rollout: ship a renderer config but keep ``enabled: false`` until QA signs off.",
          "title": "Enabled",
          "type": "boolean"
        },
        "fallback_on_error": {
          "default": true,
          "description": "When the matched renderer throws / fails to mount, fall back to the legacy chip instead of showing a broken widget. Set false during local renderer dev to surface the failure inline.",
          "title": "Fallback On Error",
          "type": "boolean"
        }
      },
      "title": "ToolRenderersBlock",
      "type": "object"
    },
    "ToolsBlock": {
      "additionalProperties": false,
      "description": "What the agent can call: modules, capabilities (grant/deny), output channels.\n\nModules expose actions; capabilities filter which actions are\ncallable (auto / approve / deny); channels are the typed output\nsurfaces (slack, email, webhook, ...) that triggers and the\nscheduler can deliver to.",
      "properties": {
        "capabilities": {
          "anyOf": [
            {
              "$ref": "#/$defs/CapabilitiesConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Application security capabilities (grant/deny). Absent = dev/test mode (no enforcement)."
        },
        "channels": {
          "additionalProperties": {
            "$ref": "#/$defs/ChannelInstanceConfig"
          },
          "description": "Named output channel instances. Keys are instance names (e.g. 'slack_alerts', 'email_reports').",
          "title": "Channels",
          "type": "object"
        },
        "modules": {
          "additionalProperties": {
            "$ref": "#/$defs/ModuleBlock"
          },
          "description": "Per-module configuration. Keys are module IDs.",
          "title": "Modules",
          "type": "object"
        }
      },
      "title": "ToolsBlock",
      "type": "object"
    },
    "TriggerConfig": {
      "additionalProperties": false,
      "description": "A trigger for background mode.\n\nExample::\n\n    triggers:\n      - id: new_csv\n        type: watch\n        paths: [\"./inbox/*.csv\"]\n        message: \"New file: {{event.path}}\"",
      "properties": {
        "id": {
          "description": "Unique trigger identifier.",
          "title": "Id",
          "type": "string"
        },
        "message": {
          "default": "",
          "description": "Message template sent to the agent. Supports {{event.*}}.",
          "title": "Message",
          "type": "string"
        },
        "method": {
          "default": "POST",
          "description": "HTTP method (http type only).",
          "enum": [
            "GET",
            "POST",
            "PUT",
            "DELETE",
            "PATCH",
            "HEAD",
            "OPTIONS"
          ],
          "title": "Method",
          "type": "string"
        },
        "path": {
          "default": "",
          "description": "HTTP endpoint path (http type only).",
          "title": "Path",
          "type": "string"
        },
        "paths": {
          "description": "Glob patterns to watch (watch type only).",
          "items": {
            "type": "string"
          },
          "title": "Paths",
          "type": "array"
        },
        "port": {
          "default": 9100,
          "description": "Port for HTTP trigger listener (default 9100).",
          "maximum": 65535,
          "minimum": 1024,
          "title": "Port",
          "type": "integer"
        },
        "routing": {
          "default": "broadcast",
          "description": "How this trigger routes to sessions: 'broadcast' (all active sessions), 'user' (all sessions of the identified user), 'session' (one specific session).",
          "title": "Routing",
          "type": "string"
        },
        "routing_key": {
          "default": "",
          "description": "Template to extract the routing identifier from the event payload. For routing='user': identifies which user (e.g. '{{event.chat_id}}'). For routing='session': identifies which session (e.g. '{{event.header.X-Session-Id}}').",
          "title": "Routing Key",
          "type": "string"
        },
        "schedule": {
          "default": "",
          "description": "Cron expression (cron type only).",
          "title": "Schedule",
          "type": "string"
        },
        "type": {
          "description": "Trigger type: 'cron', 'watch', 'http'.",
          "title": "Type",
          "type": "string"
        }
      },
      "required": [
        "id",
        "type"
      ],
      "title": "TriggerConfig",
      "type": "object"
    },
    "UIBlock": {
      "additionalProperties": false,
      "description": "How the client renders the app: pure display, daemon never reads.\n\nHolds the client manifest extensions. Theme, feature\ntoggles, declarative widgets, the workspace renderer, slash\ncommands, quick prompts, and the welcome greeting.",
      "properties": {
        "activity": {
          "anyOf": [
            {
              "$ref": "#/$defs/ActivityPanelBlock"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Opt-in Activity pane: live sub-agent fan-out + recent terminal events + aggregate stats. The block being PRESENT in YAML is the gate \u2014 both clients hide the Activity workspace mode entry when this field is null. Simple chat apps that never spawn sub-agents leave it off. Coordinator / multi-agent / dev-assistant apps opt in."
        },
        "composer": {
          "anyOf": [
            {
              "$ref": "#/$defs/ChatComposerBlock"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Composer toolbar config (file upload, voice, slash, quick prompts). Wins over the legacy ``features.X`` boolean keys for the same concepts."
        },
        "density": {
          "default": "comfortable",
          "description": "Bubble spacing density: compact | comfortable.",
          "title": "Density",
          "type": "string"
        },
        "features": {
          "additionalProperties": {
            "type": "boolean"
          },
          "description": "UI feature toggles consumed by the client. Missing keys default to true (feature visible).",
          "title": "Features",
          "type": "object"
        },
        "greeting": {
          "default": "",
          "description": "Welcome message displayed by the client at conversation start. Pure display - the daemon never reads it. Lifted from execution.greeting.",
          "title": "Greeting",
          "type": "string"
        },
        "layout": {
          "default": "default",
          "description": "High-level chat preset that sets sensible defaults for every other block: default | code | builder | research | minimal | lovable. Sub-blocks override individual flags.",
          "title": "Layout",
          "type": "string"
        },
        "message_actions": {
          "anyOf": [
            {
              "$ref": "#/$defs/MessageActionsBlock"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Phase-2 dispatcher mounting a custom widget tree UNDER each chat message that matches one of the rules. When the block is absent or ``enabled: false``, no extra row renders under messages - opt-in only. The widget tree receives ``{{message.role}}``, ``{{message.id}}``, ``{{message.text}}``, ``{{message.has_tools}}``, ``{{message.tools}}``, ``{{message.first_tool}}``, ``{{message.tool_status}}`` bindings."
        },
        "quick_prompts": {
          "description": "Suggested prompts shown to the user when the app loads.",
          "items": {
            "$ref": "#/$defs/QuickPrompt"
          },
          "title": "Quick Prompts",
          "type": "array"
        },
        "slash_commands": {
          "description": "Custom /slash palette entries rendered by the client.",
          "items": {
            "$ref": "#/$defs/SlashCommand"
          },
          "title": "Slash Commands",
          "type": "array"
        },
        "slots": {
          "anyOf": [
            {
              "$ref": "#/$defs/SlotsConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Per-app placement of inline widgets in the chat surface (header, sidebar_left, sidebar_right, above_composer, footer). Each slot references an entry from ``ui.widgets.inline`` by name. The architectural foundation for Phase 2 message actions and Phase 3 tool renderers."
        },
        "theme": {
          "additionalProperties": {
            "type": "string"
          },
          "description": "Client theme override map. Keys: accent (hex), background (hex, client-reserved).",
          "title": "Theme",
          "type": "object"
        },
        "thinking": {
          "anyOf": [
            {
              "$ref": "#/$defs/ChatThinkingBlock"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Visibility / collapse defaults for ``<thinking>`` blocks in assistant messages."
        },
        "tool_calls": {
          "anyOf": [
            {
              "$ref": "#/$defs/ChatToolCallsBlock"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Visibility / collapse defaults for tool-call chips. ``show_silent`` controls plumbing-tool rendering."
        },
        "tool_renderers": {
          "anyOf": [
            {
              "$ref": "#/$defs/ToolRenderersBlock"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Phase-3 dispatcher mapping tool names (or regex patterns) to inline-widget refs. When the block is absent or ``enabled: false``, every tool call uses the client's legacy chip - opt-in only. The widget tree receives ``{{tool.name}}``, ``{{tool.params.X}}``, ``{{tool.result.X}}``, ``{{tool.error}}``, ``{{tool.duration_ms}}``, ``{{tool.status}}`` bindings."
        },
        "visual": {
          "anyOf": [
            {
              "$ref": "#/$defs/ChatVisualBlock"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Bubble accent / style / alignment overrides. ``accent`` wins over ``theme.accent`` and ``app.color`` when set."
        },
        "widgets": {
          "anyOf": [
            {
              "$ref": "#/$defs/WidgetsConfig"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Declarative UI widgets rendered by the client."
        },
        "workspace": {
          "anyOf": [
            {
              "$ref": "#/$defs/WorkspaceBlock"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Workspace renderer config: render_mode + entry_file + title. Tells the client THIS app uses a virtual file workspace streamed via Socket.IO. Distinct from runtime.workdir (the FS path)."
        }
      },
      "title": "UIBlock",
      "type": "object"
    },
    "UserResolverConfig": {
      "additionalProperties": false,
      "description": "Configuration for auto-resolving user-specific delivery targets.\n\nWhen a channel delivers a notification, the resolver automatically\nlooks up the user's contact info (email, phone, chat_id, etc.) from\na data source, using the session_id to identify who the user is.\n\nThis works like authentication middleware: the system knows who the\nuser is and adapts. One app serves 10,000 users - no per-user\nconfiguration needed.\n\nExample::\n\n    user_resolver:\n      module: database\n      action: fetch_results\n      params:\n        query: \"SELECT phone, email FROM users WHERE session_id = :session_id\"\n      mapping:\n        to_number: phone\n        to_address: email\n      cache_ttl: 300",
      "properties": {
        "action": {
          "description": "Action to call on the module (e.g. 'fetch_results', 'get'). The action should return user-specific data.",
          "title": "Action",
          "type": "string"
        },
        "cache_ttl": {
          "default": 300.0,
          "description": "How long to cache resolved results in seconds. 0 = no cache. Default: 300 (5 min).",
          "minimum": 0,
          "title": "Cache Ttl",
          "type": "number"
        },
        "mapping": {
          "additionalProperties": {
            "type": "string"
          },
          "description": "Maps result field names to per-delivery config field names. e.g. {'to_number': 'phone'} means: take the 'phone' column from the query result and pass it as the channel's 'to_number'.",
          "title": "Mapping",
          "type": "object"
        },
        "module": {
          "description": "Module ID to query for user info (e.g. 'database', 'http'). Must be declared in the app's modules: block.",
          "title": "Module",
          "type": "string"
        },
        "params": {
          "additionalProperties": true,
          "description": "Parameters for the action. Use ':session_id' or '{{session_id}}' as a placeholder - it will be replaced with the actual session ID at delivery time.",
          "title": "Params",
          "type": "object"
        }
      },
      "required": [
        "module",
        "action"
      ],
      "title": "UserResolverConfig",
      "type": "object"
    },
    "WidgetNode": {
      "additionalProperties": true,
      "description": "Recursive widget tree node - every primitive shares this base.\n\nPydantic refuses extra fields globally, BUT each primitive needs\nits own keys (``items`` for list, ``rows`` for table, ``children``\nfor column/row, etc.). Rather than declare 30 strict subclasses\nwe use a permissive shape and validate the per-primitive contract\nin :func:`digitorn.core.app.compiler._validate_widget_tree`.",
      "properties": {
        "accent": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Accent"
        },
        "as": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "As"
        },
        "body": {
          "anyOf": [
            {
              "$ref": "#/$defs/WidgetNode"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "children": {
          "anyOf": [
            {
              "items": {
                "$ref": "#/$defs/WidgetNode"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Children"
        },
        "density": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Density"
        },
        "empty": {
          "anyOf": [
            {
              "$ref": "#/$defs/WidgetNode"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "first": {
          "anyOf": [
            {
              "$ref": "#/$defs/WidgetNode"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "for": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "For"
        },
        "hidden": {
          "anyOf": [
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Hidden"
        },
        "id": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Id"
        },
        "item": {
          "anyOf": [
            {
              "$ref": "#/$defs/WidgetNode"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "key": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Key"
        },
        "loading": {
          "anyOf": [
            {
              "$ref": "#/$defs/WidgetNode"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "render": {
          "anyOf": [
            {
              "$ref": "#/$defs/WidgetNode"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "reset": {
          "anyOf": [
            {
              "additionalProperties": true,
              "type": "object"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Reset"
        },
        "second": {
          "anyOf": [
            {
              "$ref": "#/$defs/WidgetNode"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "submit": {
          "anyOf": [
            {
              "additionalProperties": true,
              "type": "object"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Submit"
        },
        "type": {
          "description": "Primitive name - must be in WIDGET_PRIMITIVES.",
          "title": "Type",
          "type": "string"
        },
        "when": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Conditional render expression.",
          "title": "When"
        }
      },
      "required": [
        "type"
      ],
      "title": "WidgetNode",
      "type": "object"
    },
    "WidgetsConfig": {
      "additionalProperties": false,
      "description": "Top-level ``widgets:`` block in app.yaml.\n\nStructure mirrors the client spec v1: one optional chat_side\npanel, an array of workspace_tabs, a dict of named modals, and a\ndict of named inline widgets that the agent can push via\n``widget.render`` with a ``ref:``.\n\nExternal widget files under ``./widgets/*.yaml`` in the bundle\ndir are loaded by the compiler and merged into the ``inline``\nmap (keyed by file stem) - same pattern as skills.",
      "properties": {
        "chat_side": {
          "anyOf": [
            {
              "$ref": "#/$defs/ChatSideWidget"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        },
        "inline": {
          "additionalProperties": {
            "$ref": "#/$defs/InlineWidget"
          },
          "title": "Inline",
          "type": "object"
        },
        "modals": {
          "additionalProperties": {
            "$ref": "#/$defs/ModalWidget"
          },
          "title": "Modals",
          "type": "object"
        },
        "version": {
          "default": 1,
          "description": "Spec version. Daemon refuses unknown versions.",
          "title": "Version",
          "type": "integer"
        },
        "workspace_tabs": {
          "items": {
            "$ref": "#/$defs/WorkspaceTabWidget"
          },
          "title": "Workspace Tabs",
          "type": "array"
        }
      },
      "title": "WidgetsConfig",
      "type": "object"
    },
    "WorkspaceBlock": {
      "additionalProperties": false,
      "description": "Top-level ``workspace:`` block in app.yaml.\n\nTells the client this app uses a virtual file workspace streamed\nvia Socket.IO.  The daemon emits ``preview:state_changed`` with\n``key: \"workspace\"`` on the first file write, carrying these values\nso the client can pick the correct renderer.\n\nExample YAML::\n\n    workspace:\n      render_mode: react\n      entry_file: src/App.tsx\n      title: \"My App\"\n      position: right\n      width_pct: 60\n      auto_open_on_first_tool: true",
      "properties": {
        "auto_open_on_first_tool": {
          "default": true,
          "description": "When true (default), the client opens the workspace pane the first time the agent writes a file or emits a workbench_* event (Lovable-style). Set to ``false`` to keep the workspace closed unless the user opens it manually - useful for chat-only apps that should not surface a renderer just because a tool wrote one log.",
          "title": "Auto Open On First Tool",
          "type": "boolean"
        },
        "entry_file": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Main file the client opens by default in the preview (e.g. src/App.tsx, index.html, main.tex). If omitted, a render_mode-specific default is used.",
          "title": "Entry File"
        },
        "position": {
          "default": "right",
          "description": "Where the workspace sits relative to the chat: right | bottom | hidden | overlay.",
          "title": "Position",
          "type": "string"
        },
        "render_mode": {
          "default": "auto",
          "description": "How the client should render workspace files. Values: react, html, markdown, slides, code, latex, builder, auto. When 'auto', the daemon detects from the first file written.",
          "title": "Render Mode",
          "type": "string"
        },
        "title": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Optional title shown in the workspace toolbar.",
          "title": "Title"
        },
        "width_pct": {
          "default": 50,
          "description": "Workspace pane width as a percentage of the available split (10..90). Ignored when ``position`` is ``hidden`` / ``overlay``.",
          "maximum": 90,
          "minimum": 10,
          "title": "Width Pct",
          "type": "integer"
        }
      },
      "title": "WorkspaceBlock",
      "type": "object"
    },
    "WorkspaceTabWidget": {
      "additionalProperties": false,
      "description": "Z3 - one tab in the workspace 'Widgets' container.",
      "properties": {
        "accent": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Accent"
        },
        "data": {
          "additionalProperties": true,
          "title": "Data",
          "type": "object"
        },
        "density": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Density"
        },
        "icon": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Icon"
        },
        "id": {
          "title": "Id",
          "type": "string"
        },
        "title": {
          "title": "Title",
          "type": "string"
        },
        "tree": {
          "$ref": "#/$defs/WidgetNode"
        }
      },
      "required": [
        "id",
        "title",
        "tree"
      ],
      "title": "WorkspaceTabWidget",
      "type": "object"
    }
  },
  "$id": "https://digitorn.ai/schema/v1.json",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "additionalProperties": false,
  "description": "Schema for a Digitorn application. Wire it into your editor by adding this comment as the first line of your app.yaml:\n\n    # yaml-language-server: $schema=https://digitorn.ai/schema/v1.json\n\nVSCode + the YAML extension by Red Hat will then auto-complete every field, surface inline error messages, and document each value on hover.",
  "properties": {
    "agents": {
      "description": "Agent definitions. Each agent has a brain (LLM config) and role.",
      "items": {
        "$ref": "#/$defs/AgentDefinition"
      },
      "title": "Agents",
      "type": "array"
    },
    "app": {
      "$ref": "#/$defs/AppMeta",
      "description": "Application identity (id, name, version, icon, color, ...)."
    },
    "dev": {
      "$ref": "#/$defs/DevBlock",
      "description": "Developer affordances: skills + variables + include (fragmentation)."
    },
    "flow": {
      "anyOf": [
        {
          "$ref": "#/$defs/FlowConfig"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Optional declarative orchestration graph for multi-agent apps. A top-level block (NOT under runtime) because flow changes how agents coordinate: explicit scenography instead of agents coordinating themselves via Agent() tool calls. Nodes are agent / tool / parallel / approval / decision / terminal; routes are conditional edges."
    },
    "runtime": {
      "$ref": "#/$defs/RuntimeBlock",
      "description": "Lifecycle + execution: mode, triggers, middleware, pipeline, hooks, context."
    },
    "schema_version": {
      "default": 2,
      "description": "Canonical schema version. v2 = 8 nested top-level blocks (``app``, ``runtime``, ``agents``, ``tools``, ``security``, ``ui``, ``dev``, ``flow``). v1 = legacy flat shape (``execution:``, ``modules:`` at top level, ...). The alias pass auto-detects when this field is absent. Setting it explicitly future-proofs the file against breaking changes.",
      "title": "Schema Version",
      "type": "integer"
    },
    "security": {
      "$ref": "#/$defs/SecurityBlock",
      "description": "Runtime boundaries: behavior + sandbox + credentials_schema."
    },
    "tools": {
      "$ref": "#/$defs/ToolsBlock",
      "description": "Modules + capabilities + channels. What the agent can call."
    },
    "ui": {
      "$ref": "#/$defs/UIBlock",
      "description": "Pure display layer: theme + features + widgets + workspace + preview + slash_commands + quick_prompts + greeting."
    }
  },
  "required": [
    "app"
  ],
  "title": "Digitorn app.yaml",
  "type": "object"
}
