{"slug": "talos-0-20-a-self-hosted-ai-agent-with-a-permission-kernel", "title": "Talos 0.20 – A self-hosted AI agent with a permission kernel", "summary": "Talos released version 0.20.3, a stable update to its open-source, self-hosted AI agent for terminal and Telegram on macOS and Linux, including Raspberry Pi, that runs actions only after a deterministic permission kernel rules on them. The MIT-licensed agent supports local models via Ollama, Claude through the Claude Code CLI, or a configured API provider, and the 0.20.3 update adds native Telegram command blocks, migration from Hermes or OpenClaw into inactive staging with one validated imported skill activated at a time, and Talos Connections secret-free disabled connector drafts. The release retains the frozen operator contract, permissions and dependency locks unchanged, and the project states stable is not a claim of error-free operation or a substitute for reviewing permissions and keeping backups.", "body_md": "*An autonomous agent you can hand a shell to, because it can prove what it will not do.*\n\n| Terminal | Telegram | \n|---|---|\n\n<sub>Terminal: real local run from the current source with Ollama. Telegram: simulated transport using text generated by the current Talos implementation; no private chat or token is included. [Demo provenance](https://github.com/talos-kernel/Talos/blob/main/assets/demo/README.md).</sub>\n\n[**talos-agent.ch**](https://talos-agent.ch) ·\n  [Field manual](https://talos-agent.ch/docs/) ·\n  [FAQ](https://talos-agent.ch/faq/) ·\n  [Changelog](https://github.com/talos-kernel/Talos/blob/main/CHANGELOG.md) ·\n  **Fork Talos**\n\nTalos is an **open-source, self-hosted AI agent for terminal and Telegram** on macOS\nand Linux, including Raspberry Pi. Use **Ollama** for local models, **Claude** through\nthe Claude Code CLI, or a configured API provider. Talos searches, writes and runs\ntools — but only after a deterministic permission kernel has ruled on the action.\n**The model proposes. It never decides.**\n\nChoose **Allow once**, **Allow this task**, or an exact-action standing approval;\nyou do not have to approve every step individually. Sandboxed shell execution and an\nappend-only audit trail keep actions bounded and reviewable. Talos is MIT-licensed;\nmodel-provider and hosting costs depend on your setup. The **0.20.3 stable** release retains the frozen\n[operator contract](https://github.com/talos-kernel/Talos/blob/main/docs/beta-contract.md). Stable is not a claim of error-free\noperation or a substitute for reviewing permissions and keeping backups.\n\n[Install Talos](#install) · [Run local models with Ollama](https://talos-agent.ch/docs/#model) ·\n[Set up Telegram](https://talos-agent.ch/docs/#channels) ·\n[Watch a recorded session](https://talos-agent.ch/console.html) ·\n[Install with Pinokio](https://github.com/talos-kernel/talos-pinokio)\n\nApple Silicon on macOS 14+ with Homebrew 7+: [install the CLI with Homebrew](https://github.com/talos-kernel/Talos/blob/main/docs/homebrew.md).\n\n**New to Talos?** [Try three small, checkable tasks](https://github.com/talos-kernel/Talos/blob/main/docs/first-task.md): turn notes into\na handover, save and read back a local note, and compare release notes without inventing\nmissing facts. Already tried it? [Share your first-run result](https://github.com/talos-kernel/Talos/issues/new?template=first_run.yml),\nincluding installation blockers. No star or positive review required.\n\n**Latest signed release — [0.20.3](https://github.com/talos-kernel/Talos/releases/tag/v0.20.3):**\nthis stable update shows concrete Telegram work: native command blocks for known\ndiagnostic arguments, file and skill names, and separate measured result states. It retains safe\nagent migration, Talos Connections and labelled scheduled results. The operator\ncontract, permissions and dependency locks remain unchanged.\n\nEmoji-only streaming prefixes wait for readable text, avoiding oversized checkmark flashes.\n\n**Included in 0.20.3:** migration from Hermes or\nOpenClaw can now fingerprint and apply an exact redacted plan into inactive staging,\narchive that staging for recovery, and activate one validated imported skill at a time.\n[Talos Connections](https://github.com/talos-kernel/Talos/blob/main/docs/connections.md) adds secret-free disabled connector drafts and\nan inventory of the existing isolated-worker registry. Neither feature inherits accounts,\ncredentials, permissions, schedules or plugin authority. See the\n[migration contract](https://github.com/talos-kernel/Talos/blob/main/docs/import-agents.md) and [release changelog](https://github.com/talos-kernel/Talos/blob/main/CHANGELOG.md).\n\nRecent milestones include [missions and projects](https://github.com/talos-kernel/Talos/blob/main/docs/missions.md), managed skill\ncreation and restore, hash-locked installer tooling, and Telegram receipts that say\n`Reply done` while separately counting still-running worker jobs. The current source\nalso names safe work-trail details such as the skill or shell executable, and delivers\nscheduled results as one labelled notification without streaming their private work.\nSee the\n[full changelog](https://github.com/talos-kernel/Talos/blob/main/CHANGELOG.md).\n\n**Source versus release:** `main` can contain newer changes. Signed installers and\npackage managers follow their pinned release; older archives remain immutable.\n\n0.20.3 includes the Beta.8 pip 26.2.2 hash lock and isolates pip from per-user\nconfiguration. When upgrading from Beta.7 or older with custom pip settings, use\n`PIP_CONFIG_FILE=/dev/null talos update` once; those older updaters still inherit pip\nconfiguration.\n\n- **Run local models:** connect Ollama on hardware you control, or use a configured cloud provider.\n- **Work from terminal or Telegram:** inspect projects, organise notes, prepare reports and run permitted tools.\n- **Keep authority explicit:** approve once, grant eligible steps for the current task, or maintain exact-action standing rules. Hard denials remain denials.\n- **Inspect the evidence:** review tool outcomes and the audit trail; do not mistake a confident answer for a successful action.\n\nStart with the [model, cost and permission FAQ](https://talos-agent.ch/faq/).\nTalos is not Talos Linux or Cisco Talos. It is a self-hosted AI agent, with a stable 0.20.3 release.\nIf this approach is useful, **star the repository to bookmark it**, try a small task and\n[report a reproducible result](https://github.com/talos-kernel/Talos/issues).\nWant to improve it? [Fork Talos](https://github.com/talos-kernel/Talos/fork) and read\n[the contribution guide](https://github.com/talos-kernel/Talos/blob/main/CONTRIBUTING.md). Keep private configuration and credentials out of your fork.\n\n```\n   message ──▶ event log ──▶ reason ──▶ ╔══════════╗ ──▶ capability ──▶ execute\n                                        ║  KERNEL  ║        token          │\n                                        ╚══════════╝                       ▼\n                                             ▲                     verify ──▶ receipt\n                              the only place effects are authorised\n```\n\n| **Authority** | a token bound to exact arguments — valid **once** , for**30 seconds** | \n| **A tool without a target extractor** | `DENY` by construction, not by a rule someone wrote | \n| **Ships with** | **zero** identities that may command it | \n| **The shell** | sandboxed, or it refuses to run at all | \n\n```\ncurl -fsSL https://talos-agent.ch/install.sh | less   # read it first\ncurl -fsSL https://talos-agent.ch/install.sh | bash   # then run it\n```\n\nThe installer verifies the signature and the checksum, runs the full suite — and then\n**stops**. Nothing starts listening until you say so.\n\n## **Contents** — in the order they matter\n\n**Start here**\n[Why this exists](#why-this-exists) ·\n[What it does not do](#what-it-does-not-do) ·\n[Install](#install) ·\n[Fork and build](#fork-and-build)\n\n**Using it**\n[A session in the terminal](#a-session-in-the-terminal) ·\n[Work on the side](#work-on-the-side) ·\n[What it remembers](#what-it-remembers) ·\n[Seeing what it did](#seeing-what-it-did) ·\n[Commands](#commands)\n\n**How it holds**\n[How the kernel decides](#how-the-kernel-decides) ·\n[The autonomy dial](#the-autonomy-dial) ·\n[Timed runs](#timed-runs) ·\n[Announced plans](#announced-plans) ·\n[The browser that only reads](#the-browser-that-only-reads) ·\n[Delegating](#delegating) ·\n[WhatsApp through your own broker](#whatsapp-through-your-own-broker) ·\n[The MCP registry](#the-mcp-registry) ·\n[Identity](#identity)\n\n**Evidence**\n[What a run looks like](#what-a-run-looks-like) ·\n[Audit trail](#audit-trail) ·\n[Tools](#tools) ·\n[Architecture](#architecture) ·\n[Roadmap](#roadmap)\n\nEvery capable agent eventually asks for shell access. At that moment you are trusting a language model with your machine, and the usual answers are unsatisfying: either it asks you to confirm everything (and you stop reading the prompts by day three), or it runs free behind a regex blocklist and you hope.\n\nTalos takes a third position. **Authority is a token, not a list.** Every effect is\nauthorised individually, bound to its exact arguments and targets, valid once, for thirty\nseconds. Forgetting to call the gate does not produce an unchecked effect — it produces no\neffect at all, because the raw runners are unreachable without a token.\n\nThat design is testable, and it is tested: 277 adversarial scenarios run on every change and\ntry to get an effect past the kernel. They are in [`redteam.py`](https://github.com/talos-kernel/Talos/blob/main/redteam.py). Read them\nbefore you trust anything written above.\n\nStated plainly, because a security claim without its limits is marketing:\n\n- **`run_shell` needs a sandbox the platform can actually provide.** It runs under\nbubblewrap on Linux and`sandbox-exec` on macOS. Where neither is available it*refuses* rather than running unprotected, so on such a host the shell tool is simply unavailable\nuntil the operator overrides it on purpose (`TALOS_SANDBOX_ALLOW_UNCONFINED=1` ).\n- **It is not a multi-tenant security boundary.** One operator, one machine. Anyone who can\nrun code in the process can reach the token mint.\n- **There is no gateway, no setup portal and no `config.yaml`.** Each was considered and\nleft out, for the same reason. A gateway terminates foreign identities and then*claims*`channel:id` to the kernel instead of proving it. Identity checking would then hang on a\ncomponent outside the kernel. A web portal is a network service with its own\nauthentication, so it is a second source of permission beside the allowlist. And a\nsecond config file would be two truths about who may command it, when the path floor\nprotects exactly one. What weakens it is not the number of commands. It is a second\nway in.\n- **It does not defend against a malicious model.** It defends against a*mistaken* one,\nand against prompt injection arriving through tool output. Those are different threats.\n- **Search needs no account.** Without`TALOS_BRAVE_API_KEY` it used to refuse; now it\nanswers over DuckDuckGo (the`ddgs` package, imported at call time). A key decides*which* provider answers, not*whether* one does.\n- **It has three ways in, and all of them fetch** (Telegram long-polling, mail over\nIMAP, the WhatsApp broker queue over SSH). That is the rule, not an accident: an\ninbound webhook would need a port the world can reach, which turns \"outbound only\"\ninto \"publicly reachable\". Mail sits at`Trust.ASK` — an address proves no account,\nso it may ask and receive answers but never approve anything. WhatsApp over the\nCloud API stays delivery-only for the same reason; the broker channel below is the\none that fetches.\n- **It hears locally or not at all.**`hear` transcribes a recording with faster-whisper\non the machine it runs on — what was said is often the most private thing in a day, and\nsending it to somebody else's model to be understood is the one place where \"runs on your\nmachine\" would be traded for convenience. It is an ordinary`READ` with the file as its\ntarget, so a recording under`~/.secrets/` is refused without the module knowing anything\nabout secrets. The model loads on first use, not at boot.\n- **It can see, on a real file.** A photo you send is fetched into`workspace/inbox/` and\nthe note carries the path, so`see_image` has something to point at — and the kernel has\na target to judge. The fetch asks the allowlist first: the channel parses updates before\nthe kernel has ruled on identity, so an unconditional fetch would let anyone who finds\nthe bot write files to your disk.`speak` writes a WAV offline (piper).\n- **It can take one still out of a video.**`grab_frame` runs ffmpeg once and puts a\nsingle picture in the inbox;`see_image` looks at it afterwards. Both the video and the\npicture are targets the kernel judges, so a video under`~/.secrets/` is refused exactly\nlike a recording — without that, calling a file a video would have been the way around\nthe secret floor. Where the picture lands is derived by the kernel, not chosen by the\nmodel, and it is deliberately one frame rather than a series: fanning ffmpeg out over a\nwhole film and encoding every frame is how a small machine ends up unreachable.\n- **It does not generate pictures or video, on purpose.** Reading one is a`READ` with a\ntarget; making one is a paid call to somebody else's model, and it was removed rather\nthan kept around waiting for credit. Seeing stays.\n- **Fast evaluation is local and advisory.**`/eval <text>` uses a bounded deterministic\nclassifier with no provider, key, network call or Vercel dependency. It reports a\nmeasured duration; it never grants, denies or changes a permission. The same local\npreflight runs once before every model-backed task, including background tasks and\nresumptions, and records only category, confidence and duration. It does not replace\nor shorten Claude, Gemini, Ollama or another configured provider call; the deterministic\nkernel remains the only authority.\n\nRequires Python 3.11+ on Linux or macOS and a model connection: your own API key, or an installed, signed-in Claude or Hermes CLI. The setup wizard offers the connections available on your machine.\n\n```\ngit clone https://github.com/talos-kernel/talos.git\ncd talos\npython3 -m venv .venv && . .venv/bin/activate\npip install --require-hashes -r requirements.lock -r requirements-dev.lock\nexport PATH=\"$PWD/bin:$PATH\"              # makes `talos` available in this shell\n\npython -m talos setup terminal           # local identity + model; no Telegram needed\npython -m talos chat                     # your first conversation\n```\n\nPrefer Telegram? Run the bot wizard, then start the service:\n\n```\npython -m talos setup                    # token, identity and model\npython -m talos                          # run the Telegram agent\n```\n\n`setup` proves what it can rather than trusting what you type: the token goes to `getMe`,\nand your identity comes from a **real message you send your own bot** — not from a number\nyou copy. A wrong token announces itself on the first poll; a wrong identity does not, and\na stranger's identity never does. Setup preserves unrelated settings and writes each\nsection atomically with mode 0600. It prints the configuration path and the next command.\nUse `talos doctor` for missing prerequisites and `talos help <command>` for command help.\nA running service needs a restart after changing its file settings.\n\nComing from Hermes or OpenClaw? `talos import` produces a read-only, redacted plan for\nportable profile files, memory documents and Agent Skills. Repeating its fingerprint with\n`--apply` copies that exact plan into private inactive staging; it neither overwrites active\nstate nor inherits authority. Staging can be moved intact into a recovery archive, and an\nimported skill activates only by name through the existing managed-skill validator. Secrets\nare re-entered, permissions are approved again, and schedules/plugins/connections remain\ndisabled review tasks. See [the migration contract](https://github.com/talos-kernel/Talos/blob/main/docs/import-agents.md).\n\n**Talos Connections** is the public connector surface built into Talos. It inventories the\nexisting isolated-worker MCP registry and stores secret-free connector drafts in a disabled\nstate. It bundles no account, endpoint, credential or private service, and a draft cannot run\nuntil the operator separately installs it and opts in through both allowlists. See\n[the Connections contract](https://github.com/talos-kernel/Talos/blob/main/docs/connections.md).\n\nLater, a single part can be redone without repeating the rest:\n\n```\npython -m talos setup model              # switch what it thinks with\npython -m talos setup mail               # add the second way in (IMAP)\npython -m talos config list model        # find settings by name or description\npython -m talos config describe TALOS_STATUS_STYLE\npython -m talos config set TALOS_STATUS_STYLE expressive\npython -m talos config set TALOS_MODEL claude-fable-5-1\npython -m talos config set TALOS_MODEL_OVERRIDES '{\"local-model\": {\"context_window\": 128000}}'\n                                         # a window or a price the catalogue does not know —\n                                         # never a provider, url or key; shown as your word\npython -m talos models --refresh         # force the same provider-catalogue sync now\npython -m talos import list              # supported agent-home previews\npython -m talos import hermes --source \"$HOME/.hermes\" --dry-run\npython -m talos import openclaw --source \"$HOME/.openclaw\" --dry-run\npython -m talos import hermes --source \"$HOME/.hermes\" --apply --confirm <fingerprint>\npython -m talos connections list         # active worker registry + disabled drafts\npython -m talos status                   # what it did last\npython -m talos chat                     # a session here; approvals at a terminal\npython -m talos ask \"how many …?\"        # one turn from here, answer on stdout\npython -m talos events --tool run_shell  # what happened — read-only, filterable\npython -m talos why 4831                 # why that was allowed or refused\npython -m talos review                   # what this installation should change\npython -m talos report --out audit.txt   # what was done and what was refused\n```\n\nWith `TALOS_STATUS_STYLE=expressive`, Telegram keeps one live activity card with\ntool icons, elapsed time and an outcome for each step. It updates in place; the\nanswer stays separate. Tool and plan JSON is hidden even after a prose introduction.\nCompleted answers stay formatted after approval buttons, and Markdown tables become\ncompact labelled rows. Approval prompts keep the exact command visible.\n`/stop` interrupts the run and `/log` opens its receipts. A finished conversation\ndoes not imply that a delegated background job has finished.\n\nVersion 0.20.1 added phase-specific headings, code-styled tool identifiers and\nseparate completed/failed counts to the expressive card. A quiet activity indicator\nuses the existing five-second heartbeat; `TALOS_STATUS_ANIMATION=0` disables it.\nApproval waits stay still, and a long wait is labelled without inventing progress\nor an ETA. Version 0.20.2 added native shell code blocks for a small set of known\ndiagnostic argument grammars. Unknown arguments, inline scripts and credentials are\nvisibly omitted; service names, test paths and host aliases remain operator-facing\nmetadata. These previews never grant permission. Approval prompts retain the exact\naction. Each card distinguishes In progress, Done, Failed and Not executed; missing\nresults never become successful actions.\n\nOperator-defined OpenAI-compatible endpoints can be listed in the JSON file named by\n`TALOS_CUSTOM_PROVIDERS` (default: `data/custom-providers.json`). Each entry needs\n`name`, `base_url` and `models`; optional `env_key` names that endpoint's own key.\nThese providers use the native API adapter, including runtime fallback. Without\n`env_key` no Authorization header is sent; a named but missing key fails closed.\nBuilt-in provider names cannot be replaced. The isolated model worker does **not**\naccept agent-defined custom providers; it retains its own provider allowlist.\n\nThe long-running service refreshes configured model catalogues in a background thread at startup and at most once per day; startup and replies never wait for it. A complete, validated OpenAI, Anthropic, Ollama or LM Studio listing may add new models and remove retired ones. Paginated, incomplete, CLI/OAuth or otherwise unproven listings are additive only. Authentication, network, HTTP, JSON and empty-list failures preserve the exact last-known-good catalogue. If a complete list retires the active model, Talos hides it from new selections and records the fact, but never switches a running conversation to another model silently.\n\nEmpty API replies and stream errors stay failures throughout the configured fallback\nchain. If every permitted route fails, the task ends with an explicit error and keeps\nits context. A short promise to act gets one bounded follow-up; completed tool actions\nare never replayed. Failed channel polling backs off from 1 to 30 seconds and resets\nwhen the channel recovers, while messages received from healthy channels continue.\n`talos health` marks a channel `degraded` until a successful poll confirms recovery;\nerror counts remain in the log. Startup and recovery each record one `channel.ready`\ntransition, including empty successful polls, not an event for every idle poll.\nThis is polling evidence, not proof that a process is still alive or every reply arrived.\n\nCancellation follows the provider actually doing the work, including an active fallback. Model switching stays busy until that call finishes. Background tasks retain the same configured fallback routes with independent cancellation. After each successful tool receipt, a fresh short work announcement can receive one correction; repeating it without progress ends explicitly as unfinished.\n\nA full walkthrough — install, identity, the session, every command, and the ones that are\nmissing on purpose — is at **[talos-agent.ch/docs](https://talos-agent.ch/docs/)**.\n\n`ask` is not a second way in. It is a **channel like any other**, with the same protocol\nand no special right. Two consequences follow, and both are the point of it. While the\nallowlist is empty the local caller is admitted as `cli:<uid>` automatically, so the first\nrun works. The moment the list names one identity it becomes exhaustive, and the uid must\nappear as `cli:<uid>` exactly like a Telegram number. A shell next to the agent is not an\nargument for granting it anything. And the turn always runs under\nthe **unattended ceiling**: a one-liner waits for nothing, so `NEEDS_HUMAN` becomes `DENY`\nand says so. Approve in the chat, where somebody is actually looking.\n\nIt also refuses to run inside the agent's own sandbox. Otherwise the agent could start\n`talos ask` from its shell and give itself orders — no channel, no foreign identity,\nnobody reading along.\n\nYou do **not** need a fork to run or configure Talos. Keep a fork when you want to add a\nprovider, polling channel, blueprint, gated tool, package or red-team case — work that is\nuseful to your installation and may be useful upstream.\n\n1. \n**[Fork Talos](https://github.com/talos-kernel/Talos/fork)** on GitHub.\n2. \nClone your fork and keep this repository as `upstream` :\n\n```\ngit clone https://github.com/YOUR-ACCOUNT/Talos.git\ncd Talos\ngit remote add upstream https://github.com/talos-kernel/Talos.git\npython3 -m venv .venv && . .venv/bin/activate\npip install --require-hashes -r requirements.lock -r requirements-dev.lock\n```\n\n3. \nPick a bounded first change from the **[good first issues](https://github.com/talos-kernel/Talos/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)** ,\ncreate a branch and prove it locally:\n\n```\ngit switch -c feat/your-change\npython -m pytest tests/ -q\npython redteam.py\n```\n\n4. \nOpen a pull request that says what changed, what you ran and — for anything near the kernel — what the change makes possible that was impossible before.\n\nThe [contribution guide](https://github.com/talos-kernel/Talos/blob/main/CONTRIBUTING.md) names the safe extension points and the higher\nbar for kernel changes. If you find a way past the kernel, do not demonstrate it in a\npublic fork or issue; use the private process in [SECURITY.md](https://github.com/talos-kernel/Talos/blob/main/SECURITY.md).\n\n`ask` answers once and leaves. For anything that takes more than one turn there is\n`talos chat` — the same channel name, the same identity, the same conductor, the same\nkernel. The loop is line for line the one the service runs for Telegram; only the messages\ncome from `stdin` instead of `getUpdates`.\n\n``` bash\n$ talos chat\n\n  ◉  Talos / 0.20.3\n     Your terminal. Your rules.\n  ──────────────────────────────────────────────────────────────────────────\n  model      claude-cli/claude-opus-4-1\n  policy     autonomy 5\n  identity   cli:1000\n  session    approvals possible — you are at a terminal\n\n  /help for commands, `exit` to leave\n\n  you › what changed in the log today?\n```\n\nThe `session` line is the one to read: it says, before you ask for anything, whether an\napproval is even possible here — so a refusal later is explained in advance rather than\nguessed at.\n\n**The unattended ceiling hangs on the terminal, not on the command.** This was the\ncontested decision. Applying `ask`'s rule to an interactive session would mean `chat`\ncould never write anything; dropping it would mean `talos chat < jobs.txt` in a cron job\nlooks exactly like a human. So it is measured rather than claimed: **both `stdin` and\n`stdout` must be a real tty**. A pipe, a redirect, a cron run has none, and there\n`NEEDS_HUMAN` becomes `DENY` as before. Checking only `stdin` would let\n`talos chat > out.log` pass as attended — nobody reads a question written to a file.\n\nThe in-session commands are the same ones the messenger has, through the same command centre. A second vocabulary that only worked here is exactly the duplication this project has paid for elsewhere.\n\nSome questions take two minutes, and the conversation should not stand still for them.\n\n```\n› /background go through /var/log and summarise today's errors\n\nBackground #1 started: go through /var/log and summarise today's…\n  id bg_a1b2c3d4e5f6 · runs unattended, so anything needing approval is refused\n```\n\n**A background task is a scheduled run, not a helper carrying your rights.** That is the\nwhole security decision, and it avoids inventing a new concept. It is *not* a subagent:\nthat one is born from model text and may therefore only read. Here a human typed the task,\nso its origin is the same as any other message. But nobody is sitting in front of it — so\nthe ceiling from the timed run applies, and anything that would need approval is refused.\nPutting a question into a chat where a different conversation is running is the most\nreliable way to land a \"yes\" on the wrong thing.\n\nIts context is **empty**: the task and nothing else. Two runs sharing one history write\ninto each other, and afterwards nobody can say which of them said what. The result does not\nflow back into the history either — it arrives as its own message, marked as a report.\n\nIf no ceiling is wired, the task is **refused** rather than run uncapped. A forgotten\nparameter may only ever allow less.\n\nA running background task can be **steered**: `delegate_steer` queues a course correction,\nand supersedes unfinished reasoning before its next action — the same seam where a typed\ncorrection enters a foreground run, only from its own mailbox. The instruction arrives as a framed\nturn (\"no additional rights\"); every tool call it provokes passes the same kernel under\nthe same ceiling. Only the person and conversation that started the task may steer it,\na delegated run may not steer at all, and anything without a step boundary in this\nprocess — a synchronous `delegate`, a worker job, a timed run — answers with a refusal\nrather than a pretend \"ok\".\n\nAnd everything stoppable stops at once: `/stopall` (also `/estop`) aborts the running\nthought, drains the queue, detaches every background task at its next step and discards\nits report, and **discards** every pending approval — it never approves one. Timed runs\nstay; a stop that deleted timers would create the next incident while ending this one.\nThe reply is an honest balance per category, and a second `/stopall` says so.\n\nThe conversation is kept per channel, in memory only, bounded by turns and by characters.\nWhen the bound is reached the **middle is summarised** and both ends stay verbatim — the\nhead usually carries the actual task, the tail whatever \"and that too\" refers to.\n\n```\nYou:                  set up the backup for the NAS\nAgent:                done — see /etc/backup.d\nEarlier (summarised): discussed retention, settled on 30 days; the NFS\n                      mount needed the hard option\nYou:                  and add the offsite copy\n```\n\nThe summary is labelled as one and never disguised as a verbatim turn — a summary that\nlooks like something actually said is a claim about words nobody used, and the model could\nquote it back as if it were. It goes into the prompt as history, never into the standing\ninstructions, and the transcript reaches the summariser explicitly framed as *data*:\notherwise \"summarise this\" is the most convenient place for an injected line to become an\ninstruction, and a permanent one, because the summary stays.\n\n**The bound is not a comfort feature.** If summarising fails, the oldest turns are\ndropped as before. A history that kept growing after a failed summary would turn a cost\nquestion into a leak — what was said weeks ago would go out again.\n\nThe thin feeling with an agent like this does not come from missing verbs. It comes from\nnot being able to see what it did and why. Both of these are **read-only**:\n\n```\npython -m talos events --tool run_shell   # what happened, filterable\npython -m talos why 4831                  # why that was allowed or refused\n```\n\n`why` names the verdict, the rule that produced it, and the targets it applied to — and\nstates that those targets were *derived from the real arguments*, never taken from what\nthe model wrote. Then it shows the rest of the same run, because \"refused, and then?\" is\nthe question that makes people stop reading logs.\n\nThere is deliberately no `talos undo` beside them. `/undo` exists, and since `talos chat`\nit is reachable from the command line through the same path the messenger uses. A second\none would be convenience against the design.\n\n**`config set` refuses two whole classes of key**, and that is the point of it. Secrets\nnever go on a command line — that lands them in the shell history and in `ps` for every\nuser of the machine. And `TALOS_ALLOWED_PRINCIPALS`, `TALOS_SECRETS_ENV`,\n`TALOS_MAIL_AUTHSERV_ID`, every `TALOS_BASE_URL_<PROVIDER>` and\n`TALOS_WEB_ALLOWED_ADDRESSES` are not settings but *policy*:\nwhoever writes them does not have to talk the kernel into anything, they reconfigure it.\nNot settable, not even with a confirmation — a confirmation is exactly what gets clicked\naway. `config get` answers `[REDACTED]` for a secret **whether it is set or not**, so the\ncommand cannot be used to find out which doors a machine has.\n\n**A key belongs to one provider.** There is no single `api_key` and no single base url;\n`credentials.py` keeps a route per provider — key *and* address in one piece, resolved at\nthe moment of the call rather than frozen when the reasoner was built, because `/model`\nswitches providers while the process keeps running. If the selected provider has no key,\nthe switch is refused and the previous one stays active: no other provider's key stands\nin for it. Until 2026-08-05 one field held `ANTHROPIC_API_KEY or OPENAI_API_KEY` for every\nprovider, so choosing `openai-api` sent an Anthropic key to OpenAI as a bearer token — a\ncredential disclosure that looks like a typo, because the wrong recipient answers `401`.\n\n**There is no default allowlist.** `TALOS_ALLOWED_PRINCIPALS` must name your identity or\nthe agent refuses to start. A shipped default would be a backdoor printed in the source.\n\n`PolicyKernel.decide()` returns exactly three verdicts — `ALLOW`, `NEEDS_HUMAN`, `DENY` —\nand the executor calls no runner before it has one.\n\n| Class | Examples | Verdict | \n|---|---|---|\n| Ordinary work | `ls` ,`git status` , tests, builds, writes inside the workspace | **runs** | \n| Risky but recoverable | `curl \\| sh` ,`git reset --hard` ,`rm -rf <path>` | asks you | \n| Persistence & secrets | `.bashrc` , systemd units,`~/.ssh` , the agent's own source | asks you | \n| Catastrophic | `rm -rf /` ,`mkfs` ,`dd` to a block device, fork bomb, reboot | **refused** | \n| Secret reads, system paths | `cat /etc/passwd` , anything under`/etc /boot /usr /bin` | **refused** | \n\n`DENY` returns **before** the approval check, so a \"yes\" cannot reach a hardline rule.\nA one-shot approval has a five-minute TTL, is bound to the exact request, and is re-verified\nagainst file hashes immediately before execution (TOCTOU). When a human is asked, the\napproval surface offers four deliberately different grants:\n\n- **Allow once** approves one exact action. The approval is consumed when that action runs;\na later request asks again.\n- **Allow this task** approves every approval-required action in the current foreground task\nuntil it ends or the operator stops it. It has no time limit, includes later commands and\ntargets in that task, and does not carry into a new task. Kernel`DENY` still wins.\n- **Always allow** creates a standing rule for the exact action fingerprint — its command or\nkernel-derived target — not for the tool in general. It remains until the operator revokes\nit through`/revoke` .\n- **Deny** closes the request. Nothing runs, and a denial never becomes permission.\n\nThe model can request an action, but it cannot choose a wider grant. The kernel still judges each action, even when the question itself is skipped by a standing rule.\n\nTargets are derived from real tool arguments, never from a field the model could omit. A\ntool without a target extractor is `DENY` by construction.\n\n`/autonomy 0..5` sits above the kernel and **can only tighten, never loosen**. Level 5 is\nexactly the unfiltered kernel — not \"anything goes\". A dial that granted rights would be a\nsecond source of permission next to the kernel, which is the thing this design removes.\n\n| Level | Reads | Writes | Shell | \n|---|---|---|---|\n| 0 | refused | refused | refused | \n| 1 | asks | refused | refused | \n| 2 | free | refused | refused | \n| 3 | free | asks | asks | \n| 4 | free | free in workspace | asks | \n| 5 | free | kernel decides | kernel decides | \n\nThe level survives restarts because it lives in the event log. An unreadable log drops to level 0, not to the last convenient value.\n\n`/every 90 <task>` repeats on an interval; `/every 0 8 * * MON-FRI <task>` takes a cron\nexpression, because an interval can say \"every 90 minutes\" but never \"weekdays at 08:00\".\nAn expression is a better clock, not an extra permission — what runs afterwards passes the\nsame kernel.\n\nAnd it passes one ceiling more. During an unattended run `NEEDS_HUMAN` becomes `DENY`:\nwhat may run without asking runs, everything else is **reported rather than performed**.\nNot parked until morning either — an approval question whose occasion is six hours old is\nhow reflexive clicking starts. So a timed run may do strictly *less* than something you\ntyped, which is the opposite of how cron usually works.\n\nA timed run stays quiet while it works. Its final result arrives once, labelled\n`Scheduled run`, with the job ID plus `/schedules` and `/unschedule` controls. This is\nseparate from an interactive work trail: the detailed tool evidence remains available in\nthe audit log without making a background run look like the current conversation.\n\nA blueprint can give a timed run a **memory** and a **probe**. With `continuity: true`\nthe previous result is placed in front of the task as data — \"yesterday 91 %, and\ntoday?\" — and a run that ends in the same failure as its predecessor is logged instead\nof repeated into the chat. With `monitor: true` and a `probe` (a shell command) the\nprobe is read *before* the model is asked: an unchanged output means no model call at\nall, with `schedule.skipped_unchanged` in the log. The probe is an ordinary `run_shell`\nof the task's principal through the same executor, kernel and sandbox, under the same\nunattended ceiling — no new permission. And a probe that fails, whatever the reason,\nfires the run: a broken sensor must not be the way to silence a watcher.\n\nFor a task with several steps the agent may announce the sequence before it starts:\n\n```\n≡ 3 steps — collect the log, find the failing service, report\n```\n\nThe announcement changes nothing about permission. Every step still passes the kernel one\nat a time, exactly as if it had been asked for alone, and a step that needs approval still\nstops and asks. What the announcement does is **bind the run**:\n\n- the step budget shrinks from the house limit to what was announced — a three-step plan cannot become forty tool calls;\n- the first step that fails ends the run with a report of what ran, what stopped it, and what therefore did not happen — instead of the model improvising around the failure, which is how an agent turns a refusal into a bigger second attempt;\n- the plan is read **once** . A tool result is a stranger's text; if it could install a\nsecond, larger plan mid-run, prompt injection would be a way to buy budget.\n\nA step may carry a condition, and the condition is **not evaluated by the model**:\n\n```\nPLAN: {\"goal\": \"restart and confirm\", \"steps\": [\n  {\"intent\": \"restart the service\", \"check\": \"contains:rc=0\"},\n  {\"intent\": \"write the report\",     \"check\": \"wrote:/tmp/report.md\"}\n]}\n```\n\nThis closes a real hole. `run_shell` returns `rc=1` and the executor still records\n`DONE` — because the *tool* ran. Whether the *work* succeeded lived only in the output\ntext, and the only reader of that text was the model, which has an interest in the\nanswer. A condition is checked in code, in the order announced, against the receipt of\nthe step that ran: `ok` (finished cleanly), `contains:<text>` (its output held that\ntext), `wrote:<path>` (it wrote exactly there, as the *kernel* derived the target — not\nas the model claimed it).\n\nA condition can only ever withhold a confirmation. If one never comes true, the answer is delivered in full and the system adds its own line beneath it:\n\n```\n✕ 1/2 announced checks met — NOT confirmed done. Still open: step 2 (it writes to /tmp/report.md).\n```\n\nThe honest limit: **the model writes the conditions too.** A met condition proves\nthat the run's own stated expectation came true — not that the job was done well. Someone\nwho sets themselves a trivial condition passes it. What the code guarantees is narrower and\nstill worth a lot: that the condition was really *evaluated* rather than asserted, that it\nis shown to you word for word so you can judge its worth, and that the real count also\nlands in the event log — which the model cannot write to, and where a forged verdict line\nin its prose is contradicted.\n\nConditions inspect only the receipt of their own step, never the state of the world. One\nthat could read arbitrary files would be an oracle around the kernel (*\"read `/etc/shadow`\nand tell me whether it contains root\"*), and an unknown condition is dropped rather than\ncounted, so inventing vocabulary gains nothing.\n\nSo planning here does the opposite of what it usually does: it makes a run more\npredictable and *less* powerful. That is deliberate, and it is the same inversion the\nunattended ceiling makes — a capability that arrives with more rights attached is not a\ncapability this design accepts. Under that ceiling the two compose without knowing about\neach other: an unattended plan stops at the first step that would need a human, and says\nso.\n\n`web_fetch` returns source; half the web is an empty shell without JavaScript. `browse`\nrenders the page in real Chromium and hands back what a reader would see.\n\nWhat it deliberately cannot do is **operate** a page — no clicking, typing or submitting.\nA click has no derivable target: \"click the third element\" cannot be mapped to a resource\nyou could bind a permission to, and a tool without a derivable target is `DENY` here by\nconstruction. That is the right answer rather than a shortfall — an agent that operates\nstrangers' pages has a path to effect that no kernel catches afterwards.\n\nIt is also fenced more tightly than a URL check alone can manage. Chromium is pinned to\nexactly the address `guard_url` verified, and nothing else resolves at all:\n\n```\n--host-resolver-rules=\"MAP * ~NOTFOUND, MAP <host> <ip>\"\n```\n\nA redirect to another name goes nowhere, a script pulled from an ad network likewise, and a DNS rebind between check and fetch — the classic hole in every filter that inspects only the first name — has no effect. The cost is honest: pages that load their content from a CDN under a different name come back incomplete. Incomplete beats uncontrolled.\n\nEvery call gets a throwaway profile: no cookies, no signed-in sessions, no history. A page Talos opens sees a factory-fresh browser.\n\n`delegate` hands one self-contained question to a second run — and that run **may only\nread**. No writing, no shell, nothing that needs approval. It uses the same executor, the\nsame kernel and the same identity as its caller; what changes is a fourth ceiling that\nturns everything except reading into `DENY` for as long as it lasts.\n\nThe reason is the same one that shapes timed runs. A delegated run is born from *model\ntext*, not from someone typing. Giving it the rights of the run that started it would make\ndelegation the most convenient way to produce an effect nobody asked for — a second source\nof permission wearing a different name. So delegation buys reach, never power: it is for\nlooking things up without filling the main run's context with the search. What comes back\nis data, bounded like any tool result, never an instruction.\n\nThe Cloud-API channel (`whatsapp.py`) can only deliver, because Meta's inbound path is a\nwebhook — and a webhook is a port the world can reach. The broker channel\n(`wabroker.py`) gets inbound without breaking the rule: a listener on **an\noperator-controlled WhatsApp broker reachable over SSH** appends the messages routed to\nTalos to a JSONL queue, and Talos *pulls* that queue over SSH with a persisted byte\ncursor. Every connection goes outward, to a machine the operator controls, with the\noperator's own key. Nothing listens.\n\nReplies go back the same way — through the broker's send script, base64-safe; files\ntravel as `scp` plus broker. The sender number comes out of the operator's own WhatsApp\naccount rather than a text field, so the channel carries `Trust.FULL`, and the\nconversations live in the same `whatsapp:<number>` namespace as before. Three env vars,\nall off by default: `TALOS_WA_BROKER_SSH` (the SSH target — empty means the channel does\nnot exist), `TALOS_WA_BROKER_QUEUE` and `TALOS_WA_BROKER_CLI_DIR`. Configured, the broker\nwins over the Cloud-API variant: both are named `whatsapp`, and this is the one that\nfetches. A failed poll is a loud `channel.error`, and the cursor never advances on\nfailure — silence would look exactly like an empty queue. Operator details:\n[docs/whatsapp-broker.md](https://github.com/talos-kernel/Talos/blob/main/docs/whatsapp-broker.md).\n\nMCP is shipped — over the claude-worker seam, not into the agent. Talos itself never\nspeaks MCP; the `claude -p` child inside a UID-separated worker job does, and Talos only\nwrites the MCP config and passes it through. Which servers may exist at all is an\noperator decision, declared in `data/mcp-servers.json` (gitignored, fail-closed like\n`entities.json`): `version: 1` mandatory, absolute commands only, an `env` field costs\nthe whole entry, sixteen servers at most. The socket frame carries server *names*, never\ncommands or args.\n\nAnd it is gated twice: a server must appear in the registry file **and** in the worker's\n`TALOS_CLAUDE_WORKER_MCP_SERVERS` allowlist, or the request is refused by name before any\njob starts. There is no marketplace and no third-party code in the agent process — a\ncurated starting point is [examples/mcp-servers.json](https://github.com/talos-kernel/Talos/blob/main/examples/mcp-servers.json), the\nfull contract is [docs/claude-worker.md](https://github.com/talos-kernel/Talos/blob/main/docs/claude-worker.md).\n\n`SOUL.md` carries the agent's **name and character**. Its first heading is the name —\nchange `# TALOS` to `# ARGUS`, restart, and the agent is renamed everywhere, including the\nheader of the live display. There is no second place where the name lives.\n\n`AGENTS.md` carries durable operating discipline; `USER.md` carries stable preferences\nfor the operator. All three reload on the next message, have explicit context limits, are\nprotected by the persistence floor, and survive updates as operator-owned state. Missing\n`AGENTS.md` or `USER.md` is harmless; a missing or broken `SOUL.md` uses the safe fallback.\n\nA plain question gets a plain answer — no status chrome. The moment a tool runs, one message starts tracking it and stays afterwards as the receipt:\n\n```\n◉ Talos · 9s · step 2/100\n✓ shell command  2s\n▸ write note — disk.md\n```\n\n`◉` agent · `◈` thinking · `≡` plan announced · `▸` tool · `✓` done · `✕` failed ·\n`⏸` waiting on you · `⛒` refused · `↩` rolled back. One glyph, one meaning, never in the prose of an answer.\n\n`exec.intent` is written **before** anything happens:\n\n```\nexec.intent → approval.parked → approval.granted → exec.intent → exec.result\n```\n\nA crash mid-action still leaves a trace. Every run carries the id of the token that\npermitted it. `/log` shows the last effects, `/undo` rolls back the last file change.\n\nThirty-three, and every one of them passes the same gate. There is no privileged tool and no\ntool that skips the kernel — a tool without a target extractor is `DENY` by construction.\n\n| `run_shell` | a command, sandboxed, or refused where no sandbox exists | \n| `computer_run` /`computer_status` | guarded computer use: bounded desktop actions and their status, each step through the gate | \n| `remote_exec` | a command on another machine over ssh — operator-allowlisted hosts, always a human's yes, standing rules bind to exact host+command | \n| `http_request` | any REST API — read methods through the SSRF-hardened door, state-changing methods always a human's yes, binding to exact method+URL | \n| `git` | clone / fetch / pull / push with credentials — every op a human's yes, binding to exact op+repo+remote; local git work stays in the sandboxed shell | \n| `cli_anything` | a pinned CLI-Anything harness (curated third-party CLI) from the operator-owned registry — unknown harness or subcommand is `DENY` , the rest always a human's yes, binding to exact name+subcommand;[registry format](https://github.com/talos-kernel/Talos/blob/main/docs/cli-anything.md) | \n| `entity_status` | a known name resolved to an operator-configured fixed read-only probe | \n| `mailbox_list` /`mailbox_read` | the configured inbox, read-only through IMAP EXAMINE and BODY.PEEK; no sending, marking, moving or deletion | \n| `read_file` /`write_file` /`undo_last` | ordinary work on the kernel-derived path, with atomic publication and a snapshot behind the write | \n| `browse` /`web_fetch` /`web_search` | render-only, guarded URL, keyless search by default | \n| `see_image` /`grab_frame` /`hear` /`speak` | a picture, one still out of a video, a recording, a voice | \n| `read_document` | a PDF, Word, Excel or PowerPoint file turned into text — locally, framed as untrusted | \n| `vault_search` /`vault_get` /`vault_write_note` | a markdown knowledge base, if you point it at one | \n| `session_search` | what was said in earlier turns | \n| `delegate` | a sub-run that can only read | \n| `delegate_code` /`delegate_dag` /`delegate_status` | a bounded coding job — or a small acyclic graph of them — for a confined Claude worker: opt-in, off by default, writes only into a kernel-derived disposable workspace | \n| `delegate_agy` | the same confined worker's second backend (the agy CLI): same trust form and workspace, behind its own opt-in gates on agent and worker, no MCP | \n| `delegate_codex` | bounded implementation and independent review with Codex: the same kernel and sandbox, isolated auth, verified stream completion and originating-chat notifications; [setup](https://github.com/talos-kernel/Talos/blob/main/docs/codex-worker.md) | \n| `delegate_steer` | a course correction into a *running*`/background` task — a framed turn that supersedes unfinished reasoning, never a new right | \n| `agent_consult` | bounded advice from a second, operator-configured agent — data, never permission | \n| `ask_operator` | the one way it can ask you something on purpose | \n| `skill_write` | a new skill, written exactly once — and never without a human's yes | \n| `skill_view` | inspect managed skills, supporting files, hashes and archived revisions | \n| `skill_manage` | create, edit, patch, deactivate and restore skills; every change needs approval | \n\nTalos ships with no real entity names, hosts or service units. To enable entity-aware status checks, copy the neutral example and replace every placeholder with infrastructure you control:\n\n```\nmkdir -p data\ncp examples/entities.json data/entities.json\n```\n\n`data/entities.json` is runtime state: Git ignores it, updates preserve it, and Talos reads\nit as context rather than authority. `entity_status` accepts only a configured entity name;\nthe fixed URL or systemd user unit comes from this file and cannot be supplied by the model.\n\nTalos can turn verified successful workflows into reusable skills and improve them later. The prompt receives a compact catalog; skill bodies are read on demand. Skills remain untrusted guidance: every suggested action still passes the kernel.\n\n`skill_view` lists the managed library, reads a skill or supporting file, and returns\n`expected_sha256` plus archived revisions. `skill_manage` supports `create`, `edit`,\n`patch`, `write_file`, `remove_file`, `delete` and `restore`. Every mutation is declared\nirreversible: it needs operator approval, cannot receive attended auto-approval, and is\ndenied during unattended runs. The original create-only `skill_write` stays compatible.\n\nChanges require the current hash from `skill_view` (or `\"absent\"` for a new file).\nPatches require a unique match unless `replace_all` is explicit. Prior content is archived\nunder `.history`; restoration verifies its hash. `delete` deactivates the skill by removing\n`SKILL.md`, retaining supporting files and history so it can be restored. It is not secure\nerasure. Text files may live under `references/`, `scripts/`, `templates/` and `assets/`;\nscripts are stored as data and never run automatically. Files stay inside the managed\nroot, private by default, and secret-shaped content and `allowed-tools` are refused.\n\nConcurrent manager calls are serialized. Hash checks reject stale edits, but they are not an atomic compare-and-swap against external editors: avoid editing the same file while approving a change. History is bounded to 200 revisions per skill; reaching the limit requires operator maintenance, never silent pruning.\n\nTwo blueprints use it on a schedule. `skill-distillation` (\"every sunday 20:15\") reads the\nweek's event log and distils at most one repeated, proven workflow into a skill candidate —\nwritten through `skill_write` when a human is there to approve, parked as a vault note under\n`patterns/` when the unattended run is refused. And `daily-reflection` closes each day with\na look at the same log — verified live against a real installation, schedules and ceiling\nincluded, not only in the suite.\n\nThere is no image generation. That is a decision, not a gap: an agent that can produce photographs is a different conversation than one that can only look at them.\n\nThe repository contains a generic `QmpInputAdapter` for bounded visual input over a\npinned QMP transport. It is not tied to a guest distribution or desktop. A successful\ninput receipt records dispatch only; it does not prove guest delivery or a visible\nresult.\n\nThe planned macOS deployment is deliberately narrower: one fixed **x86_64 Debian guest\nwith Hyprland**, running under QEMU **TCG** on Apple silicon, not HVF. The intended\nboundary excludes inbound forwarding, bridged networking, host folders, clipboard\nsharing and host credentials. Shell, browser-protocol, file and routine operations\nremain outside this visual-only path.\n\nThat deployment is not yet hardware-qualified. It requires a signed,\noperator-supplied VM bundle and a matching pre-provisioned disk; Talos does not bundle\nor download QEMU or a guest disk. Evidence collected for any earlier guest prototype\nis not reusable for this fixed contract. The ARM64 Linux/KVM Computer is a separate\nbackend documented in [the Computer guide](https://github.com/talos-kernel/Talos/blob/main/docs/computer.md); the planned Mac contract\nis described in [the QMP VM boundary](https://github.com/talos-kernel/Talos/blob/main/docs/qmp-debian-vm.md).\n\n**In a session** — the same set in the terminal and in the messenger, through one\ncommand centre.\n\n`/stop` `/stopall` `/queue` `/status` `/new` `/retry` `/background` · `/pending` `/approve` `/deny`\n`/allowed` `/revoke` · `/log` `/undo` `/policy` `/autonomy` `/tools` `/whoami` `/version` ·\n`/usage` `/model` `/eval` `/reasoning` `/debug`\n\n**On the command line** — each answering a question an operator actually asks:\n\n| `chat` | a session here; approvals at a terminal | \n| `ask \"…\"` | one turn, for scripts and cron | \n| `setup` ·`doctor` ·`config` ·`models` | set it up, and find out why something won't work | \n| `import list` ·`import <hermes\\|openclaw> …` | preview, stage, activate one skill, or archive rollback | \n| `connections list` ·`connections draft …` | isolated-worker inventory and disabled secret-free connector drafts | \n| `status` ·`events` ·`why <id>` | what it did, and why that was allowed or refused | \n| `report` ·`review` | a record someone else can read · what this installation should change | \n| `update` ·`version` | signature checked, tests first | \n\n`/policy <path|command>` is a dry run: it calls `decide()` and shows the verdict without\nexecuting anything. It is the fastest way to understand the kernel.\n\n`/eval <text>` is a separate local diagnostic. The local classifier reports a\nlikely category and explicitly does not establish truth. Its result is never consulted\nby `decide()` and cannot approve an action.\n\nSmall modules on purpose. The gate path (`policy.py`, 996 lines) has to be readable in one\nsitting — a gate you cannot read is not a gate.\n\n| Module | Role | \n|---|---|\n| `policy.py` | the kernel: target derivation, three verdicts | \n| `capability.py` | tokens: mint, redeem, invalidate | \n| `command_floor.py` | hardline and dangerous command detection | \n| `approval.py` /`standing.py` | one-shot and standing approvals | \n| `autonomy.py` /`trust.py` /`channel.py` | ceilings above the kernel | \n| `executor.py` /`verifier.py` /`snapshot.py` | write-ahead execution, TOCTOU, undo | \n| `conductor.py` /`agent_loop.py` /`worker.py` | orchestration | \n| `plan.py` /`schedule.py` /`subagent.py` | announced sequences, timed runs, delegation — all only tighten | \n| `web.py` /`browser.py` | the only door out: URL guard, then a resolver cage | \n| `reasoner.py` /`provider.py` | pluggable model backends | \n| `intelligence.py` /`evaluation.py` | bounded entity context, working state, factual review, adaptive reasoning and trace regression metrics | \n| `credentials.py` | one route per provider — key *and* address, resolved at call time | \n| `sandbox.py` | bubblewrap / `sandbox-exec` ; refuses rather than running unconfined | \n| `identity.py` /`ux.py` | name and glyphs | \n| `telegram.py` /`mail.py` /`whatsapp.py` /`wabroker.py` | channels — every way in fetches, none listens | \n| `vision.py` /`hearing.py` /`speech.py` /`frames.py` | reading a picture, hearing a recording, speaking, one still out of a video — ordinary READ/WRITE with a target | \n| `cli.py` /`doctor.py` /`configcli.py` /`schema.py` | the subcommands: diagnose, read and change settings — the schema decides what may be written, and the allowlist and the network exceptions never are | \n| `askcli.py` | `talos ask` — one turn from a script, as a channel with no special right | \n| `import_agents.py` /`import_apply.py` | bounded migration plans, inactive atomic staging, recovery archive and managed skill activation | \n| `connections.py` /`mcpservers.py` | public disabled connection drafts and the operator-owned isolated-worker registry | \n| `models.py` /`model_catalog_sync.py` | provider-bound live model lists; complete snapshots may replace membership, incomplete ones only add, and failures keep the last good cache | \n| `updater.py` | update beside the old tree, both suites in the new one, switch only if green | \n| `eventlog.py` /`memory.py` /`usage.py` | durable log, conversation memory, metering | \n| `skillwrite.py` | the one gated write into the skills directory | \n\nThe [beta release evidence](https://github.com/talos-kernel/Talos/blob/main/docs/beta-readiness.md) separates source tests, signed\nartifact checks and the seven-day operating observation. The frozen\n[beta contract](https://github.com/talos-kernel/Talos/blob/main/docs/beta-contract.md), [recovery runbook](https://github.com/talos-kernel/Talos/blob/main/docs/recovery.md) and\nlocal `python scripts/beta-gate.py --previous <published-version>` make these checks repeatable.\nThe [stable qualification record and gates](https://github.com/talos-kernel/Talos/blob/main/docs/stable-readiness.md) cover the\ntransition from RC.3 to `0.20.0`, including seven days of deployed observation and\nseparate final-artifact checks. The path to 1.0 remains a separate compatibility\ncommitment supported by operating and upgrade evidence; it is not a calendar promise.\n\n1. `openat2()` with`RESOLVE_BENEATH` instead of realpath checks.\n2. **An adapter seam for tools.** Adding a channel is already just implementing a\nprotocol; adding a*tool* still touches three places, one of them inside the kernel\n(`TARGET_EXTRACTORS` ). The seam should make a new effect routine without making the\nkernel optional — everything still passes the same gate, the same tokens, the same\nsnapshots, the same receipts.\n3. **Ownership as the boundary, not just the floor.** While the agent and its config file\nbelong to the same writable user, the floor and the sandbox are code in the same\nprocess. A separate user for the model worker would make it a real separation. On a\nhardened installation the config already belongs to root and the agent may only read it.\n\nLanded since this list was first written: the sandbox for `run_shell` (bubblewrap /\n`sandbox-exec`, see [What it does not do](#what-it-does-not-do)), streaming replies,\ntimed runs under a ceiling that makes them weaker than typed ones,\n[announced plans](#announced-plans) built so that planning tightens a run instead of\nwidening it, read-only delegation, a render-only browser, the media tools, hearing on the\nmachine itself, one still out of a video, keyless search, **mail as a second way in**,\na command line that grants nothing by being one, **MCP over the claude-worker seam**\n(an [operator-owned registry](#the-mcp-registry) instead of a marketplace),\n[WhatsApp through an operator-controlled broker](#whatsapp-through-your-own-broker), and\n[skills the agent may write — but never alone](#skills-it-writes-for-itself).\n\nStart with **[Fork and build](#fork-and-build)** and the\n**[good first issues](https://github.com/talos-kernel/Talos/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)**.\nProvider adapters, blueprints, packaging, tests and documentation are the most useful\nfirst contributions because they improve the agent without making the gate harder to\naudit.\n\nChanges to the kernel (`policy.py`, `capability.py`, `command_floor.py`, `approval.py`,\n`standing.py`, `autonomy.py`, `trust.py`, `verifier.py`, `executor.py`) need:\n\n1. `pytest` ,`redteam.py` and`e2e.py` green, in that order.\n2. **Any loosening must add a red-team case** proving the boundary next to it still holds.\n\nIf you find a way past the kernel, that is the most valuable thing you can contribute —\nand it belongs in the **private** channel, not in a public issue: see\n[SECURITY.md](https://github.com/talos-kernel/Talos/blob/main/SECURITY.md), which also states the scope and the limits no patch will\nremove. Everything else — a missing tool, a refusal you disagree with, a wrong answer —\nis a normal issue and welcome as one.\n\nMIT. See [LICENSE](https://github.com/talos-kernel/Talos/blob/main/LICENSE).\n\nTelegram receipts distinguish a finished reply from ongoing delegated work: `Reply done`\ncan include a count of pending background jobs whose results arrive separately.", "url": "https://wpnews.pro/news/talos-0-20-a-self-hosted-ai-agent-with-a-permission-kernel", "canonical_source": "https://github.com/talos-kernel/Talos", "published_at": "2026-11-11 17:55:37+00:00", "updated_at": "2026-11-11 18:21:17.731365+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-safety", "developer-tools", "artificial-intelligence"], "entities": ["Talos", "Ollama", "Claude Code CLI", "Hermes", "OpenClaw", "Raspberry Pi", "Pinokio", "Homebrew"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/talos-0-20-a-self-hosted-ai-agent-with-a-permission-kernel", "markdown": "https://wpnews.pro/news/talos-0-20-a-self-hosted-ai-agent-with-a-permission-kernel.md", "text": "https://wpnews.pro/news/talos-0-20-a-self-hosted-ai-agent-with-a-permission-kernel.txt", "jsonld": "https://wpnews.pro/news/talos-0-20-a-self-hosted-ai-agent-with-a-permission-kernel.jsonld"}}