@orygn/opa-mcp 0.2.1 → 0.4.0
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.
- package/CHANGELOG.md +91 -3
- package/README.md +67 -31
- package/dist/config.d.ts +14 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/lib/child-env.d.ts +34 -0
- package/dist/lib/child-env.d.ts.map +1 -0
- package/dist/lib/child-env.js +144 -0
- package/dist/lib/child-env.js.map +1 -0
- package/dist/lib/conftest-cli.d.ts.map +1 -1
- package/dist/lib/conftest-cli.js +1 -0
- package/dist/lib/conftest-cli.js.map +1 -1
- package/dist/lib/opa-cli.d.ts.map +1 -1
- package/dist/lib/opa-cli.js +1 -0
- package/dist/lib/opa-cli.js.map +1 -1
- package/dist/lib/regal-cli.d.ts.map +1 -1
- package/dist/lib/regal-cli.js +1 -0
- package/dist/lib/regal-cli.js.map +1 -1
- package/dist/lib/rego-smt-encoder.d.ts.map +1 -1
- package/dist/lib/rego-smt-encoder.js.map +1 -1
- package/dist/lib/subprocess.d.ts +36 -3
- package/dist/lib/subprocess.d.ts.map +1 -1
- package/dist/lib/subprocess.js +121 -20
- package/dist/lib/subprocess.js.map +1 -1
- package/dist/lib/tool-helpers.d.ts.map +1 -1
- package/dist/lib/tool-helpers.js +19 -5
- package/dist/lib/tool-helpers.js.map +1 -1
- package/dist/resources/patterns.d.ts +1 -1
- package/dist/resources/patterns.d.ts.map +1 -1
- package/dist/resources/patterns.js +11 -5
- package/dist/resources/patterns.js.map +1 -1
- package/dist/tools/authoring/capabilities.js +2 -2
- package/dist/tools/authoring/check-schema.js +1 -1
- package/dist/tools/authoring/check-schema.js.map +1 -1
- package/dist/tools/evaluation/test-multiroot.d.ts.map +1 -1
- package/dist/tools/evaluation/test-multiroot.js +9 -10
- package/dist/tools/evaluation/test-multiroot.js.map +1 -1
- package/dist/tools/evaluation/test.js +13 -3
- package/dist/tools/evaluation/test.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,82 @@ not part of the public surface and may change in minor releases.
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
|
|
20
|
+
## [0.4.0] - 2026-09-03
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
|
|
24
|
+
- Policies evaluated through this server no longer receive the server's environment.
|
|
25
|
+
Rego exposes the environment of the `opa` process through `opa.runtime().env`, and
|
|
26
|
+
child processes inherited `process.env`, so any evaluated policy could read
|
|
27
|
+
`OPA_TOKEN`, the `GITHUB_TOKEN` the README asks users to set for
|
|
28
|
+
`rego_playground_share`, and everything else the operator's client passed in.
|
|
29
|
+
`rego_eval` accepts inline source, so `OPA_MCP_ALLOWED_PATHS` never applied and no
|
|
30
|
+
filesystem access was needed; a policy arriving through a README, an issue, or a
|
|
31
|
+
diff was enough. `conftest_test` was affected the same way, and is the likelier
|
|
32
|
+
route to third-party policy. Children now get an explicit allow-list containing no
|
|
33
|
+
secret. `OPA_MCP_PASSTHROUGH_ENV` opts individual variables back in, and anything
|
|
34
|
+
named there is readable by evaluated policy by design.
|
|
35
|
+
|
|
36
|
+
On Windows, libuv copies a fixed set of variables to every child regardless of what
|
|
37
|
+
is requested. `USERNAME`, `USERDOMAIN` and `LOGONSERVER` cannot be removed, so they
|
|
38
|
+
are blanked rather than left to disclose the operating-system user and domain.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- A subprocess that produces very large output no longer takes the server down.
|
|
43
|
+
stdout and stderr were captured without a size limit, and decoding a capture past
|
|
44
|
+
V8's maximum string length throws inside an async `close` handler, where no tool's
|
|
45
|
+
`try`/`catch` can reach it and the process exits. A policy iterating
|
|
46
|
+
`numbers.range(1, 1000)` under `--explain full` produced 518 MiB in under seven
|
|
47
|
+
seconds, so the 30-second timeout never applied; `opa` buffers its result and writes
|
|
48
|
+
it in one burst at exit, which means the runs that complete comfortably are the
|
|
49
|
+
dangerous ones. Capture is now capped per stream, the child is stopped on overflow,
|
|
50
|
+
and the tool returns the new `OUTPUT_TOO_LARGE` error code. Configurable with
|
|
51
|
+
`OPA_MCP_MAX_SUBPROCESS_BYTES`, default 32 MiB.
|
|
52
|
+
|
|
53
|
+
- `rego_test_multiroot` reports `OUTPUT_TOO_LARGE` and `TIMEOUT` through the same mapper as
|
|
54
|
+
every other tool. It carried its own copy of the failure ladder, and a run killed for
|
|
55
|
+
producing too much output reported `OPA_BINARY_NOT_FOUND`, sending the caller after an
|
|
56
|
+
install problem that did not exist. This is the same defect that was fixed for timeouts in
|
|
57
|
+
0.3.0, in the one place that did not share the fix.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- `OPA_MCP_MAX_SUBPROCESS_BYTES` and `OPA_MCP_PASSTHROUGH_ENV` environment variables.
|
|
62
|
+
- `OUTPUT_TOO_LARGE` error code.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
## [0.3.0] - 2026-08-05
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- The bundled OPA binary moves from 0.69.0 to 1.19.0. This is a breaking change for
|
|
70
|
+
anyone relying on the bundle. OPA 1.0 made Rego v1 the default, so rule bodies
|
|
71
|
+
require the `if` keyword and partial set rules require `contains`; policies written
|
|
72
|
+
against OPA 0.x no longer parse. `rego_migrate_v1` converts them. Installs that
|
|
73
|
+
supply their own binary through `OPA_BINARY` or `PATH` are unaffected, since the
|
|
74
|
+
bundled copy is only a fallback. The Docker image ships the same version.
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
|
|
78
|
+
- `rego_test` returns `INVALID_REGO` when the policies under test fail to load. It
|
|
79
|
+
previously reported a successful run with zero tests, so a caller whose policies had
|
|
80
|
+
stopped compiling was told the suite was fine. Most likely to surface right after
|
|
81
|
+
this upgrade, on trees that still hold v0 policies.
|
|
82
|
+
|
|
83
|
+
- A subprocess that exceeds the configured timeout returns `TIMEOUT` instead of
|
|
84
|
+
`OPA_BINARY_NOT_FOUND`. A killed process reports a null exit code, and the failure
|
|
85
|
+
mappers checked that before the timeout flag, so a slow command looked like a missing
|
|
86
|
+
install. Affected every CLI-backed tool, and `rego_test_multiroot` separately.
|
|
87
|
+
|
|
88
|
+
- Three Rego snippets in the bundled pattern library did not compile: two failed to
|
|
89
|
+
parse, and the rate-limiting example named a rule `count`, which shadowed the built-in
|
|
90
|
+
it then called. All shipped snippets are now compiled against the bundled OPA in CI.
|
|
91
|
+
|
|
92
|
+
- Corrected the tool list on the Docker Hub page, which named seven tools that do not
|
|
93
|
+
exist (`rego_bundle_*`, `rego_parse`, `rego_profile`, `rego_compile`) and undercounted
|
|
94
|
+
the total. Added the missing `rego_playground_share` entry to the README tool tables.
|
|
95
|
+
|
|
20
96
|
## [0.2.1] - 2026-06-25
|
|
21
97
|
|
|
22
98
|
### Fixed
|
|
@@ -952,7 +1028,19 @@ wrappers end-to-end. CI matrix: Ubuntu, macOS, and Windows on Node
|
|
|
952
1028
|
20 and 22, plus CodeQL security scanning and weekly Dependabot updates
|
|
953
1029
|
for npm, GitHub Actions, and Docker base images.
|
|
954
1030
|
|
|
955
|
-
[Unreleased]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.
|
|
1031
|
+
[Unreleased]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.4.0...HEAD
|
|
1032
|
+
[0.4.0]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.3.0...v0.4.0
|
|
1033
|
+
[0.3.0]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.2.1...v0.3.0
|
|
1034
|
+
[0.2.1]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.2.0...v0.2.1
|
|
1035
|
+
[0.2.0]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.20...v0.2.0
|
|
1036
|
+
[0.1.20]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.19...v0.1.20
|
|
1037
|
+
[0.1.19]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.18...v0.1.19
|
|
1038
|
+
[0.1.18]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.17...v0.1.18
|
|
1039
|
+
[0.1.17]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.16...v0.1.17
|
|
1040
|
+
[0.1.16]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.15...v0.1.16
|
|
1041
|
+
[0.1.15]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.14...v0.1.15
|
|
1042
|
+
[0.1.14]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.13...v0.1.14
|
|
1043
|
+
[0.1.13]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.12...v0.1.13
|
|
956
1044
|
[0.1.12]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.11...v0.1.12
|
|
957
1045
|
[0.1.11]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.10...v0.1.11
|
|
958
1046
|
[0.1.10]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.9...v0.1.10
|
|
@@ -963,6 +1051,6 @@ for npm, GitHub Actions, and Docker base images.
|
|
|
963
1051
|
[0.1.5]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.4...v0.1.5
|
|
964
1052
|
[0.1.4]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.3...v0.1.4
|
|
965
1053
|
[0.1.3]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.2...v0.1.3
|
|
966
|
-
[0.1.2]: https://github.com/OrygnsCode/opa-mcp-server/
|
|
967
|
-
[0.1.1]: https://github.com/OrygnsCode/opa-mcp-server/
|
|
1054
|
+
[0.1.2]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.1...v0.1.2
|
|
1055
|
+
[0.1.1]: https://github.com/OrygnsCode/opa-mcp-server/compare/v0.1.0...v0.1.1
|
|
968
1056
|
[0.1.0]: https://github.com/OrygnsCode/opa-mcp-server/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -22,10 +22,20 @@ environment.
|
|
|
22
22
|
+--------------------+ 52 tools +-----------------+ +---------------------+
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
> **Status:** v0.
|
|
25
|
+
> **Status:** v0.4.0. Tool surface, error codes, and
|
|
26
26
|
> environment variables follow [SemVer](https://semver.org/) from
|
|
27
27
|
> v0.1.0 forward.
|
|
28
28
|
|
|
29
|
+
> **Upgrading to 0.4.0:** subprocesses no longer inherit the server's
|
|
30
|
+
> environment. A policy that read a variable through `opa.runtime().env`
|
|
31
|
+
> will no longer see it; name the variable in `OPA_MCP_PASSTHROUGH_ENV` if
|
|
32
|
+
> it is genuinely needed. See the security section for why.
|
|
33
|
+
|
|
34
|
+
> **Upgrading to 0.3.0:** the bundled OPA is now 1.19, so Rego v0 policies
|
|
35
|
+
> no longer parse (`if` is required before a rule body, `contains` before a
|
|
36
|
+
> partial set). Run `rego_migrate_v1` to convert them. If you supply your own
|
|
37
|
+
> binary via `OPA_BINARY` or `PATH`, nothing changes.
|
|
38
|
+
|
|
29
39
|
---
|
|
30
40
|
|
|
31
41
|
## Table of contents
|
|
@@ -225,8 +235,9 @@ which opa && which regal # macOS / Linux
|
|
|
225
235
|
Get-Command opa, regal | Select-Object Source # Windows
|
|
226
236
|
```
|
|
227
237
|
|
|
228
|
-
This does not affect the **Docker**
|
|
229
|
-
|
|
238
|
+
This does not affect the **Docker** install path, which ships `opa` and
|
|
239
|
+
`regal` in the image and bypasses `PATH` entirely. The **MCPB** bundle
|
|
240
|
+
carries neither and resolves `opa` the same way the npm install does.
|
|
230
241
|
See [Troubleshooting](#troubleshooting) for full detail.
|
|
231
242
|
|
|
232
243
|
## Configuration
|
|
@@ -235,20 +246,22 @@ The server reads its configuration from environment variables. Every
|
|
|
235
246
|
variable is optional; defaults are sensible for a local OPA on
|
|
236
247
|
`http://localhost:8181`.
|
|
237
248
|
|
|
238
|
-
| Variable
|
|
239
|
-
|
|
|
240
|
-
| `OPA_URL`
|
|
241
|
-
| `OPA_TOKEN`
|
|
242
|
-
| `OPA_BINARY`
|
|
243
|
-
| `REGAL_BINARY`
|
|
244
|
-
| `CONFTEST_BINARY`
|
|
245
|
-
| `OPA_MCP_ALLOWED_PATHS`
|
|
246
|
-
| `OPA_MCP_LOG_FILE`
|
|
247
|
-
| `OPA_MCP_LOG_LEVEL`
|
|
248
|
-
| `OPA_MCP_MAX_RESPONSE_BYTES`
|
|
249
|
-
| `OPA_MCP_TIMEOUT_MS`
|
|
250
|
-
| `OPA_MCP_HTTP_TIMEOUT_MS`
|
|
251
|
-
| `OPA_MCP_NO_TELEMETRY`
|
|
249
|
+
| Variable | Default | Purpose |
|
|
250
|
+
| ------------------------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
251
|
+
| `OPA_URL` | `http://localhost:8181` | Base URL of an OPA REST endpoint, used by `opa_*` tools. |
|
|
252
|
+
| `OPA_TOKEN` | _(unset)_ | Bearer token for OPA, if your instance requires auth. Treated as a secret. Never echoed in logs or tool responses. |
|
|
253
|
+
| `OPA_BINARY` | `opa` (on `PATH`) | Path to the `opa` CLI, used by `rego_*` tools. |
|
|
254
|
+
| `REGAL_BINARY` | `regal` (on `PATH`) | Path to the `regal` linter. Required by `rego_lint`, `rego_fix`, and `rego_security_audit`. |
|
|
255
|
+
| `CONFTEST_BINARY` | `conftest` (on `PATH`) | Path to the `conftest` binary. Only required by `conftest_*` tools. Returns `CONFTEST_NOT_FOUND` if absent. |
|
|
256
|
+
| `OPA_MCP_ALLOWED_PATHS` | _(unset)_ | Comma- or semicolon-separated list of directories the server is allowed to read policies from. **When unset, file-based tools refuse to read from disk.** |
|
|
257
|
+
| `OPA_MCP_LOG_FILE` | `<tmpdir>/orygn-opa-mcp.log` | Path the server appends logs to. The server never writes to stdout; that channel is reserved for the MCP protocol. |
|
|
258
|
+
| `OPA_MCP_LOG_LEVEL` | `info` | One of `debug`, `info`, `warn`, `error`. |
|
|
259
|
+
| `OPA_MCP_MAX_RESPONSE_BYTES` | `100000` | Hard cap on a single tool response. Larger payloads are truncated with a `__truncated: true` marker. |
|
|
260
|
+
| `OPA_MCP_TIMEOUT_MS` | `30000` | Hard timeout for any spawned subprocess (`opa`, `regal`). After this, the child gets `SIGTERM` and then `SIGKILL`. |
|
|
261
|
+
| `OPA_MCP_HTTP_TIMEOUT_MS` | `15000` | Timeout for HTTP requests to the OPA REST API. |
|
|
262
|
+
| `OPA_MCP_NO_TELEMETRY` | _(unset)_ | Set to `1` to disable the anonymous startup ping. The ping sends the server version, OS platform, and a random install ID. The install ID is stored at `~/.orygn/opa-mcp/install-id` and is generated once on first run. No policy content or file paths are ever sent. |
|
|
263
|
+
| `OPA_MCP_MAX_SUBPROCESS_BYTES` | `33554432` (32 MiB) | Maximum bytes captured from a subprocess's stdout and stderr, counted separately. On overflow the stream is clamped, the child is stopped, and the tool returns `OUTPUT_TOO_LARGE`. Distinct from `OPA_MCP_MAX_RESPONSE_BYTES`, which trims the reply after the output is already in memory. |
|
|
264
|
+
| `OPA_MCP_PASSTHROUGH_ENV` | _(unset)_ | Comma-separated variable names to pass through to `opa`, `regal` and `conftest`. Everything else is withheld. **Anything named here is readable by any policy the server evaluates**, via `opa.runtime().env`, so use it only for values that are safe in that position. |
|
|
252
265
|
|
|
253
266
|
Paths in `OPA_MCP_ALLOWED_PATHS` and the `*_BINARY` variables must be
|
|
254
267
|
absolute. Relative paths and missing binaries are rejected with structured
|
|
@@ -269,7 +282,8 @@ Stable error codes: `INVALID_INPUT`, `INVALID_REGO`, `INVALID_BUNDLE`,
|
|
|
269
282
|
`OPA_AUTH_FAILED`, `POLICY_NOT_FOUND`, `DATA_NOT_FOUND`, `PATH_NOT_ALLOWED`,
|
|
270
283
|
`PATH_NOT_FOUND`, `DEPENDENCY_CONFLICT`, `NO_TESTS_FOUND`,
|
|
271
284
|
`COVERAGE_BELOW_THRESHOLD`, `OPA_VERSION_UNSUPPORTED`, `VERIFY_INCONCLUSIVE`,
|
|
272
|
-
`Z3_INIT_ERROR`, `GITHUB_TOKEN_MISSING`, `GIST_CREATE_FAILED`,
|
|
285
|
+
`Z3_INIT_ERROR`, `GITHUB_TOKEN_MISSING`, `GIST_CREATE_FAILED`,
|
|
286
|
+
`OUTPUT_TOO_LARGE`, `TIMEOUT`,
|
|
273
287
|
`CANCELLED`, `UNKNOWN_ERROR`.
|
|
274
288
|
|
|
275
289
|
### Category A: Authoring & static analysis
|
|
@@ -295,7 +309,7 @@ Operate on Rego source code without needing a running OPA server. Wrap
|
|
|
295
309
|
```jsonc
|
|
296
310
|
// Input
|
|
297
311
|
{
|
|
298
|
-
"source": "package x\nallow
|
|
312
|
+
"source": "package x\nallow if input.user==\"admin\""
|
|
299
313
|
}
|
|
300
314
|
|
|
301
315
|
// Output (ok)
|
|
@@ -419,6 +433,7 @@ the tasks agents are actually asked to do.
|
|
|
419
433
|
| `rego_policy_diff` | Evaluate the same query against two policies in parallel and compare the results. Returns `equal: true/false`, the raw value from each side (`resultA`/`resultB`), and `changedPaths` -- dot/bracket JSON paths that differ. Each side takes inline source or a file/directory path. Useful for verifying refactor equivalence or mapping divergence between two policy versions. |
|
|
420
434
|
| `rego_verify` | Formally verify a property about a Rego rule using SMT solving (Microsoft Z3 via WASM). Unlike testing, this checks ALL possible inputs mathematically and either proves the property holds or returns a concrete counterexample. Supports `always_true`, `never_true`, and `satisfiable` property kinds. Handles equality, comparison, string built-ins (`startswith`, `endswith`, `contains`, `regex.match`), multi-clause rules, and cross-rule inlining. Reports `INCONCLUSIVE` for negation-as-failure and comprehensions. |
|
|
421
435
|
| `rego_explain_undefined` | Explain why a Rego query is undefined. Combines a plain eval, a full-trace eval, and per-condition AST analysis to identify the exact body expression blocking each rule. Returns a structured breakdown of which conditions blocked each rule plus a human-readable summary. |
|
|
436
|
+
| `rego_playground_share` | Publish a policy (and optional input) as a secret GitHub Gist and return the link, for sharing a reproduction. Requires `GITHUB_TOKEN` with the `gist` scope; returns `GITHUB_TOKEN_MISSING` otherwise. |
|
|
422
437
|
|
|
423
438
|
### Category F: Conftest (configuration policy testing)
|
|
424
439
|
|
|
@@ -440,7 +455,7 @@ YAML/JSON/HCL/TOML/INI against Rego policies using
|
|
|
440
455
|
// Input
|
|
441
456
|
{
|
|
442
457
|
"inlineConfig": "apiVersion: v1\nkind: Pod\nspec:\n containers:\n - name: app\n image: nginx:latest",
|
|
443
|
-
"inlinePolicy": "package main\ndeny
|
|
458
|
+
"inlinePolicy": "package main\ndeny contains msg if { input.spec.containers[_].image == \"nginx:latest\"; msg := \"pin your image tag\" }"
|
|
444
459
|
}
|
|
445
460
|
|
|
446
461
|
// Output
|
|
@@ -566,18 +581,28 @@ Agent: Done. Policy `rbac` is live on staging at $OPA_URL.
|
|
|
566
581
|
└───────────────────────────────────────────────────────────────────────────────────────┘
|
|
567
582
|
```
|
|
568
583
|
|
|
569
|
-
|
|
584
|
+
Four things worth knowing if you're going to operate this:
|
|
570
585
|
|
|
571
586
|
1. **stdout is the protocol channel.** The server logs to a file via
|
|
572
587
|
`lib/logger.ts` and never writes to stdout. If you see stray stdout
|
|
573
588
|
bytes, the client disconnects; the MCP transport layer is strict.
|
|
574
589
|
2. **No tool throws.** Every tool catches its own exceptions and returns
|
|
575
590
|
a structured `{ ok: false, error: ... }` envelope. The agent sees a
|
|
576
|
-
stable error vocabulary, not a stack trace.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
`
|
|
580
|
-
|
|
591
|
+
stable error vocabulary, not a stack trace. Decoding subprocess output
|
|
592
|
+
happens inside an async callback, where a throw would bypass those
|
|
593
|
+
handlers entirely, so that path is bounded by bytes rather than left to
|
|
594
|
+
a `try`/`catch` that could not see it.
|
|
595
|
+
3. **Subprocesses are bounded in time, size, and environment.**
|
|
596
|
+
`lib/subprocess.ts` runs the binaries with `shell: false`, a hard
|
|
597
|
+
timeout with `SIGTERM`-then-`SIGKILL` escalation, and a per-stream byte
|
|
598
|
+
cap. There is no path through the server where an agent can construct a
|
|
599
|
+
shell command. The timeout alone is not enough: `opa` buffers a result
|
|
600
|
+
in memory and writes it in one burst at exit, so a command that finishes
|
|
601
|
+
well inside the timeout can still deliver hundreds of megabytes.
|
|
602
|
+
4. **Children do not inherit the server's environment.** `lib/child-env.ts`
|
|
603
|
+
builds an explicit allow-list instead. Rego can read its interpreter's
|
|
604
|
+
environment through `opa.runtime().env`, so anything passed down is
|
|
605
|
+
readable by any policy the server evaluates.
|
|
581
606
|
|
|
582
607
|
## Security
|
|
583
608
|
|
|
@@ -587,8 +612,18 @@ be exposed on the network.
|
|
|
587
612
|
|
|
588
613
|
- File-based tools refuse to read anything outside `OPA_MCP_ALLOWED_PATHS`.
|
|
589
614
|
When that variable is unset, file tools return `PATH_NOT_ALLOWED`.
|
|
590
|
-
- Subprocesses run with `shell: false
|
|
591
|
-
|
|
615
|
+
- Subprocesses run with `shell: false`, a hard timeout, and a byte cap on
|
|
616
|
+
captured output.
|
|
617
|
+
- **Evaluated policy cannot read the server's environment.** Rego exposes the
|
|
618
|
+
environment of the `opa` process through `opa.runtime().env`, so a child that
|
|
619
|
+
inherited `process.env` would hand `OPA_TOKEN`, `GITHUB_TOKEN`, and every
|
|
620
|
+
other variable to any policy it evaluated. Since `rego_eval` accepts inline
|
|
621
|
+
source, no filesystem access is needed to reach that, which puts it one
|
|
622
|
+
prompt injection away from any untrusted Rego an agent reads. Children get an
|
|
623
|
+
explicit allow-list instead (`lib/child-env.ts`), containing no secret.
|
|
624
|
+
`OPA_MCP_PASSTHROUGH_ENV` opts individual variables back in.
|
|
625
|
+
- `OPA_TOKEN` is never echoed in tool responses or log entries, and is not
|
|
626
|
+
passed to any child process.
|
|
592
627
|
- Releases are published with
|
|
593
628
|
[npm provenance](https://docs.npmjs.com/generating-provenance-statements);
|
|
594
629
|
the Docker image is built reproducibly from the committed `Dockerfile`.
|
|
@@ -633,8 +668,9 @@ is not enough -- the spawned server gets a reduced `PATH`. The
|
|
|
633
668
|
[`examples/`](./examples) configs already include these env vars; just
|
|
634
669
|
edit the placeholder paths.
|
|
635
670
|
|
|
636
|
-
This issue does **not** affect the Docker
|
|
637
|
-
|
|
671
|
+
This issue does **not** affect the Docker install path, which bundles
|
|
672
|
+
`opa` and `regal` and bypasses `PATH` entirely. The MCPB bundle resolves
|
|
673
|
+
`opa` from `OPA_BINARY` or `PATH`, so it can hit this.
|
|
638
674
|
|
|
639
675
|
**The server starts, then the client says "disconnected."**
|
|
640
676
|
|
|
@@ -715,7 +751,7 @@ Breaking changes will be:
|
|
|
715
751
|
|
|
716
752
|
Pinned versions of the upstream toolchain (`opa` and `regal`) are treated
|
|
717
753
|
as part of the build, not as a dependency the operator manages. The
|
|
718
|
-
Dockerfile
|
|
754
|
+
Dockerfile and CI use the same pin; bumps go through
|
|
719
755
|
Dependabot or a manual PR.
|
|
720
756
|
|
|
721
757
|
## License
|
package/dist/config.d.ts
CHANGED
|
@@ -31,6 +31,16 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
31
31
|
* write to a file path the agent specifies.
|
|
32
32
|
*/
|
|
33
33
|
maxResponseBytes: z.ZodDefault<z.ZodNumber>;
|
|
34
|
+
/**
|
|
35
|
+
* Maximum bytes captured from a subprocess's stdout and stderr, each counted
|
|
36
|
+
* separately. On overflow the stream is clamped and the child is killed.
|
|
37
|
+
*
|
|
38
|
+
* This is a memory bound, distinct from `maxResponseBytes`, which trims the
|
|
39
|
+
* response after the output has already been read into the heap. A capture
|
|
40
|
+
* past V8's max string length cannot be decoded at all, and the throw would
|
|
41
|
+
* land in an async callback where no tool can catch it.
|
|
42
|
+
*/
|
|
43
|
+
maxSubprocessBytes: z.ZodDefault<z.ZodNumber>;
|
|
34
44
|
}, "strip", z.ZodTypeAny, {
|
|
35
45
|
opaUrl: string;
|
|
36
46
|
opaBinary: string;
|
|
@@ -40,8 +50,9 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
40
50
|
httpTimeoutMs: number;
|
|
41
51
|
allowedPaths: string[];
|
|
42
52
|
logFile: string;
|
|
43
|
-
logLevel: "
|
|
53
|
+
logLevel: "error" | "debug" | "info" | "warn";
|
|
44
54
|
maxResponseBytes: number;
|
|
55
|
+
maxSubprocessBytes: number;
|
|
45
56
|
opaToken?: string | undefined;
|
|
46
57
|
}, {
|
|
47
58
|
opaUrl?: string | undefined;
|
|
@@ -53,8 +64,9 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
53
64
|
httpTimeoutMs?: number | undefined;
|
|
54
65
|
allowedPaths?: string[] | undefined;
|
|
55
66
|
logFile?: string | undefined;
|
|
56
|
-
logLevel?: "
|
|
67
|
+
logLevel?: "error" | "debug" | "info" | "warn" | undefined;
|
|
57
68
|
maxResponseBytes?: number | undefined;
|
|
69
|
+
maxSubprocessBytes?: number | undefined;
|
|
58
70
|
}>;
|
|
59
71
|
export type Config = z.infer<typeof ConfigSchema>;
|
|
60
72
|
export declare function loadConfig(): Config;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,QAAA,MAAM,YAAY;IAChB,wEAAwE;;IASxE,2EAA2E;;IAG3E,2DAA2D;;IAG3D,+DAA+D;;IAG/D,qEAAqE;;IAGrE,kEAAkE;;IAGlE,mDAAmD;;IAGnD;;;;;OAKG;;IAGH,uEAAuE;;IAGvE,qCAAqC;;IAGrC;;;;OAIG;;IAGH;;;;;;;;OAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUH,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AA0BlD,wBAAgB,UAAU,IAAI,MAAM,CAsCnC"}
|
package/dist/config.js
CHANGED
|
@@ -9,6 +9,7 @@ import { tmpdir } from 'node:os';
|
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
import { resolveOpaBinary } from './lib/resolve-binary.js';
|
|
12
|
+
import { DEFAULT_MAX_OUTPUT_BYTES } from './lib/subprocess.js';
|
|
12
13
|
const ConfigSchema = z.object({
|
|
13
14
|
/** Base URL of a running OPA server (used by `opa_*` runtime tools). */
|
|
14
15
|
opaUrl: z
|
|
@@ -47,6 +48,21 @@ const ConfigSchema = z.object({
|
|
|
47
48
|
* write to a file path the agent specifies.
|
|
48
49
|
*/
|
|
49
50
|
maxResponseBytes: z.coerce.number().int().positive().default(100_000),
|
|
51
|
+
/**
|
|
52
|
+
* Maximum bytes captured from a subprocess's stdout and stderr, each counted
|
|
53
|
+
* separately. On overflow the stream is clamped and the child is killed.
|
|
54
|
+
*
|
|
55
|
+
* This is a memory bound, distinct from `maxResponseBytes`, which trims the
|
|
56
|
+
* response after the output has already been read into the heap. A capture
|
|
57
|
+
* past V8's max string length cannot be decoded at all, and the throw would
|
|
58
|
+
* land in an async callback where no tool can catch it.
|
|
59
|
+
*/
|
|
60
|
+
maxSubprocessBytes: z.coerce
|
|
61
|
+
.number()
|
|
62
|
+
.int()
|
|
63
|
+
.positive()
|
|
64
|
+
.max(DEFAULT_MAX_OUTPUT_BYTES * 8, 'OPA_MCP_MAX_SUBPROCESS_BYTES is too large to decode safely; keep it well under 512 MiB.')
|
|
65
|
+
.default(DEFAULT_MAX_OUTPUT_BYTES),
|
|
50
66
|
});
|
|
51
67
|
function parseAllowedPaths(raw) {
|
|
52
68
|
if (!raw)
|
|
@@ -69,6 +85,7 @@ const ENV_VAR_NAMES = {
|
|
|
69
85
|
logFile: 'OPA_MCP_LOG_FILE',
|
|
70
86
|
logLevel: 'OPA_MCP_LOG_LEVEL',
|
|
71
87
|
maxResponseBytes: 'OPA_MCP_MAX_RESPONSE_BYTES',
|
|
88
|
+
maxSubprocessBytes: 'OPA_MCP_MAX_SUBPROCESS_BYTES',
|
|
72
89
|
};
|
|
73
90
|
export function loadConfig() {
|
|
74
91
|
const allowedPaths = parseAllowedPaths(process.env['OPA_MCP_ALLOWED_PATHS']);
|
|
@@ -84,6 +101,7 @@ export function loadConfig() {
|
|
|
84
101
|
logFile: process.env['OPA_MCP_LOG_FILE'],
|
|
85
102
|
logLevel: process.env['OPA_MCP_LOG_LEVEL'],
|
|
86
103
|
maxResponseBytes: process.env['OPA_MCP_MAX_RESPONSE_BYTES'],
|
|
104
|
+
maxSubprocessBytes: process.env['OPA_MCP_MAX_SUBPROCESS_BYTES'],
|
|
87
105
|
});
|
|
88
106
|
if (!parsed.success) {
|
|
89
107
|
console.error('opa-mcp: invalid configuration');
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the environment handed to `opa`, `regal` and `conftest`.
|
|
3
|
+
*
|
|
4
|
+
* Rego can read its interpreter's environment: `opa.runtime().env` returns the
|
|
5
|
+
* whole thing. So every variable this server passes down is readable by any
|
|
6
|
+
* policy it evaluates, and `rego_eval` takes inline source -- no filesystem
|
|
7
|
+
* access, so `OPA_MCP_ALLOWED_PATHS` never applies. Untrusted Rego reaches an
|
|
8
|
+
* agent through a README, an issue, or a pull request diff, which makes a
|
|
9
|
+
* pass-through of `process.env` a prompt injection away from handing over
|
|
10
|
+
* `OPA_TOKEN`, the `GITHUB_TOKEN` this project's own README asks users to put in
|
|
11
|
+
* their client config, and whatever else the operator's shell happens to hold.
|
|
12
|
+
*
|
|
13
|
+
* So the child gets an explicit allow-list instead. Nothing on it is a secret.
|
|
14
|
+
* Measured against the bundled OPA 1.19.0, `version`, `eval`, `check`, `test`
|
|
15
|
+
* and `build` all succeed with a completely empty environment, so the entries
|
|
16
|
+
* below exist for correctness in real-world setups (proxies, custom CA bundles,
|
|
17
|
+
* tool config discovery), not to make the binaries run.
|
|
18
|
+
*
|
|
19
|
+
* Operators who genuinely need a variable in policy can name it in
|
|
20
|
+
* `OPA_MCP_PASSTHROUGH_ENV`, which is opt-in precisely because it undoes this.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Build the child environment: the allow-list, plus any opted-in passthrough,
|
|
24
|
+
* plus `extra` from the caller (which wins, since it is explicit intent).
|
|
25
|
+
*
|
|
26
|
+
* @param extra Variables the calling command needs. Readable by evaluated
|
|
27
|
+
* policy like everything else here, so pass secrets only when the
|
|
28
|
+
* command actually requires them.
|
|
29
|
+
* @param source Environment to read from. Injectable for testing.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildChildEnv(extra?: Record<string, string>, source?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
32
|
+
/** Exposed for tests and for documenting the surface. */
|
|
33
|
+
export declare const ALLOWED_CHILD_ENV_VARS: readonly string[];
|
|
34
|
+
//# sourceMappingURL=child-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"child-env.d.ts","sourceRoot":"","sources":["../../src/lib/child-env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA0GH;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,GAAE,MAAM,CAAC,UAAwB,GACtC,MAAM,CAAC,UAAU,CAanB;AAED,yDAAyD;AACzD,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAAY,CAAC"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the environment handed to `opa`, `regal` and `conftest`.
|
|
3
|
+
*
|
|
4
|
+
* Rego can read its interpreter's environment: `opa.runtime().env` returns the
|
|
5
|
+
* whole thing. So every variable this server passes down is readable by any
|
|
6
|
+
* policy it evaluates, and `rego_eval` takes inline source -- no filesystem
|
|
7
|
+
* access, so `OPA_MCP_ALLOWED_PATHS` never applies. Untrusted Rego reaches an
|
|
8
|
+
* agent through a README, an issue, or a pull request diff, which makes a
|
|
9
|
+
* pass-through of `process.env` a prompt injection away from handing over
|
|
10
|
+
* `OPA_TOKEN`, the `GITHUB_TOKEN` this project's own README asks users to put in
|
|
11
|
+
* their client config, and whatever else the operator's shell happens to hold.
|
|
12
|
+
*
|
|
13
|
+
* So the child gets an explicit allow-list instead. Nothing on it is a secret.
|
|
14
|
+
* Measured against the bundled OPA 1.19.0, `version`, `eval`, `check`, `test`
|
|
15
|
+
* and `build` all succeed with a completely empty environment, so the entries
|
|
16
|
+
* below exist for correctness in real-world setups (proxies, custom CA bundles,
|
|
17
|
+
* tool config discovery), not to make the binaries run.
|
|
18
|
+
*
|
|
19
|
+
* Operators who genuinely need a variable in policy can name it in
|
|
20
|
+
* `OPA_MCP_PASSTHROUGH_ENV`, which is opt-in precisely because it undoes this.
|
|
21
|
+
*/
|
|
22
|
+
/** Vars every platform benefits from. Locale keeps output formatting stable. */
|
|
23
|
+
const COMMON = ['PATH', 'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_NUMERIC', 'TZ'];
|
|
24
|
+
/** POSIX home and temp. Regal and Conftest look under HOME for config. */
|
|
25
|
+
const POSIX = ['HOME', 'TMPDIR', 'SHELL', 'USER', 'LOGNAME'];
|
|
26
|
+
/**
|
|
27
|
+
* Windows equivalents. A Go binary starts without these, but process creation
|
|
28
|
+
* and temp-file handling are better behaved with them present.
|
|
29
|
+
*/
|
|
30
|
+
const WINDOWS = [
|
|
31
|
+
'PATHEXT',
|
|
32
|
+
'SystemRoot',
|
|
33
|
+
'SystemDrive',
|
|
34
|
+
'COMSPEC',
|
|
35
|
+
'windir',
|
|
36
|
+
'TEMP',
|
|
37
|
+
'TMP',
|
|
38
|
+
'USERPROFILE',
|
|
39
|
+
'HOMEDRIVE',
|
|
40
|
+
'HOMEPATH',
|
|
41
|
+
'APPDATA',
|
|
42
|
+
'LOCALAPPDATA',
|
|
43
|
+
'PROCESSOR_ARCHITECTURE',
|
|
44
|
+
'NUMBER_OF_PROCESSORS',
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Proxy and TLS trust. Without these, `http.send`, bundle downloads and remote
|
|
48
|
+
* schema fetches fail behind a corporate proxy or a custom CA.
|
|
49
|
+
*
|
|
50
|
+
* A proxy URL can embed credentials, so these are the least inert entries here.
|
|
51
|
+
* They are included because breaking every corporate user is the worse trade,
|
|
52
|
+
* and they are the operator's own infrastructure credentials rather than the
|
|
53
|
+
* cloud and repository tokens that motivated this module.
|
|
54
|
+
*/
|
|
55
|
+
const NETWORK = [
|
|
56
|
+
'HTTP_PROXY',
|
|
57
|
+
'HTTPS_PROXY',
|
|
58
|
+
'NO_PROXY',
|
|
59
|
+
'ALL_PROXY',
|
|
60
|
+
'http_proxy',
|
|
61
|
+
'https_proxy',
|
|
62
|
+
'no_proxy',
|
|
63
|
+
'all_proxy',
|
|
64
|
+
'SSL_CERT_FILE',
|
|
65
|
+
'SSL_CERT_DIR',
|
|
66
|
+
];
|
|
67
|
+
/**
|
|
68
|
+
* Where the tools look for their own config and credentials. These name file
|
|
69
|
+
* locations; they are not themselves secrets. Conftest reads registry auth from
|
|
70
|
+
* the Docker config file, so `conftest_pull` and `conftest_push` need these.
|
|
71
|
+
*/
|
|
72
|
+
const TOOL_CONFIG = [
|
|
73
|
+
'XDG_CONFIG_HOME',
|
|
74
|
+
'XDG_CACHE_HOME',
|
|
75
|
+
'XDG_DATA_HOME',
|
|
76
|
+
'DOCKER_CONFIG',
|
|
77
|
+
'REGISTRY_AUTH_FILE',
|
|
78
|
+
];
|
|
79
|
+
const ALLOWED = [...COMMON, ...POSIX, ...WINDOWS, ...NETWORK, ...TOOL_CONFIG];
|
|
80
|
+
/** Names an operator opted into via OPA_MCP_PASSTHROUGH_ENV. */
|
|
81
|
+
function passthroughNames(source) {
|
|
82
|
+
const raw = source['OPA_MCP_PASSTHROUGH_ENV'];
|
|
83
|
+
if (!raw)
|
|
84
|
+
return [];
|
|
85
|
+
return raw
|
|
86
|
+
.split(/[;,]/)
|
|
87
|
+
.map((s) => s.trim())
|
|
88
|
+
.filter((s) => s.length > 0);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Windows treats variable names case-insensitively, and the casing in
|
|
92
|
+
* `process.env` follows whatever the parent used, so an exact-key lookup would
|
|
93
|
+
* silently miss `Path` where the list says `PATH`.
|
|
94
|
+
*/
|
|
95
|
+
function lookup(source, name) {
|
|
96
|
+
const direct = source[name];
|
|
97
|
+
if (direct !== undefined)
|
|
98
|
+
return direct;
|
|
99
|
+
if (process.platform !== 'win32')
|
|
100
|
+
return undefined;
|
|
101
|
+
const wanted = name.toLowerCase();
|
|
102
|
+
for (const key of Object.keys(source)) {
|
|
103
|
+
if (key.toLowerCase() === wanted)
|
|
104
|
+
return source[key];
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Windows-only identity variables that libuv copies from the parent no matter
|
|
110
|
+
* what environment is requested -- spawning with `{}` still delivers them, and
|
|
111
|
+
* measurably so. They are not secrets, but they name the operating-system user,
|
|
112
|
+
* the Windows domain and the domain controller, which is more than an untrusted
|
|
113
|
+
* policy needs to know about the machine evaluating it.
|
|
114
|
+
*
|
|
115
|
+
* They cannot be removed, only overwritten, so they are blanked. None of the
|
|
116
|
+
* three binaries reads them: every representative `opa` command succeeds with a
|
|
117
|
+
* completely empty environment on both platforms.
|
|
118
|
+
*/
|
|
119
|
+
const WINDOWS_IDENTITY_TO_BLANK = ['USERNAME', 'USERDOMAIN', 'LOGONSERVER'];
|
|
120
|
+
/**
|
|
121
|
+
* Build the child environment: the allow-list, plus any opted-in passthrough,
|
|
122
|
+
* plus `extra` from the caller (which wins, since it is explicit intent).
|
|
123
|
+
*
|
|
124
|
+
* @param extra Variables the calling command needs. Readable by evaluated
|
|
125
|
+
* policy like everything else here, so pass secrets only when the
|
|
126
|
+
* command actually requires them.
|
|
127
|
+
* @param source Environment to read from. Injectable for testing.
|
|
128
|
+
*/
|
|
129
|
+
export function buildChildEnv(extra, source = process.env) {
|
|
130
|
+
const env = {};
|
|
131
|
+
if (process.platform === 'win32') {
|
|
132
|
+
for (const name of WINDOWS_IDENTITY_TO_BLANK)
|
|
133
|
+
env[name] = '';
|
|
134
|
+
}
|
|
135
|
+
for (const name of [...ALLOWED, ...passthroughNames(source)]) {
|
|
136
|
+
const value = lookup(source, name);
|
|
137
|
+
if (value !== undefined)
|
|
138
|
+
env[name] = value;
|
|
139
|
+
}
|
|
140
|
+
return extra ? { ...env, ...extra } : env;
|
|
141
|
+
}
|
|
142
|
+
/** Exposed for tests and for documenting the surface. */
|
|
143
|
+
export const ALLOWED_CHILD_ENV_VARS = ALLOWED;
|
|
144
|
+
//# sourceMappingURL=child-env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"child-env.js","sourceRoot":"","sources":["../../src/lib/child-env.ts"],"names":[],"mappings":"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"}
|