{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://nanobpm.io/spec-app/nano-app.schema.json",
  "title": "AppManifest",
  "description": "Urban App manifest (nano.app.json) — the declared-data binding of an Urban RAD application (ADR 0027). Owns the envelope + cross-reference rules; each block's detail is owned by its ADR (data=0024, triggers=0025, surfaces=0026, security=0028, workers/llm=0022 §E). This file is the source of truth: TypeScript types are generated from it (scripts/generate-app-manifest.sh) and it doubles as the $schema editors use for nano.app.json autocompletion.",
  "type": "object",
  "additionalProperties": false,
  "required": ["schemaVersion", "id", "name"],
  "properties": {
    "$schema": {
      "type": "string",
      "description": "Optional editor hint pointing at this schema for autocompletion."
    },
    "schemaVersion": {
      "const": 1,
      "description": "Manifest schema version for forward-compat. Currently always 1."
    },
    "id": {
      "$ref": "#/$defs/slug",
      "description": "Stable App identifier (slug). Required."
    },
    "name": {
      "type": "string",
      "minLength": 1,
      "description": "Human-readable App name. Required."
    },
    "codename": {
      "type": "string",
      "description": "Informational codename, surfaced as App.CODENAME (ADR 0015). Optional."
    },
    "entrypoint": {
      "type": "string",
      "minLength": 1,
      "pattern": ".*\\S.*",
      "description": "App-declared entrypoint module the Studio supervisor runs/compiles, resolved relative to the app root (issue #957). Overrides the default `main.ts` convention so an app whose real entrypoint lives elsewhere (e.g. `src/main.ts`) is self-describing and runnable without a placeholder root shim. Precedence: an explicit project-config `main` (nanobpm.project.json) > this `entrypoint` > the `main.ts` default. Omit to keep the `main.ts` convention (zero regression)."
    },
    "runtime": { "$ref": "#/$defs/runtime" },
    "models": { "$ref": "#/$defs/models" },
    "data": { "$ref": "#/$defs/data" },
    "types": {
      "type": "object",
      "description": "The domain type registry (ADR 0029 §4, ADR 0031). Named record types keyed by a stable id — the *nominal* identity every reference resolves against. A type's fields project onto three shapes: form field (face), process variable (motion) and datasource row (rest); the Process-Relational Mapper (ADR 0031) generates the mapping. Types here are the transient/declared source; a datasource table is the other (ADR 0029 §4).",
      "additionalProperties": { "$ref": "#/$defs/domainType" }
    },
    "triggers": {
      "type": "array",
      "items": { "$ref": "#/$defs/trigger" },
      "description": "Event sources bound to engine actions (ADR 0025)."
    },
    "bindings": {
      "type": "array",
      "items": { "$ref": "#/$defs/binding" },
      "description": "Declares the domain type in scope for a model's FEEL (ADR 0029 §5): a form's default-value expressions and a decision's input expressions autocomplete + validate against the bound type's fields. The same 'typed reference replaces a free-string id' move as trigger.bodyType, applied to forms and decisions."
    },
    "instanceTracking": {
      "type": "array",
      "items": { "$ref": "#/$defs/instanceTracking" },
      "description": "Declarative process-instance lifecycle bindings. Each entry names a datasource table whose rows track a process instance by a key column; the runtime polls the engine and, when an instance reaches the TERMINATED state while its row is still marked active, applies the declared `onTerminated` patch. Reconciliation is scoped to TERMINATED only — never COMPLETED, whose terminal row-write is owned by the app's own completion (finalize) worker, so reconciling it here would race that worker and could clobber a legitimately-completed row. This closes rows whose instance ended with no completion worker running — an operator cancel/termination, a crash, or a fire-and-forget row-cancel — so a UI derived from the read model reflects the engine's real state."
    },
    "connections": {
      "type": "object",
      "description": "Named connections (credentials/endpoint) referenced by triggers/workers by id, so configs carry no inline secrets (ADR 0025 §1).",
      "additionalProperties": { "$ref": "#/$defs/connection" }
    },
    "surfaces": { "$ref": "#/$defs/surfaces" },
    "actions": {
      "type": "array",
      "items": { "$ref": "#/$defs/actionDecl" },
      "description": "App-authored action handler overrides (ADR 0055 §3): each binds a route to a handler module that wraps the generic pages start/cancel/message actions. Mounted before the generic routes, so an exact override shadows the generic one."
    },
    "api": { "$ref": "#/$defs/apiBinding" },
    "workers": {
      "type": "array",
      "items": { "$ref": "#/$defs/worker" },
      "description": "Service-task handlers: referenced files, an llm binding, or a connector supplied by an installed pack (ADR 0022 §E, ADR 0050)."
    },
    "externalTaskTypes": {
      "type": "array",
      "items": { "type": "string", "minLength": 1 },
      "uniqueItems": true,
      "description": "Service-task types that are serviced by an EXTERNAL worker (a coding-agent harness, an out-of-process fleet, another deployment) and are deliberately NOT hosted by this app. `urban gen` skips these when scaffolding write-once worker stubs, so they never appear as missing-stub drift or get auto-wired into `workers[]`. Declaring one is an explicit assertion that the app owns the model but not the handler (ADR 0056)."
    },
    "llm": {
      "type": "object",
      "description": "Named LLM bindings usable as workers or as a chat surface agent (ADR 0022 §E).",
      "additionalProperties": { "$ref": "#/$defs/llmBinding" }
    },
    "network": { "$ref": "#/$defs/network" },
    "security": { "$ref": "#/$defs/security" },
    "ui": { "$ref": "#/$defs/appUi" }
  },
  "$defs": {
    "appUi": {
      "type": "object",
      "additionalProperties": false,
      "description": "Console-integrated app view (ADR 0057, issue #638). When the app runs under the studio supervisor it appears in the left rail as a running app; a UI app embeds its own webview in the right pane, a headless app shows a control (status/logs/stop) pane. Omitting this block still lists the app (headless).",
      "properties": {
        "enabled": {
          "type": "boolean",
          "default": true,
          "description": "Opt in to an embedded UI. false ⇒ headless (control-only), but the app is still listed in the running-apps rail."
        },
        "port": {
          "type": "integer",
          "minimum": 1,
          "maximum": 65535,
          "description": "The integrated-UI port. Takes precedence over portEnv. Apps that expose multiple ports use this to name which one is the UI."
        },
        "portEnv": {
          "type": "string",
          "description": "Name of the env var the app reads its UI port from (e.g. \"PORT\"). The studio resolves it from the project run config so it can discover the port without allocating one."
        },
        "path": {
          "type": "string",
          "description": "Path the embedded webview opens (default \"/\")."
        },
        "icon": {
          "type": "string",
          "description": "Left-rail icon: either a bundled glyph name (e.g. \"workers\") resolved by the console, or a project-relative asset path the app ships itself (e.g. \"assets/icon.svg\"), served path-guarded and image-only from /console/app-view-icon/<project>. Display hint only; the console falls back to a default glyph when absent/invalid/unresolvable."
        },
        "label": {
          "type": "string",
          "description": "Left-rail label hint. Display only; the console disambiguates on the project name, since same-template apps share a manifest."
        }
      }
    },
    "slug": {
      "type": "string",
      "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
      "description": "Lowercase kebab-case slug."
    },
    "domainType": {
      "type": "object",
      "additionalProperties": false,
      "required": ["fields"],
      "description": "A named domain record type (ADR 0029 §4, ADR 0031). Its map key is the stable id; matching is nominal (by id), consistent with model reference pickers.",
      "properties": {
        "name": {
          "type": "string",
          "description": "Human-readable label. The map key remains the stable id every reference uses."
        },
        "match": {
          "enum": ["nominal", "structural"],
          "default": "nominal",
          "description": "Identity discipline. `nominal` (default): references resolve by this type's id. `structural` is a reserved escape hatch (match by field shape) — declared here but not yet honoured by the validator/mapper."
        },
        "table": {
          "$ref": "#/$defs/slug",
          "description": "Optional datasource table this type binds to as its rest projection (ADR 0031 rest bank). Absent = transient / non-persisted (ADR 0029 §4.2). Table existence is validated once the datasource schema() runtime (ADR 0024) lands; the shape is checked now."
        },
        "fields": {
          "type": "object",
          "minProperties": 1,
          "additionalProperties": { "$ref": "#/$defs/domainField" },
          "description": "Field name → field definition. Field names are the keys the form field, the variable path and the datasource column share (ADR 0029 §4)."
        }
      }
    },
    "domainField": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type"],
      "description": "A single field of a domain type.",
      "properties": {
        "type": {
          "description": "A primitive type, or the id of another domain type in the registry (nominal reference). Primitive ids take precedence over an identically named type.",
          "anyOf": [
            { "enum": ["string", "number", "integer", "boolean", "date", "datetime", "json"] },
            { "$ref": "#/$defs/slug" }
          ]
        },
        "optional": {
          "type": "boolean",
          "default": false,
          "description": "Whether the field may be absent."
        },
        "list": {
          "type": "boolean",
          "default": false,
          "description": "Whether the field is a list of `type` rather than a single value."
        }
      }
    },
    "envTemplate": {
      "type": "string",
      "pattern": "^\\$\\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\\}$",
      "description": "A ${VAR} or ${VAR:-default} boot-time substitution reference (ADR 0027 §5). Resolved at App boot / IDE Run, never persisted. The validator checks the reference shape, not the resolved value."
    },
    "feel": {
      "type": "string",
      "description": "A FEEL expression (conventionally prefixed with '='), evaluated over the trigger event body."
    },
    "runtime": {
      "type": "object",
      "additionalProperties": false,
      "description": "The shipping topology of the compiled App (ADR 0005). Distinct from the IDE dev-loop deployTarget, which lives in nanobpm.project.json (ADR 0027 §1).",
      "properties": {
        "engine": {
          "enum": ["embedded", "remote", "cluster"],
          "default": "embedded",
          "description": "How the App reaches the engine at runtime."
        },
        "node": {
          "enum": ["single", "cluster"],
          "default": "single"
        }
      }
    },
    "models": {
      "type": "object",
      "additionalProperties": false,
      "description": "OPTIONAL override of the deploy-by-convention layout (ADR 0062). By convention an app keeps its deployables under `resources/` (the deploy root): Urban walks `resources/` and infers content-types by extension, and codegen defaults its BPMN scan to `resources/**/*.bpmn` (and `.dmn`). Docs live outside `resources/`; the tree is one level deep by convention. Declare `models` only for advanced/non-standard layouts where the convention does not fit — each subkey then overrides the corresponding convention default. Each glob must resolve to at least one file (cross-reference rule, ADR 0027 §4).",
      "properties": {
        "processes": { "$ref": "#/$defs/globList", "description": "BPMN process globs. Overrides the convention default (`resources/**/*.bpmn`)." },
        "decisions": { "$ref": "#/$defs/globList", "description": "DMN decision globs. Overrides the convention default (`resources/**/*.dmn`)." },
        "forms": { "$ref": "#/$defs/globList", "description": "form-js form globs. Overrides the convention default (form files under `resources/`)." }
      }
    },
    "globList": {
      "type": "array",
      "items": { "type": "string", "minLength": 1 }
    },
    "data": {
      "type": "object",
      "additionalProperties": false,
      "required": ["sources"],
      "description": "Named datasources — the BDE-alias abstraction (ADR 0024). Consumers bind by name, never by driver, so the same bundle runs on SQLite in the IDE and Postgres in production by flipping env only.",
      "properties": {
        "default": {
          "type": "string",
          "description": "Name of the datasource used when a consumer names none."
        },
        "sources": {
          "type": "object",
          "minProperties": 1,
          "additionalProperties": { "$ref": "#/$defs/dataSource" }
        }
      }
    },
    "dataSource": {
      "type": "object",
      "additionalProperties": false,
      "required": ["driver", "url"],
      "properties": {
        "driver": {
          "description": "Driver id. May be an env template so deployment flips SQLite to Postgres without a source change (ADR 0024 §1).",
          "anyOf": [
            { "enum": ["sqlite", "postgres"] },
            { "$ref": "#/$defs/envTemplate" }
          ]
        },
        "url": {
          "type": "string",
          "description": "Connection URL, typically an env template (e.g. file:./app.db or ${NANO_APP_DB_URL:-file:./app.db})."
        },
        "migrations": {
          "type": "string",
          "description": "Path to a migrations directory."
        }
      }
    },
    "trigger": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "type", "action"],
      "properties": {
        "id": { "$ref": "#/$defs/slug" },
        "type": {
          "type": "string",
          "description": "Source kind. Core (in-binary): cron | webhook | file. Pack sources add imap, mqtt, cloud, … (ADR 0025 §1)."
        },
        "spec": {
          "type": "string",
          "description": "cron: the crontab spec (e.g. '0 6 * * *'). 5 fields, evaluated in UTC (ADR 0025 §2)."
        },
        "onMissed": {
          "type": "string",
          "enum": ["skip", "once", "all"],
          "default": "skip",
          "description": "cron catch-up policy for fires missed while the App was down (ADR 0025 §Open questions): skip them, fire once for the whole span, or enqueue every missed instant (dedup keys keep it idempotent)."
        },
        "config": {
          "type": "object",
          "description": "Source-kind-specific settings. Core: file may set { pollMs }. Pack sources (nano-ide-trigger-*, ADR 0025 §6) read their declared config fields from here.",
          "additionalProperties": true
        },
        "path": {
          "type": "string",
          "description": "webhook: the HTTP path served on the App backend (e.g. /hooks/temp)."
        },
        "connection": {
          "type": "string",
          "description": "Name of a connections[] entry supplying this source's credentials (e.g. imap mailbox)."
        },
        "auth": {
          "type": "string",
          "description": "Inbound auth policy for a webhook, e.g. 'hmac:sensors' referencing a connection (ADR 0025)."
        },
        "bodyType": {
          "$ref": "#/$defs/slug",
          "description": "Optional: id of a `types` registry domain type declaring this source's event body shape. It puts a type in scope for the action's FEEL expressions (variables/correlationKey), so `body.<field>` autocompletes and wrong paths become diagnostics (ADR 0029 §5)."
        },
        "action": { "$ref": "#/$defs/triggerAction" }
      }
    },
    "triggerAction": {
      "type": "object",
      "additionalProperties": false,
      "description": "Maps an event to exactly one engine call (ADR 0025 §1): start a process, or publish a CorrelateMessage.",
      "oneOf": [
        { "required": ["start"] },
        { "required": ["message"] }
      ],
      "properties": {
        "start": {
          "type": "string",
          "description": "Process id/name to start; variables seeded by a FEEL expression over the event body."
        },
        "variables": {
          "$ref": "#/$defs/feel",
          "description": "FEEL over the event body producing the started instance's variables."
        },
        "message": {
          "type": "string",
          "description": "messageName to publish as a CorrelateMessage (correlates to a message-start subscription to start a new instance, or to a running-instance catch to feed a token — ADR 0025 §5)."
        },
        "correlationKey": {
          "$ref": "#/$defs/feel",
          "description": "FEEL over the event body producing the correlationKey (message actions)."
        }
      }
    },
    "binding": {
      "type": "object",
      "additionalProperties": false,
      "required": ["type"],
      "oneOf": [{ "required": ["form"] }, { "required": ["decision"] }, { "required": ["process"] }],
      "description": "Binds one model — a form, a decision, OR a process — to the domain type in scope for its FEEL (ADR 0029 §5, ADR 0030). Exactly one of form/decision/process.",
      "properties": {
        "form": {
          "type": "string",
          "description": "form-js form id (schema.id) whose default-value FEEL is scoped to `type`."
        },
        "decision": {
          "type": "string",
          "description": "DMN decision id whose input-expression FEEL is scoped to `type`."
        },
        "process": {
          "type": "string",
          "description": "BPMN process id whose FEEL (component/service-task inputs, conditions) is scoped to `type` — the process as the motion of a typed domain object (ADR 0030)."
        },
        "type": {
          "$ref": "#/$defs/slug",
          "description": "Declared domain type id in scope for the bound model's FEEL."
        }
      }
    },
    "connection": {
      "type": "object",
      "description": "A named connection (credentials/endpoint). Shape is source-specific; secrets should be env templates, never inline literals (ADR 0025 §1).",
      "required": ["type"],
      "properties": {
        "type": {
          "type": "string",
          "description": "Connection kind (e.g. imap, mqtt, hmac)."
        }
      }
    },
    "instanceTracking": {
      "type": "object",
      "additionalProperties": false,
      "required": ["table", "keyField", "onTerminated"],
      "description": "One process-instance lifecycle binding: reconcile rows of `table` when their tracked instance reaches the TERMINATED engine state (never COMPLETED — see the top-level `instanceTracking` description).",
      "properties": {
        "table": {
          "type": "string",
          "minLength": 1,
          "description": "Datasource table whose rows each track one process instance."
        },
        "keyField": {
          "type": "string",
          "minLength": 1,
          "description": "Column holding the tracked process instance key."
        },
        "statusField": {
          "type": "string",
          "minLength": 1,
          "description": "Column governing whether a row is still active. Combined with `activeStatuses` (allow-list) or `terminalStatuses` (exclusion list) to select the rows worth polling; when omitted, every row of `table` is polled."
        },
        "activeStatuses": {
          "type": "array",
          "items": { "type": "string" },
          "minItems": 1,
          "description": "Values of `statusField` considered still-open. Only rows in one of these states are polled; a row already in a terminal status is skipped. FAIL-CLOSED: a non-terminal status omitted here is silently dropped from reconciliation — prefer `terminalStatuses` for a fail-open selector. When neither is set, every row is polled (use with care on large tables). Requires `statusField`; mutually exclusive with `terminalStatuses`."
        },
        "terminalStatuses": {
          "type": "array",
          "items": { "type": "string" },
          "minItems": 1,
          "description": "Values of `statusField` considered FINISHED. Fail-open alternative to `activeStatuses`: every row whose `statusField` is NOT one of these is polled, so a newly-added non-terminal status is reconciled by default instead of being silently dropped. Prefer this and mirror the app's terminal-status enum. Requires `statusField`; mutually exclusive with `activeStatuses`."
        },
        "onTerminated": {
          "type": "object",
          "additionalProperties": false,
          "required": ["set"],
          "description": "The reconciliation applied to a row whose instance has reached the TERMINATED engine state.",
          "properties": {
            "set": {
              "type": "object",
              "minProperties": 1,
              "additionalProperties": {
                "anyOf": [
                  { "type": "string" },
                  { "type": "number" },
                  { "type": "boolean" },
                  { "type": "null" }
                ]
              },
              "description": "Column → literal value patch written to the row (e.g. set the status to an abandoned/terminal value and clear open-task pointers)."
            }
          }
        },
        "pollMs": {
          "type": "integer",
          "minimum": 1000,
          "default": 15000,
          "description": "Poll interval in milliseconds. Default 15000."
        }
      }
    },
    "surfaces": {
      "type": "object",
      "additionalProperties": false,
      "description": "Batteries-included human surfaces generated from the manifest (ADR 0026).",
      "properties": {
        "taskInbox": { "$ref": "#/$defs/taskInboxSurface" },
        "chat": { "$ref": "#/$defs/chatSurface" },
        "pages": { "$ref": "#/$defs/pagesSurface" }
      }
    },
    "taskInboxSurface": {
      "type": "object",
      "additionalProperties": false,
      "description": "Generic task inbox: lists open user tasks and renders their .form to claim/complete (ADR 0026).",
      "properties": {
        "enabled": { "type": "boolean", "default": false },
        "path": { "type": "string", "default": "/tasks" }
      }
    },
    "chatSurface": {
      "type": "object",
      "additionalProperties": false,
      "description": "Conversational surface whose LLM agent drives the action API via its tools (ADR 0026).",
      "properties": {
        "enabled": { "type": "boolean", "default": false },
        "path": { "type": "string", "default": "/chat" },
        "agent": {
          "type": "string",
          "description": "Name of an llm[] binding backing this chat (cross-reference rule, ADR 0027 §4)."
        }
      }
    },
    "pagesSurface": {
      "type": "object",
      "additionalProperties": false,
      "description": "Schema-driven page runtime (ADR 0042): serves pages/<homePage>.page.json at / and the generic /app/actions + /app/data routes over the named datasource, with no hand-written frontend.",
      "properties": {
        "enabled": { "type": "boolean", "default": false },
        "pagesDir": {
          "type": "string",
          "default": "pages",
          "description": "Directory of *.page.json composed pages, relative to the app root."
        },
        "homePage": {
          "type": "string",
          "default": "home",
          "description": "Id of the page served at / (loaded as <pagesDir>/<homePage>.page.json)."
        },
        "rowLimit": {
          "type": "integer",
          "minimum": 0,
          "default": 200,
          "description": "Maximum rows a dataGrid fetch returns."
        },
        "sourceName": {
          "type": "string",
          "default": "app",
          "description": "Name of the data[] source the page runtime reads (cross-reference rule, ADR 0027 §4)."
        }
      }
    },
    "actionDecl": {
      "type": "object",
      "additionalProperties": false,
      "required": ["path", "module"],
      "description": "An app-authored action handler override (ADR 0055 §3): binds a route to a handler module that default-exports an ActionHandler.",
      "properties": {
        "path": {
          "type": "string",
          "description": "Route path to serve, e.g. \"/app/actions/cancel\" or \"/app/actions/start/convergence-loop\"."
        },
        "module": {
          "type": "string",
          "description": "Handler module path relative to the app root; default-exports an ActionHandler (or a named `handler`)."
        },
        "method": {
          "type": "string",
          "default": "POST",
          "description": "HTTP method to match."
        },
        "prefix": {
          "type": "boolean",
          "default": false,
          "description": "Match `path` as a prefix rather than exactly."
        }
      }
    },
    "apiBinding": {
      "type": "object",
      "additionalProperties": false,
      "required": ["spec"],
      "description": "The OpenAPI endpoint surface (ADR 0058): a contract-first API where the toolkit derives the controller layer (typed request/response contracts + runtime validators + route table) from an OpenAPI document and the author writes only the delegated implementation per `operationId`. Coexists with `actions[]` (both mount together, first-match-wins) and is ejectable — `eject` (whole surface) or an `x-urban-eject: true` vendor extension on an operation skips generated validation and hands the delegate the raw request. Every operation MUST carry a unique `operationId` (the delegate module key + type stem); `urban check` fails closed otherwise.",
      "properties": {
        "spec": {
          "type": "string",
          "description": "OpenAPI 3.x document, app-root-relative (e.g. \"openapi.json\"). JSON is supported first; YAML is a fast-follow (ADR 0058 open questions)."
        },
        "dir": {
          "type": "string",
          "default": "operations",
          "description": "Directory (app-root-relative) holding the per-`operationId` delegate modules; each default-exports an operation handler (or a named `handler`)."
        },
        "base": {
          "type": "string",
          "default": "/app/api",
          "description": "Route prefix the derived operation paths mount under."
        },
        "validateResponses": {
          "enum": ["dev", "always", "never"],
          "default": "dev",
          "description": "When to run the derived response validators: \"dev\" (IDE/Run only), \"always\", or \"never\". Response validation is off in production by default for perf."
        },
        "eject": {
          "type": "boolean",
          "default": false,
          "description": "Opt the whole surface out of generated request validation: routes + docs are still mounted, but every delegate receives the raw request. Per-operation opt-out uses the `x-urban-eject: true` OpenAPI vendor extension instead."
        }
      }
    },
    "worker": {
      "type": "object",
      "additionalProperties": false,
      "required": ["taskType"],
      "description": "A service-task worker: a referenced handler file, an llm binding, or a connector supplied by an installed pack (ADR 0022 §E, ADR 0050).",
      "oneOf": [
        { "required": ["handler"] },
        { "required": ["llm"] },
        { "required": ["connector"] }
      ],
      "properties": {
        "taskType": { "type": "string", "minLength": 1 },
        "handler": {
          "type": "string",
          "description": "Path to a handler file (language via ADR 0008 packs)."
        },
        "llm": {
          "type": "string",
          "description": "Name of an llm[] binding used as the worker (LLM-as-worker)."
        },
        "connector": {
          "type": "string",
          "description": "Id of the installed pack (`nano-ide.ext.json` `id`) that supplies this worker's out-of-process handler, enabled into the project rather than authored in it (ADR 0050 — the outbound I/O edge). The host resolves + supervises the pack's worker `entry` by `taskType`; the pack's element template (its `zeebe:taskDefinition:type` = this `taskType`) is the design-time face. Mutually exclusive with `handler`/`llm`."
        },
        "connection": {
          "type": "string",
          "description": "Optional named `connections[]` entry supplying this worker's shared credential/endpoint (ADR 0025 §1), symmetric to `trigger.connection`. Its secrets stay env pointers (ADR 0027 §5)."
        },
        "inputType": {
          "$ref": "#/$defs/slug",
          "description": "Declared domain type this worker reads from `job.variables` — the incoming process-variable payload (ADR 0033 §3). Binds to the worker via `taskType`; declaring `inputType` types the handler's `job.variables` while authoring (the generated typed `defineWorker` keys off `taskType`), erased at runtime."
        },
        "outputType": {
          "$ref": "#/$defs/slug",
          "description": "Declared domain type this worker writes as its result (ADR 0033 §3). A component's `zeebe:taskDefinition:type` binds to this worker via `taskType`; declaring `outputType` types the process variables its output mappings write, so downstream FEEL autocompletes on the result's fields."
        }
      }
    },
    "llmBinding": {
      "type": "object",
      "additionalProperties": false,
      "required": ["provider", "model"],
      "properties": {
        "provider": {
          "type": "string",
          "description": "LLM provider selector (e.g. 'env' to resolve from environment)."
        },
        "model": {
          "type": "string",
          "description": "Model id, typically an env template (e.g. ${NANO_APP_LLM_MODEL})."
        },
        "output": {
          "type": "object",
          "additionalProperties": false,
          "description": "Constrains the model's structured output (e.g. to a DMN decision).",
          "properties": {
            "decision": {
              "type": "string",
              "description": "DMN decision id constraining the output shape."
            }
          }
        },
        "tools": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Action-API tools the agent may call (e.g. start-process, complete-task, query-data)."
        }
      }
    },
    "network": {
      "type": "object",
      "additionalProperties": false,
      "description": "App-level network settings (nanobpm/nano-ide#235). The loopback default applies when this block is absent, when it is `{}`, or when `bind` is omitted.",
      "properties": {
        "bind": {
          "$ref": "#/$defs/bindMode",
          "default": "loopback",
          "description": "Which interface the app's embedded HTTP server binds to. Default \"loopback\" (127.0.0.1 — secure by default, refuses off-box connections); \"all\" (0.0.0.0) exposes it on the LAN for a distributed worker fleet. The URBAN_BIND env var overrides this at runtime."
        }
      }
    },
    "bindMode": { "enum": ["loopback", "all"] },
    "security": {
      "type": "object",
      "additionalProperties": false,
      "description": "App-user auth/identity/authorization policy (ADR 0028). Default (block absent) is single-user, unsecured. Secrets are env templates resolved at boot, never persisted.",
      "properties": {
        "mode": {
          "description": "Enabled auth tier(s): none (default), local (username/password), oidc (social). A list combines them.",
          "anyOf": [
            { "$ref": "#/$defs/securityMode" },
            { "type": "array", "items": { "$ref": "#/$defs/securityMode" } }
          ]
        },
        "providers": {
          "type": "array",
          "items": { "$ref": "#/$defs/securityProvider" }
        },
        "roles": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Role names; the maker may add domain roles beyond admin/user."
        },
        "rules": {
          "type": "object",
          "additionalProperties": false,
          "description": "Role-based authorization: which roles may reach each action/surface/datasource (ADR 0028).",
          "properties": {
            "actions": { "$ref": "#/$defs/roleMap" },
            "surfaces": { "$ref": "#/$defs/roleMap" },
            "data": {
              "type": "object",
              "additionalProperties": {
                "type": "object",
                "additionalProperties": { "type": "array", "items": { "type": "string" } }
              }
            }
          }
        }
      }
    },
    "securityMode": { "enum": ["none", "local", "oidc"] },
    "securityProvider": {
      "type": "object",
      "required": ["id", "type"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "type": {
          "enum": ["oidc", "password"],
          "description": "oidc: social/generic OIDC (Authorization Code + PKCE). password: local username/password."
        },
        "preset": {
          "type": "string",
          "description": "OIDC preset shorthand (e.g. google, github, auth0)."
        },
        "clientId": { "type": "string" },
        "clientSecret": { "type": "string" },
        "signup": {
          "enum": ["open", "invite", "closed"],
          "description": "password providers: self sign-up policy."
        }
      }
    },
    "roleMap": {
      "type": "object",
      "description": "Maps a pattern (e.g. 'start/*') to the list of roles permitted.",
      "additionalProperties": {
        "type": "array",
        "items": { "type": "string" }
      }
    }
  }
}
