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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project are documented in this file.

## Unreleased

### Added (operator console)

- `console.py` — an operator console replacing the API dashboard at `/`. The old page was a form per endpoint: useful for poking the API, useless for the job phase 1 actually requires, which is looking at what the engine decided and reviewing every `hard_deny` by hand before enforcement is switched on. The new page shows a live decision feed with per-state filtering and a distribution bar, the phase-readiness criteria with their verdict, and a hard-deny review list. Server-rendered, no build step, no external assets, so it ships in the same container as the engine and the audit trail stays behind one network boundary instead of two.
- `GET /v1/decisions?limit=&state=` and `GET /v1/rollout/report?phase=` — read-only views over the audit trail backing the console. Gated behind `AETHERYA_CONSOLE_API_KEY` when that is set; open otherwise, like the rest of the decision profile. **This view exposes every recorded action, so the port must not be public** — stated in the page footer and in the route comment.
- Removed the previous `_dashboard_html` (~11KB of now-unreachable HTML) rather than leaving it to rot alongside its replacement.

### Added (phase-1 sidecar)

- `enforcement.py` + `enforcement.phase` in policy — the rollout posture is now configuration. The engine always computes a full decision; the phase decides how much is enforced, so advancing is a config change and never a code change, and all three phases exist from day one. `apply_enforcement` never mutates the decision: the audit records what the engine ruled, not what a partially-enforcing deployment did about it, and conflating those would make phase-1 data worthless.
Expand Down
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: fmt lint type test cov check security_baseline chaos_benchmark pipeline_benchmark pipeline_memory_soak property_tests audit_fuzz openai_shadow_smoke pre_api_gate api_serve api_decision_serve api_approvals_serve
.PHONY: fmt lint type test cov check security_baseline chaos_benchmark pipeline_benchmark pipeline_memory_soak property_tests audit_fuzz openai_shadow_smoke pre_api_gate api_serve api_decision_serve api_approvals_serve gateway_serve gateway_smoke

fmt:
ruff format src tests
Expand Down Expand Up @@ -52,6 +52,12 @@ api_decision_serve:
api_approvals_serve:
python -m aetherya.api_server --service-mode approvals --host 127.0.0.1 --port 8081

gateway_serve:
python -m aetherya.gateway_server --host 127.0.0.1 --port 8090

gateway_smoke:
python scripts/gateway_smoke.py --provider anthropic --phase 2

api_stop:
@echo "Liberando puerto 8080..."
-fuser -k 8080/tcp || true
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ python -m aetherya.audit_verify --audit-path "$AUDIT" --require-chain --json \
| HTTP API | [docs/api.md](docs/api.md) |
| Integrations (Python, CLI, agents) | [docs/integrations.md](docs/integrations.md) |
| Production rollout phases | [docs/rollout-phases.md](docs/rollout-phases.md) |
| Gateway for OpenClaw & other agents | [docs/gateway-openclaw.md](docs/gateway-openclaw.md) |
| Testing & benchmarks | [docs/testing-and-benchmarks.md](docs/testing-and-benchmarks.md) |
| Release & verification | [docs/release-and-verification.md](docs/release-and-verification.md) |

Expand Down
169 changes: 169 additions & 0 deletions docs/gateway-openclaw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Putting ÆTHERYA in front of an agent you did not write

The sidecar in [rollout-phases.md](rollout-phases.md) requires editing the agent
loop. Most agent runtimes — OpenClaw, LiteLLM, LangChain, anything that accepts
a custom OpenAI-compatible provider — cannot be edited that way, or you do not
want to fork them.

The gateway solves it by changing what the agent talks to instead of what it
does. It speaks the OpenAI chat-completions protocol, so the runtime treats it
as just another provider:

```
OpenClaw ──► ÆTHERYA gateway ──► OpenAI | Anthropic
│
every tool call the model proposes is ruled on
before the agent ever sees it
```

The agent never learns there is a boundary. A tool call the policy refuses is
removed from the response and replaced with a line of text explaining why, so
the model can replan — and the audit trail records the decision either way.

---

## Run it

```bash
export ANTHROPIC_API_KEY=sk-ant-...
aetherya-gateway --provider anthropic --model claude-opus-5 --actor openclaw
```

or

```bash
export OPENAI_API_KEY=sk-...
aetherya-gateway --provider openai --model gpt-4o-mini --actor openclaw
```

It binds `127.0.0.1:8090` and prints its posture:

```
[gateway] anthropic/claude-opus-5 · phase 1 (shadow) · http://127.0.0.1:8090/v1
```

| Flag | Default | Notes |
|---|---|---|
| `--provider` | `anthropic` | `anthropic` or `openai` |
| `--model` | `claude-opus-5` | Passed upstream; a request may override it |
| `--actor` | `openclaw` | **Must exist in `capability_matrix.actors`** — see below |
| `--phase` | from `policy.yaml` | `1` shadow · `2` hard-deny · `3` full |
| `--audit-path` | `audit/decisions.jsonl` | Feeds the operator console |
| `--effort` | `high` | Anthropic only |

**The gateway holds your upstream API key and authenticates nobody.** Keep it on
loopback. Binding it elsewhere prints a warning; it is not a suggestion.

---

## Point OpenClaw at it

In `openclaw.json`:

```json5
{
agents: {
defaults: {
model: { primary: "aetherya/claude-opus-5" }
}
},
models: {
providers: {
aetherya: {
baseUrl: "http://localhost:8090/v1",
apiKey: "not-used",
api: "openai-completions",
timeoutSeconds: 300,
models: [
{
id: "claude-opus-5",
name: "Claude Opus 5 via ÆTHERYA",
contextWindow: 200000,
maxTokens: 8192
}
]
}
}
}
}
```

`apiKey` is deliberately junk: OpenClaw talks to the gateway, and the gateway
holds the real credential. That is a property worth keeping — the agent process
never sees your provider key.

Then `openclaw configure` (existing install) or `openclaw onboard` (fresh).

---

## Add your actor to the capability matrix

This is the one step that is easy to skip and expensive to skip:

```yaml
# config/policy.yaml
capability_matrix:
actors:
openclaw:
roles: [operator]
tools: []
operations: []
```

An actor the matrix has never heard of is denied on **every** call. In phases 1
and 2 that is invisible, because `deny` is not enforced — so the
misconfiguration sits there silently until phase 3 turns your agent into a
brick. The gateway therefore refuses to start at phase 3 with an unknown actor,
and warns at phases 1 and 2.

Note that the tools your agent uses must also be in `execution_gate.allowed_tools`.
A tool that is not on the allowlist is a `hard_deny` — the tier phase 2 enforces.
Run in phase 1 first and read the audit trail; that is exactly what it is for.

---

## Check it before trusting it

```bash
export ANTHROPIC_API_KEY=sk-ant-...
python scripts/gateway_smoke.py --provider anthropic --phase 2
```

This starts the gateway, points a real OpenAI SDK client at it, and asks a real
model to run `rm -rf /` with a `shell` tool available. It reports what the model
proposed, what the policy ruled, and what survived to the client. Exits non-zero
if a destructive call got through. Costs a few cents.

---

## What this costs you

**Streaming latency.** A verdict needs the whole tool call — you cannot rule on
half of one. So the upstream response is completed before anything reaches the
client. `stream: true` still returns SSE frames and clients keep working, but
the tokens arrive in a burst at the end rather than as they are generated. For a
tool-using agent this is usually invisible; for a chat UI it is not.

**A hop.** One extra process between the agent and the provider, holding your
key. Gate the port accordingly.

**Tool vocabulary mapping.** The gateway does not know your agent's tools, so it
maps them generically: tool name → `tool`, arguments → `parameters`, and a
readable rendering of the arguments → `raw_input` (which is what ProceduralGuard
and JailbreakGuard read, so a destructive command hidden inside an argument is
still seen). If your tools carry structure the policy should reason about
specifically, encode it in `execution_gate.allowed_parameters` rather than
relying on the generic mapping.

---

## What it does not do

- **It does not sandbox.** ÆTHERYA decides; execution isolation is a separate
concern and a complementary one.
- **It does not gate model output text**, only proposed tool calls. Use
`output_gate` for response content.
- **It does not handle phase-3 confirmations.** An `escalate` at phase 3 is
refused like a `deny`, because there is no approval round trip in a single
chat-completions call. Agents that need held-for-approval semantics want the
sidecar in [rollout-phases.md](rollout-phases.md#phase-3--full-enforcement).
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ See a runnable simulation: [`examples/agent_integration.py`](../examples/agent_i
|------|----------------|
| [integrations.md](./integrations.md) | How to integrate — Python, CLI, agent loop, wrap-a-tool pattern |
| [rollout-phases.md](./rollout-phases.md) | Shadow → hard-deny → full enforcement, with exit criteria per phase |
| [gateway-openclaw.md](./gateway-openclaw.md) | OpenAI-compatible gateway: gate an agent you cannot modify (OpenClaw, LiteLLM, …) |
| [architecture.md](./architecture.md) | Full pipeline, component roles, fail-closed guarantee |
| [security-model.md](./security-model.md) | Threat model, JailbreakGuard, confirmation + replay, audit chain |
| [policy-model.md](./policy-model.md) | Modes, Constitution, semantic thresholds, policy.yaml structure |
Expand Down
26 changes: 26 additions & 0 deletions docs/rollout-phases.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,32 @@ the source for the rest.
3. Zero `hard_deny` on a code path a human agrees should have been allowed.
4. The audit chain verifies clean (`--require-chain`) over the whole window.

**Recording the review.** Criterion 2 is a human judgement, so the tool cannot
make it — but it can check that someone did. Open the operator console, work
through the hard-deny queue, and mark each event:

```bash
export AETHERYA_CONSOLE_API_KEY=... # required: the verdict is attributed
make api_serve # console at http://127.0.0.1:8080/
```

Each verdict is appended to `audit/reviews.jsonl` against the audit `event_id`,
with the reviewer's name and timestamp. The store is append-only: re-reviewing
an event adds a new verdict and the earlier one stays on disk, because a review
is what unblocks enforcement and who said what has to survive.

`hard_deny_reviewed` then passes only when every sampled event carries a
`true_positive`. Two things deliberately do *not* pass it:

- **Any `false_positive`.** Reviewing everything is not the same as approving
it — enforcing a rule a human already called wrong is the exact outcome this
phase exists to prevent. Fix the rule, then restart the window.
- **Events beyond `max_hard_deny_samples`.** The report samples; reviewing the
sampled ones cannot clear a gate covering events nobody was shown.

A window with no `hard_deny` at all passes: nothing to judge is not the same as
a judgement withheld.

**If criterion 2 fails** — you found a false positive, which is exactly what
this phase is for. In order of preference:

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ aetherya = "aetherya.cli:main"
aetherya-api = "aetherya.api_server:main"
aetherya-decision-server = "aetherya.decision_server:main"
aetherya-approvals-server = "aetherya.approvals_server:main"
aetherya-gateway = "aetherya.gateway_server:main"

[[tool.mypy.overrides]]
module = ["numpy", "numpy.*", "sentence_transformers", "sentence_transformers.*"]
Expand Down
Loading
Loading