# Teleos

> See who and what is landing code in your codebase — engineers, bots, agents — and which human vouched for each change. Self-hosted: your data never leaves your infrastructure.

Teleos shows how software is built, not how it runs. It reads the one place every change has to land (your GitHub org), resolves who answers for each change (merger → approver → domain owner → declared automation owner), and tracks team ownership of code areas over time (as of then / as of now). Ships as one signed container with a `teleos` CLI (`brew install teleos`); `teleos scan OWNER/REPO --json` is the agent interface. Built by Wiglaf Software.


<!-- https://docs.teleos.dev/setup/requirements/ -->

Teleos is in **private pilot**. You need pilot access to the release repository and the Homebrew tap; ask to be added.

## Docker

Docker Desktop 24+ (macOS, Windows) or Docker Engine 24+ (Linux). Teleos ships as one signed container image. SQLite is included and persisted in a Docker volume, so there is no database to provision.

Anywhere Docker runs is fine: a laptop, a small VM, ECS, Kubernetes. Teleos needs very little.

## The GitHub CLI (`gh`)

The `teleos` launcher reads your GitHub token from `gh auth token`, so there's no token to paste.

```sh
brew install gh
gh auth login
gh auth refresh -s read:packages   # pilot only: lets Docker pull the private image
```

## GitHub access

Teleos reads, never writes. It needs read access to repository metadata, contents, pull requests and org membership.

| Where Teleos runs | How it authenticates |
|---|---|
| Your laptop (`teleos scan`, `teleos up`) | your `gh` login |
| A server | a read-only fine-grained personal access token today; a **customer-owned GitHub App** is coming (tokens minted per installation, no personal token on the server) |

Your token lives in your environment or `.env` and never leaves your machine. Teleos does not phone home.

---

<!-- https://docs.teleos.dev/setup/install/ -->

## Homebrew (pilot)

```sh
brew tap TeleosApp/tap https://github.com/TeleosApp/homebrew-tap
brew trust teleosapp/tap            # Homebrew 7+ asks you to trust third-party taps
brew install teleos

teleos doctor                       # checks Docker, gh, package access, release access
```

`brew install teleos` installs the **launcher**, a small script that runs the signed Teleos container. Upgrades: `brew upgrade teleos && teleos upgrade`.

## Two ways to use it

**One-shot scan, nothing installed:**

```sh
teleos scan rails/rails --days 30
teleos scan your-org/your-repo --json   # machine-readable, for agents
```

**A local instance with the web UI:**

```sh
teleos up        # writes ~/.teleos/{docker-compose.yml,.env}, starts, creates your admin account
teleos open      # http://localhost:8000
```

## Verify before you run it

Every release is signed and carries two bills of materials:

```sh
teleos verify    # checksums, signed checksums, and the image signature
teleos sbom      # what's inside the container
teleos sbom --app   # the source dependencies
```

See [Security & verification](/security/verification/).

## Manual Docker Compose

If you manage the files yourself, pin a released version (never `latest`):

```yaml
services:
  teleos:
    image: ghcr.io/teleosapp/teleos:0.3.3
    container_name: teleos
    ports:
      - "${PORT:-8000}:8000"
    env_file:
      - .env
    environment:
      DATABASE_PATH: /app/data/db.sqlite3
    volumes:
      - teleos-data:/app/data
    restart: unless-stopped

volumes:
  teleos-data:
```

Then create `.env` (see [Environment variables](/configuration/environment-variables/)) and run `docker compose up -d`.

---

<!-- https://docs.teleos.dev/setup/first-run/ -->

## Health

```sh
teleos status
curl http://localhost:8000/api/health/ready
# {"status": "ok", "version": "0.3.3", "database": "ok"}
```

## First sync

```sh
docker exec teleos uv run python manage.py sync_org --days 90
```

This reads 90 days of merged pull requests across your org. Then read who vouched for them:

```sh
docker exec teleos uv run python manage.py ownership --days 30
```

## What a first read looks like

```
Teleos scan — rails/rails, last 30 days

  • 14% of 137 merged changes carry an AI signal (19 assisted, 0 autonomous).
  • 5 people merged 69% of changes.
  • 100% of changes landed in code no CODEOWNERS rule covers (1 rule).
```

## Troubleshooting

- **`teleos doctor` fails:** it prints the exact fix for each check.
- **401 from GitHub:** the token expired or lacks read scopes. With the launcher, run `gh auth status`.
- **Logs:** `teleos logs` (or `docker logs -f teleos`).

---

<!-- https://docs.teleos.dev/concepts/ownership/ -->

Teleos shows how software is built, not how it runs: who and what landed each change, and which human answers for it.

## A change is owned by whoever vouched for it

A robot can't be held accountable. Authorship can be anything (a person, an agent, a bot, an agent acting on a person's token), so ownership resolves in this order:

1. **The human who merged it.** Merging is vouching.
2. Otherwise, **a human who explicitly approved it.** Bot approvals don't count.
3. Otherwise, **the owner of the domain it landed in** (CODEOWNERS or a curated map).
4. Otherwise, for fully autonomous changes, **the owner declared for that automation** (an identity's accountable team).
5. Otherwise **nobody**. An unvouched change is a finding.

## Attribution is evidence, ranked

| Rank | Evidence | Example |
|---|---|---|
| Declared | GitHub App installations, identity review, team rosters | an app installed with write access to all repos |
| Observed | behavior in the data: unsigned commits from a person's account, machine branch names, activity minutes after an install | an agent opening a PR under a human account |
| Named | the signatures file (a vendor-naming hint, never the only basis) | `claude[bot]` → Anthropic |

## Teams own areas, and organizations evolve

A **domain** is an area of code (e.g. Billing). Its **scope** (which repo paths belong to it) and its **assignment** (which team owns it) are both *effective-dated*. A reorg or a moved directory never edits history; the old fact ends and a new one begins.

Every report can be read two ways:

- **As of then** (the default, and the audit answer): a change counts for the team that owned the area when it landed, using the paths as they were then.
- **As of now**: today's owners and paths applied to the whole window. *What happened in the areas I just inherited?*

```sh
manage.py team_domains forge --days 60 --lens then
manage.py team_domains forge --days 60 --lens now
```

## Teleos reads your org chart from your code

Ownership sources, each tagged on every fact: **CODEOWNERS**, **team repository permissions**, **service-catalog files** (Datadog `entity.datadog.yaml`, Backstage `catalog-info.yaml`, OpsLevel), and a curated map. When sources disagree, Teleos reports the contradiction rather than silently picking one.

Status: CODEOWNERS, dated domains and vouching are shipped. Catalog files, versioning ownership from each file's history, and a `teleos.yaml` curated map are in progress.

---

<!-- https://docs.teleos.dev/reference/cli/ -->

The `teleos` launcher wraps the signed container. Everything runs on your machine. Your GitHub token comes from `gh auth token` (or `GITHUB_TOKEN` if set).

| Command | What it does |
|---|---|
| `teleos scan OWNER/REPO [--days N] [--json]` | One-shot scan in a throwaway container; nothing installed |
| `teleos up` | Install and start a local instance in `~/.teleos` and create the admin account |
| `teleos status` / `logs` / `open` / `down` | Manage the local instance |
| `teleos upgrade` | Move to the latest signed release |
| `teleos doctor` | Check Docker, `gh`, package access and release access; prints the fix for each |
| `teleos sbom [--app] [--path]` | This release's bill of materials: the container (default) or source dependencies (`--app`); checksum-verified |
| `teleos verify` | Verify checksums, the signed checksums and the image signature with cosign |
| `teleos version` | Launcher and image versions |

Environment: `TELEOS_HOME` (default `~/.teleos`), `TELEOS_VERSION` (pin a release), `TELEOS_IMAGE`, `TELEOS_REPO`.

## `teleos scan --json` (schema `teleos.scan/v1`)

Progress goes to stderr; stdout carries only the JSON.

```json
{
  "schema": "teleos.scan/v1",
  "repo": "rails/rails",
  "window_days": 30,
  "generated_at": "2026-09-26T02:13:30+00:00",
  "teleos_version": "0.3.3",
  "signatures_version": "2026.09.25",
  "findings": ["14% of 137 merged changes carry an AI signal (19 assisted, 0 autonomous)."],
  "changes": {
    "merged": 137,
    "authors": 68,
    "ai_signal": {"none": 118, "assisted": 19, "autonomous": 0},
    "ai_actors": {},
    "bot_authors": {}
  },
  "ownership": {
    "codeowners_rules": 1,
    "vouched_by": {"merged_by": 137, "approved_by": 0, "domain_owner": 0, "declared_owner": 0, "unowned": 0},
    "landed_in_unowned_code": 137,
    "vouch_concentration": {"distinct_vouchers": 30, "top_n": 5, "top_n_share_pct": 69},
    "unvouched": []
  }
}
```

There are no ranked lists of named people: Teleos reports concentration, not leaderboards.

---

<!-- https://docs.teleos.dev/reference/commands/ -->

Run inside the container: `docker exec teleos uv run python manage.py <command>`.

| Command | What it does |
|---|---|
| `sync_org [--days 90]` | Sync merged pull requests across the connected GitHub org |
| `sync_repos --repos a/b,c/d [--days N]` | Sync specific repositories |
| `enrich_ai_mode [--repo a/b]` | Read AI co-author trailers from each PR's commits |
| `scan OWNER/REPO [--days N] [--json]` | The engine behind `teleos scan` |
| `ownership [--repo a/b] [--days 30] [--no-sync]` | Who vouched for each change; share landing in code nobody owns |
| `team_domains TEAM [--days 14] [--lens then\|now]` | A team's domains and what landed in them, as of then or as of now |
| `sync_installations` | GitHub App installations on the org; bots with no active installation behind them |
| `signatures [--unknown]` | The loaded agent-signature definitions, and bot identities none of them recognize |
| `generate_sbom` | Rebuild the source bill of materials |

Local development only: `seed_demo --reset` builds a synthetic workspace (two teams, a reorg, humans, agents and bots) in a resettable database.

---

<!-- https://docs.teleos.dev/security/verification/ -->

## Your data stays yours

Teleos runs on your infrastructure. It reads from GitHub with your token and stores what it reads in your database. It sends nothing to Wiglaf Software: no telemetry, no license check-in.

## Every release is reproducible

Every input is pinned: Python dependencies by lockfile, base images and build tools by digest. A release built today and rebuilt tomorrow from the same tag uses the same inputs.

## Two bills of materials per release

| File | Contents |
|---|---|
| `teleos-image-sbom.cdx.json` | Everything inside the shipped container: OS packages, the Python environment, binaries (CycloneDX, generated by syft) |
| `teleos-sbom.cdx.json` | Source dependencies, including the JavaScript bundled into the UI |

Both are attached to the GitHub Release and covered by the signed checksums. The container SBOM is also attached to the image as a cosign attestation.

```sh
teleos sbom          # summarize the container SBOM for your installed version
teleos sbom --app    # source dependencies
teleos sbom --path   # print the file path, e.g. to hand to your scanner
```

## Verify signatures yourself

Releases are signed keylessly with Sigstore cosign by the release workflow. No keys to trust but the workflow's identity.

```sh
teleos verify
```

Or by hand:

```sh
ID='https://github.com/TeleosApp/teleos/.github/workflows/release.yml@refs/heads/main'
ISSUER=https://token.actions.githubusercontent.com
cosign verify-blob SHA256SUMS --bundle SHA256SUMS.sigstore.json \
  --certificate-identity "$ID" --certificate-oidc-issuer "$ISSUER"
sha256sum -c SHA256SUMS
cosign verify ghcr.io/teleosapp/teleos@<digest> \
  --certificate-identity "$ID" --certificate-oidc-issuer "$ISSUER"
```

Note: keyless signing records the release workflow's identity in Sigstore's public transparency log.

---

<!-- https://docs.teleos.dev/configuration/environment-variables/ -->

Configure Teleos with a `.env` file next to `docker-compose.yml` (`teleos up` writes one for you). Never commit it.

## Required for a server

| Variable | Description |
|---|---|
| `GITHUB_TOKEN` | Read-only GitHub token. The launcher fills it from `gh auth token`; on a server use a read-only fine-grained token |
| `SECRET_KEY` | Long random string for signing sessions. Generate once; keep it secret |
| `DEBUG` | Set to `false` on any server |
| `ALLOWED_HOSTS` | Comma-separated hostnames Teleos answers to |
| `CSRF_TRUSTED_ORIGINS` | Origins allowed to submit forms, e.g. `https://teleos.example.com` |

## Optional

| Variable | Default | Description |
|---|---|---|
| `PORT` | `8000` | Host port |
| `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING` or `ERROR` |
| `SCHEDULER_ENABLED` | `true` | Background sync schedule |
| `DATABASE_PATH` | set by compose | SQLite file location inside the container |
| `TELEOS_SIGNATURES_PATH` | bundled | Use a newer agent-signature definitions file without upgrading |
| `TELEOS_TIER` / `TELEOS_LICENSE_KEY` | | Licensed tier (pilot: not enforced) |
| `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY` or `GITHUB_APP_PRIVATE_KEY_PATH` | | GitHub App credentials (in progress) |

## Local development only

| Variable | Description |
|---|---|
| `DISABLE_AUTH` | Skip the login screen. **Requires `DEBUG=true`; Teleos refuses to start otherwise.** Only applies to requests from this machine |
| `TELEOS_DEV_USER` | Which user `DISABLE_AUTH` signs you in as; switch per session with `?dev_as=<username>` |

## Generating a SECRET_KEY

```sh
python3 -c "import secrets; print(secrets.token_urlsafe(50))"
```