From c60d154fedbd32d93865d875d29119d44e4a8a1c Mon Sep 17 00:00:00 2001 From: Anna Eilering Date: Mon, 6 Apr 2026 20:10:01 -0700 Subject: [PATCH 1/3] Initialize main branch Change-Id: I124932567cc2f15e2fa006a8546ce8df3d27dacb --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c0b0b0..3ac65f3 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -Coming soon +# releasing/cel-expr/skills From 7c206475061db1084b7e0f7ba9f0f806869fcede Mon Sep 17 00:00:00 2001 From: Tristan Swadell Date: Sat, 9 May 2026 05:14:20 +0000 Subject: [PATCH 2/3] CEL Skills for authoring, testing, and debugging The skills are accompanied by stdio MCP server Change-Id: I40510a021f606bd3aaec3a0de7eab966e6e40549 --- .github/ISSUE_TEMPLATE/feature_request.md | 21 + .gitignore | 4 + BUILD | 8 + CODE_OF_CONDUCT.md | 96 +++ CONTRIBUTING.md | 66 ++ GEMINI.md | 21 + LICENSE | 202 ++++++ MODULE.bazel | 29 + MODULE.bazel.lock | 638 ++++++++++++++++++ PULL_REQUEST_TEMPLATE.md | 36 + README.md | 18 +- cmd/mcp/BUILD | 27 + cmd/mcp/main.go | 134 ++++ cmd/mcp/main_test.go | 252 +++++++ go.mod | 25 + go.sum | 40 ++ internal/proto/BUILD | 28 + internal/proto/test_schema.pb.go | 245 +++++++ internal/proto/test_schema.proto | 13 + internal/tools/BUILD | 47 ++ internal/tools/compile.go | 45 ++ internal/tools/compile_test.go | 79 +++ internal/tools/config.go | 324 +++++++++ internal/tools/config_test.go | 520 ++++++++++++++ internal/tools/coverage.go | 181 +++++ internal/tools/evaluate.go | 118 ++++ internal/tools/evaluate_test.go | 187 +++++ internal/tools/input_schema.go | 246 +++++++ internal/tools/input_schema_test.go | 214 ++++++ internal/tools/prompt.go | 37 + internal/tools/prompt_test.go | 99 +++ internal/tools/testdata/cloud_armor.json | 48 ++ .../tools/testdata/request_headers_env.json | 14 + .../tools/testdata/user_agent_mozilla.cel | 3 + internal/tools/testdata/user_agent_test.json | 29 + skills/cel-authoring/SKILL.md | 150 ++++ .../cel-authoring/examples/network_env.json | 62 ++ .../examples/network_headers.cel | 4 + .../examples/user_age_and_location.cel | 2 + skills/cel-authoring/examples/user_env.json | 10 + skills/cel-authoring/examples/user_roles.cel | 4 + .../references/type_grammar_ebnf.txt | 11 + skills/cel-debugging/SKILL.md | 92 +++ skills/cel-testing/SKILL.md | 60 ++ skills/cel-testing/examples/is_admin_env.json | 9 + .../cel-testing/examples/is_admin_policy.cel | 1 + .../cel-testing/examples/is_admin_test.json | 26 + 47 files changed, 4524 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitignore create mode 100644 BUILD create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 GEMINI.md create mode 100644 LICENSE create mode 100644 MODULE.bazel create mode 100644 MODULE.bazel.lock create mode 100644 PULL_REQUEST_TEMPLATE.md create mode 100644 cmd/mcp/BUILD create mode 100644 cmd/mcp/main.go create mode 100644 cmd/mcp/main_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/proto/BUILD create mode 100755 internal/proto/test_schema.pb.go create mode 100644 internal/proto/test_schema.proto create mode 100644 internal/tools/BUILD create mode 100644 internal/tools/compile.go create mode 100644 internal/tools/compile_test.go create mode 100644 internal/tools/config.go create mode 100644 internal/tools/config_test.go create mode 100644 internal/tools/coverage.go create mode 100644 internal/tools/evaluate.go create mode 100644 internal/tools/evaluate_test.go create mode 100644 internal/tools/input_schema.go create mode 100644 internal/tools/input_schema_test.go create mode 100644 internal/tools/prompt.go create mode 100644 internal/tools/prompt_test.go create mode 100644 internal/tools/testdata/cloud_armor.json create mode 100644 internal/tools/testdata/request_headers_env.json create mode 100644 internal/tools/testdata/user_agent_mozilla.cel create mode 100644 internal/tools/testdata/user_agent_test.json create mode 100644 skills/cel-authoring/SKILL.md create mode 100644 skills/cel-authoring/examples/network_env.json create mode 100644 skills/cel-authoring/examples/network_headers.cel create mode 100644 skills/cel-authoring/examples/user_age_and_location.cel create mode 100644 skills/cel-authoring/examples/user_env.json create mode 100644 skills/cel-authoring/examples/user_roles.cel create mode 100644 skills/cel-authoring/references/type_grammar_ebnf.txt create mode 100644 skills/cel-debugging/SKILL.md create mode 100644 skills/cel-testing/SKILL.md create mode 100644 skills/cel-testing/examples/is_admin_env.json create mode 100644 skills/cel-testing/examples/is_admin_policy.cel create mode 100644 skills/cel-testing/examples/is_admin_test.json 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 3ac65f3..6199dda 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ -# releasing/cel-expr/skills +# 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..01af7a1 --- /dev/null +++ b/internal/proto/test_schema.proto @@ -0,0 +1,13 @@ +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 From 79334e95c747b264091839d4964453fc2f19cdc0 Mon Sep 17 00:00:00 2001 From: Tristan Swadell Date: Thu, 14 May 2026 00:36:07 +0000 Subject: [PATCH 3/3] Add missing license to proto file Change-Id: Ie815007570d2bad48fb2d9ea5fdceaacbea8e802 --- internal/proto/test_schema.proto | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/proto/test_schema.proto b/internal/proto/test_schema.proto index 01af7a1..937ea16 100644 --- a/internal/proto/test_schema.proto +++ b/internal/proto/test_schema.proto @@ -1,3 +1,17 @@ +// 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;