openmaskit 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. openmaskit-0.1.1/.claude/CLAUDE.md +35 -0
  2. openmaskit-0.1.1/.dockerignore +50 -0
  3. openmaskit-0.1.1/.github/workflows/publish.yml +158 -0
  4. openmaskit-0.1.1/.github/workflows/test.yml +31 -0
  5. openmaskit-0.1.1/.gitignore +35 -0
  6. openmaskit-0.1.1/.python-version +1 -0
  7. openmaskit-0.1.1/CLAUDE.md +261 -0
  8. openmaskit-0.1.1/CONTRIBUTING.md +72 -0
  9. openmaskit-0.1.1/Dockerfile +39 -0
  10. openmaskit-0.1.1/LICENSE +201 -0
  11. openmaskit-0.1.1/NOTICE +10 -0
  12. openmaskit-0.1.1/PKG-INFO +229 -0
  13. openmaskit-0.1.1/README.md +195 -0
  14. openmaskit-0.1.1/SECURITY.md +32 -0
  15. openmaskit-0.1.1/TRADEMARKS.md +70 -0
  16. openmaskit-0.1.1/assets/icon.png +0 -0
  17. openmaskit-0.1.1/pyproject.toml +61 -0
  18. openmaskit-0.1.1/src/openmaskit/__init__.py +19 -0
  19. openmaskit-0.1.1/src/openmaskit/__main__.py +617 -0
  20. openmaskit-0.1.1/src/openmaskit/backend_client.py +181 -0
  21. openmaskit-0.1.1/src/openmaskit/cli.py +112 -0
  22. openmaskit-0.1.1/src/openmaskit/config.py +126 -0
  23. openmaskit-0.1.1/src/openmaskit/container.py +285 -0
  24. openmaskit-0.1.1/src/openmaskit/logging_config.py +74 -0
  25. openmaskit-0.1.1/src/openmaskit/masking/__init__.py +0 -0
  26. openmaskit-0.1.1/src/openmaskit/masking/engine.py +536 -0
  27. openmaskit-0.1.1/src/openmaskit/masking/mappers.py +20 -0
  28. openmaskit-0.1.1/src/openmaskit/masking/parsing.py +51 -0
  29. openmaskit-0.1.1/src/openmaskit/masking/rules.py +103 -0
  30. openmaskit-0.1.1/src/openmaskit/masking/store.py +619 -0
  31. openmaskit-0.1.1/src/openmaskit/models.py +66 -0
  32. openmaskit-0.1.1/src/openmaskit/oauth/__init__.py +0 -0
  33. openmaskit-0.1.1/src/openmaskit/oauth/handler.py +431 -0
  34. openmaskit-0.1.1/src/openmaskit/proxy/__init__.py +0 -0
  35. openmaskit-0.1.1/src/openmaskit/proxy/core.py +574 -0
  36. openmaskit-0.1.1/src/openmaskit/proxy/http_downstream.py +159 -0
  37. openmaskit-0.1.1/src/openmaskit/proxy/manager.py +260 -0
  38. openmaskit-0.1.1/src/openmaskit/proxy/upstream.py +321 -0
  39. openmaskit-0.1.1/src/openmaskit/security.py +145 -0
  40. openmaskit-0.1.1/src/openmaskit/traffic/__init__.py +0 -0
  41. openmaskit-0.1.1/src/openmaskit/traffic/buffer.py +44 -0
  42. openmaskit-0.1.1/src/openmaskit/traffic/store.py +223 -0
  43. openmaskit-0.1.1/src/openmaskit/web/__init__.py +0 -0
  44. openmaskit-0.1.1/src/openmaskit/web/app.py +142 -0
  45. openmaskit-0.1.1/src/openmaskit/web/health.py +109 -0
  46. openmaskit-0.1.1/src/openmaskit/web/origin.py +110 -0
  47. openmaskit-0.1.1/src/openmaskit/web/routes/__init__.py +0 -0
  48. openmaskit-0.1.1/src/openmaskit/web/routes/custom_targets.py +280 -0
  49. openmaskit-0.1.1/src/openmaskit/web/routes/guardrails.py +160 -0
  50. openmaskit-0.1.1/src/openmaskit/web/routes/hidden_tools.py +40 -0
  51. openmaskit-0.1.1/src/openmaskit/web/routes/injections.py +142 -0
  52. openmaskit-0.1.1/src/openmaskit/web/routes/mappers.py +382 -0
  53. openmaskit-0.1.1/src/openmaskit/web/routes/marketplace.py +607 -0
  54. openmaskit-0.1.1/src/openmaskit/web/routes/oauth.py +162 -0
  55. openmaskit-0.1.1/src/openmaskit/web/routes/oauth_callback.py +191 -0
  56. openmaskit-0.1.1/src/openmaskit/web/routes/pages.py +158 -0
  57. openmaskit-0.1.1/src/openmaskit/web/routes/rules.py +124 -0
  58. openmaskit-0.1.1/src/openmaskit/web/routes/traffic.py +82 -0
  59. openmaskit-0.1.1/src/openmaskit/web/static/big.png +0 -0
  60. openmaskit-0.1.1/src/openmaskit/web/static/favicon.png +0 -0
  61. openmaskit-0.1.1/src/openmaskit/web/static/icon.png +0 -0
  62. openmaskit-0.1.1/src/openmaskit/web/static/marketplace.html +937 -0
  63. openmaskit-0.1.1/src/openmaskit/web/static/new_maskit-removebg-preview.png +0 -0
  64. openmaskit-0.1.1/src/openmaskit/web/static/onboarding.css +386 -0
  65. openmaskit-0.1.1/src/openmaskit/web/static/original_icon.png +0 -0
  66. openmaskit-0.1.1/src/openmaskit/web/static/shared.js +322 -0
  67. openmaskit-0.1.1/src/openmaskit/web/static/style.css +5036 -0
  68. openmaskit-0.1.1/src/openmaskit/web/static/targets.html +1174 -0
  69. openmaskit-0.1.1/src/openmaskit/web/static/tool_detail.html +936 -0
  70. openmaskit-0.1.1/src/openmaskit/web/static/tools.html +661 -0
  71. openmaskit-0.1.1/src/openmaskit/web/static/tutorial.css +377 -0
  72. openmaskit-0.1.1/src/openmaskit/web/static/tutorial.js +546 -0
  73. openmaskit-0.1.1/src/openmaskit/web/static/tutorials/guardrails.json +31 -0
  74. openmaskit-0.1.1/src/openmaskit/web/static/tutorials/hide-tool.json +16 -0
  75. openmaskit-0.1.1/src/openmaskit/web/static/tutorials/injections.json +31 -0
  76. openmaskit-0.1.1/src/openmaskit/web/static/tutorials/masking-with-result.json +31 -0
  77. openmaskit-0.1.1/src/openmaskit/web/static/tutorials/masking.json +16 -0
  78. openmaskit-0.1.1/tests/__init__.py +0 -0
  79. openmaskit-0.1.1/tests/test_backend_client.py +575 -0
  80. openmaskit-0.1.1/tests/test_cli.py +91 -0
  81. openmaskit-0.1.1/tests/test_config.py +255 -0
  82. openmaskit-0.1.1/tests/test_container.py +342 -0
  83. openmaskit-0.1.1/tests/test_custom_targets.py +343 -0
  84. openmaskit-0.1.1/tests/test_engine.py +442 -0
  85. openmaskit-0.1.1/tests/test_guardrails.py +189 -0
  86. openmaskit-0.1.1/tests/test_guardrails_routes.py +414 -0
  87. openmaskit-0.1.1/tests/test_health.py +137 -0
  88. openmaskit-0.1.1/tests/test_http_downstream.py +331 -0
  89. openmaskit-0.1.1/tests/test_http_downstream_no_leak.py +274 -0
  90. openmaskit-0.1.1/tests/test_injections.py +202 -0
  91. openmaskit-0.1.1/tests/test_injections_routes.py +459 -0
  92. openmaskit-0.1.1/tests/test_logging_config.py +82 -0
  93. openmaskit-0.1.1/tests/test_main.py +275 -0
  94. openmaskit-0.1.1/tests/test_mappers.py +319 -0
  95. openmaskit-0.1.1/tests/test_mappers_routes.py +546 -0
  96. openmaskit-0.1.1/tests/test_marketplace.py +469 -0
  97. openmaskit-0.1.1/tests/test_oauth.py +418 -0
  98. openmaskit-0.1.1/tests/test_oauth_state_cleanup.py +98 -0
  99. openmaskit-0.1.1/tests/test_origin_middleware.py +475 -0
  100. openmaskit-0.1.1/tests/test_parsing.py +84 -0
  101. openmaskit-0.1.1/tests/test_proxy.py +199 -0
  102. openmaskit-0.1.1/tests/test_proxy_manager.py +409 -0
  103. openmaskit-0.1.1/tests/test_proxy_upstream.py +689 -0
  104. openmaskit-0.1.1/tests/test_security.py +163 -0
  105. openmaskit-0.1.1/tests/test_shutdown.py +167 -0
  106. openmaskit-0.1.1/tests/test_store.py +400 -0
  107. openmaskit-0.1.1/tests/test_target_manager.py +362 -0
  108. openmaskit-0.1.1/tests/test_traffic_buffer.py +95 -0
  109. openmaskit-0.1.1/tests/test_traffic_routes.py +209 -0
  110. openmaskit-0.1.1/tests/test_traffic_store.py +179 -0
  111. openmaskit-0.1.1/tests/test_ui_tutorials.py +87 -0
  112. openmaskit-0.1.1/tests/test_upstream_container_lifecycle.py +411 -0
  113. openmaskit-0.1.1/tests/test_web_routes.py +302 -0
  114. openmaskit-0.1.1/uv.lock +1514 -0
@@ -0,0 +1,35 @@
1
+ <!-- CODEGRAPH_START -->
2
+ ## CodeGraph
3
+
4
+ This project has a CodeGraph MCP server (`codegraph_*` tools) configured. CodeGraph is a tree-sitter-parsed knowledge graph of every symbol, edge, and file. Reads are sub-millisecond and return structural information grep cannot.
5
+
6
+ ### When to prefer codegraph over native search
7
+
8
+ Use codegraph for **structural** questions — what calls what, what would break, where is X defined, what is X's signature. Use native grep/read only for **literal text** queries (string contents, comments, log messages) or after you already have a specific file open.
9
+
10
+ | Question | Tool |
11
+ |---|---|
12
+ | "Where is X defined?" / "Find symbol named X" | `codegraph_search` |
13
+ | "What calls function Y?" | `codegraph_callers` |
14
+ | "What does Y call?" | `codegraph_callees` |
15
+ | "How does X reach/become Y? / trace the flow from X to Y" | `codegraph_trace` (one call = the whole path, incl. callback/React/JSX dynamic hops) |
16
+ | "What would break if I changed Z?" | `codegraph_impact` |
17
+ | "Show me Y's signature / source / docstring" | `codegraph_node` |
18
+ | "Give me focused context for a task/area" | `codegraph_context` |
19
+ | "See several related symbols' source at once" | `codegraph_explore` |
20
+ | "What files exist under path/" | `codegraph_files` |
21
+ | "Is the index healthy?" | `codegraph_status` |
22
+
23
+ ### Rules of thumb
24
+
25
+ - **Answer directly — don't delegate exploration.** For "how does X work" / architecture questions, answer with 2-3 codegraph calls: `codegraph_context` first, then ONE `codegraph_explore` for the source of the symbols it surfaces. For a specific **flow** ("how does X reach Y") start with `codegraph_trace` from→to — one call returns the whole path with dynamic hops bridged — then ONE `codegraph_explore` for the bodies; don't rebuild the path with `codegraph_search` + `codegraph_callers`. Codegraph IS the pre-built index, so spawning a separate file-reading sub-task/agent — or running a grep + read loop — repeats work codegraph already did and costs more for the same answer.
26
+ - **Trust codegraph results.** They come from a full AST parse. Do NOT re-verify them with grep — that's slower, less accurate, and wastes context.
27
+ - **Don't grep first** when looking up a symbol by name. `codegraph_search` is faster and returns kind + location + signature in one call.
28
+ - **Don't chain `codegraph_search` + `codegraph_node`** when you just want context — `codegraph_context` is one call.
29
+ - **Don't loop `codegraph_node` over many symbols** — one `codegraph_explore` call returns several symbols' source grouped in a single capped call, while each separate node/Read call re-reads the whole context and costs far more.
30
+ - **Index lag — check the staleness banner, don't guess a wait.** When a codegraph response starts with "⚠️ Some files referenced below were edited since the last index sync…", the listed files are pending re-index — Read those specific files for accurate content. Files NOT in that banner are fresh and codegraph is authoritative for them. `codegraph_status` also lists pending files under "Pending sync".
31
+
32
+ ### If `.codegraph/` doesn't exist
33
+
34
+ The MCP server returns "not initialized." Ask the user: *"I notice this project doesn't have CodeGraph initialized. Want me to run `codegraph init -i` to build the index?"*
35
+ <!-- CODEGRAPH_END -->
@@ -0,0 +1,50 @@
1
+ # Version control
2
+ .git
3
+ .gitignore
4
+
5
+ # Python virtual environment
6
+ .venv
7
+ venv
8
+ env
9
+ *.egg-info
10
+
11
+ # Python cache
12
+ __pycache__
13
+ *.pyc
14
+ *.pyo
15
+ *.pyd
16
+ .Python
17
+
18
+ # Testing
19
+ tests
20
+ .pytest_cache
21
+ .coverage
22
+ htmlcov
23
+ .tox
24
+
25
+ # IDE / Editor
26
+ .vscode
27
+ .idea
28
+ *.swp
29
+ *.swo
30
+ *~
31
+ .DS_Store
32
+
33
+ # Claude Code
34
+ .claude
35
+
36
+ # CI/CD
37
+ .github
38
+
39
+ # Documentation (not needed in image)
40
+ CONTRIBUTING.md
41
+
42
+ # Misc
43
+ *.log
44
+ *.db
45
+ *.sqlite
46
+ .env
47
+ .env.*
48
+
49
+ .codegraph
50
+ bench_unmask.py
@@ -0,0 +1,158 @@
1
+ # Two-step publish: tag pushes → TestPyPI; manual dispatch against the same tag → PyPI.
2
+ #
3
+ # Flow:
4
+ # 1. `git tag v0.1.1 && git push origin v0.1.1`
5
+ # → runs tests, builds wheel, publishes to TestPyPI.
6
+ # 2. Smoke-test the TestPyPI release (`uvx --from openmaskit==0.1.1 ...`).
7
+ # 3. Actions UI → "Publish" → "Run workflow" against ref `v0.1.1`
8
+ # → runs tests, rebuilds wheel, publishes to PyPI.
9
+ #
10
+ # Splitting the PyPI half off the tag trigger gives us the same gate that a
11
+ # "required reviewer on the pypi environment" would on a public repo —
12
+ # without needing GitHub Pro/Team on a private repo.
13
+ #
14
+ # One-time setup before this workflow will succeed (does not need to be done
15
+ # again until you change repo/workflow/env names):
16
+ #
17
+ # 1. TestPyPI Trusted Publisher
18
+ # https://test.pypi.org/manage/account/publishing/ → Add a new pending publisher
19
+ # Owner: MaskitMCP
20
+ # Repository: openmaskit
21
+ # Workflow: publish.yml
22
+ # Environment: testpypi
23
+ #
24
+ # 2. PyPI Trusted Publisher
25
+ # https://pypi.org/manage/account/publishing/ → Add a new pending publisher
26
+ # Owner: MaskitMCP
27
+ # Repository: openmaskit
28
+ # Workflow: publish.yml
29
+ # Environment: pypi
30
+ #
31
+ # 3. GitHub Environments (Settings → Environments)
32
+ # Create two environments named `testpypi` and `pypi`. The `pypi`
33
+ # environment should have a deployment tag policy restricting it to
34
+ # `v*` tags so only tagged commits can deploy.
35
+ #
36
+ # Until step 1-3 are done, this workflow will fail at the publish step with an
37
+ # OIDC error. That's expected and harmless.
38
+
39
+ name: Publish
40
+
41
+ on:
42
+ push:
43
+ tags: ['v*']
44
+ workflow_dispatch:
45
+
46
+ run-name: Publish ${{ github.ref_name }}
47
+
48
+ concurrency:
49
+ group: publish-${{ github.ref }}
50
+ cancel-in-progress: false
51
+
52
+ jobs:
53
+ test:
54
+ name: Run tests
55
+ runs-on: ubuntu-latest
56
+ steps:
57
+ - uses: actions/checkout@v4
58
+
59
+ - name: Install uv
60
+ uses: astral-sh/setup-uv@v4
61
+ with:
62
+ enable-cache: true
63
+
64
+ - name: Set up Python
65
+ run: uv python install 3.12
66
+
67
+ - name: Install dependencies
68
+ run: uv sync --frozen
69
+
70
+ - name: Run tests
71
+ run: uv run pytest tests/ -v
72
+
73
+ build:
74
+ name: Build sdist + wheel
75
+ needs: test
76
+ runs-on: ubuntu-latest
77
+ steps:
78
+ - uses: actions/checkout@v4
79
+
80
+ - name: Install uv
81
+ uses: astral-sh/setup-uv@v4
82
+ with:
83
+ enable-cache: true
84
+
85
+ - name: Set up Python
86
+ run: uv python install 3.12
87
+
88
+ - name: Verify tag matches pyproject.toml version
89
+ run: |
90
+ tag="${GITHUB_REF_NAME#v}"
91
+ version=$(uv run python -c "import tomllib, pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
92
+ if [ "$tag" != "$version" ]; then
93
+ echo "::error::Tag v$tag does not match pyproject.toml version $version. Bump pyproject.toml or retag."
94
+ exit 1
95
+ fi
96
+ echo "Tag and pyproject.toml agree on version: $version"
97
+
98
+ - name: Build
99
+ run: uv build
100
+
101
+ - name: Validate distributions with twine
102
+ run: uvx twine check dist/*
103
+
104
+ - name: Upload dist artifact
105
+ uses: actions/upload-artifact@v4
106
+ with:
107
+ name: dist
108
+ path: dist/
109
+ retention-days: 7
110
+
111
+ publish-testpypi:
112
+ name: Publish to TestPyPI
113
+ needs: build
114
+ if: github.event_name == 'push'
115
+ runs-on: ubuntu-latest
116
+ environment:
117
+ name: testpypi
118
+ url: https://test.pypi.org/project/openmaskit/
119
+ permissions:
120
+ id-token: write # required for OIDC trusted publishing
121
+ steps:
122
+ - name: Download dist artifact
123
+ uses: actions/download-artifact@v4
124
+ with:
125
+ name: dist
126
+ path: dist/
127
+
128
+ - name: Publish
129
+ uses: pypa/gh-action-pypi-publish@release/v1
130
+ with:
131
+ repository-url: https://test.pypi.org/legacy/
132
+
133
+ publish-pypi:
134
+ name: Publish to PyPI
135
+ needs: build
136
+ if: github.event_name == 'workflow_dispatch'
137
+ runs-on: ubuntu-latest
138
+ environment:
139
+ name: pypi
140
+ url: https://pypi.org/project/openmaskit/
141
+ permissions:
142
+ id-token: write
143
+ steps:
144
+ - name: Refuse non-tag refs
145
+ run: |
146
+ if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
147
+ echo "::error::PyPI publish must be dispatched against a v* tag, got ${GITHUB_REF}."
148
+ exit 1
149
+ fi
150
+
151
+ - name: Download dist artifact
152
+ uses: actions/download-artifact@v4
153
+ with:
154
+ name: dist
155
+ path: dist/
156
+
157
+ - name: Publish
158
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,31 @@
1
+ name: Tests
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ push:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: test-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v4
21
+ with:
22
+ enable-cache: true
23
+
24
+ - name: Set up Python
25
+ run: uv python install 3.12
26
+
27
+ - name: Install dependencies
28
+ run: uv sync --frozen
29
+
30
+ - name: Run tests
31
+ run: uv run pytest tests/ -v
@@ -0,0 +1,35 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ test-venv/
12
+
13
+ # Test / coverage artifacts
14
+ .coverage
15
+ coverage.json
16
+ htmlcov/
17
+ .pytest_cache/
18
+
19
+ # Mac related files
20
+ .DS_Store
21
+ */.DS_Store
22
+
23
+ # Local OpenMaskit config (root-level only)
24
+ /openmaskit*.yaml
25
+
26
+ # Jetbrains related files
27
+ .idea
28
+ .bsp
29
+
30
+ # Local tooling config (not for the public repo)
31
+ .claude/settings.json
32
+ .mcp.json
33
+
34
+ .codegraph
35
+ bench_unmask.py
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,261 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What is OpenMaskit
6
+
7
+ OpenMaskit is an MCP (Model Context Protocol) server proxy that sits between an AI host (e.g., Claude Code) and a real MCP server. It intercepts tool call responses to mask sensitive field values (replacing `prod-db.internal.net` with `host_1`) and unmasks them when the agent sends those aliases back in tool call arguments.
8
+
9
+ ## Deployment model (important for security reasoning)
10
+
11
+ OpenMaskit runs **locally on the user's own machine** — like a CLI dev tool (Docker Desktop, Jupyter, a local DB client). It is **not a hosted service**, the Python backend is **not deployed anywhere**, and the FE↔Python channel is a localhost link on the same machine. So:
12
+
13
+ - **Localhost-only auth on the Web UI / API / MCP endpoint is not required.** The user already owns the machine.
14
+ - **Running arbitrary subprocesses (stdio targets, `docker run ...` from the marketplace) is not RCE in any meaningful sense.** OpenMaskit is a UI for commands the user would otherwise type into their own terminal — it confers no privilege the user doesn't already have.
15
+ - **Multi-user shared-machine threats (other local users reading token files, etc.) are out of scope** unless explicitly raised.
16
+
17
+ The threats that **do** still matter, even for a local tool:
18
+
19
+ - **Browser-based cross-origin attacks against localhost.** A malicious webpage the user visits can `fetch()`/`WebSocket` against `127.0.0.1:9473`/`9474`/`3131` and exfiltrate secrets. This is the canonical "localhost service" attack class (cf. the Docker daemon, ethdev wallets, etc.). CSRF tokens, `Origin` header checks on POST and WS, and not echoing the alias map / unmasked previews to API callers are all still required.
20
+ - **OAuth callback integrity** — the OAuth flow physically goes through the browser, so `state` validation and code-injection defenses still apply.
21
+ - **Malicious upstream MCP server** — OpenMaskit talks to third-party MCP servers; their responses must not be able to crash the proxy, ReDoS the masking engine, or poison persistent state.
22
+ - **Correctness bugs** (mask/unmask collisions, races, leaks) — same as any other software.
23
+
24
+ When reviewing security findings, classify by whether the attacker is (a) the local user themselves [out of scope], (b) a malicious upstream MCP server [in scope], or (c) a webpage in the user's browser / a remote OAuth peer [in scope].
25
+
26
+ ## Commands
27
+
28
+ ```bash
29
+ uv sync # Install dependencies
30
+ uv run pytest tests/ -v # Run all tests
31
+ uv run pytest tests/test_engine.py::TestMaskingEngine::test_mask_structured_content -v # Single test
32
+ uv run openmaskit # Run with ./openmaskit.yaml
33
+ uv run openmaskit path/to/config.yaml # Run with custom config
34
+ ```
35
+
36
+ ## Naming
37
+
38
+ The UI says "Servers" but the codebase uses "target" everywhere (classes, DB columns, API routes, variables). They mean the same thing — an upstream MCP server that OpenMaskit proxies to. "Server" is the user-facing term; "target" is the internal term. Do not rename internal code.
39
+
40
+ ## Architecture
41
+
42
+ The system has four concurrent components running in one asyncio event loop (via anyio task groups):
43
+
44
+ 1. **Proxy Core** (`__main__.py` + `proxy/core.py`) — Bidirectional JSON-RPC relay between downstream clients and upstream MCP server. Operates at the raw `JSONRPCMessage` level for full protocol transparency — all non-tool messages pass through unmodified. Bootstraps the upstream session (initialize + tools/list) at startup.
45
+
46
+ 2. **MCP HTTP Endpoint** (`proxy/http_downstream.py`, Starlette on port 9474) — HTTP MCP endpoint that AI agents connect to. Implements the MCP streamable HTTP transport (POST /mcp). Uses `ResponseDispatcher` to correlate requests with responses through the proxy relay.
47
+
48
+ 3. **Masking Engine** (`masking/engine.py`) — Synchronous mask/unmask using an in-memory cache. Aliases are created in-memory for speed (`_alias_cache`, `_reverse_cache`) and flushed to SQLite periodically by `_flush_loop`. The engine handles both `structuredContent` dicts (path-based masking) and `TextContent` blocks (JSON/Python-repr-parse-then-mask, fallback to string replacement). Supports two mapper types: `regex_replace` (regex pattern matching on text) and `json_field_mask` (dot-notation path targeting specific fields in parsed JSON/repr).
49
+
50
+ 4. **Web UI** (`web/app.py`, Starlette on port 9473) — Dashboard for viewing tool schemas, managing masking rules, trying out tools, and inspecting the traffic audit log (lazy-loaded, paginated).
51
+
52
+ ### Key data flow
53
+
54
+ ```
55
+ AI Agent HTTP (:9474/mcp) ─┐
56
+ ├──► ds_read stream
57
+ AI Host stdin ─────────────┘ ↓
58
+ _intercept_request (unmask aliases in arguments)
59
+
60
+ upstream MCP server
61
+
62
+ _intercept_response (mask values, cache tool schemas)
63
+
64
+ ResponseDispatcher (routes to HTTP waiters)
65
+ ↓ (if no waiter)
66
+ ds_write stream → _stdout_writer → AI Host stdout
67
+ ```
68
+
69
+ ### Important: stdout is sacred
70
+
71
+ The MCP stdio protocol uses stdout exclusively. All logging goes to stderr. The Web UI must never write to stdout.
72
+
73
+ ### ResponseDispatcher pattern
74
+
75
+ HTTP clients (MCP endpoint and web UI "Try it out") register a waiter by request ID before injecting a message into the proxy relay. When the response arrives from upstream, `_relay_upstream_to_downstream` checks the dispatcher first — if a waiter exists, it routes the response directly to the waiter instead of sending it to stdout.
76
+
77
+ ### Masking engine's dual-layer caching
78
+
79
+ `MaskingEngine.mask_response()` is synchronous (called from the proxy relay hot path). It writes new aliases to `_pending_writes` which are batch-flushed to SQLite every second by `_flush_loop` in `__main__.py`. This avoids blocking the relay on DB I/O.
80
+
81
+ ### Traffic audit log (`traffic/`)
82
+
83
+ Tool-call records are persisted to a **separate** SQLite database (`~/.openmaskit/traffic.db`, configurable via `OPENMASKIT_TRAFFIC_DB_PATH`) so rotation/vacuum is isolated and a corrupt traffic DB can't kill masking config.
84
+
85
+ - **`TrafficStore` (`traffic/store.py`)** — async aiosqlite wrapper. Opens with `PRAGMA journal_mode=WAL` and `PRAGMA synchronous=NORMAL` for low-latency batched writes (WAL is critical here — the flush loop writes every 1s and rotation deletes are concurrent with reads from the GET endpoint). Unmasked args + unmasked response columns are encrypted at rest using the shared Fernet key from `security.TokenEncryption`. Masked args/response columns are plaintext (safe to read without the key).
86
+ - **`TrafficBuffer` (`traffic/buffer.py`)** — process-wide in-memory queue. `_intercept_response` (and the two block paths in `_intercept_request`) call `target.traffic_buffer.append(...)` synchronously on **terminal state only** — there are no pending/in-flight rows. `_traffic_flush_loop` in `__main__.py` drains the buffer to the store every 1s. This mirrors the `MaskingEngine._pending_writes` + `_flush_loop` idiom.
87
+ - **Rotation** — `_traffic_rotation_loop` enforces a global row cap (`OPENMASKIT_TRAFFIC_MAX_ROWS`, default 10000) every 5 minutes by deleting the oldest rows beyond the cap.
88
+ - **Lazy UI** — there is no WebSocket stream. The dashboard fetches pages on demand via `GET /api/targets/{target_name}/traffic?limit=&before=<id>`. The endpoint flushes the buffer before reading so the response reflects the latest writes.
89
+ - **Status values** — `ok`, `error`, `blocked`. Blocked entries (hidden tool or guardrail violation) record the unmasked args (encrypted) and put the block reason into `masked_response`.
90
+
91
+ ### Hidden tools
92
+
93
+ Tools can be hidden per-server via the Web UI. Hidden tools are stored in the `hidden_tools` SQLite table and loaded into `TargetState.hidden_tools` at startup. When an agent calls a hidden tool, the proxy returns a `METHOD_NOT_FOUND` error without forwarding to upstream.
94
+
95
+ ### Request interception pipeline
96
+
97
+ In `_intercept_request()`, tool calls pass through these stages in order:
98
+ 1. **Hidden tool check** — blocks with `METHOD_NOT_FOUND` error
99
+ 2. **Unmask arguments** — replaces aliases with real values
100
+ 3. **Guardrail check** — validates unmasked args against patterns, blocks with `-32602` error if violated
101
+ 4. **Injection application** — injects/overrides argument values before forwarding
102
+
103
+ ### Argument guardrails
104
+
105
+ Block tool calls whose arguments match dangerous patterns. Stored in `guardrails` table, loaded into `MaskingEngine._guardrails`. Support three match types: `contains`, `equals`, `regex`. When `argument_name="*"`, scans all string values recursively.
106
+
107
+ ### Argument injections
108
+
109
+ Silently inject or override argument values before forwarding. Stored in `injections` table, loaded into `MaskingEngine._injections`. Three modes: `set` (always override), `default` (only if absent), `append` (append to string/list). Values are JSON-encoded strings.
110
+
111
+ ### Field stripping
112
+
113
+ Rules with `action="strip"` remove fields entirely from responses (no alias created, field is gone). Only applies to structured data (parsed JSON/repr); plain text blocks skip strip rules.
114
+
115
+ ### Text parsing (`masking/parsing.py`)
116
+
117
+ The `try_parse_structured` utility attempts JSON first, then falls back to `ast.literal_eval` for Python repr strings (common in some MCP tool responses). Results are serialized back in their original format after masking.
118
+
119
+ ### Persistence
120
+
121
+ Two SQLite databases:
122
+
123
+ **`~/.openmaskit/store.db`** (masking config + state):
124
+ - `mappings` — alias ↔ real_value (persists across restarts so the same real value always gets the same alias)
125
+ - `rules` — masking rules created via Web UI (merged with config-file rules at startup), supports `action` column (`mask` or `strip`)
126
+ - `response_mappers` — output mapper configs (regex or json_field_mask) with optional `config` JSON column
127
+ - `hidden_tools` — tools hidden per server (blocked from agent access)
128
+ - `guardrails` — argument validation rules that block tool calls matching dangerous patterns
129
+ - `injections` — argument injection rules that inject/override values before forwarding
130
+ - `mcp_servers` — marketplace and custom servers (id, name, config JSON, active flag). Used for both marketplace installs and custom targets added via the UI
131
+
132
+ **`~/.openmaskit/traffic.db`** (audit log, separate file by design):
133
+ - `traffic` — one row per terminal-state tool call. Columns: `id`, `ts`, `target_name`, `tool_name`, `request_id`, `status`, `duration_ms`, `args_enc` (BLOB, Fernet), `response_enc` (BLOB, Fernet), `masked_args` (TEXT), `masked_resp` (TEXT). Indexed on `(target_name, id DESC)`.
134
+
135
+ ### Marketplace
136
+
137
+ The marketplace catalog is **fetched from a remote backend**, not a local file. There is **no `marketplace.json`** in the repo — entries live on `api.maskitmcp.com` and are paged in over HTTP. Installed servers are persisted in the `mcp_servers` SQLite table and connected at runtime via `TargetManager`.
138
+
139
+ - `backend_client.py` is the HTTP client. It targets `OPENMASKIT_MARKETPLACE_API_URL` (default `https://api.maskitmcp.com`) for catalog reads and `OPENMASKIT_AUTH_BACKEND_URL` (default `https://auth.maskitmcp.com`) for OAuth brokering.
140
+ - Catalog endpoint: `GET {marketplace_url}/api/marketplace/catalog?page=&size=&q=`. Returns `{data: [...], meta: {...}}`. Fail-open: failures return an empty page so the UI still renders.
141
+ - On install, the user provides any required env vars / credentials via a modal; config is saved to `mcp_servers` and the server is hot-connected via `TargetManager`.
142
+ - Servers can be deactivated (disconnected but config retained) and reactivated without re-entering credentials.
143
+ - Active marketplace servers are automatically reconnected on startup (`__main__.py` loads them from DB).
144
+
145
+ #### OAuth install modes
146
+
147
+ Catalog entries carry an `oauth_mode` field that determines the install flow. Three modes:
148
+
149
+ | `oauth_mode` | Flow | Redirect URI |
150
+ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
151
+ | `null` (hosted) | Hosted broker via `auth.maskitmcp.com`. `BackendClient.get_oauth_authorize_url` builds the URL; the broker handles code exchange; tokens land back over HTTPS. | `http://localhost:9473/oauth/callback/{handle}` |
152
+ | `"byo"` | Bring-your-own OAuth client. User pastes `client_id` / `client_secret` in the install modal; OpenMaskit runs the OAuth flow directly against the provider. | `http://localhost:3131/callback` |
153
+ | `"dcr"` | Dynamic Client Registration. OpenMaskit fetches the provider's well-known doc, registers a client at install time, then runs the OAuth flow against the provider. | `http://localhost:3131/callback` |
154
+
155
+ For BYO entries the catalog provides `meta.available_scopes` (`[{scope, label, required, default}]`) which the modal renders as a checklist; required scopes are locked-checked. For DCR entries scopes are discovered live from the well-known doc via `/api/oauth/discover` — catalog doesn't need to ship them. Any catalog entry — BYO, DCR, or a plain env-var stdio install — can ship `meta.setup_guide_url`; the install modal renders a "Setup guide ↗" link inline with the credentials/env-var prompt.
156
+
157
+ BYO and DCR installs both build a `transport: "http"` config with an `oauth` block (same shape as custom targets) and call `manager.add_target`. The existing `oauth/handler.py:create_oauth_provider` already handles both modes — BYO uses its manual branch (pre-seeds `client_info` from the config), DCR uses its discovery + DCR branch. Either way the local `OAuthCallbackServer` on port 3131 (`config.oauth_port`) receives the callback. **No new code paths in `oauth/handler.py` are needed for marketplace BYO/DCR — the install handler just shapes the config dict and the existing OAuth provider does the rest.**
158
+
159
+ Hosted-broker installs are tagged in storage with the placeholder `config.oauth.client_id == "managed-by-backend"`; this is how `marketplace_reauthorize` distinguishes them from BYO/DCR. Hosted entries also preserve `config.backend_id` so reauthorize can ask the broker for a fresh authorize URL.
160
+
161
+ #### Re-authorize
162
+
163
+ `POST /api/marketplace/{target_id}/reauthorize` triggers a fresh OAuth flow for an installed server. The Re-authorize button on each server card (`targets.html`) calls it.
164
+
165
+ - **BYO / DCR**: drops the `tokens` key from the encrypted `{store_dir}/oauth/{handle}.json` (preserves `client_info` so we don't re-prompt for creds or re-run DCR), `remove_target` + `add_target`, the browser-popup OAuth flow runs, returns `{connected: true}` once tokens are written. The token file is updated by `FileTokenStorage` from `oauth/handler.py`.
166
+ - **Hosted broker**: re-runs `BackendClient.get_oauth_authorize_url` and returns `{oauth_url}` for the UI to redirect to. The callback then re-exchanges via `oauth_callback.py` as on first install.
167
+
168
+ ### Custom targets (runtime)
169
+
170
+ Users can add arbitrary MCP servers via the dashboard (Servers page → "Add Server"). These are also stored in the `mcp_servers` SQLite table and managed by `TargetManager`. Same hot-add/remove lifecycle as marketplace servers.
171
+
172
+ ### TargetManager (`proxy/manager.py`)
173
+
174
+ Handles hot-adding and removing MCP server targets at runtime. Holds references to the shared task group and exit stack so it can spawn proxy loops and connect upstream without restarting. Called by both marketplace and custom target API routes.
175
+
176
+ ### OAuth handler (`oauth/handler.py`)
177
+
178
+ Shared OAuth 2.1 callback server running on port 3131. Started once in `__main__.py` and shared across all targets. OAuth tokens are stored per-server at `{store_dir}/oauth/{server_id}.json`, Fernet-encrypted.
179
+
180
+ Two OAuth flow shapes go through this callback:
181
+
182
+ - **Marketplace servers (hosted broker).** OpenMaskit redirects the browser to `auth.maskitmcp.com`, which redirects to the provider, handles the code exchange server-side, and bounces back to the local callback with tokens. The local code never sees the provider's client_secret. See `BackendClient.get_oauth_authorize_url` / `exchange_code` / `refresh_oauth_token`.
183
+ - **Custom HTTP targets (DCR or direct).** The local handler runs the flow against the upstream provider directly using either Dynamic Client Registration or user-supplied credentials.
184
+
185
+ When adding BYO-credential or new DCR paths, the catalog entry signals the flow via an `oauth_mode` field (`"byo" | "dcr" | null`); the absence of the field implies the hosted-broker default.
186
+
187
+ ### Bind host
188
+
189
+ All servers (web, MCP, OAuth callback) bind to the address in `OPENMASKIT_HOST` env var (default `127.0.0.1`). The Dockerfile sets this to `0.0.0.0` so the container is accessible from the host.
190
+
191
+ ## Configuration
192
+
193
+ `openmaskit.yaml` at project root. Upstream supports `stdio` transport (spawns child process) and `http` transport (connects to remote MCP server with optional OAuth 2.1). If no config file exists, OpenMaskit starts with no pre-configured targets (marketplace/custom targets can still be added via UI).
194
+
195
+ ## Web UI
196
+
197
+ ### Pages
198
+
199
+ - `/` — Servers page: lists all connected targets (config, marketplace, custom), add/remove custom servers
200
+ - `/marketplace` — Browse and install servers from the catalog
201
+ - `/targets/{name}/tools` — Tool list for a specific server, connect agent button
202
+ - `/targets/{name}/tools/{tool}` — Tool detail: schema, try it out, masking rules, mappers, guardrails, injections
203
+
204
+ ### API routes
205
+
206
+ All target-scoped routes: `/api/targets/{target_name}/...`
207
+
208
+ - `GET /api/targets/{target_name}/tools` — cached tool schemas
209
+ - `POST /api/targets/{target_name}/tools/call` — invoke a tool through the proxy (used by "Try it out")
210
+ - `GET/POST/PUT/DELETE /api/targets/{target_name}/rules` — masking rule CRUD (supports `action: "mask"|"strip"`)
211
+ - `GET/POST/PUT/DELETE /api/targets/{target_name}/mappers` — response mapper CRUD
212
+ - `GET/POST/PUT/DELETE /api/targets/{target_name}/guardrails` — argument guardrail CRUD
213
+ - `GET/POST/PUT/DELETE /api/targets/{target_name}/injections` — argument injection CRUD
214
+ - `GET /api/targets/{target_name}/mappings` — current alias mappings
215
+ - `GET/POST /api/targets/{target_name}/hidden_tools` — hide/unhide tools from the agent
216
+ - `GET /api/targets/{target_name}/traffic?limit=&before=<id>` — paginated audit log (cursor pagination; newest first; flushes pending buffer before reading)
217
+
218
+ Marketplace routes:
219
+
220
+ - `GET /api/marketplace` — catalog with install/active status
221
+ - `POST /api/marketplace/install` — install a server from catalog
222
+ - `POST /api/marketplace/activate` — reactivate a previously installed server
223
+ - `POST /api/marketplace/deactivate` — disconnect and deactivate
224
+ - `POST /api/marketplace/{target_id}/reauthorize` — kick off a fresh OAuth flow for an installed server (BYO/DCR clears tokens and runs the flow inline; hosted-broker returns a fresh `oauth_url`)
225
+
226
+ Custom target routes:
227
+
228
+ - `GET /api/custom-targets` — list custom targets
229
+ - `POST /api/custom-targets` — add a new custom target
230
+ - `POST /api/custom-targets/{target_id}/activate` — activate a deactivated custom target
231
+ - `POST /api/custom-targets/{target_id}/deactivate` — deactivate a custom target (keeps config)
232
+ - `POST /api/custom-targets/{target_id}/delete` — permanently remove a custom target
233
+
234
+ Server list routes:
235
+
236
+ - `GET /api/targets` — list all servers (active AND inactive) with runtime state merged from database
237
+
238
+ ### Server lifecycle states
239
+
240
+ Servers can be in three states:
241
+ 1. **Active** — Connected and running, appears in "Active Servers" section
242
+ 2. **Inactive** — Disconnected but config retained in database, appears in "Inactive Servers" section, can be reactivated
243
+ 3. **Deleted** — Permanently removed from database (custom servers only)
244
+
245
+ The Servers page (`/`) shows both active and inactive servers in separate sections. Users can:
246
+ - **Deactivate** any server (marketplace or custom) to temporarily disconnect it
247
+ - **Activate** any inactive server to reconnect using stored configuration
248
+ - **Delete** custom servers permanently (marketplace servers can only be deactivated)
249
+ - **View details** of inactive servers to see their configuration
250
+
251
+ ### Container runtime compatibility
252
+
253
+ OpenMaskit auto-detects container runtimes (Docker, Podman, nerdctl, Finch) for containerized MCP servers:
254
+
255
+ - Detection happens at startup (`container.py` module)
256
+ - Commands starting with `docker` are automatically substituted with detected runtime
257
+ - Optional override via `container_runtime` config field in `openmaskit.yaml`
258
+ - Example: `docker run mcp-server` → `podman run mcp-server` (if Podman is detected)
259
+ - Logs show detected/configured runtime at startup
260
+
261
+ This makes containerized marketplace servers work across different environments without user intervention.
@@ -0,0 +1,72 @@
1
+ # Contributing to OpenMaskit
2
+
3
+ Thanks for your interest in contributing! OpenMaskit is in early development and contributions of all kinds are welcome — bug reports, fixes, features, docs, and marketplace catalog entries.
4
+
5
+ ## Reporting issues
6
+
7
+ 1. Check [existing issues](https://github.com/OpenMaskitMCP/openmaskit/issues) to avoid duplicates.
8
+ 2. Open a new issue with:
9
+ - A clear title and description
10
+ - Steps to reproduce (for bugs)
11
+ - Expected vs. actual behavior
12
+ - Relevant logs, screenshots, or config
13
+
14
+ ## Development setup
15
+
16
+ ```bash
17
+ git clone https://github.com/OpenMaskitMCP/openmaskit.git
18
+ cd openmaskit
19
+ uv sync
20
+ ```
21
+
22
+ Run OpenMaskit locally:
23
+
24
+ ```bash
25
+ uv run openmaskit # uses ./openmaskit.yaml if present
26
+ uv run openmaskit path/to/config.yaml
27
+ ```
28
+
29
+ Then open the dashboard at `http://127.0.0.1:9473`.
30
+
31
+ ## Testing
32
+
33
+ ```bash
34
+ uv run pytest tests/ -v # all tests
35
+ uv run pytest tests/test_engine.py -v # one module
36
+ uv run pytest tests/test_engine.py::TestMaskingEngine::test_mask_structured_content -v # one test
37
+ ```
38
+
39
+ New features and bug fixes should come with tests.
40
+
41
+ ## Submitting a change
42
+
43
+ 1. **Fork and branch**: `git checkout -b feature/your-feature-name`
44
+ 2. **Code** — follow the conventions of the surrounding files. Python 3.12+, type hints where they clarify intent, docstrings on public APIs. Keep functions focused.
45
+ 3. **Test** — run the suite above. Add cases for what you changed.
46
+ 4. **Commit** with a clear message describing the *why*, not just the *what*.
47
+ 5. **Push** and open a pull request. Reference any related issues.
48
+
49
+ ### Pull request guidelines
50
+
51
+ - One feature or fix per PR.
52
+ - Update docs (`README.md`, `CLAUDE.md`) if your change affects how OpenMaskit is used or how it's structured.
53
+ - Be responsive to review feedback.
54
+ - For large changes, open an issue first to align on direction before writing code.
55
+
56
+ ## Areas where help is wanted
57
+
58
+ - **Test coverage** — integration tests, fuzzing, concurrency stress tests
59
+ - **Documentation** — examples, tutorials, architecture diagrams
60
+ - **Edge cases** — binary data, large payloads, streaming responses
61
+ - **Security review** — threat modeling, timing attack analysis
62
+ - **Observability** — metrics, structured logging improvements
63
+ - **Marketplace** — more pre-configured MCP servers in the catalog
64
+ - **Bug fixes** — see the issue tracker
65
+
66
+ ## License
67
+
68
+ By contributing, you agree that your contributions are licensed under the Apache License, Version 2.0 (see [LICENSE](LICENSE)). This includes the patent grant in §3 of that license.
69
+
70
+ ## Questions?
71
+
72
+ Open an issue or start a discussion in the repo — we're happy to help.