Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5382e1b
docs(spec): tray glance v2 — grouped calls, intent reasons, failure-o…
Dumbris Jul 31, 2026
bd3b87f
docs(spec): tray glance v2 — revise after codex round 1 (data contrac…
Dumbris Jul 31, 2026
477cd42
docs(spec): add sanitized activity replay fixture for spec 090 (1564 …
Dumbris Jul 31, 2026
0a67366
docs(spec): tray glance v2 round-2 fixes — subtitle mechanism w/ macO…
Dumbris Jul 31, 2026
c30323a
docs(spec): tray glance v2 round-3 fixes — canonical blocked literal,…
Dumbris Jul 31, 2026
92ecafa
docs(plan): tray glance v2 — plan, research, data model, contracts, q…
Dumbris Jul 31, 2026
8e8298d
docs(plan): tray glance v2 — plan review fixes (16 policy emit sites,…
Dumbris Jul 31, 2026
d780d23
docs(tasks): tray glance v2 — 24 TDD tasks across 5 stories
Dumbris Jul 31, 2026
41a40bb
docs(tasks): tray glance v2 — strict red/green task pairs (35 tasks),…
Dumbris Jul 31, 2026
373fc7f
docs(tasks): tray glance v2 — clarify [P] scope (within-phase only) a…
Dumbris Jul 31, 2026
eb6d778
feat(tray): add ActivityEntry reason and outcome-class accessors
Dumbris Jul 31, 2026
0c708c9
feat(tray): group consecutive glance records into runs
Dumbris Jul 31, 2026
34cb704
feat(tray): render grouped glance rows with a repeat count
Dumbris Jul 31, 2026
deee455
feat(httpapi): keep contextual metadata whitelist under exclude_payloads
Dumbris Jul 31, 2026
4ee0a87
fix(storage): order recent sessions by last activity before truncating
Dumbris Jul 31, 2026
a70c5ba
feat(tray): carry caller intent reasons on live glance rows
Dumbris Jul 31, 2026
acd8115
feat(tray): show the reason a call happened as a glance row subtitle
Dumbris Jul 31, 2026
0856d45
fix(tray): treat a glance row's line count as structural, atomically
Dumbris Jul 31, 2026
cd1b33d
feat(tray): ask the activity poll for policy decisions too
Dumbris Jul 31, 2026
b9306eb
feat(tray): route policy decisions into the live glance feed
Dumbris Jul 31, 2026
2053f53
feat(tray): mark only failures and show blocked calls in the glance
Dumbris Jul 31, 2026
176d1cc
feat(activity): thread a request id through every policy decision
Dumbris Jul 31, 2026
136733d
feat(tray): classify client presence instead of "connected or not"
Dumbris Jul 31, 2026
1c4c6af
feat(tray): show client presence instead of an empty Clients section
Dumbris Jul 31, 2026
eae413e
feat(tray): put the 24h picture above the rows, not below them
Dumbris Jul 31, 2026
57bddfe
test(tray): replay the six-week fixture through the row pipeline
Dumbris Jul 31, 2026
3529a29
test(tray): drive the real menu-open path and count its requests
Dumbris Jul 31, 2026
419fa10
test(tray): simplify the glance-built assertion in MenuOpenNetworkTests
Dumbris Jul 31, 2026
dafeb16
docs(activity): document the exclude_payloads contextual metadata whi…
Dumbris Jul 31, 2026
dbf0f26
fix(tray): treat zero-value last_activity as missing in glance presence
Dumbris Jul 31, 2026
9004a77
fix(activity): make the fallback request-id mint collision-free
Dumbris Jul 31, 2026
8fbc54d
fix(api): keep only string values in the exclude_payloads whitelist
Dumbris Jul 31, 2026
395c865
test(activity): pin the request-id counter suffix deterministically
Dumbris Jul 31, 2026
f6a3f60
fix(activity): route every locally minted id through the counter
Dumbris Jul 31, 2026
d6eaa29
fix(dashboard): share the tray's last-activity rule in Recent Sessions
Dumbris Jul 31, 2026
52094d4
feat(macos): MCPPROXY_SOCKET_PATH override for QA against an isolated…
Dumbris Jul 31, 2026
2798d98
docs(qa): tray glance v2 manual protocol executed — 11/14 pass, 3 wai…
Dumbris Aug 1, 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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,6 @@ tail -f ~/Library/Logs/mcpproxy/main.log # main log (macOS; Linux: ~/.mcpproxy/
- **Windows installer**: [docs/github-actions-windows-wix-research.md](docs/github-actions-windows-wix-research.md). **Prerelease** (`next` branch + `v*-rc.*` tags, opt-in, off stable channels): [docs/prerelease-builds.md](docs/prerelease-builds.md).

## Recent Changes
- 090-tray-glance-v2: Added Swift 5.9 (AppKit, SwiftUI for histogram only) + Go 1.25 (core) + AppKit `NSMenuItem` (incl. `subtitle`, macOS 14.4+), fixture-driven XCTest; Go: chi HTTP API, bbolt storage, zap
- 088-scanner-trust-ui: Added TypeScript 5.9, Vue 3.5 (Composition API, `<script setup>`), Tailwind v4 + Existing only — hand-written `ApiClient` singleton (`frontend/src/services/api.ts`), Pinia stores (`stores/servers.ts`, `stores/system.ts`), generated `types/contracts.ts` (via `cmd/generate-types`), hand-written `types/api.ts`. **No new npm dependency.**
- 084-toon-output: Adaptive TOON encoding of `call_tool_*` result text blocks (`internal/toonenc/`, seam in `internal/server/toon_encode.go`). Config `toon_output` (off|adaptive|always, per-server override) + `toon_min_savings_pct`; off by default, hot-reloadable; never-larger by construction; sanitise-before-encode, detection-parity, truncate-after-encode invariants. **New dependency: `github.com/toon-format/toon-go`** (the production encoder; shared with the spec-083 profiler). Docs: [docs/features/toon-output.md](docs/features/toon-output.md).
- 083-discovery-profiler: Added Go 1.24 (bench package, same module as mcpproxy-go); Node.js ≥20 for the TSCG arm subprocess (CI-provided, matches existing E2E prereqs); Python via `uv` for dataset fetch + LAP (CI only)
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -791,3 +791,4 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—`
| [087-tpa-daily-refresh](./specs/087-tpa-daily-refresh/) | — | — |
| [088-scanner-trust-ui](./specs/088-scanner-trust-ui/) | `shipped` | 29/29 (100%) |
| [089-remote-access-tunnel](./specs/089-remote-access-tunnel/) | — | — |
| [090-tray-glance-v2](./specs/090-tray-glance-v2/) | `shipped` | 35/35 (100%) |
44 changes: 44 additions & 0 deletions docs/features/activity-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,47 @@ GET /api/v1/activity
| `limit` | integer | Max records (1-100, default: 50) |
| `offset` | integer | Pagination offset (default: 0) |
| `include_call_tool` | boolean | Include successful `call_tool_*` internal tool calls (default: false). By default, successful `call_tool_*` are excluded because they appear as duplicates alongside their upstream `tool_call` entries. Failed `call_tool_*` are always shown. |
| `exclude_payloads` | boolean | Omit the bulky fields — `arguments`, `response` — and narrow `metadata` to a contextual whitelist (default: false). See below. |

#### `exclude_payloads` and the contextual metadata whitelist

`exclude_payloads=true` is a projection, not a filter: it never changes which
records match, only how much of each record is serialized. It exists for clients
that poll frequently and render summary fields only (the macOS tray glance polls
the newest 100 records on every menu open). Measured against a real log those
100 records are ~848 KB whole and ~30 KB projected.

The projection drops `arguments` and `response` entirely, and keeps only these
`metadata` keys:

| Kept key | Why |
|----------|-----|
| `intent.reason` | The caller's stated reason, rendered as the row subtitle |
| `intent.operation_type` | `read` / `write` / `destructive` |
| `decision` | `blocked`, `warned`, … for `policy_decision` records |
| `reason` | Why a policy decision blocked or warned |
| `client_name` | Which MCP client made the call |
| `client_version` | Its version |

Everything else in `metadata` — sensitive-data detection payloads, toon
renderings, classifier scores, raw intent objects — is dropped. Whitelisted keys
that a record does not have are simply omitted, and a record whose metadata is
entirely non-whitelisted serializes with no `metadata` object at all.

Every kept key is kept only when its value is a **string**. A whitelisted key is
not a promise about its value: a producer that writes a structured error under
`reason` would otherwise smuggle that whole payload through the projection, so
non-string values are dropped like any other non-whitelisted content.

Two things survive the projection deliberately:

- `has_sensitive_data` is derived from `metadata` **before** it is narrowed, so
the flag outlives its source.
- `request_id` is a top-level field, not metadata, so correlation still works on
projected responses.

Fetch the full record with `GET /api/v1/activity/{id}` when a client needs the
arguments, the response, or any non-whitelisted metadata.

**Example:**

Expand All @@ -313,6 +354,9 @@ curl -H "X-API-Key: $KEY" "http://127.0.0.1:8080/api/v1/activity?server=github-s

# Filter by time range
curl -H "X-API-Key: $KEY" "http://127.0.0.1:8080/api/v1/activity?start_time=2025-01-15T00:00:00Z"

# Summary-only poll: no arguments/response, metadata narrowed to the whitelist
curl -H "X-API-Key: $KEY" "http://127.0.0.1:8080/api/v1/activity?type=tool_call,internal_tool_call,policy_decision&limit=100&exclude_payloads=true"
```

**Response:**
Expand Down
60 changes: 58 additions & 2 deletions internal/httpapi/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func parseActivityFilters(r *http.Request) storage.ActivityFilter {
// @Param end_time query string false "Filter activities before this time (RFC3339)"
// @Param limit query int false "Maximum records to return (1-100, default 50)"
// @Param offset query int false "Pagination offset (default 0)"
// @Param exclude_payloads query bool false "Omit arguments, response and metadata (default: false). For clients that render summary fields only; has_sensitive_data is still derived before metadata is dropped."
// @Param exclude_payloads query bool false "Omit arguments, response and metadata except a contextual whitelist (intent.reason, intent.operation_type, decision, reason, client_name, client_version) (default: false). For clients that render summary fields only; has_sensitive_data is still derived before metadata is dropped."
// @Success 200 {object} contracts.APIResponse{data=contracts.ActivityListResponse}
// @Failure 400 {object} contracts.APIResponse
// @Failure 401 {object} contracts.APIResponse
Expand Down Expand Up @@ -172,6 +172,11 @@ func (s *Server) handleListActivity(w http.ResponseWriter, r *http.Request) {
// log, the newest 100 tool-call records are ~848KB whole and ~30KB projected.
// HasSensitiveData is derived by storageToContractActivity from Metadata
// BEFORE it is dropped, so the flag survives its source.
//
// Metadata is not dropped wholesale: a small whitelist of short contextual
// strings (why a call happened, whether policy blocked it, who called)
// survives, because the tray renders them and re-fetching each record in
// full to get an 80-character reason would undo the projection's point.
excludePayloads := r.URL.Query().Get("exclude_payloads") == "true"
contractActivities := make([]contracts.ActivityRecord, len(activities))
for i, a := range activities {
Expand All @@ -180,7 +185,7 @@ func (s *Server) handleListActivity(w http.ResponseWriter, r *http.Request) {
contractActivities[i].Arguments = nil
contractActivities[i].Response = ""
contractActivities[i].ResponseTruncated = false
contractActivities[i].Metadata = nil
contractActivities[i].Metadata = projectContextualMetadata(contractActivities[i].Metadata)
}
}

Expand Down Expand Up @@ -234,6 +239,57 @@ func (s *Server) handleGetActivityDetail(w http.ResponseWriter, r *http.Request)
s.writeSuccess(w, response)
}

// contextualMetadataKeys are the top-level metadata keys that survive the
// `exclude_payloads` projection (contracts/api-deltas.md §1). Everything here is
// a short string the tray glance shows verbatim; anything unbounded (arguments,
// responses, toon renderings, detection payloads) is deliberately absent.
var contextualMetadataKeys = []string{"decision", "reason", "client_name", "client_version"}

// contextualIntentKeys are the keys kept inside `metadata.intent`. The rest of
// the intent object (scores, raw classifier output) is not rendered anywhere.
var contextualIntentKeys = []string{"reason", "operation_type"}

// projectContextualMetadata narrows metadata to the contextual whitelist,
// returning nil when nothing whitelisted is present so an all-dropped record
// serialises as an absent object rather than an empty one.
//
// Only string values are kept. A whitelisted key is not a promise about its
// value, and nothing stops a producer from putting a structured error under
// `reason` — copying by key alone would carry that whole nested payload through
// the one boundary callers are told payloads cannot cross.
//
// The result is always a fresh map: the input belongs to the storage layer (the
// controller may hand back live records) and must not be edited in place.
func projectContextualMetadata(metadata map[string]interface{}) map[string]interface{} {
if len(metadata) == 0 {
return nil
}

projected := make(map[string]interface{}, len(contextualMetadataKeys)+1)
for _, key := range contextualMetadataKeys {
if value, ok := metadata[key].(string); ok {
projected[key] = value
}
}

if intent, ok := metadata["intent"].(map[string]interface{}); ok {
projectedIntent := make(map[string]interface{}, len(contextualIntentKeys))
for _, key := range contextualIntentKeys {
if value, ok := intent[key].(string); ok {
projectedIntent[key] = value
}
}
if len(projectedIntent) > 0 {
projected["intent"] = projectedIntent
}
}

if len(projected) == 0 {
return nil
}
return projected
}

// storageToContractActivity converts a storage ActivityRecord to a contracts ActivityRecord.
func storageToContractActivity(a *storage.ActivityRecord) contracts.ActivityRecord {
hasSensitiveData, detectionTypes, maxSeverity := extractSensitiveDataInfo(a)
Expand Down
Loading
Loading