[
  {
    "function": {
      "description": "Workspace resource maintenance. list returns this dashboard's resources and dependencies, with exact IDs and consumers; widget narrows it, all=true includes the environment. Follow next_offset with offset for more pages; counts include references omitted from compact samples. link(widget, ids) replaces dedicated backend declarations. IDs: tool:name, cron:name, service:name, file:data/path. Prefer resources on widget add/update. For ordinary widget removal, widget action=remove already checks usage and reports cleanup: no global inventory is needed. cleanup previews by default; dry_run=false only for user-requested cleanup. Shared/protected resources stay; data/legacy need explicit selection. Creation provenance is not proof of exclusive usage.",
      "name": "resources",
      "parameters": {
        "properties": {
          "action": {
            "enum": [
              "list",
              "link",
              "cleanup"
            ],
            "type": "string"
          },
          "all": {
            "description": "list only: true includes all accessible workspaces and legacy resources. Default false lists this dashboard and its dependencies.",
            "type": "boolean"
          },
          "dry_run": {
            "description": "cleanup only: true previews (default), false applies the freshly checked plan.",
            "type": "boolean"
          },
          "ids": {
            "description": "Exact resource IDs returned by list. For link: resources dedicated to this widget. For cleanup: explicitly selected additional resources.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "offset": {
            "description": "list only: use next_offset returned by the preceding page to continue (default 0).",
            "type": "integer"
          },
          "widget": {
            "description": "Existing widget ID on this dashboard. Required for link; optional for list and cleanup.",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Delegate a bounded independent task within the user's request. spawn takes task and optional context; returns id. status lists your children; wait(id) waits for a result; cancel(id) stops one. Children share your workspace, permissions, model-call budget and lifetime; no recursive delegation. Use distinct files to avoid conflicting edits. Delegate substantial independent work, not simple lookups. Wait for useful results and integrate them before finishing; unfinished children are cancelled when you stop. Children report missing user input to you.",
      "name": "subagent",
      "parameters": {
        "properties": {
          "action": {
            "enum": [
              "spawn",
              "status",
              "wait",
              "cancel"
            ],
            "type": "string"
          },
          "context": {
            "description": "Only relevant facts, constraints and file paths. Never secret values.",
            "type": "string"
          },
          "id": {
            "description": "Child id returned by spawn.",
            "type": "string"
          },
          "task": {
            "description": "Bounded mission and expected deliverable.",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Start a Docker service container. A port is auto-allocated on the configured Docker backend. Use the widget and script URLs returned by this tool (they depend on the deployment). For a requested user interface, also integrate it into a dashboard widget unless the user asked for a separate app. Prefer a Docker image over installing a service into the workspace.",
      "name": "docker_run",
      "parameters": {
        "properties": {
          "command": {
            "description": "Optional command to override the image's default entrypoint (e.g. 'tail -f /dev/null' to keep a CLI image alive, or 'nginx -g daemon off;'). Leave empty to use the image default.",
            "type": "string"
          },
          "env": {
            "description": "Optional environment variables as key/value pairs",
            "type": "object"
          },
          "extra_ports": {
            "description": "Additional internal ports to expose (e.g. [9090, 8080] for metrics or API). Each gets its own auto-allocated published port.",
            "type": "array"
          },
          "gpu": {
            "description": "Set to true to give the container access to all GPUs (requires nvidia-container-toolkit on the host)",
            "type": "boolean"
          },
          "image": {
            "description": "Docker image to run (e.g. 'jgraph/drawio')",
            "type": "string"
          },
          "name": {
            "description": "Short service name, letters/digits/hyphens only (e.g. 'drawio'). Container will be named prism-svc-\u003cname\u003e.",
            "type": "string"
          },
          "port": {
            "description": "Primary port the service listens on inside the container (e.g. 3000 for the UI)",
            "type": "integer"
          },
          "purpose": {
            "description": "One short line: what this service is for and when to use it. Always set it — it's shown back to you in your running-services index so you (later) know what you deployed instead of guessing. E.g. 'Uptime Kuma — status monitoring for the user's self-hosted services'.",
            "type": "string"
          },
          "volumes": {
            "description": "Ignored — the workspace (/workspace) is automatically available inside every service container via --volumes-from.",
            "type": "array"
          }
        },
        "required": [
          "image",
          "name",
          "port"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Manage Docker containers. Actions: ps (list Prism service containers), list (all containers on the host), logs (recent logs from a service), exec (run a shell command inside a service container), stop (stop and remove a service started with docker_run).",
      "name": "docker_manage",
      "parameters": {
        "properties": {
          "action": {
            "description": "One of: ps, list, logs, exec, stop",
            "enum": [
              "ps",
              "list",
              "logs",
              "exec",
              "stop"
            ],
            "type": "string"
          },
          "command": {
            "description": "Shell command to run inside the container (exec action only)",
            "type": "string"
          },
          "name": {
            "description": "Service name as given to docker_run (required for logs, exec, stop)",
            "type": "string"
          },
          "tail": {
            "description": "Number of log lines to return (logs action, default 50)",
            "type": "integer"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Manage multi-container applications with Docker Compose. Write the docker-compose.yml to workspace first with write_file or wget, then call this tool. Container names are automatically prefixed prism-svc-\u003cdir\u003e-\u003cservice\u003e-1.",
      "name": "docker_compose",
      "parameters": {
        "properties": {
          "action": {
            "description": "Action: up (start all services), down (stop and remove), ps (list services), logs (get logs), restart, exec (run a command inside a service container)",
            "enum": [
              "up",
              "down",
              "ps",
              "logs",
              "restart",
              "exec"
            ],
            "type": "string"
          },
          "command": {
            "description": "Shell command to run inside the service container (exec action only)",
            "type": "string"
          },
          "file": {
            "description": "Path to docker-compose.yml relative to /workspace (e.g. 'greenbone/docker-compose.yml')",
            "type": "string"
          },
          "project": {
            "description": "Optional project name override. If omitted, auto-set to prism-svc-\u003cdirectory\u003e (e.g. 'prism-svc-greenbone').",
            "type": "string"
          },
          "service": {
            "description": "Service name to target for logs, restart, or exec",
            "type": "string"
          },
          "tail": {
            "description": "Number of log lines per service (default: 50, for logs action)",
            "type": "integer"
          }
        },
        "required": [
          "action",
          "file"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Execute a shell command in the workspace container. Use this to run code, compile, test, manage files, etc. Note: the Docker CLI is NOT available here — use docker_run/docker_manage/docker_compose to manage containers.",
      "name": "exec_command",
      "parameters": {
        "properties": {
          "command": {
            "description": "The bash command to execute",
            "type": "string"
          }
        },
        "required": [
          "command"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Download a file from a URL straight to the workspace (streamed to disk — no transcription, no truncation). Always use this instead of http_request + write_file to save a remote file as-is.",
      "name": "wget",
      "parameters": {
        "properties": {
          "path": {
            "description": "Destination path relative to /workspace (e.g. 'greenbone/docker-compose.yml')",
            "type": "string"
          },
          "url": {
            "description": "HTTP or HTTPS URL to download from",
            "type": "string"
          }
        },
        "required": [
          "url",
          "path"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Edit a UTF-8 text file in the workspace by replacing exact text. Read the file first. By default old_text must match exactly once; use replace_all=true only when every occurrence should change. An empty new_text deletes the matched text. Shipped Prism tools cannot be edited.",
      "name": "edit_file",
      "parameters": {
        "properties": {
          "new_text": {
            "description": "Replacement text; empty deletes the matched text",
            "type": "string"
          },
          "old_text": {
            "description": "Exact UTF-8 text to replace; read the current file first",
            "type": "string"
          },
          "path": {
            "description": "File path relative to /workspace",
            "type": "string"
          },
          "replace_all": {
            "description": "Replace every occurrence instead of requiring exactly one match (default false)",
            "type": "boolean"
          }
        },
        "required": [
          "path",
          "old_text",
          "new_text"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Write content to a file in the workspace. Path is relative to /workspace.",
      "name": "write_file",
      "parameters": {
        "properties": {
          "content": {
            "description": "File content to write",
            "type": "string"
          },
          "path": {
            "description": "File path relative to /workspace (e.g. main.py)",
            "type": "string"
          }
        },
        "required": [
          "path",
          "content"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Read the content of a file in the workspace.",
      "name": "read_file",
      "parameters": {
        "properties": {
          "path": {
            "description": "File path relative to /workspace",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "List recent versioned changes to the workspace. Every agent turn is auto-committed to git, so this is the undo history — use it to find a point to roll back to when a file was clobbered or a change went wrong.",
      "name": "workspace_history",
      "parameters": {
        "properties": {
          "limit": {
            "description": "How many recent changes to list (default 20, max 50).",
            "type": "integer"
          }
        },
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Roll one file back to how it was at a past commit (a hash from workspace_history) and record the restoration. Restores only the given path, and is itself reversible — nothing is lost.",
      "name": "workspace_restore",
      "parameters": {
        "properties": {
          "commit": {
            "description": "Commit hash from workspace_history.",
            "type": "string"
          },
          "path": {
            "description": "Workspace-relative path of the file to restore.",
            "type": "string"
          }
        },
        "required": [
          "commit",
          "path"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Prism's own user documentation plus what is currently configured for this user. Call it FIRST whenever the user asks how to use or configure Prism, what you can do, where a setting lives, or whether something (email, calendar, notes vault, Telegram, Slack, Webex, MCP, knowledge base) is connected. Without a topic: the list of topics AND the current configuration. With a topic (a name from that list): the full page. Then guide the user step by step from their actual situation.",
      "name": "prism_help",
      "parameters": {
        "properties": {
          "topic": {
            "description": "A topic name from the index (e.g. 'settings-agent', 'connect-telegram'). Omit for the index + current configuration.",
            "type": "string"
          }
        },
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Read or change the user's own agent settings (Settings → Agent): name, personality (the default one, every workspace), max_iterations per turn, thinking (extended reasoning on/off), prompt_profile (guided, standard or minimal) and reasoning_effort. Use it when the user asks to rename you, raise the turn budget after an 'iteration limit reached', turn reasoning off for speed, or pick a reasoning effort. AI configuration: Settings → AI provider in single-user, Admin → AI provider (global admin only) in multi-user. Use ai_get to see all sources. ai_source_set adds/updates a source_id; ai_source_remove deletes it; ai_source_default picks its default model; ai_source_models and ai_source_test inspect one source. ai_model_get/ai_model_set inspect/configure one source_id + model without changing the default (context_window, model_vision). Legacy ai_set edits the default without removing other sources. ai_embedding_models/ai_embedding_test inspect embeddings; ai_reset restores server defaults. Embedding changes apply automatically; ai_get reports progress; set reindex=true only after the user agrees to rebuilding their document index and sending indexed text to the selected provider. Pass a stored key_secret NAME, never a raw API key; ask for keys with request_secret or direct the user to the secure settings form. ai_test makes a short potentially billable request. Changes apply from the next message. A group's shared agent is configured by a group admin in the admin console instead.",
      "name": "agent_settings",
      "parameters": {
        "properties": {
          "action": {
            "description": "get/set: agent behavior; ai_*: deployment AI connection",
            "enum": [
              "get",
              "set",
              "ai_get",
              "ai_set",
              "ai_test",
              "ai_models",
              "ai_reset",
              "ai_embedding_models",
              "ai_embedding_test",
              "ai_source_set",
              "ai_source_remove",
              "ai_source_default",
              "ai_source_models",
              "ai_source_test",
              "ai_model_get",
              "ai_model_set"
            ],
            "type": "string"
          },
          "base_url": {
            "description": "AI server URL; defaults to official provider, custom servers require admin",
            "type": "string"
          },
          "chat_vision": {
            "description": "Whether the conversation model accepts images",
            "type": "boolean"
          },
          "context_window": {
            "description": "ai_model_set: effective model context in tokens (4096–2000000); 0 restores Automatic. Omit to retain.",
            "type": "integer"
          },
          "embedding_base_url": {
            "description": "Separate embedding URL; OpenAI-compatible URLs include /v1",
            "type": "string"
          },
          "embedding_key_secret": {
            "description": "Stored credential name for a separate embedding provider",
            "type": "string"
          },
          "embedding_model": {
            "description": "Embedding model ID; empty disables document search",
            "type": "string"
          },
          "embedding_provider": {
            "description": "Separate embedding provider",
            "enum": [
              "openai",
              "ollama",
              "other"
            ],
            "type": "string"
          },
          "embedding_source_id": {
            "description": "Reuse an existing source for embeddings when use_same_provider=false; empty selects a dedicated connection",
            "type": "string"
          },
          "embedding_use_same_provider": {
            "description": "Reuse conversation connection for embeddings",
            "type": "boolean"
          },
          "key_secret": {
            "description": "Name of your stored personal credential, never the API key itself",
            "type": "string"
          },
          "lean_prompt": {
            "description": "set: lean system-prompt profile for frontier models",
            "type": "boolean"
          },
          "make_default": {
            "description": "Make this source and its model the conversation default",
            "type": "boolean"
          },
          "max_iterations": {
            "description": "set: model calls allowed per turn (10–500; 0 = default 75)",
            "type": "integer"
          },
          "model": {
            "description": "AI model ID",
            "type": "string"
          },
          "model_vision": {
            "description": "ai_model_set: image support for this model; omit to retain.",
            "enum": [
              "auto",
              "yes",
              "no"
            ],
            "type": "string"
          },
          "name": {
            "description": "set: the agent's display name (empty = default)",
            "type": "string"
          },
          "personality": {
            "description": "set: the default personality applied in every workspace (Settings → Agent → Default personality); empty = none. For one workspace only, use update_system_prompt instead",
            "type": "string"
          },
          "prompt_profile": {
            "description": "Prompt detail: guided for small models, standard (former lean), minimal for capable models; applies next message.",
            "enum": [
              "guided",
              "standard",
              "minimal"
            ],
            "type": "string"
          },
          "provider": {
            "description": "AI connection provider",
            "enum": [
              "openai",
              "anthropic",
              "ollama",
              "other"
            ],
            "type": "string"
          },
          "reasoning_effort": {
            "description": "set: One of: low, medium, high, xhigh, default (the model decides which it accepts — Qwen3.8-Flash-Next: low/medium/xhigh; gpt-oss: low/medium/high)",
            "enum": [
              "low",
              "medium",
              "high",
              "xhigh",
              "default"
            ],
            "type": "string"
          },
          "reindex": {
            "description": "Explicit user agreement to rebuild index when saved; required when changing embedding model/provider or resetting to different defaults",
            "type": "boolean"
          },
          "source_id": {
            "description": "Source ID from ai_get, or a new short ID for ai_source_set",
            "type": "string"
          },
          "source_name": {
            "description": "Display name for an AI source",
            "type": "string"
          },
          "thinking": {
            "description": "set: extended reasoning on/off",
            "type": "boolean"
          },
          "timezone": {
            "description": "set: Personal IANA timezone (Settings → Profile), e.g. Europe/Paris or America/Martinique. Empty restores the server default. Used for agent time, dates without offsets and new cron schedules; existing jobs keep their timezone.",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Manage the user's inbound webhooks (Settings → Webhooks): an external system POSTs to a Prism URL and the payload becomes a message to you. Actions: list, add, update or remove. add/update accept a name, optional prompt wrapping the payload — use {{content}} where it goes — optional deliver to push your answer to a configured channel, and optional respond for a synchronous reply; update/remove use the id from list. add returns the URL and the token the caller must send as X-Prism-Token. Each webhook runs in its own session so feeds never pollute the user's chat.",
      "name": "webhook",
      "parameters": {
        "properties": {
          "action": {
            "description": "One of: list, add, update, remove — update edits an existing webhook in place (URL and token unchanged)",
            "enum": [
              "list",
              "add",
              "update",
              "remove"
            ],
            "type": "string"
          },
          "deliver": {
            "description": "add/update: One of: telegram, slack, webex — push the answer there too (empty = none)",
            "enum": [
              "telegram",
              "slack",
              "webex"
            ],
            "type": "string"
          },
          "enabled": {
            "description": "update: false pauses the webhook (callers get 403), true resumes it",
            "type": "boolean"
          },
          "id": {
            "description": "update/remove: the webhook id from list",
            "type": "string"
          },
          "model": {
            "description": "add/update: model for this webhook's turns — empty = deployment default",
            "type": "string"
          },
          "name": {
            "description": "add/update: a short label, e.g. 'github-ci'",
            "type": "string"
          },
          "prompt": {
            "description": "add/update: instructions wrapping the payload, e.g. 'Summarize this CI event in one line: {{content}}'",
            "type": "string"
          },
          "respond": {
            "description": "add/update: reply synchronously with your answer in the HTTP response (off by default)",
            "type": "boolean"
          },
          "session": {
            "description": "add/update: chat session (workspace) the turn runs in — empty = a dedicated webhook-\u003cid\u003e session so the feed never lands in a human chat",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Read or change where the user's calendar, tasks and notes come from (Settings → Calendar / Notes): status; set (calendar=auto|local|caldav|google|microsoft, tasks=auto|local|caldav|todoist); connect_caldav (url, user, password_secret = the NAME of a secret stored with request_secret, optional event_path/task_path — the connection is tested before anything is saved); disconnect_caldav; connect_todoist (token_secret = secret name, validated first); disconnect_todoist; set_notes_vault (path of a Markdown vault mounted in the server container); use_builtin_notes. Google and Microsoft accounts need a browser OAuth sign-in: guide the user to Settings → Calendar for those.",
      "name": "pim_source",
      "parameters": {
        "properties": {
          "action": {
            "description": "One of: status, set, connect_caldav, disconnect_caldav, connect_todoist, disconnect_todoist, set_notes_vault, use_builtin_notes",
            "enum": [
              "status",
              "set",
              "connect_caldav",
              "disconnect_caldav",
              "connect_todoist",
              "disconnect_todoist",
              "set_notes_vault",
              "use_builtin_notes"
            ],
            "type": "string"
          },
          "calendar": {
            "description": "set: One of: auto, local, caldav, google, microsoft",
            "enum": [
              "auto",
              "local",
              "caldav",
              "google",
              "microsoft"
            ],
            "type": "string"
          },
          "event_path": {
            "description": "connect_caldav: optional calendar collection path (auto-discovered when omitted)",
            "type": "string"
          },
          "password_secret": {
            "description": "connect_caldav: NAME of the secret holding the password (request_secret first)",
            "type": "string"
          },
          "path": {
            "description": "set_notes_vault: absolute directory of the vault as seen by the server container",
            "type": "string"
          },
          "task_path": {
            "description": "connect_caldav: optional task collection path (auto-discovered when omitted)",
            "type": "string"
          },
          "tasks": {
            "description": "set: One of: auto, local, caldav, todoist",
            "enum": [
              "auto",
              "local",
              "caldav",
              "todoist"
            ],
            "type": "string"
          },
          "token_secret": {
            "description": "connect_todoist: NAME of the secret holding the API token (request_secret first)",
            "type": "string"
          },
          "url": {
            "description": "connect_caldav: server URL (e.g. https://cloud.example.com/remote.php/dav)",
            "type": "string"
          },
          "user": {
            "description": "connect_caldav: login",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Messaging channels for this user (Settings → Channels): status; telegram_connect (token_secret = the NAME of a secret holding the BotFather token, stored with request_secret — the token is checked against Telegram before it is saved; the user then sends /start to the bot to link the chat); telegram_unlink. Slack is deployment-wide (global admin, Settings → Channels) and Webex is per group (group admin, admin console): guide the user there for those.",
      "name": "channel",
      "parameters": {
        "properties": {
          "action": {
            "description": "One of: status, telegram_connect, telegram_unlink",
            "enum": [
              "status",
              "telegram_connect",
              "telegram_unlink"
            ],
            "type": "string"
          },
          "token_secret": {
            "description": "telegram_connect: NAME of the secret holding the bot token",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "List files and directories in the workspace.",
      "name": "list_files",
      "parameters": {
        "properties": {
          "path": {
            "description": "Directory path relative to /workspace (default: .)",
            "type": "string"
          }
        },
        "required": [],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Delete a file from the workspace.",
      "name": "delete_file",
      "parameters": {
        "properties": {
          "path": {
            "description": "File path relative to /workspace",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Install workspace libraries with apt or pip. The deployment handles privileges and records successful installations for restoration. Python user libraries persist in the workspace. Installation failures are reported; do not claim success on failure.",
      "name": "install_packages",
      "parameters": {
        "properties": {
          "manager": {
            "description": "Package manager: apt or pip",
            "enum": [
              "apt",
              "pip"
            ],
            "type": "string"
          },
          "packages": {
            "description": "Space-separated list of packages to install",
            "type": "string"
          }
        },
        "required": [
          "manager",
          "packages"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Dashboard widgets — the default interface for requested interactive tools/apps and persistent views, even when the user does not say widget. Embed Docker apps using their returned browser URL or call their backend from a widget; a service link alone is not dashboard integration. Honor requests for a separate app or backend-only tool. Answer ordinary questions and one-off results in chat. Widgets are self-contained HTML/JS panels displayed in iframes (see the widget guidelines in the system prompt). Actions: add (title+content required; id optional, auto-derived by slugifying the title if omitted), update (id + any of title/content/cols/height; only provided fields change), remove (id), list. Before update/remove, call list and copy the exact id from its output — never guess an id from memory; remove ONLY when the user explicitly asked for that widget to go. add/update return a screenshot of the rendered widget plus console errors — inspect them and fix any problem before telling the user the widget is ready. Group sharing (multi-user): list_shared (browse widgets/dashboards your group published), add_shared (add one to THIS board — id is the numeric id from list_shared), share (publish a widget of THIS board to your group — id is the widget's id from list; set kind=dashboard to share the whole board; add group=\u003cname\u003e only if you belong to several groups), unshare (remove an item from the group gallery — id is the numeric id from list_shared; only the user's own shares, or anyone's if they admin the group; copies already added to boards are kept).",
      "name": "widget",
      "parameters": {
        "properties": {
          "action": {
            "description": "One of: add, update, remove, list, list_shared, add_shared, share, unshare",
            "enum": [
              "add",
              "update",
              "remove",
              "list",
              "list_shared",
              "add_shared",
              "share",
              "unshare"
            ],
            "type": "string"
          },
          "cols": {
            "description": "Width: 1=small (default), 2=medium, 3=full-width",
            "type": "integer"
          },
          "content": {
            "description": "Complete self-contained HTML for the widget (include \u003cstyle\u003e and \u003cscript\u003e tags as needed)",
            "type": "string"
          },
          "description": {
            "description": "For share: optional short description of what the widget/dashboard does (max 1000 characters). Prism captures its preview automatically.",
            "type": "string"
          },
          "group": {
            "description": "For share: which group to publish to, by name — only needed when you belong to several groups.",
            "type": "string"
          },
          "height": {
            "description": "Height in pixels (default: 280)",
            "type": "integer"
          },
          "id": {
            "description": "Widget ID (add: optional, derived from title; update/remove/share: the id from list). For add_shared/unshare: the NUMERIC id from list_shared.",
            "type": "string"
          },
          "kind": {
            "description": "For list_shared/share: 'widget' (default) or 'dashboard' (the whole board).",
            "type": "string"
          },
          "resources": {
            "description": "Optional dedicated backends: tool:name, cron:name, file:data/path (IDs from resources list). Prism preserves shared resources; removes tracked dedicated tools/cron with the widget, retains data. Omit on update to keep existing links; [] clears links.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "title": {
            "description": "Widget title shown in the card header",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Open a file in the user's editor. Does not return the file content — use read_file for that.",
      "name": "show_in_editor",
      "parameters": {
        "properties": {
          "path": {
            "description": "File path relative to /workspace",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Manage scheduled cron jobs running inside the workspace container. Actions: list, add (name+schedule+command, plus a description), remove (name). Jobs also appear read-only in the user's Tasks list.\nTo deliver a result to the user on a messaging channel on a schedule, make the command POST a prompt to yourself with delivery enabled. Set \"deliver\" to \"telegram\", \"slack\" or \"webex\" (Webex posts to the announcement room the group admin picked in the Webex config), e.g.:\ncurl -s -X POST $PRISM_URL/api/chat -H \"Authorization: Bearer $PRISM_TOKEN\" -H 'Content-Type: application/json' -d '{\"message\":\"Summarize my unread emails in 5 bullet points\",\"session\":\"'$PRISM_SESSION'\",\"deliver\":\"telegram\"}'\nALWAYS use $PRISM_SESSION for \"session\" (never a hardcoded channel name like \"telegram\"/\"webex\") — it's the session you're in right now, and it carries THIS chat's knowledge base/MCP servers along to the scheduled run. A hardcoded literal creates a different, empty session with no tools.\nUse \"deliver\" only when you want the model's ANSWER delivered. To post an exact text, push a raw line instead:\n  Telegram: curl -s -X POST $PRISM_URL/api/telegram/send -H \"Authorization: Bearer $PRISM_TOKEN\" -d '{\"text\":\"Backup finished ✅\"}'\n  Webex:    curl -s -X POST $PRISM_URL/api/webex/send -H \"Authorization: Bearer $PRISM_TOKEN\" -d '{\"text\":\"Bonjour à tous !\"}'  (posts to the group's announcement room)\n($PRISM_URL/$PRISM_SESSION/$PRISM_TOKEN are available both ways: as real environment variables for a script's own os.environ lookups, and substituted directly into the command text for inline shell $VAR usage.)\nSECRETS ARE NOT INJECTED INTO CRON JOBS (unlike exec_command): a scheduled script that reads os.environ['MY_SECRET'] silently fails. Fetch it over HTTP instead: curl -s \"$PRISM_URL/api/user/secrets/\u003cname\u003e?session=$PRISM_SESSION\" -H \"Authorization: Bearer $PRISM_TOKEN\" (JSON with a 'value' field).",
      "name": "cron",
      "parameters": {
        "properties": {
          "action": {
            "description": "One of: list, add, update, remove, enable, disable — disable pauses a job (kept, not run), enable resumes it; update changes schedule/command/description of an existing job in place",
            "enum": [
              "list",
              "add",
              "update",
              "remove",
              "enable",
              "disable"
            ],
            "type": "string"
          },
          "command": {
            "description": "Shell command to run (e.g. 'python3 /workspace/myscript.py \u003e\u003e /workspace/logs/myscript.log 2\u003e\u00261')",
            "type": "string"
          },
          "description": {
            "description": "Required for 'add': one short human sentence explaining what this job does and which widget/dashboard it feeds, so the user can tell at a glance what it's for (e.g. 'Refreshes the weather widget's data every 30 min').",
            "type": "string"
          },
          "name": {
            "description": "Unique job identifier (e.g. 'daily-backup', 'sync-feed')",
            "type": "string"
          },
          "schedule": {
            "description": "Cron expression (e.g. '*/5 * * * *' every 5 min, '0 9 * * 1-5' weekdays 9am)",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Make an HTTP request and return the response. For GET requests to HTML pages, returns readable text. For APIs and other content types, returns the raw response body. For JS-heavy SPAs use browser_get instead.",
      "name": "http_request",
      "parameters": {
        "properties": {
          "body": {
            "description": "Request body for POST/PUT/PATCH requests (e.g. JSON string)",
            "type": "string"
          },
          "headers": {
            "description": "Optional HTTP headers as key/value pairs (e.g. {\"Authorization\": \"Bearer token\", \"Content-Type\": \"application/json\"})",
            "type": "object"
          },
          "method": {
            "description": "HTTP method: GET, POST, PUT, DELETE, PATCH (default: GET)",
            "type": "string"
          },
          "url": {
            "description": "The URL to request (http or https)",
            "type": "string"
          }
        },
        "required": [
          "url"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Search the web. Returns titles, URLs, and snippets for the top results. Use http_request or browser_get to read a full page afterwards.",
      "name": "web_search",
      "parameters": {
        "properties": {
          "query": {
            "description": "The search query",
            "type": "string"
          }
        },
        "required": [
          "query"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Run multi-round, in-depth web research on a question. Iteratively plans, searches the web, reads pages, and synthesizes an evolving report until comprehensive, then returns a long Markdown report with sources. Use for open-ended questions needing many sources (comparisons, surveys, 'everything about X'); not for a single quick lookup (use web_search for that). Slow — runs several search+read+LLM rounds.",
      "name": "deep_research",
      "parameters": {
        "properties": {
          "max_rounds": {
            "description": "Max research rounds (default 4, max 10). More rounds = deeper but slower.",
            "type": "integer"
          },
          "question": {
            "description": "The research question to investigate in depth",
            "type": "string"
          }
        },
        "required": [
          "question"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Reusable skills library. Save a procedure you worked out OR that the user walked you through (action=add) so you can recall it reliably later; an index of every saved skill (name + when-to-use) is always in your system prompt, every conversation, with no similarity matching — unlike save_learning, which only surfaces when the new message happens to embed close to what you saved. action=get loads a skill's full steps; action=list / delete also available. If the user spends real time teaching you how to handle a kind of task, save it here, not (only) as a learning.",
      "name": "skill",
      "parameters": {
        "properties": {
          "action": {
            "description": "add, get, list, or delete",
            "enum": [
              "add",
              "save",
              "update",
              "get",
              "list",
              "delete"
            ],
            "type": "string"
          },
          "body": {
            "description": "The full procedure / steps, Markdown (for add)",
            "type": "string"
          },
          "description": {
            "description": "One-line summary (for add)",
            "type": "string"
          },
          "name": {
            "description": "Skill name (required for add/get/delete)",
            "type": "string"
          },
          "when_to_use": {
            "description": "When this skill applies, shown in the index (for add)",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Read or update the user's current note, email draft, task form or calendar form in the browser. With Mail open, action=open app=email opens a new composer; pass folder and uid to draft a reply with the original quoted. Open returns the draft and its revision; it never replaces an existing draft. Then update requested fields with that revision. Otherwise read first. If the user edits or switches, read again and reconsider. Notes are saved; other changes stay in the form. NEVER sends mail. Use email to send only when requested.",
      "name": "editor",
      "parameters": {
        "properties": {
          "action": {
            "description": "open a mail composer, read or update",
            "enum": [
              "open",
              "read",
              "update"
            ],
            "type": "string"
          },
          "all_day": {
            "description": "Event all-day flag.",
            "type": "boolean"
          },
          "app": {
            "description": "open only: email. Mail must be open in this browser.",
            "type": "string"
          },
          "body": {
            "description": "Replacement body. Read all pages first. Keep the quoted original in email replies unless asked otherwise. Empty string clears it.",
            "type": "string"
          },
          "description": {
            "description": "Event description; empty clears it.",
            "type": "string"
          },
          "due": {
            "description": "Task due date/time, ISO format in editor timezone unless offset supplied. Empty clears it; new task form uses date only.",
            "type": "string"
          },
          "end": {
            "description": "Event end. All-day end is exclusive (day after last included day).",
            "type": "string"
          },
          "folder": {
            "description": "open only: original email folder, paired with uid; defaults to INBOX.",
            "type": "string"
          },
          "location": {
            "description": "Event location; empty clears it.",
            "type": "string"
          },
          "offset": {
            "description": "read only: body character offset, follow next_offset while truncated.",
            "type": "integer"
          },
          "priority": {
            "description": "Task priority.",
            "enum": [
              "low",
              "normal",
              "high"
            ],
            "type": "string"
          },
          "revision": {
            "description": "Exact revision returned by read; required for update.",
            "type": "string"
          },
          "start": {
            "description": "Event start, ISO date/time or YYYY-MM-DD for all-day. Changing start alone preserves duration. Times without offset use editor timezone.",
            "type": "string"
          },
          "subject": {
            "description": "Email subject.",
            "type": "string"
          },
          "tags": {
            "description": "Note tags, comma-separated.",
            "type": "string"
          },
          "title": {
            "description": "Note, task or event title. Omit unchanged fields.",
            "type": "string"
          },
          "to": {
            "description": "Email recipients.",
            "type": "string"
          },
          "uid": {
            "description": "open only: message UID to reply to. Omit for a new email.",
            "type": "integer"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Personal notes, shared across the dashboard. For the currently open note, prefer editor read/update to include unsaved text and show changes in place. action=add|list|update|delete. update/delete: copy the exact id from a fresh list call — never guess or recall one. delete ONLY the note the user explicitly asked to delete, never as cleanup or preparation for another task. Notes have a title, body (Markdown) and comma-separated tags. Use to remember free-form information the user wants kept — and proactively offer to save substantial outputs (deep_research reports, summaries, drafts) as a note so they persist in the Notes app. To embed an image (from a URL, or a file already in the workspace): save/download it under workspace/data/... (wget's path param, e.g. \"data/notes/photo.png\") — NOT elsewhere, /api/file forces text/plain and breaks images — then reference it in the body as Markdown ![alt](/data/notes/photo.png) (the /data/ prefix is what makes it a real, browsable URL).",
      "name": "note",
      "parameters": {
        "properties": {
          "action": {
            "description": "add, list, update, delete, share (publish one of your notes to your group, read-only for members; re-sharing updates the copy) or unshare (remove a group-shared note — its id from the group-shared list; author or group admin only)",
            "enum": [
              "add",
              "list",
              "update",
              "delete",
              "share",
              "unshare"
            ],
            "type": "string"
          },
          "body": {
            "description": "Note body (Markdown). On update, omit it to leave the body unchanged; pass an empty string to clear it.",
            "type": "string"
          },
          "id": {
            "description": "Note id from list, used for update/delete/share/unshare. Opaque: a number for local notes, or a file path like \"folder/Note.md\" when a Markdown vault is connected.",
            "type": "string"
          },
          "tags": {
            "description": "Comma-separated tags. On update, omit them to leave the tags unchanged.",
            "type": "string"
          },
          "title": {
            "description": "Note title. On update, omit it to leave the title unchanged.",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Personal tasks. Update takes id and only changed fields. action=add|update|list|done|reopen|delete. Tasks have a title, priority (low|normal|high) and optional due date. list hides completed tasks unless include_done=true.",
      "name": "task",
      "parameters": {
        "properties": {
          "action": {
            "description": "add, update, list, done, reopen, or delete",
            "enum": [
              "add",
              "update",
              "list",
              "done",
              "reopen",
              "delete"
            ],
            "type": "string"
          },
          "due": {
            "description": "update: omit to keep, empty string clears. Due date, e.g. '2026-07-01 09:00' or '2026-07-01'",
            "type": "string"
          },
          "filter": {
            "description": "list: optional deadline or priority filter",
            "enum": [
              "all",
              "today",
              "overdue",
              "upcoming",
              "high"
            ],
            "type": "string"
          },
          "id": {
            "description": "Task id from list, used for update/done/reopen/delete. Opaque: a number locally, or an object path when a CalDAV account is connected.",
            "type": "string"
          },
          "include_done": {
            "description": "Include completed tasks when listing",
            "type": "boolean"
          },
          "priority": {
            "description": "low, normal, or high",
            "enum": [
              "low",
              "normal",
              "high"
            ],
            "type": "string"
          },
          "query": {
            "description": "list: title contains this text",
            "type": "string"
          },
          "title": {
            "description": "Task title (for add/update)",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Email: for the currently open composer use editor read/update (does not send). read returns readable text and links; if truncated=true, read the same folder/uid with offset=next_offset for the rest. list/read/search/send/reply, mark_read/mark_unread, move/archive/trash. folder defaults to INBOX; always keep folder with uid. folders lists exact names; create_folder/rename_folder/delete_folder manage folders (delete requires empty). Create/rename accept simple names; Prism handles provider prefixes and returns the actual folder path. Rules: rules lists, rule_save creates/updates by name, rule_delete removes, rule_preview shows matches without changing mail, rules_apply runs now. Enabled rules run every minute on INBOX including existing mail, without a model. Use prism_help topic=email for the workflow.",
      "name": "email",
      "parameters": {
        "properties": {
          "action": {
            "description": "Email operation",
            "enum": [
              "config",
              "list",
              "read",
              "search",
              "send",
              "reply",
              "folders",
              "create_folder",
              "rename_folder",
              "delete_folder",
              "move",
              "archive",
              "trash",
              "mark_read",
              "mark_unread",
              "rules",
              "rule_save",
              "rule_delete",
              "rule_preview",
              "rules_apply"
            ],
            "type": "string"
          },
          "body": {
            "description": "Message body (send/reply)",
            "type": "string"
          },
          "enabled": {
            "description": "rule_save: automatic execution every minute (default true); false pauses.",
            "type": "boolean"
          },
          "folder": {
            "description": "Source folder, exact name from folders; default INBOX. UIDs are local to this folder.",
            "type": "string"
          },
          "from": {
            "description": "From address if different from user (config)",
            "type": "string"
          },
          "imap_host": {
            "description": "IMAP server host (config)",
            "type": "string"
          },
          "imap_port": {
            "description": "IMAP port (config, default 993)",
            "type": "integer"
          },
          "insecure": {
            "description": "config: accept a self-signed certificate (ProtonMail Bridge needs it)",
            "type": "boolean"
          },
          "limit": {
            "description": "Max messages to return (list/search, default 20)",
            "type": "integer"
          },
          "list_limit": {
            "description": "config: how many messages the Email app loads in the inbox (1–500, default 60)",
            "type": "integer"
          },
          "name": {
            "description": "Rule name; rule_save updates an existing rule of that name, preserving omitted fields.",
            "type": "string"
          },
          "offset": {
            "description": "Default 0. list: messages to skip. read: text character offset; use next_offset from the previous result.",
            "type": "integer"
          },
          "password": {
            "description": "Legacy only: account password. Prefer request_secret then password_secret; raw passwords may appear in conversation logs.",
            "type": "string"
          },
          "password_secret": {
            "description": "config: NAME of a secret created with request_secret; its value is stored as the mailbox password",
            "type": "string"
          },
          "query": {
            "description": "Search query (search)",
            "type": "string"
          },
          "rule_action": {
            "description": "Rule operation",
            "enum": [
              "move",
              "mark_read"
            ],
            "type": "string"
          },
          "rule_contains": {
            "description": "Case-insensitive text to match in the header.",
            "type": "string"
          },
          "rule_field": {
            "description": "Header matched by a rule",
            "enum": [
              "from",
              "to",
              "subject"
            ],
            "type": "string"
          },
          "security": {
            "description": "config: One of: ssl, starttls — ssl = implicit TLS (993/465, the default), starttls = plain port upgraded (143/587, e.g. ProtonMail Bridge)",
            "enum": [
              "ssl",
              "starttls"
            ],
            "type": "string"
          },
          "smtp_host": {
            "description": "SMTP server host (config)",
            "type": "string"
          },
          "smtp_port": {
            "description": "SMTP port (config, default 587; 465 = implicit TLS)",
            "type": "integer"
          },
          "subject": {
            "description": "Subject (send)",
            "type": "string"
          },
          "target": {
            "description": "Destination folder for move/rule_save; new name for rename_folder.",
            "type": "string"
          },
          "to": {
            "description": "Recipient (send); defaults to original sender for reply",
            "type": "string"
          },
          "uid": {
            "description": "Message UID from list/search (read/reply)",
            "type": "integer"
          },
          "user": {
            "description": "Account username/email (config)",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Personal calendar events. Update takes id and only changed fields; moving start keeps duration. action=add|update|list|delete. Events have a title, start (required), optional end, description and location. For list, optionally pass from/to to bound the range.",
      "name": "calendar",
      "parameters": {
        "properties": {
          "action": {
            "description": "add, update, list, or delete",
            "enum": [
              "add",
              "update",
              "list",
              "delete"
            ],
            "type": "string"
          },
          "all_day": {
            "description": "All-day event. Use dates for start/end; end is exclusive (next day for one day).",
            "type": "boolean"
          },
          "description": {
            "description": "Event description",
            "type": "string"
          },
          "end": {
            "description": "End time; update: omit to keep duration, empty clears",
            "type": "string"
          },
          "from": {
            "description": "List filter: range start",
            "type": "string"
          },
          "id": {
            "description": "Event id from list, used for update/delete. Opaque: a number locally, or an object path when a CalDAV account is connected.",
            "type": "string"
          },
          "location": {
            "description": "Event location",
            "type": "string"
          },
          "start": {
            "description": "Start time, e.g. '2026-07-01 09:00' (required for add; omit on update to keep it)",
            "type": "string"
          },
          "title": {
            "description": "Event title (for add/update)",
            "type": "string"
          },
          "to": {
            "description": "List filter: range end",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Search the RAG knowledge base for relevant information from uploaded documents. Use this to answer questions based on the documentation available in a collection.",
      "name": "rag_search",
      "parameters": {
        "properties": {
          "collection": {
            "description": "Name of the document collection to search",
            "type": "string"
          },
          "limit": {
            "description": "Number of chunks to return (default: 5)",
            "type": "integer"
          },
          "query": {
            "description": "The question or search query",
            "type": "string"
          }
        },
        "required": [
          "query",
          "collection"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Add a document to a RAG collection so it can be retrieved later with rag_search. Use this for user-provided documents, reference material, or any content the user wants to query. For agent learnings use save_learning instead. Prefer source_path for files: PDF is parsed page by page, PPTX is converted to PDF first, anything else is indexed as text. ONE file per call — for a folder, list_files then ingest each file into the same collection.",
      "name": "rag_ingest",
      "parameters": {
        "properties": {
          "collection": {
            "description": "Collection name (created automatically if it doesn't exist)",
            "type": "string"
          },
          "content": {
            "description": "The full text content to index (use for inline text). Omit when using source_path.",
            "type": "string"
          },
          "source": {
            "description": "A descriptive name for this document (e.g. 'api-docs', 'meeting-notes-2025'). Defaults to the filename when source_path is used.",
            "type": "string"
          },
          "source_path": {
            "description": "Path to ONE file inside the workspace (relative to workspace root). Directories are not accepted — ingest their files one at a time.",
            "type": "string"
          }
        },
        "required": [
          "collection"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Inspect, describe or delete RAG collections. Actions: list (all collections, or the documents of one collection if collection is given), delete (one document if document is given, else the entire collection), describe (set the one-line description shown in Settings → Knowledge and in your own prompt — do it for every collection you create so future turns know what it holds).",
      "name": "rag_manage",
      "parameters": {
        "properties": {
          "action": {
            "description": "One of: list, delete, describe",
            "enum": [
              "list",
              "delete",
              "describe"
            ],
            "type": "string"
          },
          "collection": {
            "description": "Collection name (required for delete and describe)",
            "type": "string"
          },
          "description": {
            "description": "describe action: what the collection holds, one line",
            "type": "string"
          },
          "document": {
            "description": "Document filename (delete action — omit to delete the whole collection)",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Attach a file from the workspace to your response. Images (jpg, png, webp) are displayed inline in your reply. Other file types will be made available for download. Only use paths returned by other tools or known workspace locations — never invent paths.",
      "name": "add_attachment",
      "parameters": {
        "properties": {
          "path": {
            "description": "Workspace-relative path to the file to attach",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Save a fact about the user to their permanent profile. Call this whenever the user explicitly states something about themselves. Same topic overwrites the previous value.",
      "name": "save_user_info",
      "parameters": {
        "properties": {
          "content": {
            "description": "The fact as stated by the user",
            "type": "string"
          },
          "topic": {
            "description": "Short stable key, e.g. 'diet', 'job', 'location', 'hobbies'",
            "type": "string"
          }
        },
        "required": [
          "topic",
          "content"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Save a one-off lesson or gotcha to the permanent agent-learnings knowledge base. Call this when the user confirms that something complex finally worked, or a non-obvious solution was found. Retrieval is NOT reliable: it's a per-turn similarity search against the new message, top 3 above a threshold, nothing if the wording doesn't match closely — no fallback, no signal on a miss. For a repeatable PROCEDURE the user is teaching you (how to handle a kind of ticket/task), use the `skill` tool instead — skills are always listed in full every conversation, with no similarity gate.",
      "name": "save_learning",
      "parameters": {
        "properties": {
          "content": {
            "description": "The full lesson: what the problem was, what failed, what finally worked, and any gotchas to remember.",
            "type": "string"
          },
          "title": {
            "description": "Short descriptive title (e.g. 'ComfyUI install on prism-workspace')",
            "type": "string"
          }
        },
        "required": [
          "title",
          "content"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Full-text search across ALL past conversations — every workspace, the global assistant, and Telegram. Use it to recall what was said or decided earlier (even in another session) when the current context doesn't contain it. Returns matching messages with their date and session.",
      "name": "search_history",
      "parameters": {
        "properties": {
          "limit": {
            "description": "Max results (default 15)",
            "type": "integer"
          },
          "query": {
            "description": "Keywords to search for (e.g. 'postgres password', 'lisbon offsite plan')",
            "type": "string"
          }
        },
        "required": [
          "query"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Write a Python script that becomes a callable tool, named after its own header — no filename to pick or keep in sync. The script must start with exactly this comment (one line, valid JSON): # TOOL: {\"name\":\"...\",\"description\":\"...\",\"when_to_use\":\"...\",\"usage\":\"...\",\"parameters\":{\"type\":\"object\",\"properties\":{\"arg\":{\"type\":\"string\",\"description\":\"...\"}},\"required\":[\"arg\"]}}. Always fill \"when_to_use\" (the situations this tool is for — this is how you'll recognise it later, don't rely on the name) and \"usage\" (how to call it, gotchas, an example); both are optional but strongly recommended. Arguments arrive as a JSON string in sys.argv[1]. Print the result to stdout. The tool is immediately callable, by its header's \"name\", right after registration.",
      "name": "register_tool",
      "parameters": {
        "properties": {
          "code": {
            "description": "Complete Python script. Must contain a # TOOL: {...} header line (all on one line) — its \"name\" is what you'll call the tool by.",
            "type": "string"
          }
        },
        "required": [
          "code"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "List all custom tools in this workspace, each with its source-file path so you can inspect or edit it directly with read_file/write_file/edit. Tools shipped with Prism are labelled and must not be edited.",
      "name": "list_tools",
      "parameters": {
        "properties": {},
        "required": [],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Fetch a URL using a headless Chromium browser (Playwright) and return the page as a reader sees it. Use for JS-heavy pages, SPAs, sites that block plain HTTP, and for any local HTML file — a page whose content is drawn by scripts has no readable source. Optionally evaluate a JavaScript expression to extract specific data.",
      "name": "browser_get",
      "parameters": {
        "properties": {
          "script": {
            "description": "Optional JS expression evaluated in the page context (e.g. 'document.title' or 'Array.from(document.querySelectorAll(\"h2\")).map(e=\u003ee.textContent)'). If omitted, returns the full readable page text.",
            "type": "string"
          },
          "url": {
            "description": "URL to navigate to: http://, https://, or file:///workspace/... for a local HTML file (e.g. a chat attachment saved under file:///workspace/uploads/)",
            "type": "string"
          }
        },
        "required": [
          "url"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Interact with a web page using a sequence of actions. Cookies persist across calls so you can log in once and reuse the session. Use for login flows, form submission, multi-step navigation, widget preview/debugging, or any site requiring interaction. Console messages (console.log, console.error, uncaught exceptions) are always captured automatically and returned as a {\"action\":\"console\",\"messages\":[...]} entry — no special action needed.",
      "name": "browser_act",
      "parameters": {
        "properties": {
          "actions": {
            "description": "List of action objects to execute in order. Each object must have a \"type\" field. Supported types: navigate (url), click (selector), type (selector,text), clear (selector), select (selector,value), wait_for (selector), wait_ms (ms), screenshot, evaluate (expression), get_text (selector). Console output is captured automatically. Do NOT use \"wait\", \"timeMs\" or \"console\" — they do not exist.",
            "type": "array"
          },
          "url": {
            "description": "Initial URL to navigate to before running actions. Omit to reuse the current session page.",
            "type": "string"
          }
        },
        "required": [
          "actions"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Send a notification to the dashboard (appears as a toast and in the bell). Set delay_seconds for reminders — more reliable than nohup/sleep.",
      "name": "notify",
      "parameters": {
        "properties": {
          "delay_seconds": {
            "description": "Delay before sending (0 = immediate, e.g. 120 = 2 minutes from now)",
            "type": "integer"
          },
          "level": {
            "description": "Severity: info (default), success, warning, error",
            "enum": [
              "info",
              "success",
              "warning",
              "error"
            ],
            "type": "string"
          },
          "message": {
            "description": "Optional detail message",
            "type": "string"
          },
          "title": {
            "description": "Short notification title",
            "type": "string"
          }
        },
        "required": [
          "title"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Request a sensitive value (API key, password, token) from the user via a secure dialog. Stored encrypted, never appears in chat. Available as an env var in exec_command (name uppercased: 'openai_key' → $OPENAI_KEY). Returns immediately if the secret already exists.",
      "name": "request_secret",
      "parameters": {
        "properties": {
          "description": {
            "description": "Human-readable description shown to the user in the dialog (e.g. 'OpenAI API key for the MCP server')",
            "type": "string"
          },
          "name": {
            "description": "Unique name (e.g. 'openai_key', 'github_token'); reuse the exact name from secrets list. PRISM_* and built-in integration credentials are reserved.",
            "type": "string"
          }
        },
        "required": [
          "name",
          "description"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Manage stored secrets. Actions: list (names only, never values; includes your group's shared secrets), delete (name — personal secrets only), share (name + optional group — MOVES a personal secret to the group so every member's agent can use it; the value never transits the chat). To create a secret, use request_secret.",
      "name": "secrets",
      "parameters": {
        "properties": {
          "action": {
            "description": "One of: list, delete, share",
            "enum": [
              "list",
              "delete",
              "share"
            ],
            "type": "string"
          },
          "group": {
            "description": "share: group name — only needed when the user is in several groups",
            "type": "string"
          },
          "name": {
            "description": "Secret name (delete / share)",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Manage remote MCP (Model Context Protocol) servers. Actions: list (servers and their tools), add (name+url; for authenticated servers call request_secret first and pass its name as auth_secret), refresh (name; reconnect and update tools after a server update, keeping credentials and enabled state), remove (name), enable/disable (name). Changes apply immediately.",
      "name": "mcp",
      "parameters": {
        "properties": {
          "action": {
            "description": "One of: list, add, refresh, remove, enable, disable",
            "enum": [
              "list",
              "add",
              "refresh",
              "remove",
              "enable",
              "disable"
            ],
            "type": "string"
          },
          "auth_secret": {
            "description": "Optional: name of a stored secret used as Bearer token (add action)",
            "type": "string"
          },
          "name": {
            "description": "Short server name (e.g. 'github', 'linear')",
            "type": "string"
          },
          "url": {
            "description": "HTTP endpoint of the MCP server (add action)",
            "type": "string"
          }
        },
        "required": [
          "action"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Update your personality: name, tone, language, and behavior. Use this when the user asks you to change how you talk or present yourself.",
      "name": "update_system_prompt",
      "parameters": {
        "properties": {
          "personality": {
            "description": "New personality and behavior instructions (e.g. 'You are Alex, a concise French-speaking assistant...'). This replaces the current personality section.",
            "type": "string"
          }
        },
        "required": [
          "personality"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Place an outbound phone call carried out by an AI voice agent. Use it when the user asks you to call someone (e.g. \"appelle le plombier et prends rendez-vous\"). Give either contact_name (a name from the outbound directory — the phone system looks the number up) or phone_number, or both. The call is queued and placed shortly; you are told the outcome afterwards — you do not wait on the line. Give a clear, self-contained mission: the agent on the call only knows what you write here.",
      "name": "place_call",
      "parameters": {
        "properties": {
          "contact_name": {
            "description": "Name of the person/company to call, e.g. \"le plombier\". Looked up in the outbound directory when no number is given.",
            "type": "string"
          },
          "mission": {
            "description": "What the calling agent must accomplish and say — a clear, standalone brief (goal, key info to convey, what to obtain).",
            "type": "string"
          },
          "phone_number": {
            "description": "Number to call, in international format (e.g. +596696...). Omit it when contact_name is in the directory.",
            "type": "string"
          }
        },
        "required": [
          "mission"
        ],
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "List the outbound phone calls: those still queued, the one being dialled, and recently finished ones with their outcome. A status of done means the call finished, not that its mission succeeded. Read the summary before claiming the goal was accomplished; if it does not establish success, say the result is unconfirmed. Use it when the user asks what calls are planned or how a call went, and to find the id of a call to cancel.",
      "name": "list_calls",
      "parameters": {
        "properties": {},
        "type": "object"
      }
    },
    "type": "function"
  },
  {
    "function": {
      "description": "Cancel a scheduled outbound phone call that has not been placed yet. Use it when the user changes their mind (\"finalement n'appelle pas Jean\"). Call list_calls first to get the id. A call already being dialled cannot be cancelled.",
      "name": "cancel_call",
      "parameters": {
        "properties": {
          "call_id": {
            "description": "Id of the queued call, as given by list_calls.",
            "type": "integer"
          }
        },
        "required": [
          "call_id"
        ],
        "type": "object"
      }
    },
    "type": "function"
  }
]
