Skip to content
PIPER

PIPER

An AI DevOps copilot that cannot invent a command.

It reads your servers, finds what is broken, and cites the output behind every claim — driving ssh, docker, kubectl and friends from a typed catalog it is not allowed to step outside of. Runs on a 4B model on your laptop, with no API key and nothing leaving the machine.

CI License: Apache 2.0 Bun TypeScript Tests Local first

Why · Quick start · Run it local · The gate · Catalog · Security


Important

The LLM never executes anything. It only picks an action from a fixed catalog. PIPER then validates the choice and runs the command on your own machine through a single audited executor. The LLM is a planner, not a shell. This is the entire product.

Demo

PIPER restarting a broken compose stack — proposes the real mutation, hits the docker permission boundary, asks for sudo with the verbatim command, executes after approval, verifies every service came back

"Restart the stack." PIPER proposes the real docker compose restart, hits the host's permission boundary, asks for sudo showing the exact command that will run elevated, shows the pre-state snapshot, and only executes after approval — then verifies every service came back up. Even the follow-up read-only check gets its own gate: an approval for one command never carries over to a different one. The LLM proposes; the deterministic gate validates; the human stays in the loop.


In one sentence

PIPER drives the tools you already trust (ssh, kubectl, docker, gh, aws, gcloud, journalctl, ...) from a conversational terminal UI — but every command runs locally, picked from a typed action catalog, validated by a path denylist + secret scrubber, and (for anything that mutates) gated behind an explicit human approval. The LLM can hallucinate freely; it cannot reach your infrastructure unless a real human says yes.

›  uptime, memory and disk on staging — tail nginx logs if anything looks off

PIPER planning…  (3 actions chosen from the catalog)
  1. system.uptime
  2. system.memory
  3. system.disk_usage
   ✓ system.uptime         (520ms, ran locally)
   ✓ system.memory         (340ms, ran locally)
   ✓ system.disk_usage     (410ms, ran locally)

▌ Y(◉ ◉)Y
▌ Staging has been up for 14 days with a 0.43 load average [ev-1]. Memory
▌ has plenty of headroom — 12 GB free out of 16 GB total [ev-2] — and the
▌ root volume sits at 38% [ev-3]. Nothing worth flagging on the resource
▌ side, no need to dig into the nginx logs right now.

Every [ev-N] is a link back to the exact command output that produced the claim. PIPER cannot make claims without evidence — the verifier rejects ungrounded synthesis and retries.


Catalog actions, not LLM-generated shell

This is the heart of the product. Read it twice.

What most LLM CLIs do What PIPER does
Who composes the command The LLM writes a shell string (tail -f …, kubectl get …) The LLM picks an action name + typed args from a closed catalog
Who runs it An execution layer that runs whatever the LLM wrote PIPER's local executor runs a fixed command template bound to that action
What if the LLM hallucinates A bogus command might run on your infrastructure The catalog has no entry for a bogus action → the executor refuses
What you can audit Prompts + arbitrary shell history A typed list of actions in source — src/actions/builtin/ — plus the verbatim local exec in audit_log
Where the command runs Sometimes a remote sandbox, sometimes your machine Always your machine. Local subprocess, optionally SSH'ing into an allowlisted host you registered

Concretely: when the LLM wants to check disk space, it does not emit "df -h /". It emits a typed tool call —

{ "name": "system.disk_usage", "args": { "host": "staging", "path": "/" } }

— and PIPER's executor (only src/exec/executor.ts runs anything) translates that into df -h / and spawns a local subprocess. The shell string is built in PIPER's source code, not by the LLM. The args are validated by Zod before the spawn. Secrets are stripped on the way out, before going to the audit log and before going back to the LLM.

The LLM can ask for system.disk_usage. It cannot ask for system.evil_undocumented_thing. That's the safety property.


Why this exists

We built PIPER for two people, both real:

  1. The lone developer with no DevOps support. You shipped an app, you need to keep it running, and there's no one to call when the staging container won't come up at 11pm. Today your fallback is pasting logs into ChatGPT and hoping.
  2. The DevOps engineer doing the same diagnostic dance fifty times a day. Tail the logs on that node. Check why the deploy is stuck. Verify the cron. You don't need a tutor — you need an editor for infrastructure with audit trail and rollback wired in.

Both meet on the same contract: PIPER never silently mutates anything, and you can always see exactly what it is about to do.

Non-goals

  • Not an autonomous agent. PIPER does not act on mutate / destructive actions without approval, and never will.
  • Not a chat product. The TUI is a working surface, not a conversation.
  • Not a Kubernetes admin panel, not a CI replacement, not a monitoring tool. PIPER drives the CLIs you already trust and adds the safety + grounding layer.
  • Not a black box. Every action, prompt, approval rule and audit log entry is readable in source.

Status

Milestone What State
M0 Spike — Bun --compile + Ink + PGlite WASM ✅ shipped
M1 Read-only diagnostics: SSH, logs, health, container/pod status, deterministic gate ✅ shipped
M1.5 RAG/memory layer, 3 embedding backends, sessions + resume, auto-compaction, interactive /model & /memory, HUMAN/YOLO modes, 40+ read actions ✅ shipped
M2 Mutations behind HITL — docker deploy gated by snapshot → dry-run → approve → verify → rollback ✅ shipped
M3a Watch mode/watch compiles plain-English requests into deterministic, read-only checks; anomalies trigger automatic grounded diagnosis and (gated) remediation proposals ✅ shipped
M3b/c Kubernetes gated deploy, repo suggestions
M4 On-prem / regulated — local-model-only path, encrypted audit, runbook ingestion at install

Mutations land in M2 behind the full approval gate; the read-only diagnostic flow and the new watch loop are diagnostic by construction.


Quick start

Option A — pre-built binary (recommended)

curl -fsSL https://antoniociccia.github.io/piper/install | sh

The script is 40 lines you can read first: it detects your OS/arch and downloads the matching binary from the latest release. No sudo, no package manager. Or do it by hand — grab the binary for your platform from the latest release — no Bun, no node_modules, single ~76 MB file.

# macOS (Apple Silicon)
curl -fsSLO https://github.com/antoniociccia/piper/releases/latest/download/piper-darwin-arm64
# Strip Gatekeeper's quarantine xattr (the binary isn't notarised yet).
xattr -d com.apple.quarantine piper-darwin-arm64 2>/dev/null || true
chmod +x piper-darwin-arm64 && mv piper-darwin-arm64 /usr/local/bin/piper
piper

# Linux x64
curl -fsSLO https://github.com/antoniociccia/piper/releases/latest/download/piper-linux-x64
chmod +x piper-linux-x64 && sudo mv piper-linux-x64 /usr/local/bin/piper
piper

A .sha256 is published alongside each binary — verify the download before running.

Note

macOS — PIPER is a terminal CLI, not a Mac app. Run piper from a shell; do not double-click the downloaded binary in Finder. The binary isn't notarised by Apple yet, so double-clicking shows "is damaged and can't be opened". From the terminal it runs normally. Notarisation is tracked on the roadmap.

Option B — from source

Need Bun ≥ 1.3 (one-line install: curl -fsSL https://bun.sh/install | bash).

git clone https://github.com/antoniociccia/piper
cd piper
bun install
bun dev

On first launch PIPER detects that ~/.piper/credentials.json doesn't exist and runs an interactive wizard:

  1. Backend — probes for any local LLM server running (Ollama :11434, LM Studio :1234, llama.cpp :8080, vLLM :8000), or asks for an OpenRouter API key.
  2. Model — pick a tier (Featherweight ~$0.10/M, Economy ~$0.44/M, Balanced ~$3/M, Premium $30+/M) or a local model from the listed catalog.
  3. Embedding backendwasm (default, in-process, offline after first run), http (local OpenAI-compatible endpoint), openrouter (cloud, paid), or none (disable RAG).
  4. Budget — per-session USD cap (default $0.50; hard stop, not a warning).
  5. SSH environment — optionally add a first host PIPER will be able to reach.

The wizard writes ~/.piper/credentials.json with mode 0600. From there:

›  check uptime and disk usage on staging

To resume a previous session at startup:

bun dev -- --resume      # opens a picker over recent sessions

Build a single binary

bun run build      # ./dist/piper, ~76 MB
./dist/piper       # runs without Bun, without node_modules

The binary embeds PostgreSQL WASM (~13 MB) and Yoga layout. The embedding model is not bundled — it lazy-fetches on first RAG use (~120 MB, one time) and caches at ~/.piper/cache/models/.


Run it fully local

PIPER speaks one API — the OpenAI /v1/chat/completions shape — so any local server works: Ollama, LM Studio, llama.cpp, vLLM. Point it at one and no prompt, no log line and no command output ever leaves your machine. There is no cloud component to opt out of.

The question that matters is how small a model can be before the loop stops holding together. So we measured it, rather than guessing.

The test: a Debian host with seven planted incidents — Redis OOM-killed, its worker dead, the app degraded, a backup failing for two days, an expired TLS certificate, a runaway process, and a 62 MB log nothing had rotated. One prompt: "analyze this server and tell me everything that is broken." Same scenario, same flow, four models.

Model Size Grounded citations Incidents found
qwen3.5:9b 6.6 GB 11 4 / 7
qwen3.5:4b 3.4 GB 8 3 / 7
granite4.1:3b 2.1 GB 0 2 / 7
phi4-mini:3.8b 2.5 GB 0 1 / 7

All four called tools correctly. Tool-calling is not the bottleneck at this size — producing evidence-linked output is. Granite and Phi wrote fluent prose citing nothing, and PIPER's verifier refused every report they produced, which is the system working as designed: an ungrounded report is not shown at all.

So the floor is a 4B model on an ordinary laptop, and qwen3.5:4b is the default when PIPER detects Ollama. Nothing here needs a GPU rental or a workstation.

ollama pull qwen3.5:4b
PIPER_PROVIDER=ollama piper

Two things worth saying plainly, because a benchmark table invites over-reading:

  • These are single runs, not averaged. Treat the ordering as indicative.
  • A 4B model handles a focused question well ("uptime, memory and disk — anything unhealthy?"). Open-ended triage across a genuinely messy host is where the bigger models pull ahead. Both are honest uses; they are not the same difficulty.

The deterministic gate

You cannot get "no hallucination" from an LLM. Don't try. Instead, make being wrong safe. PIPER's LLM lives inside a deterministic cage:

       ┌────────────┐  proposes actions      ┌────────────────┐
       │    LLM     │ ─────tool_calls──────► │  Action catalog│
       │ (any model)│                        │  (read|mutate| │
       │            │                        │   destructive) │
       └────────────┘                        └───────┬────────┘
              ▲                                      │ validate
              │ scrubbed                             │ args (Zod)
              │ messages                             ▼
              │                              ┌────────────────┐
              │                              │   Executor     │
              │                              │   (the ONLY    │
              │                              │   side-effect  │
              │                              │   surface)     │
              │                              └───────┬────────┘
              │                                      │
              │           scrub stdout/stderr        │ spawns kubectl /
              └──────────────────────────────────────┤ docker / ssh /
                                                     │ nc / gh / ...
                                                     ▼
                                           ┌──────────────────┐
                                           │ PGlite + pgvector│
                                           │ audit_log,       │
                                           │ evidence,        │
                                           │ knowledge        │
                                           └──────────────────┘

Three permission tiers with no overrides:

Tier Examples Approval
read uptime, docker.ps, kubectl get None. Executes directly. Safe by definition.
mutate (M2) docker deploy, env update Per-env approval prompt; remembered
destructive (M2) delete, drop, prune, force-push Fresh prompt every time. Never remembered. Ever.

Five overlapping defenses, applied at every layer:

  • Architectural — SSH keys never leave the OS ssh binary; API keys never enter messages[].content. Single-module discipline + CI rule.
  • Path denylist~/.ssh/id_*, ~/.aws/credentials, ~/.kube/config, ~/.gnupg/, ~/.docker/config.json, ~/.netrc, ~/.piper/, .env*. Non-disablable. User config can extend the list, never weaken it.
  • Two-pass scrubbing — write-time (every Executor output → audit log) and pre-LLM (every message body → HTTP call). Defense in depth.
  • Args refuse — if the LLM tries to embed a recognisable secret (AKIA…, sk-or-…, JWTs, PEM blocks, Bearer …) in an action's args, the Executor refuses the action — it does not redact. Redaction would mutate semantics.
  • Provider-level privacy — OpenRouter requests set body.provider.data_collection = 'deny'. Local mode routes inference through Ollama / llama.cpp / LM Studio / vLLM — network egress for inference is zero.

Sudo elevation — PIPER can propose running a command with sudo for commands that need root (either because the planner knows in advance, or reactively after a permission-denied failure). Every elevation — read-tier included — is shown to you and requires explicit approval. The path denylist still blocks sensitive files (~/.ssh/id_rsa, ~/.aws/credentials, etc.) under sudo, before the approval prompt is shown. If the host requires a sudo password, you type it on your own terminal via an interactive ssh -tt session; PIPER's buffers, audit log, and model messages never see it.

Full design rationale in docs/architecture.md and docs/decisions/ADR-001-deterministic-gate.md.


Action catalog

40+ read-tier actions across the major DevOps surfaces. Every action is a typed object registered in src/actions/builtin/, validated by Zod, executed only through src/exec/executor.ts. Free-form shell from the LLM is not representable in the type system.

Click to expand the full catalog
Category Action What it does
System system.uptime uptime (load average + time up)
system.os_info uname -a + /etc/os-release
system.memory free -h
system.disk_usage df -h [path?]
system.process_list ps -eo pid,user,pcpu,pmem,args -ww
system.list_dir ls -la <path> (deny-list enforced)
system.file_stat stat <path>
system.cpu_info lscpu / /proc/cpuinfo
system.dmesg Kernel ring buffer tail
system.package_list Installed packages (dpkg -l / rpm -qa)
system.cron_list User + system crontabs
system.systemctl_list systemctl list-units --type=service
system.iptables_list iptables -L -n -v
Network network.connections ss -tunap
network.port_check nc -zv (open / refused / timeout / closed)
network.ping ping -c N -W T
network.dns_lookup dig / host lookup
ssh.connect Probe SSH reachability against an allowlisted host
Logs logs.tail tail -n N <path> with optional grep
Services service.status systemctl status <unit>
service.journal journalctl -u <unit> -n N
Docker docker.ps Container list (JSON)
docker.logs Container log tail
docker.inspect Container inspect (summarised)
docker.compose_ps docker compose ps for a project
Kubernetes kubernetes.get kubectl get <kind> (pods, deploys, services…)
kubernetes.logs kubectl logs <pod> (with -c, --previous, tail-N)
kubernetes.describe kubectl describe <kind>/<name>
kubernetes.top_pod kubectl top pod
kubernetes.events kubectl get events --sort-by=.lastTimestamp
kubernetes.context_current kubectl config current-context
Git git.status git status --porcelain=v1
git.log git log -n N --oneline --decorate
GitHub github.pr_list gh pr list
github.pr_view gh pr view <number>
github.run_list gh run list (Actions)
github.run_view gh run view <id> (logs, conclusion)
github.issue_list gh issue list
AWS aws.s3_ls aws s3 ls
aws.ec2_describe aws ec2 describe-instances
aws.cloudwatch_tail aws logs tail (CloudWatch)
aws.rds_describe aws rds describe-db-instances
GCP gcp.compute_list gcloud compute instances list
gcp.logging_read gcloud logging read
Azure azure.vm_list az vm list
Database postgres.pg_isready pg_isready against host:port
Memory memory.search In-process semantic search over the local knowledge base

Knowledge base / RAG

PIPER ships a memory.search action. It is not a shell action — it's in-process semantic search over a local PGlite + pgvector store of:

  • runbook — markdown under docs/runbooks/
  • adr — architecture decision records under docs/decisions/
  • session-summary — produced by /session-report
  • solved-case — distilled incident notes (annex format, opt-in)
  • note — free-form knowledge you add yourself

The planner is instructed to call memory.search first when the user's prompt looks like a known incident pattern, a deploy procedure, or references a host that has prior session notes. The agent stays grounded in your runbooks instead of the model's training data.

Three embedding backends

Backend Model Dim Cost Notes
wasm (default) Xenova/multilingual-e5-small 384 free In-process via @huggingface/transformers. 94 languages. ~120 MB downloaded once, then fully offline. Cached at ~/.piper/cache/models/.
http OpenAI-compatible local endpoint varies free Ollama (nomic-embed-text, 768-dim), LM Studio, llama.cpp, vLLM.
openrouter Cloud paid embedding model varies paid Only offered if an API key is configured.
none Disables RAG. memory.search returns empty.

The schema auto-recreates if the dimension mismatches — switching e.g. from Ollama 768-dim to WASM 384-dim drops the old vectors and rebuilds from source. Zero manual migration.


Watch mode

/watch compiles a plain-English request into deterministic, read-only checks, runs them on a cadence with zero LLM cost per tick, and — only when a check actually fails — fires an automatic grounded diagnosis and a gated remediation proposal. The LLM is never in the monitoring hot loop.

/watch docker-basics                  # start a bundled stock plan
/watch keep an eye on staging         # compile a custom plan from plain English
piper check docker-basics staging     # one-shot mode for cron/CI (exit 0/1/2/3)

A watch plan is a skill-like markdown file (~/.piper/watches/*.md): YAML frontmatter listing the checks plus a prose runbook the LLM reads only on anomaly. Each check pairs a read-tier catalog action with an expectation from a closed DSL of seven kinds (exit_zero, all_running, max_percent, min_count, regex_match, regex_absent, json_path_eq), evaluated in-process. Plans that name a mutate/destructive action, or whose args don't satisfy the action's schema, are rejected at load.

When a check fails twice in a row (debounce), PIPER fires once and stays quiet for 15 minutes (cooldown). On fire it notifies you (TUI bell, desktop notification via the audited notify.desktop action, and an optional https-only webhook with a metadata-only, scrubbed payload), then runs a budget-guarded diagnosis through the normal agent runner. Any fix it proposes goes through the M2 approval gate — the watch loop never mutates anything on its own.

Three stock plans ship in source: docker-basics, k8s-basics, disk-and-memory. piper check <plan> [env] runs every check once and exits with a meaningful code (0 all-passed, 1 expectation failed, 2 check error, 3 plan not found) — drop it into cron or CI. Full design rationale in docs/decisions/ADR-003-watch-mode.md.


TUI modes & commands

Toggle modes with Shift+Tab:

  • HUMAN (default) — PIPER asks for approval per planned step. Verbatim command is shown before any run.
  • YOLO — read-tier actions execute without per-step approval. mutate and destructive actions still always ask, every time, by design.
Slash commands
/model               interactive model picker (Local / OpenRouter tabs, paging, filter)
/watch [plan|text]   list watch plans, start one, or compile a new plan from plain English
/memory              knowledge-base viewer (Overview + Sources, delete with d)
/mem, /rag           aliases for /memory
/resume              pick a recent session and reload its history into scrollback
/env add <name> <user@host[:port]> [--key <path>] [--desc "..."] [--tag a,b]
/env list
/env remove <name>
/session-report      summarise the current session into the knowledge base
/debug               toggle verbose agent events (costs, synth status, RAG hits, LLM trace)
/help                show context-sensitive help
/save [file.md]      export the last report to a file
/quit                exit PIPER (Ctrl+C also works)
Keyboard
Keys Effect
Enter Send
Shift+Enter New line (multi-line input)
Shift+Tab Toggle HUMAN ↔ YOLO
Ctrl+O Collapse reasoning — hide agent-event lines from future turns
? Context-sensitive help
Esc Clear current input
Ctrl+C Quit

Status bar

The bottom strip of the TUI shows everything at a glance:

Y(◉ ◉)Y  diagnosing staging      $0.0123 | google/gemini-pro-1.5 | OR $4.32 left | 12.4k/128k (10%) ███▒▒▒▒▒▒▒  HUMAN
  • Alien mascot — color-cycles while PIPER thinks; idle when waiting on you.
  • Session title — auto-generated from the first user prompt by a tiny LLM call.
  • Cost — running session cost in USD, real provider pricing.
  • Model id — the model currently driving the planner (/model to switch).
  • OpenRouter remaining credit — live-fetched every 60s on paid backends.
  • Token meterN/limit (%) against the model's maxContextTokens (minus 4k reserved for output), measured with real gpt-tokenizer cl100k_base.
  • Mode badge — HUMAN (green) or YOLO (red).

Sessions, lifecycle, output

  • Persistent by default. PGlite stores sessions at ~/.piper/data/pglite/. Override with PIPER_DATA_DIR=/path. Force in-memory (ephemeral) with PIPER_EPHEMERAL=1.
  • Auto-titled. Small LLM call on the first user prompt names the session.
  • Auto-saved reports. Every done writes the final answer to ~/.piper/data/reports/{sessionId}/run-{ts}.md.
  • Resume. bun dev -- --resume at startup, or /resume mid-session.
  • Auto-compaction. When the planner's context exceeds 70% of the model's maxContextTokens, older turns are rolled into a single summary message.
  • Grounded synthesis. Every claim cites [ev-N]. A run passes the verifier if ≥75% of substantive lines are cited; ungrounded answers retry.
  • <Static> scrollback persistence. History stays in the terminal's native scrollback — append-only, no redraw, no flicker, no loss when you scroll up.

Stack

Concern Choice
Runtime Bun ≥ 1.3 (single-binary via bun build --compile; Bun.YAML for watch plans)
Language TypeScript strict (noUncheckedIndexedAccess, exactOptionalPropertyTypes, no any)
Terminal UI Ink (React for the terminal)
Persistence PGlite (PostgreSQL in WASM — single embedded DB)
Vectors pgvector inside the same PGlite DB (HNSW index)
Embeddings (default) @huggingface/transformers + Xenova/multilingual-e5-small (WASM)
Tokenizer gpt-tokenizer (cl100k_base)
Schema validation Zod
Model API OpenAI-compatible /v1/chat/completions

Why these choices: see docs/decisions/.


Configuration

~/.piper/credentials.json (created by the wizard, mode 0600)
{
  "openrouter_api_key": "sk-or-v1-...",
  "default_provider": "openrouter",
  "default_model": "deepseek/deepseek-v4-pro",
  "embedding_backend": "wasm",
  "max_session_cost_usd": 0.50,
  "max_followup_iterations": 1,
  "compaction_keep_recent": 6,
  "compaction_trigger_pct": 0.70,
  "environments": {
    "prod-web": {
      "host": "192.0.2.10",
      "ssh_user": "deploy",
      "port": 22,
      "identity_file": "/Users/me/.ssh/id_ed25519",
      "description": "production web tier",
      "tags": ["prod", "web"]
    }
  }
}
Environment variables (override the file — useful in CI)
Variable Purpose
PIPER_PROVIDER openrouter | ollama | lmstudio | llamacpp | vllm | custom
PIPER_BASE_URL Endpoint override
PIPER_API_KEY / OPENROUTER_API_KEY API key
PIPER_MODEL Model id
PIPER_EMBEDDING_BACKEND wasm | http | openrouter | none
PIPER_MAX_SESSION_COST_USD Hard budget cap
PIPER_DATA_DIR Persistent storage (default: ~/.piper/data/pglite/)
PIPER_EPHEMERAL Set to 1 for in-memory storage (loses sessions at exit)

If an env var doesn't look like a valid API key (e.g. a leftover test value), PIPER ignores it with a warning and falls back to the file.


Tests

bun test                       # 386 unit + gate tests (no Docker, no network)
bun run e2e                    # Docker sshd fixture, E2E tests, teardown
bun run typecheck              # tsc --noEmit, strict

Coverage focuses on the security-critical layers: catalog gate, path denylist, secret scrubber, audit log persistence, verifier, embedding-dim migration.

CI runs license-checker and rejects any GPL transitive dependency.


Security

PIPER is built around a deterministic safety gate. Vulnerability disclosure process: see SECURITY.md. Coordinated disclosure, 90-day default. Particular care for:

  • Prompt-injection that smuggles a command into the gate
  • Any code path that runs shell outside the Executor
  • Any code path that logs or sends unredacted secrets
  • Any code path that lets a remembered rule auto-approve a destructive action
  • Any code path that bypasses the SSH host allowlist

The full architecture + threat model is at docs/architecture.md.


License & contributing

Apache-2.0. See LICENSE and NOTICE — the NOTICE file discloses the Apache-2.0 transitive deps and the LGPL transitive disclosure for @img/sharp-libvips (pulled in by the embedding pipeline).

Contributions welcome. See CONTRIBUTING.md for the flow. Two-eyes rule on anything touching src/exec/, src/security/, or src/actions/ — the maintainer reviews these personally.


Built with the conviction that making being wrong safe beats trying to make the LLM never wrong.

About

Pipeline Intelligence, Provisioning, Error-handling & Releases — terminal-first LLM-driven DevOps copilot

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages