268 lines
22 KiB
Markdown
268 lines
22 KiB
Markdown
---
|
|||
|
|
phase: 13-aiui-functional-conversational-node-control-and-content-surf
|
||
|
|
plan: 02
|
||
|
|
type: execute
|
||
|
|
wave: 1
|
||
|
|
depends_on: []
|
||
|
|
files_modified:
|
||
|
|
- core/archipelago/src/api/handler/model_proxy.rs
|
||
|
|
- core/archipelago/src/api/handler/mod.rs
|
||
|
|
- core/archipelago/src/api/rpc/system/handlers.rs
|
||
|
|
- image-recipe/configs/nginx-archipelago.conf
|
||
|
|
- scripts/deploy-to-target.sh
|
||
|
|
- scripts/setup-aiui-server.sh
|
||
|
|
- tests/production-quality/aiui-proxy-closed.sh
|
||
|
|
autonomous: false
|
||
|
|
requirements: [AIUI-04]
|
||
|
|
|
||
|
|
must_haves:
|
||
|
|
truths:
|
||
|
|
- "Nobody without an authenticated session can reach the node's model backends or spend the owner's API budget (AI-SPEC failure mode 4)"
|
||
|
|
- "There is exactly one Claude credential ledger on the node — `data_dir/secrets/claude-api-key`. The second one (`secrets/claude-api-proxy.env` + the systemd unit's `ANTHROPIC_API_KEY`) is gone (D-01)"
|
||
|
|
- "A logged-in operator's currently-deployed AIUI build keeps working through the migration window — the URL path is unchanged, only its authentication and its upstream change (D-17)"
|
||
|
|
- "`/aiui/api/openrouter/` no longer exists on a node: it held no node key, is not in D-04's backend chain, and was a plain open relay to a paid third-party API"
|
||
|
|
artifacts:
|
||
|
|
- path: "core/archipelago/src/api/handler/model_proxy.rs"
|
||
|
|
provides: "Session-gated forwarder for /aiui/api/claude/* and /aiui/api/ollama/*, replacing claude-api-proxy.py"
|
||
|
|
contains: "is_authenticated"
|
||
|
|
- path: "tests/production-quality/aiui-proxy-closed.sh"
|
||
|
|
provides: "S-15 deployed-surface check — the one check a green cargo test cannot make"
|
||
|
|
contains: "aiui/api/claude"
|
||
|
|
key_links:
|
||
|
|
- from: "image-recipe/configs/nginx-archipelago.conf"
|
||
|
|
to: "core/archipelago/src/api/handler/model_proxy.rs"
|
||
|
|
via: "/aiui/api/claude/ proxy_pass re-pointed from 127.0.0.1:3142 to the Rust daemon on 127.0.0.1:5678"
|
||
|
|
pattern: "proxy_pass http://127\\.0\\.0\\.1:5678"
|
||
|
|
- from: "core/archipelago/src/api/handler/model_proxy.rs"
|
||
|
|
to: "core/archipelago/src/api/rpc/mesh/assistant.rs"
|
||
|
|
via: "reads the same data_dir/secrets/claude-api-key — one ledger, not two"
|
||
|
|
pattern: "secrets/claude-api-key"
|
||
|
|
---
|
||
|
|
|
||
|
|
<objective>
|
||
|
|
Close a live production exposure. Verified in `image-recipe/configs/nginx-archipelago.conf`
|
||
|
|
(two server blocks, lines ~49-88 and ~961-993): `/aiui/api/claude/` proxies to a standalone
|
||
|
|
Python server on port 3142 holding its **own** `ANTHROPIC_API_KEY`, and `/aiui/api/openrouter/`
|
||
|
|
proxies straight to openrouter.ai — both with **no session gate**. The config comment says
|
||
|
|
"API key managed by proxy, no session gate needed", which confuses key *secrecy* with spend
|
||
|
|
*authorization*. Anyone who can reach the node's web port can bill the owner.
|
||
|
|
|
||
|
|
This is RESEARCH Open Question 1, answered: **delete-and-replace, not gate-then-deprecate.**
|
||
|
|
The replacement is a session-gated forwarder inside the Rust daemon that reads the node's
|
||
|
|
single existing key ledger. Because the URL path does not change, every currently-deployed
|
||
|
|
AIUI build keeps working for a logged-in operator — but stops working for an anonymous caller.
|
||
|
|
The nginx location blocks themselves are retired in 13-09, once 13-01's `assistant.chat` path
|
||
|
|
is the one AIUI actually uses.
|
||
|
|
|
||
|
|
Purpose: this exposure is more severe than "chat can't act on the node" and is not mentioned
|
||
|
|
in CONTEXT.md. It is fixed first, in wave 1, independently of the assistant work.
|
||
|
|
|
||
|
|
Output: `api/handler/model_proxy.rs`, a rewritten nginx AIUI-API section, a deploy path with no
|
||
|
|
Python sidecar, and `tests/production-quality/aiui-proxy-closed.sh`.
|
||
|
|
</objective>
|
||
|
|
|
||
|
|
<flagged_assumptions>
|
||
|
|
None in this plan.
|
||
|
|
</flagged_assumptions>
|
||
|
|
|
||
|
|
<artifacts_this_phase_produces>
|
||
|
|
Symbols created by **this plan**:
|
||
|
|
|
||
|
|
- `core/archipelago/src/api/handler/model_proxy.rs`: `handle_model_proxy`, `forward_claude`,
|
||
|
|
`forward_ollama`, `const CLAUDE_UPSTREAM`, `const OLLAMA_UPSTREAM`
|
||
|
|
- `core/archipelago/src/api/handler/mod.rs`: `mod model_proxy;` plus two new path arms
|
||
|
|
- New file `tests/production-quality/aiui-proxy-closed.sh` (shell, follows the existing
|
||
|
|
`tests/production-quality/lnd-cors-test.sh` precedent)
|
||
|
|
|
||
|
|
Symbols **deleted** by this plan (so a later drift scan does not flag their absence):
|
||
|
|
- the embedded `claude-api-proxy.py` heredoc in `scripts/deploy-to-target.sh` (~lines 879-955)
|
||
|
|
- the `claude-api-proxy` systemd unit and its `ANTHROPIC_API_KEY` environment line
|
||
|
|
- the `secrets/claude-api-proxy.env` write and the `systemctl restart claude-api-proxy` call in
|
||
|
|
`core/archipelago/src/api/rpc/system/handlers.rs` (~lines 1052-1067)
|
||
|
|
- the `3141` → `3142` `proxy_pass` sed fixups in `scripts/deploy-to-target.sh` (~lines 399, 779)
|
||
|
|
- the `location /aiui/api/openrouter/` blocks in both nginx server blocks
|
||
|
|
</artifacts_this_phase_produces>
|
||
|
|
|
||
|
|
<execution_context>
|
||
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
||
|
|
</execution_context>
|
||
|
|
|
||
|
|
<context>
|
||
|
|
@.planning/PROJECT.md
|
||
|
|
@.planning/STATE.md
|
||
|
|
@CLAUDE.md
|
||
|
|
@.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-RESEARCH.md
|
||
|
|
@.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-CONTEXT.md
|
||
|
|
</context>
|
||
|
|
|
||
|
|
<tasks>
|
||
|
|
|
||
|
|
<task type="auto" tdd="true">
|
||
|
|
<name>Task 1: Session-gated model forwarder in the Rust daemon</name>
|
||
|
|
<files>core/archipelago/src/api/handler/model_proxy.rs, core/archipelago/src/api/handler/mod.rs</files>
|
||
|
|
<behavior>
|
||
|
|
- A POST to `/aiui/api/claude/v1/messages` with **no** session cookie returns 401 and makes no upstream request.
|
||
|
|
- A POST with an invalid/expired session cookie returns 401.
|
||
|
|
- A POST with a valid session cookie forwards to `https://api.anthropic.com/v1/messages` with `x-api-key` read from `data_dir/secrets/claude-api-key`.
|
||
|
|
- When `data_dir/secrets/claude-api-key` is absent, an authenticated caller gets 503 with a plain-language body naming the missing key — never a 500 and never the key path itself echoed as a filesystem hint.
|
||
|
|
- A GET/POST to `/aiui/api/ollama/*` with no session returns 401; with a session it forwards to `http://127.0.0.1:11434/*`.
|
||
|
|
- The API key never appears in any response body, response header, or log line at any level.
|
||
|
|
</behavior>
|
||
|
|
<read_first>
|
||
|
|
- `core/archipelago/src/api/handler/mod.rs` — the WebSocket arms at lines 380-418 for the exact `if !self.is_authenticated(req.headers()).await { return Ok(Self::unauthorized()); }` idiom, the `match (method, path.as_str())` table starting ~line 435, and `use crate::session::{self, SessionStore}` at line 15.
|
||
|
|
- `core/archipelago/src/api/handler/proxy.rs` lines 188-265 — the existing peer Range-streaming proxy; the in-repo pattern for building an upstream `reqwest` request and streaming its response back through hyper. Its docstring explains why base64 blobs broke seeking; reuse the streaming shape, not a buffered one.
|
||
|
|
- `core/archipelago/src/api/rpc/mesh/assistant.rs` lines 27-30 — the `data_dir/secrets/claude-api-key` probe. Use this exact path.
|
||
|
|
- `image-recipe/configs/nginx-archipelago.conf` lines 49-88 — what is being replaced.
|
||
|
|
</read_first>
|
||
|
|
<action>
|
||
|
|
Create `core/archipelago/src/api/handler/model_proxy.rs` with `pub(super) async fn handle_model_proxy(&self, req, path) -> Result<Response<Body>>` plus `forward_claude` and `forward_ollama`, and declare `mod model_proxy;` in `api/handler/mod.rs`.
|
||
|
|
|
||
|
|
Add two arms to the existing path dispatch in `api/handler/mod.rs`, placed alongside the WebSocket arms so the auth check is impossible to miss: a prefix match on `/aiui/api/claude/` and one on `/aiui/api/ollama/`. Each arm calls `self.is_authenticated(req.headers()).await` FIRST and returns `Self::unauthorized()` on failure — the same primitive `/ws/db` already uses. Do not add these paths to any allowlist and do not touch `UNAUTHENTICATED_METHODS` (that is the RPC surface; this is the HTTP surface, and the Phase-10 boundary applies to both).
|
||
|
|
|
||
|
|
`forward_claude` strips the `/aiui/api/claude/` prefix, appends the remainder to `https://api.anthropic.com/`, and forwards the method, body and the `content-type`/`accept` request headers only. It sets `x-api-key` from `tokio::fs::read_to_string(self.config.data_dir.join("secrets/claude-api-key"))` (trimmed) and `anthropic-version: 2023-06-01`. It must NOT forward an inbound `x-api-key`, `authorization`, or `cookie` header upstream — a caller must not be able to bill a different account or leak the node's session to Anthropic. Use a `reqwest::Client` built with `ASSISTANT_HTTP_TIMEOUT`-equivalent generosity (180s) and `stream` so token-by-token responses still stream.
|
||
|
|
|
||
|
|
`forward_ollama` does the same shape against `http://127.0.0.1:11434/`, with no key.
|
||
|
|
|
||
|
|
Logging: emit `tracing::warn!` on a 401 naming the path but not the headers, and `tracing::info!` on a successful forward naming only the upstream host and the status code. Never log the key, the request body, or the response body — this handler carries user chat text by definition, and AI-SPEC §7b's field policy is a security control, not a style preference.
|
||
|
|
|
||
|
|
Write the `#[cfg(test)] mod tests` FIRST, covering every bullet in `<behavior>` above, using the `SessionStore::new_for_tests` constructor and `tempfile` (both already in-tree) for the data_dir. Name them `model_proxy::tests::claude_without_session_is_401`, `..::ollama_without_session_is_401`, `..::missing_key_is_503_not_500`, `..::inbound_authorization_header_is_not_forwarded`.
|
||
|
|
</action>
|
||
|
|
<verify>
|
||
|
|
<automated>cd core && CARGO_INCREMENTAL=0 cargo test --package archipelago model_proxy:: 2>&1 | tail -20</automated>
|
||
|
|
</verify>
|
||
|
|
<acceptance_criteria>
|
||
|
|
- `grep -q "is_authenticated" core/archipelago/src/api/handler/model_proxy.rs`
|
||
|
|
- `grep -q "secrets/claude-api-key" core/archipelago/src/api/handler/model_proxy.rs`
|
||
|
|
- `grep -c "mod model_proxy" core/archipelago/src/api/handler/mod.rs` returns 1
|
||
|
|
- `cd core && cargo test --package archipelago model_proxy::` exits 0 with `claude_without_session_is_401`, `ollama_without_session_is_401`, `missing_key_is_503_not_500` and `inbound_authorization_header_is_not_forwarded` all passing
|
||
|
|
- `grep -rniE 'debug!|info!|warn!|error!' core/archipelago/src/api/handler/model_proxy.rs | grep -ciE 'body|api_key|x-api-key' | grep -qx 0` — no log statement in this file references a body or a key
|
||
|
|
</acceptance_criteria>
|
||
|
|
<reversibility rating="reversible">A forwarder is a handler; reverting restores the previous nginx target. The one-way part is the deleted second key ledger, which is a strict improvement.</reversibility>
|
||
|
|
<done>Unauthenticated requests to both `/aiui/api/claude/` and `/aiui/api/ollama/` are refused before any upstream call; authenticated ones succeed using the node's single key ledger.</done>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
<task type="auto">
|
||
|
|
<name>Task 2: Retire the Python sidecar, its key, and the OpenRouter open relay</name>
|
||
|
|
<files>image-recipe/configs/nginx-archipelago.conf, scripts/deploy-to-target.sh, scripts/setup-aiui-server.sh, core/archipelago/src/api/rpc/system/handlers.rs</files>
|
||
|
|
<read_first>
|
||
|
|
- `image-recipe/configs/nginx-archipelago.conf` lines 46-100 (first server block) **and** lines 955-995 (second server block) — both must change; a fix applied to only one leaves the exposure live on whichever block serves the request.
|
||
|
|
- `scripts/deploy-to-target.sh` lines 399, 703-735, 778-780, 875-956 — the `3141`→`3142` seds, the AIUI rsync section, and the embedded `claude-api-proxy.py` heredoc plus its systemd unit.
|
||
|
|
- `scripts/setup-aiui-server.sh` lines 29-47 and 115-125 — the `ANTHROPIC_API_KEY` requirement and the `patch-nginx-claude.py` step.
|
||
|
|
- `core/archipelago/src/api/rpc/system/handlers.rs` lines 1015-1072 — `handle_system_settings_set`'s `claude_api_key` branch, which today writes a **second** key copy to `secrets/claude-api-proxy.env` and restarts the sidecar.
|
||
|
|
- `CLAUDE.md` — "Verify on the real node .228 before any tag" and the commit/push discipline.
|
||
|
|
</read_first>
|
||
|
|
<action>
|
||
|
|
In **both** nginx server blocks: change each `location /aiui/api/claude/` and `location /aiui/api/ollama/` `proxy_pass` target from `http://127.0.0.1:3142/` and `http://127.0.0.1:11434/` to `http://127.0.0.1:5678` (the Rust daemon), preserving the full original request URI so the daemon sees `/aiui/api/claude/...` — i.e. use a `proxy_pass` without a trailing path component. Keep the existing long `proxy_read_timeout 300s` and `proxy_buffering off` so streaming still works. Replace the comment "API key managed by proxy, no session gate needed" with one stating that the daemon enforces the session — the old comment is the reasoning error that produced the exposure and must not survive as a template for the next person.
|
||
|
|
|
||
|
|
Delete both `location /aiui/api/openrouter/` blocks outright. Rationale to record in a replacement comment: the node holds no OpenRouter key, OpenRouter is not in D-04's backend chain, and an unauthenticated `proxy_pass` to a paid third-party API from the node's IP is a plain open relay. AIUI's standalone mode keeps its own proxy (D-17) and is unaffected.
|
||
|
|
|
||
|
|
In `scripts/deploy-to-target.sh`: delete the embedded `claude-api-proxy.py` heredoc, the `claude-api-proxy.service` unit creation, the `systemctl enable/restart claude-api-proxy` calls, the `EXISTING_KEY`/`ANTHROPIC_API_KEY` extraction, and both `3141`→`3142` `sed` fixups. Add a step that stops, disables and removes any pre-existing `claude-api-proxy` unit and deletes `/opt/archipelago/claude-api-proxy.py` and `<data_dir>/secrets/claude-api-proxy.env` on the target — deploying the fix without removing the old listener leaves the exposure running on every already-provisioned node.
|
||
|
|
|
||
|
|
In `scripts/setup-aiui-server.sh`: drop the hard `ANTHROPIC_API_KEY` requirement and the `patch-nginx-claude.py` invocation. The script's remaining job is the AIUI dist rsync; the key now lives only where `system.settings.set claude_api_key` puts it.
|
||
|
|
|
||
|
|
In `core/archipelago/src/api/rpc/system/handlers.rs`: in the `claude_api_key` branch, delete the `secrets/claude-api-proxy.env` write and the `systemctl restart claude-api-proxy` command. Keep the `secrets/claude-api-key` write and its 0600 permissions exactly as they are. Add a one-line comment naming that this is deliberately the only ledger.
|
||
|
|
|
||
|
|
Commit each file group as its own focused commit and push to `gitea-ai main` per CLAUDE.md. Stage explicitly by path — another agent may share the tree.
|
||
|
|
</action>
|
||
|
|
<!-- planner-discipline-allow: openrouter -->
|
||
|
|
<verify>
|
||
|
|
<automated>grep -c 'openrouter' image-recipe/configs/nginx-archipelago.conf | grep -qx 0</automated>
|
||
|
|
<automated>grep -c '3142' image-recipe/configs/nginx-archipelago.conf scripts/deploy-to-target.sh scripts/setup-aiui-server.sh | grep -vq ':[1-9]'</automated>
|
||
|
|
<automated>grep -c 'claude-api-proxy' core/archipelago/src/api/rpc/system/handlers.rs | grep -qx 0</automated>
|
||
|
|
<automated>cd core && CARGO_INCREMENTAL=0 cargo build --package archipelago 2>&1 | tail -5</automated>
|
||
|
|
</verify>
|
||
|
|
<acceptance_criteria>
|
||
|
|
- `grep -c 'openrouter' image-recipe/configs/nginx-archipelago.conf` returns 0
|
||
|
|
- `grep -c '127.0.0.1:3142' image-recipe/configs/nginx-archipelago.conf` returns 0
|
||
|
|
- `grep -c 'location /aiui/api/claude/' image-recipe/configs/nginx-archipelago.conf` returns 2 — **both** server blocks were changed, not one
|
||
|
|
- `grep -c 'claude-api-proxy' scripts/deploy-to-target.sh` returns a number > 0 only for lines that *remove* the unit; `grep -c 'PORT = 3142' scripts/deploy-to-target.sh` returns 0
|
||
|
|
- `grep -c 'claude-api-proxy' core/archipelago/src/api/rpc/system/handlers.rs` returns 0
|
||
|
|
- `grep -c 'secrets/claude-api-key' core/archipelago/src/api/rpc/system/handlers.rs` returns ≥ 1 — the surviving single ledger
|
||
|
|
- `cd core && cargo build --package archipelago` exits 0
|
||
|
|
- `git log --oneline -3` shows focused commits pushed to `gitea-ai main`
|
||
|
|
</acceptance_criteria>
|
||
|
|
<done>No node built or deployed from this repo starts a `claude-api-proxy` unit, holds a second `ANTHROPIC_API_KEY`, or serves an OpenRouter relay; both nginx server blocks route the AIUI model paths through the authenticated daemon.</done>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
<task type="checkpoint:human-verify" gate="blocking">
|
||
|
|
<name>Task 3: Prove it on a real node — a green cargo test proves nothing here</name>
|
||
|
|
<files>tests/production-quality/aiui-proxy-closed.sh</files>
|
||
|
|
<read_first>
|
||
|
|
- `tests/production-quality/lnd-cors-test.sh` — the existing shell-test precedent in this directory: shebang, argument handling, pass/fail output shape, exit code convention.
|
||
|
|
- `.planning/phases/13-.../13-AI-SPEC.md` §5, invariant **S-15** and the sentence after the table: "S-15 is not a unit test and must not be treated as one."
|
||
|
|
- `CLAUDE.md` — "Verify on the real node .228 before any tag"; reachable dev nodes and their creds are in the project memory notes.
|
||
|
|
</read_first>
|
||
|
|
<what-built>
|
||
|
|
`tests/production-quality/aiui-proxy-closed.sh <node-host>` — a shell check that, with **no**
|
||
|
|
session cookie, requests `/aiui/api/claude/v1/messages`, `/aiui/api/ollama/api/tags` and
|
||
|
|
`/aiui/api/openrouter/` against a live node and asserts each returns 401, 403 or 404 and never
|
||
|
|
200. It also asserts over SSH that no `claude-api-proxy` systemd unit is loaded and that nothing
|
||
|
|
is listening on port 3142.
|
||
|
|
|
||
|
|
Write the script (following `lnd-cors-test.sh`'s shape), deploy the built binary and the nginx
|
||
|
|
config to a dev node, then run it.
|
||
|
|
</what-built>
|
||
|
|
<how-to-verify>
|
||
|
|
1. Build and deploy to the dev pair per `CLAUDE.md` (`ARCHIPELAGO_TARGET=... scripts/deploy-to-target.sh`) — archi-dev-box first, per the standing "deploy to the dev pair BEFORE any OTA" rule.
|
||
|
|
2. Run `bash tests/production-quality/aiui-proxy-closed.sh <node-host>` from your workstation. Expect every line to report the status code and `ok`.
|
||
|
|
3. Confirm the positive case still works: log in to neode-ui on that node in a browser, open the Chat view, and confirm the embedded AIUI still answers. (The path is unchanged; only its auth and upstream moved.)
|
||
|
|
4. On the node: `systemctl status claude-api-proxy` must report `Unit claude-api-proxy.service could not be found`, and `ss -ltnp | grep 3142` must return nothing.
|
||
|
|
5. Confirm the key ledger: `sudo ls /var/lib/archipelago/secrets/` shows `claude-api-key` and **no** `claude-api-proxy.env`.
|
||
|
|
</how-to-verify>
|
||
|
|
<acceptance_criteria>
|
||
|
|
- `bash tests/production-quality/aiui-proxy-closed.sh <node>` exits 0
|
||
|
|
- `curl -s -o /dev/null -w '%{http_code}' http://<node>/aiui/api/claude/v1/messages` returns 401, 403 or 404 — never 200
|
||
|
|
- `curl -s -o /dev/null -w '%{http_code}' http://<node>/aiui/api/openrouter/` returns 404
|
||
|
|
- `ssh <node> 'systemctl is-active claude-api-proxy'` reports `inactive` or `unknown`, and `ssh <node> 'ss -ltn | grep -c :3142'` returns 0
|
||
|
|
- `ssh <node> 'sudo ls /var/lib/archipelago/secrets/'` lists `claude-api-key` and does not list `claude-api-proxy.env`
|
||
|
|
- An authenticated browser session on that node still gets a chat reply in the embedded AIUI
|
||
|
|
</acceptance_criteria>
|
||
|
|
<resume-signal>Type "approved" with the four status codes you observed, or describe what still answered 200.</resume-signal>
|
||
|
|
</task>
|
||
|
|
|
||
|
|
</tasks>
|
||
|
|
|
||
|
|
<threat_model>
|
||
|
|
## Trust Boundaries
|
||
|
|
|
||
|
|
| Boundary | Description |
|
||
|
|
|----------|-------------|
|
||
|
|
| public web port → `/aiui/api/*` | **The boundary that is currently open.** Today: anonymous → paid third-party API on the owner's dime |
|
||
|
|
| nginx → Rust daemon (127.0.0.1:5678) | Loopback; the daemon re-derives auth from the forwarded cookie, it does not trust nginx |
|
||
|
|
| node → api.anthropic.com / 127.0.0.1:11434 | Egress carrying chat text and the node's key |
|
||
|
|
| operator settings → key at rest | `system.settings.set claude_api_key` → `secrets/claude-api-key`, 0600 |
|
||
|
|
|
||
|
|
## STRIDE Threat Register
|
||
|
|
|
||
|
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||
|
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
||
|
|
| T-13-08 | Elevation of Privilege | `/aiui/api/claude/` (port-3142 proxy) | **critical** | mitigate | Re-point to the daemon behind `is_authenticated`; delete the sidecar, its unit and its key. Verified on a real node by `aiui-proxy-closed.sh` (S-15), not by `cargo test` |
|
||
|
|
| T-13-09 | Denial of Service (financial) | Same — anonymous budget exhaustion | **critical** | mitigate | Same fix. Budget exhaustion was reachable by anyone who could route to the node's web port |
|
||
|
|
| T-13-10 | Elevation of Privilege | `/aiui/api/openrouter/` open relay | high | mitigate | Deleted. Not in D-04's chain; the node holds no key for it; an unauthenticated relay from the node's IP is abusable independently of any node key |
|
||
|
|
| T-13-11 | Elevation of Privilege | `/aiui/api/ollama/` free local compute | medium | mitigate | Same session gate. Anonymous local-GPU/CPU inference is a resource-exhaustion vector even with no key involved |
|
||
|
|
| T-13-12 | Information Disclosure | Two key ledgers (`claude-api-key` + `claude-api-proxy.env`) | high | mitigate | Collapse to one. `secrets/claude-api-proxy.env` is deleted on deploy, and `system.settings.set` stops writing it |
|
||
|
|
| T-13-13 | Information Disclosure | Chat bodies in the daemon's journal | medium | mitigate | Field policy in `model_proxy.rs`: log path/status/upstream host only. Asserted by the no-body-in-log grep |
|
||
|
|
| T-13-14 | Spoofing | Inbound `authorization`/`x-api-key` forwarded upstream | medium | mitigate | Request headers are allowlisted to `content-type`/`accept`; asserted by `inbound_authorization_header_is_not_forwarded` |
|
||
|
|
| T-13-15 | Tampering | Fix applied to only one of the two nginx server blocks | high | mitigate | Acceptance criterion counts `location /aiui/api/claude/` == 2 and `openrouter` == 0 across the whole file |
|
||
|
|
| T-13-SC | Tampering | npm/pip/cargo installs | high | mitigate | This plan adds **zero** packages; it removes a Python one. No install task, so no legitimacy checkpoint is required |
|
||
|
|
</threat_model>
|
||
|
|
|
||
|
|
<verification>
|
||
|
|
- `cd core && CARGO_INCREMENTAL=0 cargo test --package archipelago model_proxy::` green
|
||
|
|
- `grep -c openrouter image-recipe/configs/nginx-archipelago.conf` == 0
|
||
|
|
- `bash tests/production-quality/aiui-proxy-closed.sh <dev-node>` exits 0 against a real deployed node
|
||
|
|
- Positive path preserved: an authenticated browser session still gets a chat reply from the embedded AIUI
|
||
|
|
</verification>
|
||
|
|
|
||
|
|
<success_criteria>
|
||
|
|
The live unauthenticated door into a paid API is closed on the source of truth (both nginx
|
||
|
|
server blocks), on the deploy path (no sidecar is installed and any existing one is removed),
|
||
|
|
and on already-provisioned nodes — and that is demonstrated with `curl` against a real node,
|
||
|
|
not with a unit test.
|
||
|
|
</success_criteria>
|
||
|
|
|
||
|
|
<output>
|
||
|
|
Create `.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-02-SUMMARY.md` when done
|
||
|
|
</output>
|