{
  "markdown": "# sentinelx-cloud-core\n\nOperate your Linux, macOS, and Windows servers from Claude.ai or ChatGPT — safely. SentinelX gives\nyour LLM an **allowlisted, auditable** shell: it can only run commands you've\nexplicitly permitted, filesystem access is gated by a per-path allowlist, and\nevery action is recorded. No inbound ports — just a single outbound WebSocket.\n\nThe security model is the point. Handing an LLM unrestricted shell on a server\nyou care about is the thing SentinelX is designed to avoid: the allowlist is the\nreal trust boundary, so the agent can't run — or invent — anything you didn't\nallow. This is the agent you install on the host; structured file edits and\nservice management come with it.\n\n<p align=\"center\">\n  <img src=\"https://sentinelx.pensa.ar/sentinelx-ram.png\" alt=\"SentinelX checking a server's RAM and storage from ChatGPT\" width=\"480\">\n  <br>\n  <sub><i>SentinelX in ChatGPT &mdash; it reads the allowlist, runs only what's permitted (<code>df -h</code>, <code>/proc/meminfo</code>), and reports back.</i></sub>\n</p>\n\n## Install\n\nMost people start with the one-liner — it auto-detects Linux or macOS:\n\n```bash\ncurl -fsSL https://get.sentinelx.app | bash\n```\n\n**Windows** (PowerShell — needs Python 3.12+ and `git` on `PATH`):\n\n*Service install* — runs as LocalSystem at boot; needs an **elevated** PowerShell:\n\n```powershell\niwr -useb https://get.sentinelx.app/install.ps1 -OutFile \"$env:TEMP\\sx.ps1\"\npowershell -ExecutionPolicy Bypass -File \"$env:TEMP\\sx.ps1\"\n```\n\n*Per-user install* — no admin; runs as you at logon (locked-down machines):\n\n```powershell\niwr -useb https://get.sentinelx.app/install.ps1 -OutFile \"$env:TEMP\\sx.ps1\"\npowershell -ExecutionPolicy Bypass -File \"$env:TEMP\\sx.ps1\" -User\n```\n\nThe installer clones this repo into a virtualenv, registers the agent as a\nservice (systemd / launchd / Windows service), and walks you through enrollment.\nOn networks that block PyPI, add `-Bundle <zip-or-url>` for an offline install\nfrom the wheel bundle on the latest release; the agent uses the OS trust store\n(`truststore`) so a TLS-inspecting proxy's CA is accepted. Full options —\nper-user vs service, offline bundle, all flags, uninstall — live in\n[`sentinelx-cloud-installer`](https://github.com/pensados/sentinelx-cloud-installer).\n\nSentinelX is also listed in the [ChatGPT app directory](https://chatgpt.com/apps/sentinelx/asdk_app_69f63e01766881919640f03b5e7912a5) —\nChatGPT users can connect it in one click, no custom MCP URL required.\n\nThis repo is also the agent's source — read on if you want to audit or\ncontribute.\n\n## Architecture\n\n```\n                                        Internet\n                                            │\n   ┌────────────────┐                       │                  ┌─────────────────┐\n   │  Claude.ai or  │   MCP over HTTPS      │   WebSocket      │  Your host      │\n   │   ChatGPT      │ ◄───────────────────► │ ◄──────────────► │                 │\n   │                │   (OAuth via Google)  │                  │  ┌───────────┐  │\n   └────────────────┘                       │                  │  │  agent    │  │\n                                ┌───────────────────────┐      │  │ (this)    │  │\n                                │   mcp.sentinelx.app   │      │  └─────┬─────┘  │\n                                │  (SentinelX hub —     │      │        │        │\n                                │   closed source)      │      │   shell, edit,  │\n                                └───────────────────────┘      │   service mgmt  │\n                                                               └─────────────────┘\n```\n\nThe agent is the box on the right. It opens **one outbound WebSocket** to the\nhub at install time (after enrollment) and stays connected. No inbound ports,\nno port-forwarding, no reverse tunnel.\n\n## What runs where\n\n| Component | Where | What it does |\n|---|---|---|\n| `sentinelx-cloud-core` (this repo) | `/opt/sentinelx-cloud-core` on your host | Receives MCP tool calls from the hub, executes them locally, returns output |\n| Hub | `mcp.sentinelx.app` (operated by Pensa) | Auth, multi-host routing, MCP transport |\n| Config | `/etc/sentinelx/config.yaml` | Allowlist: which commands, services, and paths the agent will accept |\n| Identity | `/etc/sentinelx/identity.json` | The agent's enrollment JWT, used to authenticate the WebSocket handshake |\n\n**Supported platforms:** any modern Linux distribution with `systemd`\n(tested on Ubuntu 22.04 / 24.04 and Debian 12), **macOS** with `launchd`\n(Intel and Apple Silicon), and **Windows** — as a service (WinSW, admin) or\nas a no-admin per-user Scheduled Task (`-User`); see *Install on Windows*\nbelow. The one-line installer auto-detects Linux and macOS; Windows uses a\nPowerShell installer. The agent also runs unmodified inside **WSL2**.\n\n## Tools exposed\n\nThe agent exposes its host's operations as MCP tools to your LLM via the hub:\n\n| Tool | What it does |\n|---|---|\n| `sentinel_exec` | Run an allowlisted shell command |\n| `sentinel_script_run` | Run a one-off bash or python3 script |\n| `sentinel_edit` | Structured file edit (replace, regex, replace-block, write, append, prepend) |\n| `sentinel_edit_upload_*` | Three-step upload for large file edits |\n| `sentinel_move` | Move/rename a file or directory |\n| `sentinel_copy` | Copy a file or directory |\n| `sentinel_delete` | Delete a file or directory |\n| `sentinel_chmod` | Change file permissions |\n| `sentinel_chown` | Change file owner/group |\n| `sentinel_service` | systemctl start/stop/restart/reload/status |\n| `sentinel_restart` | Shortcut for `sentinel_service` with action=restart |\n| `sentinel_upload_file` | Single-shot file upload to the host |\n| `sentinel_upload_*` | Three-step chunked upload for large files |\n| `sentinel_read` | Read a file's contents, with optional line-range slicing |\n| `sentinel_list` | Structured directory listing (name, type, size, mtime) |\n| `sentinel_search` | Recursive content search with regex and glob filters |\n| `sentinel_capabilities` | Host policy/capabilities; optional bounded summary projection |\n| `sentinel_help` | Rich orientation/help; optional topic/path/single-playbook projections |\n| `sentinel_state` | Internal agent state, for debugging |\n| `sentinel_ping` | Cheap connectivity check |\n\n### Progressive introspection (agent operation contract)\n\nThe agent-side `help` and `capabilities` operations also support optional\nnarrow-response selectors. A Hub/MCP profile may expose these fields through\nwhatever model-facing tool shape it uses; the selectors are backend-operation\nsemantics and do not depend on full vs compact tool names.\n\n- `help({\"topic\":\"index\"})` — small topic + paged playbook index.\n- `help({\"topic\":\"security\"})` — one broad help section.\n- `help({\"path\":\"security_model.permission_errors\"})` — one exact leaf from\n  the existing help tree.\n- `help({\"playbook\":\"update_sentinelx_code\"})` — one playbook only; optional\n  `path`, `offset`, and `limit` can select/page a subfield such as `steps`.\n- `capabilities({\"detail\":\"summary\"})` — host/operation/limit metadata and\n  policy counts without command/service/location/playbook bodies.\n\nProgressive help/playbook responses normalize explicit full-profile\n`sentinel_*` references to `op:<name>` canonical operation hints so the same\nguidance can be routed through compact or full presentation without teaching a\nplaybook two sets of MCP tool names.\n\nFor compatibility, empty `help({})` and `capabilities({})` requests retain the\nlegacy full responses. A Hub that wants compact-first behavior should map its\ncompact help/capabilities branches to the narrow selectors rather than changing\nthe agent's legacy empty-payload semantics.\n\n`sentinel_read`, `sentinel_list`, and `sentinel_search` are **read-only\nfilesystem primitives**. `sentinel_edit` and the mutating primitives\n(`sentinel_move`, `sentinel_copy`, `sentinel_delete`, `sentinel_chmod`,\n`sentinel_chown`) **write**. All of them give the LLM structured access to\nthe filesystem without shelling out to `cat`/`ls`/`mv`/`rm` through `exec`,\nand all of them are gated by the same **path allowlist** (`file_ops` in the\nconfig, see below), not the command allowlist. Each path in that allowlist\ndeclares an access level: `r` (read-only ops) or `rw` (read-only ops **plus**\nthe writing ops). Destructive operations that overwrite or remove an existing\ntarget make a timestamped backup first.\n\nThe hub additionally exposes a handful of **hub-side integrations** (Cloudflare DNS, Resend email, Telegram) as MCP tools your LLM can use alongside the agent's tools — those live on the hub, not in this repo. See [the integrations table on sentinelx.app](https://sentinelx.app/#integrations).\n\n## Config (`/etc/sentinelx/config.yaml`)\n\nA starter config is generated at install time. Editable. Reloaded when the\nservice restarts. Schema:\n\n```yaml\n# Commands the agent can execute via the `exec` op. Prefix-matched against\n# this list; empty/missing = nothing allowed (deny by default).\nallowed_commands:\n  - uptime\n  - df -h\n  - free\n  - systemctl\n  - sudo systemctl\n  - journalctl\n  # See config.example.yaml for the full starter list (file inspection,\n  # networking, containers, git, etc.) plus opt-in categories\n  # (Cloudflare tunnels, WireGuard, Android tooling, firewalling, SSH).\n\n# Service units the agent is allowed to control via `service` / `restart`.\n# Each unit explicitly lists which actions are permitted.\nservices:\n  nginx:\n    actions: [status, start, stop, restart, reload]\n  docker:\n    actions: [status, restart]\n  # The agent itself, so the LLM can reload policy after editing the\n  # config. Restarting re-reads /etc/sentinelx/config.yaml. Conservative\n  # actions only — no start/stop, since the agent can't remotely start\n  # itself once stopped.\n  sentinelx-cloud-core:\n    actions: [status, restart, is-active, is-enabled]\n  # postgresql:\n  #   actions: [status, start, stop, restart, reload]\n\n# Optional: named playbooks the LLM can read or execute. Two shapes are\n# supported and can coexist in the same map:\n#\n# 1) Diagnostic playbook — a fixed sequence of allowlisted commands the\n#    agent can run in order. Useful for \"give me a quick health snapshot\"\n#    type prompts where you want one named entry-point.\n#\n# 2) Procedure playbook — a structured recipe for the LLM to follow,\n#    expressed in `description` / `when` / `steps` / `requires` / `notes`\n#    fields. Pure documentation: the agent does NOT execute the steps,\n#    the LLM reads them via `capabilities` and then calls the regular\n#    tools (sentinel_exec, sentinel_edit, sentinel_service) on its own.\n#\n# See `config.example.yaml` for the full reference.\nplaybooks:\n  # Diagnostic playbook (shape 1)\n  health:\n    description: \"Show system health summary\"\n    commands:\n      - \"uptime\"\n      - \"df -h /\"\n      - \"free -m\"\n\n  # Procedure playbook (shape 2) — guides the LLM through extending the\n  # allowlist itself. Useful so users can ask \"let me run htop here\" and\n  # the LLM knows the exact procedure (edit config, restart service,\n  # verify with capabilities).\n  add_allowed_command:\n    description: \"How to add a new command to this host's allowlist\"\n    when: \"User asks to allow a new command on this host\"\n    steps:\n      - \"Read /etc/sentinelx/config.yaml with sentinel_exec\"\n      - \"Insert under allowed_commands with sentinel_edit (sudo, validator_preset=yaml)\"\n      - \"Restart the agent: sentinel_service restart sentinelx-cloud-core\"\n      - \"Verify with sentinel_capabilities\"\n\n# Logging\nlog:\n  path: /var/log/sentinelx/core.log\n  level: INFO\n\n# SSRF defense for upload_file's file_url. The hostname must be in this\n# allowlist AND must resolve to a public-routable IP (no loopback,\n# RFC1918, link-local, etc.). Default empty = file_url disabled.\n# Only add hosts YOU control — third-party hosts (github.com, pypi,\n# random CDNs) expose your agent to supply-chain compromise.\nsecurity:\n  trusted_fetch_hosts:\n    - drop.pensa.ar\n    - get.sentinelx.app\n  file_url_timeout_seconds: 15\n\n# Filesystem primitives (sentinel_read/list/search + edit, move, copy,\n# delete, chmod, chown). Gated by a PATH allowlist, separate from the\n# command allowlist above. Empty/missing = all the filesystem primitives\n# are effectively disabled (they return path_not_allowed for any input).\n#\n# Each entry declares an access level:\n#   r  = read-only ops (read, list, search) may touch this subtree\n#   rw = read-only ops AND writing ops (edit, move, copy, delete,\n#        chmod, chown) may touch this subtree\n#\n# A path is permitted only if, after canonicalization (symlinks\n# resolved, .. collapsed), it falls under one of these entries. That\n# canonical-resolve-then-prefix-check is what defeats both path\n# traversal and symlink escapes. Writing ops additionally require the\n# matched entry to be `rw`.\n#\n# Back-compat: an older `allowed_read_paths:` list is still accepted and\n# is interpreted as a set of `r` entries (with a deprecation warning).\nfile_ops:\n  paths:\n    - path: /etc/nginx\n      access: r\n    - path: /var/log\n      access: r\n    - path: /home/youruser/projects\n      access: rw\n  max_read_bytes: 65536       # per read; larger files come back truncated\n  max_list_entries: 1000      # per list\n  max_search_results: 200     # per search\n```\n\nThe agent **only** runs commands that prefix-match `allowed_commands`. So\nallowing `git` lets the LLM run `git status`, `git log`, etc.; allowing\n`ls` is enough to cover `ls -lah /var/log`. Out of the box the config is\nrestrictive — see `config.example.yaml` for the full starter list with\nsensible categories.\n\nThere are **two independent allowlists**, and they protect different ops:\n\n- `allowed_commands` gates `exec` (and the commands inside `script_run`).\n- `file_ops.paths` gates every filesystem primitive — the read-only ones\n  (`sentinel_read`, `sentinel_list`, `sentinel_search`) on any `r` or `rw`\n  entry, and the writing ones (`sentinel_edit`, `sentinel_move`,\n  `sentinel_copy`, `sentinel_delete`, `sentinel_chmod`, `sentinel_chown`)\n  only on `rw` entries.\n\nSo a directory listed as `r` lets the LLM inspect it but not modify it; a\ndirectory listed as `rw` allows both. A directory in neither is invisible to\nall the filesystem primitives (the LLM would have to fall back to `exec`,\nwhich is governed by `allowed_commands` instead).\n\nOne deliberate exception: `sentinel_edit` with `sudo=true` is **not** gated\nby `file_ops.paths`. The trust boundary for sudo'd edits is the operator's\nsudoers policy, not the path allowlist — this is what lets the\n`add_allowed_command` playbook edit the root-owned config. Path\ncanonicalization still runs (no traversal/symlink bypass); only the\n`rw`-membership check is waived for the sudo path. This carve-out and its\nresidual risk are documented in [`THREAT_MODEL.md`](./THREAT_MODEL.md)\n(§4.2.1).\n\n## Security model\n\n- **No inbound ports.** Only an outbound WebSocket to the hub.\n- **JWT-bound identity.** `identity.json` is signed by the hub at enrollment.\n  Compromising one host doesn't grant access to others.\n- **Allowlist-gated.** Anything not in `config.yaml` returns\n  `command_not_allowed`. The agent won't synthesize new commands. **This is\n  the actual security boundary** — not the unix user, not sudo policy.\n  When a command is rejected, the agent returns a classified error\n  (multi-line input, bash keyword, shell pipeline, or simply not in the\n  allowlist) that points the LLM at the right tool instead of guessing.\n- **Path-allowlisted filesystem primitives.** Every structured filesystem\n  op only touches paths under `file_ops.paths`. Read-only ops (`sentinel_read`,\n  `sentinel_list`, `sentinel_search`) work on `r` and `rw` entries; writing\n  ops (`sentinel_edit`, `sentinel_move`, `sentinel_copy`, `sentinel_delete`,\n  `sentinel_chmod`, `sentinel_chown`) require an `rw` entry. Paths are\n  canonicalized — symlinks resolved, `..` collapsed — *before* the prefix\n  check, so neither path traversal nor a symlink pointing outside the\n  allowlist can escape it. Empty allowlist = the primitives are disabled.\n  Writing ops that overwrite or delete an existing target back it up first\n  (timestamped `.bak`). `sentinel_edit` with `sudo=true` is a documented\n  exception to the `rw` check — see `THREAT_MODEL.md` §4.2.1.\n- **Unprivileged user with passwordless sudo.** The agent runs as `sentinelx`,\n  not as root. By default the installer grants `sentinelx` passwordless sudo\n  so it can manage services and edit system files — but it can still only\n  invoke what's in your allowlist. To run with no sudo, set\n  `SENTINELX_SKIP_SUDO=1` during install.\n- **SSRF-defended `file_url`.** When `upload_file` is called with a URL, the\n  agent validates the hostname against `security.trusted_fetch_hosts`,\n  resolves it to an IP, and rejects loopback / RFC1918 / link-local addresses\n  (so an attacker can't pivot to cloud metadata services or LAN-internal\n  hosts). Redirects are disabled, https only, default timeout 15s. The\n  allowlist defaults to empty — `file_url` is effectively disabled until\n  the operator opts into specific hosts.\n- **Path-traversal-defended uploads.** All `target_path` arguments are\n  resolved under `upload_base` via `safe_path_under()`; `..` and absolute\n  paths that escape are rejected up front.\n- **No telemetry.** The agent reports nothing about your host or activity to\n  anyone but the hub you're explicitly connected to.\n\nFor a deeper view, see [`THREAT_MODEL.md`](./THREAT_MODEL.md) (assets,\nadversaries, trust boundaries, per-threat mitigations) and\n[`SECURITY.md`](./SECURITY.md) (vulnerability reporting + disclosure\npolicy).\n\n## Local development\n\n```bash\ngit clone https://github.com/pensados/sentinelx-cloud-core\ncd sentinelx-cloud-core\npython3 -m venv .venv\nsource .venv/bin/activate\npip install -e '.[dev]'\npytest                              # unit tests\n```\n\nTo run the agent against a hub other than the production one:\n\n```bash\nSENTINELX_HUB_URL=wss://localhost:8000/agent/connect \\\n  python3 -m sentinelx_core --identity-file /tmp/dev-identity.json\n```\n\n## Vendored: pensa-safe-edit\n\nThe actual file mutation for `sentinel_edit` is done by a small stdlib-only\nmodule vendored at `src/sentinelx_core/vendored/pensa_safe_edit.py`. It is\ncalled in-process via its Python API (not shelled out), so there is no\n`shell=True` anywhere in the edit path. It does its work via temp files +\natomic rename, makes a timestamped backup before mutating, preserves file\nmetadata, and can run an optional pre-commit validator\n(json/yaml/toml/python/sh/nginx/systemd presets) — if validation fails the\noriginal file is left untouched. It is still also registered as a `pip`\nconsole-script entry point for standalone/manual use.\n\n## Related\n\n- [`sentinelx-cloud-installer`](https://github.com/pensados/sentinelx-cloud-installer) — the bash + python installer\n- [`sentinelx-cloud-protocol`](https://github.com/pensados/sentinelx-cloud-protocol) — wire format spec\n\n## License\n\nApache License 2.0 — see [LICENSE](./LICENSE).\n",
  "bytes": 18975,
  "sha": "3ed17e777fb32575b2cd06c7a460cdafe86accc8db91477eeff7e663f6d9f5c1",
  "repo_slug": "pensados/sentinelx-cloud-core",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_app_sentinelx_sentinelx_3760c65f/readme"
}