diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..382c08c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,21 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+---
+
+**Feature request checklist**
+
+- [ ] There are no issues that match the desired change
+- [ ] The change is large enough it can't be addressed with a simple Pull
+ Request
+
+**Change**
+Summary of the proposed change and some details about what problem
+it helps solve.
+
+**Example**
+Replace this text with an example indicating the desired functionality.
+
+**Alternatives considered**
+Briefly list alternative designs, or an example of the functionality to be
+replaced.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5520e57
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+bazel-bin/
+bazel-out/
+bazel-skills/
+bazel-testlogs/
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..f0c23cc
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,8 @@
+
+
+
+exports_files(["LICENSE"])
+
+
+
+licenses(["notice"]) # Apache 2.0
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..ffae581
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,96 @@
+# Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of
+experience, education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, or to ban temporarily or permanently any
+contributor for other behaviors that they deem inappropriate, threatening,
+offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+This Code of Conduct also applies outside the project spaces when the Project
+Steward has a reasonable belief that an individual's behavior may have a
+negative impact on the project or its community.
+
+## Conflict Resolution
+
+We do not believe that all conflict is bad; healthy debate and disagreement
+often yield positive results. However, it is never okay to be disrespectful or
+to engage in behavior that violates the project’s code of conduct.
+
+If you see someone violating the code of conduct, you are encouraged to address
+the behavior directly with those involved. Many issues can be resolved quickly
+and easily, and this gives people more control over the outcome of their
+dispute. If you are unable to resolve the matter for any reason, or if the
+behavior is threatening or harassing, report it. We are dedicated to providing
+an environment where participants feel welcome and safe.
+
+Reports should be directed to Tristan Swadell, tswadell at google dot com, the
+Project Steward(s) for CEL. It is the Project Steward’s duty to receive and
+address reported violations of the code of conduct. They will then work with a
+committee consisting of representatives from the Open Source Programs Office
+and the Google Open Source Strategy team. If for any reason you are
+uncomfortable reaching out to the Project Steward, please email
+opensource@google.com.
+
+We will investigate every complaint, but you may not receive a direct response.
+We will use our discretion in determining when and how to follow up on reported
+incidents, which may range from not taking action to permanent expulsion from
+the project and project-sponsored spaces. We will notify the accused of the
+report and provide them an opportunity to discuss it before any action is taken.
+The identity of the reporter will be omitted from the details of the report
+supplied to the accused. In potentially harmful situations, such as ongoing
+harassment or threats to anyone's safety, we may take action without notice.
+
+## Attribution
+
+This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
+available at
+https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+Note: A version of this file is also available in the
+[New Project repo](https://github.com/google/new-project/blob/master/docs/code-of-conduct.md).
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..697d7cb
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,66 @@
+# Contributing
+
+Thank you for your interest in contributing to CEL Skills!
+
+## Where to Start?
+
+CEL is a dynamic expression language that parses into a portable format,
+and lends itself well to evaluation across processes through its support
+of evaluation with partial state.
+
+We currently welcome a wide variety of contributions:
+
+* Requests for bugs and features through issues.
+* Fixes of bugs and implementations of features.
+* Documentation and examples.
+
+The general guideline is that contributions must improve the user experience
+without significantly altering its semantics or performance characteristics.
+
+Learn more about CEL semantics in the [CEL Spec][1] repo.
+
+## Contribution Process
+
+All submissions require review. Small changes can be submitted directly via
+[Pull Request](./PULL_REQUEST_TEMPLATE.md) on GitHub. Use your judgement about
+what constitutes a small change, but if in doubt, follow this process for
+larger changes:
+
+* Determine whether the changes has already been requested in Issues.
+* If so, please add a comment to indicate your interest as this helps
+ the CEL maintainers prioritize changes.
+* If not, file an [Issue](https://github.com/cel-expr/skills/issues/new/choose).
+
+## Code Contributions
+
+See [PULL_REQUEST_TEMPLATE.md](./PULL_REQUEST_TEMPLATE.md) for guidelines on
+creating, reviewing, and merging code contributions.
+
+### Contributor License Agreement
+
+Code contributions must be accompanied by a Contributor License Agreement. You
+(or your employer) retain the copyright to your contribution, but the agreement
+gives us permission to use and redistribute your contributions as part of the
+project. Visit to view prior agreements,
+or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted
+one (even if it was for a different project), you probably don't need to do it
+again.
+
+## Code Reviews
+
+All code changes must be reviewed before merge. Expect maintainers to respond
+to new issues or pull requests within a week.
+
+If approval is given to a code change, commits should be squashed on merge.
+This makes it easier to triage issues related to a change, and makes the commit
+graph human-readable.
+
+For outstanding and ongoing issues and particularly for long-running pull
+requests, expect the maintainers to review within a week of a contributor
+asking for a new review. There is no commitment to resolution -- merging
+or closing a pull request, or fixing or closing an issue -- because some
+issues will require more discussion than others.
+
+[1]: https://github.com/google/cel-spec
diff --git a/GEMINI.md b/GEMINI.md
new file mode 100644
index 0000000..c582702
--- /dev/null
+++ b/GEMINI.md
@@ -0,0 +1,21 @@
+# Google Common Expression Language (CEL) Extension
+
+This extension provides the Gemini CLI with tools and skills to author, test,
+and debug CEL expressions.
+
+## Provided Tools
+
+The `cel` MCP server provides the following tools:
+
+- `cel_create_environment`: Defines the variables, functions, types for an
+ expression.
+- `cel_generate_prompt`: Generates an authoring prompt for an expression based
+ on the configuration and requirement.
+- `cel_compile`: Compiles a CEL expression to validate syntax, correctness, and
+ type checking against an environment definition.
+- `cel_evaluate`: Evaluates a compiled expression against provided test cases.
+
+## Available Skills
+
+To understand how to best use these tools, please refer to the `cel-authoring`,
+`cel-testing`, and `cel-debugging` skills.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/MODULE.bazel b/MODULE.bazel
new file mode 100644
index 0000000..6993f8a
--- /dev/null
+++ b/MODULE.bazel
@@ -0,0 +1,29 @@
+module(
+ name = "cel-skills",
+)
+
+bazel_dep(
+ name = "googleapis",
+ version = "0.0.0-20241220-5e258e33.bcr.1",
+ repo_name = "com_google_googleapis",
+)
+bazel_dep(
+ name = "protobuf",
+ version = "33.4",
+ repo_name = "com_google_protobuf",
+)
+bazel_dep(name = "gazelle", version = "0.41.0")
+bazel_dep(name = "rules_go", version = "0.60.0")
+bazel_dep(name = "rules_proto", version = "7.1.0")
+
+go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
+go_sdk.download(version = "1.25.0")
+
+go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
+go_deps.gazelle_default_attributes(
+ directives = [
+ "gazelle:proto disable_global",
+ ],
+)
+go_deps.from_file(go_mod = "//:go.mod")
+use_repo(go_deps, "com_github_google_cel_go", "com_github_modelcontextprotocol_go_sdk", "org_golang_google_protobuf")
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
new file mode 100644
index 0000000..cbe16f1
--- /dev/null
+++ b/MODULE.bazel.lock
@@ -0,0 +1,638 @@
+{
+ "lockFileVersion": 26,
+ "registryFileHashes": {
+ "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497",
+ "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2",
+ "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589",
+ "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0",
+ "https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb",
+ "https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16",
+ "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915",
+ "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
+ "https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16",
+ "https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1",
+ "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215",
+ "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2",
+ "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c",
+ "https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896",
+ "https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85",
+ "https://bcr.bazel.build/modules/apple_support/1.21.0/MODULE.bazel": "ac1824ed5edf17dee2fdd4927ada30c9f8c3b520be1b5fd02a5da15bc10bff3e",
+ "https://bcr.bazel.build/modules/apple_support/1.21.1/MODULE.bazel": "5809fa3efab15d1f3c3c635af6974044bac8a4919c62238cce06acee8a8c11f1",
+ "https://bcr.bazel.build/modules/apple_support/1.24.2/MODULE.bazel": "0e62471818affb9f0b26f128831d5c40b074d32e6dda5a0d3852847215a41ca4",
+ "https://bcr.bazel.build/modules/apple_support/1.24.2/source.json": "2c22c9827093250406c5568da6c54e6fdf0ef06238def3d99c71b12feb057a8d",
+ "https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b",
+ "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
+ "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101",
+ "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8",
+ "https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d",
+ "https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d",
+ "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
+ "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
+ "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
+ "https://bcr.bazel.build/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a",
+ "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
+ "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
+ "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
+ "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
+ "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6",
+ "https://bcr.bazel.build/modules/bazel_features/1.36.0/MODULE.bazel": "596cb62090b039caf1cad1d52a8bc35cf188ca9a4e279a828005e7ee49a1bec3",
+ "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
+ "https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080",
+ "https://bcr.bazel.build/modules/bazel_features/1.42.1/source.json": "fcd4396b2df85f64f2b3bb436ad870793ecf39180f1d796f913cc9276d355309",
+ "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67",
+ "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb",
+ "https://bcr.bazel.build/modules/buildozer/8.5.1/MODULE.bazel": "a35d9561b3fc5b18797c330793e99e3b834a473d5fbd3d7d7634aafc9bdb6f8f",
+ "https://bcr.bazel.build/modules/buildozer/8.5.1/source.json": "e3386e6ff4529f2442800dee47ad28d3e6487f36a1f75ae39ae56c70f0cd2fbd",
+ "https://bcr.bazel.build/modules/gazelle/0.32.0/MODULE.bazel": "b499f58a5d0d3537f3cf5b76d8ada18242f64ec474d8391247438bf04f58c7b8",
+ "https://bcr.bazel.build/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350",
+ "https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a",
+ "https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0",
+ "https://bcr.bazel.build/modules/gazelle/0.41.0/MODULE.bazel": "fdce8a8f5129d5b6d693d91cb191d0a014fdcb88e9094e528325a7165de2a826",
+ "https://bcr.bazel.build/modules/gazelle/0.41.0/source.json": "bc00c665344d775b5cae6064608262e0478789c523677d40eef8f85031c6bfda",
+ "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
+ "https://bcr.bazel.build/modules/googleapis-rules-registry/1.0.0/MODULE.bazel": "97c6a4d413b373d4cc97065da3de1b2166e22cbbb5f4cc9f05760bfa83619e24",
+ "https://bcr.bazel.build/modules/googleapis-rules-registry/1.0.0/source.json": "cf611c836a60e98e2e2ab2de8004f119e9f06878dcf4ea2d95a437b1b7a89fe9",
+ "https://bcr.bazel.build/modules/googleapis/0.0.0-20241220-5e258e33.bcr.1/MODULE.bazel": "ee6c30f82ecd476e61f019fb1151aaab380ea419958ff274ef2f0efca7969f5c",
+ "https://bcr.bazel.build/modules/googleapis/0.0.0-20241220-5e258e33.bcr.1/source.json": "d6f66e3d95ec52821e994015e83ed194f8888c655068e192659e55a8987dfe77",
+ "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
+ "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
+ "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
+ "https://bcr.bazel.build/modules/googletest/1.15.2/MODULE.bazel": "6de1edc1d26cafb0ea1a6ab3f4d4192d91a312fd2d360b63adaa213cd00b2108",
+ "https://bcr.bazel.build/modules/googletest/1.17.0/MODULE.bazel": "dbec758171594a705933a29fcf69293d2468c49ec1f2ebca65c36f504d72df46",
+ "https://bcr.bazel.build/modules/googletest/1.17.0/source.json": "38e4454b25fc30f15439c0378e57909ab1fd0a443158aa35aec685da727cd713",
+ "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075",
+ "https://bcr.bazel.build/modules/jsoncpp/1.9.6/MODULE.bazel": "2f8d20d3b7d54143213c4dfc3d98225c42de7d666011528dc8fe91591e2e17b0",
+ "https://bcr.bazel.build/modules/jsoncpp/1.9.6/source.json": "a04756d367a2126c3541682864ecec52f92cdee80a35735a3cb249ce015ca000",
+ "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902",
+ "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74",
+ "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9",
+ "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
+ "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f",
+ "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee",
+ "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37",
+ "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615",
+ "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814",
+ "https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d",
+ "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc",
+ "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580",
+ "https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96",
+ "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
+ "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
+ "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d",
+ "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
+ "https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92",
+ "https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e",
+ "https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95",
+ "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
+ "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573",
+ "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858",
+ "https://bcr.bazel.build/modules/protobuf/32.1/MODULE.bazel": "89cd2866a9cb07fee9ff74c41ceace11554f32e0d849de4e23ac55515cfada4d",
+ "https://bcr.bazel.build/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42",
+ "https://bcr.bazel.build/modules/protobuf/33.4/source.json": "555f8686b4c7d6b5ba731fbea13bf656b4bfd9a7ff629c1d9d3f6e1d6155de79",
+ "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e",
+ "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34",
+ "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/source.json": "6900fdc8a9e95866b8c0d4ad4aba4d4236317b5c1cd04c502df3f0d33afed680",
+ "https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206",
+ "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/MODULE.bazel": "b4963dda9b31080be1905ef085ecd7dd6cd47c05c79b9cdf83ade83ab2ab271a",
+ "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/source.json": "2ff292be6ef3340325ce8a045ecc326e92cbfab47c7cbab4bd85d28971b97ac4",
+ "https://bcr.bazel.build/modules/re2/2024-07-02/MODULE.bazel": "0eadc4395959969297cbcf31a249ff457f2f1d456228c67719480205aa306daa",
+ "https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
+ "https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e",
+ "https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a",
+ "https://bcr.bazel.build/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68",
+ "https://bcr.bazel.build/modules/rules_apple/4.1.0/source.json": "8ee81e1708756f81b343a5eb2b2f0b953f1d25c4ab3d4a68dc02754872e80715",
+ "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
+ "https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
+ "https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191",
+ "https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc",
+ "https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87",
+ "https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a",
+ "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c",
+ "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f",
+ "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
+ "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
+ "https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513",
+ "https://bcr.bazel.build/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0",
+ "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8",
+ "https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c",
+ "https://bcr.bazel.build/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37",
+ "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84",
+ "https://bcr.bazel.build/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07",
+ "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642",
+ "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
+ "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8",
+ "https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8",
+ "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270",
+ "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd",
+ "https://bcr.bazel.build/modules/rules_go/0.50.1/MODULE.bazel": "b91a308dc5782bb0a8021ad4330c81fea5bda77f96b9e4c117b9b9c8f6665ee0",
+ "https://bcr.bazel.build/modules/rules_go/0.60.0/MODULE.bazel": "4a57ff2ffc2a3570e3c5646575c5a4b07287e91bcdac5d1f72383d51502b48cb",
+ "https://bcr.bazel.build/modules/rules_go/0.60.0/source.json": "1e21368c5e0c3013a110bd79a8fcff8ca46b5bcb2b561713a7273cbfcff7c464",
+ "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
+ "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
+ "https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31",
+ "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a",
+ "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6",
+ "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab",
+ "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
+ "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
+ "https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
+ "https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2",
+ "https://bcr.bazel.build/modules/rules_java/9.1.0/MODULE.bazel": "ee63f27e36a3fada80342869361182f120a9819c74320e8e65b1e04ba0cd7a9d",
+ "https://bcr.bazel.build/modules/rules_java/9.1.0/source.json": "da589573c1dee2c9ac4a568b301269a2e8191110ff0345c1a959fa7ea6c4dfd6",
+ "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
+ "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909",
+ "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
+ "https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0",
+ "https://bcr.bazel.build/modules/rules_jvm_external/6.7/MODULE.bazel": "e717beabc4d091ecb2c803c2d341b88590e9116b8bf7947915eeb33aab4f96dd",
+ "https://bcr.bazel.build/modules/rules_jvm_external/6.7/source.json": "5426f412d0a7fc6b611643376c7e4a82dec991491b9ce5cb1cfdd25fe2e92be4",
+ "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
+ "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5",
+ "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
+ "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
+ "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c",
+ "https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb",
+ "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
+ "https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
+ "https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a",
+ "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
+ "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
+ "https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483",
+ "https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f",
+ "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73",
+ "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2",
+ "https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96",
+ "https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e",
+ "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
+ "https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300",
+ "https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382",
+ "https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed",
+ "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58",
+ "https://bcr.bazel.build/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937",
+ "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
+ "https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13",
+ "https://bcr.bazel.build/modules/rules_python/1.4.1/MODULE.bazel": "8991ad45bdc25018301d6b7e1d3626afc3c8af8aaf4bc04f23d0b99c938b73a6",
+ "https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8",
+ "https://bcr.bazel.build/modules/rules_python/1.7.0/MODULE.bazel": "d01f995ecd137abf30238ad9ce97f8fc3ac57289c8b24bd0bf53324d937a14f8",
+ "https://bcr.bazel.build/modules/rules_python/1.7.0/source.json": "028a084b65dcf8f4dc4f82f8778dbe65df133f234b316828a82e060d81bdce32",
+ "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
+ "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b",
+ "https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b",
+ "https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c",
+ "https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca",
+ "https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046",
+ "https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd",
+ "https://bcr.bazel.build/modules/rules_swift/3.1.2/MODULE.bazel": "72c8f5cf9d26427cee6c76c8e3853eb46ce6b0412a081b2b6db6e8ad56267400",
+ "https://bcr.bazel.build/modules/rules_swift/3.1.2/source.json": "e85761f3098a6faf40b8187695e3de6d97944e98abd0d8ce579cb2daf6319a66",
+ "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
+ "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
+ "https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c",
+ "https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
+ "https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
+ "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91",
+ "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f",
+ "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b",
+ "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
+ "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
+ "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
+ "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",
+ "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806",
+ "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198"
+ },
+ "selectedYankedVersions": {},
+ "moduleExtensions": {
+ "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
+ "general": {
+ "bzlTransitiveDigest": "Ga4z8lQy1YQ5rAMy+dOl0dqcCEBnYNCXku8x3YQmDZI=",
+ "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=",
+ "recordedInputs": [
+ "REPO_MAPPING:rules_kotlin+,bazel_tools bazel_tools"
+ ],
+ "generatedRepoSpecs": {
+ "com_github_jetbrains_kotlin_git": {
+ "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_compiler_git_repository",
+ "attributes": {
+ "urls": [
+ "https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip"
+ ],
+ "sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88"
+ }
+ },
+ "com_github_jetbrains_kotlin": {
+ "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_capabilities_repository",
+ "attributes": {
+ "git_repository_name": "com_github_jetbrains_kotlin_git",
+ "compiler_version": "1.9.23"
+ }
+ },
+ "com_github_google_ksp": {
+ "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:ksp.bzl%ksp_compiler_plugin_repository",
+ "attributes": {
+ "urls": [
+ "https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip"
+ ],
+ "sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d",
+ "strip_version": "1.9.23-1.0.20"
+ }
+ },
+ "com_github_pinterest_ktlint": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_file",
+ "attributes": {
+ "sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985",
+ "urls": [
+ "https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint"
+ ],
+ "executable": true
+ }
+ },
+ "rules_android": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806",
+ "strip_prefix": "rules_android-0.1.1",
+ "urls": [
+ "https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "@@rules_python+//python/extensions:config.bzl%config": {
+ "general": {
+ "bzlTransitiveDigest": "iibnRYgg8LpcfmH7EAnVwYePC3jsVaJ6Id8XxUjSZps=",
+ "usagesDigest": "ZVSXMAGpD+xzVNPuvF1IoLBkty7TROO0+akMapt1pAg=",
+ "recordedInputs": [
+ "REPO_MAPPING:rules_python+,bazel_tools bazel_tools",
+ "REPO_MAPPING:rules_python+,pypi__build rules_python++config+pypi__build",
+ "REPO_MAPPING:rules_python+,pypi__click rules_python++config+pypi__click",
+ "REPO_MAPPING:rules_python+,pypi__colorama rules_python++config+pypi__colorama",
+ "REPO_MAPPING:rules_python+,pypi__importlib_metadata rules_python++config+pypi__importlib_metadata",
+ "REPO_MAPPING:rules_python+,pypi__installer rules_python++config+pypi__installer",
+ "REPO_MAPPING:rules_python+,pypi__more_itertools rules_python++config+pypi__more_itertools",
+ "REPO_MAPPING:rules_python+,pypi__packaging rules_python++config+pypi__packaging",
+ "REPO_MAPPING:rules_python+,pypi__pep517 rules_python++config+pypi__pep517",
+ "REPO_MAPPING:rules_python+,pypi__pip rules_python++config+pypi__pip",
+ "REPO_MAPPING:rules_python+,pypi__pip_tools rules_python++config+pypi__pip_tools",
+ "REPO_MAPPING:rules_python+,pypi__pyproject_hooks rules_python++config+pypi__pyproject_hooks",
+ "REPO_MAPPING:rules_python+,pypi__setuptools rules_python++config+pypi__setuptools",
+ "REPO_MAPPING:rules_python+,pypi__tomli rules_python++config+pypi__tomli",
+ "REPO_MAPPING:rules_python+,pypi__wheel rules_python++config+pypi__wheel",
+ "REPO_MAPPING:rules_python+,pypi__zipp rules_python++config+pypi__zipp"
+ ],
+ "generatedRepoSpecs": {
+ "rules_python_internal": {
+ "repoRuleId": "@@rules_python+//python/private:internal_config_repo.bzl%internal_config_repo",
+ "attributes": {
+ "transition_setting_generators": {},
+ "transition_settings": []
+ }
+ },
+ "pypi__build": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/e2/03/f3c8ba0a6b6e30d7d18c40faab90807c9bb5e9a1e3b2fe2008af624a9c97/build-1.2.1-py3-none-any.whl",
+ "sha256": "75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__click": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl",
+ "sha256": "ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__colorama": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl",
+ "sha256": "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__importlib_metadata": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/2d/0a/679461c511447ffaf176567d5c496d1de27cbe34a87df6677d7171b2fbd4/importlib_metadata-7.1.0-py3-none-any.whl",
+ "sha256": "30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__installer": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl",
+ "sha256": "05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__more_itertools": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/50/e2/8e10e465ee3987bb7c9ab69efb91d867d93959095f4807db102d07995d94/more_itertools-10.2.0-py3-none-any.whl",
+ "sha256": "686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__packaging": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl",
+ "sha256": "2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__pep517": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/25/6e/ca4a5434eb0e502210f591b97537d322546e4833dcb4d470a48c375c5540/pep517-0.13.1-py3-none-any.whl",
+ "sha256": "31b206f67165b3536dd577c5c3f1518e8fbaf38cbc57efff8369a392feff1721",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__pip": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl",
+ "sha256": "ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__pip_tools": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/0d/dc/38f4ce065e92c66f058ea7a368a9c5de4e702272b479c0992059f7693941/pip_tools-7.4.1-py3-none-any.whl",
+ "sha256": "4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__pyproject_hooks": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/ae/f3/431b9d5fe7d14af7a32340792ef43b8a714e7726f1d7b69cc4e8e7a3f1d7/pyproject_hooks-1.1.0-py3-none-any.whl",
+ "sha256": "7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__setuptools": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/90/99/158ad0609729111163fc1f674a5a42f2605371a4cf036d0441070e2f7455/setuptools-78.1.1-py3-none-any.whl",
+ "sha256": "c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__tomli": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl",
+ "sha256": "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__wheel": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/7d/cd/d7460c9a869b16c3dd4e1e403cce337df165368c71d6af229a74699622ce/wheel-0.43.0-py3-none-any.whl",
+ "sha256": "55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ },
+ "pypi__zipp": {
+ "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+ "attributes": {
+ "url": "https://files.pythonhosted.org/packages/da/55/a03fd7240714916507e1fcf7ae355bd9d9ed2e6db492595f1a67f61681be/zipp-3.18.2-py3-none-any.whl",
+ "sha256": "dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e",
+ "type": "zip",
+ "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
+ }
+ }
+ }
+ }
+ },
+ "@@rules_python+//python/uv:uv.bzl%uv": {
+ "general": {
+ "bzlTransitiveDigest": "ijW9KS7qsIY+yBVvJ+Nr1mzwQox09j13DnE3iIwaeTM=",
+ "usagesDigest": "H8dQoNZcoqP+Mu0tHZTi4KHATzvNkM5ePuEqoQdklIU=",
+ "recordedInputs": [
+ "REPO_MAPPING:rules_python+,bazel_tools bazel_tools",
+ "REPO_MAPPING:rules_python+,platforms platforms"
+ ],
+ "generatedRepoSpecs": {
+ "uv": {
+ "repoRuleId": "@@rules_python+//python/uv/private:uv_toolchains_repo.bzl%uv_toolchains_repo",
+ "attributes": {
+ "toolchain_type": "'@@rules_python+//python/uv:uv_toolchain_type'",
+ "toolchain_names": [
+ "none"
+ ],
+ "toolchain_implementations": {
+ "none": "'@@rules_python+//python:none'"
+ },
+ "toolchain_compatible_with": {
+ "none": [
+ "@platforms//:incompatible"
+ ]
+ },
+ "toolchain_target_settings": {}
+ }
+ }
+ }
+ }
+ }
+ },
+ "facts": {
+ "@@rules_go+//go:extensions.bzl%go_sdk": {
+ "1.25.0": {
+ "aix_ppc64": [
+ "go1.25.0.aix-ppc64.tar.gz",
+ "e5234a7dac67bc86c528fe9752fc9d63557918627707a733ab4cac1a6faed2d4"
+ ],
+ "darwin_amd64": [
+ "go1.25.0.darwin-amd64.tar.gz",
+ "5bd60e823037062c2307c71e8111809865116714d6f6b410597cf5075dfd80ef"
+ ],
+ "darwin_arm64": [
+ "go1.25.0.darwin-arm64.tar.gz",
+ "544932844156d8172f7a28f77f2ac9c15a23046698b6243f633b0a0b00c0749c"
+ ],
+ "dragonfly_amd64": [
+ "go1.25.0.dragonfly-amd64.tar.gz",
+ "5ed3cf9a810a1483822538674f1336c06b51aa1b94d6d545a1a0319a48177120"
+ ],
+ "freebsd_386": [
+ "go1.25.0.freebsd-386.tar.gz",
+ "abea5d5c6697e6b5c224731f2158fe87c602996a2a233ac0c4730cd57bf8374e"
+ ],
+ "freebsd_amd64": [
+ "go1.25.0.freebsd-amd64.tar.gz",
+ "86e6fe0a29698d7601c4442052dac48bd58d532c51cccb8f1917df648138730b"
+ ],
+ "freebsd_arm": [
+ "go1.25.0.freebsd-arm.tar.gz",
+ "d90b78e41921f72f30e8bbc81d9dec2cff7ff384a33d8d8debb24053e4336bfe"
+ ],
+ "freebsd_arm64": [
+ "go1.25.0.freebsd-arm64.tar.gz",
+ "451d0da1affd886bfb291b7c63a6018527b269505db21ce6e14724f22ab0662e"
+ ],
+ "freebsd_riscv64": [
+ "go1.25.0.freebsd-riscv64.tar.gz",
+ "7b565f76bd8bda46549eeaaefe0e53b251e644c230577290c0f66b1ecdb3cdbe"
+ ],
+ "illumos_amd64": [
+ "go1.25.0.illumos-amd64.tar.gz",
+ "b1e1fdaab1ad25aa1c08d7a36c97d45d74b98b89c3f78c6d2145f77face54a2c"
+ ],
+ "linux_386": [
+ "go1.25.0.linux-386.tar.gz",
+ "8c602dd9d99bc9453b3995d20ce4baf382cc50855900a0ece5de9929df4a993a"
+ ],
+ "linux_amd64": [
+ "go1.25.0.linux-amd64.tar.gz",
+ "2852af0cb20a13139b3448992e69b868e50ed0f8a1e5940ee1de9e19a123b613"
+ ],
+ "linux_arm64": [
+ "go1.25.0.linux-arm64.tar.gz",
+ "05de75d6994a2783699815ee553bd5a9327d8b79991de36e38b66862782f54ae"
+ ],
+ "linux_armv6l": [
+ "go1.25.0.linux-armv6l.tar.gz",
+ "a5a8f8198fcf00e1e485b8ecef9ee020778bf32a408a4e8873371bfce458cd09"
+ ],
+ "linux_loong64": [
+ "go1.25.0.linux-loong64.tar.gz",
+ "cab86b1cf761b1cb3bac86a8877cfc92e7b036fc0d3084123d77013d61432afc"
+ ],
+ "linux_mips": [
+ "go1.25.0.linux-mips.tar.gz",
+ "d66b6fb74c3d91b9829dc95ec10ca1f047ef5e89332152f92e136cf0e2da5be1"
+ ],
+ "linux_mips64": [
+ "go1.25.0.linux-mips64.tar.gz",
+ "4082e4381a8661bc2a839ff94ba3daf4f6cde20f8fb771b5b3d4762dc84198a2"
+ ],
+ "linux_mips64le": [
+ "go1.25.0.linux-mips64le.tar.gz",
+ "70002c299ec7f7175ac2ef673b1b347eecfa54ae11f34416a6053c17f855afcc"
+ ],
+ "linux_mipsle": [
+ "go1.25.0.linux-mipsle.tar.gz",
+ "b00a3a39eff099f6df9f1c7355bf28e4589d0586f42d7d4a394efb763d145a73"
+ ],
+ "linux_ppc64": [
+ "go1.25.0.linux-ppc64.tar.gz",
+ "df166f33bd98160662560a72ff0b4ba731f969a80f088922bddcf566a88c1ec1"
+ ],
+ "linux_ppc64le": [
+ "go1.25.0.linux-ppc64le.tar.gz",
+ "0f18a89e7576cf2c5fa0b487a1635d9bcbf843df5f110e9982c64df52a983ad0"
+ ],
+ "linux_riscv64": [
+ "go1.25.0.linux-riscv64.tar.gz",
+ "c018ff74a2c48d55c8ca9b07c8e24163558ffec8bea08b326d6336905d956b67"
+ ],
+ "linux_s390x": [
+ "go1.25.0.linux-s390x.tar.gz",
+ "34e5a2e19f2292fbaf8783e3a241e6e49689276aef6510a8060ea5ef54eee408"
+ ],
+ "netbsd_386": [
+ "go1.25.0.netbsd-386.tar.gz",
+ "f8586cdb7aa855657609a5c5f6dbf523efa00c2bbd7c76d3936bec80aa6c0aba"
+ ],
+ "netbsd_amd64": [
+ "go1.25.0.netbsd-amd64.tar.gz",
+ "ae8dc1469385b86a157a423bb56304ba45730de8a897615874f57dd096db2c2a"
+ ],
+ "netbsd_arm": [
+ "go1.25.0.netbsd-arm.tar.gz",
+ "1ff7e4cc764425fc9dd6825eaee79d02b3c7cafffbb3691687c8d672ade76cb7"
+ ],
+ "netbsd_arm64": [
+ "go1.25.0.netbsd-arm64.tar.gz",
+ "e1b310739f26724216aa6d7d7208c4031f9ff54c9b5b9a796ddc8bebcb4a5f16"
+ ],
+ "openbsd_386": [
+ "go1.25.0.openbsd-386.tar.gz",
+ "4802a9b20e533da91adb84aab42e94aa56cfe3e5475d0550bed3385b182e69d8"
+ ],
+ "openbsd_amd64": [
+ "go1.25.0.openbsd-amd64.tar.gz",
+ "c016cd984bebe317b19a4f297c4f50def120dc9788490540c89f28e42f1dabe1"
+ ],
+ "openbsd_arm": [
+ "go1.25.0.openbsd-arm.tar.gz",
+ "a1e31d0bf22172ddde42edf5ec811ef81be43433df0948ece52fecb247ccfd8d"
+ ],
+ "openbsd_arm64": [
+ "go1.25.0.openbsd-arm64.tar.gz",
+ "343ea8edd8c218196e15a859c6072d0dd3246fbbb168481ab665eb4c4140458d"
+ ],
+ "openbsd_ppc64": [
+ "go1.25.0.openbsd-ppc64.tar.gz",
+ "694c14da1bcaeb5e3332d49bdc2b6d155067648f8fe1540c5de8f3cf8e157154"
+ ],
+ "openbsd_riscv64": [
+ "go1.25.0.openbsd-riscv64.tar.gz",
+ "aa510ad25cf54c06cd9c70b6d80ded69cb20188ac6e1735655eef29ff7e7885f"
+ ],
+ "plan9_386": [
+ "go1.25.0.plan9-386.tar.gz",
+ "46f8cef02086cf04bf186c5912776b56535178d4cb319cd19c9fdbdd29231986"
+ ],
+ "plan9_amd64": [
+ "go1.25.0.plan9-amd64.tar.gz",
+ "29b34391d84095e44608a228f63f2f88113a37b74a79781353ec043dfbcb427b"
+ ],
+ "plan9_arm": [
+ "go1.25.0.plan9-arm.tar.gz",
+ "0a047107d13ebe7943aaa6d54b1d7bbd2e45e68ce449b52915a818da715799c2"
+ ],
+ "solaris_amd64": [
+ "go1.25.0.solaris-amd64.tar.gz",
+ "9977f9e4351984364a3b2b78f8b88bfd1d339812356d5237678514594b7d3611"
+ ],
+ "windows_386": [
+ "go1.25.0.windows-386.zip",
+ "df9f39db82a803af0db639e3613a36681ab7a42866b1384b3f3a1045663961a7"
+ ],
+ "windows_amd64": [
+ "go1.25.0.windows-amd64.zip",
+ "89efb4f9b30812eee083cc1770fdd2913c14d301064f6454851428f9707d190b"
+ ],
+ "windows_arm64": [
+ "go1.25.0.windows-arm64.zip",
+ "27bab004c72b3d7bd05a69b6ec0fc54a309b4b78cc569dd963d8b3ec28bfdb8c"
+ ]
+ }
+ }
+ }
+}
diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..26a0ba7
--- /dev/null
+++ b/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,36 @@
+# Pull Requests Guidelines
+
+See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details about when to create
+a GitHub [Pull Request][1] and when other kinds of contributions or
+consultation might be more desirable.
+
+When creating a new pull request, please fork the repo and work within a
+development branch.
+
+## Commit Messages
+
+* Most changes should be accompanied by tests.
+* Commit messages should explain _why_ the changes were made.
+```
+Summary of change in 50 characters or less
+
+Background on why the change is being made with additional detail on
+consequences of the changes elsewhere in the code or to the general
+functionality of the library. Multiple paragraphs may be used, but
+please keep lines to 72 characters or less.
+```
+
+## Reviews
+
+* Perform a self-review.
+* Assign a reviewer once both the above have been completed.
+
+## Merging
+
+* If a CEL maintaner approves the change, it may be merged by the author if
+ they have write access. Otherwise, the change will be merged by a maintainer.
+* Multiple commits should be squashed before merging.
+* Please append the line `closes #: description` in the merge message,
+ if applicable.
+
+[1]: https://help.github.com/articles/about-pull-requests
diff --git a/README.md b/README.md
index 3c0b0b0..6199dda 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,17 @@
-Coming soon
+# CEL Skills
+
+Collection of skills and associated MCP server for working with CEL (Common
+Expression Language).
+
+The Gemini CLI looks for skills in the `.agents/skills` (or `_agent/skills`)
+directory relative to your workspace root.
+
+Once configured, you can invoke the skills by their name. For example, to test the authoring skill, you can ask Gemini CLI:
+
+```
+/cel:cel-authoring create a policy that checks if a user's age is
+over 18.
+```
+
+The agent will then follow the updated workflow in your SKILL.md, calling
+`cel_create_environment`, `cel_generate_prompt`, and `cel_compile` as needed.
diff --git a/cmd/mcp/BUILD b/cmd/mcp/BUILD
new file mode 100644
index 0000000..1e0e526
--- /dev/null
+++ b/cmd/mcp/BUILD
@@ -0,0 +1,27 @@
+load("@rules_go//go:def.bzl", "go_binary")
+load("@rules_go//go:def.bzl", "go_test")
+
+package(
+ default_visibility = ["//visibility:public"],
+)
+
+go_binary(
+ name = "mcp",
+ srcs = ["main.go"],
+ deps = [
+ "//internal/tools",
+ "@com_github_modelcontextprotocol_go_sdk//mcp",
+ ],
+)
+
+go_test(
+ name = "mcp_test",
+ srcs = [
+ "main.go",
+ "main_test.go",
+ ],
+ deps = [
+ "//internal/tools",
+ "@com_github_modelcontextprotocol_go_sdk//mcp",
+ ],
+)
diff --git a/cmd/mcp/main.go b/cmd/mcp/main.go
new file mode 100644
index 0000000..f3a8de8
--- /dev/null
+++ b/cmd/mcp/main.go
@@ -0,0 +1,134 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package main is the main package for the CEL MCP.
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+
+ "github.com/cel-expr/skills/internal/tools"
+ "github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+func main() {
+ s := newServer()
+
+ if err := s.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
+ fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
+ os.Exit(1)
+ }
+}
+
+func newServer() *mcp.Server {
+ s := mcp.NewServer(&mcp.Implementation{
+ Name: "cel-mcp",
+ Version: "1.0.0",
+ }, &mcp.ServerOptions{
+ SchemaCache: mcp.NewSchemaCache(),
+ })
+
+ mcp.AddTool[CreateEnvConfigArgs, any](s, &mcp.Tool{
+ Name: "cel_create_environment",
+ Description: "Creates a CEL environment configuration from a JSON object.",
+ }, handleCreateEnvConfig)
+ mcp.AddTool[GeneratePromptArgs, any](s, &mcp.Tool{
+ Name: "cel_generate_prompt",
+ Description: "Generates an LLM authoring prompt explaining the exact variables, functions, and types available in the CEL environment.",
+ }, handleGeneratePrompt)
+ mcp.AddTool[CompileArgs, any](s, &mcp.Tool{
+ Name: "cel_compile",
+ Description: "Compiles a CEL expression using a JSON environment configuration. Returns the expression's input and output JSON schemas if it compiles successfully.",
+ }, handleCompile)
+ mcp.AddTool[EvaluateArgs, any](s, &mcp.Tool{
+ Name: "cel_evaluate",
+ Description: "Evaluates a CEL expression against provided test cases. Returns test case results and coverage.",
+ }, handleEvaluate)
+
+ return s
+}
+
+// CreateEnvConfigArgs is the arguments for the cel_create_environment tool.
+type CreateEnvConfigArgs struct {
+ // EnvConfig is the JSON string representing the CEL environment configuration.
+ EnvConfig *tools.Config `json:"envConfig" jsonschema_description:"The JSON string representing the CEL environment configuration."`
+}
+
+// GeneratePromptArgs is the arguments for the cel_generate_prompt tool.
+type GeneratePromptArgs struct {
+ // EnvConfig is the JSON string representing the CEL environment configuration.
+ EnvConfig *tools.Config `json:"envConfig" jsonschema_description:"The JSON string representing the CEL environment schema."`
+
+ // UserPrompt is the user prompt to generate the CEL expression for.
+ UserPrompt string `json:"userPrompt" jsonschema_description:"The user prompt to generate the CEL expression for."`
+}
+
+// CompileArgs is the arguments for the cel_compile tool.
+type CompileArgs struct {
+ // EnvConfig is the JSON string representing the CEL environment configuration.
+ EnvConfig *tools.Config `json:"envConfig" jsonschema_description:"The JSON string representing the CEL environment schema."`
+
+ // Expr is the CEL expression to compile.
+ Expr string `json:"expr" jsonschema_description:"The CEL expression to compile."`
+}
+
+// EvaluateArgs is the arguments for the cel_evaluate tool.
+type EvaluateArgs struct {
+ // EnvConfig is the JSON string representing the CEL environment configuration.
+ EnvConfig *tools.Config `json:"envConfig" jsonschema_description:"The JSON string representing the CEL environment schema."`
+
+ // Expr is the CEL expression to evaluate.
+ Expr string `json:"expr" jsonschema_description:"The CEL expression to evaluate."`
+
+ // TestCases is the test cases for evaluation.
+ TestCases []tools.TestCase `json:"testCases" jsonschema_description:"The test cases for evaluation."`
+}
+
+func handleCreateEnvConfig(ctx context.Context, request *mcp.CallToolRequest, args CreateEnvConfigArgs) (*mcp.CallToolResult, any, error) {
+ _, err := tools.EnvFromConfig(args.EnvConfig)
+ if err != nil {
+ return nil, nil, err
+ }
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{&mcp.TextContent{Text: "Environment created successfully"}},
+ }, nil, nil
+}
+
+func handleCompile(ctx context.Context, request *mcp.CallToolRequest, args CompileArgs) (*mcp.CallToolResult, any, error) {
+ res, err := tools.CompileCEL(args.Expr, args.EnvConfig)
+ if err != nil {
+ return nil, nil, err
+ }
+ return nil, res, nil
+}
+
+func handleEvaluate(ctx context.Context, request *mcp.CallToolRequest, args EvaluateArgs) (*mcp.CallToolResult, any, error) {
+ res, err := tools.EvaluateCEL(args.Expr, args.EnvConfig, args.TestCases)
+ if err != nil {
+ return nil, nil, err
+ }
+ return nil, res, nil
+}
+
+func handleGeneratePrompt(ctx context.Context, request *mcp.CallToolRequest, args GeneratePromptArgs) (*mcp.CallToolResult, any, error) {
+ res, err := tools.GeneratePrompt(args.EnvConfig, args.UserPrompt)
+ if err != nil {
+ return nil, nil, err
+ }
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{&mcp.TextContent{Text: res}},
+ }, nil, nil
+}
diff --git a/cmd/mcp/main_test.go b/cmd/mcp/main_test.go
new file mode 100644
index 0000000..e099291
--- /dev/null
+++ b/cmd/mcp/main_test.go
@@ -0,0 +1,252 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "context"
+ "testing"
+
+ "github.com/cel-expr/skills/internal/tools"
+ "github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+func TestListTools(t *testing.T) {
+ ctx := context.Background()
+ s := newServer()
+
+ // Connect the server and client using in-memory transports.
+ t1, t2 := mcp.NewInMemoryTransports()
+ if _, err := s.Connect(ctx, t1, nil); err != nil {
+ t.Fatalf("Connect failed: %v", err)
+ }
+
+ client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "1.0.0"}, nil)
+ session, err := client.Connect(ctx, t2, nil)
+ if err != nil {
+ t.Fatalf("client.Connect failed: %v", err)
+ }
+ defer session.Close()
+
+ resp := session.Tools(ctx, nil)
+
+ expectedTools := map[string]bool{
+ "cel_compile": true,
+ "cel_evaluate": true,
+ "cel_generate_prompt": true,
+ "cel_create_environment": true,
+ }
+
+ foundTools := make(map[string]bool)
+ for tool, err := range resp {
+ if err != nil {
+ t.Fatalf("iteration failed: %v", err)
+ }
+ foundTools[tool.Name] = true
+ }
+
+ for name := range expectedTools {
+ if !foundTools[name] {
+ t.Errorf("expected tool %s not found in ListTools response", name)
+ }
+ }
+}
+
+type EvaluationResults struct {
+ TestCase string `json:"testCase"`
+ Status string `json:"status"`
+}
+
+type EvaluateExprOutputSchema struct {
+ EvaluationResults []EvaluationResults `json:"evaluationResults"`
+ Coverage string `json:"coverage"`
+}
+
+func TestHandleCreateEnvConfig(t *testing.T) {
+ ctx := context.Background()
+
+ args := CreateEnvConfigArgs{
+ EnvConfig: &tools.Config{
+ Variables: []*tools.Variable{
+ {Name: "foo", Type: "string"},
+ },
+ },
+ }
+
+ res, _, err := handleCreateEnvConfig(ctx, &mcp.CallToolRequest{}, args)
+ if err != nil {
+ t.Fatalf("handleCreateEnvConfig failed: %v", err)
+ }
+
+ if res.IsError {
+ t.Errorf("expected success, got error: %v", res.Content[0])
+ }
+}
+
+func TestHandleCompile(t *testing.T) {
+ ctx := context.Background()
+
+ args := CompileArgs{
+ EnvConfig: &tools.Config{
+ Variables: []*tools.Variable{
+ {Name: "foo", Type: "string"},
+ },
+ },
+ Expr: "foo == 'bar'",
+ }
+
+ res, out, err := handleCompile(ctx, &mcp.CallToolRequest{}, args)
+ if err != nil {
+ t.Fatalf("handleCompile failed: %v", err)
+ }
+
+ if res != nil && res.IsError {
+ t.Errorf("expected success, got error: %v", res.Content[0])
+ }
+
+ if out == nil {
+ t.Fatal("expected output, got nil")
+ }
+}
+
+func TestHandleEvaluate(t *testing.T) {
+ ctx := context.Background()
+
+ args := EvaluateArgs{
+ EnvConfig: &tools.Config{
+ Variables: []*tools.Variable{
+ {Name: "foo", Type: "string"},
+ },
+ },
+ Expr: "foo",
+ TestCases: []tools.TestCase{
+ {
+ TestCase: "happy path",
+ Bindings: map[string]any{"foo": "bar"},
+ Expected: "bar",
+ },
+ },
+ }
+
+ res, out, err := handleEvaluate(ctx, &mcp.CallToolRequest{}, args)
+ if err != nil {
+ t.Fatalf("handleEvaluate failed: %v", err)
+ }
+
+ if res != nil && res.IsError {
+ t.Errorf("expected success, got error: %v", res.Content[0])
+ }
+
+ if out == nil {
+ t.Fatal("expected output, got nil")
+ }
+
+ tr := out.(*tools.EvaluateExprOutput)
+ if len(tr.TestResults) == 0 {
+ t.Fatal("expected evaluation results, got none")
+ }
+
+ if tr.TestResults[0].Status != "pass" {
+ t.Errorf("expected 'pass', got '%s'", tr.TestResults[0].Status)
+ }
+}
+
+func TestHandleGeneratePrompt(t *testing.T) {
+ ctx := context.Background()
+
+ args := GeneratePromptArgs{
+ EnvConfig: &tools.Config{
+ Variables: []*tools.Variable{
+ {Name: "foo", Type: "string"},
+ },
+ },
+ UserPrompt: "create a rule that checks if foo is 'bar'",
+ }
+
+ res, _, err := handleGeneratePrompt(ctx, &mcp.CallToolRequest{}, args)
+ if err != nil {
+ t.Fatalf("handleGeneratePrompt failed: %v", err)
+ }
+
+ if res.IsError {
+ t.Errorf("expected success, got error: %v", res.Content[0])
+ }
+
+ if len(res.Content) == 0 {
+ t.Error("expected non-empty output content")
+ }
+}
+
+func TestHandleEvaluateUserAge(t *testing.T) {
+ ctx := context.Background()
+
+ args := EvaluateArgs{
+ EnvConfig: &tools.Config{
+ Variables: []*tools.Variable{
+ {Name: "user.age", Type: "int"},
+ },
+ },
+ Expr: "// Check if user age is over 18\nuser.age > 18",
+ TestCases: []tools.TestCase{
+ {
+ TestCase: "Age is 19",
+ Bindings: map[string]any{"user.age": 19},
+ Expected: true,
+ },
+ {
+ TestCase: "Age is exactly 18",
+ Bindings: map[string]any{"user.age": 18},
+ Expected: false,
+ },
+ {
+ TestCase: "Age is under 18",
+ Bindings: map[string]any{"user.age": 17},
+ Expected: false,
+ },
+ },
+ }
+
+ res, out, err := handleEvaluate(ctx, &mcp.CallToolRequest{}, args)
+ if err != nil {
+ t.Fatalf("handleEvaluate failed: %v", err)
+ }
+
+ if res != nil && res.IsError {
+ t.Errorf("expected success, got error: %v", res.Content[0])
+ }
+
+ if out == nil {
+ t.Fatal("expected output, got nil")
+ }
+
+ expectedStatusFound := map[string]bool{
+ "Age is 19": false,
+ "Age is exactly 18": false,
+ "Age is under 18": false,
+ }
+
+ tr := out.(*tools.EvaluateExprOutput)
+ for _, result := range tr.TestResults {
+ if result.Status != "pass" {
+ t.Errorf("test case '%s' failed: %s", result.TestCase, result.Status)
+ }
+ expectedStatusFound[result.TestCase] = true
+ }
+
+ for tc, found := range expectedStatusFound {
+ if !found {
+ t.Errorf("expected test case result for '%s' not found", tc)
+ }
+ }
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..542ffe5
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,25 @@
+module github.com/cel-expr/skills
+
+go 1.25.0
+
+require (
+ github.com/google/cel-go v0.28.1
+ github.com/modelcontextprotocol/go-sdk v1.6.0
+ google.golang.org/protobuf v1.36.10
+)
+
+require (
+ cel.dev/expr v0.25.1 // indirect
+ github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
+ github.com/google/jsonschema-go v0.4.3 // indirect
+ github.com/segmentio/asm v1.1.3 // indirect
+ github.com/segmentio/encoding v0.5.4 // indirect
+ github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
+ go.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
+ golang.org/x/oauth2 v0.35.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/text v0.22.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..d81a9a4
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,40 @@
+cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
+cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
+github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
+github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
+github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
+github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM=
+github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
+github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
+github.com/modelcontextprotocol/go-sdk v1.6.0 h1:PPLS3kn7WtOEnR+Af4X5H96SG0qSab8R/ZQT/HkhPkY=
+github.com/modelcontextprotocol/go-sdk v1.6.0/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
+github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
+github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
+github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
+github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
+golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
+golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
+golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
+golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
+google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw=
+google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
+google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/proto/BUILD b/internal/proto/BUILD
new file mode 100644
index 0000000..2741017
--- /dev/null
+++ b/internal/proto/BUILD
@@ -0,0 +1,28 @@
+load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library")
+load("@rules_go//proto:def.bzl", "go_proto_library")
+load("@rules_go//go:def.bzl", "go_library")
+
+package(
+ default_visibility = ["//visibility:public"],
+)
+
+proto_library(
+ name = "test_schema_proto",
+ srcs = ["test_schema.proto"],
+)
+
+go_proto_library(
+ name = "test_schema_go_proto",
+ proto = ":test_schema_proto",
+)
+
+go_library(
+ name = "proto",
+ srcs = ["test_schema.pb.go"],
+ importpath = "github.com/cel-expr/skills/internal/proto",
+ deps = [
+ "@org_golang_google_protobuf//proto:go_default_library",
+ "@org_golang_google_protobuf//reflect/protoreflect:go_default_library",
+ "@org_golang_google_protobuf//runtime/protoimpl:go_default_library",
+ ]
+)
diff --git a/internal/proto/test_schema.pb.go b/internal/proto/test_schema.pb.go
new file mode 100755
index 0000000..1e0d08c
--- /dev/null
+++ b/internal/proto/test_schema.pb.go
@@ -0,0 +1,245 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.10
+// protoc v6.33.4
+// source: internal/proto/test_schema.proto
+
+package proto
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type TestMessage struct {
+ state protoimpl.MessageState `protogen:"opaque.v1"`
+ xxx_hidden_SingleNestedMessage *TestMessage_NestedMessage `protobuf:"bytes,1,opt,name=single_nested_message,json=singleNestedMessage"`
+ xxx_hidden_SingleInt32 int32 `protobuf:"varint,2,opt,name=single_int32,json=singleInt32"`
+ XXX_raceDetectHookData protoimpl.RaceDetectHookData
+ XXX_presence [1]uint32
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TestMessage) Reset() {
+ *x = TestMessage{}
+ mi := &file_internal_proto_test_schema_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TestMessage) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TestMessage) ProtoMessage() {}
+
+func (x *TestMessage) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_proto_test_schema_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+func (x *TestMessage) GetSingleNestedMessage() *TestMessage_NestedMessage {
+ if x != nil {
+ return x.xxx_hidden_SingleNestedMessage
+ }
+ return nil
+}
+
+func (x *TestMessage) GetSingleInt32() int32 {
+ if x != nil {
+ return x.xxx_hidden_SingleInt32
+ }
+ return 0
+}
+
+func (x *TestMessage) SetSingleNestedMessage(v *TestMessage_NestedMessage) {
+ x.xxx_hidden_SingleNestedMessage = v
+}
+
+func (x *TestMessage) SetSingleInt32(v int32) {
+ x.xxx_hidden_SingleInt32 = v
+ protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 2)
+}
+
+func (x *TestMessage) HasSingleNestedMessage() bool {
+ if x == nil {
+ return false
+ }
+ return x.xxx_hidden_SingleNestedMessage != nil
+}
+
+func (x *TestMessage) HasSingleInt32() bool {
+ if x == nil {
+ return false
+ }
+ return protoimpl.X.Present(&(x.XXX_presence[0]), 1)
+}
+
+func (x *TestMessage) ClearSingleNestedMessage() {
+ x.xxx_hidden_SingleNestedMessage = nil
+}
+
+func (x *TestMessage) ClearSingleInt32() {
+ protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1)
+ x.xxx_hidden_SingleInt32 = 0
+}
+
+type TestMessage_builder struct {
+ _ [0]func() // Prevents comparability and use of unkeyed literals for the builder.
+
+ SingleNestedMessage *TestMessage_NestedMessage
+ SingleInt32 *int32
+}
+
+func (b0 TestMessage_builder) Build() *TestMessage {
+ m0 := &TestMessage{}
+ b, x := &b0, m0
+ _, _ = b, x
+ x.xxx_hidden_SingleNestedMessage = b.SingleNestedMessage
+ if b.SingleInt32 != nil {
+ protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 2)
+ x.xxx_hidden_SingleInt32 = *b.SingleInt32
+ }
+ return m0
+}
+
+type TestMessage_NestedMessage struct {
+ state protoimpl.MessageState `protogen:"opaque.v1"`
+ xxx_hidden_Bb int64 `protobuf:"varint,1,opt,name=bb"`
+ XXX_raceDetectHookData protoimpl.RaceDetectHookData
+ XXX_presence [1]uint32
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TestMessage_NestedMessage) Reset() {
+ *x = TestMessage_NestedMessage{}
+ mi := &file_internal_proto_test_schema_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TestMessage_NestedMessage) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TestMessage_NestedMessage) ProtoMessage() {}
+
+func (x *TestMessage_NestedMessage) ProtoReflect() protoreflect.Message {
+ mi := &file_internal_proto_test_schema_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+func (x *TestMessage_NestedMessage) GetBb() int64 {
+ if x != nil {
+ return x.xxx_hidden_Bb
+ }
+ return 0
+}
+
+func (x *TestMessage_NestedMessage) SetBb(v int64) {
+ x.xxx_hidden_Bb = v
+ protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 1)
+}
+
+func (x *TestMessage_NestedMessage) HasBb() bool {
+ if x == nil {
+ return false
+ }
+ return protoimpl.X.Present(&(x.XXX_presence[0]), 0)
+}
+
+func (x *TestMessage_NestedMessage) ClearBb() {
+ protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0)
+ x.xxx_hidden_Bb = 0
+}
+
+type TestMessage_NestedMessage_builder struct {
+ _ [0]func() // Prevents comparability and use of unkeyed literals for the builder.
+
+ Bb *int64
+}
+
+func (b0 TestMessage_NestedMessage_builder) Build() *TestMessage_NestedMessage {
+ m0 := &TestMessage_NestedMessage{}
+ b, x := &b0, m0
+ _, _ = b, x
+ if b.Bb != nil {
+ protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 1)
+ x.xxx_hidden_Bb = *b.Bb
+ }
+ return m0
+}
+
+var File_internal_proto_test_schema_proto protoreflect.FileDescriptor
+
+const file_internal_proto_test_schema_proto_rawDesc = "" +
+ "\n" +
+ " internal/proto/test_schema.proto\x12\x19cel.skills.internal.proto\"\xbb\x01\n" +
+ "\vTestMessage\x12h\n" +
+ "\x15single_nested_message\x18\x01 \x01(\v24.cel.skills.internal.proto.TestMessage.NestedMessageR\x13singleNestedMessage\x12!\n" +
+ "\fsingle_int32\x18\x02 \x01(\x05R\vsingleInt32\x1a\x1f\n" +
+ "\rNestedMessage\x12\x0e\n" +
+ "\x02bb\x18\x01 \x01(\x03R\x02bbB5Z3google3/third_party/cel/skills/internal/proto;protob\beditionsp\xe9\a"
+
+var file_internal_proto_test_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_internal_proto_test_schema_proto_goTypes = []any{
+ (*TestMessage)(nil), // 0: cel.skills.internal.proto.TestMessage
+ (*TestMessage_NestedMessage)(nil), // 1: cel.skills.internal.proto.TestMessage.NestedMessage
+}
+var file_internal_proto_test_schema_proto_depIdxs = []int32{
+ 1, // 0: cel.skills.internal.proto.TestMessage.single_nested_message:type_name -> cel.skills.internal.proto.TestMessage.NestedMessage
+ 1, // [1:1] is the sub-list for method output_type
+ 1, // [1:1] is the sub-list for method input_type
+ 1, // [1:1] is the sub-list for extension type_name
+ 1, // [1:1] is the sub-list for extension extendee
+ 0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_internal_proto_test_schema_proto_init() }
+func file_internal_proto_test_schema_proto_init() {
+ if File_internal_proto_test_schema_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_proto_test_schema_proto_rawDesc), len(file_internal_proto_test_schema_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_internal_proto_test_schema_proto_goTypes,
+ DependencyIndexes: file_internal_proto_test_schema_proto_depIdxs,
+ MessageInfos: file_internal_proto_test_schema_proto_msgTypes,
+ }.Build()
+ File_internal_proto_test_schema_proto = out.File
+ file_internal_proto_test_schema_proto_goTypes = nil
+ file_internal_proto_test_schema_proto_depIdxs = nil
+}
diff --git a/internal/proto/test_schema.proto b/internal/proto/test_schema.proto
new file mode 100644
index 0000000..937ea16
--- /dev/null
+++ b/internal/proto/test_schema.proto
@@ -0,0 +1,27 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+edition = "2024";
+
+package cel.skills.internal.proto;
+
+option go_package = "github.com/cel-expr/skills/internal/proto;testpb"
+
+message TestMessage {
+ message NestedMessage {
+ int64 bb = 1;
+ }
+ NestedMessage single_nested_message = 1;
+ int32 single_int32 = 2;
+}
diff --git a/internal/tools/BUILD b/internal/tools/BUILD
new file mode 100644
index 0000000..43a9f95
--- /dev/null
+++ b/internal/tools/BUILD
@@ -0,0 +1,47 @@
+load("@rules_go//go:def.bzl", "go_test")
+load("@rules_go//go:def.bzl", "go_library")
+
+package(
+ default_visibility = ["//visibility:public"],
+)
+
+go_library(
+ name = "tools",
+ srcs = [
+ "compile.go",
+ "config.go",
+ "coverage.go",
+ "evaluate.go",
+ "input_schema.go",
+ "prompt.go",
+ ],
+ importpath = "github.com/cel-expr/skills/internal/tools",
+ deps = [
+ "@com_github_google_cel_go//cel",
+ "@com_github_google_cel_go//common/ast",
+ "@com_github_google_cel_go//common/env",
+ "@com_github_google_cel_go//common/types",
+ "@com_github_google_cel_go//common/types/ref",
+ "@com_github_google_cel_go//ext",
+ "@org_golang_google_protobuf//encoding/protojson",
+ "@org_golang_google_protobuf//proto",
+ ],
+)
+
+go_test(
+ name = "tools_test",
+ srcs = [
+ "compile_test.go",
+ "config_test.go",
+ "evaluate_test.go",
+ "input_schema_test.go",
+ "prompt_test.go",
+ ],
+ data = glob(["testdata/**"]),
+ embed = [":tools"],
+ deps = [
+ "@com_github_google_cel_go//cel",
+ "@com_github_google_cel_go//common/env",
+ "//internal/proto",
+ ],
+)
diff --git a/internal/tools/compile.go b/internal/tools/compile.go
new file mode 100644
index 0000000..655a2d7
--- /dev/null
+++ b/internal/tools/compile.go
@@ -0,0 +1,45 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tools
+
+import (
+ "fmt"
+)
+
+// CompileExprOutput is the output of a CEL compilation.
+type CompileExprOutput struct {
+ InputSchema any `json:"inputSchema"`
+ OutputSchema any `json:"outputSchema"`
+}
+
+// CompileCEL compiles a CEL expression against the provided JSON environment schema.
+func CompileCEL(expr string, envConfig *Config) (*CompileExprOutput, error) {
+ env, err := EnvFromConfig(envConfig)
+ if err != nil {
+ return nil, fmt.Errorf("failed constructing env: %w", err)
+ }
+ ast, iss := env.Compile(expr)
+ if iss.Err() != nil {
+ return nil, fmt.Errorf("compile error: %w", iss.Err())
+ }
+ schema, err := ComputeInputSchema(env, ast)
+ if err != nil {
+ return nil, fmt.Errorf("failed computing references: %w", err)
+ }
+ return &CompileExprOutput{
+ InputSchema: schema,
+ OutputSchema: SchemaFromCELType(env, ast.OutputType()),
+ }, nil
+}
diff --git a/internal/tools/compile_test.go b/internal/tools/compile_test.go
new file mode 100644
index 0000000..2121ff1
--- /dev/null
+++ b/internal/tools/compile_test.go
@@ -0,0 +1,79 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tools
+
+import (
+ "testing"
+)
+
+func TestCompileCEL(t *testing.T) {
+ envConfig := &Config{
+ Name: "basic",
+ Variables: []*Variable{
+ {Name: "user", Type: "string"},
+ {Name: "age", Type: "int"},
+ },
+ }
+
+ tests := []struct {
+ name string
+ expr string
+ envConfig *Config
+ wantErr bool
+ }{
+ {
+ name: "valid expression",
+ expr: `user == "Alice" && age > 18`,
+ envConfig: envConfig,
+ wantErr: false,
+ },
+ {
+ name: "invalid expression syntax",
+ expr: `user == `,
+ envConfig: envConfig,
+ wantErr: true,
+ },
+ {
+ name: "failed constructing env",
+ expr: `user == "Alice"`,
+ envConfig: &Config{Variables: []*Variable{{Name: "a", Type: "invalid"}}}, // invalid type
+ wantErr: true,
+ },
+ {
+ name: "compile error",
+ expr: `invalid_var == "Alice"`,
+ envConfig: envConfig,
+ wantErr: true,
+ },
+ {
+ name: "another failed constructing env (duplicate variable)",
+ expr: `user == "Alice"`,
+ envConfig: &Config{Variables: []*Variable{{Name: "user", Type: "string"}, {Name: "user", Type: "int"}}}, // duplicate variable
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := CompileCEL(tt.expr, tt.envConfig)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("CompileCEL() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !tt.wantErr && got == nil {
+ t.Errorf("CompileCEL() returned nil result without error")
+ }
+ })
+ }
+}
diff --git a/internal/tools/config.go b/internal/tools/config.go
new file mode 100644
index 0000000..e0f2d70
--- /dev/null
+++ b/internal/tools/config.go
@@ -0,0 +1,324 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package tools provides the implementation of the cel-skills library.
+package tools
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/google/cel-go/cel"
+ celenv "github.com/google/cel-go/common/env"
+ celext "github.com/google/cel-go/ext"
+)
+
+// Config is a configuration for a cel-go Env.
+type Config struct {
+ Name string `json:"name,omitempty"`
+ Description string `json:"description,omitempty"`
+ Container string `json:"container,omitempty"`
+ Imports []*Import `json:"imports,omitempty"`
+ StdLib *LibrarySubset `json:"stdlib,omitempty"`
+ Extensions []*Extension `json:"extensions,omitempty"`
+ ContextVariable *ContextVariable `json:"contextVariable,omitempty"`
+ Variables []*Variable `json:"variables,omitempty"`
+ Functions []*Function `json:"functions,omitempty"`
+ Validators []*Validator `json:"validators,omitempty"`
+ Features []*Feature `json:"features,omitempty"`
+}
+
+// ToCELConfig converts a Config to a celenv.Config.
+func (c *Config) ToCELConfig() (*celenv.Config, error) {
+ if c == nil {
+ return nil, nil
+ }
+ res := celenv.NewConfig(c.Name)
+ res.Description = c.Description
+ res.Container = c.Container
+ for _, imp := range c.Imports {
+ res.Imports = append(res.Imports, imp.ToCELImport())
+ }
+ if c.StdLib != nil {
+ res.StdLib = c.StdLib.ToCELLibrarySubset()
+ }
+ for _, ext := range c.Extensions {
+ res.Extensions = append(res.Extensions, ext.ToCELExtension())
+ }
+ if c.ContextVariable != nil {
+ celCtxVar, err := c.ContextVariable.ToCELContextVariable()
+ if err != nil {
+ return nil, err
+ }
+ res.ContextVariable = celCtxVar
+ }
+ for _, v := range c.Variables {
+ celVar, err := v.ToCELVariable()
+ if err != nil {
+ return nil, err
+ }
+ res.Variables = append(res.Variables, celVar)
+ }
+ for _, f := range c.Functions {
+ celFunc, err := f.ToCELFunction()
+ if err != nil {
+ return nil, err
+ }
+ res.Functions = append(res.Functions, celFunc)
+ }
+ for _, v := range c.Validators {
+ res.Validators = append(res.Validators, v.ToCELValidator())
+ }
+ for _, f := range c.Features {
+ res.Features = append(res.Features, f.ToCELFeature())
+ }
+ return res, res.Validate()
+}
+
+// Import is an import for a cel-go Env.
+type Import struct {
+ Name string `json:"name"`
+}
+
+// ToCELImport converts an Import to a celenv.Import.
+func (i *Import) ToCELImport() *celenv.Import {
+ if i == nil {
+ return nil
+ }
+ return celenv.NewImport(i.Name)
+}
+
+// Variable is a variable for a cel-go Env.
+type Variable struct {
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ Type string `json:"type" jsonschema_description:"type name formatted as TypeName or namespace.TypeName with an optional set of type parameters in angle brackets <>"`
+}
+
+// ToCELVariable converts a Variable to a celenv.Variable.
+func (v *Variable) ToCELVariable() (*celenv.Variable, error) {
+ if v == nil {
+ return nil, nil
+ }
+ td, err := parseType(v.Type)
+ if err != nil {
+ return nil, err
+ }
+ return &celenv.Variable{
+ Name: v.Name,
+ Description: v.Description,
+ TypeDesc: td,
+ }, nil
+}
+
+// ContextVariable is a context variable for a cel-go Env.
+type ContextVariable struct {
+ Type string `json:"type"`
+}
+
+// ToCELContextVariable converts a ContextVariable to a celenv.ContextVariable.
+func (c *ContextVariable) ToCELContextVariable() (*celenv.ContextVariable, error) {
+ if c == nil {
+ return nil, nil
+ }
+ td, err := parseType(c.Type)
+ if err != nil {
+ return nil, err
+ }
+ if len(td.Params) != 0 {
+ return nil, fmt.Errorf("context variable cannot have type parameters")
+ }
+ return &celenv.ContextVariable{TypeName: td.TypeName}, nil
+}
+
+// Function is a function for a cel-go Env.
+type Function struct {
+ Name string `json:"name" jsonschema_description:"camelCase function name, either as a standalone functionName or namespace.functionName"`
+ Description string `json:"description,omitempty"`
+ Overloads []*Overload `json:"overloads"`
+}
+
+// ToCELFunction converts a Function to a celenv.Function.
+func (f *Function) ToCELFunction() (*celenv.Function, error) {
+ if f == nil {
+ return nil, nil
+ }
+ var celOverloads []*celenv.Overload
+ for _, o := range f.Overloads {
+ celOverload, err := o.ToCELOverload()
+ if err != nil {
+ return nil, err
+ }
+ celOverloads = append(celOverloads, celOverload)
+ }
+ if f.Description != "" {
+ return celenv.NewFunctionWithDoc(f.Name, f.Description, celOverloads...), nil
+ }
+ return celenv.NewFunction(f.Name, celOverloads...), nil
+}
+
+// Overload is an overload for a cel-go Env.
+type Overload struct {
+ ID string `json:"id" jsonschema_description:"overload ID in the format of function_name_type1_..._typeN for global functions and target_type_function_name_type1_..._typeN for member functions"`
+ Examples []string `json:"examples,omitempty"`
+ Target string `json:"target,omitempty" jsonschema_description:"receiver type name for member functions"`
+ Args []string `json:"args,omitempty" jsonschema_description:"argument type names"`
+ Return string `json:"return" jsonschema_description:"return type name"`
+}
+
+// ToCELOverload converts an Overload to a celenv.Overload.
+func (o *Overload) ToCELOverload() (*celenv.Overload, error) {
+ if o == nil {
+ return nil, nil
+ }
+ var args []*celenv.TypeDesc
+ for _, a := range o.Args {
+ td, err := parseType(a)
+ if err != nil {
+ return nil, err
+ }
+ args = append(args, td)
+ }
+ var ret *celenv.TypeDesc
+ var err error
+ if o.Return != "" {
+ ret, err = parseType(o.Return)
+ if err != nil {
+ return nil, err
+ }
+ }
+ var target *celenv.TypeDesc
+ if o.Target != "" {
+ target, err = parseType(o.Target)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if target != nil {
+ return celenv.NewMemberOverload(o.ID, target, args, ret, o.Examples...), nil
+ }
+ return celenv.NewOverload(o.ID, args, ret, o.Examples...), nil
+}
+
+// Extension is an extension for a cel-go Env.
+type Extension struct {
+ Name string `json:"name"`
+ Version string `json:"version,omitempty"`
+}
+
+// ToCELExtension converts an Extension to a celenv.Extension.
+func (e *Extension) ToCELExtension() *celenv.Extension {
+ if e == nil {
+ return nil
+ }
+ return &celenv.Extension{Name: e.Name, Version: e.Version}
+}
+
+// LibrarySubset is a library subset for a cel-go Env.
+type LibrarySubset struct {
+ Disabled bool `json:"disabled,omitempty"`
+ DisableMacros bool `json:"disableMacros,omitempty"`
+ IncludeMacros []string `json:"includeMacros,omitempty"`
+ ExcludeMacros []string `json:"excludeMacros,omitempty"`
+ IncludeFunctions []*FunctionSubset `json:"includeFunctions,omitempty"`
+ ExcludeFunctions []*FunctionSubset `json:"excludeFunctions,omitempty"`
+}
+
+// ToCELLibrarySubset converts a LibrarySubset to a celenv.LibrarySubset.
+func (l *LibrarySubset) ToCELLibrarySubset() *celenv.LibrarySubset {
+ if l == nil {
+ return nil
+ }
+ res := celenv.NewLibrarySubset()
+ res.Disabled = l.Disabled
+ res.DisableMacros = l.DisableMacros
+ res.IncludeMacros = append([]string{}, l.IncludeMacros...)
+ res.ExcludeMacros = append([]string{}, l.ExcludeMacros...)
+ for _, f := range l.IncludeFunctions {
+ res.IncludeFunctions = append(res.IncludeFunctions, f.ToCELFunction())
+ }
+ for _, f := range l.ExcludeFunctions {
+ res.ExcludeFunctions = append(res.ExcludeFunctions, f.ToCELFunction())
+ }
+ return res
+}
+
+// FunctionSubset is a function subset for a cel-go Env.
+type FunctionSubset struct {
+ Name string `json:"name"`
+ OverloadIDs []string `json:"overloads,omitempty"`
+}
+
+// ToCELFunction converts a FunctionSubset to a celenv.FunctionSubset.
+func (f *FunctionSubset) ToCELFunction() *celenv.Function {
+ if f == nil {
+ return nil
+ }
+ res := celenv.NewFunction(f.Name)
+ for _, id := range f.OverloadIDs {
+ res.Overloads = append(res.Overloads, celenv.NewOverload(id, nil, nil))
+ }
+ return res
+}
+
+// Validator is a validator for a cel-go Env.
+type Validator struct {
+ Name string `json:"name"`
+ Config map[string]any `json:"config,omitempty"`
+}
+
+// ToCELValidator converts a Validator to a celenv.Validator.
+func (v *Validator) ToCELValidator() *celenv.Validator {
+ if v == nil {
+ return nil
+ }
+ return celenv.NewValidator(v.Name).SetConfig(v.Config)
+}
+
+// Feature is a feature for a cel-go Env.
+type Feature struct {
+ Name string `json:"name"`
+ Enabled bool `json:"enabled"`
+}
+
+// ToCELFeature converts a Feature to a celenv.Feature.
+func (f *Feature) ToCELFeature() *celenv.Feature {
+ if f == nil {
+ return nil
+ }
+ return celenv.NewFeature(f.Name, f.Enabled)
+}
+
+// ConfigFromJSON converts a JSON string to a Config.
+func ConfigFromJSON(configJSON string) (*Config, error) {
+ var config Config
+ if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
+ return nil, fmt.Errorf("json.Unmarshal(configJSON) failed: %v", err)
+ }
+ return &config, nil
+}
+
+// EnvFromConfig takes a Config and converts it to a cel-go Env.
+func EnvFromConfig(envConfig *Config) (*cel.Env, error) {
+ celConfig, err := envConfig.ToCELConfig()
+ if err != nil {
+ return nil, err
+ }
+ return cel.NewEnv(cel.FromConfig(celConfig, celext.ExtensionOptionFactory))
+}
+
+func parseType(text string) (*celenv.TypeDesc, error) {
+ return celenv.ParseTypeDesc(text)
+}
diff --git a/internal/tools/config_test.go b/internal/tools/config_test.go
new file mode 100644
index 0000000..f867518
--- /dev/null
+++ b/internal/tools/config_test.go
@@ -0,0 +1,520 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tools
+
+import (
+ "encoding/json"
+ "reflect"
+ "strings"
+ "testing"
+
+ celenv "github.com/google/cel-go/common/env"
+)
+
+func TestConfigFromJSON(t *testing.T) {
+ tests := []struct {
+ name string
+ configJSON string
+ want *Config
+ wantErr bool
+ }{
+ {
+ name: "valid basic config",
+ configJSON: `{
+ "name": "test_env",
+ "description": "A test environment",
+ "variables": [
+ {"name": "user", "type": "User"}
+ ]
+ }`,
+ want: &Config{
+ Name: "test_env",
+ Description: "A test environment",
+ Variables: []*Variable{
+ {Name: "user", Type: "User"},
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "invalid json",
+ configJSON: `{"name": "test_env"`,
+ want: nil,
+ wantErr: true,
+ },
+ }
+
+ for _, tc := range tests {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ got, err := ConfigFromJSON(tc.configJSON)
+ if (err != nil) != tc.wantErr {
+ t.Errorf("ConfigFromJSON() error = %v, wantErr %v", err, tc.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(got, tc.want) {
+ gotJSON := mustJSONMarshal(t, got)
+ wantJSON := mustJSONMarshal(t, tc.want)
+ if string(gotJSON) != string(wantJSON) {
+ t.Errorf("ConfigFromJSON() = %v, want %v", string(gotJSON), string(wantJSON))
+ }
+ }
+ })
+ }
+}
+
+func mustJSONMarshal(t *testing.T, v any) []byte {
+ t.Helper()
+ data, err := json.Marshal(v)
+ if err != nil {
+ t.Fatalf("json.Marshal() error = %v", err)
+ }
+ return data
+}
+
+func TestEnvFromConfig(t *testing.T) {
+ tests := []struct {
+ name string
+ envJSON *Config
+ wantErr bool
+ }{
+ {
+ name: "valid env config",
+ envJSON: &Config{
+ Name: "test_env",
+ Variables: []*Variable{
+ {Name: "user_name", Type: "string"},
+ },
+ },
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ env, err := EnvFromConfig(tt.envJSON)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("EnvFromJSON() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ if !tt.wantErr && env == nil {
+ t.Errorf("EnvFromJSON() returned nil env without error")
+ }
+ })
+ }
+}
+
+func TestConfigToCEL(t *testing.T) {
+ config := &Config{
+ Name: "test_config",
+ Description: "A test config",
+ Container: "test.v1",
+ Imports: []*Import{{Name: "test.v1.TestMessage"}},
+ StdLib: &LibrarySubset{Disabled: false, DisableMacros: true},
+ Extensions: []*Extension{{Name: "strings", Version: "1"}},
+ Variables: []*Variable{
+ {Name: "user", Description: "user info", Type: "map"},
+ },
+ Functions: []*Function{
+ {Name: "myFunc", Description: "my func", Overloads: []*Overload{
+ {ID: "myFunc_string", Args: []string{"string"}, Return: "bool"},
+ {ID: "myFunc_target", Target: "string", Args: []string{"int"}, Return: "bool"},
+ }},
+ },
+ Validators: []*Validator{
+ {Name: "cel.homogenous_literals"},
+ },
+ Features: []*Feature{
+ {Name: "enable_macro_call_tracking", Enabled: true},
+ },
+ }
+
+ celConfig, err := config.ToCELConfig()
+ if err != nil {
+ t.Fatalf("ToCELConfig() failed: %v", err)
+ }
+ if celConfig == nil {
+ t.Fatalf("ToCELConfig() returned nil")
+ }
+
+ if celConfig.Name != "test_config" || celConfig.Description != "A test config" || celConfig.Container != "test.v1" {
+ t.Errorf("ToCELConfig() basic fields not mapped correctly")
+ }
+
+ if len(celConfig.Imports) != 1 || celConfig.Imports[0].Name != "test.v1.TestMessage" {
+ t.Errorf("ToCELConfig() imports not mapped correctly")
+ }
+
+ if celConfig.StdLib == nil || celConfig.StdLib.DisableMacros != true {
+ t.Errorf("ToCELConfig() StdLib not mapped correctly")
+ }
+
+ if len(celConfig.Extensions) != 1 || celConfig.Extensions[0].Name != "strings" {
+ t.Errorf("ToCELConfig() Extensions not mapped correctly")
+ }
+
+ if len(celConfig.Variables) != 1 || celConfig.Variables[0].Name != "user" {
+ t.Errorf("ToCELConfig() Variables not mapped correctly")
+ }
+
+ if len(celConfig.Functions) != 1 || celConfig.Functions[0].Name != "myFunc" || len(celConfig.Functions[0].Overloads) != 2 {
+ t.Errorf("ToCELConfig() Functions not mapped correctly")
+ }
+
+ if len(celConfig.Validators) != 1 || celConfig.Validators[0].Name != "cel.homogenous_literals" {
+ t.Errorf("ToCELConfig() Validators not mapped correctly")
+ }
+
+ if len(celConfig.Features) != 1 || celConfig.Features[0].Name != "enable_macro_call_tracking" {
+ t.Errorf("ToCELConfig() Features not mapped correctly")
+ }
+}
+
+func TestConfigNilReceivers(t *testing.T) {
+ var c *Config
+ if cfg, err := c.ToCELConfig(); err != nil || cfg != nil {
+ t.Errorf("Expected nil")
+ }
+ var i *Import
+ if i.ToCELImport() != nil {
+ t.Errorf("Expected nil")
+ }
+ var v *Variable
+ if vv, err := v.ToCELVariable(); err != nil || vv != nil {
+ t.Errorf("Expected nil")
+ }
+ var cv *ContextVariable
+ if got, err := cv.ToCELContextVariable(); got != nil || err != nil {
+ t.Errorf("Expected nil")
+ }
+ var f *Function
+ if fn, err := f.ToCELFunction(); err != nil || fn != nil {
+ t.Errorf("Expected nil")
+ }
+ var o *Overload
+ if ov, err := o.ToCELOverload(); err != nil || ov != nil {
+ t.Errorf("Expected nil")
+ }
+ var e *Extension
+ if e.ToCELExtension() != nil {
+ t.Errorf("Expected nil")
+ }
+ var ls *LibrarySubset
+ if ls.ToCELLibrarySubset() != nil {
+ t.Errorf("Expected nil")
+ }
+ var fs *FunctionSubset
+ if fs.ToCELFunction() != nil {
+ t.Errorf("Expected nil")
+ }
+ var val *Validator
+ if val.ToCELValidator() != nil {
+ t.Errorf("Expected nil")
+ }
+ var feat *Feature
+ if feat.ToCELFeature() != nil {
+ t.Errorf("Expected nil")
+ }
+}
+
+func TestToCELConfigErrors(t *testing.T) {
+ tests := []struct {
+ name string
+ config *Config
+ wantErr string
+ }{
+ {
+ name: "invalid context variable",
+ config: &Config{
+ ContextVariable: &ContextVariable{Type: "list<~"},
+ },
+ wantErr: "unexpected end of input",
+ },
+ {
+ name: "invalid variable",
+ config: &Config{
+ Variables: []*Variable{{Name: "v", Type: "list<~"}},
+ },
+ wantErr: "unexpected end of input",
+ },
+ {
+ name: "invalid function",
+ config: &Config{
+ Functions: []*Function{{Name: "f", Overloads: []*Overload{{ID: "id", Return: "list<~"}}}},
+ },
+ wantErr: "unexpected end of input",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := tt.config.ToCELConfig()
+ if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
+ t.Errorf("ToCELConfig() error = %v, wantErr %q", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestToCELContextVariableWithParams(t *testing.T) {
+ cv := &ContextVariable{Type: "list"}
+ _, err := cv.ToCELContextVariable()
+ if err == nil || !strings.Contains(err.Error(), "context variable cannot have type parameters") {
+ t.Errorf("ToCELContextVariable() error = %v, wantErr %q", err, "context variable cannot have type parameters")
+ }
+}
+
+func TestToCELVariableErrors(t *testing.T) {
+ v := &Variable{Name: "v", Type: "list<~"}
+ _, err := v.ToCELVariable()
+ if err == nil || !strings.Contains(err.Error(), "unexpected end of input") {
+ t.Errorf("ToCELVariable() error = %v, wantErr %q", err, "unexpected end of input")
+ }
+}
+
+func TestToCELContextVariableErrors(t *testing.T) {
+ cv := &ContextVariable{Type: "list<~"}
+ _, err := cv.ToCELContextVariable()
+ if err == nil || !strings.Contains(err.Error(), "unexpected end of input") {
+ t.Errorf("ToCELContextVariable() error = %v, wantErr %q", err, "unexpected end of input")
+ }
+}
+
+func TestToCELFunctionErrors(t *testing.T) {
+ f := &Function{
+ Name: "f",
+ Overloads: []*Overload{
+ {ID: "id", Return: "list<~"},
+ },
+ }
+ _, err := f.ToCELFunction()
+ if err == nil || !strings.Contains(err.Error(), "unexpected end of input") {
+ t.Errorf("ToCELFunction() error = %v, wantErr %q", err, "unexpected end of input")
+ }
+}
+
+func TestToCELOverloadErrors(t *testing.T) {
+ tests := []struct {
+ name string
+ overload *Overload
+ wantErr string
+ }{
+ {
+ name: "invalid args",
+ overload: &Overload{ID: "id", Args: []string{"list<~"}},
+ wantErr: "unexpected end of input",
+ },
+ {
+ name: "invalid return",
+ overload: &Overload{ID: "id", Return: "list<~"},
+ wantErr: "unexpected end of input",
+ },
+ {
+ name: "invalid target",
+ overload: &Overload{ID: "id", Target: "list<~"},
+ wantErr: "unexpected end of input",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := tt.overload.ToCELOverload()
+ if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
+ t.Errorf("ToCELOverload() error = %v, wantErr %q", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestEnvFromConfigError(t *testing.T) {
+ cfg := &Config{
+ Variables: []*Variable{{Name: "v", Type: "list<~"}},
+ }
+ _, err := EnvFromConfig(cfg)
+ if err == nil || !strings.Contains(err.Error(), "unexpected end of input") {
+ t.Errorf("EnvFromConfig() error = %v, wantErr %q", err, "unexpected end of input")
+ }
+}
+
+func TestFunctionWithoutDescription(t *testing.T) {
+ f := &Function{Name: "testFunc", Overloads: []*Overload{{ID: "testFunc"}}}
+ celFunc, err := f.ToCELFunction()
+ if err != nil {
+ t.Fatalf("ToCELFunction() failed: %v", err)
+ }
+ if celFunc.Description != "" {
+ t.Errorf("Expected empty description, got %q", celFunc.Description)
+ }
+}
+
+func TestParseTypeDesc(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want *celenv.TypeDesc
+ wantErr string
+ }{
+ {
+ name: "simple type",
+ input: "int",
+ want: celenv.NewTypeDesc("int"),
+ },
+ {
+ name: "namespaced identifier",
+ input: "google.protobuf.Struct",
+ want: celenv.NewTypeDesc("google.protobuf.Struct"),
+ },
+ {
+ name: "leading dot",
+ input: ".foo.bar",
+ want: celenv.NewTypeDesc(".foo.bar"),
+ },
+ {
+ name: "nested type",
+ input: "list",
+ want: celenv.NewTypeDesc("list", celenv.NewTypeDesc("int")),
+ },
+ {
+ name: "nested namespaced",
+ input: "list",
+ want: celenv.NewTypeDesc("list", celenv.NewTypeDesc("google.rpc.Status")),
+ },
+ {
+ name: "whitespace",
+ input: " list < int > ",
+ want: celenv.NewTypeDesc("list", celenv.NewTypeDesc("int")),
+ },
+ {
+ name: "bare type param",
+ input: "~T",
+ want: celenv.NewTypeParam("T"),
+ },
+ {
+ name: "complex nested",
+ input: "map>",
+ want: celenv.NewTypeDesc("map", celenv.NewTypeDesc("string"), celenv.NewTypeDesc("list", celenv.NewTypeParam("V"))),
+ },
+ {
+ name: "multiple type params",
+ input: "map>",
+ want: celenv.NewTypeDesc("map", celenv.NewTypeDesc("string"), celenv.NewTypeDesc("map", celenv.NewTypeDesc("int"), celenv.NewTypeDesc("bool"))),
+ },
+ {
+ name: "underscore and numbers",
+ input: "my_type_1",
+ want: celenv.NewTypeDesc("my_type_1"),
+ },
+ {
+ name: "invalid syntax",
+ input: "list'",
+ },
+ {
+ name: "missing comma",
+ input: "map",
+ wantErr: "expected ',' or '>'",
+ },
+ {
+ name: "invalid identifier start",
+ input: "1type",
+ wantErr: "identifier is expected, but '1' was found",
+ },
+ {
+ name: "invalid identifier character",
+ input: "int-type",
+ wantErr: "unexpected character '-'",
+ },
+ {
+ name: "invalid type parameter multiple chars",
+ input: "list<~ABC>",
+ wantErr: "invalid type param, must have a single alphabetic character",
+ },
+ {
+ name: "empty generic",
+ input: "list<>",
+ wantErr: "identifier is expected, but '>' was found",
+ },
+ {
+ name: "incomplete generic",
+ input: "map",
+ wantErr: "identifier is expected, but '>' was found",
+ },
+ {
+ name: "trailing characters",
+ input: "int bool",
+ wantErr: "unexpected character 'b'",
+ },
+ {
+ name: "double dots",
+ input: "google..protobuf.Struct",
+ wantErr: "identifier is expected, but '.' was found",
+ },
+ {
+ name: "missing identifier before generic",
+ input: "",
+ wantErr: "missing identifier at position 0",
+ },
+ {
+ name: "incomplete type param",
+ input: "list<~",
+ wantErr: "unexpected end of input",
+ },
+ {
+ name: "incomplete identifier",
+ input: "google.",
+ wantErr: "unexpected end of input",
+ },
+ {
+ name: "invalid type parameter identifier",
+ input: "list<~1>",
+ wantErr: "invalid type parameter identifier '1'",
+ },
+ }
+
+ for _, tc := range tests {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ got, err := parseType(tc.input)
+ if tc.wantErr != "" {
+ if err == nil {
+ t.Errorf("parseTypeDesc() error = nil, wantErr %q", tc.wantErr)
+ return
+ }
+ if !strings.Contains(err.Error(), tc.wantErr) {
+ t.Errorf("parseTypeDesc() error = %v, wantErr %q", err, tc.wantErr)
+ }
+ return
+ }
+ if err != nil {
+ t.Errorf("parseTypeDesc() error = %v, wantErr nil", err)
+ return
+ }
+ if !reflect.DeepEqual(got, tc.want) {
+ t.Errorf("parseTypeDesc(%v) = %v, want %v", tc.input, got, tc.want)
+ }
+ })
+ }
+}
+
+func TestLibrarySubsetFunctions(t *testing.T) {
+ ls := &LibrarySubset{
+ IncludeFunctions: []*FunctionSubset{{Name: "inc", OverloadIDs: []string{"inc"}}},
+ ExcludeFunctions: []*FunctionSubset{{Name: "exc", OverloadIDs: []string{"exc"}}},
+ }
+ celLs := ls.ToCELLibrarySubset()
+ if len(celLs.IncludeFunctions) != 1 || celLs.IncludeFunctions[0].Name != "inc" {
+ t.Errorf("expected 1 include function 'inc', got %v", celLs.IncludeFunctions)
+ }
+ if len(celLs.ExcludeFunctions) != 1 || celLs.ExcludeFunctions[0].Name != "exc" {
+ t.Errorf("expected 1 exclude function 'exc', got %v", celLs.ExcludeFunctions)
+ }
+}
diff --git a/internal/tools/coverage.go b/internal/tools/coverage.go
new file mode 100644
index 0000000..46ee804
--- /dev/null
+++ b/internal/tools/coverage.go
@@ -0,0 +1,181 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tools
+
+import (
+ "github.com/google/cel-go/cel"
+ celast "github.com/google/cel-go/common/ast"
+ "github.com/google/cel-go/common/types/ref"
+ "github.com/google/cel-go/common/types"
+)
+
+// NewCoverageTracker creates a new CoverageTracker.
+func NewCoverageTracker(ast *cel.Ast) *CoverageTracker {
+ rootExpr := celast.NavigateAST(ast.NativeRep())
+ coverageStats := make(map[int64]*nodeCoverage)
+ celast.PreOrderVisit(rootExpr, celast.NewExprVisitor(func(e celast.Expr) {
+ coverageStats[e.ID()] = &nodeCoverage{
+ exprType: ast.NativeRep().TypeMap()[e.ID()],
+ expr: e,
+ }
+ }))
+ return &CoverageTracker{
+ rootExpr: celast.NavigateAST(ast.NativeRep()),
+ coverageStats: coverageStats,
+ }
+}
+
+// CoverageTracker is a tracker for coverage stats.
+type CoverageTracker struct {
+ rootExpr celast.NavigableExpr
+ coverageStats map[int64]*nodeCoverage
+}
+
+// Record records the coverage stats for the given expression ID.
+func (c *CoverageTracker) Record(details *cel.EvalDetails) {
+ state := details.State()
+ for _, id := range state.IDs() {
+ node, ok := c.coverageStats[id]
+ if !ok {
+ continue
+ }
+ value, ok := state.Value(id)
+ if !ok {
+ continue
+ }
+ node.Record(value)
+ }
+}
+
+// GenerateReport generates a coverage report for the given coverage stats.
+func (c *CoverageTracker) GenerateReport() *CoverageReport {
+ report := &CoverageReport{
+ TotalNodes: len(c.coverageStats),
+ }
+ for _, node := range c.coverageStats {
+ if node.visited {
+ report.CoveredNodes++
+ } else {
+ report.UncoveredNodes = append(report.UncoveredNodes, node.expr)
+ }
+ if node.IsBranch() {
+ report.TotalBranches += 2
+ if node.TrueCovered() {
+ report.CoveredBranches++
+ } else if node.visited {
+ report.UncoveredTrueBranches = append(report.UncoveredTrueBranches, node.expr)
+ }
+ if node.FalseCovered() {
+ report.CoveredBranches++
+ } else if node.visited {
+ report.UncoveredFalseBranches = append(report.UncoveredFalseBranches, node.expr)
+ }
+ }
+ }
+ return report
+}
+
+// CoverageReport documents node and branch coverage for a CEL expression.
+type CoverageReport struct {
+ TotalNodes int
+ CoveredNodes int
+ TotalBranches int
+ CoveredBranches int
+ UncoveredNodes []celast.Expr
+ UncoveredFalseBranches []celast.Expr
+ UncoveredTrueBranches []celast.Expr
+}
+
+// NodeCoverage returns the node coverage for the expression.
+func (c *CoverageReport) NodeCoverage() float64 {
+ if c.TotalNodes == 0 {
+ return 100.0
+ }
+ return float64(c.CoveredNodes) / float64(c.TotalNodes) * 100.0
+}
+
+// BranchCoverage returns the branch coverage for the expression.
+func (c *CoverageReport) BranchCoverage() float64 {
+ if c.TotalBranches == 0 {
+ return 100.0
+ }
+ return float64(c.CoveredBranches) / float64(c.TotalBranches) * 100.0
+}
+
+// nodeCoverage is a node coverage tracker.
+type nodeCoverage struct {
+ exprType *cel.Type
+ expr celast.Expr
+ visited bool
+ values []ref.Val
+}
+
+// Record records the coverage stats for the given value.
+func (n *nodeCoverage) Record(value ref.Val) {
+ n.visited = true
+ n.values = append(n.values, value)
+}
+
+// NodeCoverage returns the node coverage for the expression.
+func (n *nodeCoverage) NodeCoverage() float64 {
+ if n.visited {
+ return 1.0
+ }
+ return 0.0
+}
+
+// IsBranch returns true if the expression is a branch.
+func (n *nodeCoverage) IsBranch() bool {
+ return n.exprType.Kind() == types.BoolKind && n.expr.Kind() != celast.LiteralKind
+}
+
+// BranchesCovered returns the branch coverage for the expression.
+func (n *nodeCoverage) BranchesCovered() int {
+ if !n.IsBranch() {
+ return 0
+ }
+ coverage := 0
+ if n.TrueCovered() {
+ coverage++
+ }
+ if n.FalseCovered() {
+ coverage++
+ }
+ return coverage
+}
+
+func (n *nodeCoverage) TrueCovered() bool {
+ if !n.IsBranch() {
+ return false
+ }
+ for _, value := range n.values {
+ if value == types.True {
+ return true
+ }
+ }
+ return false
+}
+
+func (n *nodeCoverage) FalseCovered() bool {
+ if !n.IsBranch() {
+ return false
+ }
+ for _, value := range n.values {
+ if value == types.False {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/tools/evaluate.go b/internal/tools/evaluate.go
new file mode 100644
index 0000000..d2c1ad8
--- /dev/null
+++ b/internal/tools/evaluate.go
@@ -0,0 +1,118 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tools
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+
+ protojson "google.golang.org/protobuf/encoding/protojson"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/google/cel-go/cel"
+ "github.com/google/cel-go/common/types"
+)
+
+// TestResult is the results of an evaluation.
+type TestResult struct {
+ TestCase string `json:"testCase"`
+ Status string `json:"status"`
+}
+
+// EvaluateExprOutput is the output of the EvaluateCEL function.
+type EvaluateExprOutput struct {
+ TestResults []TestResult `json:"testResults"`
+ Coverage string `json:"coverage"`
+}
+
+// TestCase is a test case for evaluation.
+type TestCase struct {
+ TestCase string `json:"testCase" jsonschema_description:"The name of the test case."`
+ Bindings map[string]any `json:"bindings" jsonschema_description:"The variable bindings for the expression."`
+ Expected any `json:"expected" jsonschema_description:"The expected JSON output value of the expression."`
+}
+
+// EvaluateCEL evaluates a compiled CEL expression against provided variable bindings.
+func EvaluateCEL(expr string, envConfig *Config, testCases []TestCase) (*EvaluateExprOutput, error) {
+ env, err := EnvFromConfig(envConfig)
+ if err != nil {
+ return nil, fmt.Errorf("failed constructing env: %w", err)
+ }
+
+ ast, iss := env.Compile(expr)
+ if iss.Err() != nil {
+ return nil, fmt.Errorf("compile error: %w", iss.Err())
+ }
+
+ prg, err := env.Program(ast, cel.EvalOptions(cel.OptTrackState))
+ if err != nil {
+ return nil, fmt.Errorf("program creation error: %w", err)
+ }
+ coverageTracker := NewCoverageTracker(ast)
+ var results []TestResult
+ for _, tc := range testCases {
+ out, details, err := prg.Eval(tc.Bindings)
+ status := "undefined"
+ if err != nil {
+ results = append(results, TestResult{
+ TestCase: tc.TestCase,
+ Status: err.Error(),
+ })
+ continue
+ }
+ if out != nil {
+ coverageTracker.Record(details)
+ val, err := out.ConvertToNative(types.JSONValueType)
+ if err != nil {
+ status = fmt.Sprintf("unexpected output type: %v", out.Value())
+ results = append(results, TestResult{
+ TestCase: tc.TestCase,
+ Status: status,
+ })
+ continue
+ }
+ valPB := protojson.Format(val.(proto.Message))
+ var valJSON any
+ err = json.Unmarshal([]byte(valPB), &valJSON)
+ if err != nil {
+ status = fmt.Sprintf("unexpected output type: %v", out.Value())
+ results = append(results, TestResult{
+ TestCase: tc.TestCase,
+ Status: status,
+ })
+ continue
+ }
+ eq := reflect.DeepEqual(valJSON, tc.Expected)
+ if eq {
+ status = "pass"
+ } else {
+ status = fmt.Sprintf("failed: got %v, expected %v", valJSON, tc.Expected)
+ }
+ results = append(results, TestResult{
+ TestCase: tc.TestCase,
+ Status: status,
+ })
+ }
+ }
+
+ report := coverageTracker.GenerateReport()
+ outputSchema := EvaluateExprOutput{
+ TestResults: results,
+ Coverage: fmt.Sprintf("Node: %.2f%%, Branch: %.2f%%", report.NodeCoverage(), report.BranchCoverage()),
+ }
+
+ return &outputSchema, nil
+}
diff --git a/internal/tools/evaluate_test.go b/internal/tools/evaluate_test.go
new file mode 100644
index 0000000..7374db7
--- /dev/null
+++ b/internal/tools/evaluate_test.go
@@ -0,0 +1,187 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tools
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestEvaluateCEL(t *testing.T) {
+ envJSON := &Config{
+ Name: "basic",
+ Variables: []*Variable{
+ {Name: "user", Type: "string"},
+ {Name: "age", Type: "int"},
+ },
+ }
+ namespaceEnvJSON := &Config{
+ Name: "namespace",
+ Variables: []*Variable{
+ {Name: "request.name", Type: "string"},
+ {Name: "request.path", Type: "string"},
+ {Name: "request.method", Type: "string"},
+ },
+ }
+
+ tests := []struct {
+ name string
+ expr string
+ envConfig *Config
+ testCases []TestCase
+ wantContains string
+ wantCoverage string
+ wantErr bool
+ }{
+ {
+ name: "successful evaluation",
+ expr: `user == "Alice" && age > 18`,
+ envConfig: envJSON,
+ testCases: []TestCase{{TestCase: "test-1", Bindings: map[string]any{"user": "Alice", "age": 20}, Expected: true}},
+ wantContains: `"testCase":"test-1","status":"pass"`,
+ wantErr: false,
+ wantCoverage: "Node: 100.00%, Branch: 50.00%",
+ },
+ {
+ name: "failed evaluation (returns false)",
+ expr: `user == "Alice" && age > 18`,
+ envConfig: envJSON,
+ testCases: []TestCase{{TestCase: "test-2", Bindings: map[string]any{"user": "Bob", "age": 20}, Expected: false}},
+ wantContains: `"testCase":"test-2","status":"pass"`,
+ wantErr: false,
+ wantCoverage: "Node: 57.14%, Branch: 33.33%",
+ },
+ {
+ name: "invalid bindings json",
+ expr: `user == "Alice"`,
+ envConfig: envJSON,
+ testCases: []TestCase{{TestCase: "test-3", Bindings: map[string]any{"user": "Bob"}, Expected: false}},
+ wantErr: false,
+ wantCoverage: "Node: 100.00%, Branch: 50.00%",
+ },
+ {
+ name: "missing variables in binding",
+ expr: `user == "Alice" && age > 18`,
+ envConfig: envJSON,
+ testCases: []TestCase{{TestCase: "test-4", Bindings: map[string]any{"user": "Alice"}, Expected: false}},
+ wantContains: `"status":"no such attribute(s): age"`,
+ wantErr: false, // Evaluate is resilient to per-test failures but catches them in the result status
+ },
+ {
+ name: "compile error due to bad syntax",
+ expr: `user ==`,
+ envConfig: envJSON,
+ testCases: []TestCase{},
+ wantErr: true,
+ },
+ {
+ name: "evaluation returns null",
+ expr: `null`,
+ envConfig: envJSON,
+ testCases: []TestCase{{TestCase: "test-null", Bindings: map[string]any{}, Expected: nil}},
+ wantContains: `"status":"pass"`,
+ wantErr: false,
+ },
+ {
+ name: "failed constructing env",
+ expr: "true",
+ envConfig: &Config{Variables: []*Variable{{Name: "a", Type: "invalid"}}},
+ testCases: []TestCase{},
+ wantErr: true,
+ wantCoverage: "Node: 100.00%, Branch: 100.00%",
+ },
+ {
+ name: "namespace evaluation",
+ expr: `request.name == "test"`,
+ envConfig: namespaceEnvJSON,
+ testCases: []TestCase{{TestCase: "test-namespace", Bindings: map[string]any{"request.name": "test"}, Expected: true}},
+ wantContains: `"status":"pass"`,
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := EvaluateCEL(tt.expr, tt.envConfig, tt.testCases)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("EvaluateCEL() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !tt.wantErr && tt.wantContains != "" {
+ gotBytes, _ := json.Marshal(got)
+ gotStr := string(gotBytes)
+ if !strings.Contains(gotStr, tt.wantContains) {
+ t.Errorf("EvaluateCEL() got = %v, want it to contain %v", gotStr, tt.wantContains)
+ }
+ }
+ if !tt.wantErr && tt.wantCoverage != "" {
+ if got.Coverage != tt.wantCoverage {
+ t.Errorf("EvaluateCEL() coverage = %v, want %v", got.Coverage, tt.wantCoverage)
+ }
+ }
+ })
+ }
+}
+
+func TestEvaluateTestData(t *testing.T) {
+ // 1. Read testdata/request_headers_env.json
+ envData, err := os.ReadFile(filepath.Join("testdata", "request_headers_env.json"))
+ if err != nil {
+ t.Fatalf("failed to read env config: %v", err)
+ }
+ var envConfig Config
+ if err := json.Unmarshal(envData, &envConfig); err != nil {
+ t.Fatalf("failed to unmarshal env config: %v", err)
+ }
+
+ // 2. Read testdata/user_agent_mozilla.cel
+ exprData, err := os.ReadFile(filepath.Join("testdata", "user_agent_mozilla.cel"))
+ if err != nil {
+ t.Fatalf("failed to read expression: %v", err)
+ }
+ expr := string(exprData)
+
+ // 3. Read testdata/user_agent_test.json
+ testCasesData, err := os.ReadFile(filepath.Join("testdata", "user_agent_test.json"))
+ if err != nil {
+ t.Fatalf("failed to read test cases: %v", err)
+ }
+ var testCases []TestCase
+ if err := json.Unmarshal(testCasesData, &testCases); err != nil {
+ t.Fatalf("failed to unmarshal test cases: %v", err)
+ }
+
+ // 4. Call EvaluateCEL
+ got, err := EvaluateCEL(expr, &envConfig, testCases)
+ if err != nil {
+ t.Fatalf("EvaluateCEL() error = %v", err)
+ }
+
+ // 5. Verify the output matches the expectations
+ // We expect all 3 test cases to pass (status: "pass")
+ for _, res := range got.TestResults {
+ if res.Status != "pass" {
+ t.Errorf("test case '%s' failed: status = %s", res.TestCase, res.Status)
+ }
+ }
+
+ // Verify count
+ if len(got.TestResults) != 3 {
+ t.Errorf("expected 3 evaluation results, got %d", len(got.TestResults))
+ }
+}
diff --git a/internal/tools/input_schema.go b/internal/tools/input_schema.go
new file mode 100644
index 0000000..b0a0abd
--- /dev/null
+++ b/internal/tools/input_schema.go
@@ -0,0 +1,246 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tools
+
+import (
+ "github.com/google/cel-go/cel"
+ "github.com/google/cel-go/common/ast"
+ "github.com/google/cel-go/common/types"
+)
+
+// ComputeInputSchema returns a schemaNode for the given CEL expression.
+func ComputeInputSchema(env *cel.Env, astVal *cel.Ast) (*SchemaNode, error) {
+ astRep := astVal.NativeRep()
+ root := ast.NavigateAST(astRep)
+ rootNode := newSchemaNode("object")
+ visitor := ast.NewExprVisitor(func(e ast.Expr) {
+ switch e.Kind() {
+ case ast.IdentKind:
+ rootNode.AddPropertyRef(e.AsIdent(), e.ID(), true)
+ case ast.SelectKind:
+ s := e.AsSelect()
+ op := s.Operand()
+ path := []ast.Expr{e}
+ for op.Kind() == ast.SelectKind {
+ elem := op.AsSelect()
+ if elem.IsTestOnly() {
+ break
+ }
+ path = append([]ast.Expr{op}, path...)
+ op = elem.Operand()
+ }
+ if op.Kind() == ast.IdentKind {
+ varNode := rootNode.AddPropertyRef(op.AsIdent(), op.ID(), false)
+ for i, p := range path {
+ field := p.AsSelect().FieldName()
+ varNode = varNode.AddPropertyRef(field, p.ID(), i == len(path)-1)
+ }
+ }
+ }
+ })
+ ast.PostOrderVisit(root, visitor)
+
+ // Clean up internal comprehension variables which often have names like "@result" or single/double letters like "l" "k" "v" usually but anything from iterRange essentially.
+ // Since we don't know the exact loop struct variables from just Ident, a safe heuristic is filtering out anything starting with '@'
+ // To be truly robust, we should capture comprehension iter_var and accu_var strings and ignore them in a scope map, but for now we just filter `@` and known short ones if they don't exist in config.
+ // A simpler way: Only keep variables defined in the env JSON, or filter out `@`
+ delete(rootNode.Properties, "@result")
+
+ // A better way is to collect comprehension variables while visiting
+ comprehensionVars := make(map[string]bool)
+ ast.PreOrderVisit(root, ast.NewExprVisitor(func(e ast.Expr) {
+ if e.Kind() == ast.ComprehensionKind {
+ comp := e.AsComprehension()
+ comprehensionVars[comp.IterVar()] = true
+ comprehensionVars[comp.AccuVar()] = true
+ }
+ }))
+
+ for v := range comprehensionVars {
+ delete(rootNode.Properties, v)
+ }
+
+ rootNode.ApplyTypes(env, astRep.TypeMap())
+ return rootNode, nil
+}
+
+// SchemaFromCELType returns a schemaNode for the given CEL type.
+func SchemaFromCELType(env *cel.Env, t *types.Type) *SchemaNode {
+ visited := map[string]*SchemaNode{}
+ return schemaFromCELTypeInternal(env, t, visited)
+}
+
+func schemaFromCELTypeInternal(env *cel.Env, t *types.Type, visited map[string]*SchemaNode) *SchemaNode {
+ if node, found := visited[t.TypeName()]; found {
+ return node
+ }
+ switch t.Kind() {
+ case types.MapKind:
+ m := newSchemaNode("object")
+ m.AdditionalProperties = schemaFromCELTypeInternal(env, t.Parameters()[1], visited)
+ return m
+ case types.ListKind:
+ l := newSchemaNode("array")
+ l.Items = schemaFromCELTypeInternal(env, t.Parameters()[0], visited)
+ return l
+ case types.StringKind:
+ return newSchemaNode("string")
+ case types.DoubleKind:
+ d := newSchemaNode("number")
+ d.Format = "double"
+ return d
+ case types.IntKind:
+ i := newSchemaNode("integer")
+ i.Format = "int64"
+ return i
+ case types.UintKind:
+ i := newSchemaNode("integer")
+ i.Format = "int64"
+ i.Minimum = 0
+ return i
+ case types.DurationKind:
+ return newSchemaNode("string")
+ case types.TimestampKind:
+ i := newSchemaNode("string")
+ i.Format = "date-time"
+ return i
+ case types.BoolKind:
+ return newSchemaNode("boolean")
+ case types.NullTypeKind:
+ return newSchemaNode("null")
+ case types.OpaqueKind:
+ if t.TypeName() == "optional_type" {
+ return schemaFromCELTypeInternal(env, t.Parameters()[0], visited)
+ }
+ obj := newSchemaNode("object")
+ obj.TypeID = t.TypeName()
+ return obj
+ case types.TypeKind:
+ obj := newSchemaNode("object")
+ obj.TypeID = "type"
+ return obj
+ case types.DynKind:
+ return newSchemaNode("object")
+ default:
+ obj := newSchemaNode("object")
+ visited[t.TypeName()] = obj
+ fieldNames, found := env.CELTypeProvider().FindStructFieldNames(t.TypeName())
+ if !found {
+ return obj
+ }
+ for _, fieldName := range fieldNames {
+ ft, _ := env.CELTypeProvider().FindStructFieldType(t.TypeName(), fieldName)
+ obj.Properties[fieldName] = schemaFromCELTypeInternal(env, ft.Type, visited)
+ }
+ return obj
+ }
+}
+
+func newSchemaNode(typeName string) *SchemaNode {
+ return &SchemaNode{
+ Type: typeName,
+ Properties: make(map[string]*SchemaNode),
+ }
+}
+
+type exprRef struct {
+ ExprID int64
+ Leaf bool
+}
+
+// SchemaNode is a node in a JSON schema.
+type SchemaNode struct {
+ ExprRefs []*exprRef `json:"-"`
+ TypeID string `json:"$id,omitempty"`
+ Type string `json:"type"`
+ Format string `json:"format,omitempty"`
+ Items *SchemaNode `json:"items,omitempty"`
+ Properties map[string]*SchemaNode `json:"properties,omitempty"`
+ AdditionalProperties *SchemaNode `json:"additionalProperties,omitempty"`
+ Required []string `json:"required,omitempty"`
+ Minimum float64 `json:"minimum,omitempty"`
+}
+
+// IsLeaf returns true if the schema node is a leaf node.
+func (s *SchemaNode) IsLeaf() bool {
+ for _, ref := range s.ExprRefs {
+ if ref.Leaf {
+ return true
+ }
+ }
+ return false
+}
+
+// FindType returns the CEL type for the schema node.
+func (s *SchemaNode) FindType(typeMap map[int64]*types.Type) *types.Type {
+ for _, ref := range s.ExprRefs {
+ if t, found := typeMap[ref.ExprID]; found {
+ return t
+ }
+ }
+ return nil
+}
+
+// AddExprRef adds an expression reference to the schema node.
+func (s *SchemaNode) AddExprRef(exprID int64, leaf bool) {
+ for _, ref := range s.ExprRefs {
+ if ref.ExprID == exprID {
+ ref.Leaf = false
+ return
+ }
+ }
+ s.ExprRefs = append(s.ExprRefs, &exprRef{ExprID: exprID, Leaf: leaf})
+}
+
+// AddPropertyRef adds a property reference to the schema node.
+func (s *SchemaNode) AddPropertyRef(name string, id int64, leaf bool) *SchemaNode {
+ if existing, found := s.Properties[name]; found {
+ existing.AddExprRef(id, leaf)
+ return existing
+ }
+ node := newSchemaNode("")
+ if !leaf {
+ node.Type = "object"
+ }
+ node.AddExprRef(id, leaf)
+ s.Type = "object"
+ s.Properties[name] = node
+ return node
+}
+
+// ApplyTypes applies the CEL types to the schema node.
+func (s *SchemaNode) ApplyTypes(env *cel.Env, typeMap map[int64]*types.Type) {
+ for _, p := range s.Properties {
+ p.ApplyTypes(env, typeMap)
+ t := p.FindType(typeMap)
+ if t == nil {
+ continue
+ }
+ if t.Kind() == types.StructKind && !p.IsLeaf() {
+ continue
+ }
+ schema := SchemaFromCELType(env, t)
+ p.TypeID = schema.TypeID
+ p.Type = schema.Type
+ p.Format = schema.Format
+ p.Items = schema.Items
+ p.AdditionalProperties = schema.AdditionalProperties
+ if len(p.Properties) == 0 {
+ p.Properties = schema.Properties
+ }
+ p.Required = schema.Required
+ p.Minimum = schema.Minimum
+ }
+}
diff --git a/internal/tools/input_schema_test.go b/internal/tools/input_schema_test.go
new file mode 100644
index 0000000..49a842f
--- /dev/null
+++ b/internal/tools/input_schema_test.go
@@ -0,0 +1,214 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tools
+
+import (
+ "encoding/json"
+ "reflect"
+ "testing"
+
+ "github.com/google/cel-go/cel"
+
+ testpb "github.com/cel-expr/skills/internal/proto"
+)
+
+func TestComputeInputSchema(t *testing.T) {
+ envJSON := &Config{
+ Name: "basic",
+ Variables: []*Variable{
+ {Name: "user", Type: "map"},
+ {Name: "age", Type: "int"},
+ {Name: "labels", Type: "list"},
+ {Name: "budget", Type: "double"},
+ {Name: "timeout", Type: "google.protobuf.Duration"},
+ {Name: "createdAt", Type: "google.protobuf.Timestamp"},
+ {Name: "isActive", Type: "bool"},
+ {Name: "nothing", Type: "null_type"},
+ {Name: "count", Type: "uint"},
+ {Name: "optName", Type: "optional_type"},
+ {Name: "defaultName", Type: "type"},
+ },
+ }
+
+ env, err := EnvFromConfig(envJSON)
+ if err != nil {
+ t.Fatalf("Failed constructing env: %v", err)
+ }
+ env, err = env.Extend(
+ cel.Types(&testpb.TestMessage{}),
+ cel.Variable("msg", cel.ObjectType("cel.skills.internal.proto.TestMessage")),
+ )
+ if err != nil {
+ t.Fatalf("Failed extending env: %v", err)
+ }
+
+ tests := []struct {
+ name string
+ expr string
+ want *SchemaNode
+ wantErr bool
+ }{
+ {
+ name: "basic access",
+ expr: `user.name == "Alice" && age > 18`,
+ want: &SchemaNode{
+ Type: "object",
+ Properties: map[string]*SchemaNode{
+ "user": {
+ Type: "object",
+ Properties: map[string]*SchemaNode{
+ "name": {
+ Type: "object",
+ },
+ },
+ AdditionalProperties: &SchemaNode{Type: "object"},
+ },
+ "age": {
+ Type: "integer",
+ Format: "int64",
+ },
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "list access",
+ expr: `labels.exists(l, l.matches("^foo-"))`,
+ want: &SchemaNode{
+ Type: "object",
+ Properties: map[string]*SchemaNode{
+ "labels": {
+ Type: "array",
+ Items: &SchemaNode{Type: "string"},
+ },
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "basic types access",
+ expr: `budget > 100.0 && timeout > duration("1s") && createdAt > timestamp("2024-01-01T00:00:00Z") && isActive && nothing == null && count > 0u`,
+ want: &SchemaNode{
+ Type: "object",
+ Properties: map[string]*SchemaNode{
+ "budget": {Type: "number", Format: "double"},
+ "timeout": {Type: "string"},
+ "createdAt": {Type: "string", Format: "date-time"},
+ "isActive": {Type: "boolean"},
+ "nothing": {Type: "null"},
+ "count": {Type: "integer", Format: "int64", Minimum: 0},
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "opaque access",
+ expr: `type(optName) == type(defaultName)`, // Evaluates without method access so opaque/type falls back
+ want: &SchemaNode{
+ Type: "object",
+ Properties: map[string]*SchemaNode{
+ "optName": {Type: "string"},
+ "defaultName": {Type: "object", TypeID: "type"}, // Tests default empty case
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "protobuf field graph leaf vs intermediate",
+ expr: `msg.single_nested_message.bb == 42 && msg.single_int32 == 1`,
+ want: &SchemaNode{
+ Type: "object",
+ Properties: map[string]*SchemaNode{
+ "msg": {
+ Type: "object",
+ Properties: map[string]*SchemaNode{
+ "single_nested_message": {
+ Type: "object",
+ Properties: map[string]*SchemaNode{
+ "bb": {Type: "integer", Format: "int64"},
+ },
+ },
+ "single_int32": {Type: "integer", Format: "int64"},
+ },
+ },
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "protobuf nested msg",
+ expr: `msg.single_nested_message`,
+ want: &SchemaNode{
+ Type: "object",
+ Properties: map[string]*SchemaNode{
+ "msg": {
+ Type: "object",
+ Properties: map[string]*SchemaNode{
+ "single_nested_message": {
+ Type: "object",
+ Properties: map[string]*SchemaNode{
+ "bb": {Type: "integer", Format: "int64"},
+ },
+ },
+ },
+ },
+ },
+ },
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ast, iss := env.Compile(tt.expr)
+ if iss.Err() != nil {
+ t.Fatalf("Failed compiling expression: %v", iss.Err())
+ }
+ got, err := ComputeInputSchema(env, ast)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("ComputeInputSchema() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+
+ gotJSON, _ := json.Marshal(got)
+ wantJSON, _ := json.Marshal(tt.want)
+
+ var gotMap, wantMap map[string]any
+ json.Unmarshal(gotJSON, &gotMap)
+ json.Unmarshal(wantJSON, &wantMap)
+
+ // Helper to recursively strip ExprID
+ var removeExprID func(any)
+ removeExprID = func(v any) {
+ if m, ok := v.(map[string]any); ok {
+ delete(m, "ExprRefs")
+ for _, val := range m {
+ removeExprID(val)
+ }
+ } else if l, ok := v.([]any); ok {
+ for _, val := range l {
+ removeExprID(val)
+ }
+ }
+ }
+ removeExprID(gotMap)
+ removeExprID(wantMap)
+
+ if !reflect.DeepEqual(gotMap, wantMap) {
+ t.Errorf("ComputeInputSchema()\nGot: %v\nWant: %v", gotMap, wantMap)
+ }
+ })
+ }
+}
diff --git a/internal/tools/prompt.go b/internal/tools/prompt.go
new file mode 100644
index 0000000..8426348
--- /dev/null
+++ b/internal/tools/prompt.go
@@ -0,0 +1,37 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tools
+
+import (
+ "fmt"
+
+ "github.com/google/cel-go/cel"
+)
+
+// GeneratePrompt generates an LLM authoring prompt explaining the exact variables and functions available.
+func GeneratePrompt(envConfig *Config, userPrompt string) (string, error) {
+ if envConfig == nil {
+ return "", fmt.Errorf("envConfig cannot be nil")
+ }
+ env, err := EnvFromConfig(envConfig)
+ if err != nil {
+ return "", fmt.Errorf("EnvFromConfig(envConfig) failed: %v", err)
+ }
+ prompt, err := cel.AuthoringPrompt(env)
+ if err != nil {
+ return "", fmt.Errorf("cel.AuthoringPrompt(env) failed: %v", err)
+ }
+ return prompt.Render(userPrompt), nil
+}
diff --git a/internal/tools/prompt_test.go b/internal/tools/prompt_test.go
new file mode 100644
index 0000000..f3c7799
--- /dev/null
+++ b/internal/tools/prompt_test.go
@@ -0,0 +1,99 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tools
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestGeneratePrompt(t *testing.T) {
+ envConfig := &Config{
+ Variables: []*Variable{
+ {Name: "foo", Type: "string"},
+ },
+ }
+
+ tests := []struct {
+ name string
+ envConfig *Config
+ userPrompt string
+ wantInRes []string
+ wantErr bool
+ }{
+ {
+ name: "valid prompt",
+ envConfig: envConfig,
+ userPrompt: "check if foo is bar",
+ wantInRes: []string{"foo", "string", "check if foo is bar"},
+ wantErr: false,
+ },
+ {
+ name: "nil env config",
+ envConfig: nil,
+ userPrompt: "check if foo is bar",
+ wantInRes: nil,
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := GeneratePrompt(tt.envConfig, tt.userPrompt)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("GeneratePrompt() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !tt.wantErr {
+ for _, want := range tt.wantInRes {
+ if !strings.Contains(got, want) {
+ t.Errorf("GeneratePrompt() result missing %q", want)
+ }
+ }
+ }
+ })
+ }
+}
+
+func TestGeneratePromptTestData(t *testing.T) {
+ // 1. Read testdata/cloud_armor.json
+ envData, err := os.ReadFile(filepath.Join("testdata", "cloud_armor.json"))
+ if err != nil {
+ t.Fatalf("failed to read env config: %v", err)
+ }
+ var envConfig Config
+ if err := json.Unmarshal(envData, &envConfig); err != nil {
+ t.Fatalf("failed to unmarshal env config: %v", err)
+ }
+ userPrompt := "Allow traffic from 10.0.0.0/8"
+ // 2. Call GeneratePrompt
+ got, err := GeneratePrompt(&envConfig, userPrompt)
+ if err != nil {
+ t.Fatalf("GeneratePrompt() error = %v", err)
+ }
+
+ if !strings.Contains(got, "Allow traffic from 10.0.0.0/8") {
+ t.Errorf("GeneratePrompt() prompot missing %q", got)
+ }
+ if !strings.Contains(got, "origin.ip") {
+ t.Errorf("GeneratePrompt() attribute missing %q", got)
+ }
+ if !strings.Contains(got, "inIpRange") {
+ t.Errorf("GeneratePrompt() function missing %q", got)
+ }
+}
diff --git a/internal/tools/testdata/cloud_armor.json b/internal/tools/testdata/cloud_armor.json
new file mode 100644
index 0000000..e798ae0
--- /dev/null
+++ b/internal/tools/testdata/cloud_armor.json
@@ -0,0 +1,48 @@
+{
+ "name": "google_cloud_armor",
+ "variables": [
+ {
+ "name": "origin.ip",
+ "type": "string"
+ },
+ {
+ "name": "origin.region_code",
+ "type": "string"
+ },
+ {
+ "name": "origin.asn",
+ "type": "int"
+ },
+ {
+ "name": "request.headers",
+ "type": "map"
+ },
+ {
+ "name": "request.method",
+ "type": "string"
+ },
+ {
+ "name": "request.path",
+ "type": "string"
+ },
+ {
+ "name": "request.query",
+ "type": "string"
+ }
+ ],
+ "functions": [
+ {
+ "name": "inIpRange",
+ "overloads": [
+ {
+ "id": "inIpRange_string_string",
+ "args": [
+ "string",
+ "string"
+ ],
+ "return": "bool"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/internal/tools/testdata/request_headers_env.json b/internal/tools/testdata/request_headers_env.json
new file mode 100644
index 0000000..6daadaf
--- /dev/null
+++ b/internal/tools/testdata/request_headers_env.json
@@ -0,0 +1,14 @@
+{
+ "name": "Request Headers Environment",
+ "variables": [
+ {
+ "name": "request.headers",
+ "type": "map"
+ }
+ ],
+ "extensions": [
+ {
+ "name": "strings"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/internal/tools/testdata/user_agent_mozilla.cel b/internal/tools/testdata/user_agent_mozilla.cel
new file mode 100644
index 0000000..a5488d7
--- /dev/null
+++ b/internal/tools/testdata/user_agent_mozilla.cel
@@ -0,0 +1,3 @@
+// Check if user-agent header is present and its lower-case value contains 'mozilla'
+'user-agent' in request.headers &&
+request.headers['user-agent'].lowerAscii().contains('mozilla')
diff --git a/internal/tools/testdata/user_agent_test.json b/internal/tools/testdata/user_agent_test.json
new file mode 100644
index 0000000..f7cf631
--- /dev/null
+++ b/internal/tools/testdata/user_agent_test.json
@@ -0,0 +1,29 @@
+[
+ {
+ "testCase": "Mozilla Lowercase Key",
+ "bindings": {
+ "request.headers": {
+ "user-agent": "Mozilla/5.0"
+ }
+ },
+ "expected": true
+ },
+ {
+ "testCase": "Mozilla Titlecase Key",
+ "bindings": {
+ "request.headers": {
+ "User-Agent": "Mozilla/5.0"
+ }
+ },
+ "expected": false
+ },
+ {
+ "testCase": "Non-Mozilla",
+ "bindings": {
+ "request.headers": {
+ "user-agent": "Opera/9.80"
+ }
+ },
+ "expected": false
+ }
+]
\ No newline at end of file
diff --git a/skills/cel-authoring/SKILL.md b/skills/cel-authoring/SKILL.md
new file mode 100644
index 0000000..dd56c6d
--- /dev/null
+++ b/skills/cel-authoring/SKILL.md
@@ -0,0 +1,150 @@
+---
+name: cel-authoring
+description: >-
+ Skill for authoring Google Common Expression Language (CEL) expressions.
+ Use to configure and write a new policy or CEL rule.
+---
+
+# Google Common Expression Language (CEL) Authoring Skill
+
+Use this skill to author CEL expressions, define environments (variables,
+functions, types) via JSON configuration, test, and debug.
+
+## Workflow
+
+Follow these steps to author a CEL expression:
+
+* **Collect Requirements** - Determine what the CEL expressions need to
+ accomplish: security, object transformation, filtering / routing?
+* **Determine the Environment** - Identify the variables, functions, and CEL
+ extensions needed to satisfy the requirements, reusing an `{ENV}.json`
+ config or generating a new one with the `cel_create_environment` tool.
+* **Generate an Authoring Prompt** - Generate an authoring prompt specific to
+ the environment using the `cel_generate_prompt` tool.
+* **Generate an Expression** - Use the prompt to generate an expression, and
+ validate it with the `cel_compile` tool.
+
+### 1. Collect Requirements
+
+Determine the use case, requirements, and relevant products.
+
+If the following products are mentioned, use the following techniques to
+determine variables and functions available:
+
+- **Google Cloud** - Query
+ [Cloud Documentation](https://docs.cloud.google.com/docs)
+- **Kubernetes** - Read
+ [CEL in Kubernetes](https://kubernetes.io/docs/reference/using-api/cel/)
+- Otherwise, use the built-in `googleSearch` tool to learn more.
+
+### 2. Determine the Environment
+
+Determine the variables, functions, and
+[extensions](https://github.com/google/cel-go/tree/master/ext/README.md) needed
+to satisfy the requirements. If an existing `{ENV}.json` file exists which meets
+the needs exists, prefer using it. If no such `{ENV}.json` exists, generate one
+and use the `cel_create_environment` tool to validate the config.
+
+See `examples/network_env.json` and `examples/user_env.json` for environment
+examples. Type references within the environment followed EBNF grammar defined
+in `references/type_grammar_ebnf.txt`.
+
+Example types:
+
+* Simple types: `bool`, `bytes`, `double`, `dyn`, `int`, `null_type`,
+ `string`, `uint`
+* Parameterized types: `list`, `list<~V>`, `map`,
+ `map<~K,~V>`, `type>`, `optional_type`, `map`
+* Namespaced types: `google.protobuf.Duration`, `.google.rpc.Status`
+
+### 3. Generate an Authoring Prompt
+
+Generate the authoring prompt by calling the `cel_generate_prompt` tool with the
+`{ENV}.json` content as `envConfig` and a summary of the user's requirement as
+`userPrompt`.
+
+### 4. Generate an Expression
+
+Determine if you know enough to author an expression. If not, ask the user for
+more information to address missing variables, types, functions, or extensions.
+If so, provide a summarized overview of the expression behavior and its expected
+output type.
+
+Generate a prompt using the `cel_generate_prompt` tool and save the result to
+`{ENV}.prompt` for future reference. Use the returned `{ENV}.prompt` to generate
+the expression, `{EXPR}.cel`.
+
+Validate the expression compiles using `cel_compile` tool, providing the
+`{EXPR}.cel` as the `expr` argument and `{ENV}.json` as the `envConfig`
+argument.
+
+On success, proceed to the [cel-testing](../cel-testing/SKILL.md) skill. On
+failure, consult the [cel-debugging](../cel-debugging/SKILL.md) skill.
+
+--------------------------------------------------------------------------------
+
+## CEL Syntax & General Principles
+
+### General Principles
+
+1. **Keep it simple:** CEL is deliberately simple. It doesn't support loops,
+ statements, or state modification. Expressions must evaluate to a value.
+2. **Type safety:** CEL is strongly typed. Ensure your values match the types
+ expected by operators and functions.
+3. **Dot notation:** Use dot notation for accessing fields of messages or maps,
+ e.g., `user.name`.
+
+### Standard Type Literals
+
+- **bool**: `true`, `false`
+- **bytes**: `b"abc"`, `b"\x41\x42"`
+- **double**: `3.14`
+- **int**: `42`, `-10`
+- **uint**: `42u`
+- **list**: `[1, 2, 3]`
+- **map**: `{"key": "value"}`
+- **null_type**: `null`
+- **string**: `"hello"`, `'world'`,
+
+ ```cel
+ """use for
+ multi-line"""
+ ```
+
+### Common Operators
+
+- **Logical:** `&&`, `||`, `!`
+- **Comparison:** `==`, `!=`, `<`, `<=`, `>`, `>=`
+- **Arithmetic:** `+`, `-`, `*`, `/`, `%`
+- **String and List Concat:** `+`
+- **Membership:** `in` (e.g., `1 in [1, 2, 3]`)
+
+### Standard Macros
+
+- **`has(message.field)`**: Checks if a field is present and has a non-default
+ value.
+- **`exists(e, predicate)`**: Returns true if *at least one* element `e` in
+ the collection satisfies the predicate.
+ - Example: `users.exists(u, u.age >= 18)`
+- **`all(e, predicate)`**: Returns true if *all* elements `e` in the
+ collection satisfy the predicate.
+ - Example: `users.all(u, u.isActive)`
+- **`exists_one(e, predicate)`**: Returns true if *exactly one* element `e` in
+ the collection satisfies the predicate.
+ - Example: `devices.exists_one(d, d.isPrimary == true)`
+- **`map(e, transform)`**: Applies a transformation to each element in a
+ collection, producing a new list.
+ - Example: `users.map(u, u.name)` (returns a list of names)
+- **`filter(e, predicate)`**: Returns a new collection containing only
+ elements that satisfy the predicate.
+ - Example: `users.filter(u, u.age >= 18)`
+
+
+### Formatting and Escaping
+
+- Use consistent spacing around operators (e.g., `a == b` not `a==b`).
+- When writing multi-line strings, use `"""`.
+- Remember to escape special characters in strings if necessary (e.g., `\n`,
+ `\"`, `\\`).
+
diff --git a/skills/cel-authoring/examples/network_env.json b/skills/cel-authoring/examples/network_env.json
new file mode 100644
index 0000000..4ab0c72
--- /dev/null
+++ b/skills/cel-authoring/examples/network_env.json
@@ -0,0 +1,62 @@
+{
+ "name": "network_request",
+ "extensions": [
+ {
+ "name": "strings",
+ "version": "latest"
+ }
+ ],
+ "variables": [
+ {
+ "name": "origin.ip",
+ "type": "string",
+ "description": "The IP address of the origin"
+ },
+ {
+ "name": "origin.region_code",
+ "type": "string",
+ "description": "The two-letter ISO 3166-1 alpha-2 region code of the origin"
+ },
+ {
+ "name": "origin.asn",
+ "type": "int",
+ "description": "The Autonomous System Number of the origin"
+ },
+ {
+ "name": "request.headers",
+ "type": "map",
+ "description": "The headers of the request, all header keys are lower-case"
+ },
+ {
+ "name": "request.method",
+ "type": "string",
+ "description": "The HTTP method of the request, e.g. GET, POST, PUT, DELETE"
+ },
+ {
+ "name": "request.path",
+ "type": "string",
+ "description": "The path of the request, e.g. /api/v1/users"
+ },
+ {
+ "name": "request.query",
+ "type": "string",
+ "description": "The query string of the request, e.g. ?id=123&name=test"
+ }
+ ],
+ "functions": [
+ {
+ "name": "inIpRange",
+ "description": "Check if an IP address is in a given IP range",
+ "overloads": [
+ {
+ "id": "inIpRange_string_string",
+ "examples": [
+ "inIpRange('192.168.1.1', '192.168.1.0/24') // true"
+ ],
+ "args": ["string", "string"],
+ "return": "bool"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/skills/cel-authoring/examples/network_headers.cel b/skills/cel-authoring/examples/network_headers.cel
new file mode 100644
index 0000000..42fd875
--- /dev/null
+++ b/skills/cel-authoring/examples/network_headers.cel
@@ -0,0 +1,4 @@
+// collection filtering macro with strings extension.
+request.headers.exists(h,
+ h.name.lowerAscii() == "authorization" &&
+ h.value.lowerAscii().startsWith("bearer "))
\ No newline at end of file
diff --git a/skills/cel-authoring/examples/user_age_and_location.cel b/skills/cel-authoring/examples/user_age_and_location.cel
new file mode 100644
index 0000000..2fa6c05
--- /dev/null
+++ b/skills/cel-authoring/examples/user_age_and_location.cel
@@ -0,0 +1,2 @@
+// basic conditional with strings and integers.
+user.age >= 18 && user.region_code == "US"
\ No newline at end of file
diff --git a/skills/cel-authoring/examples/user_env.json b/skills/cel-authoring/examples/user_env.json
new file mode 100644
index 0000000..aad92d0
--- /dev/null
+++ b/skills/cel-authoring/examples/user_env.json
@@ -0,0 +1,10 @@
+{
+ "name": "user_env",
+ "variables": [
+ {
+ "name": "user",
+ "type": "map",
+ "description": "Valid user attributes include: email, age, roles, and region_code."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/skills/cel-authoring/examples/user_roles.cel b/skills/cel-authoring/examples/user_roles.cel
new file mode 100644
index 0000000..a7544da
--- /dev/null
+++ b/skills/cel-authoring/examples/user_roles.cel
@@ -0,0 +1,4 @@
+// Check if the user has any roles and that at least one of the roles is
+// "admin". Be sure to type-guard the user.roles field to ensure it's a list
+// before checking for membership because 'user' is typed as map
+has(user.roles) && type(user.roles) == list && 'admin' in user.roles
\ No newline at end of file
diff --git a/skills/cel-authoring/references/type_grammar_ebnf.txt b/skills/cel-authoring/references/type_grammar_ebnf.txt
new file mode 100644
index 0000000..8c2c8b2
--- /dev/null
+++ b/skills/cel-authoring/references/type_grammar_ebnf.txt
@@ -0,0 +1,11 @@
+TypeDesc = NamespaceIdentifier [ "<" TypeList ">" ] ;
+NamespaceIdentifier = [ "." ] Identifier { "." Identifier } ;
+TypeList = TypeElem { "," TypeElem } ;
+TypeElem = TypeDesc | TypeParam
+TypeParam = "~" Alpha ;
+Identifier = ( Alpha | "_" ) { AlphaNumeric | "_" } ;
+
+(* Terminals *)
+Alpha = "a"..."z" | "A"..."Z" ;
+Digit = "0"..."9" ;
+AlphaNumeric = Alpha | Digit ;
\ No newline at end of file
diff --git a/skills/cel-debugging/SKILL.md b/skills/cel-debugging/SKILL.md
new file mode 100644
index 0000000..7da0485
--- /dev/null
+++ b/skills/cel-debugging/SKILL.md
@@ -0,0 +1,92 @@
+---
+name: cel-debugging
+description: >-
+ Skill for debugging Google Common Expression Language (CEL) expressions.
+ Use when an expression fails to compile or evaluate properly.
+---
+
+# Google Common Expression Language (CEL) Debugging Skill
+
+Use this skill to diagnose and resolve CEL compilation and evaluation errors.
+
+## Understanding the Two Phases
+
+CEL processing has two steps:
+
+1. **Compilation** Validating the syntax and type-correctness of an expression
+ using the `cel_compile` tool with an `{ENV}.json` and `{EXPR}.cel`. Consult
+ [cel-authoring](../cel-authoring/SKILL.md) for more information.
+
+2. **Evaluation:** Use `cel_evaluate` to evaluate an `{EXPR}.cel` against a set
+ of `testCases` stored in a `{SUITE}.json` file. Consult
+ [cel-testing](../cel-testing/SKILL.md) for more information.
+
+## Common Compilation Errors
+
+These occur before execution due to typos, unknown symbols, or invalid types.
+
+### 1. "Undeclared Reference"
+
+- **Cause:** Using an undefined variable, function, or field.
+- **Example:** `user.admin` when the `User` message only has `id` and `name`.
+- **Solution:** Check schema/prototype. Ensure variables exist in the
+ environment definition.
+
+### 2. "Type Mismatch" or "No Matching Overload"
+
+- **Cause:** Calling a function or operator with incorrect types.
+- **Example:** `"123" + 456` (CEL doesn't automatically coerce strings to
+ numbers).
+- **Example:** `string.startsWith(123)` (Expected string parameter).
+- **Solution:** Cast inputs (e.g., `string(456)`) or supply correct types.
+ Verify function signatures.
+
+### 3. "Syntax Error"
+
+- **Cause:** Invalid CEL syntax (e.g., mismatched parentheses).
+- **Example:** `user.age > 18 && (user.country == 'US'`
+- **Solution:** Fix grammar, match parentheses, quote strings.
+
+## Common Evaluation Errors
+
+Valid compiled expressions failing on runtime data.
+
+### 1. "No Such Field"
+
+- **Cause:** Accessing a missing structural field (e.g., map key) at runtime.
+- **Solution:** Use the `has()` macro.
+ - *Incorrect:* `user.profile.website == "google.com"` (fails if `profile`
+ isn't populated).
+ - *Correct:* `has(user.profile.website) && user.profile.website ==
+ "google.com"`
+- **Alternative:** Enable the `optional` extension and use the `?` operator.
+ - *Incorrect:* `user.profile.website == "google.com"` (fails if `profile`
+ isn't populated).
+ - *Correct:* `user.profile?.website.orValue("") == "google.com"`
+
+### 2. "Division by Zero"
+
+- **Cause:** Dividing by 0.
+- **Solution:** Add conditional checks for dynamic denominators.
+ - *Better:* `y != 0 && (x / y > 10)`
+
+### 3. "No Such Overload"
+
+- **Cause** A function has been declared in the `{ENV}.json`, but is not
+ implemented in the CEL runtime.
+- **Solution** Determine if a there is another function which could be used to
+ evaluate the desired functionality. Sometimes using more specific types will
+ reveal a scenario where the type-checker did not identify the missing
+ overload as the inputs to the function were marked as `dyn`.
+
+## Strategies for Isolating Faults
+
+To debug complex expressions:
+
+1. **Break it down:** Split `&&`/`||` expressions into chunks. Evaluate each
+ chunk to isolate the failure.
+2. **Mock Inputs Minimally:** Test minimal JSON input, adding fields until
+ failure occurs.
+3. **Verify AST:** Review AST to verify grouping and operator precedence.
+4. **Use Type Assertions:** Explicitly check dynamic types (e.g., `type(val) ==
+ string`).
diff --git a/skills/cel-testing/SKILL.md b/skills/cel-testing/SKILL.md
new file mode 100644
index 0000000..994f862
--- /dev/null
+++ b/skills/cel-testing/SKILL.md
@@ -0,0 +1,60 @@
+---
+name: cel-testing
+description: >-
+ Skill for testing Google Common Expression Language (CEL) expressions.
+ Use to test or validate an existing CEL rule.
+---
+
+# Google Common Expression Language (CEL) Testing Skill
+
+Use this skill to test and validate CEL expressions with a variety of inputs to
+ensure correctness and high coverage.
+
+## Workflow
+
+Follow these steps to test a CEL expression:
+
+* **Compile the Expression** - use `cel_compile` to validate the `{EXPR}.cel`
+ compiles with the `{ENV}.json`.
+* **Generate Tests** - Use the `inputSchema` and `outputSchema` from a
+ successful `cel_compile` to generate test inputs and outputs to a
+ `{SUITE}.json` file.
+* **Evaluate** - Evaluate the test cases with `cel_evaluate`.
+* **Improve Coverage** - Improve coverage until the `cel_evaluate` indicates
+ 100% branch and node coverage.
+
+### 1. Compile the Expression
+
+Provide the `{ENV}.json` and `{EXPR}.cel` to the `cel_compile` tool. If
+successful, the result will contain the `inputSchema` and `outputSchema`
+associated with the expression.
+
+If the compilation fails, use the [cel-debugging](../cel-debugging/SKILL.md)
+skill to correct the expression.
+
+### 2. Generate Test Input Fixtures
+
+Create a test suite JSON matching the `cel_evaluate` tool. A test suite is
+composed of multiple test cases. Within a `testCase`, the `bindings` values must
+match the `inputSchema` from the compile command. The `expected` value must
+match the `outputSchema` from the compile command.
+
+If the test input schema contains an `additionalProperties` or `items` key be
+sure to generate tests where the objects are populated and empty to validate the
+robustness of the expression to unexpected inputs.
+
+Reference examples in `examples/` if unsure:
+
+- `examples/is_admin_policy.cel`
+- `examples/is_admin_env.json`
+- `examples/is_admin_test.json`
+
+### 3. Run the Tests
+
+Run tests by calling the `cel_evaluate` tool with the expression as `expr`, the
+environment as `envConfig`, and the test suite content as `testCases`.
+
+### 4. Evaluate Coverage and Iterate
+
+Review test output for success/failure and total evaluation coverage. Pass
+multiple test cases in the `testCases` to increase coverage.
diff --git a/skills/cel-testing/examples/is_admin_env.json b/skills/cel-testing/examples/is_admin_env.json
new file mode 100644
index 0000000..d138690
--- /dev/null
+++ b/skills/cel-testing/examples/is_admin_env.json
@@ -0,0 +1,9 @@
+{
+ "name": "Admin Policy Environment",
+ "variables": [
+ {
+ "name": "request.auth.claims",
+ "type": "map"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/skills/cel-testing/examples/is_admin_policy.cel b/skills/cel-testing/examples/is_admin_policy.cel
new file mode 100644
index 0000000..dd0b38d
--- /dev/null
+++ b/skills/cel-testing/examples/is_admin_policy.cel
@@ -0,0 +1 @@
+'admin' in request.auth.claims.groups
diff --git a/skills/cel-testing/examples/is_admin_test.json b/skills/cel-testing/examples/is_admin_test.json
new file mode 100644
index 0000000..c83ed87
--- /dev/null
+++ b/skills/cel-testing/examples/is_admin_test.json
@@ -0,0 +1,26 @@
+{"testCases":[
+ {
+ "testCase": "is_admin",
+ "bindings": {
+ "request.auth.claims": {
+ "groups": [
+ "admin",
+ "editor"
+ ]
+ }
+ },
+ "expected": true
+ },
+ {
+ "testCase": "is_not_admin",
+ "bindings": {
+ "request.auth.claims": {
+ "groups": [
+ "viewer",
+ "guest"
+ ]
+ }
+ },
+ "expected": false
+ }]
+}
\ No newline at end of file