Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ jobs:
- name: Check public hygiene
run: python scripts/check-public-hygiene.py

- name: Check FAQ search metadata and links
run: python scripts/check-site-discovery.py

- name: Verify embedded signature-verifier pins
run: python scripts/sync-verifier-lock.py --check

Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<p align="center">
<a href="https://talos-agent.ch"><b>talos-agent.ch</b></a> ·
<a href="https://talos-agent.ch/docs/">Field manual</a> ·
<a href="https://talos-agent.ch/faq/">FAQ</a> ·
<a href="CHANGELOG.md">Changelog</a> ·
<a href="https://github.com/talos-kernel/Talos/fork"><b>Fork Talos</b></a>
</p>
Expand Down Expand Up @@ -53,6 +54,20 @@ operation or a substitute for reviewing permissions and keeping backups.
[Watch a recorded session](https://talos-agent.ch/console.html) ·
[Install with Pinokio](https://github.com/talos-kernel/talos-pinokio)

## Is Talos right for you?

- **Run local models:** connect Ollama on hardware you control, or use a configured cloud provider.
- **Work from terminal or Telegram:** inspect projects, organise notes, prepare reports and run permitted tools.
- **Keep authority explicit:** approve once, grant eligible steps for the current task, or maintain exact-action standing rules. Hard denials remain denials.
- **Inspect the evidence:** review tool outcomes and the audit trail; do not mistake a confident answer for a successful action.

Start with the [model, cost and permission FAQ](https://talos-agent.ch/faq/).
Talos is not Talos Linux or Cisco Talos. It is a self-hosted AI agent, and it remains beta.
If this approach is useful, **star the repository to bookmark it**, try a small task and
[report a reproducible result](https://github.com/talos-kernel/Talos/issues).
Want to improve it? [Fork Talos](https://github.com/talos-kernel/Talos/fork) and read
[the contribution guide](CONTRIBUTING.md). Keep private configuration and credentials out of your fork.

```
message ──▶ event log ──▶ reason ──▶ ╔══════════╗ ──▶ capability ──▶ execute
║ KERNEL ║ token │
Expand All @@ -70,7 +85,7 @@ operation or a substitute for reviewing permissions and keeping backups.

```bash
curl -fsSL https://talos-agent.ch/install.sh | less # read it first
curl -fsSL https://talos-agent.ch/install.sh | sh # then run it
curl -fsSL https://talos-agent.ch/install.sh | bash # then run it
```

The installer verifies the signature and the checksum, runs the full suite — and then
Expand Down
110 changes: 110 additions & 0 deletions scripts/check-site-discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Check the public FAQ's search metadata and crawlable paths without network access."""
from __future__ import annotations

import json
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import unquote, urlsplit
from xml.etree import ElementTree

ROOT = Path(__file__).resolve().parents[1]
SITE = ROOT / "site"
ORIGIN = "https://talos-agent.ch"


class Page(HTMLParser):
def __init__(self, path: Path):
super().__init__(convert_charrefs=True)
self.nodes: list[tuple[str, dict]] = []
self.ids: list[str] = []
self.schemas: list[dict] = []
self.schema_text: str | None = None
self.title = ""
self.in_title = False
self.feed(path.read_text(encoding="utf-8"))

def handle_starttag(self, tag, attrs):
fields = dict(attrs)
self.nodes.append((tag, fields))
if "id" in fields:
self.ids.append(fields["id"])
if tag == "title":
self.in_title = True
if tag == "script" and fields.get("type") == "application/ld+json":
self.schema_text = ""

def handle_data(self, data):
if self.in_title:
self.title += data
if self.schema_text is not None:
self.schema_text += data

def handle_endtag(self, tag):
if tag == "title":
self.in_title = False
if tag == "script" and self.schema_text is not None:
self.schemas.append(json.loads(self.schema_text))
self.schema_text = None

def attrs(self, tag):
return [fields for name, fields in self.nodes if name == tag]


def check() -> None:
faq = Page(SITE / "faq/index.html")
assert len(faq.attrs("h1")) == 1, "FAQ must have one H1"
assert faq.attrs("html")[0].get("lang") == "en"
assert 25 <= len(faq.title) <= 65 and "Talos" in faq.title and "FAQ" in faq.title
assert len(faq.ids) == len(set(faq.ids)), "Duplicate FAQ ids"
assert len(faq.attrs("details")) >= 15, "Expected substantive question coverage"
assert len(faq.attrs("summary")) == len(faq.attrs("details"))
metadata = {item.get("name", item.get("property")): item.get("content", "")
for item in faq.attrs("meta")}
assert 80 <= len(metadata["description"]) <= 170
assert "noindex" not in metadata.get("robots", "")
assert metadata["og:url"] == ORIGIN + "/faq/"
assert metadata["twitter:card"] == "summary_large_image"
assert metadata["og:image"].startswith(ORIGIN + "/")
canonical = [item["href"] for item in faq.attrs("link") if item.get("rel") == "canonical"]
assert canonical == [ORIGIN + "/faq/"]
graph = faq.schemas[0]["@graph"]
assert graph[0]["url"] == canonical[0]
assert graph[1]["@type"] == "BreadcrumbList"
assert graph[1]["itemListElement"][-1]["item"] == canonical[0]
assert not faq.attrs("form") and not faq.attrs("iframe"), "FAQ must stay static"

# Resolve actual local files and fragments, not just strings that look like URLs.
for tag, fields in faq.nodes:
target = fields.get("href") if tag in ("a", "link") else fields.get("src")
if not target:
continue
url = urlsplit(target)
if url.netloc and url.netloc != "talos-agent.ch":
continue
assert not url.scheme or url.scheme == "https", target
path = url.path or "/faq/"
assert path.startswith("/"), target
local = SITE / unquote(path).lstrip("/")
if local.is_dir():
local /= "index.html"
assert local.is_file(), f"Missing link target: {target}"
if url.fragment:
assert unquote(url.fragment) in Page(local).ids, f"Missing fragment: {target}"

ns = {"s": "http://www.sitemaps.org/schemas/sitemap/0.9"}
locations = [node.text for node in ElementTree.parse(SITE / "sitemap.xml").findall("s:url/s:loc", ns)]
assert locations.count(ORIGIN + "/faq/") == 1
for name in ("index.html", "docs/index.html"):
page = Page(SITE / name)
assert any(link.get("href") == "/faq/" for link in page.attrs("a")), name
assert ORIGIN + "/faq/" in (ROOT / "README.md").read_text(encoding="utf-8")
assert ORIGIN + "/faq/" in (SITE / "llms.txt").read_text(encoding="utf-8")
robots = (SITE / "robots.txt").read_text(encoding="utf-8")
assert "Sitemap: " + ORIGIN + "/sitemap.xml" in robots
assert "Disallow: /faq" not in robots
print(f"Site discovery checks passed: metadata, schema, {len(faq.attrs('details'))} FAQ entries, assets, fragments and discovery links.")


if __name__ == "__main__":
check()
4 changes: 3 additions & 1 deletion site/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@
<div class="crt-fx" aria-hidden="true"></div>

<a class="skip-link" href="#main-content">Skip to content</a>
<nav class="marble-nav" aria-label="Main navigation"><a class="marble-brand" href="/">TALOS<span>THE OPERATOR'S AGENT</span></a><div class="marble-links" id="marble-links"><a href="/#kernel">Kernel</a><a href="/#grants">Grants</a><a href="/docs/">Codex</a><a href="/console.html">Command Hall</a><details class="explore"><summary>Explore</summary><div><a href="/#chronicle">Chronicle</a><a href="/#does">Deeds</a><a href="/#tools">Arsenal</a><a href="/#nail">The Nail</a><a href="/#ledger">Ledger</a><a href="/#limits">Bounds</a><a href="/#numbers">Proof</a><a href="/registry/">Roll of Arms</a><a href="/vergleich/">Weigh the Orders</a><a href="/redteam/">Adversarial trials</a><a href="/watcher/">The Watcher</a></div></details><a class="nav-cta" href="/#install">Raise the keep ↗</a></div><button class="mobile-menu" aria-expanded="false" aria-controls="marble-links">Menu</button></nav>
<nav class="marble-nav" aria-label="Main navigation"><a class="marble-brand" href="/">TALOS<span>THE OPERATOR'S AGENT</span></a><div class="marble-links" id="marble-links"><a href="/#kernel">Kernel</a><a href="/#grants">Grants</a><a href="/docs/">Codex</a><a href="/console.html">Command Hall</a><a href="/faq/">FAQ</a><details class="explore"><summary>Explore</summary><div><a href="/#chronicle">Chronicle</a><a href="/#does">Deeds</a><a href="/#tools">Arsenal</a><a href="/#nail">The Nail</a><a href="/#ledger">Ledger</a><a href="/#limits">Bounds</a><a href="/#numbers">Proof</a><a href="/registry/">Roll of Arms</a><a href="/vergleich/">Weigh the Orders</a><a href="/redteam/">Adversarial trials</a><a href="/watcher/">The Watcher</a></div></details><a class="nav-cta" href="/#install">Raise the keep ↗</a></div><button class="mobile-menu" aria-expanded="false" aria-controls="marble-links">Menu</button></nav>


<main>
Expand All @@ -239,6 +239,8 @@ <h1 id="main-content" tabindex="-1">Talos AI agent: the field manual</h1>
model, and runs tools: a real shell, a confined Claude worker, a browser on request.
No tool runs until a deterministic kernel has ruled on the
action. <strong>The model proposes; it does not decide.</strong></p>
<p class="lede">New here? Read the <a href="/faq/">Talos FAQ: models, costs and permissions</a>
before choosing your setup.</p>
<p class="lede">That one decision shapes the rest of this manual. It is why the
command list is short, and why a few obvious conveniences are missing on purpose.</p>
<div class="spec-row">
Expand Down
Loading
Loading