feat(057): In-proxy profiles + permanent URLs (/mcp/p/<slug>)#622
Conversation
Foundational phase for in-proxy profiles (Spec 057, GH #55). No behaviour change yet — adds the config entity + validation and the request-scoped scope primitive; the /mcp/p/ routing and filter sites come next. - internal/config/profiles.go: ProfileConfig{Name,Servers} + ValidateProfiles (fatal: slug pattern ^[a-z0-9][a-z0-9_-]{0,62}$, reserved {all,code,call,p}, duplicate name; warn-skip: unknown server, empty servers). EffectiveServers helper. (T003) - internal/config/config.go: Config.Profiles []ProfileConfig `json:",omitempty"` after Servers (SC-004 byte-identical when absent); Validate() runs ValidateProfiles, fails on fatal, stashes warnings; ProfileWarnings() accessor for the boot path to log. (T004) - internal/profile/context.go: ProfileScope{Name, servers set} with nil-receiver allow-all Allows(), WithProfileScope/ProfileScopeFromContext (mirrors internal/auth/context.go). (T007) - Tests red-first: profiles_test.go (validation table + round-trip, T002/T005), profile/context_test.go (membership, nil allow-all, empty deny-all, context round-trip, T006). Both editions build; go vet clean. Related #55 Co-Authored-By: Paperclip <noreply@paperclip.ing>
…T008-T022) - profileMiddleware in server.go: resolves slug from config snapshot, builds ProfileScope, injects into context, 404s on missing/unknown profile - /mcp/p/ and /mcp/p routes registered after mcpAuthMiddleware (auth->profile) - retrieve_tools filter (T010): profile.Allows() independent of enforceAgentScope - call_tool_* filter (T011): profile error before token-scope check, names profile - upstream_servers list filter (T011a): excludes out-of-profile servers - code_execution filter (T011b): profile servers intersected into AllowedServers so empty caller list at a profile URL does not mean 'allow all' - ProfileScope.AllowedServerNames() helper for T011b intersection - withProfileMeta() injects metadata["profile"] at activity emit sites (T019) - Integration tests: 404-no-profiles, 404-unknown-slug, retrieve_tools isolation, full /mcp union unchanged, call_tool_* out-of-profile rejection, upstream_servers list filtering, policy intersection, per-server disabled_tools guard, activity metadata smoke-test, backward-compat endpoint reachability (T012-T018/T020) - docs/features/profiles.md Related #55
Copy config struct before mutating to avoid -race failures. Related #55
Deploying mcpproxy-docs with
|
| Latest commit: |
698b02f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://236955be.mcpproxy-docs.pages.dev |
| Branch Preview URL: | https://057-in-proxy-profiles-impl.mcpproxy-docs.pages.dev |
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
📦 Build ArtifactsWorkflow Run: View Run Available Artifacts
How to DownloadOption 1: GitHub Web UI (easiest)
Option 2: GitHub CLI gh run download 27101438678 --repo smart-mcp-proxy/mcpproxy-go
|
…etadata) Round-1 review fixes for the in-proxy profiles impl (MCP-1281/MCP-1278): 1. code_execution empty-effective-set bypass (MUST-FIX). A deny-all profile (servers: []) or a non-overlapping token∩profile yields an empty effective allow-list, which the JS runtime treated as "allow all" — leaking every server through code_execution. Add ExecutionOptions.RestrictToAllowed: when an active profile is in context, the allow-list is enforced even when empty (empty set = deny all). Set by the profile path in handleCodeExecution. 2. FR-011 metadata written to wrong shape (MUST-FIX). withProfileMeta smuggled the slug into the intent submap, so records landed as metadata.intent.profile and error paths dropped it entirely. Thread an explicit profile slug through EmitActivityToolCallCompleted → metadata["profile"] at top level, on success AND error paths. 3. Spec amendment (no code change): document per-request profile resolution (hot-reload takes effect on the next request) in spec.md + data-model.md, replacing the inaccurate "snapshot-until-reconnect" wording. Per-request re-resolution is kept as the safer behavior. Tests: jsruntime deny-all/non-empty enforcement + backward-compat; Activity service metadata["profile"] (success+error, not nested); code_execution deny-all/non-overlapping rejection at the profile path; upgraded the FR-011 integration test to assert the persisted record's metadata["profile"]. Related #622
There was a problem hiding this comment.
✅ Gatekeeper approval — Codex review verdict: ACCEPT.
This approval is posted automatically by the MCPProxy Gatekeeper App on behalf of the Codex reviewer (verdict of record lives in the Paperclip review thread). Author≠approver satisfied; QA + CI gates enforced separately.
Auto-approved per Model B (MCP-1249).
There was a problem hiding this comment.
✅ Gatekeeper approval — Codex review verdict: ACCEPT.
This approval is posted automatically by the MCPProxy Gatekeeper App on behalf of the Codex reviewer (verdict of record lives in the Paperclip review thread). Author≠approver satisfied; QA + CI gates enforced separately.
Auto-approved per Model B (MCP-1249).
There was a problem hiding this comment.
✅ Gatekeeper approval — Codex review verdict: ACCEPT.
This approval is posted automatically by the MCPProxy Gatekeeper App on behalf of the Codex reviewer (verdict of record lives in the Paperclip review thread). Author≠approver satisfied; QA + CI gates enforced separately.
Auto-approved per Model B (MCP-1249).
… (MCP-1249)
Hands-off half of Model B: a launchd timer runs gatekeeper-sweep.sh every 5m,
which invokes gatekeeper-approve.sh for each open PR. Codex ACCEPT -> App
approval -> GitHub auto-merge, no human, no admin.
- gatekeeper-approve.sh hardened to be safe under repeated/unattended runs:
- no-op if PR is closed/merged
- stale-verdict guard: only approve the exact SHA Codex reviewed (head moved
past it -> skip, re-review needed) [exit 6]
- idempotency: skip if the Gatekeeper already approved the current head
- resolver now also extracts the reviewer-pinned SHA
- gatekeeper-sweep.sh: bash-3.2 safe (macOS /bin/bash, no mapfile); skips
quietly when unconfigured or Paperclip unreachable; timestamped logging.
- app.mcpproxy.gatekeeper.sweep.plist: launchd template (StartInterval 300,
RunAtLoad, background/low-IO) + install/enable/disable instructions.
Validated: dry sweep over 14 open PRs no-ops correctly; idempotent no-op on the
already-merged #622; timer loaded and first run logged.
Related MCP-1249
* ci: gatekeeper App auto-approver bridge scaffold (MCP-1249) Bridge that turns a Paperclip Codex review verdict of ACCEPT into a real GitHub approving review posted by the "MCPProxy Gatekeeper" GitHub App, so the required-1-approving-review branch protection is satisfied without an admin override and without the author approving their own PR (author!=approver). Pairs with scripts/arm-auto-merge.sh for full Model B. - Reads verdict of record from the Paperclip review thread (bots don't post to GitHub); only ACCEPT approves, request_changes/unknown are no-ops. - Mints a GitHub App installation token (RS256 JWT via openssl, no extra deps). - Inert until configured (GATEKEEPER_APP_ID / _INSTALLATION_ID / _PRIVATE_KEY, e.g. ~/.mcpproxy-gatekeeper/env) — exits 2 with setup guidance. - --dry-run + --verdict overrides for testing. Validated against live data: reads accept for PR #622, fails safe unconfigured, no-ops on request_changes. App registration + cred wiring is the remaining (owner) step. Related MCP-1249 * ci: gatekeeper auto-trigger — sweep + launchd timer, hardened approve (MCP-1249) Hands-off half of Model B: a launchd timer runs gatekeeper-sweep.sh every 5m, which invokes gatekeeper-approve.sh for each open PR. Codex ACCEPT -> App approval -> GitHub auto-merge, no human, no admin. - gatekeeper-approve.sh hardened to be safe under repeated/unattended runs: - no-op if PR is closed/merged - stale-verdict guard: only approve the exact SHA Codex reviewed (head moved past it -> skip, re-review needed) [exit 6] - idempotency: skip if the Gatekeeper already approved the current head - resolver now also extracts the reviewer-pinned SHA - gatekeeper-sweep.sh: bash-3.2 safe (macOS /bin/bash, no mapfile); skips quietly when unconfigured or Paperclip unreachable; timestamped logging. - app.mcpproxy.gatekeeper.sweep.plist: launchd template (StartInterval 300, RunAtLoad, background/low-IO) + install/enable/disable instructions. Validated: dry sweep over 14 open PRs no-ops correctly; idempotent no-op on the already-merged #622; timer loaded and first run logged. Related MCP-1249 * fix(gatekeeper): fail-closed SHA guard — refuse approval without a matching reviewed SHA (MCP-1249) Codex REQUEST_CHANGES (PR #687): the stale-SHA guard in gatekeeper-approve.sh ran only `if [[ -n "$REVIEWED_SHA" && ... ]]`, so a verdict that pinned NO SHA (REVIEWED_SHA empty) — or the manual --verdict override path, which always yielded an empty SHA — bypassed the check and auto-approved the PR's CURRENT head. An old ACCEPT could thus approve unreviewed code after a post-review force-push. Fix (fail-closed): - Make the SHA checks UNCONDITIONAL. No reviewed SHA resolvable -> REFUSE (exit 7, explicit message); reviewed SHA != head -> REFUSE (exit 6, stale); only reviewed SHA == head approves. - Add --reviewed-sha to pair with --verdict so the manual override is held to the same fail-closed requirement (no blind manual approvals either). - Document the invariant + new exit code 7 in the header. Adds scripts/gatekeeper-approve.test.sh: hermetic (stubs gh, --dry-run, no network/GitHub) regression covering all four Codex acceptance cases. Red on the old script (case 1 approved blind), green on the fix. Co-Authored-By: Paperclip <noreply@paperclip.ing> --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
Summary
Implements Spec 057 — In-Proxy Profiles + Permanent URLs (GH #55).
What ships
internal/config/profiles.go):ProfileConfig{Name, Servers}, slug validation (^[a-z0-9][a-z0-9_-]{0,62}$), reserved slug rejection (all/code/call/p), dup/unknown-server rules,EffectiveServers()internal/profile/context.go): immutable request-scoped set, nil-receiver allow-all,AllowedServerNames()for T011b/mcp/p/<slug>route (server.go):profileMiddlewareregistered asmcpAuthMiddleware(profileMiddleware(logging(handler)))— auth before profile; 404 JSON responses for no-profiles and unknown-slugenforceAgentScope):retrieve_tools: profile filter before agent-scope checkcall_tool_*: profile check first, names the profile in the error messageupstream_servers list: profile filter excludes out-of-profile servers (Codex docs(057): in-proxy profiles plan + tasks (Related #55) #621 finding 1)code_execution: profile servers intersected intoAllowedServers— empty caller list at a profile URL does NOT mean "allow all" (Codex docs(057): in-proxy profiles plan + tasks (Related #55) #621 finding 2)withProfileMeta(ctx, intentMap)injects `metadata["profile"]" at tool-call activity emit sites (FR-011)docs/features/profiles.mdTests
/mcp/p/<slug>is still filtered (profile check independent of AdminContext)Gates
go test ./internal/... -race✅./scripts/test-api-e2e.sh65/65 ✅./scripts/run-linter.sh0 issues ✅go build ./cmd/mcpproxy+go build -tags server ./cmd/mcpproxy✅Closes #55
Related #55