Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
43a4d2d
test(scope): Spec 105 PR E red phase — log attribution, OAuth stop ro…
Dumbris Sep 16, 2026
d2e2c90
feat(scope): Spec 105 PR E — per-record log attribution, subject-boun…
Dumbris Sep 16, 2026
8c96f43
fix(scope): PR E critique round 1 — over-long lines skipped, containe…
Dumbris Sep 16, 2026
8fc9da3
fix(scope): PR E codex round 1 — first-boundary log attribution, owne…
Dumbris Sep 16, 2026
1de7f53
fix(scope): PR E codex round 2 — one-header console records, subject-…
Dumbris Sep 16, 2026
3839237
test(upstream): rename the fake-docker shell quoter — sandbox_linux_t…
Dumbris Sep 16, 2026
dbd0a30
test(upstream): keep a real docker out of PATH for the fake-docker fi…
Dumbris Sep 16, 2026
4ab31a9
test(logs): skip the shared-file rotation fixture on Windows
Dumbris Sep 16, 2026
c26f017
fix(scope): PR E review round 4 — sweep outcome records carry the rea…
Dumbris Sep 16, 2026
c35c20a
fix(scope): PR E review round 5 — sweeps re-verify ownership at mutat…
Dumbris Sep 16, 2026
66792da
fix(scope): PR E review round 6 — every Docker mutation re-verifies o…
Dumbris Sep 16, 2026
7c8d193
fix(scope): PR E review round 8 — cidfile refusals and container heal…
Dumbris Sep 16, 2026
b13e950
fix(scope): PR E review round 9 — lifecycle log lines and diagnostics…
Dumbris Sep 16, 2026
787b4cb
test(upstream): pin the direct-exec docker path in the fake-docker fi…
Dumbris Sep 16, 2026
fc0f831
fix(scope): PR E review round 11 — housekeeping counts and cidfile-re…
Dumbris Sep 17, 2026
b8ace88
fix(scope): PR E review round 13 — HasDockerContainers applies canoni…
Dumbris Sep 17, 2026
26a0b42
fix(upstream): drop ownerless rejected-row count from container sweep
Dumbris Sep 17, 2026
ccf9833
fix(scope): PR E review round 16 — container state from the Verify re…
Dumbris Sep 17, 2026
6855307
Merge remote-tracking branch 'origin/main' into pr1284-105-e-logs
Dumbris Sep 18, 2026
916372f
fix(scope): scope canonical container ownership to this mcpproxy inst…
Dumbris Sep 18, 2026
216fcab
fix(scope): reject tab-corrupted docker-ps rows in ownership reads (F…
Dumbris Sep 18, 2026
6aaaad4
fix(scope): stop trimming docker-ps output before splitting into rows…
Dumbris Sep 18, 2026
6e460d7
fix(scope): fail whole docker-ps read closed on any malformed row (FR…
Dumbris Sep 18, 2026
f74166a
Merge remote-tracking branch 'origin/main' into pr1284-105-e-logs
Dumbris Sep 18, 2026
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
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,7 @@ graph LR
| Web UI + macOS app UX audit | In progress | P0 | — | | |
| Release qualification gate (auto-QA matrix blocks the tag) | In progress | P0 | — | [081-release-qa-gate](./specs/081-release-qa-gate/) | |
| Action log / transparency — info at a glance | In progress | P1 | — | | |
| Agent-token scope hardening: every MCP request authorized by its own scope (spec 105) | In progress | P1 | 18/109 (17%) | [105-agent-scope-hardening](./specs/105-agent-scope-hardening/) | |
| Agent-token scope hardening: every MCP request authorized by its own scope (spec 105) | In progress | P1 | 31/109 (28%) | [105-agent-scope-hardening](./specs/105-agent-scope-hardening/) | |
| Token-efficiency benchmark: measured savings, published results | In progress | P1 | 62/64 (97%) | [103-token-bench](./specs/103-token-bench/) | |
| Telemetry identity & data quality (machine_id + CI-filter hardening) | In progress | P1 | — | | |
| Telemetry v7: honest funnel + churn instrumentation | In progress | P1 | — | [080-telemetry-v7-churn](./specs/080-telemetry-v7-churn/) | |
Expand Down Expand Up @@ -1034,6 +1034,6 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—`
| [102-schema-deferred](./specs/102-schema-deferred/) | `shipped` | 89/89 (100%) |
| [103-token-bench](./specs/103-token-bench/) | `shipped` | 62/64 (97%) |
| [104-auto-routing-mode](./specs/104-auto-routing-mode/) | — | — |
| [105-agent-scope-hardening](./specs/105-agent-scope-hardening/) | `in-flight` | 18/109 (17%) |
| [105-agent-scope-hardening](./specs/105-agent-scope-hardening/) | `in-flight` | 31/109 (28%) |
| [106-security-residual-fixes](./specs/106-security-residual-fixes/) | `shipped` | 18/19 (95%) |
| [107-server-edition-sso-hardening](./specs/107-server-edition-sso-hardening/) | `in-flight` | 102/126 (81%) |
43 changes: 43 additions & 0 deletions docs/features/agent-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,49 @@ Denied to agent tokens on both surfaces:

On the MCP surface (`upstream_servers`, `quarantine_security`) these return a tool error; on the REST surface (mutating `/api/v1/servers/...`, `/api/v1/config/...`, and `/api/v1/registries/...` routes) they return **`403 Forbidden`** (`operation requires admin access`). Read-only operations stay available to scoped tokens: `upstream_servers` `list`/`tail_log`, `GET /api/v1/servers`, per-server diagnostics, registry reads, and `GET /api/v1/index/search` (which honors quarantine — a quarantined server's tools are withheld from search on every surface). Those reads are **scope-filtered** as described above. `GET /api/v1/config` is the exception: it is an admin document (it carries the global `api_key`, every server's credentials, and a second enumeration of server names under `profiles[].servers`), so it returns `403` for an agent token rather than a filtered view.

**Log attribution on `tail_log`.** Per-server log files are named from a
*sanitised* server name, so two configured servers can share one file —
`a/b` and `a_b` both write `server-a_b.log`, and on a case-insensitive
filesystem so do `A` and `a`. `upstream_servers` `tail_log` therefore
returns a scoped token only the records **attributable to the server it
named**: every record mcpproxy writes carries its writer's server identity,
and the reader filters on it *before* applying the line limit, so
`lines_returned` counts the authorized tail and a co-owner's interleaved
record never displaces an authorized one. The rule is the same whether or
not a co-owner exists — a scoped caller never gets a whole-file refusal that
depends on another server sharing the file, and a single over-long line in
the shared file (longer than 1 MiB) is skipped rather than failing the read.
Withheld from scoped callers:
records with no writer identity (lines written before this rule existed,
hand-appended lines, torn fragments), records about a container — by id,
name or count — that do not prove the container's owner (`container_owner`,
written by container housekeeping since this rule — earlier housekeeping
records are treated as non-attributable), and records whose subject is
another server (an OAuth callback tear-down that an earlier version routed
through the wrong server's logger), and a child process's own output line
that names a container — Docker's `docker run` name-conflict error, for
instance, quotes the *other* container's name and id when two servers'
generated container names collide (`a/b` and `a-b` both produce
`mcpproxy-a-b-…`) — the same rule covers the "Connection failed" record
whose error re-emits that stderr. For the same reason a scoped token's
`connection_status.last_error` (on `tail_log` and `list`, and the health
detail derived from it) has container ids, canonical container names and
Docker's name-conflict phrase replaced by `[container]`; a server that is
itself *named* like a container (`mcpproxy-tenant-abcd`) is not a container
mention, so its ordinary child output stays attributable. Retained effects: rotation and retention
of a shared file stay shared, so a co-owner's output can rotate an authorized
record out of the readable history; and child process output is attributed
to the server whose process wrote it — a child cannot forge another server's
identity. The administrator readers — `tail_log` with the API key or over the
local socket, and `mcpproxy upstream logs` — keep the whole file exactly as
before; a profile on the URL (`/mcp/p/<slug>`) bounds *which* server an
administrator may name, not which records of it they see. The REST endpoint
`GET /api/v1/servers/{id}/logs` is **not** attributed: it serves the whole
shared file to any caller entitled to the server name, agent tokens
included. Until it is aligned with `tail_log`, do not rely on it to keep a
co-owner's records from a scoped token — the REST management API's scope
policy is a separate piece of work.

## Profile Pinning

A [profile](./profiles.md) scopes tool discovery and calls to a named subset of upstream servers. With `--profile-pin`, you can **bind a token to a single profile** so it can never operate outside it — regardless of the URL it connects to or any `set_profile` call it makes.
Expand Down
59 changes: 56 additions & 3 deletions docs/features/docker-isolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,18 +348,71 @@ When MCPProxy stops, containers are cleaned up with a 30-second timeout:
1. **Graceful Stop**: `docker stop` (sends SIGTERM to container)
2. **Force Kill**: `docker kill` if container doesn't stop gracefully

Containers are labeled with `mcpproxy.managed=true` for identification.
Containers are labeled with `com.mcpproxy.managed=true` for identification
and `com.mcpproxy.server=<server name>` (the raw, unsanitised name) for
ownership.

### Container ownership

Every container mcpproxy creates is named
`mcpproxy-<sanitised server name>-<4 random chars>`. The name alone does not
identify the server — `a/b` and `a-b` both sanitise to `a-b` — so every
cleanup path (the pre-start sweep for stale containers, the container
captured from `--cidfile`, the disconnect fallbacks by exact name, by name
pattern and by image name) inspects the container and stops or removes it
only when its `com.mcpproxy.server` label **and** canonical name both match
the server being cleaned up — and it re-inspects the container immediately
before every `docker stop`, `kill` or `rm -f` (the kill after a failed stop
included), never acting on an earlier listing, so a container renamed,
relabelled or replaced in between is left alone. Containers you started yourself with
`docker run --name …`, or that pre-date the label, are never touched by any
of these paths, and a container that merely shares an image with a server's
is never stopped on that server's behalf. Housekeeping records in the
per-server log carry `container_owner` (the label value read back from
Docker) so [`tail_log`](/features/agent-tokens) can attribute them to the
right server; the pre-start "Docker isolation configured" record names the
generated container name before Docker has created anything and carries no
owner. The child process's own output lines (the docker CLI's stderr
included) are written to the per-server log as the `message` field of a
`stderr` or `launcher` record marked `child_output`, never as the record
text.

Two consequences of the ownership rule are worth knowing:

- **Servers you configure as `docker run …` yourself** (no isolation) get no
`com.mcpproxy.server` label — MCPProxy only labels the containers it
builds for isolation — so MCPProxy never stops or removes their
container, not even through the `--cidfile` it injects into your command.
Use `--rm` (and let the container exit when its stdin closes) or stop it
by hand; earlier versions would stop it via the cidfile and, if that
capture failed, every container on the same image, yours or not.
- **Renaming a server** changes the label value a container must carry. A
container created under the old name is no longer owned by the new one,
so it is left alone by the pre-start sweep and must be removed manually
(`docker rm -f`).
- **The shutdown and emergency sweeps** (every `com.mcpproxy.managed`
container on shutdown; every one carrying this instance's id when
shutdown fails) apply the same rule: only containers canonically owned by
a server in the current configuration are stopped or removed — each one
re-inspected immediately before its stop, kill or removal through the
same check every per-server cleanup uses, so a container renamed or
relabelled after the sweep listed it is left alone — and the
disconnect-timeout path re-checks the ownership of the id it tracked
before `docker rm -f`. A container that merely carries the mcpproxy
labels — one you labelled yourself, or an orphan of a server that is no
longer configured — is left alone and only counted in a warning, never
named. Remove such orphans by hand (see below).

### Manual Cleanup

If containers remain after MCPProxy stops:

```bash
# List MCPProxy-managed containers
docker ps --filter "label=mcpproxy.managed=true"
docker ps --filter "label=com.mcpproxy.managed=true"

# Remove all MCPProxy containers
docker rm -f $(docker ps -q --filter "label=mcpproxy.managed=true")
docker rm -f $(docker ps -q --filter "label=com.mcpproxy.managed=true")
```

See [Shutdown Behavior](/operations/shutdown-behavior) for detailed subprocess lifecycle documentation.
Expand Down
Loading
Loading