From 5f35146b979e3c506c7a6d2fad55556b420a0210 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 5 May 2026 15:51:01 -0400 Subject: [PATCH 01/13] Add coding agent sidecar integrations Signed-off-by: Will Killian --- ATTRIBUTIONS-Rust.md | 12975 ++++++++++++++-- Cargo.lock | 609 +- Cargo.toml | 1 + crates/sidecar/Cargo.toml | 34 + crates/sidecar/src/adapters/claude_code.rs | 40 + crates/sidecar/src/adapters/codex.rs | 28 + crates/sidecar/src/adapters/cursor.rs | 43 + crates/sidecar/src/adapters/mod.rs | 316 + crates/sidecar/src/config.rs | 204 + crates/sidecar/src/error.rs | 46 + crates/sidecar/src/gateway.rs | 323 + crates/sidecar/src/installer.rs | 601 + crates/sidecar/src/main.rs | 27 + crates/sidecar/src/model.rs | 88 + crates/sidecar/src/server.rs | 299 + crates/sidecar/src/session.rs | 550 + docs/index.md | 4 + docs/integrate-frameworks/about.md | 4 + .../coding-agent-claude-code.md | 100 + .../coding-agent-codex.md | 102 + .../coding-agent-cursor.md | 105 + .../coding-agent-sidecar.md | 146 + integrations/coding-agents/README.md | 118 + .../claude-code/.claude-plugin/plugin.json | 19 + .../coding-agents/claude-code/README.md | 124 + .../claude-code/hooks/hooks.json | 117 + .../codex/.codex-plugin/plugin.json | 31 + integrations/coding-agents/codex/README.md | 132 + .../coding-agents/codex/hooks/hooks.json | 117 + .../coding-agents/cursor/.cursor/hooks.json | 164 + integrations/coding-agents/cursor/README.md | 135 + 31 files changed, 16108 insertions(+), 1494 deletions(-) create mode 100644 crates/sidecar/Cargo.toml create mode 100644 crates/sidecar/src/adapters/claude_code.rs create mode 100644 crates/sidecar/src/adapters/codex.rs create mode 100644 crates/sidecar/src/adapters/cursor.rs create mode 100644 crates/sidecar/src/adapters/mod.rs create mode 100644 crates/sidecar/src/config.rs create mode 100644 crates/sidecar/src/error.rs create mode 100644 crates/sidecar/src/gateway.rs create mode 100644 crates/sidecar/src/installer.rs create mode 100644 crates/sidecar/src/main.rs create mode 100644 crates/sidecar/src/model.rs create mode 100644 crates/sidecar/src/server.rs create mode 100644 crates/sidecar/src/session.rs create mode 100644 docs/integrate-frameworks/coding-agent-claude-code.md create mode 100644 docs/integrate-frameworks/coding-agent-codex.md create mode 100644 docs/integrate-frameworks/coding-agent-cursor.md create mode 100644 docs/integrate-frameworks/coding-agent-sidecar.md create mode 100644 integrations/coding-agents/README.md create mode 100644 integrations/coding-agents/claude-code/.claude-plugin/plugin.json create mode 100644 integrations/coding-agents/claude-code/README.md create mode 100644 integrations/coding-agents/claude-code/hooks/hooks.json create mode 100644 integrations/coding-agents/codex/.codex-plugin/plugin.json create mode 100644 integrations/coding-agents/codex/README.md create mode 100644 integrations/coding-agents/codex/hooks/hooks.json create mode 100644 integrations/coding-agents/cursor/.cursor/hooks.json create mode 100644 integrations/coding-agents/cursor/README.md diff --git a/ATTRIBUTIONS-Rust.md b/ATTRIBUTIONS-Rust.md index 2d0b614..ca0023c 100644 --- a/ATTRIBUTIONS-Rust.md +++ b/ATTRIBUTIONS-Rust.md @@ -429,6 +429,216 @@ This file is automatically generated. Please do not edit it directly. Regenerate ``` +## zeroize - 1.8.2 +**Repository URL**: https://github.com/RustCrypto/utils +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + + 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. + +``` + ## target-lexicon - 0.13.5 **Repository URL**: https://github.com/bytecodealliance/target-lexicon **License Type(s)**: Apache-2.0 @@ -1911,7 +2121,7 @@ Software. ``` -## windows-sys - 0.61.2 +## windows-sys - 0.52.0 **Repository URL**: https://github.com/microsoft/windows-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html @@ -2120,8 +2330,8 @@ Software. ``` -## backon - 1.6.0 -**Repository URL**: https://github.com/Xuanwo/backon +## windows-sys - 0.61.2 +**Repository URL**: https://github.com/microsoft/windows-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -2313,7 +2523,7 @@ Software. same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2021 Datafuse Labs + Copyright (c) Microsoft Corporation. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -2326,10 +2536,11 @@ Software. 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. + ``` -## zerocopy - 0.8.48 -**Repository URL**: https://github.com/google/zerocopy +## windows-targets - 0.52.6 +**Repository URL**: https://github.com/microsoft/windows-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -2521,7 +2732,7 @@ Software. same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2023 The Fuchsia Authors + Copyright (c) Microsoft Corporation. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -2535,11 +2746,10 @@ Software. See the License for the specific language governing permissions and limitations under the License. - ``` -## ipnet - 2.12.0 -**Repository URL**: https://github.com/krisprice/ipnet +## windows_aarch64_gnullvm - 0.52.6 +**Repository URL**: https://github.com/microsoft/windows-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -2723,7 +2933,7 @@ Software. 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 "{}" + 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 @@ -2731,7 +2941,7 @@ Software. same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2017 Juniper Networks, Inc. + Copyright (c) Microsoft Corporation. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -2747,30 +2957,9877 @@ Software. ``` -## futures-channel - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## windows_aarch64_msvc - 0.52.6 +**Repository URL**: https://github.com/microsoft/windows-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -1. Definitions. + 1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + "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. + "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 + "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 (c) Microsoft Corporation. + + 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. + +``` + +## windows_i686_gnu - 0.52.6 +**Repository URL**: https://github.com/microsoft/windows-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) Microsoft Corporation. + + 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. + +``` + +## windows_i686_gnullvm - 0.52.6 +**Repository URL**: https://github.com/microsoft/windows-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) Microsoft Corporation. + + 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. + +``` + +## windows_i686_msvc - 0.52.6 +**Repository URL**: https://github.com/microsoft/windows-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) Microsoft Corporation. + + 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. + +``` + +## windows_x86_64_gnu - 0.52.6 +**Repository URL**: https://github.com/microsoft/windows-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) Microsoft Corporation. + + 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. + +``` + +## windows_x86_64_gnullvm - 0.52.6 +**Repository URL**: https://github.com/microsoft/windows-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) Microsoft Corporation. + + 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. + +``` + +## windows_x86_64_msvc - 0.52.6 +**Repository URL**: https://github.com/microsoft/windows-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) Microsoft Corporation. + + 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. + +``` + +## backon - 1.6.0 +**Repository URL**: https://github.com/Xuanwo/backon +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 2021 Datafuse Labs + + 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. +``` + +## zerocopy - 0.8.48 +**Repository URL**: https://github.com/google/zerocopy +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 2023 The Fuchsia Authors + + 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. + + +``` + +## ipnet - 2.12.0 +**Repository URL**: https://github.com/krisprice/ipnet +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 2017 Juniper Networks, Inc. + + 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. + +``` + +## anstream - 1.0.0 +**Repository URL**: https://github.com/rust-cli/anstyle.git +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## anstyle-parse - 1.0.0 +**Repository URL**: https://github.com/rust-cli/anstyle.git +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## anstyle-query - 1.1.5 +**Repository URL**: https://github.com/rust-cli/anstyle.git +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## anstyle-wincon - 3.0.11 +**Repository URL**: https://github.com/rust-cli/anstyle.git +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## anstyle - 1.0.14 +**Repository URL**: https://github.com/rust-cli/anstyle.git +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## clap - 4.6.0 +**Repository URL**: https://github.com/clap-rs/clap +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## clap_builder - 4.6.0 +**Repository URL**: https://github.com/clap-rs/clap +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## clap_derive - 4.6.0 +**Repository URL**: https://github.com/clap-rs/clap +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## clap_lex - 1.1.0 +**Repository URL**: https://github.com/clap-rs/clap +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## colorchoice - 1.0.5 +**Repository URL**: https://github.com/rust-cli/anstyle.git +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## is_terminal_polyfill - 1.70.2 +**Repository URL**: https://github.com/polyfill-rs/is_terminal_polyfill +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## once_cell_polyfill - 1.70.2 +**Repository URL**: https://github.com/polyfill-rs/once_cell_polyfill +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## toml_datetime - 0.7.5+spec-1.1.0 +**Repository URL**: https://github.com/toml-rs/toml +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## toml_edit - 0.23.10+spec-1.0.0 +**Repository URL**: https://github.com/toml-rs/toml +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## toml_parser - 1.1.2+spec-1.1.0 +**Repository URL**: https://github.com/toml-rs/toml +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## toml_writer - 1.1.1+spec-1.1.0 +**Repository URL**: https://github.com/toml-rs/toml +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## futures-channel - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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. + +``` + +## futures-core - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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. + +``` + +## futures-executor - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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. + +``` + +## futures-io - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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. + +``` + +## futures-macro - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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. + +``` + +## futures-sink - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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. + +``` + +## futures-task - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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. + +``` + +## futures-util - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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. + +``` + +## futures - 0.3.32 +**Repository URL**: https://github.com/rust-lang/futures-rs +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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. + +``` + +## typenum - 1.19.0 +**Repository URL**: https://github.com/paholg/typenum +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 2014 Paho Lurie-Gregg + +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. +``` + +## reqwest - 0.12.28 +**Repository URL**: https://github.com/seanmonstar/reqwest +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 2016 Sean McArthur + +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. + +``` + +## arcstr - 1.2.0 +**Repository URL**: https://github.com/thomcc/arcstr +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 2016 The Miri Developers + +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. + +``` + +## http - 1.4.0 +**Repository URL**: https://github.com/hyperium/http +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 2017 http-rs authors + +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. + +``` + +## tokio-rustls - 0.26.4 +**Repository URL**: https://github.com/rustls/tokio-rustls +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 2017 quininer kel + +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. + +``` + +## ppv-lite86 - 0.2.21 +**Repository URL**: https://github.com/cryptocorrosion/cryptocorrosion +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 2019 The CryptoCorrosion Contributors + +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. + +``` + +## iana-time-zone-haiku - 0.1.2 +**Repository URL**: https://github.com/strawlab/iana-time-zone +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 2020 Andrew Straw + +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. + +``` + +## iana-time-zone - 0.1.65 +**Repository URL**: https://github.com/strawlab/iana-time-zone +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 2020 Andrew Straw + +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. + +``` + +## rustls-pki-types - 1.14.1 +**Repository URL**: https://github.com/rustls/pki-types +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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 2023 Dirkjan Ochtman + +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. + +``` + +## arc-swap - 1.9.1 +**Repository URL**: https://github.com/vorner/arc-swap +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + +``` + +## async-lock - 3.4.2 +**Repository URL**: https://github.com/smol-rs/async-lock +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + +``` + +## atomic-waker - 1.1.2 +**Repository URL**: https://github.com/smol-rs/atomic-waker +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + +``` + +## base64 - 0.22.1 +**Repository URL**: https://github.com/marshallpierce/rust-base64 +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. @@ -2940,8 +12997,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +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. @@ -2957,8 +13013,8 @@ limitations under the License. ``` -## futures-core - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## bitflags - 2.11.0 +**Repository URL**: https://github.com/bitflags/bitflags **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -3150,8 +13206,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +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. @@ -3167,8 +13222,8 @@ limitations under the License. ``` -## futures-executor - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## bumpalo - 3.20.2 +**Repository URL**: https://github.com/fitzgen/bumpalo **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -3360,8 +13415,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +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. @@ -3377,8 +13431,8 @@ limitations under the License. ``` -## futures-io - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## cast - 0.3.0 +**Repository URL**: https://github.com/japaric/cast.rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -3570,8 +13624,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +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. @@ -3587,8 +13640,8 @@ limitations under the License. ``` -## futures-macro - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## cfg-if - 1.0.4 +**Repository URL**: https://github.com/rust-lang/cfg-if **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -3780,8 +13833,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +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. @@ -3797,8 +13849,8 @@ limitations under the License. ``` -## futures-sink - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## concurrent-queue - 2.5.0 +**Repository URL**: https://github.com/smol-rs/concurrent-queue **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -3990,8 +14042,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +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. @@ -4007,8 +14058,8 @@ limitations under the License. ``` -## futures-task - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## core-foundation-sys - 0.8.7 +**Repository URL**: https://github.com/servo/core-foundation-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -4200,8 +14251,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +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. @@ -4217,8 +14267,8 @@ limitations under the License. ``` -## futures-util - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## core-foundation - 0.10.1 +**Repository URL**: https://github.com/servo/core-foundation-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -4410,8 +14460,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +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. @@ -4427,8 +14476,8 @@ limitations under the License. ``` -## futures - 0.3.32 -**Repository URL**: https://github.com/rust-lang/futures-rs +## crossbeam-utils - 0.8.21 +**Repository URL**: https://github.com/crossbeam-rs/crossbeam **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -4620,8 +14669,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright (c) 2016 Alex Crichton -Copyright (c) 2017 The Tokio Authors +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. @@ -4637,8 +14685,8 @@ limitations under the License. ``` -## typenum - 1.19.0 -**Repository URL**: https://github.com/paholg/typenum +## displaydoc - 0.2.5 +**Repository URL**: https://github.com/yaahc/displaydoc **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -4830,7 +14878,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2014 Paho Lurie-Gregg +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. @@ -4843,10 +14891,11 @@ 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. + ``` -## reqwest - 0.12.28 -**Repository URL**: https://github.com/seanmonstar/reqwest +## either - 1.15.0 +**Repository URL**: https://github.com/rayon-rs/either **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -5038,7 +15087,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2016 Sean McArthur +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. @@ -5054,8 +15103,8 @@ limitations under the License. ``` -## arcstr - 1.2.0 -**Repository URL**: https://github.com/thomcc/arcstr +## equivalent - 1.0.2 +**Repository URL**: https://github.com/indexmap-rs/equivalent **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -5247,13 +15296,13 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2016 The Miri Developers +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 + 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, @@ -5263,8 +15312,8 @@ limitations under the License. ``` -## http - 1.4.0 -**Repository URL**: https://github.com/hyperium/http +## errno - 0.3.14 +**Repository URL**: https://github.com/lambda-fairy/rust-errno **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -5456,7 +15505,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2017 http-rs authors +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. @@ -5472,8 +15521,8 @@ limitations under the License. ``` -## ppv-lite86 - 0.2.21 -**Repository URL**: https://github.com/cryptocorrosion/cryptocorrosion +## event-listener-strategy - 0.5.4 +**Repository URL**: https://github.com/smol-rs/event-listener-strategy **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -5665,13 +15714,13 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2019 The CryptoCorrosion Contributors +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 + 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, @@ -5681,8 +15730,8 @@ limitations under the License. ``` -## iana-time-zone-haiku - 0.1.2 -**Repository URL**: https://github.com/strawlab/iana-time-zone +## event-listener - 5.4.1 +**Repository URL**: https://github.com/smol-rs/event-listener **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -5874,7 +15923,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2020 Andrew Straw +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. @@ -5890,8 +15939,8 @@ limitations under the License. ``` -## iana-time-zone - 0.1.65 -**Repository URL**: https://github.com/strawlab/iana-time-zone +## fastrand - 2.4.1 +**Repository URL**: https://github.com/smol-rs/fastrand **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -6083,7 +16132,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -Copyright 2020 Andrew Straw +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. @@ -6099,8 +16148,8 @@ limitations under the License. ``` -## arc-swap - 1.9.1 -**Repository URL**: https://github.com/vorner/arc-swap +## fnv - 1.0.7 +**Repository URL**: https://github.com/servo/rust-fnv **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -6308,8 +16357,8 @@ limitations under the License. ``` -## async-lock - 3.4.2 -**Repository URL**: https://github.com/smol-rs/async-lock +## form_urlencoded - 1.2.2 +**Repository URL**: https://github.com/servo/rust-url **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -6517,8 +16566,8 @@ limitations under the License. ``` -## atomic-waker - 1.1.2 -**Repository URL**: https://github.com/smol-rs/atomic-waker +## hashbrown - 0.17.0 +**Repository URL**: https://github.com/rust-lang/hashbrown **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -6726,8 +16775,8 @@ limitations under the License. ``` -## base64 - 0.22.1 -**Repository URL**: https://github.com/marshallpierce/rust-base64 +## heck - 0.5.0 +**Repository URL**: https://github.com/withoutboats/heck **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -6935,8 +16984,8 @@ limitations under the License. ``` -## bitflags - 2.11.0 -**Repository URL**: https://github.com/bitflags/bitflags +## httparse - 1.10.1 +**Repository URL**: https://github.com/seanmonstar/httparse **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -7144,8 +17193,8 @@ limitations under the License. ``` -## bumpalo - 3.20.2 -**Repository URL**: https://github.com/fitzgen/bumpalo +## hyper-rustls - 0.27.9 +**Repository URL**: https://github.com/rustls/hyper-rustls **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -7353,8 +17402,8 @@ limitations under the License. ``` -## cast - 0.3.0 -**Repository URL**: https://github.com/japaric/cast.rs +## hyper-timeout - 0.5.2 +**Repository URL**: https://github.com/hjr3/hyper-timeout **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -7562,8 +17611,8 @@ limitations under the License. ``` -## cfg-if - 1.0.4 -**Repository URL**: https://github.com/rust-lang/cfg-if +## idna - 1.1.0 +**Repository URL**: https://github.com/servo/rust-url/ **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -7771,8 +17820,8 @@ limitations under the License. ``` -## concurrent-queue - 2.5.0 -**Repository URL**: https://github.com/smol-rs/concurrent-queue +## idna_adapter - 1.2.1 +**Repository URL**: https://github.com/hsivonen/idna_adapter **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -7980,8 +18029,8 @@ limitations under the License. ``` -## core-foundation-sys - 0.8.7 -**Repository URL**: https://github.com/servo/core-foundation-rs +## indexmap - 2.14.0 +**Repository URL**: https://github.com/indexmap-rs/indexmap **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -8189,8 +18238,8 @@ limitations under the License. ``` -## crossbeam-utils - 0.8.21 -**Repository URL**: https://github.com/crossbeam-rs/crossbeam +## itertools - 0.14.0 +**Repository URL**: https://github.com/rust-itertools/itertools **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -8398,8 +18447,8 @@ limitations under the License. ``` -## displaydoc - 0.2.5 -**Repository URL**: https://github.com/yaahc/displaydoc +## js-sys - 0.3.95 +**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/js-sys **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -8607,8 +18656,8 @@ limitations under the License. ``` -## either - 1.15.0 -**Repository URL**: https://github.com/rayon-rs/either +## linux-raw-sys - 0.12.1 +**Repository URL**: https://github.com/sunfishcode/linux-raw-sys **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -8816,8 +18865,8 @@ limitations under the License. ``` -## equivalent - 1.0.2 -**Repository URL**: https://github.com/indexmap-rs/equivalent +## lock_api - 0.4.14 +**Repository URL**: https://github.com/Amanieu/parking_lot **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -9025,8 +19074,8 @@ limitations under the License. ``` -## errno - 0.3.14 -**Repository URL**: https://github.com/lambda-fairy/rust-errno +## log - 0.4.29 +**Repository URL**: https://github.com/rust-lang/log **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -9234,8 +19283,8 @@ limitations under the License. ``` -## event-listener-strategy - 0.5.4 -**Repository URL**: https://github.com/smol-rs/event-listener-strategy +## mime - 0.3.17 +**Repository URL**: https://github.com/hyperium/mime **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -9443,8 +19492,8 @@ limitations under the License. ``` -## event-listener - 5.4.1 -**Repository URL**: https://github.com/smol-rs/event-listener +## minicov - 0.3.8 +**Repository URL**: https://github.com/Amanieu/minicov **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -9652,8 +19701,8 @@ limitations under the License. ``` -## fastrand - 2.4.1 -**Repository URL**: https://github.com/smol-rs/fastrand +## num-bigint - 0.4.6 +**Repository URL**: https://github.com/rust-num/num-bigint **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -9861,8 +19910,8 @@ limitations under the License. ``` -## fnv - 1.0.7 -**Repository URL**: https://github.com/servo/rust-fnv +## num-integer - 0.1.46 +**Repository URL**: https://github.com/rust-num/num-integer **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -10070,8 +20119,8 @@ limitations under the License. ``` -## form_urlencoded - 1.2.2 -**Repository URL**: https://github.com/servo/rust-url +## num-traits - 0.2.19 +**Repository URL**: https://github.com/rust-num/num-traits **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -10279,8 +20328,8 @@ limitations under the License. ``` -## hashbrown - 0.17.0 -**Repository URL**: https://github.com/rust-lang/hashbrown +## once_cell - 1.21.4 +**Repository URL**: https://github.com/matklad/once_cell **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -10488,8 +20537,8 @@ limitations under the License. ``` -## heck - 0.5.0 -**Repository URL**: https://github.com/withoutboats/heck +## openssl-probe - 0.2.1 +**Repository URL**: https://github.com/rustls/openssl-probe **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -10697,8 +20746,8 @@ limitations under the License. ``` -## httparse - 1.10.1 -**Repository URL**: https://github.com/seanmonstar/httparse +## parking - 2.2.1 +**Repository URL**: https://github.com/smol-rs/parking **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -10906,8 +20955,8 @@ limitations under the License. ``` -## hyper-timeout - 0.5.2 -**Repository URL**: https://github.com/hjr3/hyper-timeout +## parking_lot - 0.12.5 +**Repository URL**: https://github.com/Amanieu/parking_lot **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -11115,8 +21164,8 @@ limitations under the License. ``` -## idna - 1.1.0 -**Repository URL**: https://github.com/servo/rust-url/ +## parking_lot_core - 0.9.12 +**Repository URL**: https://github.com/Amanieu/parking_lot **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -11324,8 +21373,8 @@ limitations under the License. ``` -## idna_adapter - 1.2.1 -**Repository URL**: https://github.com/hsivonen/idna_adapter +## percent-encoding - 2.3.2 +**Repository URL**: https://github.com/servo/rust-url/ **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -11533,8 +21582,8 @@ limitations under the License. ``` -## indexmap - 2.14.0 -**Repository URL**: https://github.com/indexmap-rs/indexmap +## prost-derive - 0.14.3 +**Repository URL**: https://github.com/tokio-rs/prost **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -11742,8 +21791,8 @@ limitations under the License. ``` -## itertools - 0.14.0 -**Repository URL**: https://github.com/rust-itertools/itertools +## prost - 0.14.3 +**Repository URL**: https://github.com/tokio-rs/prost **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -11951,8 +22000,8 @@ limitations under the License. ``` -## js-sys - 0.3.95 -**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/js-sys +## regex-automata - 0.4.14 +**Repository URL**: https://github.com/rust-lang/regex **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -12160,8 +22209,8 @@ limitations under the License. ``` -## lock_api - 0.4.14 -**Repository URL**: https://github.com/Amanieu/parking_lot +## regex-syntax - 0.8.10 +**Repository URL**: https://github.com/rust-lang/regex **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -12369,8 +22418,8 @@ limitations under the License. ``` -## log - 0.4.29 -**Repository URL**: https://github.com/rust-lang/log +## regex - 1.12.3 +**Repository URL**: https://github.com/rust-lang/regex **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -12578,8 +22627,8 @@ limitations under the License. ``` -## minicov - 0.3.8 -**Repository URL**: https://github.com/Amanieu/minicov +## ring - 0.17.14 +**Repository URL**: https://github.com/briansmith/ring **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -12787,8 +22836,8 @@ limitations under the License. ``` -## num-bigint - 0.4.6 -**Repository URL**: https://github.com/rust-num/num-bigint +## rustix - 1.1.4 +**Repository URL**: https://github.com/bytecodealliance/rustix **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -12996,8 +23045,8 @@ limitations under the License. ``` -## num-integer - 0.1.46 -**Repository URL**: https://github.com/rust-num/num-integer +## rustls-native-certs - 0.8.3 +**Repository URL**: https://github.com/rustls/rustls-native-certs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -13205,8 +23254,8 @@ limitations under the License. ``` -## num-traits - 0.2.19 -**Repository URL**: https://github.com/rust-num/num-traits +## rustls - 0.23.40 +**Repository URL**: https://github.com/rustls/rustls **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -13414,8 +23463,8 @@ limitations under the License. ``` -## once_cell - 1.21.4 -**Repository URL**: https://github.com/matklad/once_cell +## scopeguard - 1.2.0 +**Repository URL**: https://github.com/bluss/scopeguard **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -13623,8 +23672,8 @@ limitations under the License. ``` -## parking - 2.2.1 -**Repository URL**: https://github.com/smol-rs/parking +## security-framework-sys - 2.17.0 +**Repository URL**: https://github.com/kornelski/rust-security-framework **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -13832,8 +23881,8 @@ limitations under the License. ``` -## parking_lot - 0.12.5 -**Repository URL**: https://github.com/Amanieu/parking_lot +## security-framework - 3.7.0 +**Repository URL**: https://github.com/kornelski/rust-security-framework **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -14041,8 +24090,8 @@ limitations under the License. ``` -## parking_lot_core - 0.9.12 -**Repository URL**: https://github.com/Amanieu/parking_lot +## send_wrapper - 0.6.0 +**Repository URL**: https://github.com/thk1/send_wrapper **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -14250,8 +24299,8 @@ limitations under the License. ``` -## percent-encoding - 2.3.2 -**Repository URL**: https://github.com/servo/rust-url/ +## signal-hook-registry - 1.4.8 +**Repository URL**: https://github.com/vorner/signal-hook **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -14459,8 +24508,8 @@ limitations under the License. ``` -## prost-derive - 0.14.3 -**Repository URL**: https://github.com/tokio-rs/prost +## smallvec - 1.15.1 +**Repository URL**: https://github.com/servo/rust-smallvec **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -14668,8 +24717,8 @@ limitations under the License. ``` -## prost - 0.14.3 -**Repository URL**: https://github.com/tokio-rs/prost +## socket2 - 0.6.3 +**Repository URL**: https://github.com/rust-lang/socket2 **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -14877,8 +24926,8 @@ limitations under the License. ``` -## regex-automata - 0.4.14 -**Repository URL**: https://github.com/rust-lang/regex +## stable_deref_trait - 1.2.1 +**Repository URL**: https://github.com/storyyeller/stable_deref_trait **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -15086,8 +25135,8 @@ limitations under the License. ``` -## regex-syntax - 0.8.10 -**Repository URL**: https://github.com/rust-lang/regex +## tempfile - 3.27.0 +**Repository URL**: https://github.com/Stebalien/tempfile **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -15295,8 +25344,8 @@ limitations under the License. ``` -## regex - 1.12.3 -**Repository URL**: https://github.com/rust-lang/regex +## typed-builder-macro - 0.23.2 +**Repository URL**: https://github.com/idanarye/rust-typed-builder **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -15504,8 +25553,8 @@ limitations under the License. ``` -## scopeguard - 1.2.0 -**Repository URL**: https://github.com/bluss/scopeguard +## typed-builder - 0.23.2 +**Repository URL**: https://github.com/idanarye/rust-typed-builder **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -15713,8 +25762,8 @@ limitations under the License. ``` -## send_wrapper - 0.6.0 -**Repository URL**: https://github.com/thk1/send_wrapper +## unicode-segmentation - 1.13.2 +**Repository URL**: https://github.com/unicode-rs/unicode-segmentation **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -15922,8 +25971,8 @@ limitations under the License. ``` -## signal-hook-registry - 1.4.8 -**Repository URL**: https://github.com/vorner/signal-hook +## url - 2.5.8 +**Repository URL**: https://github.com/servo/rust-url **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -16131,8 +26180,8 @@ limitations under the License. ``` -## smallvec - 1.15.1 -**Repository URL**: https://github.com/servo/rust-smallvec +## uuid - 1.18.1 +**Repository URL**: https://github.com/uuid-rs/uuid **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -16340,8 +26389,8 @@ limitations under the License. ``` -## socket2 - 0.6.3 -**Repository URL**: https://github.com/rust-lang/socket2 +## wasi - 0.11.1+wasi-snapshot-preview1 +**Repository URL**: https://github.com/bytecodealliance/wasi **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -16549,8 +26598,8 @@ limitations under the License. ``` -## stable_deref_trait - 1.2.1 -**Repository URL**: https://github.com/storyyeller/stable_deref_trait +## wasm-bindgen-futures - 0.4.68 +**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/futures **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -16758,8 +26807,8 @@ limitations under the License. ``` -## typed-builder-macro - 0.23.2 -**Repository URL**: https://github.com/idanarye/rust-typed-builder +## wasm-bindgen-macro-support - 0.2.118 +**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/macro-support **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -16967,8 +27016,8 @@ limitations under the License. ``` -## typed-builder - 0.23.2 -**Repository URL**: https://github.com/idanarye/rust-typed-builder +## wasm-bindgen-macro - 0.2.118 +**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/macro **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -17176,8 +27225,8 @@ limitations under the License. ``` -## unicode-segmentation - 1.13.2 -**Repository URL**: https://github.com/unicode-rs/unicode-segmentation +## wasm-bindgen-shared - 0.2.118 +**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/shared **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -17385,8 +27434,8 @@ limitations under the License. ``` -## url - 2.5.8 -**Repository URL**: https://github.com/servo/rust-url +## wasm-bindgen-test-macro - 0.3.68 +**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -17594,8 +27643,8 @@ limitations under the License. ``` -## uuid - 1.18.1 -**Repository URL**: https://github.com/uuid-rs/uuid +## wasm-bindgen-test-shared - 0.2.118 +**Repository URL**: https://github.com/rustwasm/wasm-bindgen/tree/master/crates/test-shared **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -17803,8 +27852,8 @@ limitations under the License. ``` -## wasi - 0.11.1+wasi-snapshot-preview1 -**Repository URL**: https://github.com/bytecodealliance/wasi +## wasm-bindgen-test - 0.3.68 +**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -18012,8 +28061,8 @@ limitations under the License. ``` -## wasm-bindgen-futures - 0.4.68 -**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/futures +## wasm-bindgen - 0.2.118 +**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -18221,8 +28270,8 @@ limitations under the License. ``` -## wasm-bindgen-macro-support - 0.2.118 -**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/macro-support +## web-sys - 0.3.95 +**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/web-sys **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -18430,8 +28479,8 @@ limitations under the License. ``` -## wasm-bindgen-macro - 0.2.118 -**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/macro +## wit-bindgen - 0.51.0 +**Repository URL**: https://github.com/bytecodealliance/wit-bindgen **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -18639,8 +28688,8 @@ limitations under the License. ``` -## wasm-bindgen-shared - 0.2.118 -**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/shared +## block-buffer - 0.12.0 +**Repository URL**: https://github.com/RustCrypto/utils **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -18838,7 +28887,7 @@ 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 + 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, @@ -18848,8 +28897,8 @@ limitations under the License. ``` -## wasm-bindgen-test-macro - 0.3.68 -**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen +## const-oid - 0.10.2 +**Repository URL**: https://github.com/RustCrypto/formats **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -19047,7 +29096,7 @@ 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 + 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, @@ -19057,8 +29106,8 @@ limitations under the License. ``` -## wasm-bindgen-test-shared - 0.2.118 -**Repository URL**: https://github.com/rustwasm/wasm-bindgen/tree/master/crates/test-shared +## cpufeatures - 0.3.0 +**Repository URL**: https://github.com/RustCrypto/utils **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -19256,7 +29305,7 @@ 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 + 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, @@ -19266,8 +29315,8 @@ limitations under the License. ``` -## wasm-bindgen-test - 0.3.68 -**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen +## crypto-common - 0.2.1 +**Repository URL**: https://github.com/RustCrypto/traits **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -19465,7 +29514,7 @@ 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 + 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, @@ -19475,8 +29524,8 @@ limitations under the License. ``` -## wasm-bindgen - 0.2.118 -**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen +## digest - 0.11.2 +**Repository URL**: https://github.com/RustCrypto/traits **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -19674,7 +29723,7 @@ 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 + 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, @@ -19684,8 +29733,8 @@ limitations under the License. ``` -## web-sys - 0.3.95 -**Repository URL**: https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/web-sys +## hybrid-array - 0.4.10 +**Repository URL**: https://github.com/RustCrypto/hybrid-array **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -19883,7 +29932,7 @@ 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 + 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, @@ -19893,8 +29942,8 @@ limitations under the License. ``` -## wit-bindgen - 0.51.0 -**Repository URL**: https://github.com/bytecodealliance/wit-bindgen +## sha2 - 0.11.0 +**Repository URL**: https://github.com/RustCrypto/hashes **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -20092,7 +30141,7 @@ 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 + 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, @@ -20102,14 +30151,14 @@ limitations under the License. ``` -## block-buffer - 0.12.0 -**Repository URL**: https://github.com/RustCrypto/utils +## rand_core - 0.9.5 +**Repository URL**: https://github.com/rust-random/rand **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -20295,30 +30344,16 @@ APPENDIX: How to apply the Apache License to your work. 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. - ``` -## const-oid - 0.10.2 -**Repository URL**: https://github.com/RustCrypto/formats +## getrandom - 0.2.17 +**Repository URL**: https://github.com/rust-random/getrandom **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -20510,7 +30545,7 @@ 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 + https://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, @@ -20520,14 +30555,14 @@ limitations under the License. ``` -## cpufeatures - 0.3.0 -**Repository URL**: https://github.com/RustCrypto/utils +## getrandom - 0.3.4 +**Repository URL**: https://github.com/rust-random/getrandom **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -20719,7 +30754,7 @@ 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 + https://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, @@ -20729,14 +30764,14 @@ limitations under the License. ``` -## crypto-common - 0.2.1 -**Repository URL**: https://github.com/RustCrypto/traits +## getrandom - 0.4.2 +**Repository URL**: https://github.com/rust-random/getrandom **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -20928,7 +30963,7 @@ 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 + https://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, @@ -20938,407 +30973,604 @@ limitations under the License. ``` -## digest - 0.11.2 -**Repository URL**: https://github.com/RustCrypto/traits +## pyo3-async-runtimes - 0.28.0 +**Repository URL**: https://github.com/PyO3/pyo3-async-runtimes **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3 -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + 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 -1. Definitions. + 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. + + + 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. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + 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: - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - "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. + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + (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 - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + (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. - "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. + 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. - "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). + 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. - "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. + 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. - "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." + 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. - "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. + 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. -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. + 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. -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: +## ctor - 0.2.9 +**Repository URL**: https://github.com/mmastrac/rust-ctor +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + 1. Definitions. - (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 + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - (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. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting 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. + "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. -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. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -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. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -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. + "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. -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. + "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). -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. + "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. -END OF TERMS AND CONDITIONS + "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." -APPENDIX: How to apply the Apache License to your work. + "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. - 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. + 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. -Copyright [yyyy] [name of copyright owner] + 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. -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 + 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: - http://www.apache.org/licenses/LICENSE-2.0 + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and -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. + (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 -## hybrid-array - 0.4.10 -**Repository URL**: https://github.com/RustCrypto/hybrid-array -**License Type(s)**: Apache-2.0 -### License: https://spdx.org/licenses/Apache-2.0.html -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + (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. -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + 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. -1. Definitions. + 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. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + 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. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + 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. - "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. + 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. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + 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. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + END OF TERMS AND CONDITIONS - "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. + APPENDIX: How to apply the Apache License to your work. - "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). + 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. - "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. + Copyright {yyyy} {name of copyright owner} - "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." + 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 - "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. + http://www.apache.org/licenses/LICENSE-2.0 -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. + 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. -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. +``` + +## httpdate - 1.0.3 +**Repository URL**: https://github.com/pyfisch/httpdate +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ -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: +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +1. Definitions. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +"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. - (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 +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. - (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. +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. - 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. +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. +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. +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. +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. +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. +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. +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] @@ -21346,7 +31578,7 @@ 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 +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, @@ -21356,198 +31588,70 @@ limitations under the License. ``` -## sha2 - 0.11.0 -**Repository URL**: https://github.com/RustCrypto/hashes +## android_system_properties - 0.1.5 +**Repository URL**: https://github.com/nical/android_system_properties **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +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. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - "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. +"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. - "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). +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - "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. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - "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." +"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. - "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. +"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). -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. +"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. -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. +"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." -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: +"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. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +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. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +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. - (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 +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: - (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. + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - 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. + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and -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. + (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 -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. + (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. -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. + 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. -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. +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. -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. +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. +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] @@ -21555,7 +31659,7 @@ 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 +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, @@ -21565,393 +31669,151 @@ limitations under the License. ``` -## rand_core - 0.9.5 -**Repository URL**: https://github.com/rust-random/rand +## anyhow - 1.0.102 +**Repository URL**: https://github.com/dtolnay/anyhow **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - https://www.apache.org/licenses/ +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. +"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. +"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. +"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. +"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. +"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. +"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). +"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. +"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." +"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. +"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. +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. +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: +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 + (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 + (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 + (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. + (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. + 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. +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. +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. +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. +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. +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. +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. ``` -## getrandom - 0.3.4 -**Repository URL**: https://github.com/rust-random/getrandom +## async-trait - 0.1.89 +**Repository URL**: https://github.com/dtolnay/async-trait **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Apache License - Version 2.0, January 2004 - https://www.apache.org/licenses/ +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. +"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. +"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. +"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. +"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. +"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. +"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). +"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. +"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." +"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. +"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. +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. +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: +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 + (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 + (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 + (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. + (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. + 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. +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. +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. +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. +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. +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. +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] @@ -21959,7 +31821,7 @@ 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 - https://www.apache.org/licenses/LICENSE-2.0 +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, @@ -21969,414 +31831,170 @@ limitations under the License. ``` -## pyo3-async-runtimes - 0.28.0 -**Repository URL**: https://github.com/PyO3/pyo3-async-runtimes +## itoa - 1.0.18 +**Repository URL**: https://github.com/dtolnay/itoa **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` - Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3 +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ - 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 +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - http://www.apache.org/licenses/LICENSE-2.0 +1. Definitions. - 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. +"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. - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +"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. - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - 1. Definitions. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +"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. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +"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). - "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. +"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. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +"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." - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +"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. - "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. +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. - "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). +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. - "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. +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: - "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." + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - "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. + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - 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. + (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. - 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. + 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. - 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: +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. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and +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. - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and +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. - (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 +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. - (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. +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. - 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. +END OF TERMS AND CONDITIONS - 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. +APPENDIX: How to apply the Apache License to your work. - 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. +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. - 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. +Copyright [yyyy] [name of copyright owner] - 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. +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 - 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. +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. ``` -## ctor - 0.2.9 -**Repository URL**: https://github.com/mmastrac/rust-ctor +## libc - 0.2.185 +**Repository URL**: https://github.com/rust-lang/libc **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Version 2.0, January 2004 +http://www.apache.org/licenses/ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - 1. Definitions. +1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +"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. +"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. +"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. +"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. +"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. +"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). +"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. +"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." +"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. +"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. +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. +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: +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 + (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 + (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 + (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. + (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. + 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. +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. +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. +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. +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. +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 +END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. +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. +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} +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 +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 +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. +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. ``` -## android_system_properties - 0.1.5 -**Repository URL**: https://github.com/nical/android_system_properties +## openinference-semantic-conventions - 0.1.1 +**Repository URL**: https://github.com/cagyirey/openinference-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -22456,8 +32074,8 @@ limitations under the License. ``` -## anyhow - 1.0.102 -**Repository URL**: https://github.com/dtolnay/anyhow +## opentelemetry-http - 0.31.0 +**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-http **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -22537,8 +32155,8 @@ limitations under the License. ``` -## async-trait - 0.1.89 -**Repository URL**: https://github.com/dtolnay/async-trait +## opentelemetry-otlp - 0.31.1 +**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-otlp **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -22618,8 +32236,8 @@ limitations under the License. ``` -## itoa - 1.0.18 -**Repository URL**: https://github.com/dtolnay/itoa +## opentelemetry-proto - 0.31.0 +**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-proto **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -22699,8 +32317,8 @@ limitations under the License. ``` -## libc - 0.2.185 -**Repository URL**: https://github.com/rust-lang/libc +## opentelemetry - 0.31.0 +**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -22780,8 +32398,8 @@ limitations under the License. ``` -## openinference-semantic-conventions - 0.1.1 -**Repository URL**: https://github.com/cagyirey/openinference-rs +## opentelemetry_sdk - 0.31.0 +**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-sdk **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -22861,8 +32479,8 @@ limitations under the License. ``` -## opentelemetry-http - 0.31.0 -**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-http +## pin-project-internal - 1.1.11 +**Repository URL**: https://github.com/taiki-e/pin-project **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -22942,8 +32560,8 @@ limitations under the License. ``` -## opentelemetry-otlp - 0.31.1 -**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-otlp +## pin-project-lite - 0.2.17 +**Repository URL**: https://github.com/taiki-e/pin-project-lite **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23023,8 +32641,8 @@ limitations under the License. ``` -## opentelemetry-proto - 0.31.0 -**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-proto +## pin-project - 1.1.11 +**Repository URL**: https://github.com/taiki-e/pin-project **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23104,8 +32722,8 @@ limitations under the License. ``` -## opentelemetry - 0.31.0 -**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry +## portable-atomic - 1.13.1 +**Repository URL**: https://github.com/taiki-e/portable-atomic **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23185,8 +32803,8 @@ limitations under the License. ``` -## opentelemetry_sdk - 0.31.0 -**Repository URL**: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-sdk +## proc-macro2 - 1.0.106 +**Repository URL**: https://github.com/dtolnay/proc-macro2 **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23266,8 +32884,8 @@ limitations under the License. ``` -## pin-project-internal - 1.1.11 -**Repository URL**: https://github.com/taiki-e/pin-project +## pyo3-build-config - 0.28.3 +**Repository URL**: https://github.com/pyo3/pyo3 **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23347,8 +32965,8 @@ limitations under the License. ``` -## pin-project-lite - 0.2.17 -**Repository URL**: https://github.com/taiki-e/pin-project-lite +## pyo3-ffi - 0.28.3 +**Repository URL**: https://github.com/pyo3/pyo3 **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23428,8 +33046,8 @@ limitations under the License. ``` -## pin-project - 1.1.11 -**Repository URL**: https://github.com/taiki-e/pin-project +## pyo3-macros-backend - 0.28.3 +**Repository URL**: https://github.com/pyo3/pyo3 **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23509,8 +33127,8 @@ limitations under the License. ``` -## portable-atomic - 1.13.1 -**Repository URL**: https://github.com/taiki-e/portable-atomic +## pyo3-macros - 0.28.3 +**Repository URL**: https://github.com/pyo3/pyo3 **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23590,8 +33208,8 @@ limitations under the License. ``` -## proc-macro2 - 1.0.106 -**Repository URL**: https://github.com/dtolnay/proc-macro2 +## pyo3 - 0.28.3 +**Repository URL**: https://github.com/pyo3/pyo3 **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23671,8 +33289,8 @@ limitations under the License. ``` -## pyo3-build-config - 0.28.3 -**Repository URL**: https://github.com/pyo3/pyo3 +## quote - 1.0.45 +**Repository URL**: https://github.com/dtolnay/quote **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23752,8 +33370,8 @@ limitations under the License. ``` -## pyo3-ffi - 0.28.3 -**Repository URL**: https://github.com/pyo3/pyo3 +## r-efi - 5.3.0 +**Repository URL**: https://github.com/r-efi/r-efi **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23833,8 +33451,8 @@ limitations under the License. ``` -## pyo3-macros-backend - 0.28.3 -**Repository URL**: https://github.com/pyo3/pyo3 +## r-efi - 6.0.0 +**Repository URL**: https://github.com/r-efi/r-efi **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23914,8 +33532,8 @@ limitations under the License. ``` -## pyo3-macros - 0.28.3 -**Repository URL**: https://github.com/pyo3/pyo3 +## rand - 0.9.3 +**Repository URL**: https://github.com/rust-random/rand **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -23995,8 +33613,8 @@ limitations under the License. ``` -## pyo3 - 0.28.3 -**Repository URL**: https://github.com/pyo3/pyo3 +## rand_chacha - 0.9.0 +**Repository URL**: https://github.com/rust-random/rand **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -24076,8 +33694,8 @@ limitations under the License. ``` -## quote - 1.0.45 -**Repository URL**: https://github.com/dtolnay/quote +## rustversion - 1.0.22 +**Repository URL**: https://github.com/dtolnay/rustversion **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -24157,8 +33775,8 @@ limitations under the License. ``` -## r-efi - 5.3.0 -**Repository URL**: https://github.com/r-efi/r-efi +## ryu-js - 1.0.2 +**Repository URL**: https://github.com/boa-dev/ryu-js **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -24238,8 +33856,8 @@ limitations under the License. ``` -## rand - 0.9.3 -**Repository URL**: https://github.com/rust-random/rand +## ryu - 1.0.23 +**Repository URL**: https://github.com/dtolnay/ryu **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -24319,8 +33937,8 @@ limitations under the License. ``` -## rand_chacha - 0.9.0 -**Repository URL**: https://github.com/rust-random/rand +## semver - 1.0.28 +**Repository URL**: https://github.com/dtolnay/semver **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -24400,8 +34018,8 @@ limitations under the License. ``` -## rustversion - 1.0.22 -**Repository URL**: https://github.com/dtolnay/rustversion +## serde - 1.0.228 +**Repository URL**: https://github.com/serde-rs/serde **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -24481,8 +34099,8 @@ limitations under the License. ``` -## ryu-js - 1.0.2 -**Repository URL**: https://github.com/boa-dev/ryu-js +## serde_core - 1.0.228 +**Repository URL**: https://github.com/serde-rs/serde **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -24562,8 +34180,8 @@ limitations under the License. ``` -## ryu - 1.0.23 -**Repository URL**: https://github.com/dtolnay/ryu +## serde_derive - 1.0.228 +**Repository URL**: https://github.com/serde-rs/serde **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -24643,8 +34261,8 @@ limitations under the License. ``` -## semver - 1.0.28 -**Repository URL**: https://github.com/dtolnay/semver +## serde_json - 1.0.149 +**Repository URL**: https://github.com/serde-rs/json **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -24724,8 +34342,8 @@ limitations under the License. ``` -## serde - 1.0.228 -**Repository URL**: https://github.com/serde-rs/serde +## serde_path_to_error - 0.1.20 +**Repository URL**: https://github.com/dtolnay/path-to-error **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -24805,8 +34423,8 @@ limitations under the License. ``` -## serde_core - 1.0.228 -**Repository URL**: https://github.com/serde-rs/serde +## serde_urlencoded - 0.7.1 +**Repository URL**: https://github.com/nox/serde_urlencoded **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -24886,8 +34504,8 @@ limitations under the License. ``` -## serde_derive - 1.0.228 -**Repository URL**: https://github.com/serde-rs/serde +## syn - 2.0.117 +**Repository URL**: https://github.com/dtolnay/syn **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -24967,8 +34585,8 @@ limitations under the License. ``` -## serde_json - 1.0.149 -**Repository URL**: https://github.com/serde-rs/json +## sync_wrapper - 1.0.2 +**Repository URL**: https://github.com/Actyx/sync_wrapper **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -25048,8 +34666,8 @@ limitations under the License. ``` -## serde_urlencoded - 0.7.1 -**Repository URL**: https://github.com/nox/serde_urlencoded +## tdigest - 0.2.3 +**Repository URL**: https://github.com/MnO2/t-digest **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -25129,8 +34747,8 @@ limitations under the License. ``` -## syn - 2.0.117 -**Repository URL**: https://github.com/dtolnay/syn +## thiserror-impl - 2.0.18 +**Repository URL**: https://github.com/dtolnay/thiserror **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -25210,8 +34828,8 @@ limitations under the License. ``` -## sync_wrapper - 1.0.2 -**Repository URL**: https://github.com/Actyx/sync_wrapper +## thiserror - 2.0.18 +**Repository URL**: https://github.com/dtolnay/thiserror **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -25291,8 +34909,8 @@ limitations under the License. ``` -## tdigest - 0.2.3 -**Repository URL**: https://github.com/MnO2/t-digest +## unicode-ident - 1.0.24 +**Repository URL**: https://github.com/dtolnay/unicode-ident **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -25372,8 +34990,8 @@ limitations under the License. ``` -## thiserror-impl - 2.0.18 -**Repository URL**: https://github.com/dtolnay/thiserror +## utf8parse - 0.2.2 +**Repository URL**: https://github.com/alacritty/vte **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -25453,8 +35071,8 @@ limitations under the License. ``` -## thiserror - 2.0.18 -**Repository URL**: https://github.com/dtolnay/thiserror +## wasip2 - 1.0.2+wasi-0.2.9 +**Repository URL**: https://github.com/bytecodealliance/wasi-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -25534,8 +35152,8 @@ limitations under the License. ``` -## unicode-ident - 1.0.24 -**Repository URL**: https://github.com/dtolnay/unicode-ident +## wasip3 - 0.4.0+wasi-0.3.0-rc-2026-01-06 +**Repository URL**: https://github.com/bytecodealliance/wasi-rs **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -25615,8 +35233,8 @@ limitations under the License. ``` -## wasip2 - 1.0.2+wasi-0.2.9 -**Repository URL**: https://github.com/bytecodealliance/wasi-rs +## wasm-streams - 0.4.2 +**Repository URL**: https://github.com/MattiasBuelens/wasm-streams/ **License Type(s)**: Apache-2.0 ### License: https://spdx.org/licenses/Apache-2.0.html ``` @@ -25928,19 +35546,93 @@ APPENDIX: How to apply the Apache License to your work. 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 +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. +~~~~ + + +``` + +## matchit - 0.8.4 +**Repository URL**: https://github.com/ibraheemdev/matchit +**License Type(s)**: BSD-3-Clause +### License: https://spdx.org/licenses/BSD-3-Clause.html +``` +BSD 3-Clause License + +Copyright (c) 2013, Julien Schmidt +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + +## subtle - 2.6.1 +**Repository URL**: https://github.com/dalek-cryptography/subtle +**License Type(s)**: BSD-3-Clause +### License: https://spdx.org/licenses/BSD-3-Clause.html +``` +Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. +Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. - http://www.apache.org/licenses/LICENSE-2.0 +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. -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. -~~~~ +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` @@ -26035,6 +35727,48 @@ DEALINGS IN THE SOFTWARE. ``` +## untrusted - 0.9.0 +**Repository URL**: https://github.com/briansmith/untrusted +**License Type(s)**: ISC +### License: https://spdx.org/licenses/ISC.html +``` +// Copyright 2015-2016 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +``` + +## ring - 0.17.14 +**Repository URL**: https://github.com/briansmith/ring +**License Type(s)**: ISC +### License: https://spdx.org/licenses/ISC.html +``` +Copyright 2015-2025 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +``` + ## libloading - 0.8.9 **Repository URL**: https://github.com/nagisa/rust_libloading/ **License Type(s)**: ISC @@ -26055,6 +35789,33 @@ THIS SOFTWARE. ``` +## rustls-webpki - 0.103.13 +**Repository URL**: https://github.com/rustls/webpki +**License Type(s)**: ISC +### License: https://spdx.org/licenses/ISC.html +``` +Except as otherwise noted, this project is licensed under the following +(ISC-style) terms: + +Copyright 2015 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +The files under third-party/chromium are licensed as described in +third-party/chromium/LICENSE. + +``` + ## mio - 1.2.0 **Repository URL**: https://github.com/tokio-rs/mio **License Type(s)**: MIT @@ -26142,6 +35903,21 @@ DEALINGS IN THE SOFTWARE. ``` +## schannel - 0.1.29 +**Repository URL**: https://github.com/steffengy/schannel-rs +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2015 steffengy + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + ## redox_syscall - 0.5.18 **Repository URL**: https://gitlab.redox-os.org/redox-os/syscall **License Type(s)**: MIT @@ -26526,6 +36302,39 @@ DEALINGS IN THE SOFTWARE. ``` +## axum - 0.8.9 +**Repository URL**: https://github.com/tokio-rs/axum +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Copyright (c) 2019 axum Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +``` + ## tower-http - 0.6.8 **Repository URL**: https://github.com/tower-rs/tower-http **License Type(s)**: MIT @@ -26751,6 +36560,40 @@ SOFTWARE. ``` +## axum-core - 0.5.6 +**Repository URL**: https://github.com/tokio-rs/axum +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) 2019–2025 axum Contributors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +``` + ## convert_case - 0.6.0 **Repository URL**: https://github.com/rutrum/convert-case **License Type(s)**: MIT @@ -26780,6 +36623,87 @@ SOFTWARE. ``` +## matchit - 0.8.4 +**Repository URL**: https://github.com/ibraheemdev/matchit +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) 2022 Ibraheem Ahmed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +## async-stream-impl - 0.3.6 +**Repository URL**: https://github.com/tokio-rs/async-stream +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## async-stream - 0.3.6 +**Repository URL**: https://github.com/tokio-rs/async-stream +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + ## libm - 0.2.16 **Repository URL**: https://github.com/rust-lang/compiler-builtins **License Type(s)**: MIT @@ -27112,6 +37036,58 @@ DEALINGS IN THE SOFTWARE. ``` +## winnow - 0.7.15 +**Repository URL**: https://github.com/winnow-rs/winnow +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + +## winnow - 1.0.1 +**Repository URL**: https://github.com/winnow-rs/winnow +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +``` + ## aho-corasick - 1.1.4 **Repository URL**: https://github.com/BurntSushi/aho-corasick **License Type(s)**: MIT @@ -27170,6 +37146,37 @@ THE SOFTWARE. ``` +## strsim - 0.11.1 +**Repository URL**: https://github.com/rapidfuzz/strsim-rs +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +The MIT License (MIT) + +Copyright (c) 2015 Danny Guo +Copyright (c) 2016 Titus Wormer +Copyright (c) 2018 Akash Kurdekar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + ## combine - 4.6.7 **Repository URL**: https://github.com/Marwes/combine **License Type(s)**: MIT diff --git a/Cargo.lock b/Cargo.lock index fec08eb..0fd0605 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,7 +56,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -67,7 +67,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -102,6 +102,28 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -125,6 +147,58 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backon" version = "1.6.0" @@ -211,6 +285,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -232,6 +312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -246,6 +327,18 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" version = "1.1.0" @@ -296,6 +389,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -377,7 +480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -522,6 +625,19 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -628,6 +744,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hybrid-array" version = "0.4.10" @@ -651,6 +773,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -658,6 +781,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -943,12 +1082,30 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minicov" version = "0.3.8" @@ -967,7 +1124,7 @@ checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1126,6 +1283,29 @@ dependencies = [ "uuid", ] +[[package]] +name = "nemo-flow-sidecar" +version = "0.2.0" +dependencies = [ + "async-stream", + "axum", + "bytes", + "clap", + "futures-util", + "http", + "http-body-util", + "nemo-flow", + "reqwest", + "serde", + "serde_json", + "tempfile", + "thiserror", + "tokio", + "toml_edit", + "tower", + "uuid", +] + [[package]] name = "nemo-flow-wasm" version = "0.2.0" @@ -1152,7 +1332,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1211,6 +1391,12 @@ dependencies = [ "opentelemetry", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "opentelemetry" version = "0.31.0" @@ -1505,6 +1691,61 @@ dependencies = [ "serde", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -1636,25 +1877,53 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustix" version = "1.1.4" @@ -1665,7 +1934,54 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -1695,12 +2011,44 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -1781,6 +2129,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -1854,7 +2213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1869,6 +2228,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -1926,7 +2291,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1959,6 +2324,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.51.1" @@ -1973,7 +2353,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1987,6 +2367,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -2036,6 +2426,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" @@ -2143,6 +2546,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2218,6 +2622,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -2413,6 +2823,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -2435,13 +2858,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2503,6 +2936,24 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -2512,11 +2963,143 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] [[package]] name = "winnow" @@ -2688,6 +3271,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index 1cb96b2..0c79b57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/core", "crates/adaptive", + "crates/sidecar", # Language Bindings "crates/python", "crates/ffi", diff --git a/crates/sidecar/Cargo.toml b/crates/sidecar/Cargo.toml new file mode 100644 index 0000000..096027a --- /dev/null +++ b/crates/sidecar/Cargo.toml @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "nemo-flow-sidecar" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Gateway sidecar for coding-agent NeMo Flow observability." + +[lints] +workspace = true + +[dependencies] +nemo-flow = { workspace = true, features = ["openinference"] } +async-stream = "0.3" +axum = "0.8" +bytes = "1" +clap = { version = "4", features = ["derive", "env"] } +futures-util = "0.3" +http = "1" +http-body-util = "0.1" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots", "stream"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2" +tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] } +toml_edit = "0.23" +uuid = { workspace = true, features = ["serde", "v7"] } + +[dev-dependencies] +tempfile = "3" +tower = { version = "0.5", features = ["util"] } diff --git a/crates/sidecar/src/adapters/claude_code.rs b/crates/sidecar/src/adapters/claude_code.rs new file mode 100644 index 0000000..6edc6c3 --- /dev/null +++ b/crates/sidecar/src/adapters/claude_code.rs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use axum::http::HeaderMap; +use serde_json::{Value, json}; + +use crate::adapters::{AdapterOutcome, ClassificationRules, classify}; +use crate::model::{AgentKind, NormalizedEvent}; + +pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { + let event = classify( + &payload, + headers, + &ClassificationRules { + kind: AgentKind::ClaudeCode, + agent_start: &["SessionStart", "sessionStart", "session_start"], + agent_end: &["SessionEnd", "sessionEnd", "session_end", "Stop", "stop"], + subagent_start: &["SubagentStart", "subagentStart"], + subagent_end: &["SubagentStop", "subagentStop", "SubagentEnd"], + tool_start: &["PreToolUse", "preToolUse"], + tool_end: &[ + "PostToolUse", + "postToolUse", + "ToolUseFailed", + "toolUseFailed", + ], + }, + ); + let response = match &event { + NormalizedEvent::ToolStarted(_) => { + json!({ "continue": true, "permissionDecision": "allow" }) + } + NormalizedEvent::AgentEnded(_) => json!({ "continue": true, "stopReason": null }), + _ => json!({ "continue": true }), + }; + AdapterOutcome { + events: vec![event], + response, + } +} diff --git a/crates/sidecar/src/adapters/codex.rs b/crates/sidecar/src/adapters/codex.rs new file mode 100644 index 0000000..73846c8 --- /dev/null +++ b/crates/sidecar/src/adapters/codex.rs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use axum::http::HeaderMap; +use serde_json::{Value, json}; + +use crate::adapters::{AdapterOutcome, ClassificationRules, classify}; +use crate::model::AgentKind; + +pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { + let event = classify( + &payload, + headers, + &ClassificationRules { + kind: AgentKind::Codex, + agent_start: &["sessionStart", "session_start", "agentStarted"], + agent_end: &["sessionEnd", "session_end", "agentEnded", "stop"], + subagent_start: &["subagentStart", "subagent_start"], + subagent_end: &["subagentStop", "subagentEnd", "subagent_stop"], + tool_start: &["preToolUse", "toolStarted", "tool_start"], + tool_end: &["postToolUse", "toolEnded", "tool_end", "toolFailed"], + }, + ); + AdapterOutcome { + events: vec![event], + response: json!({}), + } +} diff --git a/crates/sidecar/src/adapters/cursor.rs b/crates/sidecar/src/adapters/cursor.rs new file mode 100644 index 0000000..d7fb569 --- /dev/null +++ b/crates/sidecar/src/adapters/cursor.rs @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use axum::http::HeaderMap; +use serde_json::{Value, json}; + +use crate::adapters::{AdapterOutcome, ClassificationRules, classify}; +use crate::model::{AgentKind, NormalizedEvent}; + +pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { + let event = classify( + &payload, + headers, + &ClassificationRules { + kind: AgentKind::Cursor, + agent_start: &["sessionStart", "session_start"], + agent_end: &["sessionEnd", "session_end", "stop"], + subagent_start: &["subagentStart", "subagent_start"], + subagent_end: &["subagentStop", "subagentEnd", "subagent_stop"], + tool_start: &["preToolUse", "beforeShellExecution", "beforeMCPExecution"], + tool_end: &[ + "postToolUse", + "afterShellExecution", + "afterMCPExecution", + "postToolUseFailure", + ], + }, + ); + let response = match &event { + NormalizedEvent::ToolStarted(_) => json!({ + "continue": true, + "permission": "allow", + "user_message": null, + "agent_message": null + }), + NormalizedEvent::AgentEnded(_) => json!({ "continue": true }), + _ => json!({ "continue": true }), + }; + AdapterOutcome { + events: vec![event], + response, + } +} diff --git a/crates/sidecar/src/adapters/mod.rs b/crates/sidecar/src/adapters/mod.rs new file mode 100644 index 0000000..210cdf0 --- /dev/null +++ b/crates/sidecar/src/adapters/mod.rs @@ -0,0 +1,316 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod claude_code; +pub(crate) mod codex; +pub(crate) mod cursor; + +use axum::http::HeaderMap; +use serde_json::{Map, Value, json}; +use uuid::Uuid; + +use crate::config::header_string; +use crate::model::{AgentKind, NormalizedEvent, SessionEvent, SubagentEvent, ToolEvent}; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct AdapterOutcome { + pub(crate) events: Vec, + pub(crate) response: Value, +} + +pub(super) struct ClassificationRules<'a> { + kind: AgentKind, + agent_start: &'a [&'a str], + agent_end: &'a [&'a str], + subagent_start: &'a [&'a str], + subagent_end: &'a [&'a str], + tool_start: &'a [&'a str], + tool_end: &'a [&'a str], +} + +fn session_id(payload: &Value, headers: &HeaderMap) -> String { + header_string(headers, "x-nemo-flow-session-id") + .or_else(|| header_string(headers, "x-claude-code-session-id")) + .or_else(|| string_at(payload, &["session_id"])) + .or_else(|| string_at(payload, &["sessionId"])) + .or_else(|| string_at(payload, &["session", "id"])) + .or_else(|| string_at(payload, &["conversation_id"])) + .or_else(|| string_at(payload, &["conversationId"])) + .unwrap_or_else(|| format!("hook-{}", Uuid::now_v7())) +} + +fn event_name(payload: &Value) -> String { + string_at(payload, &["hook_event_name"]) + .or_else(|| string_at(payload, &["event_name"])) + .or_else(|| string_at(payload, &["eventName"])) + .or_else(|| string_at(payload, &["event"])) + .or_else(|| string_at(payload, &["type"])) + .or_else(|| string_at(payload, &["name"])) + .unwrap_or_else(|| "unknown".to_string()) +} + +fn metadata(payload: &Value, headers: &HeaderMap, kind: AgentKind, event_name: &str) -> Value { + let mut object = Map::new(); + object.insert("agent_kind".into(), json!(kind.as_str())); + object.insert("hook_event_name".into(), json!(event_name)); + if let Some(profile) = header_string(headers, "x-nemo-flow-config-profile") { + object.insert("sidecar_config_profile".into(), json!(profile)); + } + for (key, value) in [ + ("cwd", string_at(payload, &["cwd"])), + ("transcript_path", string_at(payload, &["transcript_path"])), + ("project_dir", string_at(payload, &["project_dir"])), + ("user_email", string_at(payload, &["user_email"])), + ("model", string_at(payload, &["model"])), + ] { + if let Some(value) = value { + object.insert(key.into(), json!(value)); + } + } + Value::Object(object) +} + +fn common_session_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> SessionEvent { + let event_name = event_name(payload); + SessionEvent { + session_id: session_id(payload, headers), + agent_kind: kind, + event_name: event_name.clone(), + payload: payload.clone(), + metadata: metadata(payload, headers, kind, &event_name), + } +} + +fn common_subagent_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> SubagentEvent { + let session = common_session_event(payload, headers, kind); + let subagent_id = subagent_id(payload) + .or_else(|| header_string(headers, "x-nemo-flow-subagent-id")) + .unwrap_or_else(|| "subagent".to_string()); + SubagentEvent { + session_id: session.session_id, + agent_kind: kind, + event_name: session.event_name, + subagent_id, + payload: session.payload, + metadata: session.metadata, + } +} + +fn common_tool_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> ToolEvent { + let session = common_session_event(payload, headers, kind); + let tool_call_id = string_at(payload, &["tool_call_id"]) + .or_else(|| string_at(payload, &["toolCallId"])) + .or_else(|| string_at(payload, &["call_id"])) + .or_else(|| string_at(payload, &["tool", "id"])) + .or_else(|| string_at(payload, &["tool_input", "id"])) + .or_else(|| string_at(payload, &["id"])) + .unwrap_or_else(|| format!("tool-{}", Uuid::now_v7())); + let tool_name = string_at(payload, &["tool_name"]) + .or_else(|| string_at(payload, &["toolName"])) + .or_else(|| string_at(payload, &["tool", "name"])) + .or_else(|| string_at(payload, &["tool_input", "name"])) + .or_else(|| string_at(payload, &["name"])) + .unwrap_or_else(|| "unknown_tool".to_string()); + let arguments = value_at(payload, &["tool_input"]) + .or_else(|| value_at(payload, &["input"])) + .or_else(|| value_at(payload, &["arguments"])) + .or_else(|| value_at(payload, &["args"])) + .unwrap_or(Value::Null); + let result = value_at(payload, &["tool_output"]) + .or_else(|| value_at(payload, &["output"])) + .or_else(|| value_at(payload, &["result"])) + .unwrap_or(Value::Null); + ToolEvent { + session_id: session.session_id, + agent_kind: kind, + event_name: session.event_name, + tool_call_id, + tool_name, + subagent_id: subagent_id(payload) + .or_else(|| header_string(headers, "x-nemo-flow-subagent-id")), + arguments, + result, + status: string_at(payload, &["status"]) + .or_else(|| string_at(payload, &["decision"])) + .or_else(|| string_at(payload, &["permission"])), + payload: session.payload, + metadata: session.metadata, + } +} + +fn subagent_id(payload: &Value) -> Option { + string_at(payload, &["subagent_id"]) + .or_else(|| string_at(payload, &["subagentId"])) + .or_else(|| string_at(payload, &["subagent", "id"])) + .or_else(|| string_at(payload, &["agent", "id"])) +} + +fn string_at(payload: &Value, path: &[&str]) -> Option { + value_at(payload, path).and_then(|value| match value { + Value::String(value) => Some(value), + Value::Number(value) => Some(value.to_string()), + Value::Bool(value) => Some(value.to_string()), + _ => None, + }) +} + +fn value_at(payload: &Value, path: &[&str]) -> Option { + let mut current = payload; + for key in path { + current = current.get(*key)?; + } + Some(current.clone()) +} + +fn classify( + payload: &Value, + headers: &HeaderMap, + rules: &ClassificationRules<'_>, +) -> NormalizedEvent { + let event = event_name(payload); + let normalized = normalize_name(&event); + if rules + .agent_start + .iter() + .any(|name| normalize_name(name) == normalized) + { + NormalizedEvent::AgentStarted(common_session_event(payload, headers, rules.kind)) + } else if rules + .agent_end + .iter() + .any(|name| normalize_name(name) == normalized) + { + NormalizedEvent::AgentEnded(common_session_event(payload, headers, rules.kind)) + } else if rules + .subagent_start + .iter() + .any(|name| normalize_name(name) == normalized) + { + NormalizedEvent::SubagentStarted(common_subagent_event(payload, headers, rules.kind)) + } else if rules + .subagent_end + .iter() + .any(|name| normalize_name(name) == normalized) + { + NormalizedEvent::SubagentEnded(common_subagent_event(payload, headers, rules.kind)) + } else if rules + .tool_start + .iter() + .any(|name| normalize_name(name) == normalized) + { + NormalizedEvent::ToolStarted(common_tool_event(payload, headers, rules.kind)) + } else if rules + .tool_end + .iter() + .any(|name| normalize_name(name) == normalized) + { + NormalizedEvent::ToolEnded(common_tool_event(payload, headers, rules.kind)) + } else { + match normalized.as_str() { + "beforesubmitprompt" | "promptsubmitted" | "userpromptsubmit" => { + NormalizedEvent::PromptSubmitted(common_session_event(payload, headers, rules.kind)) + } + "afteragentresponse" | "agentresponse" | "assistantresponse" => { + NormalizedEvent::AgentResponse(common_session_event(payload, headers, rules.kind)) + } + "precompact" | "compaction" => { + NormalizedEvent::Compaction(common_session_event(payload, headers, rules.kind)) + } + "notification" => { + NormalizedEvent::Notification(common_session_event(payload, headers, rules.kind)) + } + _ => NormalizedEvent::HookMark(common_session_event(payload, headers, rules.kind)), + } + } +} + +fn normalize_name(name: &str) -> String { + name.chars() + .filter(|character| character.is_ascii_alphanumeric()) + .flat_map(char::to_lowercase) + .collect() +} + +#[cfg(test)] +mod tests { + use axum::http::HeaderMap; + use serde_json::json; + + use super::*; + use crate::adapters::{claude_code, codex, cursor}; + + #[test] + fn maps_claude_canonical_tool_payload() { + let headers = HeaderMap::new(); + let outcome = claude_code::adapt( + json!({ + "session_id": "claude-session", + "transcript_path": "/tmp/transcript.jsonl", + "cwd": "/workspace", + "hook_event_name": "PreToolUse", + "tool_name": "Read", + "tool_input": { "file_path": "README.md" } + }), + &headers, + ); + match &outcome.events[0] { + NormalizedEvent::ToolStarted(event) => { + assert_eq!(event.session_id, "claude-session"); + assert_eq!(event.tool_name, "Read"); + assert_eq!(event.arguments, json!({ "file_path": "README.md" })); + assert_eq!( + event.metadata["transcript_path"], + json!("/tmp/transcript.jsonl") + ); + } + event => panic!("unexpected event: {event:?}"), + } + assert_eq!(outcome.response["continue"], json!(true)); + assert_eq!(outcome.response["permissionDecision"], json!("allow")); + } + + #[test] + fn maps_cursor_subagent_and_permission_response() { + let headers = HeaderMap::new(); + let outcome = cursor::adapt( + json!({ + "session_id": "cursor-session", + "project_dir": "/repo", + "user_email": "dev@example.com", + "hook_event_name": "beforeShellExecution", + "subagent": { "id": "worker" }, + "tool_call_id": "shell-1", + "tool_name": "shell", + "input": { "command": "cargo test" } + }), + &headers, + ); + match &outcome.events[0] { + NormalizedEvent::ToolStarted(event) => { + assert_eq!(event.session_id, "cursor-session"); + assert_eq!(event.subagent_id.as_deref(), Some("worker")); + assert_eq!(event.metadata["project_dir"], json!("/repo")); + assert_eq!(event.metadata["user_email"], json!("dev@example.com")); + } + event => panic!("unexpected event: {event:?}"), + } + assert_eq!(outcome.response["permission"], json!("allow")); + } + + #[test] + fn keeps_codex_response_unwrapped() { + let headers = HeaderMap::new(); + let outcome = codex::adapt( + json!({ + "session_id": "codex-session", + "hook_event_name": "sessionStart" + }), + &headers, + ); + assert!(matches!( + outcome.events[0], + NormalizedEvent::AgentStarted(_) + )); + assert_eq!(outcome.response, json!({})); + } +} diff --git a/crates/sidecar/src/config.rs b/crates/sidecar/src/config.rs new file mode 100644 index 0000000..620d1aa --- /dev/null +++ b/crates/sidecar/src/config.rs @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::net::SocketAddr; +use std::path::PathBuf; + +use axum::http::HeaderMap; +use clap::{Args, Parser, Subcommand, ValueEnum}; +use serde_json::Value; + +#[derive(Debug, Clone, Parser)] +#[command(name = "nemo-flow-sidecar")] +#[command(about = "Gateway sidecar for coding-agent NeMo Flow observability")] +pub(crate) struct Cli { + #[command(flatten)] + pub(crate) server: SidecarConfig, + #[command(subcommand)] + pub(crate) command: Option, +} + +#[derive(Debug, Clone, Subcommand)] +pub(crate) enum Command { + Install(InstallCommand), + HookForward(HookForwardCommand), +} + +#[derive(Debug, Clone, Args)] +pub(crate) struct SidecarConfig { + #[arg(long, env = "NEMO_FLOW_SIDECAR_BIND", default_value = "127.0.0.1:4040")] + pub(crate) bind: SocketAddr, + #[arg( + long, + env = "NEMO_FLOW_OPENAI_BASE_URL", + default_value = "https://api.openai.com" + )] + pub(crate) openai_base_url: String, + #[arg( + long, + env = "NEMO_FLOW_ANTHROPIC_BASE_URL", + default_value = "https://api.anthropic.com" + )] + pub(crate) anthropic_base_url: String, + #[arg(long, env = "NEMO_FLOW_ATIF_DIR")] + pub(crate) atif_dir: Option, + #[arg(long, env = "NEMO_FLOW_OPENINFERENCE_ENDPOINT")] + pub(crate) openinference_endpoint: Option, +} + +#[derive(Debug, Clone, Args)] +pub(crate) struct InstallCommand { + #[arg(value_enum)] + pub(crate) agent: CodingAgent, + #[arg(long, value_enum, default_value = "user")] + pub(crate) scope: InstallScope, + #[arg(long, value_enum, default_value = "both")] + pub(crate) target: InstallTarget, + #[arg(long, default_value = "http://127.0.0.1:4040")] + pub(crate) sidecar_url: String, + #[arg(long)] + pub(crate) atif_dir: Option, + #[arg(long)] + pub(crate) openinference_endpoint: Option, + #[arg(long)] + pub(crate) profile: Option, + #[arg(long)] + pub(crate) session_metadata: Option, + #[arg(long)] + pub(crate) plugin_config: Option, + #[arg(long, value_enum)] + pub(crate) gateway_mode: Option, + #[arg(long)] + pub(crate) dry_run: bool, + #[arg(long)] + pub(crate) print: bool, + #[arg(long, hide = true)] + pub(crate) home_dir: Option, + #[arg(long, hide = true)] + pub(crate) project_dir: Option, +} + +#[derive(Debug, Clone, Args)] +pub(crate) struct HookForwardCommand { + #[arg(value_enum)] + pub(crate) agent: CodingAgent, + #[arg(long, default_value = "http://127.0.0.1:4040")] + pub(crate) sidecar_url: String, + #[arg(long)] + pub(crate) atif_dir: Option, + #[arg(long)] + pub(crate) openinference_endpoint: Option, + #[arg(long)] + pub(crate) profile: Option, + #[arg(long)] + pub(crate) session_metadata: Option, + #[arg(long)] + pub(crate) plugin_config: Option, + #[arg(long, value_enum)] + pub(crate) gateway_mode: Option, + #[arg(long)] + pub(crate) fail_closed: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub(crate) enum CodingAgent { + ClaudeCode, + Codex, + Cursor, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub(crate) enum InstallScope { + User, + Project, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub(crate) enum InstallTarget { + Cli, + Gui, + Both, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +#[value(rename_all = "kebab-case")] +pub(crate) enum GatewayMode { + HookOnly, + Passthrough, + Required, +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct SessionConfig { + pub(crate) atif_dir: Option, + pub(crate) openinference_endpoint: Option, + pub(crate) metadata: Option, + pub(crate) plugin_config: Option, + pub(crate) profile: Option, + pub(crate) gateway_mode: Option, +} + +impl SidecarConfig { + pub(crate) fn session_config_from_headers(&self, headers: &HeaderMap) -> SessionConfig { + let atif_dir = header_string(headers, "x-nemo-flow-atif-dir") + .map(PathBuf::from) + .or_else(|| self.atif_dir.clone()); + let openinference_endpoint = header_string(headers, "x-nemo-flow-openinference-endpoint") + .or_else(|| self.openinference_endpoint.clone()); + let metadata = header_json(headers, "x-nemo-flow-session-metadata"); + let plugin_config = header_json(headers, "x-nemo-flow-plugin-config"); + let profile = header_string(headers, "x-nemo-flow-config-profile"); + let gateway_mode = header_string(headers, "x-nemo-flow-gateway-mode"); + SessionConfig { + atif_dir, + openinference_endpoint, + metadata, + plugin_config, + profile, + gateway_mode, + } + } +} + +pub(crate) fn header_string(headers: &HeaderMap, name: &str) -> Option { + headers + .get(name) + .and_then(|value| value.to_str().ok()) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) +} + +fn header_json(headers: &HeaderMap, name: &str) -> Option { + header_string(headers, name).and_then(|raw| serde_json::from_str(&raw).ok()) +} + +impl CodingAgent { + pub(crate) const fn hook_path(self) -> &'static str { + match self { + Self::ClaudeCode => "/hooks/claude-code", + Self::Codex => "/hooks/codex", + Self::Cursor => "/hooks/cursor", + } + } + + pub(crate) const fn as_arg(self) -> &'static str { + match self { + Self::ClaudeCode => "claude-code", + Self::Codex => "codex", + Self::Cursor => "cursor", + } + } +} + +impl GatewayMode { + pub(crate) const fn as_arg(self) -> &'static str { + match self { + Self::HookOnly => "hook-only", + Self::Passthrough => "passthrough", + Self::Required => "required", + } + } +} diff --git a/crates/sidecar/src/error.rs b/crates/sidecar/src/error.rs new file mode 100644 index 0000000..4cdfb53 --- /dev/null +++ b/crates/sidecar/src/error.rs @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use axum::Json; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use serde_json::json; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum SidecarError { + #[error("invalid hook payload: {0}")] + InvalidPayload(String), + #[error("gateway upstream error: {0}")] + Upstream(#[from] reqwest::Error), + #[error("http error: {0}")] + Http(#[from] http::Error), + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("installer error: {0}")] + Install(String), + #[error("NeMo Flow runtime error: {0}")] + Flow(#[from] nemo_flow::error::FlowError), + #[error("openinference error: {0}")] + OpenInference(#[from] nemo_flow::observability::openinference::OpenInferenceError), +} + +impl IntoResponse for SidecarError { + fn into_response(self) -> Response { + let status = match self { + Self::InvalidPayload(_) => StatusCode::BAD_REQUEST, + Self::Upstream(_) => StatusCode::BAD_GATEWAY, + Self::Http(_) + | Self::Io(_) + | Self::Install(_) + | Self::Flow(_) + | Self::OpenInference(_) => StatusCode::INTERNAL_SERVER_ERROR, + }; + let body = Json(json!({ + "error": { + "message": self.to_string(), + "type": "nemo_flow_sidecar_error" + } + })); + (status, body).into_response() + } +} diff --git a/crates/sidecar/src/gateway.rs b/crates/sidecar/src/gateway.rs new file mode 100644 index 0000000..7b84e9a --- /dev/null +++ b/crates/sidecar/src/gateway.rs @@ -0,0 +1,323 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use axum::body::{Body, Bytes}; +use axum::extract::State; +use axum::http::{HeaderMap, HeaderName, Method, Request, Response, StatusCode}; +use futures_util::StreamExt; +use nemo_flow::api::llm::LlmRequest; +use serde_json::{Map, Value, json}; + +use crate::config::header_string; +use crate::error::SidecarError; +use crate::model::AgentKind; +use crate::server::AppState; +use crate::session::LlmGatewayStart; + +pub(crate) async fn passthrough( + State(state): State, + request: Request, +) -> Result, SidecarError> { + let (parts, body) = request.into_parts(); + let provider = ProviderRoute::from_path(parts.uri.path()).ok_or_else(|| { + SidecarError::InvalidPayload(format!("unsupported gateway path {}", parts.uri.path())) + })?; + let body_bytes = axum::body::to_bytes(body, usize::MAX) + .await + .map_err(|error| SidecarError::InvalidPayload(error.to_string()))?; + let request_json = serde_json::from_slice::(&body_bytes).unwrap_or(Value::Null); + let upstream_url = provider.upstream_url( + &state.config, + parts + .uri + .path_and_query() + .map(|p| p.as_str()) + .unwrap_or(parts.uri.path()), + ); + let streaming = request_json + .get("stream") + .and_then(Value::as_bool) + .unwrap_or(false); + let session_id = gateway_session_id(&parts.headers); + let llm_request = LlmRequest { + headers: observable_headers(&parts.headers), + content: request_json.clone(), + }; + let active = state + .sessions + .start_llm( + &parts.headers, + LlmGatewayStart { + session_id, + provider: provider.name().to_string(), + model_name: request_json + .get("model") + .and_then(Value::as_str) + .map(ToOwned::to_owned), + request: llm_request, + streaming, + metadata: json!({ "gateway_path": parts.uri.path() }), + }, + ) + .await?; + + let mut upstream = state + .http + .request(parts.method.clone(), upstream_url) + .body(body_bytes.clone()); + for (name, value) in &parts.headers { + if should_forward_request_header(name) { + upstream = upstream.header(name, value); + } + } + let upstream_response = upstream.send().await?; + let status = upstream_response.status(); + let headers = response_headers(upstream_response.headers()); + let content_type = upstream_response + .headers() + .get(http::header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .unwrap_or_default() + .to_ascii_lowercase(); + let is_stream = streaming || content_type.contains("text/event-stream"); + + if is_stream { + let sessions = state.sessions.clone(); + let stream = upstream_response.bytes_stream(); + let body = Body::from_stream(async_stream::stream! { + let mut stream = stream; + let mut collected = Vec::new(); + let mut truncated = false; + while let Some(chunk) = stream.next().await { + match chunk { + Ok(bytes) => { + if collected.len() + bytes.len() <= 1_048_576 { + collected.extend_from_slice(&bytes); + } else { + truncated = true; + } + yield Ok::(bytes); + } + Err(error) => { + yield Err(error); + return; + } + } + } + let response = stream_response_json(&collected, truncated); + let _ = sessions + .end_llm( + active, + response, + json!({ "http_status": status.as_u16(), "streaming": true, "stream_truncated": truncated }), + ) + .await; + }); + return build_response(status, headers, body); + } + + let bytes = upstream_response.bytes().await?; + let response_json = serde_json::from_slice::(&bytes) + .unwrap_or_else(|_| json!({ "body_bytes": bytes.len() })); + state + .sessions + .end_llm( + active, + response_json, + json!({ "http_status": status.as_u16(), "streaming": false }), + ) + .await?; + build_response(status, headers, Body::from(bytes)) +} + +pub(crate) async fn models( + State(state): State, + request: Request, +) -> Result, SidecarError> { + let (parts, _body) = request.into_parts(); + if parts.method != Method::GET { + return build_response( + StatusCode::METHOD_NOT_ALLOWED, + HeaderMap::new(), + Body::empty(), + ); + } + let provider = ProviderRoute::OpenAiModels; + let upstream_url = provider.upstream_url( + &state.config, + parts + .uri + .path_and_query() + .map(|p| p.as_str()) + .unwrap_or(parts.uri.path()), + ); + let mut upstream = state.http.get(upstream_url); + for (name, value) in &parts.headers { + if should_forward_request_header(name) { + upstream = upstream.header(name, value); + } + } + let upstream_response = upstream.send().await?; + let status = upstream_response.status(); + let headers = response_headers(upstream_response.headers()); + let bytes = upstream_response.bytes().await?; + build_response(status, headers, Body::from(bytes)) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ProviderRoute { + OpenAiResponses, + OpenAiChatCompletions, + OpenAiModels, + AnthropicMessages, + AnthropicCountTokens, +} + +impl ProviderRoute { + fn from_path(path: &str) -> Option { + match path { + "/v1/responses" => Some(Self::OpenAiResponses), + "/v1/chat/completions" => Some(Self::OpenAiChatCompletions), + "/v1/models" => Some(Self::OpenAiModels), + "/v1/messages" => Some(Self::AnthropicMessages), + "/v1/messages/count_tokens" => Some(Self::AnthropicCountTokens), + _ => None, + } + } + + const fn name(self) -> &'static str { + match self { + Self::OpenAiResponses => "openai.responses", + Self::OpenAiChatCompletions => "openai.chat_completions", + Self::OpenAiModels => "openai.models", + Self::AnthropicMessages => "anthropic.messages", + Self::AnthropicCountTokens => "anthropic.count_tokens", + } + } + + fn upstream_url(self, config: &crate::config::SidecarConfig, path_and_query: &str) -> String { + let base = match self { + Self::OpenAiResponses | Self::OpenAiChatCompletions | Self::OpenAiModels => { + config.openai_base_url.trim_end_matches('/') + } + Self::AnthropicMessages | Self::AnthropicCountTokens => { + config.anthropic_base_url.trim_end_matches('/') + } + }; + format!("{base}{path_and_query}") + } +} + +fn gateway_session_id(headers: &HeaderMap) -> String { + header_string(headers, "x-nemo-flow-session-id") + .or_else(|| header_string(headers, "x-claude-code-session-id")) + .or_else(|| { + header_string(headers, "anthropic-beta").map(|value| format!("anthropic:{value}")) + }) + .unwrap_or_else(|| format!("{}-gateway", AgentKind::Gateway.as_str())) +} + +fn observable_headers(headers: &HeaderMap) -> Map { + let mut output = Map::new(); + for (name, value) in headers { + if should_record_header(name) + && let Ok(value) = value.to_str() + { + output.insert(name.as_str().to_string(), json!(value)); + } + } + output +} + +fn response_headers(headers: &HeaderMap) -> HeaderMap { + let mut output = HeaderMap::new(); + for (name, value) in headers { + if !is_hop_by_hop(name) { + output.insert(name.clone(), value.clone()); + } + } + output +} + +fn build_response( + status: StatusCode, + headers: HeaderMap, + body: Body, +) -> Result, SidecarError> { + let mut builder = Response::builder().status(status); + for (name, value) in headers { + if let Some(name) = name { + builder = builder.header(name, value); + } + } + Ok(builder.body(body)?) +} + +fn should_forward_request_header(name: &HeaderName) -> bool { + !is_hop_by_hop(name) && name != http::header::HOST && name != http::header::CONTENT_LENGTH +} + +fn should_record_header(name: &HeaderName) -> bool { + should_forward_request_header(name) + && name != http::header::AUTHORIZATION + && name.as_str() != "x-api-key" + && name.as_str() != "anthropic-api-key" +} + +fn is_hop_by_hop(name: &HeaderName) -> bool { + matches!( + name.as_str(), + "connection" + | "keep-alive" + | "proxy-authenticate" + | "proxy-authorization" + | "te" + | "trailer" + | "transfer-encoding" + | "upgrade" + ) +} + +fn stream_response_json(collected: &[u8], truncated: bool) -> Value { + if truncated { + return json!({ + "stream_preview": String::from_utf8_lossy(collected), + "stream_truncated": true + }); + } + json!({ "stream": String::from_utf8_lossy(collected) }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn removes_hop_by_hop_headers() { + assert!(!should_forward_request_header(&HeaderName::from_static( + "connection" + ))); + assert!(!should_forward_request_header(&HeaderName::from_static( + "host" + ))); + assert!(should_forward_request_header(&HeaderName::from_static( + "authorization" + ))); + assert!(!should_record_header(&HeaderName::from_static( + "authorization" + ))); + } + + #[test] + fn selects_provider_routes() { + assert_eq!( + ProviderRoute::from_path("/v1/responses"), + Some(ProviderRoute::OpenAiResponses) + ); + assert_eq!( + ProviderRoute::from_path("/v1/messages/count_tokens"), + Some(ProviderRoute::AnthropicCountTokens) + ); + assert_eq!(ProviderRoute::from_path("/unsupported"), None); + } +} diff --git a/crates/sidecar/src/installer.rs b/crates/sidecar/src/installer.rs new file mode 100644 index 0000000..95fa665 --- /dev/null +++ b/crates/sidecar/src/installer.rs @@ -0,0 +1,601 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue}; +use serde_json::{Value, json}; +use toml_edit::{DocumentMut, table, value}; + +use crate::config::{ + CodingAgent, GatewayMode, HookForwardCommand, InstallCommand, InstallScope, InstallTarget, +}; +use crate::error::SidecarError; + +const HOOK_EVENTS: &[&str] = &[ + "SessionStart", + "UserPromptSubmit", + "PreToolUse", + "PostToolUse", + "PostToolUseFailure", + "SubagentStart", + "SubagentStop", + "Stop", + "PreCompact", + "SessionEnd", +]; + +const CURSOR_HOOK_EVENTS: &[&str] = &[ + "sessionStart", + "beforeSubmitPrompt", + "preToolUse", + "beforeShellExecution", + "beforeMCPExecution", + "postToolUse", + "afterShellExecution", + "afterMCPExecution", + "subagentStart", + "subagentStop", + "afterAgentResponse", + "preCompact", + "stop", + "sessionEnd", +]; + +#[derive(Debug, Clone)] +struct PlannedFile { + path: PathBuf, + contents: String, +} + +pub(crate) fn install(command: InstallCommand) -> Result<(), SidecarError> { + validate_optional_json("session metadata", command.session_metadata.as_deref())?; + validate_optional_json("plugin config", command.plugin_config.as_deref())?; + let files = planned_files(&command)?; + if command.print { + for file in &files { + println!("--- {}", file.path.display()); + print!("{}", file.contents); + if !file.contents.ends_with('\n') { + println!(); + } + } + } + if command.dry_run { + println!( + "Dry run: would install {} integration for {:?} {:?}.", + command.agent.as_arg(), + command.scope, + command.target + ); + return Ok(()); + } + for file in &files { + write_planned_file(file)?; + println!("Installed {}", file.path.display()); + } + print_target_note(command.agent, command.target); + Ok(()) +} + +pub(crate) async fn hook_forward(command: HookForwardCommand) -> Result<(), SidecarError> { + validate_optional_json("session metadata", command.session_metadata.as_deref())?; + validate_optional_json("plugin config", command.plugin_config.as_deref())?; + + let mut input = String::new(); + std::io::stdin().read_to_string(&mut input)?; + if input.trim().is_empty() { + input = "{}".to_string(); + } + + let url = format!( + "{}{}", + command.sidecar_url.trim_end_matches('/'), + command.agent.hook_path() + ); + let response = reqwest::Client::new() + .post(url) + .headers(sidecar_headers( + command.atif_dir.as_deref(), + command.openinference_endpoint.as_deref(), + command.profile.as_deref(), + command.session_metadata.as_deref(), + command.plugin_config.as_deref(), + command.gateway_mode, + )?) + .header(CONTENT_TYPE, "application/json") + .body(input) + .send() + .await; + + match response { + Ok(response) => { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + if !status.is_success() { + eprintln!("nemo-flow-sidecar hook forward failed with HTTP {status}"); + if command.fail_closed { + return Err(SidecarError::Install(format!( + "hook forward failed with HTTP {status}" + ))); + } + } + if !body.is_empty() { + println!("{body}"); + } + Ok(()) + } + Err(error) => { + eprintln!("nemo-flow-sidecar hook forward failed: {error}"); + if command.fail_closed { + Err(SidecarError::Upstream(error)) + } else { + Ok(()) + } + } + } +} + +fn planned_files(command: &InstallCommand) -> Result, SidecarError> { + let base = install_base(command)?; + match command.agent { + CodingAgent::ClaudeCode => { + let path = base.join(".claude/settings.json"); + let existing = read_json_file(&path)?; + let contents = serde_json::to_string_pretty(&merge_hooks( + existing, + claude_hooks(&hook_command(command, CodingAgent::ClaudeCode)), + )?) + .map_err(|error| SidecarError::Install(error.to_string()))?; + Ok(vec![PlannedFile { path, contents }]) + } + CodingAgent::Codex => { + let config_path = base.join(".codex/config.toml"); + let hooks_path = base.join(".codex/hooks.json"); + let config = + merge_codex_config(&std::fs::read_to_string(&config_path).unwrap_or_default())?; + let hooks = serde_json::to_string_pretty(&merge_hooks( + read_json_file(&hooks_path)?, + codex_hooks(&hook_command(command, CodingAgent::Codex)), + )?) + .map_err(|error| SidecarError::Install(error.to_string()))?; + Ok(vec![ + PlannedFile { + path: config_path, + contents: config, + }, + PlannedFile { + path: hooks_path, + contents: hooks, + }, + ]) + } + CodingAgent::Cursor => { + let path = base.join(".cursor/hooks.json"); + let existing = read_json_file(&path)?; + let contents = serde_json::to_string_pretty(&merge_hooks( + existing, + cursor_hooks(&hook_command(command, CodingAgent::Cursor)), + )?) + .map_err(|error| SidecarError::Install(error.to_string()))?; + Ok(vec![PlannedFile { path, contents }]) + } + } +} + +fn install_base(command: &InstallCommand) -> Result { + match command.scope { + InstallScope::User => command + .home_dir + .clone() + .or_else(home_dir) + .ok_or_else(|| SidecarError::Install("could not resolve home directory".into())), + InstallScope::Project => command + .project_dir + .clone() + .map(Ok) + .unwrap_or_else(std::env::current_dir) + .map_err(SidecarError::from), + } +} + +fn hook_command(command: &InstallCommand, agent: CodingAgent) -> String { + let mut args = vec![ + "nemo-flow-sidecar".to_string(), + "hook-forward".to_string(), + agent.as_arg().to_string(), + "--sidecar-url".to_string(), + command.sidecar_url.clone(), + ]; + push_optional_path(&mut args, "--atif-dir", command.atif_dir.as_deref()); + push_optional( + &mut args, + "--openinference-endpoint", + command.openinference_endpoint.as_deref(), + ); + push_optional(&mut args, "--profile", command.profile.as_deref()); + push_optional( + &mut args, + "--session-metadata", + command.session_metadata.as_deref(), + ); + push_optional( + &mut args, + "--plugin-config", + command.plugin_config.as_deref(), + ); + push_optional_gateway_mode(&mut args, command.gateway_mode); + args.into_iter() + .map(|arg| shell_quote(&arg)) + .collect::>() + .join(" ") +} + +fn push_optional(args: &mut Vec, flag: &str, value: Option<&str>) { + if let Some(value) = value { + args.push(flag.to_string()); + args.push(value.to_string()); + } +} + +fn push_optional_path(args: &mut Vec, flag: &str, value: Option<&Path>) { + if let Some(value) = value { + args.push(flag.to_string()); + args.push(value.display().to_string()); + } +} + +fn push_optional_gateway_mode(args: &mut Vec, gateway_mode: Option) { + if let Some(gateway_mode) = gateway_mode { + args.push("--gateway-mode".to_string()); + args.push(gateway_mode.as_arg().to_string()); + } +} + +fn shell_quote(value: &str) -> String { + if value + .chars() + .all(|character| character.is_ascii_alphanumeric() || "-_./:=,".contains(character)) + { + value.to_string() + } else { + format!("'{}'", value.replace('\'', "'\\''")) + } +} + +fn claude_hooks(command: &str) -> Value { + hooks_for_events(HOOK_EVENTS, command, true) +} + +fn codex_hooks(command: &str) -> Value { + hooks_for_events(HOOK_EVENTS, command, true) +} + +fn cursor_hooks(command: &str) -> Value { + hooks_for_events(CURSOR_HOOK_EVENTS, command, true) +} + +fn hooks_for_events(events: &[&str], command: &str, matcher_for_tools: bool) -> Value { + let hooks: serde_json::Map = events + .iter() + .map(|event| { + let mut group = serde_json::Map::new(); + if matcher_for_tools && event_matches_tools(event) { + group.insert("matcher".into(), json!("*")); + } + group.insert( + "hooks".into(), + json!([{ + "type": "command", + "command": command, + "timeout": 30 + }]), + ); + ( + (*event).to_string(), + Value::Array(vec![Value::Object(group)]), + ) + }) + .collect(); + json!({ "hooks": Value::Object(hooks) }) +} + +fn event_matches_tools(event: &str) -> bool { + matches!( + event, + "PreToolUse" + | "PostToolUse" + | "PostToolUseFailure" + | "PermissionRequest" + | "preToolUse" + | "postToolUse" + | "beforeShellExecution" + | "afterShellExecution" + | "beforeMCPExecution" + | "afterMCPExecution" + ) +} + +fn merge_hooks(existing: Value, generated: Value) -> Result { + let mut root = match existing { + Value::Null => json!({}), + Value::Object(object) => Value::Object(object), + _ => { + return Err(SidecarError::Install( + "hook config must be a JSON object".into(), + )); + } + }; + let root_object = root.as_object_mut().expect("root checked as object"); + let hooks = root_object + .entry("hooks") + .or_insert_with(|| json!({})) + .as_object_mut() + .ok_or_else(|| SidecarError::Install("hooks must be a JSON object".into()))?; + let generated_hooks = generated + .get("hooks") + .and_then(Value::as_object) + .ok_or_else(|| SidecarError::Install("generated hooks were malformed".into()))?; + for (event, groups) in generated_hooks { + let groups = groups + .as_array() + .ok_or_else(|| SidecarError::Install("generated hook groups were malformed".into()))?; + let event_groups = hooks.entry(event.clone()).or_insert_with(|| json!([])); + let event_groups = event_groups + .as_array_mut() + .ok_or_else(|| SidecarError::Install(format!("{event} hooks must be an array")))?; + for group in groups { + if !event_groups.iter().any(|existing| existing == group) { + event_groups.push(group.clone()); + } + } + } + Ok(root) +} + +fn merge_codex_config(existing: &str) -> Result { + let mut document = if existing.trim().is_empty() { + DocumentMut::new() + } else { + existing + .parse::() + .map_err(|error| SidecarError::Install(format!("invalid TOML: {error}")))? + }; + if !document.as_table().contains_key("features") { + document["features"] = table(); + } + document["features"]["codex_hooks"] = value(true); + Ok(document.to_string()) +} + +fn read_json_file(path: &Path) -> Result { + match std::fs::read_to_string(path) { + Ok(raw) => serde_json::from_str(&raw).map_err(|error| { + SidecarError::Install(format!("invalid JSON in {}: {error}", path.display())) + }), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Value::Null), + Err(error) => Err(SidecarError::Io(error)), + } +} + +fn write_planned_file(file: &PlannedFile) -> Result<(), SidecarError> { + if let Some(parent) = file.path.parent() { + std::fs::create_dir_all(parent)?; + } + if file.path.exists() { + std::fs::copy(&file.path, backup_path(&file.path)?)?; + } + std::fs::write(&file.path, &file.contents)?; + Ok(()) +} + +fn backup_path(path: &Path) -> Result { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| SidecarError::Install(error.to_string()))? + .as_secs(); + Ok(path.with_extension(format!( + "{}.bak.{timestamp}", + path.extension() + .and_then(|extension| extension.to_str()) + .unwrap_or("config") + ))) +} + +fn home_dir() -> Option { + std::env::var_os("HOME") + .or_else(|| std::env::var_os("USERPROFILE")) + .map(PathBuf::from) +} + +fn validate_optional_json(name: &str, value: Option<&str>) -> Result<(), SidecarError> { + if let Some(value) = value { + serde_json::from_str::(value) + .map_err(|error| SidecarError::Install(format!("invalid {name}: {error}")))?; + } + Ok(()) +} + +fn sidecar_headers( + atif_dir: Option<&Path>, + openinference_endpoint: Option<&str>, + profile: Option<&str>, + session_metadata: Option<&str>, + plugin_config: Option<&str>, + gateway_mode: Option, +) -> Result { + let mut headers = HeaderMap::new(); + insert_header_path(&mut headers, "x-nemo-flow-atif-dir", atif_dir)?; + insert_header( + &mut headers, + "x-nemo-flow-openinference-endpoint", + openinference_endpoint, + )?; + insert_header(&mut headers, "x-nemo-flow-config-profile", profile)?; + insert_header( + &mut headers, + "x-nemo-flow-session-metadata", + session_metadata, + )?; + insert_header(&mut headers, "x-nemo-flow-plugin-config", plugin_config)?; + insert_header( + &mut headers, + "x-nemo-flow-gateway-mode", + gateway_mode.map(GatewayMode::as_arg), + )?; + Ok(headers) +} + +fn insert_header( + headers: &mut HeaderMap, + name: &'static str, + value: Option<&str>, +) -> Result<(), SidecarError> { + if let Some(value) = value { + headers.insert( + HeaderName::from_static(name), + HeaderValue::from_str(value).map_err(|error| { + SidecarError::Install(format!("invalid header {name}: {error}")) + })?, + ); + } + Ok(()) +} + +fn insert_header_path( + headers: &mut HeaderMap, + name: &'static str, + value: Option<&Path>, +) -> Result<(), SidecarError> { + if let Some(value) = value { + let value = value.to_string_lossy(); + insert_header(headers, name, Some(value.as_ref())) + } else { + Ok(()) + } +} + +fn print_target_note(agent: CodingAgent, target: InstallTarget) { + match (agent, target) { + (CodingAgent::ClaudeCode, InstallTarget::Gui | InstallTarget::Both) => { + println!( + "Note: Claude application/web sessions are not configured by Claude Code hooks." + ); + } + (CodingAgent::Codex, InstallTarget::Gui | InstallTarget::Both) => { + println!( + "Note: Codex GUI local sessions can use local config; cloud tasks need separate gateway support." + ); + } + (CodingAgent::Cursor, InstallTarget::Cli | InstallTarget::Both) => { + println!( + "Note: run the Cursor CLI smoke test to confirm cursor-agent loads hooks in your version." + ); + } + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn command(agent: CodingAgent, root: &Path) -> InstallCommand { + InstallCommand { + agent, + scope: InstallScope::User, + target: InstallTarget::Both, + sidecar_url: "http://127.0.0.1:4040".into(), + atif_dir: Some(root.join("atif")), + openinference_endpoint: Some("http://otel:4318/v1/traces".into()), + profile: Some("default".into()), + session_metadata: Some(r#"{"team":"agent-observability"}"#.into()), + plugin_config: Some(r#"{"components":[]}"#.into()), + gateway_mode: Some(GatewayMode::Required), + dry_run: false, + print: false, + home_dir: Some(root.to_path_buf()), + project_dir: None, + } + } + + #[test] + fn generates_claude_install_file() { + let temp = tempfile::tempdir().unwrap(); + let files = planned_files(&command(CodingAgent::ClaudeCode, temp.path())).unwrap(); + assert_eq!(files.len(), 1); + assert!(files[0].path.ends_with(".claude/settings.json")); + let json: Value = serde_json::from_str(&files[0].contents).unwrap(); + assert!(json["hooks"]["SessionStart"].is_array()); + assert!( + json["hooks"]["PreToolUse"][0]["hooks"][0]["command"] + .as_str() + .unwrap() + .contains("hook-forward claude-code") + ); + } + + #[test] + fn generates_codex_config_and_hooks() { + let temp = tempfile::tempdir().unwrap(); + let files = planned_files(&command(CodingAgent::Codex, temp.path())).unwrap(); + assert_eq!(files.len(), 2); + assert!(files[0].contents.contains("codex_hooks = true")); + let json: Value = serde_json::from_str(&files[1].contents).unwrap(); + assert!(json["hooks"]["Stop"].is_array()); + assert!( + json["hooks"]["PreToolUse"][0]["hooks"][0]["command"] + .as_str() + .unwrap() + .contains("hook-forward codex") + ); + } + + #[test] + fn generates_cursor_hooks() { + let temp = tempfile::tempdir().unwrap(); + let files = planned_files(&command(CodingAgent::Cursor, temp.path())).unwrap(); + assert_eq!(files.len(), 1); + let json: Value = serde_json::from_str(&files[0].contents).unwrap(); + assert!(json["hooks"]["beforeShellExecution"].is_array()); + assert!( + json["hooks"]["beforeShellExecution"][0]["hooks"][0]["command"] + .as_str() + .unwrap() + .contains("hook-forward cursor") + ); + } + + #[test] + fn merge_hooks_is_idempotent_and_preserves_existing_entries() { + let existing = json!({ + "hooks": { + "Stop": [{ "hooks": [{ "type": "command", "command": "existing" }] }] + } + }); + let generated = codex_hooks("nemo-flow-sidecar hook-forward codex"); + let once = merge_hooks(existing, generated.clone()).unwrap(); + let twice = merge_hooks(once.clone(), generated).unwrap(); + assert_eq!(once, twice); + assert_eq!(twice["hooks"]["Stop"].as_array().unwrap().len(), 2); + } + + #[test] + fn packaged_hook_configs_are_valid_json() { + let root = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../integrations/coding-agents"); + for path in [ + root.join("claude-code/hooks/hooks.json"), + root.join("codex/hooks/hooks.json"), + root.join("cursor/.cursor/hooks.json"), + root.join("claude-code/.claude-plugin/plugin.json"), + root.join("codex/.codex-plugin/plugin.json"), + ] { + let raw = std::fs::read_to_string(&path).unwrap(); + serde_json::from_str::(&raw) + .unwrap_or_else(|error| panic!("{} is invalid JSON: {error}", path.display())); + } + } +} diff --git a/crates/sidecar/src/main.rs b/crates/sidecar/src/main.rs new file mode 100644 index 0000000..ec5512f --- /dev/null +++ b/crates/sidecar/src/main.rs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! NeMo Flow coding-agent gateway sidecar. + +mod adapters; +mod config; +mod error; +mod gateway; +mod installer; +mod model; +mod server; +mod session; + +use clap::Parser; + +use crate::config::{Cli, Command}; + +#[tokio::main] +async fn main() -> Result<(), error::SidecarError> { + let cli = Cli::parse(); + match cli.command { + Some(Command::Install(command)) => installer::install(command), + Some(Command::HookForward(command)) => installer::hook_forward(command).await, + None => server::serve(cli.server).await, + } +} diff --git a/crates/sidecar/src/model.rs b/crates/sidecar/src/model.rs new file mode 100644 index 0000000..5f53501 --- /dev/null +++ b/crates/sidecar/src/model.rs @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use serde_json::Value; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum AgentKind { + Codex, + ClaudeCode, + Cursor, + Gateway, +} + +impl AgentKind { + pub(crate) const fn as_str(self) -> &'static str { + match self { + Self::Codex => "codex", + Self::ClaudeCode => "claude-code", + Self::Cursor => "cursor", + Self::Gateway => "gateway", + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum NormalizedEvent { + AgentStarted(SessionEvent), + AgentEnded(SessionEvent), + SubagentStarted(SubagentEvent), + SubagentEnded(SubagentEvent), + ToolStarted(ToolEvent), + ToolEnded(ToolEvent), + PromptSubmitted(SessionEvent), + AgentResponse(SessionEvent), + Compaction(SessionEvent), + Notification(SessionEvent), + HookMark(SessionEvent), +} + +impl NormalizedEvent { + pub(crate) fn session_id(&self) -> &str { + match self { + Self::AgentStarted(event) + | Self::AgentEnded(event) + | Self::PromptSubmitted(event) + | Self::AgentResponse(event) + | Self::Compaction(event) + | Self::Notification(event) + | Self::HookMark(event) => &event.session_id, + Self::SubagentStarted(event) | Self::SubagentEnded(event) => &event.session_id, + Self::ToolStarted(event) | Self::ToolEnded(event) => &event.session_id, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct SessionEvent { + pub(crate) session_id: String, + pub(crate) agent_kind: AgentKind, + pub(crate) event_name: String, + pub(crate) payload: Value, + pub(crate) metadata: Value, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct SubagentEvent { + pub(crate) session_id: String, + pub(crate) agent_kind: AgentKind, + pub(crate) event_name: String, + pub(crate) subagent_id: String, + pub(crate) payload: Value, + pub(crate) metadata: Value, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct ToolEvent { + pub(crate) session_id: String, + pub(crate) agent_kind: AgentKind, + pub(crate) event_name: String, + pub(crate) tool_call_id: String, + pub(crate) tool_name: String, + pub(crate) subagent_id: Option, + pub(crate) arguments: Value, + pub(crate) result: Value, + pub(crate) status: Option, + pub(crate) payload: Value, + pub(crate) metadata: Value, +} diff --git a/crates/sidecar/src/server.rs b/crates/sidecar/src/server.rs new file mode 100644 index 0000000..62a92a2 --- /dev/null +++ b/crates/sidecar/src/server.rs @@ -0,0 +1,299 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use axum::extract::State; +use axum::http::HeaderMap; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use reqwest::Client; +use serde_json::Value; +use tokio::net::TcpListener; + +use crate::adapters::{claude_code, codex, cursor}; +use crate::config::SidecarConfig; +use crate::error::SidecarError; +use crate::gateway; +use crate::session::SessionManager; + +#[derive(Clone)] +pub(crate) struct AppState { + pub(crate) config: SidecarConfig, + pub(crate) http: Client, + pub(crate) sessions: SessionManager, +} + +pub(crate) async fn serve(config: SidecarConfig) -> Result<(), SidecarError> { + let listener = TcpListener::bind(config.bind).await?; + let app = router(config); + axum::serve(listener, app).await?; + Ok(()) +} + +pub(crate) fn router(config: SidecarConfig) -> Router { + let sessions = SessionManager::new(config.clone()); + let state = AppState { + config, + http: Client::new(), + sessions, + }; + Router::new() + .route("/hooks/codex", post(codex_hook)) + .route("/hooks/claude-code", post(claude_code_hook)) + .route("/hooks/cursor", post(cursor_hook)) + .route("/v1/responses", post(gateway::passthrough)) + .route("/v1/chat/completions", post(gateway::passthrough)) + .route("/v1/messages", post(gateway::passthrough)) + .route("/v1/messages/count_tokens", post(gateway::passthrough)) + .route("/v1/models", get(gateway::models)) + .with_state(state) +} + +async fn codex_hook( + State(state): State, + headers: HeaderMap, + Json(payload): Json, +) -> Result, SidecarError> { + let outcome = codex::adapt(payload, &headers); + state + .sessions + .apply_events(&headers, outcome.events) + .await?; + Ok(Json(outcome.response)) +} + +async fn claude_code_hook( + State(state): State, + headers: HeaderMap, + Json(payload): Json, +) -> Result, SidecarError> { + let outcome = claude_code::adapt(payload, &headers); + state + .sessions + .apply_events(&headers, outcome.events) + .await?; + Ok(Json(outcome.response)) +} + +async fn cursor_hook( + State(state): State, + headers: HeaderMap, + Json(payload): Json, +) -> Result, SidecarError> { + let outcome = cursor::adapt(payload, &headers); + state + .sessions + .apply_events(&headers, outcome.events) + .await?; + Ok(Json(outcome.response)) +} + +#[cfg(test)] +mod tests { + use axum::body::Body; + use axum::http::{Request, StatusCode, header}; + use axum::response::IntoResponse; + use bytes::Bytes; + use futures_util::stream; + use http_body_util::BodyExt; + use serde_json::{Value, json}; + use tokio::net::TcpListener; + use tower::ServiceExt; + + use super::*; + + fn test_config() -> SidecarConfig { + SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + } + } + + #[tokio::test] + async fn codex_hook_keeps_codex_response_shape() { + let app = router(test_config()); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/hooks/codex") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "session_id": "codex-1", + "hook_event_name": "sessionStart" + }) + .to_string(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body, json!({})); + } + + #[tokio::test] + async fn claude_code_hook_returns_continue_shape() { + let app = router(test_config()); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/hooks/claude-code") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "session_id": "claude-1", + "hook_event_name": "SessionStart" + }) + .to_string(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body["continue"], json!(true)); + } + + #[tokio::test] + async fn cursor_hook_returns_cursor_permission_fields() { + let app = router(test_config()); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/hooks/cursor") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "session_id": "cursor-1", + "hook_event_name": "beforeShellExecution", + "tool_call_id": "shell-1", + "tool_name": "shell", + "input": { "command": "pwd" } + }) + .to_string(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body["continue"], json!(true)); + assert_eq!(body["permission"], json!("allow")); + } + + #[tokio::test] + async fn gateway_forwards_openai_json_without_rewriting_payload() { + let upstream = spawn_upstream(false).await; + let mut config = test_config(); + config.openai_base_url = upstream; + let app = router(config); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/v1/chat/completions") + .header("content-type", "application/json") + .header("authorization", "Bearer test") + .header("connection", "close") + .body(Body::from( + json!({ + "model": "gpt-test", + "messages": [{ "role": "user", "content": "hello" }] + }) + .to_string(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body["model"], json!("gpt-test")); + assert_eq!(body["authorization"], json!("Bearer test")); + assert_eq!(body["connection"], Value::Null); + } + + #[tokio::test] + async fn gateway_preserves_streaming_body() { + let upstream = spawn_upstream(true).await; + let mut config = test_config(); + config.openai_base_url = upstream; + let app = router(config); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/v1/responses") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "model": "gpt-test", + "input": "hello", + "stream": true + }) + .to_string(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(header::CONTENT_TYPE).unwrap(), + "text/event-stream" + ); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + assert_eq!(bytes, Bytes::from_static(b"data: one\n\ndata: two\n\n")); + } + + async fn spawn_upstream(streaming: bool) -> String { + async fn chat(headers: HeaderMap, body: Bytes) -> impl IntoResponse { + let payload: Value = serde_json::from_slice(&body).unwrap(); + Json(json!({ + "model": payload["model"], + "authorization": headers + .get(header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()), + "connection": headers + .get(header::CONNECTION) + .and_then(|value| value.to_str().ok()) + })) + } + + async fn stream_response() -> impl IntoResponse { + let chunks = stream::iter([ + Ok::<_, std::convert::Infallible>(Bytes::from_static(b"data: one\n\n")), + Ok(Bytes::from_static(b"data: two\n\n")), + ]); + ( + [(header::CONTENT_TYPE, "text/event-stream")], + Body::from_stream(chunks), + ) + } + + let app = if streaming { + Router::new().route("/v1/responses", post(stream_response)) + } else { + Router::new().route("/v1/chat/completions", post(chat)) + }; + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + format!("http://{address}") + } +} diff --git a/crates/sidecar/src/session.rs b/crates/sidecar/src/session.rs new file mode 100644 index 0000000..1bc8a08 --- /dev/null +++ b/crates/sidecar/src/session.rs @@ -0,0 +1,550 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; + +use axum::http::HeaderMap; +use nemo_flow::api::llm::{ + LlmAttributes, LlmCallEndParams, LlmCallParams, LlmHandle, LlmRequest, llm_call, llm_call_end, +}; +use nemo_flow::api::runtime::{ScopeStackHandle, TASK_SCOPE_STACK, create_scope_stack}; +use nemo_flow::api::scope::{ + EmitMarkEventParams, PopScopeParams, PushScopeParams, ScopeHandle, ScopeType, + event as emit_mark_event, get_handle, pop_scope, push_scope, +}; +use nemo_flow::api::subscriber::scope_register_subscriber; +use nemo_flow::api::tool::{ + ToolCallEndParams, ToolCallParams, ToolHandle, tool_call, tool_call_end, +}; +use nemo_flow::observability::atif::{AtifAgentInfo, AtifExporter}; +use nemo_flow::observability::openinference::{OpenInferenceConfig, OpenInferenceSubscriber}; +use serde_json::{Map, Value, json}; +use tokio::sync::Mutex; + +use crate::config::{SessionConfig, SidecarConfig}; +use crate::error::SidecarError; +use crate::model::{AgentKind, NormalizedEvent, SessionEvent, SubagentEvent, ToolEvent}; + +#[derive(Clone)] +pub(crate) struct SessionManager { + inner: Arc>>, + default_config: SidecarConfig, +} + +#[derive(Debug, Clone)] +pub(crate) struct LlmGatewayStart { + pub(crate) session_id: String, + pub(crate) provider: String, + pub(crate) model_name: Option, + pub(crate) request: LlmRequest, + pub(crate) streaming: bool, + pub(crate) metadata: Value, +} + +#[derive(Debug, Clone)] +pub(crate) struct ActiveLlm { + stack: ScopeStackHandle, + handle: LlmHandle, +} + +struct Session { + agent_kind: AgentKind, + session_id: String, + scope_stack: ScopeStackHandle, + agent_scope: Option, + subagents: HashMap, + tools: HashMap, + config: SessionConfig, + atif: Option, + openinference: Option, +} + +impl SessionManager { + pub(crate) fn new(default_config: SidecarConfig) -> Self { + Self { + inner: Arc::new(Mutex::new(HashMap::new())), + default_config, + } + } + + pub(crate) async fn apply_events( + &self, + headers: &HeaderMap, + events: Vec, + ) -> Result<(), SidecarError> { + let mut sessions = self.inner.lock().await; + for event in events { + let session_id = event.session_id().to_string(); + let config = self.default_config.session_config_from_headers(headers); + let session = sessions.entry(session_id.clone()).or_insert_with(|| { + Session::new(session_id.clone(), event_agent_kind(&event), config.clone()) + }); + session.apply(event).await?; + if session.agent_scope.is_none() + && session.subagents.is_empty() + && session.tools.is_empty() + { + sessions.remove(&session_id); + } + } + Ok(()) + } + + pub(crate) async fn start_llm( + &self, + headers: &HeaderMap, + start: LlmGatewayStart, + ) -> Result { + let mut sessions = self.inner.lock().await; + let config = self.default_config.session_config_from_headers(headers); + let session = sessions + .entry(start.session_id.clone()) + .or_insert_with(|| Session::new(start.session_id.clone(), AgentKind::Gateway, config)); + session.start_llm(start).await + } + + pub(crate) async fn end_llm( + &self, + active: ActiveLlm, + response: Value, + metadata: Value, + ) -> Result<(), SidecarError> { + TASK_SCOPE_STACK + .scope(active.stack, async move { + llm_call_end( + LlmCallEndParams::builder() + .handle(&active.handle) + .response(response) + .metadata(metadata) + .build(), + ) + .map_err(SidecarError::from) + }) + .await + } +} + +impl Session { + fn new(session_id: String, agent_kind: AgentKind, config: SessionConfig) -> Self { + Self { + agent_kind, + session_id, + scope_stack: create_scope_stack(), + agent_scope: None, + subagents: HashMap::new(), + tools: HashMap::new(), + config, + atif: None, + openinference: None, + } + } + + async fn apply(&mut self, event: NormalizedEvent) -> Result<(), SidecarError> { + let stack = self.scope_stack.clone(); + TASK_SCOPE_STACK + .scope(stack, async move { + match event { + NormalizedEvent::AgentStarted(event) => self.start_agent(event), + NormalizedEvent::AgentEnded(event) => self.end_agent(event), + NormalizedEvent::SubagentStarted(event) => self.start_subagent(event), + NormalizedEvent::SubagentEnded(event) => self.end_subagent(event), + NormalizedEvent::ToolStarted(event) => self.start_tool(event), + NormalizedEvent::ToolEnded(event) => self.end_tool(event), + NormalizedEvent::PromptSubmitted(event) => self.mark("prompt_submitted", event), + NormalizedEvent::AgentResponse(event) => self.mark("agent_response", event), + NormalizedEvent::Compaction(event) => self.mark("compaction", event), + NormalizedEvent::Notification(event) => self.mark("notification", event), + NormalizedEvent::HookMark(event) => self.mark("hook_mark", event), + } + }) + .await + } + + async fn start_llm(&mut self, start: LlmGatewayStart) -> Result { + let stack = self.scope_stack.clone(); + TASK_SCOPE_STACK + .scope(stack.clone(), async move { + self.ensure_agent_started(Value::Null)?; + let mut attributes = LlmAttributes::empty(); + if start.streaming { + attributes |= LlmAttributes::STREAMING; + } + let handle = llm_call( + LlmCallParams::builder() + .name(start.provider.as_str()) + .request(&start.request) + .attributes(attributes) + .metadata(start.metadata) + .model_name_opt(start.model_name) + .build(), + )?; + Ok(ActiveLlm { stack, handle }) + }) + .await + } + + fn start_agent(&mut self, event: SessionEvent) -> Result<(), SidecarError> { + self.agent_kind = event.agent_kind; + self.ensure_agent_started(event.metadata) + } + + fn ensure_agent_started(&mut self, event_metadata: Value) -> Result<(), SidecarError> { + if self.agent_scope.is_some() { + return Ok(()); + } + let root = get_handle()?; + self.install_observers(&root)?; + let metadata = merge_metadata( + merge_metadata( + self.config.metadata.clone().unwrap_or(Value::Null), + event_metadata, + ), + json!({ + "session_id": self.session_id, + "sidecar_config_profile": self.config.profile, + "plugin_config": self.config.plugin_config, + "gateway_mode": self.config.gateway_mode, + }), + ); + let scope = push_scope( + PushScopeParams::builder() + .name(self.agent_kind.as_str()) + .scope_type(ScopeType::Agent) + .metadata(metadata) + .build(), + )?; + self.agent_scope = Some(scope); + Ok(()) + } + + fn install_observers(&mut self, root: &ScopeHandle) -> Result<(), SidecarError> { + if self.atif.is_none() && self.config.atif_dir.is_some() { + let exporter = AtifExporter::new( + self.session_id.clone(), + AtifAgentInfo { + name: self.agent_kind.as_str().to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + model_name: None, + tool_definitions: None, + extra: self.config.metadata.clone(), + }, + ); + scope_register_subscriber(&root.uuid, "sidecar-atif", exporter.subscriber())?; + self.atif = Some(exporter); + } + if self.openinference.is_none() + && let Some(endpoint) = &self.config.openinference_endpoint + { + let subscriber = OpenInferenceSubscriber::new( + OpenInferenceConfig::new() + .with_endpoint(endpoint.clone()) + .with_service_name("nemo-flow-sidecar"), + )?; + scope_register_subscriber( + &root.uuid, + "sidecar-openinference", + subscriber.subscriber(), + )?; + self.openinference = Some(subscriber); + } + Ok(()) + } + + fn end_agent(&mut self, event: SessionEvent) -> Result<(), SidecarError> { + self.ensure_agent_started(event.metadata.clone())?; + let active_tools: Vec<_> = self.tools.drain().map(|(_, handle)| handle).collect(); + for handle in active_tools { + tool_call_end( + ToolCallEndParams::builder() + .handle(&handle) + .result(json!({ "status": "closed_by_agent_end" })) + .metadata(json!({ "status": "closed_by_agent_end" })) + .build(), + )?; + } + let active_subagents: Vec<_> = self.subagents.drain().map(|(_, handle)| handle).collect(); + for handle in active_subagents.into_iter().rev() { + let _ = pop_scope( + PopScopeParams::builder() + .handle_uuid(&handle.uuid) + .output(json!({ "status": "closed_by_agent_end" })) + .build(), + ); + } + if let Some(scope) = self.agent_scope.take() { + pop_scope( + PopScopeParams::builder() + .handle_uuid(&scope.uuid) + .output(event.payload) + .build(), + )?; + } + self.flush_observers()?; + Ok(()) + } + + fn start_subagent(&mut self, event: SubagentEvent) -> Result<(), SidecarError> { + self.ensure_agent_started(event.metadata.clone())?; + if self.subagents.contains_key(&event.subagent_id) { + return Ok(()); + } + let scope = push_scope( + PushScopeParams::builder() + .name(format!("subagent:{}", event.subagent_id).as_str()) + .scope_type(ScopeType::Agent) + .metadata(event.metadata) + .input(event.payload) + .build(), + )?; + self.subagents.insert(event.subagent_id, scope); + Ok(()) + } + + fn end_subagent(&mut self, event: SubagentEvent) -> Result<(), SidecarError> { + self.ensure_agent_started(event.metadata.clone())?; + let Some(scope) = self.subagents.remove(&event.subagent_id) else { + return self.mark( + "subagent_end_without_start", + SessionEvent { + session_id: event.session_id, + agent_kind: event.agent_kind, + event_name: event.event_name, + payload: event.payload, + metadata: event.metadata, + }, + ); + }; + if pop_scope( + PopScopeParams::builder() + .handle_uuid(&scope.uuid) + .output(event.payload.clone()) + .build(), + ) + .is_err() + { + emit_mark_event( + EmitMarkEventParams::builder() + .name("subagent_end_not_top") + .data(event.payload) + .metadata(event.metadata) + .build(), + )?; + } + Ok(()) + } + + fn start_tool(&mut self, event: ToolEvent) -> Result<(), SidecarError> { + self.ensure_agent_started(event.metadata.clone())?; + if self.tools.contains_key(&event.tool_call_id) { + return Ok(()); + } + let parent = event + .subagent_id + .as_ref() + .and_then(|id| self.subagents.get(id)) + .or(self.agent_scope.as_ref()); + let handle = tool_call( + ToolCallParams::builder() + .name(event.tool_name.as_str()) + .args(event.arguments) + .parent_opt(parent) + .metadata(event.metadata) + .tool_call_id(event.tool_call_id.clone()) + .build(), + )?; + self.tools.insert(event.tool_call_id, handle); + Ok(()) + } + + fn end_tool(&mut self, event: ToolEvent) -> Result<(), SidecarError> { + self.ensure_agent_started(event.metadata.clone())?; + let handle = match self.tools.remove(&event.tool_call_id) { + Some(handle) => handle, + None => { + let parent = event + .subagent_id + .as_ref() + .and_then(|id| self.subagents.get(id)) + .or(self.agent_scope.as_ref()); + tool_call( + ToolCallParams::builder() + .name(event.tool_name.as_str()) + .args(event.arguments) + .parent_opt(parent) + .metadata(event.metadata.clone()) + .tool_call_id(event.tool_call_id.clone()) + .build(), + )? + } + }; + tool_call_end( + ToolCallEndParams::builder() + .handle(&handle) + .result(event.result) + .metadata(merge_metadata( + event.metadata, + json!({ "status": event.status }), + )) + .build(), + )?; + Ok(()) + } + + fn mark(&mut self, name: &str, event_payload: SessionEvent) -> Result<(), SidecarError> { + self.ensure_agent_started(event_payload.metadata.clone())?; + emit_mark_event( + EmitMarkEventParams::builder() + .name(name) + .data(event_payload.payload) + .metadata(event_payload.metadata) + .build(), + )?; + Ok(()) + } + + fn flush_observers(&mut self) -> Result<(), SidecarError> { + if let Some(subscriber) = &self.openinference { + subscriber.force_flush()?; + subscriber.shutdown()?; + } + if let (Some(exporter), Some(directory)) = (&self.atif, &self.config.atif_dir) { + write_atif(directory, &self.session_id, exporter)?; + } + Ok(()) + } +} + +fn write_atif( + directory: &PathBuf, + session_id: &str, + exporter: &AtifExporter, +) -> Result<(), SidecarError> { + std::fs::create_dir_all(directory)?; + let path = directory.join(format!("{session_id}.atif.json")); + let trajectory = exporter.export(); + let serialized = serde_json::to_vec_pretty(&trajectory) + .map_err(|error| SidecarError::InvalidPayload(error.to_string()))?; + std::fs::write(path, serialized)?; + Ok(()) +} + +fn event_agent_kind(event: &NormalizedEvent) -> AgentKind { + match event { + NormalizedEvent::AgentStarted(event) + | NormalizedEvent::AgentEnded(event) + | NormalizedEvent::PromptSubmitted(event) + | NormalizedEvent::AgentResponse(event) + | NormalizedEvent::Compaction(event) + | NormalizedEvent::Notification(event) + | NormalizedEvent::HookMark(event) => event.agent_kind, + NormalizedEvent::SubagentStarted(event) | NormalizedEvent::SubagentEnded(event) => { + event.agent_kind + } + NormalizedEvent::ToolStarted(event) | NormalizedEvent::ToolEnded(event) => event.agent_kind, + } +} + +fn merge_metadata(left: Value, right: Value) -> Value { + match (left, right) { + (Value::Object(mut left), Value::Object(right)) => { + for (key, value) in right { + if !value.is_null() { + left.insert(key, value); + } + } + Value::Object(left) + } + (Value::Null, right) => right, + (left, Value::Null) => left, + (left, right) => { + let mut object = Map::new(); + object.insert("metadata".into(), left); + object.insert("extra_metadata".into(), right); + Value::Object(object) + } + } +} + +#[cfg(test)] +mod tests { + use axum::http::HeaderMap; + use serde_json::json; + + use super::*; + use crate::model::{SessionEvent, ToolEvent}; + + #[tokio::test] + async fn nests_agent_subagent_and_tool_lifecycle() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + }; + let manager = SessionManager::new(config); + let headers = HeaderMap::new(); + let events = vec![ + NormalizedEvent::AgentStarted(SessionEvent { + session_id: "s1".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SessionStart".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "s1".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "worker-1".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::ToolStarted(ToolEvent { + session_id: "s1".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "PreToolUse".into(), + tool_call_id: "t1".into(), + tool_name: "Read".into(), + subagent_id: Some("worker-1".into()), + arguments: json!({ "file_path": "README.md" }), + result: Value::Null, + status: None, + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::ToolEnded(ToolEvent { + session_id: "s1".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "PostToolUse".into(), + tool_call_id: "t1".into(), + tool_name: "Read".into(), + subagent_id: Some("worker-1".into()), + arguments: Value::Null, + result: json!({ "ok": true }), + status: Some("success".into()), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentEnded(SubagentEvent { + session_id: "s1".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStop".into(), + subagent_id: "worker-1".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::AgentEnded(SessionEvent { + session_id: "s1".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SessionEnd".into(), + payload: json!({}), + metadata: json!({}), + }), + ]; + manager.apply_events(&headers, events).await.unwrap(); + assert!(manager.inner.lock().await.is_empty()); + } +} diff --git a/docs/index.md b/docs/index.md index 9048ad9..aa33c73 100644 --- a/docs/index.md +++ b/docs/index.md @@ -161,6 +161,10 @@ About Basic Guide: Adding Scopes Basic Guide: Wrap Tool Calls Basic Guide: Wrap LLM Calls +Advanced Guide: Coding-Agent Gateway Sidecar +Claude Code Sidecar Guide +Codex Sidecar Guide +Cursor Sidecar Guide Advanced Guide: Handle Non-Serializable Data Advanced Guide: Using Codecs Advanced Guide: Provider Codecs diff --git a/docs/integrate-frameworks/about.md b/docs/integrate-frameworks/about.md index a12a1b7..fc4e3d2 100644 --- a/docs/integrate-frameworks/about.md +++ b/docs/integrate-frameworks/about.md @@ -37,6 +37,10 @@ Use these guide links to move from the overview into task-specific instructions. - [Basic Guide: Adding Scopes](adding-scopes.md) shows how framework request and run hooks become NeMo Flow ownership boundaries. - [Basic Guide: Wrap Tool Calls](wrap-tool-calls.md) explains where to place managed tool wrappers and tool lifecycle fallbacks. - [Basic Guide: Wrap LLM Calls](wrap-llm-calls.md) explains where to place managed provider wrappers, model names, streaming behavior, and LLM lifecycle fallbacks. +- [Advanced Guide: Coding-Agent Gateway Sidecar](coding-agent-sidecar.md) describes the Rust sidecar for observing Codex, Claude Code, and Cursor through canonical hooks plus a passthrough LLM gateway. +- [Claude Code Sidecar Guide](coding-agent-claude-code.md) covers Claude Code hook installation, Anthropic gateway routing, ATIF verification, and the unsupported Claude application modes. +- [Codex Sidecar Guide](coding-agent-codex.md) covers Codex CLI and local GUI/app setup, `codex_hooks = true`, model provider routing, and remote-task caveats. +- [Cursor Sidecar Guide](coding-agent-cursor.md) covers the Cursor hook bundle, GUI and CLI smoke tests, gateway routing limits, and hook-only operation. - [Advanced Guide: Handle Non-Serializable Data](non-serializable-data.md) shows how to keep clients, streams, callbacks, and SDK objects outside JSON payloads. - [Advanced Guide: Using Codecs](using-codecs.md) explains typed value codecs for framework-facing wrappers. - [Advanced Guide: Provider Codecs](provider-codecs.md) explains provider request and response codecs for normalized middleware and event annotations. diff --git a/docs/integrate-frameworks/coding-agent-claude-code.md b/docs/integrate-frameworks/coding-agent-claude-code.md new file mode 100644 index 0000000..d12c663 --- /dev/null +++ b/docs/integrate-frameworks/coding-agent-claude-code.md @@ -0,0 +1,100 @@ + + +# Claude Code Sidecar Guide + +Use this guide to observe Claude Code sessions with NeMo Flow. Claude Code is +the supported integration target. The Claude application, Claude web, and Claude +desktop sessions are unsupported unless they expose the same local hook and +gateway controls as Claude Code. + +## Install Hooks + +Inspect the generated config first: + +```bash +nemo-flow-sidecar install claude-code \ + --scope user \ + --target cli \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --gateway-mode required \ + --dry-run \ + --print +``` + +Then install it: + +```bash +nemo-flow-sidecar install claude-code \ + --scope user \ + --target cli \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --gateway-mode required +``` + +The packaged hook files live in +`integrations/coding-agents/claude-code/`. The installer merges equivalent hook +entries into `.claude/settings.json` and backs up an existing file before +writing. + +## Start The Sidecar + +```bash +NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ +nemo-flow-sidecar --bind 127.0.0.1:4040 +``` + +Add `NEMO_FLOW_OPENINFERENCE_ENDPOINT` or `--openinference-endpoint` when the +session should also export OpenInference traces. + +## Configure The Gateway + +Route Claude Code Anthropic traffic through the sidecar: + +```bash +export ANTHROPIC_BASE_URL=http://127.0.0.1:4040 +``` + +The sidecar forwards Anthropic `/v1/messages`, `/v1/messages/count_tokens`, and +model routes without rewriting provider JSON. Hook-only mode observes agent, +subagent, and tool lifecycle, but it cannot prove complete LLM lifecycle without +this gateway routing. + +## Smoke Test + +Run a small Claude Code prompt that starts a session and uses one simple tool. +Then check that hook forwarding reaches the sidecar: + +```bash +printf '{"session_id":"smoke-claude","hook_event_name":"SessionStart"}' \ + | nemo-flow-sidecar hook-forward claude-code --sidecar-url http://127.0.0.1:4040 +``` + +The response should be valid Claude Code hook JSON. For most lifecycle events it +is an allow/continue response. + +## Verify Export + +End the Claude Code session and confirm that session-end closed the NeMo Flow +agent scope and wrote ATIF: + +```bash +ls .nemo-flow/atif +``` + +The sidecar exports `.atif.json` on session end. If no file appears, +confirm that `SessionEnd` hooks fire, `--atif-dir` or `NEMO_FLOW_ATIF_DIR` is +set, and the sidecar process can write to the directory. + +## Troubleshoot LLM Lifecycle + +Missing hooks usually means Claude Code did not load the local hook config or +the `nemo-flow-sidecar` binary is not on `PATH`. + +Missing LLM spans with present hook spans means Anthropic traffic is not routed +through the sidecar. Verify `ANTHROPIC_BASE_URL` in the Claude Code process +environment and confirm that requests hit `/v1/messages`. diff --git a/docs/integrate-frameworks/coding-agent-codex.md b/docs/integrate-frameworks/coding-agent-codex.md new file mode 100644 index 0000000..670c9f6 --- /dev/null +++ b/docs/integrate-frameworks/coding-agent-codex.md @@ -0,0 +1,102 @@ + + +# Codex Sidecar Guide + +Use this guide to observe local Codex CLI sessions and local Codex GUI or app +sessions that honor the same local config and gateway routing. Cloud or remote +Codex tasks are partial or unsupported for local sidecar LLM capture because the +local sidecar cannot observe provider traffic that never reaches the machine. + +## Install Hooks + +Inspect the generated config first: + +```bash +nemo-flow-sidecar install codex \ + --scope user \ + --target both \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --gateway-mode required \ + --dry-run \ + --print +``` + +Then install it: + +```bash +nemo-flow-sidecar install codex \ + --scope user \ + --target both \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --gateway-mode required +``` + +The packaged Codex plugin files live in `integrations/coding-agents/codex/`. +The installer merges hook entries into `.codex/hooks.json` and enables hooks in +`.codex/config.toml` with: + +```toml +[features] +codex_hooks = true +``` + +## Start The Sidecar + +```bash +NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ +nemo-flow-sidecar --bind 127.0.0.1:4040 +``` + +Use `--openai-base-url` if the sidecar should forward OpenAI-compatible traffic +to a provider other than `https://api.openai.com`. + +## Configure The Gateway + +For Codex CLI, configure the model provider `base_url` to use the sidecar: + +```toml +[model_providers.openai] +base_url = "http://127.0.0.1:4040" +``` + +Local Codex GUI or app sessions have the same support level only when they read +the same local hook/plugin config and provider routing. Cloud tasks may still +emit some lifecycle hooks, but complete LLM lifecycle capture requires model +traffic to pass through the sidecar. + +## Smoke Test + +Run a small Codex prompt that starts a session and uses one simple tool. Then +check hook forwarding directly: + +```bash +printf '{"session_id":"smoke-codex","hook_event_name":"sessionStart"}' \ + | nemo-flow-sidecar hook-forward codex --sidecar-url http://127.0.0.1:4040 +``` + +The response should match Codex hook semantics. For most lifecycle events it is +an empty JSON object. + +## Verify Export + +End the Codex session and confirm ATIF exists: + +```bash +ls .nemo-flow/atif +``` + +The sidecar writes `.atif.json` on session end. If the file is +missing, confirm `codex_hooks = true`, hook config loading, and `--atif-dir` or +`NEMO_FLOW_ATIF_DIR`. + +## Troubleshoot LLM Lifecycle + +If agent/tool events exist but LLM spans are missing, the provider `base_url` is +not pointing at the sidecar for the active Codex process. If only GUI sessions +are missing spans, confirm the GUI is using local provider configuration rather +than a remote execution path. diff --git a/docs/integrate-frameworks/coding-agent-cursor.md b/docs/integrate-frameworks/coding-agent-cursor.md new file mode 100644 index 0000000..25e8731 --- /dev/null +++ b/docs/integrate-frameworks/coding-agent-cursor.md @@ -0,0 +1,105 @@ + + +# Cursor Sidecar Guide + +Use this guide to observe Cursor hook lifecycle events with NeMo Flow. The +repository ships a Cursor hook bundle under `integrations/coding-agents/cursor/` +because this integration does not assume an official Cursor plugin package +format. + +Cursor GUI or IDE sessions can provide agent, subagent, tool, shell, MCP, file, +and response lifecycle events through `.cursor/hooks.json`. Complete LLM +lifecycle observability additionally requires Cursor model traffic to route +through the sidecar gateway if your Cursor build exposes that configuration. + +Cursor CLI support must be verified separately with `cursor-agent`. If CLI hooks +do not fire, treat Cursor CLI support as hook-limited and gateway-only where +model routing is configurable. + +## Install Hooks + +Inspect the generated config first: + +```bash +nemo-flow-sidecar install cursor \ + --scope project \ + --target gui \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --gateway-mode passthrough \ + --dry-run \ + --print +``` + +Then install it: + +```bash +nemo-flow-sidecar install cursor \ + --scope project \ + --target gui \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --gateway-mode passthrough +``` + +The installer merges NeMo Flow entries into `.cursor/hooks.json` and backs up an +existing file before writing. + +## Start The Sidecar + +```bash +NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ +nemo-flow-sidecar --bind 127.0.0.1:4040 +``` + +Use `--openai-base-url` or `--anthropic-base-url` when the sidecar should +forward to non-default upstream providers. + +## Configure The Gateway + +If Cursor exposes provider base URL configuration, point OpenAI-compatible or +Anthropic-compatible traffic at: + +```text +http://127.0.0.1:4040 +``` + +Hook-only Cursor mode observes agent and tool lifecycle but cannot provide +complete LLM lifecycle. Missing LLM spans are expected when Cursor sends model +traffic directly to the provider or through a remote service. + +## Smoke Test + +Run a small Cursor GUI session that starts an agent and uses one simple tool. +Then check hook forwarding directly: + +```bash +printf '{"session_id":"smoke-cursor","hook_event_name":"sessionStart"}' \ + | nemo-flow-sidecar hook-forward cursor --sidecar-url http://127.0.0.1:4040 +``` + +For Cursor CLI, run an equivalent `cursor-agent` session and verify the sidecar +receives hook requests. If no hook requests arrive, document that CLI version as +hook-limited and rely only on gateway observability where provider routing is +available. + +## Verify Export + +End the Cursor session and confirm ATIF exists: + +```bash +ls .nemo-flow/atif +``` + +The sidecar writes `.atif.json` on session end. If the file is +missing, confirm Cursor loaded `.cursor/hooks.json`, the sidecar binary is on +`PATH`, and `--atif-dir` or `NEMO_FLOW_ATIF_DIR` is configured. + +## Troubleshoot LLM Lifecycle + +If Cursor hook events appear but LLM spans are missing, provider traffic is not +routed through the sidecar. Confirm the active Cursor GUI or CLI mode supports +provider base URL configuration for the model path being used. diff --git a/docs/integrate-frameworks/coding-agent-sidecar.md b/docs/integrate-frameworks/coding-agent-sidecar.md new file mode 100644 index 0000000..cc481b8 --- /dev/null +++ b/docs/integrate-frameworks/coding-agent-sidecar.md @@ -0,0 +1,146 @@ + + +# Advanced Guide: Coding-Agent Gateway Sidecar + +The `nemo-flow-sidecar` binary observes coding agents that do not expose every +LLM call site directly. It combines agent-specific hook endpoints with a +passthrough LLM gateway so NeMo Flow owns both the agent lifecycle and the model +request lifecycle. + +Use the sidecar when you need one observability boundary for OpenAI Codex, +Claude Code, and Cursor without replacing each agent's canonical hook payload. + +## Hook Endpoints + +Each hook endpoint accepts the agent's native hook JSON directly. Do not wrap +the payload in a shared sidecar envelope. + +- `POST /hooks/codex` accepts Codex hook JSON and returns the Codex-compatible + hook response object. +- `POST /hooks/claude-code` accepts Claude Code hook JSON and returns + Claude-compatible fields such as `continue` and permission decisions when the + hook event supports them. +- `POST /hooks/cursor` accepts Cursor hook JSON and returns Cursor-compatible + fields such as `continue`, `permission`, `user_message`, and `agent_message` + when the hook event supports them. + +The adapters preserve vendor fields such as session IDs, working directories, +transcript paths, model names, tool payloads, shell payloads, MCP payloads, file +payloads, user identity, and subagent metadata in NeMo Flow event metadata. + +## Gateway Routes + +Route all coding-agent LLM traffic through the sidecar when full LLM lifecycle +observability is required. + +- `POST /v1/responses` +- `POST /v1/chat/completions` +- `POST /v1/messages` +- `POST /v1/messages/count_tokens` +- `GET /v1/models` + +The gateway forwards raw provider JSON without rewriting OpenAI or Anthropic +payload schemas. It removes only hop-by-hop transport headers, forwards +streaming responses as streams, and emits NeMo Flow LLM start and end events +under the active session scope. + +## Session Configuration + +Sidecar-specific configuration travels through hook registration settings, +headers, environment variables, or a referenced sidecar profile. It must not +replace the coding agent's canonical hook schema. + +Common headers are: + +- `x-nemo-flow-session-id` +- `x-nemo-flow-config-profile` +- `x-nemo-flow-session-metadata` +- `x-nemo-flow-plugin-config` +- `x-nemo-flow-openinference-endpoint` +- `x-nemo-flow-atif-dir` + +Common environment variables are: + +- `NEMO_FLOW_SIDECAR_BIND` +- `NEMO_FLOW_OPENAI_BASE_URL` +- `NEMO_FLOW_ANTHROPIC_BASE_URL` +- `NEMO_FLOW_OPENINFERENCE_ENDPOINT` +- `NEMO_FLOW_ATIF_DIR` + +Per-session configuration controls the scope-local OpenInference subscriber, +the ATIF exporter, structured metadata on the top-level agent begin event, and +the plugin configuration metadata associated with the session. + +## Runtime Mapping + +The sidecar normalizes vendor hook payloads into private internal events before +calling NeMo Flow APIs. + +- Agent start opens a top-level `ScopeType::Agent` scope on a dedicated + `ScopeStackHandle`. +- Subagent start opens a child `ScopeType::Agent` scope. Subagent stop closes + that scope when it is still active. +- Tool pre-use starts a NeMo Flow tool span. Tool post-use, denial, or failure + closes it. +- Prompt, response, compaction, notification, and unknown hook events become + mark events under the active session scope. +- Gateway requests emit NeMo Flow LLM start and end events under the active + session scope. + +Cursor hook-only mode observes agent, subagent, and tool lifecycle. To observe +Cursor LLM lifecycle completely, configure Cursor model traffic to use the +sidecar gateway. + +## Install Integrations + +The repository includes installable integration packages under +`integrations/coding-agents/` and an installer in the sidecar binary. + +```bash +nemo-flow-sidecar install claude-code --scope user --target cli --sidecar-url http://127.0.0.1:4040 +nemo-flow-sidecar install codex --scope user --target both --sidecar-url http://127.0.0.1:4040 +nemo-flow-sidecar install cursor --scope project --target gui --sidecar-url http://127.0.0.1:4040 +``` + +Use `--dry-run` to see which files would be changed. Use `--print` to print the +merged file contents. Existing config files are backed up before the installer +writes replacement files, and generated hook entries are appended only when the +same NeMo Flow entry is not already present. + +Common install options become hook-forwarding command arguments and sidecar +headers: + +- `--atif-dir` sets `x-nemo-flow-atif-dir`. +- `--openinference-endpoint` sets `x-nemo-flow-openinference-endpoint`. +- `--profile` sets `x-nemo-flow-config-profile`. +- `--session-metadata` sets `x-nemo-flow-session-metadata`. +- `--plugin-config` sets `x-nemo-flow-plugin-config`. +- `--gateway-mode hook-only|passthrough|required` sets + `x-nemo-flow-gateway-mode`. + +The generated hooks run: + +```bash +nemo-flow-sidecar hook-forward +``` + +`hook-forward` reads the canonical hook payload from standard input, sends it to +the matching endpoint, and prints the endpoint response. It fails open by +default so observability outages do not block the coding agent. Add +`--fail-closed` only when policy requires hook delivery to block the agent. + +## Agent Guides + +Use the per-agent guide for end-to-end setup, smoke tests, and GUI or +application-mode caveats. + +- [Claude Code Sidecar Guide](coding-agent-claude-code.md) +- [Codex Sidecar Guide](coding-agent-codex.md) +- [Cursor Sidecar Guide](coding-agent-cursor.md) + +Each guide covers plugin or hook installation, sidecar startup, gateway routing, +hook smoke tests, ATIF export verification on session end, and troubleshooting +missing LLM lifecycle data. diff --git a/integrations/coding-agents/README.md b/integrations/coding-agents/README.md new file mode 100644 index 0000000..b4211f0 --- /dev/null +++ b/integrations/coding-agents/README.md @@ -0,0 +1,118 @@ + + +# NeMo Flow Coding-Agent Observability Integrations + +This directory contains installable hook integrations for coding agents that +should be observed by `nemo-flow-sidecar`. + +The sidecar combines two observability paths: + +- Agent lifecycle hooks for sessions, prompts, subagents, tool calls, + compaction, responses, and stop events. +- A passthrough LLM gateway for OpenAI-compatible and Anthropic-compatible + provider traffic. + +Hook integrations preserve each coding agent's canonical hook payload. They do +not wrap the payload in a shared NeMo Flow envelope. Sidecar-specific settings +travel through hook command arguments and HTTP headers. + +## Packages + +- `claude-code/` installs Claude Code hook entries targeting + `POST /hooks/claude-code`. +- `codex/` installs Codex hook entries targeting `POST /hooks/codex` and enables + `codex_hooks = true`. +- `cursor/` installs a Cursor `.cursor/hooks.json` bundle targeting + `POST /hooks/cursor`. + +## Common Setup + +Build or install the sidecar binary so `nemo-flow-sidecar` is on `PATH`. + +Start the sidecar: + +```bash +NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ +nemo-flow-sidecar --bind 127.0.0.1:4040 +``` + +Install an integration: + +```bash +nemo-flow-sidecar install claude-code --scope user --target cli --sidecar-url http://127.0.0.1:4040 +nemo-flow-sidecar install codex --scope user --target both --sidecar-url http://127.0.0.1:4040 +nemo-flow-sidecar install cursor --scope project --target gui --sidecar-url http://127.0.0.1:4040 +``` + +Inspect generated changes before writing: + +```bash +nemo-flow-sidecar install codex \ + --scope user \ + --target both \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --gateway-mode required \ + --dry-run \ + --print +``` + +The installer backs up existing config files, merges only NeMo Flow hook +entries, and avoids adding duplicate NeMo Flow entries on repeated runs. + +## Common Options + +The installer writes hook commands that call: + +```bash +nemo-flow-sidecar hook-forward +``` + +`hook-forward` reads the canonical hook JSON from standard input, forwards it to +the matching sidecar endpoint, and prints the vendor-specific hook response. + +Useful install options: + +- `--atif-dir ` writes ATIF trajectories on session end. +- `--openinference-endpoint ` exports OpenInference traces. +- `--profile ` records a sidecar profile name in session metadata. +- `--session-metadata ''` adds structured metadata to the agent begin + event. +- `--plugin-config ''` records scope-local plugin configuration metadata. +- `--gateway-mode hook-only|passthrough|required` records the intended gateway + mode for the session. +- `--fail-closed` can be added to generated hook commands when the agent should + block on hook delivery failures. The default is fail-open. + +## LLM Gateway + +Complete LLM lifecycle observability requires model traffic to pass through the +sidecar. Hook-only mode observes agent, subagent, and tool lifecycle, but it +cannot observe provider request and response lifecycle when the coding agent +sends model traffic directly to an upstream provider or remote service. + +The sidecar exposes these passthrough routes: + +- `POST /v1/responses` +- `POST /v1/chat/completions` +- `POST /v1/messages` +- `POST /v1/messages/count_tokens` +- `GET /v1/models` + +Configure each coding agent's provider base URL to use +`http://127.0.0.1:4040` where that agent supports local provider routing. + +## Verify Export + +Run a coding-agent session that starts, uses one tool, and ends. Then confirm +that ATIF was written: + +```bash +ls .nemo-flow/atif +``` + +The sidecar writes `.atif.json` when it receives a session-end hook +for a session with ATIF configured. diff --git a/integrations/coding-agents/claude-code/.claude-plugin/plugin.json b/integrations/coding-agents/claude-code/.claude-plugin/plugin.json new file mode 100644 index 0000000..3baf016 --- /dev/null +++ b/integrations/coding-agents/claude-code/.claude-plugin/plugin.json @@ -0,0 +1,19 @@ +{ + "name": "nemo-flow-claude-code-observability", + "version": "0.1.0", + "description": "Claude Code hooks that forward canonical lifecycle payloads to nemo-flow-sidecar.", + "author": { + "name": "NVIDIA Corporation and Affiliates", + "url": "https://github.com/NVIDIA/NeMo-Flow" + }, + "homepage": "https://github.com/NVIDIA/NeMo-Flow", + "repository": "https://github.com/NVIDIA/NeMo-Flow", + "license": "Apache-2.0", + "keywords": [ + "nemo-flow", + "claude-code", + "hooks", + "observability" + ], + "hooks": "../hooks/hooks.json" +} diff --git a/integrations/coding-agents/claude-code/README.md b/integrations/coding-agents/claude-code/README.md new file mode 100644 index 0000000..5469e72 --- /dev/null +++ b/integrations/coding-agents/claude-code/README.md @@ -0,0 +1,124 @@ + + +# NeMo Flow Claude Code Observability + +This package installs Claude Code hook entries that forward canonical Claude Code +hook JSON to `nemo-flow-sidecar` at `/hooks/claude-code`. + +Claude Code is the supported Claude integration target. Claude application, +Claude web, and Claude desktop sessions are unsupported unless they expose the +same local hook and gateway controls as Claude Code. + +## Files + +- `.claude-plugin/plugin.json` describes the installable Claude Code hook + package. +- `hooks/hooks.json` contains hook entries that run + `nemo-flow-sidecar hook-forward claude-code`. + +## Start The Sidecar + +Build or install the sidecar binary so `nemo-flow-sidecar` is on `PATH`. + +Start a local sidecar with ATIF export enabled: + +```bash +NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ +nemo-flow-sidecar --bind 127.0.0.1:4040 +``` + +Add OpenInference export when needed: + +```bash +nemo-flow-sidecar \ + --bind 127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --openinference-endpoint http://127.0.0.1:4318/v1/traces +``` + +## Install Hooks + +Inspect the generated config before writing: + +```bash +nemo-flow-sidecar install claude-code \ + --scope user \ + --target cli \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --gateway-mode required \ + --dry-run \ + --print +``` + +Install for Claude Code: + +```bash +nemo-flow-sidecar install claude-code \ + --scope user \ + --target cli \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --gateway-mode required +``` + +The installer merges NeMo Flow hook entries into `.claude/settings.json` and +backs up any existing file before writing. Sidecar-specific options are stored +in the generated hook command and forwarded as HTTP headers. + +## Configure LLM Gateway + +For complete LLM lifecycle observability, route Claude Code's Anthropic traffic +through the sidecar: + +```bash +export ANTHROPIC_BASE_URL=http://127.0.0.1:4040 +``` + +The sidecar forwards Anthropic `/v1/messages`, `/v1/messages/count_tokens`, and +model routes without rewriting provider request or response JSON. + +Hook-only mode observes Claude Code sessions, prompts, subagents, tools, +compaction, and stop events. It does not observe provider request and response +lifecycle unless model traffic goes through the sidecar gateway. + +## Smoke Test + +Verify the sidecar endpoint directly: + +```bash +printf '{"session_id":"smoke-claude","hook_event_name":"SessionStart"}' \ + | nemo-flow-sidecar hook-forward claude-code --sidecar-url http://127.0.0.1:4040 +``` + +The command should print a Claude-compatible continue response. + +Then run a small Claude Code prompt that starts a session and uses one simple +tool. The sidecar should receive hook requests for session and tool lifecycle +events. + +## Verify ATIF Export + +End the Claude Code session and confirm that ATIF was written: + +```bash +ls .nemo-flow/atif +``` + +The sidecar writes `.atif.json` when it receives `SessionEnd` for a +session with ATIF enabled. + +## Troubleshooting + +If no hook events arrive, confirm `nemo-flow-sidecar` is on `PATH`, Claude Code +loaded `.claude/settings.json`, and the sidecar is listening on the configured +URL. + +If hooks arrive but LLM spans are missing, confirm `ANTHROPIC_BASE_URL` is set +in the Claude Code process environment and points to `http://127.0.0.1:4040`. + +If ATIF is missing, confirm `--atif-dir` or `NEMO_FLOW_ATIF_DIR` is configured +and that the sidecar process can write to the directory. diff --git a/integrations/coding-agents/claude-code/hooks/hooks.json b/integrations/coding-agents/claude-code/hooks/hooks.json new file mode 100644 index 0000000..873df11 --- /dev/null +++ b/integrations/coding-agents/claude-code/hooks/hooks.json @@ -0,0 +1,117 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward claude-code", + "timeout": 30 + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward claude-code", + "timeout": 30 + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward claude-code", + "timeout": 30 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward claude-code", + "timeout": 30 + } + ] + } + ], + "PostToolUseFailure": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward claude-code", + "timeout": 30 + } + ] + } + ], + "SubagentStart": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward claude-code", + "timeout": 30 + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward claude-code", + "timeout": 30 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward claude-code", + "timeout": 30 + } + ] + } + ], + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward claude-code", + "timeout": 30 + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward claude-code", + "timeout": 30 + } + ] + } + ] + } +} diff --git a/integrations/coding-agents/codex/.codex-plugin/plugin.json b/integrations/coding-agents/codex/.codex-plugin/plugin.json new file mode 100644 index 0000000..556e50a --- /dev/null +++ b/integrations/coding-agents/codex/.codex-plugin/plugin.json @@ -0,0 +1,31 @@ +{ + "name": "nemo-flow-codex-observability", + "version": "0.1.0", + "description": "Codex hooks that forward canonical lifecycle payloads to nemo-flow-sidecar.", + "author": { + "name": "NVIDIA Corporation and Affiliates", + "url": "https://github.com/NVIDIA/NeMo-Flow" + }, + "homepage": "https://github.com/NVIDIA/NeMo-Flow", + "repository": "https://github.com/NVIDIA/NeMo-Flow", + "license": "Apache-2.0", + "keywords": [ + "nemo-flow", + "codex", + "hooks", + "observability" + ], + "hooks": "../hooks/hooks.json", + "interface": { + "displayName": "NeMo Flow Codex Observability", + "shortDescription": "Forward Codex lifecycle hooks to a local NeMo Flow sidecar.", + "longDescription": "Installs command hooks that preserve Codex hook payloads and forward them to nemo-flow-sidecar for agent, subagent, tool, and LLM observability.", + "developerName": "NVIDIA", + "category": "Coding", + "capabilities": [ + "Read" + ], + "websiteURL": "https://github.com/NVIDIA/NeMo-Flow", + "brandColor": "#76B900" + } +} diff --git a/integrations/coding-agents/codex/README.md b/integrations/coding-agents/codex/README.md new file mode 100644 index 0000000..6d98c88 --- /dev/null +++ b/integrations/coding-agents/codex/README.md @@ -0,0 +1,132 @@ + + +# NeMo Flow Codex Observability + +This package installs Codex hook entries that forward canonical Codex hook JSON +to `nemo-flow-sidecar` at `/hooks/codex`. + +Codex CLI is fully supported for local sessions when hooks and provider routing +are configured locally. Codex GUI or app sessions are supported only when they +run on the same machine and honor the same local hook/plugin config and provider +routing. Cloud or remote Codex tasks are partial or unsupported for local +sidecar LLM capture. + +## Files + +- `.codex-plugin/plugin.json` describes the installable Codex plugin package. +- `hooks/hooks.json` contains hook entries that run + `nemo-flow-sidecar hook-forward codex`. + +## Start The Sidecar + +Build or install the sidecar binary so `nemo-flow-sidecar` is on `PATH`. + +Start a local sidecar with ATIF export enabled: + +```bash +NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ +nemo-flow-sidecar --bind 127.0.0.1:4040 +``` + +Use a custom OpenAI-compatible upstream when needed: + +```bash +nemo-flow-sidecar \ + --bind 127.0.0.1:4040 \ + --openai-base-url https://api.openai.com \ + --atif-dir .nemo-flow/atif +``` + +## Install Hooks + +Inspect generated changes before writing: + +```bash +nemo-flow-sidecar install codex \ + --scope user \ + --target both \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --gateway-mode required \ + --dry-run \ + --print +``` + +Install for Codex CLI and local GUI/app sessions: + +```bash +nemo-flow-sidecar install codex \ + --scope user \ + --target both \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --gateway-mode required +``` + +The installer merges NeMo Flow hook entries into `.codex/hooks.json`, backs up +existing config files, and enables Codex hooks in `.codex/config.toml`: + +```toml +[features] +codex_hooks = true +``` + +## Configure LLM Gateway + +For complete LLM lifecycle observability, configure the local Codex model +provider `base_url` to use the sidecar gateway: + +```toml +[model_providers.openai] +base_url = "http://127.0.0.1:4040" +``` + +The sidecar forwards OpenAI-compatible `/v1/responses`, +`/v1/chat/completions`, and model routes without rewriting provider request or +response JSON. + +Hook-only mode observes Codex sessions, prompts, subagents, tools, compaction, +and stop events. It does not observe provider request and response lifecycle +unless model traffic goes through the sidecar gateway. + +## Smoke Test + +Verify the sidecar endpoint directly: + +```bash +printf '{"session_id":"smoke-codex","hook_event_name":"sessionStart"}' \ + | nemo-flow-sidecar hook-forward codex --sidecar-url http://127.0.0.1:4040 +``` + +The command should print a Codex-compatible hook response. Most lifecycle events +return an empty JSON object. + +Then run a small Codex prompt that starts a session and uses one simple tool. +The sidecar should receive hook requests for session and tool lifecycle events. + +## Verify ATIF Export + +End the Codex session and confirm that ATIF was written: + +```bash +ls .nemo-flow/atif +``` + +The sidecar writes `.atif.json` when it receives a session-end hook +for a session with ATIF enabled. + +## Troubleshooting + +If no hook events arrive, confirm `codex_hooks = true`, Codex loaded the +expected `.codex/hooks.json`, `nemo-flow-sidecar` is on `PATH`, and the sidecar +is listening on the configured URL. + +If hooks arrive but LLM spans are missing, confirm the active Codex process uses +a provider `base_url` of `http://127.0.0.1:4040`. For GUI/app sessions, confirm +the session is local rather than remote. + +If ATIF is missing, confirm `--atif-dir` or `NEMO_FLOW_ATIF_DIR` is configured +and that the sidecar process can write to the directory. diff --git a/integrations/coding-agents/codex/hooks/hooks.json b/integrations/coding-agents/codex/hooks/hooks.json new file mode 100644 index 0000000..a2541a7 --- /dev/null +++ b/integrations/coding-agents/codex/hooks/hooks.json @@ -0,0 +1,117 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward codex", + "timeout": 30 + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward codex", + "timeout": 30 + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward codex", + "timeout": 30 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward codex", + "timeout": 30 + } + ] + } + ], + "PostToolUseFailure": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward codex", + "timeout": 30 + } + ] + } + ], + "SubagentStart": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward codex", + "timeout": 30 + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward codex", + "timeout": 30 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward codex", + "timeout": 30 + } + ] + } + ], + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward codex", + "timeout": 30 + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward codex", + "timeout": 30 + } + ] + } + ] + } +} diff --git a/integrations/coding-agents/cursor/.cursor/hooks.json b/integrations/coding-agents/cursor/.cursor/hooks.json new file mode 100644 index 0000000..c44b2d9 --- /dev/null +++ b/integrations/coding-agents/cursor/.cursor/hooks.json @@ -0,0 +1,164 @@ +{ + "hooks": { + "sessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], + "beforeSubmitPrompt": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], + "preToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], + "beforeShellExecution": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], + "beforeMCPExecution": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], + "postToolUse": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], + "afterShellExecution": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], + "afterMCPExecution": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], + "subagentStart": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], + "subagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], + "afterAgentResponse": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], + "preCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], + "stop": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], + "sessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ] + } +} diff --git a/integrations/coding-agents/cursor/README.md b/integrations/coding-agents/cursor/README.md new file mode 100644 index 0000000..0484a7e --- /dev/null +++ b/integrations/coding-agents/cursor/README.md @@ -0,0 +1,135 @@ + + +# NeMo Flow Cursor Observability + +This package is a Cursor hook bundle, not an official Cursor plugin package. It +installs `.cursor/hooks.json` entries that forward canonical Cursor hook JSON to +`nemo-flow-sidecar` at `/hooks/cursor`. + +Cursor GUI or IDE sessions can provide agent, subagent, tool, shell, MCP, file, +and response lifecycle events through `.cursor/hooks.json`. Complete LLM +lifecycle observability additionally requires Cursor model traffic to route +through the sidecar gateway if the active Cursor build exposes provider base URL +configuration. + +Cursor CLI support must be verified separately with `cursor-agent`. If CLI hooks +do not fire, treat Cursor CLI support as hook-limited and gateway-only where +model routing is configurable. + +## Files + +- `.cursor/hooks.json` contains hook entries that run + `nemo-flow-sidecar hook-forward cursor`. + +## Start The Sidecar + +Build or install the sidecar binary so `nemo-flow-sidecar` is on `PATH`. + +Start a local sidecar with ATIF export enabled: + +```bash +NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ +nemo-flow-sidecar --bind 127.0.0.1:4040 +``` + +Use custom upstreams when needed: + +```bash +nemo-flow-sidecar \ + --bind 127.0.0.1:4040 \ + --openai-base-url https://api.openai.com \ + --anthropic-base-url https://api.anthropic.com \ + --atif-dir .nemo-flow/atif +``` + +## Install Hooks + +Inspect generated changes before writing: + +```bash +nemo-flow-sidecar install cursor \ + --scope project \ + --target gui \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --gateway-mode passthrough \ + --dry-run \ + --print +``` + +Install for a project-local Cursor GUI or IDE session: + +```bash +nemo-flow-sidecar install cursor \ + --scope project \ + --target gui \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif \ + --gateway-mode passthrough +``` + +The installer merges NeMo Flow hook entries into `.cursor/hooks.json` and backs +up an existing file before writing. + +## Configure LLM Gateway + +If Cursor exposes provider base URL configuration for the model path being used, +point OpenAI-compatible or Anthropic-compatible traffic at: + +```text +http://127.0.0.1:4040 +``` + +The sidecar forwards OpenAI-compatible `/v1/responses`, +`/v1/chat/completions`, Anthropic-compatible `/v1/messages`, token-count, and +model routes without rewriting provider request or response JSON. + +Hook-only mode observes Cursor agent and tool lifecycle. Missing LLM spans are +expected when Cursor sends model traffic directly to the provider or through a +remote service. + +## Smoke Test + +Verify the sidecar endpoint directly: + +```bash +printf '{"session_id":"smoke-cursor","hook_event_name":"sessionStart"}' \ + | nemo-flow-sidecar hook-forward cursor --sidecar-url http://127.0.0.1:4040 +``` + +The command should print a Cursor-compatible continue response. + +Then run a small Cursor GUI session that starts an agent and uses one simple +tool. The sidecar should receive hook requests for session and tool lifecycle +events. + +For Cursor CLI, run an equivalent `cursor-agent` session and verify that the +sidecar receives hook requests. If no hook requests arrive, treat that CLI +version as hook-limited. + +## Verify ATIF Export + +End the Cursor session and confirm that ATIF was written: + +```bash +ls .nemo-flow/atif +``` + +The sidecar writes `.atif.json` when it receives a session-end hook +for a session with ATIF enabled. + +## Troubleshooting + +If no hook events arrive, confirm Cursor loaded `.cursor/hooks.json`, +`nemo-flow-sidecar` is on `PATH`, and the sidecar is listening on the configured +URL. + +If hooks arrive but LLM spans are missing, confirm the active Cursor GUI or CLI +mode supports provider base URL configuration and points provider traffic to +`http://127.0.0.1:4040`. + +If ATIF is missing, confirm `--atif-dir` or `NEMO_FLOW_ATIF_DIR` is configured +and that the sidecar process can write to the directory. From c508ae0bac2a559e6b956b5add24c85ac8de2b51 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 5 May 2026 16:06:02 -0400 Subject: [PATCH 02/13] chore: publish sidecar crate in release helpers Signed-off-by: Will Killian --- .github/workflows/ci.yaml | 2 +- Cargo.toml | 1 + README.md | 1 + RELEASING.md | 20 +++++++++++--------- docs/getting-started/installation.md | 10 ++++++++++ docs/getting-started/rust.md | 9 +++++++++ docs/reference/api/rust/index.md | 13 +++++++++++-- justfile | 2 +- 8 files changed, 45 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d2691d1..15de867 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -351,7 +351,7 @@ jobs: CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }} run: | set -euo pipefail - for package in nemo-flow nemo-flow-adaptive nemo-flow-ffi; do + for package in nemo-flow nemo-flow-adaptive nemo-flow-ffi nemo-flow-sidecar; do cargo publish --package "$package" --no-verify --allow-dirty done diff --git a/Cargo.toml b/Cargo.toml index 0c79b57..b8c0708 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ repository = "https://github.com/NVIDIA/NeMo-Flow" nemo-flow = { version = "0.2.0", path = "crates/core", default-features = false } nemo-flow-adaptive = { version = "0.2.0", path = "crates/adaptive" } nemo-flow-ffi = { version = "0.2.0", path = "crates/ffi" } +nemo-flow-sidecar = { version = "0.2.0", path = "crates/sidecar" } uuid = "=1.18.1" [workspace.lints.rust] diff --git a/README.md b/README.md index 9682f6d..b50bdcf 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ SPDX-License-Identifier: Apache-2.0 [![npm wasm](https://img.shields.io/npm/v/nemo-flow-wasm?label=nemo-flow-wasm&color=CC3534&logo=npm)](https://www.npmjs.com/package/nemo-flow-wasm) [![Crates.io](https://img.shields.io/crates/v/nemo-flow?label=nemo-flow&color=B7410E&logo=rust)](https://crates.io/crates/nemo-flow) [![Crates.io](https://img.shields.io/crates/v/nemo-flow-adaptive?label=nemo-flow-adaptive&color=B7410E&logo=rust)](https://crates.io/crates/nemo-flow-adaptive) +[![Crates.io](https://img.shields.io/crates/v/nemo-flow-sidecar?label=nemo-flow-sidecar&color=B7410E&logo=rust)](https://crates.io/crates/nemo-flow-sidecar) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/NVIDIA/NeMo-Flow) # NeMo Flow diff --git a/RELEASING.md b/RELEASING.md index 1b3cd16..a652996 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -30,7 +30,7 @@ The release pipeline publishes these package surfaces from a tag push: | Ecosystem | Published Surface | |---|---| -| crates.io | `nemo-flow`, `nemo-flow-adaptive`, `nemo-flow-ffi` | +| crates.io | `nemo-flow`, `nemo-flow-adaptive`, `nemo-flow-ffi`, `nemo-flow-sidecar` | | PyPI | `nemo-flow` | | npm | `nemo-flow-node`, `nemo-flow-wasm` | | GitHub Pages | The documentation site, including the versioned docs build | @@ -45,9 +45,9 @@ NeMo Flow versions are anchored on the workspace SemVer in the repository root - The root `Cargo.toml` `workspace.package.version` is the canonical release version for the Rust workspace. -- The root `Cargo.toml` `workspace.dependencies` entries for - `nemo-flow`, `nemo-flow-adaptive`, and `nemo-flow-ffi` must stay aligned with - that same version. +- The root `Cargo.toml` `workspace.dependencies` entries for `nemo-flow`, + `nemo-flow-adaptive`, `nemo-flow-ffi`, and `nemo-flow-sidecar` must stay + aligned with that same version. - `crates/node/package.json` and `crates/node/package-lock.json` carry the base npm version for the Node.js package and must be bumped explicitly. - The Python package version is derived at packaging time. `pyproject.toml` @@ -86,8 +86,8 @@ Before you create a release tag, confirm the following: 3. The working tree you use for local validation is clean or disposable. 4. Registry credentials and repository settings are in place: - GitHub Actions `id-token: write` access for the top-level crates.io publish job - - crates.io trusted publishers for `nemo-flow`, `nemo-flow-adaptive`, and - `nemo-flow-ffi` are configured for the top-level + - crates.io trusted publishers for `nemo-flow`, `nemo-flow-adaptive`, + `nemo-flow-ffi`, and `nemo-flow-sidecar` are configured for the top-level [`.github/workflows/ci.yaml`](.github/workflows/ci.yaml) workflow - GitHub Actions `id-token: write` access is available for the top-level npm publish job - GitHub Actions `id-token: write` access for the top-level PyPI publish job @@ -100,7 +100,8 @@ Update the versioned source files in the release PR or release-prep commit: 1. Update the root [`Cargo.toml`](Cargo.toml) workspace version. 2. Update the root [`Cargo.toml`](Cargo.toml) `workspace.dependencies` versions - for `nemo-flow`, `nemo-flow-adaptive`, and `nemo-flow-ffi`. + for `nemo-flow`, `nemo-flow-adaptive`, `nemo-flow-ffi`, and + `nemo-flow-sidecar`. 3. Update [`crates/node/package.json`](crates/node/package.json) and [`crates/node/package-lock.json`](crates/node/package-lock.json) to the same release version. @@ -181,8 +182,9 @@ The release pipeline then: 5. Publishes packages from the top-level workflow after the reusable packaging jobs complete: - `publish-rust` stamps Cargo workspace versions from the release tag, then - runs `cargo publish --package` for `nemo-flow`, `nemo-flow-adaptive`, and - `nemo-flow-ffi` through trusted publishing from the top-level workflow + runs `cargo publish --package` for `nemo-flow`, `nemo-flow-adaptive`, + `nemo-flow-ffi`, and `nemo-flow-sidecar` through trusted publishing from + the top-level workflow - `publish-python` uploads the wheel artifacts to PyPI with trusted publishing from the top-level workflow - `publish-npm` publishes the Node.js and WebAssembly npm packages through npm diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index eac059f..2d3b480 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -47,6 +47,9 @@ nemo-flow-adaptive = "0.1.*" - `nemo-flow` provides the core runtime APIs for scopes, middleware, subscribers, plugins, tool calls, and LLM calls. - `nemo-flow-adaptive` provides adaptive runtime primitives and Redis-backed learning components when you want adaptive optimization behavior in Rust. +- `nemo-flow-sidecar` is a published binary crate for coding-agent hook and LLM + gateway observability. Install it with `cargo install nemo-flow-sidecar` when + you need the sidecar executable. ## Install from Source @@ -100,6 +103,13 @@ nemo-flow = { path = "../NeMo-Flow/crates/core" } nemo-flow-adaptive = { path = "../NeMo-Flow/crates/adaptive" } ``` +Install the local sidecar binary from a source checkout when you need to run the +gateway during development: + +```bash +cargo install --path ../NeMo-Flow/crates/sidecar +``` + ## Install from the Repository Use the repository workflow when you are developing against local source, validating unpublished changes, or working across multiple bindings. diff --git a/docs/getting-started/rust.md b/docs/getting-started/rust.md index ced57a8..855bdd7 100644 --- a/docs/getting-started/rust.md +++ b/docs/getting-started/rust.md @@ -25,6 +25,8 @@ serde_json = "1" - `nemo-flow` is the core Rust runtime surface. - `nemo-flow-adaptive` is the companion crate for adaptive runtime primitives and Redis-backed learning components. +- `nemo-flow-sidecar` is a binary crate. Use `cargo install --path + ../NeMo-Flow/crates/sidecar` when you need the local coding-agent gateway. ### Install from a Package Manager @@ -37,6 +39,13 @@ nemo-flow-adaptive = "0.1.*" serde_json = "1" ``` +Install the published sidecar binary separately when you need coding-agent hook +and LLM gateway observability: + +```bash +cargo install nemo-flow-sidecar +``` + ## Push a Scope and Emit a Mark The example below creates a scope and records a mark event from Rust. diff --git a/docs/reference/api/rust/index.md b/docs/reference/api/rust/index.md index 65402f8..a675dc9 100644 --- a/docs/reference/api/rust/index.md +++ b/docs/reference/api/rust/index.md @@ -11,8 +11,10 @@ These pages are generated from the public Rust crates that back the core runtime This summary lists the package identity and support status for the binding. -- Published crates: `nemo-flow` and `nemo-flow-adaptive` -- Local development paths: `crates/core` and `crates/adaptive` +- Published crates: `nemo-flow`, `nemo-flow-adaptive`, `nemo-flow-ffi`, and + `nemo-flow-sidecar` +- Local development paths: `crates/core`, `crates/adaptive`, `crates/ffi`, + and `crates/sidecar` - Primary audience: Rust consumers who want the native runtime surface directly The Rust docs are organized by crate because the Rust binding is the source @@ -25,6 +27,9 @@ These entry points are the primary APIs to use from this binding. - `nemo-flow`: core runtime APIs for scopes, tools, LLMs, registries, subscribers, codecs, streams, and observability - `nemo-flow-adaptive`: adaptive runtime helpers, learner implementations, storage backends, and adaptive configuration +- `nemo-flow-ffi`: raw C ABI used by downstream native bindings +- `nemo-flow-sidecar`: binary gateway sidecar for coding-agent hooks and + passthrough LLM observability Within `nemo-flow`, most integrations start in `api`, especially the `scope`, `tool`, `llm`, `registry`, and `subscriber` modules. Other important public @@ -33,6 +38,8 @@ modules include `codec`, `observability`, `stream`, `error`, and `json`. Within `nemo-flow-adaptive`, the main surfaces include adaptive configuration, plugin components, storage abstractions, learners, trie-backed data structures, and optional Redis-backed helpers when the feature is enabled. +`nemo-flow-sidecar` is a binary crate, so its end-user surface is documented in +the coding-agent sidecar guides rather than generated Rust API pages. ## How To Read The Generated Pages @@ -40,6 +47,8 @@ Use the crate pages first, then expand into the public modules under each crate: - `nemo-flow` for core runtime behavior - `nemo-flow-adaptive` for adaptive and learning-oriented behavior +- `nemo-flow-sidecar` for coding-agent observability through hooks and the + passthrough LLM gateway That structure matches how Rust consumers import items from the crates. diff --git a/justfile b/justfile index c30b941..5700419 100644 --- a/justfile +++ b/justfile @@ -327,7 +327,7 @@ section = "" output = [] changed = [] found_workspace_version = False -local_dependencies = ("nemo-flow", "nemo-flow-adaptive", "nemo-flow-ffi") +local_dependencies = ("nemo-flow", "nemo-flow-adaptive", "nemo-flow-ffi", "nemo-flow-sidecar") found_dependencies = set() for line in text.splitlines(keepends=True): From 8d9596663bb97f6c1ce67160b1ed5be83b279e97 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 5 May 2026 16:26:40 -0400 Subject: [PATCH 03/13] test: increase sidecar coverage Signed-off-by: Will Killian --- crates/sidecar/src/adapters/mod.rs | 112 +++++++++++++++++++ crates/sidecar/src/config.rs | 88 +++++++++++++++ crates/sidecar/src/gateway.rs | 90 +++++++++++++++ crates/sidecar/src/installer.rs | 134 +++++++++++++++++++++++ crates/sidecar/src/server.rs | 62 +++++++++++ crates/sidecar/src/session.rs | 169 +++++++++++++++++++++++++++++ 6 files changed, 655 insertions(+) diff --git a/crates/sidecar/src/adapters/mod.rs b/crates/sidecar/src/adapters/mod.rs index 210cdf0..dc015f3 100644 --- a/crates/sidecar/src/adapters/mod.rs +++ b/crates/sidecar/src/adapters/mod.rs @@ -313,4 +313,116 @@ mod tests { )); assert_eq!(outcome.response, json!({})); } + + #[test] + fn normalizes_mark_style_events_and_header_session_ids() { + let mut headers = HeaderMap::new(); + headers.insert("x-nemo-flow-session-id", "header-session".parse().unwrap()); + headers.insert("x-nemo-flow-config-profile", "coverage".parse().unwrap()); + + for (event_name, expected) in [ + ("UserPromptSubmit", "prompt"), + ("afterAgentResponse", "response"), + ("PreCompact", "compact"), + ("Notification", "notification"), + ("Unrecognized.Event", "hook"), + ] { + let outcome = cursor::adapt( + json!({ + "eventName": event_name, + "model": "model-a", + "cwd": "/repo" + }), + &headers, + ); + let session = match &outcome.events[0] { + NormalizedEvent::PromptSubmitted(event) if expected == "prompt" => event, + NormalizedEvent::AgentResponse(event) if expected == "response" => event, + NormalizedEvent::Compaction(event) if expected == "compact" => event, + NormalizedEvent::Notification(event) if expected == "notification" => event, + NormalizedEvent::HookMark(event) if expected == "hook" => event, + event => panic!("unexpected event for {event_name}: {event:?}"), + }; + assert_eq!(session.session_id, "header-session"); + assert_eq!(session.metadata["model"], json!("model-a")); + assert_eq!(session.metadata["cwd"], json!("/repo")); + assert_eq!( + session.metadata["sidecar_config_profile"], + json!("coverage") + ); + } + } + + #[test] + fn extracts_tool_fields_from_fallback_payload_shapes() { + let headers = HeaderMap::new(); + let outcome = codex::adapt( + json!({ + "conversationId": "conversation-1", + "event": "toolEnded", + "tool": { "id": "tool-id", "name": "Shell" }, + "arguments": { "cmd": "pwd" }, + "result": { "stdout": "/repo" }, + "permission": "allow" + }), + &headers, + ); + + match &outcome.events[0] { + NormalizedEvent::ToolEnded(event) => { + assert_eq!(event.session_id, "conversation-1"); + assert_eq!(event.tool_call_id, "tool-id"); + assert_eq!(event.tool_name, "Shell"); + assert_eq!(event.arguments, json!({ "cmd": "pwd" })); + assert_eq!(event.result, json!({ "stdout": "/repo" })); + assert_eq!(event.status.as_deref(), Some("allow")); + } + event => panic!("unexpected event: {event:?}"), + } + } + + #[test] + fn generated_ids_are_used_when_payload_omits_identifiers() { + let headers = HeaderMap::new(); + let outcome = claude_code::adapt( + json!({ + "hook_event_name": "PreToolUse", + "tool_input": { "name": "Read", "file_path": "Cargo.toml" } + }), + &headers, + ); + + match &outcome.events[0] { + NormalizedEvent::ToolStarted(event) => { + assert!(event.session_id.starts_with("hook-")); + assert!(event.tool_call_id.starts_with("tool-")); + assert_eq!(event.tool_name, "Read"); + } + event => panic!("unexpected event: {event:?}"), + } + } + + #[test] + fn stop_responses_preserve_vendor_shapes() { + let headers = HeaderMap::new(); + let claude = claude_code::adapt( + json!({ + "session_id": "claude-session", + "hook_event_name": "Stop" + }), + &headers, + ); + assert!(matches!(claude.events[0], NormalizedEvent::AgentEnded(_))); + assert_eq!(claude.response["stopReason"], Value::Null); + + let cursor = cursor::adapt( + json!({ + "session_id": "cursor-session", + "hook_event_name": "stop" + }), + &headers, + ); + assert!(matches!(cursor.events[0], NormalizedEvent::AgentEnded(_))); + assert_eq!(cursor.response, json!({ "continue": true })); + } } diff --git a/crates/sidecar/src/config.rs b/crates/sidecar/src/config.rs index 620d1aa..4713646 100644 --- a/crates/sidecar/src/config.rs +++ b/crates/sidecar/src/config.rs @@ -202,3 +202,91 @@ impl GatewayMode { } } } + +#[cfg(test)] +mod tests { + use super::*; + use axum::http::HeaderValue; + use serde_json::json; + + fn config() -> SidecarConfig { + SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://openai".into(), + anthropic_base_url: "http://anthropic".into(), + atif_dir: Some(PathBuf::from("default-atif")), + openinference_endpoint: Some("http://default-otel".into()), + } + } + + #[test] + fn session_config_prefers_headers_and_parses_json() { + let mut headers = HeaderMap::new(); + headers.insert( + "x-nemo-flow-atif-dir", + HeaderValue::from_static("header-atif"), + ); + headers.insert( + "x-nemo-flow-openinference-endpoint", + HeaderValue::from_static("http://header-otel"), + ); + headers.insert( + "x-nemo-flow-config-profile", + HeaderValue::from_static("profile-a"), + ); + headers.insert( + "x-nemo-flow-session-metadata", + HeaderValue::from_static(r#"{"team":"obs"}"#), + ); + headers.insert( + "x-nemo-flow-plugin-config", + HeaderValue::from_static(r#"{"components":[]}"#), + ); + headers.insert( + "x-nemo-flow-gateway-mode", + HeaderValue::from_static("required"), + ); + + let session = config().session_config_from_headers(&headers); + + assert_eq!(session.atif_dir, Some(PathBuf::from("header-atif"))); + assert_eq!( + session.openinference_endpoint.as_deref(), + Some("http://header-otel") + ); + assert_eq!(session.profile.as_deref(), Some("profile-a")); + assert_eq!(session.metadata, Some(json!({ "team": "obs" }))); + assert_eq!(session.plugin_config, Some(json!({ "components": [] }))); + assert_eq!(session.gateway_mode.as_deref(), Some("required")); + } + + #[test] + fn session_config_uses_defaults_and_ignores_bad_json() { + let mut headers = HeaderMap::new(); + headers.insert( + "x-nemo-flow-session-metadata", + HeaderValue::from_static("not-json"), + ); + headers.insert("x-empty", HeaderValue::from_static("")); + + let session = config().session_config_from_headers(&headers); + + assert_eq!(session.atif_dir, Some(PathBuf::from("default-atif"))); + assert_eq!( + session.openinference_endpoint.as_deref(), + Some("http://default-otel") + ); + assert_eq!(session.metadata, None); + assert_eq!(header_string(&headers, "x-empty"), None); + } + + #[test] + fn agent_and_gateway_mode_arguments_are_stable() { + assert_eq!(CodingAgent::ClaudeCode.hook_path(), "/hooks/claude-code"); + assert_eq!(CodingAgent::Codex.hook_path(), "/hooks/codex"); + assert_eq!(CodingAgent::Cursor.hook_path(), "/hooks/cursor"); + assert_eq!(GatewayMode::HookOnly.as_arg(), "hook-only"); + assert_eq!(GatewayMode::Passthrough.as_arg(), "passthrough"); + assert_eq!(GatewayMode::Required.as_arg(), "required"); + } +} diff --git a/crates/sidecar/src/gateway.rs b/crates/sidecar/src/gateway.rs index 7b84e9a..24b92b6 100644 --- a/crates/sidecar/src/gateway.rs +++ b/crates/sidecar/src/gateway.rs @@ -291,6 +291,8 @@ fn stream_response_json(collected: &[u8], truncated: bool) -> Value { #[cfg(test)] mod tests { use super::*; + use crate::config::SidecarConfig; + use axum::http::{HeaderMap, HeaderValue}; #[test] fn removes_hop_by_hop_headers() { @@ -306,6 +308,13 @@ mod tests { assert!(!should_record_header(&HeaderName::from_static( "authorization" ))); + assert!(!should_record_header(&HeaderName::from_static("x-api-key"))); + assert!(!should_record_header(&HeaderName::from_static( + "anthropic-api-key" + ))); + assert!(should_record_header(&HeaderName::from_static( + "x-request-id" + ))); } #[test] @@ -318,6 +327,87 @@ mod tests { ProviderRoute::from_path("/v1/messages/count_tokens"), Some(ProviderRoute::AnthropicCountTokens) ); + assert_eq!( + ProviderRoute::from_path("/v1/chat/completions") + .unwrap() + .name(), + "openai.chat_completions" + ); assert_eq!(ProviderRoute::from_path("/unsupported"), None); } + + #[test] + fn provider_routes_preserve_path_query_and_choose_upstream() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://openai/".into(), + anthropic_base_url: "http://anthropic/".into(), + atif_dir: None, + openinference_endpoint: None, + }; + + assert_eq!( + ProviderRoute::OpenAiResponses.upstream_url(&config, "/v1/responses?x=1"), + "http://openai/v1/responses?x=1" + ); + assert_eq!( + ProviderRoute::AnthropicMessages.upstream_url(&config, "/v1/messages"), + "http://anthropic/v1/messages" + ); + } + + #[test] + fn gateway_session_id_prefers_headers_and_has_fallbacks() { + let mut headers = HeaderMap::new(); + headers.insert( + "anthropic-beta", + HeaderValue::from_static("prompt-caching-2024-07-31"), + ); + assert_eq!( + gateway_session_id(&headers), + "anthropic:prompt-caching-2024-07-31" + ); + + headers.insert( + "x-claude-code-session-id", + HeaderValue::from_static("claude-session"), + ); + assert_eq!(gateway_session_id(&headers), "claude-session"); + + headers.insert( + "x-nemo-flow-session-id", + HeaderValue::from_static("explicit-session"), + ); + assert_eq!(gateway_session_id(&headers), "explicit-session"); + + assert_eq!(gateway_session_id(&HeaderMap::new()), "gateway-gateway"); + } + + #[test] + fn observable_headers_omit_secrets_and_transport_headers() { + let mut headers = HeaderMap::new(); + headers.insert("authorization", HeaderValue::from_static("Bearer secret")); + headers.insert("x-api-key", HeaderValue::from_static("secret")); + headers.insert("connection", HeaderValue::from_static("close")); + headers.insert("x-request-id", HeaderValue::from_static("req-1")); + + let observed = observable_headers(&headers); + + assert_eq!(observed.get("x-request-id"), Some(&json!("req-1"))); + assert!(!observed.contains_key("authorization")); + assert!(!observed.contains_key("x-api-key")); + assert!(!observed.contains_key("connection")); + } + + #[test] + fn stream_response_records_preview_and_truncation() { + assert_eq!( + stream_response_json(b"data: done", false), + json!({ "stream": "data: done" }) + ); + assert_eq!( + stream_response_json(b"partial", true), + json!({ "stream_preview": "partial", "stream_truncated": true }) + ); + } } diff --git a/crates/sidecar/src/installer.rs b/crates/sidecar/src/installer.rs index 95fa665..f21caab 100644 --- a/crates/sidecar/src/installer.rs +++ b/crates/sidecar/src/installer.rs @@ -521,6 +521,14 @@ mod tests { } } + fn project_command(agent: CodingAgent, root: &Path) -> InstallCommand { + InstallCommand { + scope: InstallScope::Project, + project_dir: Some(root.to_path_buf()), + ..command(agent, root) + } + } + #[test] fn generates_claude_install_file() { let temp = tempfile::tempdir().unwrap(); @@ -582,6 +590,132 @@ mod tests { assert_eq!(twice["hooks"]["Stop"].as_array().unwrap().len(), 2); } + #[test] + fn project_install_uses_project_dir_and_preserves_codex_toml() { + let temp = tempfile::tempdir().unwrap(); + let codex_dir = temp.path().join(".codex"); + std::fs::create_dir_all(&codex_dir).unwrap(); + std::fs::write( + codex_dir.join("config.toml"), + "[features]\nother = true\n[model_providers.openai]\nbase_url = \"http://old\"\n", + ) + .unwrap(); + + let files = planned_files(&project_command(CodingAgent::Codex, temp.path())).unwrap(); + + assert!(files[0].path.starts_with(temp.path())); + assert!(files[0].contents.contains("other = true")); + assert!(files[0].contents.contains("codex_hooks = true")); + assert!(files[0].contents.contains("[model_providers.openai]")); + } + + #[test] + fn install_writes_file_and_backs_up_existing_config() { + let temp = tempfile::tempdir().unwrap(); + let claude_dir = temp.path().join(".claude"); + std::fs::create_dir_all(&claude_dir).unwrap(); + let settings = claude_dir.join("settings.json"); + std::fs::write(&settings, r#"{"hooks":{"Stop":[]}}"#).unwrap(); + + install(command(CodingAgent::ClaudeCode, temp.path())).unwrap(); + + let installed = std::fs::read_to_string(&settings).unwrap(); + assert!(installed.contains("hook-forward claude-code")); + let backups: Vec<_> = std::fs::read_dir(&claude_dir) + .unwrap() + .map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned()) + .filter(|name| name.starts_with("settings.json.bak.")) + .collect(); + assert_eq!(backups.len(), 1); + } + + #[test] + fn install_dry_run_does_not_write_files() { + let temp = tempfile::tempdir().unwrap(); + let mut command = command(CodingAgent::Cursor, temp.path()); + command.dry_run = true; + command.print = true; + + install(command).unwrap(); + + assert!(!temp.path().join(".cursor/hooks.json").exists()); + } + + #[test] + fn invalid_json_config_is_rejected_before_planning() { + let temp = tempfile::tempdir().unwrap(); + let mut command = command(CodingAgent::Codex, temp.path()); + command.session_metadata = Some("not-json".into()); + + let error = install(command).unwrap_err().to_string(); + + assert!(error.contains("invalid session metadata")); + } + + #[test] + fn merge_hooks_rejects_malformed_shapes() { + assert!(merge_hooks(json!([]), codex_hooks("cmd")).is_err()); + assert!(merge_hooks(json!({ "hooks": [] }), codex_hooks("cmd")).is_err()); + assert!(merge_hooks(json!({ "hooks": { "Stop": {} } }), codex_hooks("cmd")).is_err()); + assert!(merge_hooks(json!({}), json!({ "hooks": [] })).is_err()); + } + + #[test] + fn invalid_existing_files_are_reported() { + let temp = tempfile::tempdir().unwrap(); + let cursor_dir = temp.path().join(".cursor"); + std::fs::create_dir_all(&cursor_dir).unwrap(); + std::fs::write(cursor_dir.join("hooks.json"), "not-json").unwrap(); + + let error = planned_files(&command(CodingAgent::Cursor, temp.path())) + .unwrap_err() + .to_string(); + + assert!(error.contains("invalid JSON")); + + let codex_dir = temp.path().join(".codex"); + std::fs::create_dir_all(&codex_dir).unwrap(); + std::fs::write(codex_dir.join("config.toml"), "not = [valid").unwrap(); + let error = planned_files(&command(CodingAgent::Codex, temp.path())) + .unwrap_err() + .to_string(); + assert!(error.contains("invalid TOML")); + } + + #[test] + fn helper_formatting_and_headers_cover_optional_paths() { + assert_eq!(shell_quote("plain/arg-1"), "plain/arg-1"); + assert_eq!(shell_quote("needs space"), "'needs space'"); + assert_eq!(shell_quote("can't"), "'can'\\''t'"); + assert!(event_matches_tools("PermissionRequest")); + assert!(!event_matches_tools("SessionStart")); + + let temp = tempfile::tempdir().unwrap(); + let headers = sidecar_headers( + Some(temp.path()), + Some("http://otel"), + Some("profile"), + Some(r#"{"team":"obs"}"#), + Some(r#"{"plugins":[]}"#), + Some(GatewayMode::Passthrough), + ) + .unwrap(); + assert_eq!( + headers + .get("x-nemo-flow-gateway-mode") + .and_then(|value| value.to_str().ok()), + Some("passthrough") + ); + assert!( + insert_header( + &mut HeaderMap::new(), + "x-nemo-flow-config-profile", + Some("bad\nvalue") + ) + .is_err() + ); + } + #[test] fn packaged_hook_configs_are_valid_json() { let root = diff --git a/crates/sidecar/src/server.rs b/crates/sidecar/src/server.rs index 62a92a2..36fe36f 100644 --- a/crates/sidecar/src/server.rs +++ b/crates/sidecar/src/server.rs @@ -259,6 +259,49 @@ mod tests { assert_eq!(bytes, Bytes::from_static(b"data: one\n\ndata: two\n\n")); } + #[tokio::test] + async fn gateway_rejects_unsupported_paths() { + let app = router(test_config()); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/v1/unsupported") + .header("content-type", "application/json") + .body(Body::from("{}")) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } + + #[tokio::test] + async fn models_route_forwards_get_requests() { + let upstream = spawn_models_upstream().await; + let mut config = test_config(); + config.openai_base_url = upstream; + let app = router(config); + let response = app + .oneshot( + Request::builder() + .method("GET") + .uri("/v1/models?limit=1") + .header("authorization", "Bearer test") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body["path"], json!("/v1/models?limit=1")); + assert_eq!(body["authorization"], json!("Bearer test")); + } + async fn spawn_upstream(streaming: bool) -> String { async fn chat(headers: HeaderMap, body: Bytes) -> impl IntoResponse { let payload: Value = serde_json::from_slice(&body).unwrap(); @@ -296,4 +339,23 @@ mod tests { }); format!("http://{address}") } + + async fn spawn_models_upstream() -> String { + async fn models(headers: HeaderMap, request: Request) -> impl IntoResponse { + Json(json!({ + "path": request.uri().path_and_query().map(|value| value.as_str()), + "authorization": headers + .get(header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()) + })) + } + + let app = Router::new().route("/v1/models", get(models)); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + format!("http://{address}") + } } diff --git a/crates/sidecar/src/session.rs b/crates/sidecar/src/session.rs index 1bc8a08..96237fc 100644 --- a/crates/sidecar/src/session.rs +++ b/crates/sidecar/src/session.rs @@ -547,4 +547,173 @@ mod tests { manager.apply_events(&headers, events).await.unwrap(); assert!(manager.inner.lock().await.is_empty()); } + + #[tokio::test] + async fn writes_atif_on_session_end_from_header_config() { + let temp = tempfile::tempdir().unwrap(); + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + }; + let manager = SessionManager::new(config); + let mut headers = HeaderMap::new(); + headers.insert( + "x-nemo-flow-atif-dir", + temp.path().to_string_lossy().parse().unwrap(), + ); + headers.insert( + "x-nemo-flow-session-metadata", + r#"{"team":"coverage"}"#.parse().unwrap(), + ); + headers.insert("x-nemo-flow-gateway-mode", "required".parse().unwrap()); + + manager + .apply_events( + &headers, + vec![ + NormalizedEvent::AgentStarted(SessionEvent { + session_id: "atif-session".into(), + agent_kind: AgentKind::Codex, + event_name: "sessionStart".into(), + payload: json!({ "start": true }), + metadata: json!({ "agent": "codex" }), + }), + NormalizedEvent::PromptSubmitted(SessionEvent { + session_id: "atif-session".into(), + agent_kind: AgentKind::Codex, + event_name: "UserPromptSubmit".into(), + payload: json!({ "prompt": "hello" }), + metadata: json!({}), + }), + NormalizedEvent::AgentEnded(SessionEvent { + session_id: "atif-session".into(), + agent_kind: AgentKind::Codex, + event_name: "sessionEnd".into(), + payload: json!({ "done": true }), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + let path = temp.path().join("atif-session.atif.json"); + let atif: Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); + assert_eq!(atif["agent"]["name"], json!("codex")); + } + + #[tokio::test] + async fn handles_out_of_order_subagent_and_tool_end_events() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + }; + let manager = SessionManager::new(config); + let headers = HeaderMap::new(); + + manager + .apply_events( + &headers, + vec![ + NormalizedEvent::SubagentEnded(SubagentEvent { + session_id: "out-of-order".into(), + agent_kind: AgentKind::Cursor, + event_name: "subagentStop".into(), + subagent_id: "missing".into(), + payload: json!({ "reason": "missing-start" }), + metadata: json!({}), + }), + NormalizedEvent::ToolEnded(ToolEvent { + session_id: "out-of-order".into(), + agent_kind: AgentKind::Cursor, + event_name: "postToolUse".into(), + tool_call_id: "tool-without-start".into(), + tool_name: "Shell".into(), + subagent_id: None, + arguments: json!({ "cmd": "pwd" }), + result: json!({ "stdout": "/repo" }), + status: Some("success".into()), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::AgentEnded(SessionEvent { + session_id: "out-of-order".into(), + agent_kind: AgentKind::Cursor, + event_name: "sessionEnd".into(), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + assert!(manager.inner.lock().await.is_empty()); + } + + #[tokio::test] + async fn llm_lifecycle_starts_implicit_gateway_session() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + }; + let manager = SessionManager::new(config); + let active = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: "llm-session".into(), + provider: "openai.responses".into(), + model_name: Some("gpt-test".into()), + request: LlmRequest { + headers: Map::new(), + content: json!({ "model": "gpt-test", "input": "hello" }), + }, + streaming: true, + metadata: json!({ "gateway_path": "/v1/responses" }), + }, + ) + .await + .unwrap(); + manager + .end_llm( + active, + json!({ "output_text": "hello" }), + json!({ "http_status": 200 }), + ) + .await + .unwrap(); + + let sessions = manager.inner.lock().await; + assert!(sessions.contains_key("llm-session")); + } + + #[test] + fn merge_metadata_handles_objects_nulls_and_scalars() { + assert_eq!( + merge_metadata(json!({ "a": 1 }), json!({ "b": 2, "c": null })), + json!({ "a": 1, "b": 2 }) + ); + assert_eq!( + merge_metadata(Value::Null, json!({ "a": 1 })), + json!({ "a": 1 }) + ); + assert_eq!( + merge_metadata(json!({ "a": 1 }), Value::Null), + json!({ "a": 1 }) + ); + assert_eq!( + merge_metadata(json!("left"), json!("right")), + json!({ "metadata": "left", "extra_metadata": "right" }) + ); + } } From afe8a88b42f876c33ded57772d29077ff7f7167e Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 6 May 2026 10:39:16 -0400 Subject: [PATCH 04/13] Add transparent sidecar execution Signed-off-by: Will Killian --- ATTRIBUTIONS-Rust.md | 420 +++++++++++++++ Cargo.lock | 1 + codecov.yml | 2 + crates/sidecar/Cargo.toml | 3 +- crates/sidecar/src/adapters/claude_code.rs | 12 +- crates/sidecar/src/adapters/mod.rs | 205 +------- crates/sidecar/src/config.rs | 487 ++++++++++++++---- crates/sidecar/src/error.rs | 6 + crates/sidecar/src/gateway.rs | 187 ++----- crates/sidecar/src/installer.rs | 270 ++-------- crates/sidecar/src/launcher.rs | 414 +++++++++++++++ crates/sidecar/src/main.rs | 32 +- crates/sidecar/src/server.rs | 300 +---------- crates/sidecar/src/session.rs | 268 +--------- .../sidecar/tests/coverage/adapters_tests.rs | 229 ++++++++ crates/sidecar/tests/coverage/config_tests.rs | 278 ++++++++++ .../sidecar/tests/coverage/gateway_tests.rs | 128 +++++ .../sidecar/tests/coverage/installer_tests.rs | 234 +++++++++ .../sidecar/tests/coverage/launcher_tests.rs | 308 +++++++++++ crates/sidecar/tests/coverage/server_tests.rs | 294 +++++++++++ .../sidecar/tests/coverage/session_tests.rs | 311 +++++++++++ docs/integrate-frameworks/about.md | 6 +- .../coding-agent-claude-code.md | 83 +-- .../coding-agent-codex.md | 84 +-- .../coding-agent-cursor.md | 72 +-- .../coding-agent-sidecar.md | 105 +++- integrations/coding-agents/README.md | 63 ++- .../coding-agents/claude-code/README.md | 120 ++--- integrations/coding-agents/codex/README.md | 133 ++--- integrations/coding-agents/cursor/README.md | 126 ++--- 30 files changed, 3602 insertions(+), 1579 deletions(-) create mode 100644 crates/sidecar/src/launcher.rs create mode 100644 crates/sidecar/tests/coverage/adapters_tests.rs create mode 100644 crates/sidecar/tests/coverage/config_tests.rs create mode 100644 crates/sidecar/tests/coverage/gateway_tests.rs create mode 100644 crates/sidecar/tests/coverage/installer_tests.rs create mode 100644 crates/sidecar/tests/coverage/launcher_tests.rs create mode 100644 crates/sidecar/tests/coverage/server_tests.rs create mode 100644 crates/sidecar/tests/coverage/session_tests.rs diff --git a/ATTRIBUTIONS-Rust.md b/ATTRIBUTIONS-Rust.md index ca0023c..2aa3bcd 100644 --- a/ATTRIBUTIONS-Rust.md +++ b/ATTRIBUTIONS-Rust.md @@ -7565,6 +7565,426 @@ Software. limitations under the License. +``` + +## serde_spanned - 1.1.1 +**Repository URL**: https://github.com/toml-rs/toml +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + +``` + +## toml - 0.9.12+spec-1.1.0 +**Repository URL**: https://github.com/toml-rs/toml +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` + 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. + + ``` ## toml_datetime - 0.7.5+spec-1.1.0 diff --git a/Cargo.lock b/Cargo.lock index 0fd0605..7070a48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1301,6 +1301,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "toml", "toml_edit", "tower", "uuid", diff --git a/codecov.yml b/codecov.yml index 29581c4..f044574 100644 --- a/codecov.yml +++ b/codecov.yml @@ -48,6 +48,7 @@ component_management: paths: - "crates/core/src" - "crates/adaptive/src" + - "crates/sidecar/src" statuses: - type: project target: 95% @@ -112,6 +113,7 @@ ignore: - "**/examples/**" - "third_party/" - "**/tests/**" + - "crates/sidecar/tests/" - "**/tests-js/**" # The Node binding currently reports JS package coverage separately; exclude the # native Rust bridge until we have direct Rust-side coverage for this crate. diff --git a/crates/sidecar/Cargo.toml b/crates/sidecar/Cargo.toml index 096027a..0020907 100644 --- a/crates/sidecar/Cargo.toml +++ b/crates/sidecar/Cargo.toml @@ -25,7 +25,8 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "rus serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" -tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] } +tokio = { version = "1", features = ["macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"] } +toml = "0.9" toml_edit = "0.23" uuid = { workspace = true, features = ["serde", "v7"] } diff --git a/crates/sidecar/src/adapters/claude_code.rs b/crates/sidecar/src/adapters/claude_code.rs index 6edc6c3..c5d6bd9 100644 --- a/crates/sidecar/src/adapters/claude_code.rs +++ b/crates/sidecar/src/adapters/claude_code.rs @@ -21,15 +21,21 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { tool_end: &[ "PostToolUse", "postToolUse", + "PostToolUseFailure", + "postToolUseFailure", "ToolUseFailed", "toolUseFailed", ], }, ); let response = match &event { - NormalizedEvent::ToolStarted(_) => { - json!({ "continue": true, "permissionDecision": "allow" }) - } + NormalizedEvent::ToolStarted(_) => json!({ + "continue": true, + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow" + } + }), NormalizedEvent::AgentEnded(_) => json!({ "continue": true, "stopReason": null }), _ => json!({ "continue": true }), }; diff --git a/crates/sidecar/src/adapters/mod.rs b/crates/sidecar/src/adapters/mod.rs index dc015f3..4511c8d 100644 --- a/crates/sidecar/src/adapters/mod.rs +++ b/crates/sidecar/src/adapters/mod.rs @@ -100,6 +100,7 @@ fn common_tool_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> T let session = common_session_event(payload, headers, kind); let tool_call_id = string_at(payload, &["tool_call_id"]) .or_else(|| string_at(payload, &["toolCallId"])) + .or_else(|| string_at(payload, &["tool_use_id"])) .or_else(|| string_at(payload, &["call_id"])) .or_else(|| string_at(payload, &["tool", "id"])) .or_else(|| string_at(payload, &["tool_input", "id"])) @@ -117,9 +118,11 @@ fn common_tool_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> T .or_else(|| value_at(payload, &["args"])) .unwrap_or(Value::Null); let result = value_at(payload, &["tool_output"]) + .or_else(|| value_at(payload, &["tool_response"])) .or_else(|| value_at(payload, &["output"])) .or_else(|| value_at(payload, &["result"])) .unwrap_or(Value::Null); + let normalized_event = normalize_name(&session.event_name); ToolEvent { session_id: session.session_id, agent_kind: kind, @@ -132,7 +135,11 @@ fn common_tool_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> T result, status: string_at(payload, &["status"]) .or_else(|| string_at(payload, &["decision"])) - .or_else(|| string_at(payload, &["permission"])), + .or_else(|| string_at(payload, &["permission"])) + .or_else(|| { + (normalized_event.contains("failure") || normalized_event.contains("failed")) + .then_some("error".to_string()) + }), payload: session.payload, metadata: session.metadata, } @@ -232,197 +239,5 @@ fn normalize_name(name: &str) -> String { } #[cfg(test)] -mod tests { - use axum::http::HeaderMap; - use serde_json::json; - - use super::*; - use crate::adapters::{claude_code, codex, cursor}; - - #[test] - fn maps_claude_canonical_tool_payload() { - let headers = HeaderMap::new(); - let outcome = claude_code::adapt( - json!({ - "session_id": "claude-session", - "transcript_path": "/tmp/transcript.jsonl", - "cwd": "/workspace", - "hook_event_name": "PreToolUse", - "tool_name": "Read", - "tool_input": { "file_path": "README.md" } - }), - &headers, - ); - match &outcome.events[0] { - NormalizedEvent::ToolStarted(event) => { - assert_eq!(event.session_id, "claude-session"); - assert_eq!(event.tool_name, "Read"); - assert_eq!(event.arguments, json!({ "file_path": "README.md" })); - assert_eq!( - event.metadata["transcript_path"], - json!("/tmp/transcript.jsonl") - ); - } - event => panic!("unexpected event: {event:?}"), - } - assert_eq!(outcome.response["continue"], json!(true)); - assert_eq!(outcome.response["permissionDecision"], json!("allow")); - } - - #[test] - fn maps_cursor_subagent_and_permission_response() { - let headers = HeaderMap::new(); - let outcome = cursor::adapt( - json!({ - "session_id": "cursor-session", - "project_dir": "/repo", - "user_email": "dev@example.com", - "hook_event_name": "beforeShellExecution", - "subagent": { "id": "worker" }, - "tool_call_id": "shell-1", - "tool_name": "shell", - "input": { "command": "cargo test" } - }), - &headers, - ); - match &outcome.events[0] { - NormalizedEvent::ToolStarted(event) => { - assert_eq!(event.session_id, "cursor-session"); - assert_eq!(event.subagent_id.as_deref(), Some("worker")); - assert_eq!(event.metadata["project_dir"], json!("/repo")); - assert_eq!(event.metadata["user_email"], json!("dev@example.com")); - } - event => panic!("unexpected event: {event:?}"), - } - assert_eq!(outcome.response["permission"], json!("allow")); - } - - #[test] - fn keeps_codex_response_unwrapped() { - let headers = HeaderMap::new(); - let outcome = codex::adapt( - json!({ - "session_id": "codex-session", - "hook_event_name": "sessionStart" - }), - &headers, - ); - assert!(matches!( - outcome.events[0], - NormalizedEvent::AgentStarted(_) - )); - assert_eq!(outcome.response, json!({})); - } - - #[test] - fn normalizes_mark_style_events_and_header_session_ids() { - let mut headers = HeaderMap::new(); - headers.insert("x-nemo-flow-session-id", "header-session".parse().unwrap()); - headers.insert("x-nemo-flow-config-profile", "coverage".parse().unwrap()); - - for (event_name, expected) in [ - ("UserPromptSubmit", "prompt"), - ("afterAgentResponse", "response"), - ("PreCompact", "compact"), - ("Notification", "notification"), - ("Unrecognized.Event", "hook"), - ] { - let outcome = cursor::adapt( - json!({ - "eventName": event_name, - "model": "model-a", - "cwd": "/repo" - }), - &headers, - ); - let session = match &outcome.events[0] { - NormalizedEvent::PromptSubmitted(event) if expected == "prompt" => event, - NormalizedEvent::AgentResponse(event) if expected == "response" => event, - NormalizedEvent::Compaction(event) if expected == "compact" => event, - NormalizedEvent::Notification(event) if expected == "notification" => event, - NormalizedEvent::HookMark(event) if expected == "hook" => event, - event => panic!("unexpected event for {event_name}: {event:?}"), - }; - assert_eq!(session.session_id, "header-session"); - assert_eq!(session.metadata["model"], json!("model-a")); - assert_eq!(session.metadata["cwd"], json!("/repo")); - assert_eq!( - session.metadata["sidecar_config_profile"], - json!("coverage") - ); - } - } - - #[test] - fn extracts_tool_fields_from_fallback_payload_shapes() { - let headers = HeaderMap::new(); - let outcome = codex::adapt( - json!({ - "conversationId": "conversation-1", - "event": "toolEnded", - "tool": { "id": "tool-id", "name": "Shell" }, - "arguments": { "cmd": "pwd" }, - "result": { "stdout": "/repo" }, - "permission": "allow" - }), - &headers, - ); - - match &outcome.events[0] { - NormalizedEvent::ToolEnded(event) => { - assert_eq!(event.session_id, "conversation-1"); - assert_eq!(event.tool_call_id, "tool-id"); - assert_eq!(event.tool_name, "Shell"); - assert_eq!(event.arguments, json!({ "cmd": "pwd" })); - assert_eq!(event.result, json!({ "stdout": "/repo" })); - assert_eq!(event.status.as_deref(), Some("allow")); - } - event => panic!("unexpected event: {event:?}"), - } - } - - #[test] - fn generated_ids_are_used_when_payload_omits_identifiers() { - let headers = HeaderMap::new(); - let outcome = claude_code::adapt( - json!({ - "hook_event_name": "PreToolUse", - "tool_input": { "name": "Read", "file_path": "Cargo.toml" } - }), - &headers, - ); - - match &outcome.events[0] { - NormalizedEvent::ToolStarted(event) => { - assert!(event.session_id.starts_with("hook-")); - assert!(event.tool_call_id.starts_with("tool-")); - assert_eq!(event.tool_name, "Read"); - } - event => panic!("unexpected event: {event:?}"), - } - } - - #[test] - fn stop_responses_preserve_vendor_shapes() { - let headers = HeaderMap::new(); - let claude = claude_code::adapt( - json!({ - "session_id": "claude-session", - "hook_event_name": "Stop" - }), - &headers, - ); - assert!(matches!(claude.events[0], NormalizedEvent::AgentEnded(_))); - assert_eq!(claude.response["stopReason"], Value::Null); - - let cursor = cursor::adapt( - json!({ - "session_id": "cursor-session", - "hook_event_name": "stop" - }), - &headers, - ); - assert!(matches!(cursor.events[0], NormalizedEvent::AgentEnded(_))); - assert_eq!(cursor.response, json!({ "continue": true })); - } -} +#[path = "../../tests/coverage/adapters_tests.rs"] +mod tests; diff --git a/crates/sidecar/src/config.rs b/crates/sidecar/src/config.rs index 4713646..af2b74e 100644 --- a/crates/sidecar/src/config.rs +++ b/crates/sidecar/src/config.rs @@ -6,14 +6,17 @@ use std::path::PathBuf; use axum::http::HeaderMap; use clap::{Args, Parser, Subcommand, ValueEnum}; +use serde::Deserialize; use serde_json::Value; +use crate::error::SidecarError; + #[derive(Debug, Clone, Parser)] #[command(name = "nemo-flow-sidecar")] #[command(about = "Gateway sidecar for coding-agent NeMo Flow observability")] pub(crate) struct Cli { #[command(flatten)] - pub(crate) server: SidecarConfig, + pub(crate) server: ServerArgs, #[command(subcommand)] pub(crate) command: Option, } @@ -22,28 +25,34 @@ pub(crate) struct Cli { pub(crate) enum Command { Install(InstallCommand), HookForward(HookForwardCommand), + Run(RunCommand), } -#[derive(Debug, Clone, Args)] +#[derive(Debug, Clone, Default, Args)] +pub(crate) struct ServerArgs { + #[arg(long)] + pub(crate) config: Option, + #[arg(long, env = "NEMO_FLOW_SIDECAR_BIND")] + pub(crate) bind: Option, + #[arg(long, env = "NEMO_FLOW_OPENAI_BASE_URL")] + pub(crate) openai_base_url: Option, + #[arg(long, env = "NEMO_FLOW_ANTHROPIC_BASE_URL")] + pub(crate) anthropic_base_url: Option, + #[arg(long, env = "NEMO_FLOW_ATIF_DIR")] + pub(crate) atif_dir: Option, + #[arg(long, env = "NEMO_FLOW_OPENINFERENCE_ENDPOINT")] + pub(crate) openinference_endpoint: Option, +} + +#[derive(Debug, Clone)] pub(crate) struct SidecarConfig { - #[arg(long, env = "NEMO_FLOW_SIDECAR_BIND", default_value = "127.0.0.1:4040")] pub(crate) bind: SocketAddr, - #[arg( - long, - env = "NEMO_FLOW_OPENAI_BASE_URL", - default_value = "https://api.openai.com" - )] pub(crate) openai_base_url: String, - #[arg( - long, - env = "NEMO_FLOW_ANTHROPIC_BASE_URL", - default_value = "https://api.anthropic.com" - )] pub(crate) anthropic_base_url: String, - #[arg(long, env = "NEMO_FLOW_ATIF_DIR")] pub(crate) atif_dir: Option, - #[arg(long, env = "NEMO_FLOW_OPENINFERENCE_ENDPOINT")] pub(crate) openinference_endpoint: Option, + pub(crate) metadata: Option, + pub(crate) plugin_config: Option, } #[derive(Debug, Clone, Args)] @@ -82,8 +91,8 @@ pub(crate) struct InstallCommand { pub(crate) struct HookForwardCommand { #[arg(value_enum)] pub(crate) agent: CodingAgent, - #[arg(long, default_value = "http://127.0.0.1:4040")] - pub(crate) sidecar_url: String, + #[arg(long)] + pub(crate) sidecar_url: Option, #[arg(long)] pub(crate) atif_dir: Option, #[arg(long)] @@ -100,6 +109,32 @@ pub(crate) struct HookForwardCommand { pub(crate) fail_closed: bool, } +#[derive(Debug, Clone, Args)] +pub(crate) struct RunCommand { + #[arg(long, value_enum)] + pub(crate) agent: Option, + #[arg(long)] + pub(crate) config: Option, + #[arg(long)] + pub(crate) openai_base_url: Option, + #[arg(long)] + pub(crate) anthropic_base_url: Option, + #[arg(long)] + pub(crate) atif_dir: Option, + #[arg(long)] + pub(crate) openinference_endpoint: Option, + #[arg(long)] + pub(crate) session_metadata: Option, + #[arg(long)] + pub(crate) plugin_config: Option, + #[arg(long)] + pub(crate) dry_run: bool, + #[arg(long)] + pub(crate) print: bool, + #[arg(last = true)] + pub(crate) command: Vec, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] #[value(rename_all = "kebab-case")] pub(crate) enum CodingAgent { @@ -148,8 +183,10 @@ impl SidecarConfig { .or_else(|| self.atif_dir.clone()); let openinference_endpoint = header_string(headers, "x-nemo-flow-openinference-endpoint") .or_else(|| self.openinference_endpoint.clone()); - let metadata = header_json(headers, "x-nemo-flow-session-metadata"); - let plugin_config = header_json(headers, "x-nemo-flow-plugin-config"); + let metadata = + header_json(headers, "x-nemo-flow-session-metadata").or_else(|| self.metadata.clone()); + let plugin_config = header_json(headers, "x-nemo-flow-plugin-config") + .or_else(|| self.plugin_config.clone()); let profile = header_string(headers, "x-nemo-flow-config-profile"); let gateway_mode = header_string(headers, "x-nemo-flow-gateway-mode"); SessionConfig { @@ -163,6 +200,317 @@ impl SidecarConfig { } } +#[derive(Debug, Clone, Default)] +pub(crate) struct ResolvedConfig { + pub(crate) sidecar: SidecarConfig, + pub(crate) agents: AgentConfigs, +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct AgentConfigs { + pub(crate) claude_code: AgentCommandConfig, + pub(crate) codex: AgentCommandConfig, + pub(crate) cursor: CursorAgentConfig, +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct AgentCommandConfig { + pub(crate) command: Option, +} + +#[derive(Debug, Clone)] +pub(crate) struct CursorAgentConfig { + pub(crate) command: Option, + pub(crate) patch_restore_hooks: bool, +} + +impl Default for CursorAgentConfig { + fn default() -> Self { + Self { + command: None, + patch_restore_hooks: true, + } + } +} + +#[derive(Debug, Clone, Default, Deserialize)] +struct FileConfig { + server: Option, + session: Option, + export: Option, + agents: Option, +} + +#[derive(Debug, Clone, Default, Deserialize)] +struct FileServerConfig { + openai_base_url: Option, + anthropic_base_url: Option, +} + +#[derive(Debug, Clone, Default, Deserialize)] +struct FileSessionConfig { + atif_dir: Option, + metadata: Option, + plugin_config: Option, +} + +#[derive(Debug, Clone, Default, Deserialize)] +struct FileExportConfig { + openinference: Option, +} + +#[derive(Debug, Clone, Default, Deserialize)] +struct FileOpenInferenceConfig { + endpoint: Option, +} + +#[derive(Debug, Clone, Default, Deserialize)] +struct FileAgentsConfig { + #[serde(rename = "claude-code")] + claude_code: Option, + codex: Option, + cursor: Option, +} + +#[derive(Debug, Clone, Default, Deserialize)] +struct FileAgentCommandConfig { + command: Option, +} + +#[derive(Debug, Clone, Default, Deserialize)] +struct FileCursorAgentConfig { + command: Option, + patch_restore_hooks: Option, +} + +impl Default for SidecarConfig { + fn default() -> Self { + Self { + bind: "127.0.0.1:4040" + .parse() + .expect("valid default bind address"), + openai_base_url: "https://api.openai.com".into(), + anthropic_base_url: "https://api.anthropic.com".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + } + } +} + +pub(crate) fn resolve_server_config(args: &ServerArgs) -> Result { + let mut resolved = load_shared_config(args.config.as_ref())?; + apply_server_overrides(&mut resolved.sidecar, args); + Ok(resolved) +} + +pub(crate) fn resolve_run_config( + command: &RunCommand, + inherited: Option<&ServerArgs>, +) -> Result { + let config = command + .config + .as_ref() + .or_else(|| inherited.and_then(|args| args.config.as_ref())); + let mut resolved = load_shared_config(config)?; + if let Some(args) = inherited { + apply_server_overrides(&mut resolved.sidecar, args); + } + if let Some(value) = &command.openai_base_url { + resolved.sidecar.openai_base_url = value.clone(); + } + if let Some(value) = &command.anthropic_base_url { + resolved.sidecar.anthropic_base_url = value.clone(); + } + if let Some(value) = &command.atif_dir { + resolved.sidecar.atif_dir = Some(value.clone()); + } + if let Some(value) = &command.openinference_endpoint { + resolved.sidecar.openinference_endpoint = Some(value.clone()); + } + if let Some(value) = &command.session_metadata { + resolved.sidecar.metadata = Some(parse_json_option("session metadata", value)?); + } + if let Some(value) = &command.plugin_config { + resolved.sidecar.plugin_config = Some(parse_json_option("plugin config", value)?); + } + resolved.sidecar.bind = "127.0.0.1:0" + .parse() + .expect("valid transparent bind address"); + Ok(resolved) +} + +fn apply_server_overrides(config: &mut SidecarConfig, args: &ServerArgs) { + if let Some(value) = args.bind { + config.bind = value; + } + if let Some(value) = &args.openai_base_url { + config.openai_base_url = value.clone(); + } + if let Some(value) = &args.anthropic_base_url { + config.anthropic_base_url = value.clone(); + } + if let Some(value) = &args.atif_dir { + config.atif_dir = Some(value.clone()); + } + if let Some(value) = &args.openinference_endpoint { + config.openinference_endpoint = Some(value.clone()); + } +} + +fn load_shared_config(explicit: Option<&PathBuf>) -> Result { + let mut merged = toml::Value::Table(toml::map::Map::new()); + for path in config_paths(explicit) { + if path.exists() { + let raw = std::fs::read_to_string(&path)?; + let parsed = raw + .parse::() + .map(toml::Value::Table) + .map_err(|error| { + SidecarError::Config(format!("invalid TOML in {}: {error}", path.display())) + })?; + merge_toml(&mut merged, parsed); + } + } + let mut resolved = ResolvedConfig { + sidecar: SidecarConfig::default(), + ..ResolvedConfig::default() + }; + apply_file_config(&mut resolved, merged)?; + apply_env_config(&mut resolved.sidecar); + Ok(resolved) +} + +fn config_paths(explicit: Option<&PathBuf>) -> Vec { + if let Some(path) = explicit { + return vec![path.clone()]; + } + let mut paths = vec![PathBuf::from("/etc/nemo-flow/sidecar.toml")]; + if let Ok(cwd) = std::env::current_dir() + && let Some(project) = find_project_config(&cwd) + { + paths.push(project); + } + if let Some(user) = user_config_path() { + paths.push(user); + } + paths +} + +fn find_project_config(start: &std::path::Path) -> Option { + for ancestor in start.ancestors() { + let path = ancestor.join(".nemo-flow/sidecar.toml"); + if path.exists() { + return Some(path); + } + } + None +} + +fn user_config_path() -> Option { + if let Some(base) = std::env::var_os("XDG_CONFIG_HOME") { + return Some(PathBuf::from(base).join("nemo-flow/sidecar.toml")); + } + home_dir().map(|home| home.join(".config/nemo-flow/sidecar.toml")) +} + +fn apply_file_config( + resolved: &mut ResolvedConfig, + value: toml::Value, +) -> Result<(), SidecarError> { + let config: FileConfig = value.try_into().map_err(|error| { + SidecarError::Config(format!("invalid sidecar configuration shape: {error}")) + })?; + if let Some(server) = config.server { + if let Some(value) = server.openai_base_url { + resolved.sidecar.openai_base_url = value; + } + if let Some(value) = server.anthropic_base_url { + resolved.sidecar.anthropic_base_url = value; + } + } + if let Some(session) = config.session { + if let Some(value) = session.atif_dir { + resolved.sidecar.atif_dir = Some(value); + } + if let Some(value) = session.metadata { + resolved.sidecar.metadata = Some(value); + } + if let Some(value) = session.plugin_config { + resolved.sidecar.plugin_config = Some(value); + } + } + if let Some(export) = config.export + && let Some(openinference) = export.openinference + && let Some(value) = openinference.endpoint + { + resolved.sidecar.openinference_endpoint = Some(value); + } + if let Some(agents) = config.agents { + if let Some(value) = agents.claude_code { + resolved.agents.claude_code.command = value.command; + } + if let Some(value) = agents.codex { + resolved.agents.codex.command = value.command; + } + if let Some(value) = agents.cursor { + resolved.agents.cursor.command = value.command; + if let Some(patch_restore_hooks) = value.patch_restore_hooks { + resolved.agents.cursor.patch_restore_hooks = patch_restore_hooks; + } + } + } + Ok(()) +} + +fn apply_env_config(config: &mut SidecarConfig) { + if let Ok(value) = std::env::var("NEMO_FLOW_SIDECAR_BIND") + && let Ok(value) = value.parse() + { + config.bind = value; + } + if let Ok(value) = std::env::var("NEMO_FLOW_OPENAI_BASE_URL") { + config.openai_base_url = value; + } + if let Ok(value) = std::env::var("NEMO_FLOW_ANTHROPIC_BASE_URL") { + config.anthropic_base_url = value; + } + if let Some(value) = std::env::var_os("NEMO_FLOW_ATIF_DIR") { + config.atif_dir = Some(PathBuf::from(value)); + } + if let Ok(value) = std::env::var("NEMO_FLOW_OPENINFERENCE_ENDPOINT") { + config.openinference_endpoint = Some(value); + } +} + +fn merge_toml(left: &mut toml::Value, right: toml::Value) { + match (left, right) { + (toml::Value::Table(left), toml::Value::Table(right)) => { + for (key, value) in right { + match left.get_mut(&key) { + Some(existing) => merge_toml(existing, value), + None => { + left.insert(key, value); + } + } + } + } + (left, right) => *left = right, + } +} + +fn parse_json_option(name: &str, value: &str) -> Result { + serde_json::from_str::(value) + .map_err(|error| SidecarError::Config(format!("invalid {name}: {error}"))) +} + +fn home_dir() -> Option { + std::env::var_os("HOME") + .or_else(|| std::env::var_os("USERPROFILE")) + .map(PathBuf::from) +} + pub(crate) fn header_string(headers: &HeaderMap, name: &str) -> Option { headers .get(name) @@ -191,6 +539,19 @@ impl CodingAgent { Self::Cursor => "cursor", } } + + pub(crate) fn infer(command: &str) -> Option { + let name = std::path::Path::new(command) + .file_name() + .and_then(|value| value.to_str()) + .unwrap_or(command); + match name { + "claude" | "claude-code" => Some(Self::ClaudeCode), + "codex" => Some(Self::Codex), + "cursor" | "cursor-agent" => Some(Self::Cursor), + _ => None, + } + } } impl GatewayMode { @@ -204,89 +565,5 @@ impl GatewayMode { } #[cfg(test)] -mod tests { - use super::*; - use axum::http::HeaderValue; - use serde_json::json; - - fn config() -> SidecarConfig { - SidecarConfig { - bind: "127.0.0.1:0".parse().unwrap(), - openai_base_url: "http://openai".into(), - anthropic_base_url: "http://anthropic".into(), - atif_dir: Some(PathBuf::from("default-atif")), - openinference_endpoint: Some("http://default-otel".into()), - } - } - - #[test] - fn session_config_prefers_headers_and_parses_json() { - let mut headers = HeaderMap::new(); - headers.insert( - "x-nemo-flow-atif-dir", - HeaderValue::from_static("header-atif"), - ); - headers.insert( - "x-nemo-flow-openinference-endpoint", - HeaderValue::from_static("http://header-otel"), - ); - headers.insert( - "x-nemo-flow-config-profile", - HeaderValue::from_static("profile-a"), - ); - headers.insert( - "x-nemo-flow-session-metadata", - HeaderValue::from_static(r#"{"team":"obs"}"#), - ); - headers.insert( - "x-nemo-flow-plugin-config", - HeaderValue::from_static(r#"{"components":[]}"#), - ); - headers.insert( - "x-nemo-flow-gateway-mode", - HeaderValue::from_static("required"), - ); - - let session = config().session_config_from_headers(&headers); - - assert_eq!(session.atif_dir, Some(PathBuf::from("header-atif"))); - assert_eq!( - session.openinference_endpoint.as_deref(), - Some("http://header-otel") - ); - assert_eq!(session.profile.as_deref(), Some("profile-a")); - assert_eq!(session.metadata, Some(json!({ "team": "obs" }))); - assert_eq!(session.plugin_config, Some(json!({ "components": [] }))); - assert_eq!(session.gateway_mode.as_deref(), Some("required")); - } - - #[test] - fn session_config_uses_defaults_and_ignores_bad_json() { - let mut headers = HeaderMap::new(); - headers.insert( - "x-nemo-flow-session-metadata", - HeaderValue::from_static("not-json"), - ); - headers.insert("x-empty", HeaderValue::from_static("")); - - let session = config().session_config_from_headers(&headers); - - assert_eq!(session.atif_dir, Some(PathBuf::from("default-atif"))); - assert_eq!( - session.openinference_endpoint.as_deref(), - Some("http://default-otel") - ); - assert_eq!(session.metadata, None); - assert_eq!(header_string(&headers, "x-empty"), None); - } - - #[test] - fn agent_and_gateway_mode_arguments_are_stable() { - assert_eq!(CodingAgent::ClaudeCode.hook_path(), "/hooks/claude-code"); - assert_eq!(CodingAgent::Codex.hook_path(), "/hooks/codex"); - assert_eq!(CodingAgent::Cursor.hook_path(), "/hooks/cursor"); - assert_eq!(GatewayMode::HookOnly.as_arg(), "hook-only"); - assert_eq!(GatewayMode::Passthrough.as_arg(), "passthrough"); - assert_eq!(GatewayMode::Required.as_arg(), "required"); - } -} +#[path = "../tests/coverage/config_tests.rs"] +mod tests; diff --git a/crates/sidecar/src/error.rs b/crates/sidecar/src/error.rs index 4cdfb53..4d1451f 100644 --- a/crates/sidecar/src/error.rs +++ b/crates/sidecar/src/error.rs @@ -18,6 +18,10 @@ pub(crate) enum SidecarError { Io(#[from] std::io::Error), #[error("installer error: {0}")] Install(String), + #[error("configuration error: {0}")] + Config(String), + #[error("launcher error: {0}")] + Launch(String), #[error("NeMo Flow runtime error: {0}")] Flow(#[from] nemo_flow::error::FlowError), #[error("openinference error: {0}")] @@ -32,6 +36,8 @@ impl IntoResponse for SidecarError { Self::Http(_) | Self::Io(_) | Self::Install(_) + | Self::Config(_) + | Self::Launch(_) | Self::Flow(_) | Self::OpenInference(_) => StatusCode::INTERNAL_SERVER_ERROR, }; diff --git a/crates/sidecar/src/gateway.rs b/crates/sidecar/src/gateway.rs index 24b92b6..bec17ab 100644 --- a/crates/sidecar/src/gateway.rs +++ b/crates/sidecar/src/gateway.rs @@ -10,7 +10,6 @@ use serde_json::{Map, Value, json}; use crate::config::header_string; use crate::error::SidecarError; -use crate::model::AgentKind; use crate::server::AppState; use crate::session::LlmGatewayStart; @@ -70,7 +69,20 @@ pub(crate) async fn passthrough( upstream = upstream.header(name, value); } } - let upstream_response = upstream.send().await?; + let upstream_response = match upstream.send().await { + Ok(response) => response, + Err(error) => { + state + .sessions + .end_llm( + active, + json!({ "error": error.to_string() }), + json!({ "gateway_error": true, "stage": "send" }), + ) + .await?; + return Err(SidecarError::Upstream(error)); + } + }; let status = upstream_response.status(); let headers = response_headers(upstream_response.headers()); let content_type = upstream_response @@ -86,6 +98,7 @@ pub(crate) async fn passthrough( let stream = upstream_response.bytes_stream(); let body = Body::from_stream(async_stream::stream! { let mut stream = stream; + let mut active = Some(active); let mut collected = Vec::new(); let mut truncated = false; while let Some(chunk) = stream.next().await { @@ -99,24 +112,48 @@ pub(crate) async fn passthrough( yield Ok::(bytes); } Err(error) => { + if let Some(active) = active.take() { + let _ = sessions + .end_llm( + active, + json!({ "error": error.to_string() }), + json!({ "http_status": status.as_u16(), "streaming": true, "gateway_error": true, "stage": "stream" }), + ) + .await; + } yield Err(error); return; } } } let response = stream_response_json(&collected, truncated); - let _ = sessions - .end_llm( - active, - response, - json!({ "http_status": status.as_u16(), "streaming": true, "stream_truncated": truncated }), - ) - .await; + if let Some(active) = active.take() { + let _ = sessions + .end_llm( + active, + response, + json!({ "http_status": status.as_u16(), "streaming": true, "stream_truncated": truncated }), + ) + .await; + } }); return build_response(status, headers, body); } - let bytes = upstream_response.bytes().await?; + let bytes = match upstream_response.bytes().await { + Ok(bytes) => bytes, + Err(error) => { + state + .sessions + .end_llm( + active, + json!({ "error": error.to_string() }), + json!({ "http_status": status.as_u16(), "streaming": false, "gateway_error": true, "stage": "body" }), + ) + .await?; + return Err(SidecarError::Upstream(error)); + } + }; let response_json = serde_json::from_slice::(&bytes) .unwrap_or_else(|_| json!({ "body_bytes": bytes.len() })); state @@ -208,13 +245,9 @@ impl ProviderRoute { } } -fn gateway_session_id(headers: &HeaderMap) -> String { +fn gateway_session_id(headers: &HeaderMap) -> Option { header_string(headers, "x-nemo-flow-session-id") .or_else(|| header_string(headers, "x-claude-code-session-id")) - .or_else(|| { - header_string(headers, "anthropic-beta").map(|value| format!("anthropic:{value}")) - }) - .unwrap_or_else(|| format!("{}-gateway", AgentKind::Gateway.as_str())) } fn observable_headers(headers: &HeaderMap) -> Map { @@ -289,125 +322,5 @@ fn stream_response_json(collected: &[u8], truncated: bool) -> Value { } #[cfg(test)] -mod tests { - use super::*; - use crate::config::SidecarConfig; - use axum::http::{HeaderMap, HeaderValue}; - - #[test] - fn removes_hop_by_hop_headers() { - assert!(!should_forward_request_header(&HeaderName::from_static( - "connection" - ))); - assert!(!should_forward_request_header(&HeaderName::from_static( - "host" - ))); - assert!(should_forward_request_header(&HeaderName::from_static( - "authorization" - ))); - assert!(!should_record_header(&HeaderName::from_static( - "authorization" - ))); - assert!(!should_record_header(&HeaderName::from_static("x-api-key"))); - assert!(!should_record_header(&HeaderName::from_static( - "anthropic-api-key" - ))); - assert!(should_record_header(&HeaderName::from_static( - "x-request-id" - ))); - } - - #[test] - fn selects_provider_routes() { - assert_eq!( - ProviderRoute::from_path("/v1/responses"), - Some(ProviderRoute::OpenAiResponses) - ); - assert_eq!( - ProviderRoute::from_path("/v1/messages/count_tokens"), - Some(ProviderRoute::AnthropicCountTokens) - ); - assert_eq!( - ProviderRoute::from_path("/v1/chat/completions") - .unwrap() - .name(), - "openai.chat_completions" - ); - assert_eq!(ProviderRoute::from_path("/unsupported"), None); - } - - #[test] - fn provider_routes_preserve_path_query_and_choose_upstream() { - let config = SidecarConfig { - bind: "127.0.0.1:0".parse().unwrap(), - openai_base_url: "http://openai/".into(), - anthropic_base_url: "http://anthropic/".into(), - atif_dir: None, - openinference_endpoint: None, - }; - - assert_eq!( - ProviderRoute::OpenAiResponses.upstream_url(&config, "/v1/responses?x=1"), - "http://openai/v1/responses?x=1" - ); - assert_eq!( - ProviderRoute::AnthropicMessages.upstream_url(&config, "/v1/messages"), - "http://anthropic/v1/messages" - ); - } - - #[test] - fn gateway_session_id_prefers_headers_and_has_fallbacks() { - let mut headers = HeaderMap::new(); - headers.insert( - "anthropic-beta", - HeaderValue::from_static("prompt-caching-2024-07-31"), - ); - assert_eq!( - gateway_session_id(&headers), - "anthropic:prompt-caching-2024-07-31" - ); - - headers.insert( - "x-claude-code-session-id", - HeaderValue::from_static("claude-session"), - ); - assert_eq!(gateway_session_id(&headers), "claude-session"); - - headers.insert( - "x-nemo-flow-session-id", - HeaderValue::from_static("explicit-session"), - ); - assert_eq!(gateway_session_id(&headers), "explicit-session"); - - assert_eq!(gateway_session_id(&HeaderMap::new()), "gateway-gateway"); - } - - #[test] - fn observable_headers_omit_secrets_and_transport_headers() { - let mut headers = HeaderMap::new(); - headers.insert("authorization", HeaderValue::from_static("Bearer secret")); - headers.insert("x-api-key", HeaderValue::from_static("secret")); - headers.insert("connection", HeaderValue::from_static("close")); - headers.insert("x-request-id", HeaderValue::from_static("req-1")); - - let observed = observable_headers(&headers); - - assert_eq!(observed.get("x-request-id"), Some(&json!("req-1"))); - assert!(!observed.contains_key("authorization")); - assert!(!observed.contains_key("x-api-key")); - assert!(!observed.contains_key("connection")); - } - - #[test] - fn stream_response_records_preview_and_truncation() { - assert_eq!( - stream_response_json(b"data: done", false), - json!({ "stream": "data: done" }) - ); - assert_eq!( - stream_response_json(b"partial", true), - json!({ "stream_preview": "partial", "stream_truncated": true }) - ); - } -} +#[path = "../tests/coverage/gateway_tests.rs"] +mod tests; diff --git a/crates/sidecar/src/installer.rs b/crates/sidecar/src/installer.rs index f21caab..1dd6f29 100644 --- a/crates/sidecar/src/installer.rs +++ b/crates/sidecar/src/installer.rs @@ -90,9 +90,24 @@ pub(crate) async fn hook_forward(command: HookForwardCommand) -> Result<(), Side input = "{}".to_string(); } + let Some(sidecar_url) = command + .sidecar_url + .clone() + .or_else(|| std::env::var("NEMO_FLOW_SIDECAR_URL").ok()) + else { + eprintln!( + "nemo-flow-sidecar hook forward failed: missing sidecar URL; pass --sidecar-url or set NEMO_FLOW_SIDECAR_URL" + ); + if command.fail_closed { + return Err(SidecarError::Install( + "missing sidecar URL; pass --sidecar-url or set NEMO_FLOW_SIDECAR_URL".into(), + )); + } + return Ok(()); + }; let url = format!( "{}{}", - command.sidecar_url.trim_end_matches('/'), + sidecar_url.trim_end_matches('/'), command.agent.hook_path() ); let response = reqwest::Client::new() @@ -121,6 +136,7 @@ pub(crate) async fn hook_forward(command: HookForwardCommand) -> Result<(), Side "hook forward failed with HTTP {status}" ))); } + return Ok(()); } if !body.is_empty() { println!("{body}"); @@ -265,6 +281,18 @@ fn shell_quote(value: &str) -> String { } } +pub(crate) fn generated_hooks(agent: CodingAgent, command: &str) -> Value { + match agent { + CodingAgent::ClaudeCode => claude_hooks(command), + CodingAgent::Codex => codex_hooks(command), + CodingAgent::Cursor => cursor_hooks(command), + } +} + +pub(crate) fn hook_forward_command(agent: CodingAgent) -> String { + format!("nemo-flow-sidecar hook-forward {}", agent.as_arg()) +} + fn claude_hooks(command: &str) -> Value { hooks_for_events(HOOK_EVENTS, command, true) } @@ -318,7 +346,7 @@ fn event_matches_tools(event: &str) -> bool { ) } -fn merge_hooks(existing: Value, generated: Value) -> Result { +pub(crate) fn merge_hooks(existing: Value, generated: Value) -> Result { let mut root = match existing { Value::Null => json!({}), Value::Object(object) => Value::Object(object), @@ -370,7 +398,7 @@ fn merge_codex_config(existing: &str) -> Result { Ok(document.to_string()) } -fn read_json_file(path: &Path) -> Result { +pub(crate) fn read_json_file(path: &Path) -> Result { match std::fs::read_to_string(path) { Ok(raw) => serde_json::from_str(&raw).map_err(|error| { SidecarError::Install(format!("invalid JSON in {}: {error}", path.display())) @@ -499,237 +527,5 @@ fn print_target_note(agent: CodingAgent, target: InstallTarget) { } #[cfg(test)] -mod tests { - use super::*; - - fn command(agent: CodingAgent, root: &Path) -> InstallCommand { - InstallCommand { - agent, - scope: InstallScope::User, - target: InstallTarget::Both, - sidecar_url: "http://127.0.0.1:4040".into(), - atif_dir: Some(root.join("atif")), - openinference_endpoint: Some("http://otel:4318/v1/traces".into()), - profile: Some("default".into()), - session_metadata: Some(r#"{"team":"agent-observability"}"#.into()), - plugin_config: Some(r#"{"components":[]}"#.into()), - gateway_mode: Some(GatewayMode::Required), - dry_run: false, - print: false, - home_dir: Some(root.to_path_buf()), - project_dir: None, - } - } - - fn project_command(agent: CodingAgent, root: &Path) -> InstallCommand { - InstallCommand { - scope: InstallScope::Project, - project_dir: Some(root.to_path_buf()), - ..command(agent, root) - } - } - - #[test] - fn generates_claude_install_file() { - let temp = tempfile::tempdir().unwrap(); - let files = planned_files(&command(CodingAgent::ClaudeCode, temp.path())).unwrap(); - assert_eq!(files.len(), 1); - assert!(files[0].path.ends_with(".claude/settings.json")); - let json: Value = serde_json::from_str(&files[0].contents).unwrap(); - assert!(json["hooks"]["SessionStart"].is_array()); - assert!( - json["hooks"]["PreToolUse"][0]["hooks"][0]["command"] - .as_str() - .unwrap() - .contains("hook-forward claude-code") - ); - } - - #[test] - fn generates_codex_config_and_hooks() { - let temp = tempfile::tempdir().unwrap(); - let files = planned_files(&command(CodingAgent::Codex, temp.path())).unwrap(); - assert_eq!(files.len(), 2); - assert!(files[0].contents.contains("codex_hooks = true")); - let json: Value = serde_json::from_str(&files[1].contents).unwrap(); - assert!(json["hooks"]["Stop"].is_array()); - assert!( - json["hooks"]["PreToolUse"][0]["hooks"][0]["command"] - .as_str() - .unwrap() - .contains("hook-forward codex") - ); - } - - #[test] - fn generates_cursor_hooks() { - let temp = tempfile::tempdir().unwrap(); - let files = planned_files(&command(CodingAgent::Cursor, temp.path())).unwrap(); - assert_eq!(files.len(), 1); - let json: Value = serde_json::from_str(&files[0].contents).unwrap(); - assert!(json["hooks"]["beforeShellExecution"].is_array()); - assert!( - json["hooks"]["beforeShellExecution"][0]["hooks"][0]["command"] - .as_str() - .unwrap() - .contains("hook-forward cursor") - ); - } - - #[test] - fn merge_hooks_is_idempotent_and_preserves_existing_entries() { - let existing = json!({ - "hooks": { - "Stop": [{ "hooks": [{ "type": "command", "command": "existing" }] }] - } - }); - let generated = codex_hooks("nemo-flow-sidecar hook-forward codex"); - let once = merge_hooks(existing, generated.clone()).unwrap(); - let twice = merge_hooks(once.clone(), generated).unwrap(); - assert_eq!(once, twice); - assert_eq!(twice["hooks"]["Stop"].as_array().unwrap().len(), 2); - } - - #[test] - fn project_install_uses_project_dir_and_preserves_codex_toml() { - let temp = tempfile::tempdir().unwrap(); - let codex_dir = temp.path().join(".codex"); - std::fs::create_dir_all(&codex_dir).unwrap(); - std::fs::write( - codex_dir.join("config.toml"), - "[features]\nother = true\n[model_providers.openai]\nbase_url = \"http://old\"\n", - ) - .unwrap(); - - let files = planned_files(&project_command(CodingAgent::Codex, temp.path())).unwrap(); - - assert!(files[0].path.starts_with(temp.path())); - assert!(files[0].contents.contains("other = true")); - assert!(files[0].contents.contains("codex_hooks = true")); - assert!(files[0].contents.contains("[model_providers.openai]")); - } - - #[test] - fn install_writes_file_and_backs_up_existing_config() { - let temp = tempfile::tempdir().unwrap(); - let claude_dir = temp.path().join(".claude"); - std::fs::create_dir_all(&claude_dir).unwrap(); - let settings = claude_dir.join("settings.json"); - std::fs::write(&settings, r#"{"hooks":{"Stop":[]}}"#).unwrap(); - - install(command(CodingAgent::ClaudeCode, temp.path())).unwrap(); - - let installed = std::fs::read_to_string(&settings).unwrap(); - assert!(installed.contains("hook-forward claude-code")); - let backups: Vec<_> = std::fs::read_dir(&claude_dir) - .unwrap() - .map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned()) - .filter(|name| name.starts_with("settings.json.bak.")) - .collect(); - assert_eq!(backups.len(), 1); - } - - #[test] - fn install_dry_run_does_not_write_files() { - let temp = tempfile::tempdir().unwrap(); - let mut command = command(CodingAgent::Cursor, temp.path()); - command.dry_run = true; - command.print = true; - - install(command).unwrap(); - - assert!(!temp.path().join(".cursor/hooks.json").exists()); - } - - #[test] - fn invalid_json_config_is_rejected_before_planning() { - let temp = tempfile::tempdir().unwrap(); - let mut command = command(CodingAgent::Codex, temp.path()); - command.session_metadata = Some("not-json".into()); - - let error = install(command).unwrap_err().to_string(); - - assert!(error.contains("invalid session metadata")); - } - - #[test] - fn merge_hooks_rejects_malformed_shapes() { - assert!(merge_hooks(json!([]), codex_hooks("cmd")).is_err()); - assert!(merge_hooks(json!({ "hooks": [] }), codex_hooks("cmd")).is_err()); - assert!(merge_hooks(json!({ "hooks": { "Stop": {} } }), codex_hooks("cmd")).is_err()); - assert!(merge_hooks(json!({}), json!({ "hooks": [] })).is_err()); - } - - #[test] - fn invalid_existing_files_are_reported() { - let temp = tempfile::tempdir().unwrap(); - let cursor_dir = temp.path().join(".cursor"); - std::fs::create_dir_all(&cursor_dir).unwrap(); - std::fs::write(cursor_dir.join("hooks.json"), "not-json").unwrap(); - - let error = planned_files(&command(CodingAgent::Cursor, temp.path())) - .unwrap_err() - .to_string(); - - assert!(error.contains("invalid JSON")); - - let codex_dir = temp.path().join(".codex"); - std::fs::create_dir_all(&codex_dir).unwrap(); - std::fs::write(codex_dir.join("config.toml"), "not = [valid").unwrap(); - let error = planned_files(&command(CodingAgent::Codex, temp.path())) - .unwrap_err() - .to_string(); - assert!(error.contains("invalid TOML")); - } - - #[test] - fn helper_formatting_and_headers_cover_optional_paths() { - assert_eq!(shell_quote("plain/arg-1"), "plain/arg-1"); - assert_eq!(shell_quote("needs space"), "'needs space'"); - assert_eq!(shell_quote("can't"), "'can'\\''t'"); - assert!(event_matches_tools("PermissionRequest")); - assert!(!event_matches_tools("SessionStart")); - - let temp = tempfile::tempdir().unwrap(); - let headers = sidecar_headers( - Some(temp.path()), - Some("http://otel"), - Some("profile"), - Some(r#"{"team":"obs"}"#), - Some(r#"{"plugins":[]}"#), - Some(GatewayMode::Passthrough), - ) - .unwrap(); - assert_eq!( - headers - .get("x-nemo-flow-gateway-mode") - .and_then(|value| value.to_str().ok()), - Some("passthrough") - ); - assert!( - insert_header( - &mut HeaderMap::new(), - "x-nemo-flow-config-profile", - Some("bad\nvalue") - ) - .is_err() - ); - } - - #[test] - fn packaged_hook_configs_are_valid_json() { - let root = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../integrations/coding-agents"); - for path in [ - root.join("claude-code/hooks/hooks.json"), - root.join("codex/hooks/hooks.json"), - root.join("cursor/.cursor/hooks.json"), - root.join("claude-code/.claude-plugin/plugin.json"), - root.join("codex/.codex-plugin/plugin.json"), - ] { - let raw = std::fs::read_to_string(&path).unwrap(); - serde_json::from_str::(&raw) - .unwrap_or_else(|error| panic!("{} is invalid JSON: {error}", path.display())); - } - } -} +#[path = "../tests/coverage/installer_tests.rs"] +mod tests; diff --git a/crates/sidecar/src/launcher.rs b/crates/sidecar/src/launcher.rs new file mode 100644 index 0000000..01ce0f4 --- /dev/null +++ b/crates/sidecar/src/launcher.rs @@ -0,0 +1,414 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::path::{Path, PathBuf}; +use std::process::ExitCode; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use reqwest::Client; +use serde_json::{Value, json}; +use tokio::net::TcpListener; +use tokio::process::Command; +use tokio::sync::oneshot; + +use crate::config::{ + AgentConfigs, CodingAgent, ResolvedConfig, RunCommand, ServerArgs, resolve_run_config, +}; +use crate::error::SidecarError; +use crate::installer::{generated_hooks, hook_forward_command, merge_hooks, read_json_file}; +use crate::server; + +pub(crate) async fn run( + command: RunCommand, + inherited: Option<&ServerArgs>, +) -> Result { + let mut resolved = resolve_run_config(&command, inherited)?; + let (agent, argv) = resolve_agent_and_argv(&command, &resolved.agents)?; + let listener = TcpListener::bind("127.0.0.1:0").await?; + let address = listener.local_addr()?; + let sidecar_url = format!("http://{address}"); + resolved.sidecar.bind = address; + + let prepared = PreparedRun::new(agent, argv, &sidecar_url, &resolved, command.dry_run)?; + if command.print || command.dry_run { + prepared.print(agent, &sidecar_url, &resolved); + } + if command.dry_run { + return Ok(ExitCode::SUCCESS); + } + + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let server_config = resolved.sidecar.clone(); + let server_task = tokio::spawn(async move { + server::serve_listener(listener, server_config, Some(shutdown_rx)).await + }); + if let Err(error) = wait_for_health(&sidecar_url).await { + let _ = shutdown_tx.send(()); + let _ = server_task.await; + return Err(error); + } + + let status = prepared.spawn_and_wait().await; + let restore = prepared.restore(); + let _ = shutdown_tx.send(()); + let server_result = server_task + .await + .map_err(|error| SidecarError::Launch(format!("sidecar task failed: {error}")))?; + restore?; + server_result?; + + let status = status?; + Ok(status + .code() + .and_then(|code| u8::try_from(code).ok()) + .map(ExitCode::from) + .unwrap_or(ExitCode::FAILURE)) +} + +fn resolve_agent_and_argv( + command: &RunCommand, + agents: &AgentConfigs, +) -> Result<(CodingAgent, Vec), SidecarError> { + let argv = if command.command.is_empty() { + let agent = command.agent.ok_or_else(|| { + SidecarError::Launch( + "missing command; pass -- or --agent with a configured command" + .into(), + ) + })?; + configured_command(agent, agents).ok_or_else(|| { + SidecarError::Launch(format!( + "no configured command for {}; pass -- ", + agent.as_arg() + )) + })? + } else { + command.command.clone() + }; + + let agent = match command.agent { + Some(agent) => agent, + None => CodingAgent::infer(&argv[0]).ok_or_else(|| { + SidecarError::Launch(format!( + "could not infer coding agent from command {:?}; pass --agent claude-code, --agent codex, or --agent cursor", + argv[0] + )) + })?, + }; + Ok((agent, argv)) +} + +fn configured_command(agent: CodingAgent, agents: &AgentConfigs) -> Option> { + let command = match agent { + CodingAgent::ClaudeCode => agents.claude_code.command.as_ref(), + CodingAgent::Codex => agents.codex.command.as_ref(), + CodingAgent::Cursor => agents.cursor.command.as_ref(), + }?; + let argv: Vec<_> = command.split_whitespace().map(ToOwned::to_owned).collect(); + (!argv.is_empty()).then_some(argv) +} + +struct PreparedRun { + argv: Vec, + env: Vec<(String, String)>, + temp_dirs: Vec, + cursor_restore: Option, + notes: Vec, +} + +struct CursorRestore { + path: PathBuf, + backup_path: Option, + had_original: bool, +} + +impl PreparedRun { + fn new( + agent: CodingAgent, + argv: Vec, + sidecar_url: &str, + resolved: &ResolvedConfig, + dry_run: bool, + ) -> Result { + let mut run = Self { + argv, + env: vec![("NEMO_FLOW_SIDECAR_URL".into(), sidecar_url.into())], + temp_dirs: Vec::new(), + cursor_restore: None, + notes: Vec::new(), + }; + match agent { + CodingAgent::ClaudeCode => { + if dry_run { + run.prepare_claude_dry(sidecar_url); + } else { + run.prepare_claude(sidecar_url)?; + } + } + CodingAgent::Codex => run.prepare_codex(sidecar_url), + CodingAgent::Cursor => { + if resolved.agents.cursor.patch_restore_hooks { + if dry_run { + run.prepare_cursor_dry()?; + } else { + run.prepare_cursor()?; + } + } + } + } + Ok(run) + } + + fn prepare_claude_dry(&mut self, sidecar_url: &str) { + insert_after_agent( + &mut self.argv, + CodingAgent::ClaudeCode, + [ + "--plugin-dir".into(), + "".into(), + ], + ); + self.env + .push(("ANTHROPIC_BASE_URL".into(), sidecar_url.to_string())); + self.notes + .push("would generate a temporary Claude Code plugin directory".into()); + } + + fn prepare_claude(&mut self, sidecar_url: &str) -> Result<(), SidecarError> { + let root = temp_dir("nemo-flow-claude-plugin")?; + std::fs::create_dir_all(root.join(".claude-plugin"))?; + std::fs::create_dir_all(root.join("hooks"))?; + std::fs::write( + root.join(".claude-plugin/plugin.json"), + serde_json::to_vec_pretty(&json!({ + "name": "nemo-flow-sidecar", + "version": env!("CARGO_PKG_VERSION"), + "description": "Temporary NeMo Flow sidecar hooks" + })) + .map_err(|error| SidecarError::Launch(error.to_string()))?, + )?; + write_hooks( + &root.join("hooks/hooks.json"), + generated_hooks( + CodingAgent::ClaudeCode, + &hook_forward_command(CodingAgent::ClaudeCode), + ), + )?; + insert_after_agent( + &mut self.argv, + CodingAgent::ClaudeCode, + ["--plugin-dir".into(), root.display().to_string()], + ); + self.env + .push(("ANTHROPIC_BASE_URL".into(), sidecar_url.to_string())); + self.temp_dirs.push(root); + Ok(()) + } + + fn prepare_codex(&mut self, sidecar_url: &str) { + let hook_command = hook_forward_command(CodingAgent::Codex); + let mut args = vec![ + "--config".to_string(), + "features.codex_hooks=true".to_string(), + "--config".to_string(), + format!( + "model_providers.openai.base_url={}", + toml_string(sidecar_url) + ), + ]; + for (event, groups) in generated_hooks(CodingAgent::Codex, &hook_command)["hooks"] + .as_object() + .into_iter() + .flatten() + { + args.push("--config".to_string()); + args.push(format!("hooks.{event}={}", hook_groups_toml(groups))); + } + insert_after_agent(&mut self.argv, CodingAgent::Codex, args); + } + + fn prepare_cursor(&mut self) -> Result<(), SidecarError> { + let path = std::env::current_dir()?.join(".cursor/hooks.json"); + let had_original = path.exists(); + let backup_path = if had_original { + let backup = path.with_extension(format!("json.nemo-flow-run.bak.{}", timestamp()?)); + std::fs::copy(&path, &backup)?; + Some(backup) + } else { + None + }; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let contents = serde_json::to_string_pretty(&merge_hooks( + read_json_file(&path)?, + generated_hooks( + CodingAgent::Cursor, + &hook_forward_command(CodingAgent::Cursor), + ), + )?) + .map_err(|error| SidecarError::Launch(error.to_string()))?; + std::fs::write(&path, contents)?; + self.cursor_restore = Some(CursorRestore { + path, + backup_path, + had_original, + }); + Ok(()) + } + + fn prepare_cursor_dry(&mut self) -> Result<(), SidecarError> { + let path = std::env::current_dir()?.join(".cursor/hooks.json"); + self.notes.push(format!( + "would temporarily merge NeMo Flow hooks into {}", + path.display() + )); + Ok(()) + } + + async fn spawn_and_wait(&self) -> Result { + let mut command = Command::new(&self.argv[0]); + command.args(&self.argv[1..]); + for (name, value) in &self.env { + command.env(name, value); + } + let mut child = command.spawn()?; + child.wait().await.map_err(SidecarError::from) + } + + fn restore(&self) -> Result<(), SidecarError> { + for dir in &self.temp_dirs { + let _ = std::fs::remove_dir_all(dir); + } + let Some(cursor) = &self.cursor_restore else { + return Ok(()); + }; + match (&cursor.backup_path, cursor.had_original) { + (Some(backup), true) => { + std::fs::copy(backup, &cursor.path).map_err(|error| { + SidecarError::Launch(format!( + "failed to restore Cursor hooks from {}: {error}", + backup.display() + )) + })?; + let _ = std::fs::remove_file(backup); + } + (_, false) => { + if cursor.path.exists() { + std::fs::remove_file(&cursor.path).map_err(|error| { + SidecarError::Launch(format!( + "failed to remove temporary Cursor hooks {}: {error}", + cursor.path.display() + )) + })?; + } + } + _ => {} + } + Ok(()) + } + + fn print(&self, agent: CodingAgent, sidecar_url: &str, resolved: &ResolvedConfig) { + println!("agent = {}", agent.as_arg()); + println!("sidecar_url = {sidecar_url}"); + println!("openai_base_url = {}", resolved.sidecar.openai_base_url); + println!( + "anthropic_base_url = {}", + resolved.sidecar.anthropic_base_url + ); + if let Some(path) = &resolved.sidecar.atif_dir { + println!("atif_dir = {}", path.display()); + } + if let Some(endpoint) = &resolved.sidecar.openinference_endpoint { + println!("openinference_endpoint = {endpoint}"); + } + println!("argv = {}", self.argv.join(" ")); + for (name, value) in &self.env { + println!("env.{name} = {value}"); + } + if let Some(cursor) = &self.cursor_restore { + println!("cursor_hooks = {}", cursor.path.display()); + } + for note in &self.notes { + println!("note = {note}"); + } + } +} + +async fn wait_for_health(sidecar_url: &str) -> Result<(), SidecarError> { + let client = Client::new(); + let url = format!("{}/healthz", sidecar_url.trim_end_matches('/')); + for _ in 0..50 { + if let Ok(response) = client.get(&url).send().await + && response.status().is_success() + { + return Ok(()); + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + Err(SidecarError::Launch(format!( + "sidecar did not become ready at {url}" + ))) +} + +fn insert_after_agent( + argv: &mut Vec, + agent: CodingAgent, + args: impl IntoIterator, +) { + let index = argv + .iter() + .enumerate() + .filter_map(|(index, arg)| (CodingAgent::infer(arg) == Some(agent)).then_some(index)) + .next_back() + .unwrap_or(0); + argv.splice(index + 1..index + 1, args); +} + +fn write_hooks(path: &Path, hooks: Value) -> Result<(), SidecarError> { + std::fs::write( + path, + serde_json::to_vec_pretty(&hooks) + .map_err(|error| SidecarError::Launch(error.to_string()))?, + )?; + Ok(()) +} + +fn hook_groups_toml(value: &Value) -> String { + let mut groups = Vec::new(); + for group in value.as_array().into_iter().flatten() { + let matcher = group + .get("matcher") + .and_then(Value::as_str) + .map(|matcher| format!("matcher={},", toml_string(matcher))) + .unwrap_or_default(); + let command = group["hooks"][0]["command"].as_str().unwrap_or_default(); + groups.push(format!( + "{{{matcher}hooks=[{{type=\"command\",command={},timeout=30}}]}}", + toml_string(command) + )); + } + format!("[{}]", groups.join(",")) +} + +fn toml_string(value: &str) -> String { + let escaped = value.replace('\\', "\\\\").replace('"', "\\\""); + format!("\"{escaped}\"") +} + +fn temp_dir(prefix: &str) -> Result { + let path = std::env::temp_dir().join(format!("{prefix}-{}", timestamp()?)); + std::fs::create_dir_all(&path)?; + Ok(path) +} + +fn timestamp() -> Result { + Ok(SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| SidecarError::Launch(error.to_string()))? + .as_nanos()) +} + +#[cfg(test)] +#[path = "../tests/coverage/launcher_tests.rs"] +mod tests; diff --git a/crates/sidecar/src/main.rs b/crates/sidecar/src/main.rs index ec5512f..a16b8ea 100644 --- a/crates/sidecar/src/main.rs +++ b/crates/sidecar/src/main.rs @@ -8,20 +8,44 @@ mod config; mod error; mod gateway; mod installer; +mod launcher; mod model; mod server; mod session; +use std::process::ExitCode; + use clap::Parser; use crate::config::{Cli, Command}; #[tokio::main] -async fn main() -> Result<(), error::SidecarError> { +async fn main() -> ExitCode { + match run().await { + Ok(code) => code, + Err(error) => { + eprintln!("{error}"); + ExitCode::FAILURE + } + } +} + +async fn run() -> Result { let cli = Cli::parse(); match cli.command { - Some(Command::Install(command)) => installer::install(command), - Some(Command::HookForward(command)) => installer::hook_forward(command).await, - None => server::serve(cli.server).await, + Some(Command::Install(command)) => { + installer::install(command)?; + Ok(ExitCode::SUCCESS) + } + Some(Command::HookForward(command)) => { + installer::hook_forward(command).await?; + Ok(ExitCode::SUCCESS) + } + Some(Command::Run(command)) => launcher::run(command, Some(&cli.server)).await, + None => { + let config = config::resolve_server_config(&cli.server)?; + server::serve(config.sidecar).await?; + Ok(ExitCode::SUCCESS) + } } } diff --git a/crates/sidecar/src/server.rs b/crates/sidecar/src/server.rs index 36fe36f..89b9197 100644 --- a/crates/sidecar/src/server.rs +++ b/crates/sidecar/src/server.rs @@ -8,6 +8,7 @@ use axum::{Json, Router}; use reqwest::Client; use serde_json::Value; use tokio::net::TcpListener; +use tokio::sync::oneshot; use crate::adapters::{claude_code, codex, cursor}; use crate::config::SidecarConfig; @@ -24,8 +25,27 @@ pub(crate) struct AppState { pub(crate) async fn serve(config: SidecarConfig) -> Result<(), SidecarError> { let listener = TcpListener::bind(config.bind).await?; + serve_listener(listener, config, None).await +} + +pub(crate) async fn serve_listener( + listener: TcpListener, + config: SidecarConfig, + shutdown: Option>, +) -> Result<(), SidecarError> { let app = router(config); - axum::serve(listener, app).await?; + match shutdown { + Some(receiver) => { + axum::serve(listener, app) + .with_graceful_shutdown(async { + let _ = receiver.await; + }) + .await?; + } + None => { + axum::serve(listener, app).await?; + } + } Ok(()) } @@ -37,6 +57,7 @@ pub(crate) fn router(config: SidecarConfig) -> Router { sessions, }; Router::new() + .route("/healthz", get(healthz)) .route("/hooks/codex", post(codex_hook)) .route("/hooks/claude-code", post(claude_code_hook)) .route("/hooks/cursor", post(cursor_hook)) @@ -48,6 +69,10 @@ pub(crate) fn router(config: SidecarConfig) -> Router { .with_state(state) } +async fn healthz() -> Json { + Json(serde_json::json!({ "status": "ok" })) +} + async fn codex_hook( State(state): State, headers: HeaderMap, @@ -88,274 +113,5 @@ async fn cursor_hook( } #[cfg(test)] -mod tests { - use axum::body::Body; - use axum::http::{Request, StatusCode, header}; - use axum::response::IntoResponse; - use bytes::Bytes; - use futures_util::stream; - use http_body_util::BodyExt; - use serde_json::{Value, json}; - use tokio::net::TcpListener; - use tower::ServiceExt; - - use super::*; - - fn test_config() -> SidecarConfig { - SidecarConfig { - bind: "127.0.0.1:0".parse().unwrap(), - openai_base_url: "http://127.0.0.1".into(), - anthropic_base_url: "http://127.0.0.1".into(), - atif_dir: None, - openinference_endpoint: None, - } - } - - #[tokio::test] - async fn codex_hook_keeps_codex_response_shape() { - let app = router(test_config()); - let response = app - .oneshot( - Request::builder() - .method("POST") - .uri("/hooks/codex") - .header("content-type", "application/json") - .body(Body::from( - json!({ - "session_id": "codex-1", - "hook_event_name": "sessionStart" - }) - .to_string(), - )) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let bytes = response.into_body().collect().await.unwrap().to_bytes(); - let body: Value = serde_json::from_slice(&bytes).unwrap(); - assert_eq!(body, json!({})); - } - - #[tokio::test] - async fn claude_code_hook_returns_continue_shape() { - let app = router(test_config()); - let response = app - .oneshot( - Request::builder() - .method("POST") - .uri("/hooks/claude-code") - .header("content-type", "application/json") - .body(Body::from( - json!({ - "session_id": "claude-1", - "hook_event_name": "SessionStart" - }) - .to_string(), - )) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let bytes = response.into_body().collect().await.unwrap().to_bytes(); - let body: Value = serde_json::from_slice(&bytes).unwrap(); - assert_eq!(body["continue"], json!(true)); - } - - #[tokio::test] - async fn cursor_hook_returns_cursor_permission_fields() { - let app = router(test_config()); - let response = app - .oneshot( - Request::builder() - .method("POST") - .uri("/hooks/cursor") - .header("content-type", "application/json") - .body(Body::from( - json!({ - "session_id": "cursor-1", - "hook_event_name": "beforeShellExecution", - "tool_call_id": "shell-1", - "tool_name": "shell", - "input": { "command": "pwd" } - }) - .to_string(), - )) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let bytes = response.into_body().collect().await.unwrap().to_bytes(); - let body: Value = serde_json::from_slice(&bytes).unwrap(); - assert_eq!(body["continue"], json!(true)); - assert_eq!(body["permission"], json!("allow")); - } - - #[tokio::test] - async fn gateway_forwards_openai_json_without_rewriting_payload() { - let upstream = spawn_upstream(false).await; - let mut config = test_config(); - config.openai_base_url = upstream; - let app = router(config); - let response = app - .oneshot( - Request::builder() - .method("POST") - .uri("/v1/chat/completions") - .header("content-type", "application/json") - .header("authorization", "Bearer test") - .header("connection", "close") - .body(Body::from( - json!({ - "model": "gpt-test", - "messages": [{ "role": "user", "content": "hello" }] - }) - .to_string(), - )) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let bytes = response.into_body().collect().await.unwrap().to_bytes(); - let body: Value = serde_json::from_slice(&bytes).unwrap(); - assert_eq!(body["model"], json!("gpt-test")); - assert_eq!(body["authorization"], json!("Bearer test")); - assert_eq!(body["connection"], Value::Null); - } - - #[tokio::test] - async fn gateway_preserves_streaming_body() { - let upstream = spawn_upstream(true).await; - let mut config = test_config(); - config.openai_base_url = upstream; - let app = router(config); - let response = app - .oneshot( - Request::builder() - .method("POST") - .uri("/v1/responses") - .header("content-type", "application/json") - .body(Body::from( - json!({ - "model": "gpt-test", - "input": "hello", - "stream": true - }) - .to_string(), - )) - .unwrap(), - ) - .await - .unwrap(); - assert_eq!(response.status(), StatusCode::OK); - assert_eq!( - response.headers().get(header::CONTENT_TYPE).unwrap(), - "text/event-stream" - ); - let bytes = response.into_body().collect().await.unwrap().to_bytes(); - assert_eq!(bytes, Bytes::from_static(b"data: one\n\ndata: two\n\n")); - } - - #[tokio::test] - async fn gateway_rejects_unsupported_paths() { - let app = router(test_config()); - let response = app - .oneshot( - Request::builder() - .method("POST") - .uri("/v1/unsupported") - .header("content-type", "application/json") - .body(Body::from("{}")) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[tokio::test] - async fn models_route_forwards_get_requests() { - let upstream = spawn_models_upstream().await; - let mut config = test_config(); - config.openai_base_url = upstream; - let app = router(config); - let response = app - .oneshot( - Request::builder() - .method("GET") - .uri("/v1/models?limit=1") - .header("authorization", "Bearer test") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - let bytes = response.into_body().collect().await.unwrap().to_bytes(); - let body: Value = serde_json::from_slice(&bytes).unwrap(); - assert_eq!(body["path"], json!("/v1/models?limit=1")); - assert_eq!(body["authorization"], json!("Bearer test")); - } - - async fn spawn_upstream(streaming: bool) -> String { - async fn chat(headers: HeaderMap, body: Bytes) -> impl IntoResponse { - let payload: Value = serde_json::from_slice(&body).unwrap(); - Json(json!({ - "model": payload["model"], - "authorization": headers - .get(header::AUTHORIZATION) - .and_then(|value| value.to_str().ok()), - "connection": headers - .get(header::CONNECTION) - .and_then(|value| value.to_str().ok()) - })) - } - - async fn stream_response() -> impl IntoResponse { - let chunks = stream::iter([ - Ok::<_, std::convert::Infallible>(Bytes::from_static(b"data: one\n\n")), - Ok(Bytes::from_static(b"data: two\n\n")), - ]); - ( - [(header::CONTENT_TYPE, "text/event-stream")], - Body::from_stream(chunks), - ) - } - - let app = if streaming { - Router::new().route("/v1/responses", post(stream_response)) - } else { - Router::new().route("/v1/chat/completions", post(chat)) - }; - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let address = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - format!("http://{address}") - } - - async fn spawn_models_upstream() -> String { - async fn models(headers: HeaderMap, request: Request) -> impl IntoResponse { - Json(json!({ - "path": request.uri().path_and_query().map(|value| value.as_str()), - "authorization": headers - .get(header::AUTHORIZATION) - .and_then(|value| value.to_str().ok()) - })) - } - - let app = Router::new().route("/v1/models", get(models)); - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let address = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - format!("http://{address}") - } -} +#[path = "../tests/coverage/server_tests.rs"] +mod tests; diff --git a/crates/sidecar/src/session.rs b/crates/sidecar/src/session.rs index 96237fc..6293a3b 100644 --- a/crates/sidecar/src/session.rs +++ b/crates/sidecar/src/session.rs @@ -35,7 +35,7 @@ pub(crate) struct SessionManager { #[derive(Debug, Clone)] pub(crate) struct LlmGatewayStart { - pub(crate) session_id: String, + pub(crate) session_id: Option, pub(crate) provider: String, pub(crate) model_name: Option, pub(crate) request: LlmRequest, @@ -99,9 +99,14 @@ impl SessionManager { ) -> Result { let mut sessions = self.inner.lock().await; let config = self.default_config.session_config_from_headers(headers); + let session_id = start + .session_id + .clone() + .or_else(|| single_active_session_id(&sessions)) + .unwrap_or_else(|| format!("{}-gateway", AgentKind::Gateway.as_str())); let session = sessions - .entry(start.session_id.clone()) - .or_insert_with(|| Session::new(start.session_id.clone(), AgentKind::Gateway, config)); + .entry(session_id.clone()) + .or_insert_with(|| Session::new(session_id, AgentKind::Gateway, config)); session.start_llm(start).await } @@ -446,6 +451,12 @@ fn event_agent_kind(event: &NormalizedEvent) -> AgentKind { } } +fn single_active_session_id(sessions: &HashMap) -> Option { + (sessions.len() == 1) + .then(|| sessions.keys().next().cloned()) + .flatten() +} + fn merge_metadata(left: Value, right: Value) -> Value { match (left, right) { (Value::Object(mut left), Value::Object(right)) => { @@ -468,252 +479,5 @@ fn merge_metadata(left: Value, right: Value) -> Value { } #[cfg(test)] -mod tests { - use axum::http::HeaderMap; - use serde_json::json; - - use super::*; - use crate::model::{SessionEvent, ToolEvent}; - - #[tokio::test] - async fn nests_agent_subagent_and_tool_lifecycle() { - let config = SidecarConfig { - bind: "127.0.0.1:0".parse().unwrap(), - openai_base_url: "http://127.0.0.1".into(), - anthropic_base_url: "http://127.0.0.1".into(), - atif_dir: None, - openinference_endpoint: None, - }; - let manager = SessionManager::new(config); - let headers = HeaderMap::new(); - let events = vec![ - NormalizedEvent::AgentStarted(SessionEvent { - session_id: "s1".into(), - agent_kind: AgentKind::ClaudeCode, - event_name: "SessionStart".into(), - payload: json!({}), - metadata: json!({}), - }), - NormalizedEvent::SubagentStarted(SubagentEvent { - session_id: "s1".into(), - agent_kind: AgentKind::ClaudeCode, - event_name: "SubagentStart".into(), - subagent_id: "worker-1".into(), - payload: json!({}), - metadata: json!({}), - }), - NormalizedEvent::ToolStarted(ToolEvent { - session_id: "s1".into(), - agent_kind: AgentKind::ClaudeCode, - event_name: "PreToolUse".into(), - tool_call_id: "t1".into(), - tool_name: "Read".into(), - subagent_id: Some("worker-1".into()), - arguments: json!({ "file_path": "README.md" }), - result: Value::Null, - status: None, - payload: json!({}), - metadata: json!({}), - }), - NormalizedEvent::ToolEnded(ToolEvent { - session_id: "s1".into(), - agent_kind: AgentKind::ClaudeCode, - event_name: "PostToolUse".into(), - tool_call_id: "t1".into(), - tool_name: "Read".into(), - subagent_id: Some("worker-1".into()), - arguments: Value::Null, - result: json!({ "ok": true }), - status: Some("success".into()), - payload: json!({}), - metadata: json!({}), - }), - NormalizedEvent::SubagentEnded(SubagentEvent { - session_id: "s1".into(), - agent_kind: AgentKind::ClaudeCode, - event_name: "SubagentStop".into(), - subagent_id: "worker-1".into(), - payload: json!({}), - metadata: json!({}), - }), - NormalizedEvent::AgentEnded(SessionEvent { - session_id: "s1".into(), - agent_kind: AgentKind::ClaudeCode, - event_name: "SessionEnd".into(), - payload: json!({}), - metadata: json!({}), - }), - ]; - manager.apply_events(&headers, events).await.unwrap(); - assert!(manager.inner.lock().await.is_empty()); - } - - #[tokio::test] - async fn writes_atif_on_session_end_from_header_config() { - let temp = tempfile::tempdir().unwrap(); - let config = SidecarConfig { - bind: "127.0.0.1:0".parse().unwrap(), - openai_base_url: "http://127.0.0.1".into(), - anthropic_base_url: "http://127.0.0.1".into(), - atif_dir: None, - openinference_endpoint: None, - }; - let manager = SessionManager::new(config); - let mut headers = HeaderMap::new(); - headers.insert( - "x-nemo-flow-atif-dir", - temp.path().to_string_lossy().parse().unwrap(), - ); - headers.insert( - "x-nemo-flow-session-metadata", - r#"{"team":"coverage"}"#.parse().unwrap(), - ); - headers.insert("x-nemo-flow-gateway-mode", "required".parse().unwrap()); - - manager - .apply_events( - &headers, - vec![ - NormalizedEvent::AgentStarted(SessionEvent { - session_id: "atif-session".into(), - agent_kind: AgentKind::Codex, - event_name: "sessionStart".into(), - payload: json!({ "start": true }), - metadata: json!({ "agent": "codex" }), - }), - NormalizedEvent::PromptSubmitted(SessionEvent { - session_id: "atif-session".into(), - agent_kind: AgentKind::Codex, - event_name: "UserPromptSubmit".into(), - payload: json!({ "prompt": "hello" }), - metadata: json!({}), - }), - NormalizedEvent::AgentEnded(SessionEvent { - session_id: "atif-session".into(), - agent_kind: AgentKind::Codex, - event_name: "sessionEnd".into(), - payload: json!({ "done": true }), - metadata: json!({}), - }), - ], - ) - .await - .unwrap(); - - let path = temp.path().join("atif-session.atif.json"); - let atif: Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); - assert_eq!(atif["agent"]["name"], json!("codex")); - } - - #[tokio::test] - async fn handles_out_of_order_subagent_and_tool_end_events() { - let config = SidecarConfig { - bind: "127.0.0.1:0".parse().unwrap(), - openai_base_url: "http://127.0.0.1".into(), - anthropic_base_url: "http://127.0.0.1".into(), - atif_dir: None, - openinference_endpoint: None, - }; - let manager = SessionManager::new(config); - let headers = HeaderMap::new(); - - manager - .apply_events( - &headers, - vec![ - NormalizedEvent::SubagentEnded(SubagentEvent { - session_id: "out-of-order".into(), - agent_kind: AgentKind::Cursor, - event_name: "subagentStop".into(), - subagent_id: "missing".into(), - payload: json!({ "reason": "missing-start" }), - metadata: json!({}), - }), - NormalizedEvent::ToolEnded(ToolEvent { - session_id: "out-of-order".into(), - agent_kind: AgentKind::Cursor, - event_name: "postToolUse".into(), - tool_call_id: "tool-without-start".into(), - tool_name: "Shell".into(), - subagent_id: None, - arguments: json!({ "cmd": "pwd" }), - result: json!({ "stdout": "/repo" }), - status: Some("success".into()), - payload: json!({}), - metadata: json!({}), - }), - NormalizedEvent::AgentEnded(SessionEvent { - session_id: "out-of-order".into(), - agent_kind: AgentKind::Cursor, - event_name: "sessionEnd".into(), - payload: json!({}), - metadata: json!({}), - }), - ], - ) - .await - .unwrap(); - - assert!(manager.inner.lock().await.is_empty()); - } - - #[tokio::test] - async fn llm_lifecycle_starts_implicit_gateway_session() { - let config = SidecarConfig { - bind: "127.0.0.1:0".parse().unwrap(), - openai_base_url: "http://127.0.0.1".into(), - anthropic_base_url: "http://127.0.0.1".into(), - atif_dir: None, - openinference_endpoint: None, - }; - let manager = SessionManager::new(config); - let active = manager - .start_llm( - &HeaderMap::new(), - LlmGatewayStart { - session_id: "llm-session".into(), - provider: "openai.responses".into(), - model_name: Some("gpt-test".into()), - request: LlmRequest { - headers: Map::new(), - content: json!({ "model": "gpt-test", "input": "hello" }), - }, - streaming: true, - metadata: json!({ "gateway_path": "/v1/responses" }), - }, - ) - .await - .unwrap(); - manager - .end_llm( - active, - json!({ "output_text": "hello" }), - json!({ "http_status": 200 }), - ) - .await - .unwrap(); - - let sessions = manager.inner.lock().await; - assert!(sessions.contains_key("llm-session")); - } - - #[test] - fn merge_metadata_handles_objects_nulls_and_scalars() { - assert_eq!( - merge_metadata(json!({ "a": 1 }), json!({ "b": 2, "c": null })), - json!({ "a": 1, "b": 2 }) - ); - assert_eq!( - merge_metadata(Value::Null, json!({ "a": 1 })), - json!({ "a": 1 }) - ); - assert_eq!( - merge_metadata(json!({ "a": 1 }), Value::Null), - json!({ "a": 1 }) - ); - assert_eq!( - merge_metadata(json!("left"), json!("right")), - json!({ "metadata": "left", "extra_metadata": "right" }) - ); - } -} +#[path = "../tests/coverage/session_tests.rs"] +mod tests; diff --git a/crates/sidecar/tests/coverage/adapters_tests.rs b/crates/sidecar/tests/coverage/adapters_tests.rs new file mode 100644 index 0000000..970a774 --- /dev/null +++ b/crates/sidecar/tests/coverage/adapters_tests.rs @@ -0,0 +1,229 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use axum::http::HeaderMap; +use serde_json::json; + +use super::*; +use crate::adapters::{claude_code, codex, cursor}; + +#[test] +fn maps_claude_canonical_tool_payload() { + let headers = HeaderMap::new(); + let outcome = claude_code::adapt( + json!({ + "session_id": "claude-session", + "transcript_path": "/tmp/transcript.jsonl", + "cwd": "/workspace", + "hook_event_name": "PreToolUse", + "tool_use_id": "toolu-1", + "tool_name": "Read", + "tool_input": { "file_path": "README.md" } + }), + &headers, + ); + match &outcome.events[0] { + NormalizedEvent::ToolStarted(event) => { + assert_eq!(event.session_id, "claude-session"); + assert_eq!(event.tool_call_id, "toolu-1"); + assert_eq!(event.tool_name, "Read"); + assert_eq!(event.arguments, json!({ "file_path": "README.md" })); + assert_eq!( + event.metadata["transcript_path"], + json!("/tmp/transcript.jsonl") + ); + } + event => panic!("unexpected event: {event:?}"), + } + assert_eq!(outcome.response["continue"], json!(true)); + assert_eq!( + outcome.response["hookSpecificOutput"], + json!({ + "hookEventName": "PreToolUse", + "permissionDecision": "allow" + }) + ); +} + +#[test] +fn maps_claude_post_tool_failure_with_canonical_fields() { + let headers = HeaderMap::new(); + let outcome = claude_code::adapt( + json!({ + "session_id": "claude-session", + "hook_event_name": "PostToolUseFailure", + "tool_use_id": "toolu-1", + "tool_name": "Bash", + "tool_input": { "command": "false" }, + "tool_response": { "stderr": "failed" } + }), + &headers, + ); + + match &outcome.events[0] { + NormalizedEvent::ToolEnded(event) => { + assert_eq!(event.tool_call_id, "toolu-1"); + assert_eq!(event.tool_name, "Bash"); + assert_eq!(event.result, json!({ "stderr": "failed" })); + assert_eq!(event.status.as_deref(), Some("error")); + } + event => panic!("unexpected event: {event:?}"), + } +} + +#[test] +fn maps_cursor_subagent_and_permission_response() { + let headers = HeaderMap::new(); + let outcome = cursor::adapt( + json!({ + "session_id": "cursor-session", + "project_dir": "/repo", + "user_email": "dev@example.com", + "hook_event_name": "beforeShellExecution", + "subagent": { "id": "worker" }, + "tool_call_id": "shell-1", + "tool_name": "shell", + "input": { "command": "cargo test" } + }), + &headers, + ); + match &outcome.events[0] { + NormalizedEvent::ToolStarted(event) => { + assert_eq!(event.session_id, "cursor-session"); + assert_eq!(event.subagent_id.as_deref(), Some("worker")); + assert_eq!(event.metadata["project_dir"], json!("/repo")); + assert_eq!(event.metadata["user_email"], json!("dev@example.com")); + } + event => panic!("unexpected event: {event:?}"), + } + assert_eq!(outcome.response["permission"], json!("allow")); +} + +#[test] +fn keeps_codex_response_unwrapped() { + let headers = HeaderMap::new(); + let outcome = codex::adapt( + json!({ + "session_id": "codex-session", + "hook_event_name": "sessionStart" + }), + &headers, + ); + assert!(matches!( + outcome.events[0], + NormalizedEvent::AgentStarted(_) + )); + assert_eq!(outcome.response, json!({})); +} + +#[test] +fn normalizes_mark_style_events_and_header_session_ids() { + let mut headers = HeaderMap::new(); + headers.insert("x-nemo-flow-session-id", "header-session".parse().unwrap()); + headers.insert("x-nemo-flow-config-profile", "coverage".parse().unwrap()); + + for (event_name, expected) in [ + ("UserPromptSubmit", "prompt"), + ("afterAgentResponse", "response"), + ("PreCompact", "compact"), + ("Notification", "notification"), + ("Unrecognized.Event", "hook"), + ] { + let outcome = cursor::adapt( + json!({ + "eventName": event_name, + "model": "model-a", + "cwd": "/repo" + }), + &headers, + ); + let session = match &outcome.events[0] { + NormalizedEvent::PromptSubmitted(event) if expected == "prompt" => event, + NormalizedEvent::AgentResponse(event) if expected == "response" => event, + NormalizedEvent::Compaction(event) if expected == "compact" => event, + NormalizedEvent::Notification(event) if expected == "notification" => event, + NormalizedEvent::HookMark(event) if expected == "hook" => event, + event => panic!("unexpected event for {event_name}: {event:?}"), + }; + assert_eq!(session.session_id, "header-session"); + assert_eq!(session.metadata["model"], json!("model-a")); + assert_eq!(session.metadata["cwd"], json!("/repo")); + assert_eq!( + session.metadata["sidecar_config_profile"], + json!("coverage") + ); + } +} + +#[test] +fn extracts_tool_fields_from_fallback_payload_shapes() { + let headers = HeaderMap::new(); + let outcome = codex::adapt( + json!({ + "conversationId": "conversation-1", + "event": "toolEnded", + "tool": { "id": "tool-id", "name": "Shell" }, + "arguments": { "cmd": "pwd" }, + "result": { "stdout": "/repo" }, + "permission": "allow" + }), + &headers, + ); + + match &outcome.events[0] { + NormalizedEvent::ToolEnded(event) => { + assert_eq!(event.session_id, "conversation-1"); + assert_eq!(event.tool_call_id, "tool-id"); + assert_eq!(event.tool_name, "Shell"); + assert_eq!(event.arguments, json!({ "cmd": "pwd" })); + assert_eq!(event.result, json!({ "stdout": "/repo" })); + assert_eq!(event.status.as_deref(), Some("allow")); + } + event => panic!("unexpected event: {event:?}"), + } +} + +#[test] +fn generated_ids_are_used_when_payload_omits_identifiers() { + let headers = HeaderMap::new(); + let outcome = claude_code::adapt( + json!({ + "hook_event_name": "PreToolUse", + "tool_input": { "name": "Read", "file_path": "Cargo.toml" } + }), + &headers, + ); + + match &outcome.events[0] { + NormalizedEvent::ToolStarted(event) => { + assert!(event.session_id.starts_with("hook-")); + assert!(event.tool_call_id.starts_with("tool-")); + assert_eq!(event.tool_name, "Read"); + } + event => panic!("unexpected event: {event:?}"), + } +} + +#[test] +fn stop_responses_preserve_vendor_shapes() { + let headers = HeaderMap::new(); + let claude = claude_code::adapt( + json!({ + "session_id": "claude-session", + "hook_event_name": "Stop" + }), + &headers, + ); + assert!(matches!(claude.events[0], NormalizedEvent::AgentEnded(_))); + assert_eq!(claude.response["stopReason"], Value::Null); + + let cursor = cursor::adapt( + json!({ + "session_id": "cursor-session", + "hook_event_name": "stop" + }), + &headers, + ); + assert!(matches!(cursor.events[0], NormalizedEvent::AgentEnded(_))); + assert_eq!(cursor.response, json!({ "continue": true })); +} diff --git a/crates/sidecar/tests/coverage/config_tests.rs b/crates/sidecar/tests/coverage/config_tests.rs new file mode 100644 index 0000000..5df33e9 --- /dev/null +++ b/crates/sidecar/tests/coverage/config_tests.rs @@ -0,0 +1,278 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use axum::http::HeaderValue; +use serde_json::json; + +fn config() -> SidecarConfig { + SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://openai".into(), + anthropic_base_url: "http://anthropic".into(), + atif_dir: Some(PathBuf::from("default-atif")), + openinference_endpoint: Some("http://default-otel".into()), + metadata: None, + plugin_config: None, + } +} + +#[test] +fn session_config_prefers_headers_and_parses_json() { + let mut headers = HeaderMap::new(); + headers.insert( + "x-nemo-flow-atif-dir", + HeaderValue::from_static("header-atif"), + ); + headers.insert( + "x-nemo-flow-openinference-endpoint", + HeaderValue::from_static("http://header-otel"), + ); + headers.insert( + "x-nemo-flow-config-profile", + HeaderValue::from_static("profile-a"), + ); + headers.insert( + "x-nemo-flow-session-metadata", + HeaderValue::from_static(r#"{"team":"obs"}"#), + ); + headers.insert( + "x-nemo-flow-plugin-config", + HeaderValue::from_static(r#"{"components":[]}"#), + ); + headers.insert( + "x-nemo-flow-gateway-mode", + HeaderValue::from_static("required"), + ); + + let session = config().session_config_from_headers(&headers); + + assert_eq!(session.atif_dir, Some(PathBuf::from("header-atif"))); + assert_eq!( + session.openinference_endpoint.as_deref(), + Some("http://header-otel") + ); + assert_eq!(session.profile.as_deref(), Some("profile-a")); + assert_eq!(session.metadata, Some(json!({ "team": "obs" }))); + assert_eq!(session.plugin_config, Some(json!({ "components": [] }))); + assert_eq!(session.gateway_mode.as_deref(), Some("required")); +} + +#[test] +fn session_config_uses_defaults_and_ignores_bad_json() { + let mut headers = HeaderMap::new(); + headers.insert( + "x-nemo-flow-session-metadata", + HeaderValue::from_static("not-json"), + ); + headers.insert("x-empty", HeaderValue::from_static("")); + + let session = config().session_config_from_headers(&headers); + + assert_eq!(session.atif_dir, Some(PathBuf::from("default-atif"))); + assert_eq!( + session.openinference_endpoint.as_deref(), + Some("http://default-otel") + ); + assert_eq!(session.metadata, None); + assert_eq!(header_string(&headers, "x-empty"), None); +} + +#[test] +fn agent_and_gateway_mode_arguments_are_stable() { + assert_eq!(CodingAgent::ClaudeCode.hook_path(), "/hooks/claude-code"); + assert_eq!(CodingAgent::Codex.hook_path(), "/hooks/codex"); + assert_eq!(CodingAgent::Cursor.hook_path(), "/hooks/cursor"); + assert_eq!(GatewayMode::HookOnly.as_arg(), "hook-only"); + assert_eq!(GatewayMode::Passthrough.as_arg(), "passthrough"); + assert_eq!(GatewayMode::Required.as_arg(), "required"); +} + +#[test] +fn agent_inference_uses_executable_basename() { + assert_eq!( + CodingAgent::infer("/opt/bin/claude"), + Some(CodingAgent::ClaudeCode) + ); + assert_eq!(CodingAgent::infer("codex"), Some(CodingAgent::Codex)); + assert_eq!( + CodingAgent::infer("cursor-agent"), + Some(CodingAgent::Cursor) + ); + assert_eq!(CodingAgent::infer("wrapper"), None); +} + +#[test] +fn explicit_toml_config_maps_supported_sections() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("sidecar.toml"); + std::fs::write( + &path, + r#" +[server] +openai_base_url = "http://openai" +anthropic_base_url = "http://anthropic" + +[session] +atif_dir = "atif" +metadata = { team = "obs" } +plugin_config = { components = [] } + +[export.openinference] +endpoint = "http://otel" + +[agents.claude-code] +command = "claude" + +[agents.codex] +command = "codex --approval-mode never" + +[agents.cursor] +command = "cursor-agent" +patch_restore_hooks = false +"#, + ) + .unwrap(); + let command = RunCommand { + agent: None, + config: Some(path), + openai_base_url: None, + anthropic_base_url: None, + atif_dir: None, + openinference_endpoint: None, + session_metadata: None, + plugin_config: None, + dry_run: false, + print: false, + command: vec![], + }; + + let resolved = resolve_run_config(&command, None).unwrap(); + + assert_eq!(resolved.sidecar.bind.to_string(), "127.0.0.1:0"); + assert_eq!(resolved.sidecar.openai_base_url, "http://openai"); + assert_eq!(resolved.sidecar.anthropic_base_url, "http://anthropic"); + assert_eq!(resolved.sidecar.atif_dir, Some(PathBuf::from("atif"))); + assert_eq!( + resolved.sidecar.openinference_endpoint.as_deref(), + Some("http://otel") + ); + assert_eq!(resolved.sidecar.metadata, Some(json!({ "team": "obs" }))); + assert_eq!( + resolved.agents.codex.command.as_deref(), + Some("codex --approval-mode never") + ); + assert!(!resolved.agents.cursor.patch_restore_hooks); +} + +#[test] +fn cli_run_overrides_config_values() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("sidecar.toml"); + std::fs::write( + &path, + r#" +[server] +openai_base_url = "http://file-openai" + +[session] +atif_dir = "file-atif" +metadata = { team = "file" } +"#, + ) + .unwrap(); + let command = RunCommand { + agent: Some(CodingAgent::Codex), + config: Some(path), + openai_base_url: Some("http://cli-openai".into()), + anthropic_base_url: None, + atif_dir: Some(PathBuf::from("cli-atif")), + openinference_endpoint: None, + session_metadata: Some(r#"{"team":"cli"}"#.into()), + plugin_config: None, + dry_run: false, + print: false, + command: vec!["codex".into()], + }; + + let resolved = resolve_run_config(&command, None).unwrap(); + + assert_eq!(resolved.sidecar.openai_base_url, "http://cli-openai"); + assert_eq!(resolved.sidecar.atif_dir, Some(PathBuf::from("cli-atif"))); + assert_eq!(resolved.sidecar.metadata, Some(json!({ "team": "cli" }))); +} + +#[test] +fn run_inherits_top_level_server_flags_when_subcommand_flags_are_absent() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("sidecar.toml"); + std::fs::write( + &path, + r#" +[server] +openai_base_url = "http://file-openai" +"#, + ) + .unwrap(); + let server = ServerArgs { + config: Some(path), + openai_base_url: Some("http://top-level-openai".into()), + ..ServerArgs::default() + }; + let command = RunCommand { + agent: Some(CodingAgent::Codex), + config: None, + openai_base_url: None, + anthropic_base_url: None, + atif_dir: None, + openinference_endpoint: None, + session_metadata: None, + plugin_config: None, + dry_run: false, + print: false, + command: vec!["codex".into()], + }; + + let resolved = resolve_run_config(&command, Some(&server)).unwrap(); + + assert_eq!(resolved.sidecar.openai_base_url, "http://top-level-openai"); +} + +#[test] +fn recursive_toml_merge_replaces_scalars_and_preserves_tables() { + let mut left: toml::Value = r#" +[server] +openai_base_url = "http://old" +anthropic_base_url = "http://anthropic" + +[session.metadata] +team = "old" +env = "dev" +"# + .parse::() + .map(toml::Value::Table) + .unwrap(); + let right: toml::Value = r#" +[server] +openai_base_url = "http://new" + +[session.metadata] +team = "new" +"# + .parse::() + .map(toml::Value::Table) + .unwrap(); + + merge_toml(&mut left, right); + + assert_eq!( + left["server"]["openai_base_url"].as_str(), + Some("http://new") + ); + assert_eq!( + left["server"]["anthropic_base_url"].as_str(), + Some("http://anthropic") + ); + assert_eq!(left["session"]["metadata"]["team"].as_str(), Some("new")); + assert_eq!(left["session"]["metadata"]["env"].as_str(), Some("dev")); +} diff --git a/crates/sidecar/tests/coverage/gateway_tests.rs b/crates/sidecar/tests/coverage/gateway_tests.rs new file mode 100644 index 0000000..7d3102d --- /dev/null +++ b/crates/sidecar/tests/coverage/gateway_tests.rs @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::config::SidecarConfig; +use axum::http::{HeaderMap, HeaderValue}; + +#[test] +fn removes_hop_by_hop_headers() { + assert!(!should_forward_request_header(&HeaderName::from_static( + "connection" + ))); + assert!(!should_forward_request_header(&HeaderName::from_static( + "host" + ))); + assert!(should_forward_request_header(&HeaderName::from_static( + "authorization" + ))); + assert!(!should_record_header(&HeaderName::from_static( + "authorization" + ))); + assert!(!should_record_header(&HeaderName::from_static("x-api-key"))); + assert!(!should_record_header(&HeaderName::from_static( + "anthropic-api-key" + ))); + assert!(should_record_header(&HeaderName::from_static( + "x-request-id" + ))); +} + +#[test] +fn selects_provider_routes() { + assert_eq!( + ProviderRoute::from_path("/v1/responses"), + Some(ProviderRoute::OpenAiResponses) + ); + assert_eq!( + ProviderRoute::from_path("/v1/messages/count_tokens"), + Some(ProviderRoute::AnthropicCountTokens) + ); + assert_eq!( + ProviderRoute::from_path("/v1/chat/completions") + .unwrap() + .name(), + "openai.chat_completions" + ); + assert_eq!(ProviderRoute::from_path("/unsupported"), None); +} + +#[test] +fn provider_routes_preserve_path_query_and_choose_upstream() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://openai/".into(), + anthropic_base_url: "http://anthropic/".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + + assert_eq!( + ProviderRoute::OpenAiResponses.upstream_url(&config, "/v1/responses?x=1"), + "http://openai/v1/responses?x=1" + ); + assert_eq!( + ProviderRoute::AnthropicMessages.upstream_url(&config, "/v1/messages"), + "http://anthropic/v1/messages" + ); +} + +#[test] +fn gateway_session_id_prefers_headers_and_has_fallbacks() { + let mut headers = HeaderMap::new(); + headers.insert( + "anthropic-beta", + HeaderValue::from_static("prompt-caching-2024-07-31"), + ); + assert_eq!(gateway_session_id(&headers), None); + + headers.insert( + "x-claude-code-session-id", + HeaderValue::from_static("claude-session"), + ); + assert_eq!( + gateway_session_id(&headers).as_deref(), + Some("claude-session") + ); + + headers.insert( + "x-nemo-flow-session-id", + HeaderValue::from_static("explicit-session"), + ); + assert_eq!( + gateway_session_id(&headers).as_deref(), + Some("explicit-session") + ); + + assert_eq!(gateway_session_id(&HeaderMap::new()), None); +} + +#[test] +fn observable_headers_omit_secrets_and_transport_headers() { + let mut headers = HeaderMap::new(); + headers.insert("authorization", HeaderValue::from_static("Bearer secret")); + headers.insert("x-api-key", HeaderValue::from_static("secret")); + headers.insert("connection", HeaderValue::from_static("close")); + headers.insert("x-request-id", HeaderValue::from_static("req-1")); + + let observed = observable_headers(&headers); + + assert_eq!(observed.get("x-request-id"), Some(&json!("req-1"))); + assert!(!observed.contains_key("authorization")); + assert!(!observed.contains_key("x-api-key")); + assert!(!observed.contains_key("connection")); +} + +#[test] +fn stream_response_records_preview_and_truncation() { + assert_eq!( + stream_response_json(b"data: done", false), + json!({ "stream": "data: done" }) + ); + assert_eq!( + stream_response_json(b"partial", true), + json!({ "stream_preview": "partial", "stream_truncated": true }) + ); +} diff --git a/crates/sidecar/tests/coverage/installer_tests.rs b/crates/sidecar/tests/coverage/installer_tests.rs new file mode 100644 index 0000000..f2c340b --- /dev/null +++ b/crates/sidecar/tests/coverage/installer_tests.rs @@ -0,0 +1,234 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::*; + +fn command(agent: CodingAgent, root: &Path) -> InstallCommand { + InstallCommand { + agent, + scope: InstallScope::User, + target: InstallTarget::Both, + sidecar_url: "http://127.0.0.1:4040".into(), + atif_dir: Some(root.join("atif")), + openinference_endpoint: Some("http://otel:4318/v1/traces".into()), + profile: Some("default".into()), + session_metadata: Some(r#"{"team":"agent-observability"}"#.into()), + plugin_config: Some(r#"{"components":[]}"#.into()), + gateway_mode: Some(GatewayMode::Required), + dry_run: false, + print: false, + home_dir: Some(root.to_path_buf()), + project_dir: None, + } +} + +fn project_command(agent: CodingAgent, root: &Path) -> InstallCommand { + InstallCommand { + scope: InstallScope::Project, + project_dir: Some(root.to_path_buf()), + ..command(agent, root) + } +} + +#[test] +fn generates_claude_install_file() { + let temp = tempfile::tempdir().unwrap(); + let files = planned_files(&command(CodingAgent::ClaudeCode, temp.path())).unwrap(); + assert_eq!(files.len(), 1); + assert!(files[0].path.ends_with(".claude/settings.json")); + let json: Value = serde_json::from_str(&files[0].contents).unwrap(); + assert!(json["hooks"]["SessionStart"].is_array()); + assert!( + json["hooks"]["PreToolUse"][0]["hooks"][0]["command"] + .as_str() + .unwrap() + .contains("hook-forward claude-code") + ); +} + +#[test] +fn generates_codex_config_and_hooks() { + let temp = tempfile::tempdir().unwrap(); + let files = planned_files(&command(CodingAgent::Codex, temp.path())).unwrap(); + assert_eq!(files.len(), 2); + assert!(files[0].contents.contains("codex_hooks = true")); + let json: Value = serde_json::from_str(&files[1].contents).unwrap(); + assert!(json["hooks"]["Stop"].is_array()); + assert!( + json["hooks"]["PreToolUse"][0]["hooks"][0]["command"] + .as_str() + .unwrap() + .contains("hook-forward codex") + ); +} + +#[test] +fn generates_cursor_hooks() { + let temp = tempfile::tempdir().unwrap(); + let files = planned_files(&command(CodingAgent::Cursor, temp.path())).unwrap(); + assert_eq!(files.len(), 1); + let json: Value = serde_json::from_str(&files[0].contents).unwrap(); + assert!(json["hooks"]["beforeShellExecution"].is_array()); + assert!( + json["hooks"]["beforeShellExecution"][0]["hooks"][0]["command"] + .as_str() + .unwrap() + .contains("hook-forward cursor") + ); +} + +#[test] +fn merge_hooks_is_idempotent_and_preserves_existing_entries() { + let existing = json!({ + "hooks": { + "Stop": [{ "hooks": [{ "type": "command", "command": "existing" }] }] + } + }); + let generated = codex_hooks("nemo-flow-sidecar hook-forward codex"); + let once = merge_hooks(existing, generated.clone()).unwrap(); + let twice = merge_hooks(once.clone(), generated).unwrap(); + assert_eq!(once, twice); + assert_eq!(twice["hooks"]["Stop"].as_array().unwrap().len(), 2); +} + +#[test] +fn project_install_uses_project_dir_and_preserves_codex_toml() { + let temp = tempfile::tempdir().unwrap(); + let codex_dir = temp.path().join(".codex"); + std::fs::create_dir_all(&codex_dir).unwrap(); + std::fs::write( + codex_dir.join("config.toml"), + "[features]\nother = true\n[model_providers.openai]\nbase_url = \"http://old\"\n", + ) + .unwrap(); + + let files = planned_files(&project_command(CodingAgent::Codex, temp.path())).unwrap(); + + assert!(files[0].path.starts_with(temp.path())); + assert!(files[0].contents.contains("other = true")); + assert!(files[0].contents.contains("codex_hooks = true")); + assert!(files[0].contents.contains("[model_providers.openai]")); +} + +#[test] +fn install_writes_file_and_backs_up_existing_config() { + let temp = tempfile::tempdir().unwrap(); + let claude_dir = temp.path().join(".claude"); + std::fs::create_dir_all(&claude_dir).unwrap(); + let settings = claude_dir.join("settings.json"); + std::fs::write(&settings, r#"{"hooks":{"Stop":[]}}"#).unwrap(); + + install(command(CodingAgent::ClaudeCode, temp.path())).unwrap(); + + let installed = std::fs::read_to_string(&settings).unwrap(); + assert!(installed.contains("hook-forward claude-code")); + let backups: Vec<_> = std::fs::read_dir(&claude_dir) + .unwrap() + .map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned()) + .filter(|name| name.starts_with("settings.json.bak.")) + .collect(); + assert_eq!(backups.len(), 1); +} + +#[test] +fn install_dry_run_does_not_write_files() { + let temp = tempfile::tempdir().unwrap(); + let mut command = command(CodingAgent::Cursor, temp.path()); + command.dry_run = true; + command.print = true; + + install(command).unwrap(); + + assert!(!temp.path().join(".cursor/hooks.json").exists()); +} + +#[test] +fn invalid_json_config_is_rejected_before_planning() { + let temp = tempfile::tempdir().unwrap(); + let mut command = command(CodingAgent::Codex, temp.path()); + command.session_metadata = Some("not-json".into()); + + let error = install(command).unwrap_err().to_string(); + + assert!(error.contains("invalid session metadata")); +} + +#[test] +fn merge_hooks_rejects_malformed_shapes() { + assert!(merge_hooks(json!([]), codex_hooks("cmd")).is_err()); + assert!(merge_hooks(json!({ "hooks": [] }), codex_hooks("cmd")).is_err()); + assert!(merge_hooks(json!({ "hooks": { "Stop": {} } }), codex_hooks("cmd")).is_err()); + assert!(merge_hooks(json!({}), json!({ "hooks": [] })).is_err()); +} + +#[test] +fn invalid_existing_files_are_reported() { + let temp = tempfile::tempdir().unwrap(); + let cursor_dir = temp.path().join(".cursor"); + std::fs::create_dir_all(&cursor_dir).unwrap(); + std::fs::write(cursor_dir.join("hooks.json"), "not-json").unwrap(); + + let error = planned_files(&command(CodingAgent::Cursor, temp.path())) + .unwrap_err() + .to_string(); + + assert!(error.contains("invalid JSON")); + + let codex_dir = temp.path().join(".codex"); + std::fs::create_dir_all(&codex_dir).unwrap(); + std::fs::write(codex_dir.join("config.toml"), "not = [valid").unwrap(); + let error = planned_files(&command(CodingAgent::Codex, temp.path())) + .unwrap_err() + .to_string(); + assert!(error.contains("invalid TOML")); +} + +#[test] +fn helper_formatting_and_headers_cover_optional_paths() { + assert_eq!(shell_quote("plain/arg-1"), "plain/arg-1"); + assert_eq!(shell_quote("needs space"), "'needs space'"); + assert_eq!(shell_quote("can't"), "'can'\\''t'"); + assert!(event_matches_tools("PermissionRequest")); + assert!(!event_matches_tools("SessionStart")); + + let temp = tempfile::tempdir().unwrap(); + let headers = sidecar_headers( + Some(temp.path()), + Some("http://otel"), + Some("profile"), + Some(r#"{"team":"obs"}"#), + Some(r#"{"plugins":[]}"#), + Some(GatewayMode::Passthrough), + ) + .unwrap(); + assert_eq!( + headers + .get("x-nemo-flow-gateway-mode") + .and_then(|value| value.to_str().ok()), + Some("passthrough") + ); + assert!( + insert_header( + &mut HeaderMap::new(), + "x-nemo-flow-config-profile", + Some("bad\nvalue") + ) + .is_err() + ); +} + +#[test] +fn packaged_hook_configs_are_valid_json() { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../integrations/coding-agents"); + for path in [ + root.join("claude-code/hooks/hooks.json"), + root.join("codex/hooks/hooks.json"), + root.join("cursor/.cursor/hooks.json"), + root.join("claude-code/.claude-plugin/plugin.json"), + root.join("codex/.codex-plugin/plugin.json"), + ] { + let raw = std::fs::read_to_string(&path).unwrap(); + serde_json::from_str::(&raw) + .unwrap_or_else(|error| panic!("{} is invalid JSON: {error}", path.display())); + } +} diff --git a/crates/sidecar/tests/coverage/launcher_tests.rs b/crates/sidecar/tests/coverage/launcher_tests.rs new file mode 100644 index 0000000..d47d440 --- /dev/null +++ b/crates/sidecar/tests/coverage/launcher_tests.rs @@ -0,0 +1,308 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::config::{AgentCommandConfig, CursorAgentConfig, SidecarConfig}; +use std::sync::{Mutex, OnceLock}; + +fn current_dir_lock() -> &'static Mutex<()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) +} + +#[test] +fn infers_agent_from_command_or_uses_override() { + let command = RunCommand { + agent: None, + config: None, + openai_base_url: None, + anthropic_base_url: None, + atif_dir: None, + openinference_endpoint: None, + session_metadata: None, + plugin_config: None, + dry_run: false, + print: false, + command: vec!["/usr/bin/codex".into()], + }; + let (agent, argv) = resolve_agent_and_argv(&command, &AgentConfigs::default()).unwrap(); + assert_eq!(agent, CodingAgent::Codex); + assert_eq!(argv, vec!["/usr/bin/codex"]); + + let command = RunCommand { + agent: Some(CodingAgent::ClaudeCode), + command: vec!["wrapper".into()], + ..command + }; + let (agent, _) = resolve_agent_and_argv(&command, &AgentConfigs::default()).unwrap(); + assert_eq!(agent, CodingAgent::ClaudeCode); +} + +#[test] +fn uses_configured_command_when_no_argv_is_supplied() { + let agents = AgentConfigs { + codex: AgentCommandConfig { + command: Some("codex --full-auto".into()), + }, + ..AgentConfigs::default() + }; + let command = RunCommand { + agent: Some(CodingAgent::Codex), + config: None, + openai_base_url: None, + anthropic_base_url: None, + atif_dir: None, + openinference_endpoint: None, + session_metadata: None, + plugin_config: None, + dry_run: false, + print: false, + command: vec![], + }; + + let (agent, argv) = resolve_agent_and_argv(&command, &agents).unwrap(); + + assert_eq!(agent, CodingAgent::Codex); + assert_eq!(argv, vec!["codex", "--full-auto"]); +} + +#[test] +fn inference_failure_has_actionable_message() { + let command = RunCommand { + agent: None, + config: None, + openai_base_url: None, + anthropic_base_url: None, + atif_dir: None, + openinference_endpoint: None, + session_metadata: None, + plugin_config: None, + dry_run: false, + print: false, + command: vec!["my-agent".into()], + }; + + let error = resolve_agent_and_argv(&command, &AgentConfigs::default()) + .unwrap_err() + .to_string(); + + assert!(error.contains("pass --agent claude-code")); +} + +#[test] +fn prepares_codex_config_overrides() { + let resolved = ResolvedConfig { + sidecar: SidecarConfig::default(), + agents: AgentConfigs::default(), + }; + let prepared = PreparedRun::new( + CodingAgent::Codex, + vec!["codex".into()], + "http://127.0.0.1:1234", + &resolved, + false, + ) + .unwrap(); + + assert!(prepared.argv.contains(&"features.codex_hooks=true".into())); + assert!( + prepared + .argv + .iter() + .any(|arg| arg.contains("model_providers.openai.base_url")) + ); + assert!( + prepared + .argv + .iter() + .any(|arg| arg.contains("hooks.SessionStart")) + ); +} + +#[test] +fn prepares_claude_temp_plugin() { + let resolved = ResolvedConfig { + sidecar: SidecarConfig::default(), + agents: AgentConfigs::default(), + }; + let prepared = PreparedRun::new( + CodingAgent::ClaudeCode, + vec!["claude".into()], + "http://127.0.0.1:1234", + &resolved, + false, + ) + .unwrap(); + + let plugin_index = prepared + .argv + .iter() + .position(|arg| arg == "--plugin-dir") + .unwrap(); + let plugin_dir = PathBuf::from(&prepared.argv[plugin_index + 1]); + assert!(plugin_dir.join("hooks/hooks.json").exists()); + assert!( + prepared + .env + .contains(&("ANTHROPIC_BASE_URL".into(), "http://127.0.0.1:1234".into())) + ); + prepared.restore().unwrap(); +} + +#[test] +fn cursor_patch_restore_restores_original_file() { + let _guard = current_dir_lock().lock().unwrap(); + let temp = tempfile::tempdir().unwrap(); + let previous = std::env::current_dir().unwrap(); + std::env::set_current_dir(temp.path()).unwrap(); + std::fs::create_dir_all(".cursor").unwrap(); + std::fs::write(".cursor/hooks.json", r#"{"hooks":{"sessionStart":[]}}"#).unwrap(); + let resolved = ResolvedConfig { + sidecar: SidecarConfig::default(), + agents: AgentConfigs { + cursor: CursorAgentConfig { + command: None, + patch_restore_hooks: true, + }, + ..AgentConfigs::default() + }, + }; + + let prepared = PreparedRun::new( + CodingAgent::Cursor, + vec!["cursor-agent".into()], + "http://s", + &resolved, + false, + ) + .unwrap(); + assert!( + std::fs::read_to_string(".cursor/hooks.json") + .unwrap() + .contains("hook-forward cursor") + ); + prepared.restore().unwrap(); + assert_eq!( + std::fs::read_to_string(".cursor/hooks.json").unwrap(), + r#"{"hooks":{"sessionStart":[]}}"# + ); + std::env::set_current_dir(previous).unwrap(); +} + +#[test] +fn cursor_patch_restore_removes_temporary_file() { + let _guard = current_dir_lock().lock().unwrap(); + let temp = tempfile::tempdir().unwrap(); + let previous = std::env::current_dir().unwrap(); + std::env::set_current_dir(temp.path()).unwrap(); + let resolved = ResolvedConfig { + sidecar: SidecarConfig::default(), + agents: AgentConfigs::default(), + }; + + let prepared = PreparedRun::new( + CodingAgent::Cursor, + vec!["cursor-agent".into()], + "http://s", + &resolved, + false, + ) + .unwrap(); + assert!(Path::new(".cursor/hooks.json").exists()); + prepared.restore().unwrap(); + assert!(!Path::new(".cursor/hooks.json").exists()); + std::env::set_current_dir(previous).unwrap(); +} + +#[test] +fn cursor_dry_run_does_not_write_hooks() { + let _guard = current_dir_lock().lock().unwrap(); + let temp = tempfile::tempdir().unwrap(); + let previous = std::env::current_dir().unwrap(); + std::env::set_current_dir(temp.path()).unwrap(); + let resolved = ResolvedConfig { + sidecar: SidecarConfig::default(), + agents: AgentConfigs::default(), + }; + + let prepared = PreparedRun::new( + CodingAgent::Cursor, + vec!["cursor-agent".into()], + "http://s", + &resolved, + true, + ) + .unwrap(); + + assert!(!Path::new(".cursor/hooks.json").exists()); + assert!(prepared.notes[0].contains("would temporarily merge")); + std::env::set_current_dir(previous).unwrap(); +} + +#[tokio::test] +async fn run_starts_sidecar_injects_env_and_returns_agent_exit_code() { + let temp = tempfile::tempdir().unwrap(); + let script = temp.path().join("fake-agent.sh"); + let output = temp.path().join("env.txt"); + std::fs::write( + &script, + format!( + "#!/bin/sh\nprintf '%s' \"$NEMO_FLOW_SIDECAR_URL\" > {}\nexit 7\n", + output.display() + ), + ) + .unwrap(); + make_executable(&script); + let command = RunCommand { + agent: Some(CodingAgent::Codex), + config: None, + openai_base_url: None, + anthropic_base_url: None, + atif_dir: None, + openinference_endpoint: None, + session_metadata: None, + plugin_config: None, + dry_run: false, + print: false, + command: vec![script.display().to_string()], + }; + + let code = run(command, None).await.unwrap(); + + assert_eq!(code, ExitCode::from(7)); + let url = std::fs::read_to_string(output).unwrap(); + assert!(url.starts_with("http://127.0.0.1:")); + assert!(!url.ends_with(":0")); +} + +#[tokio::test] +async fn dry_run_does_not_spawn_agent() { + let command = RunCommand { + agent: Some(CodingAgent::Codex), + config: None, + openai_base_url: None, + anthropic_base_url: None, + atif_dir: None, + openinference_endpoint: None, + session_metadata: None, + plugin_config: None, + dry_run: true, + print: false, + command: vec!["/path/that/does/not/exist".into()], + }; + + let code = run(command, None).await.unwrap(); + + assert_eq!(code, ExitCode::SUCCESS); +} + +#[cfg(unix)] +fn make_executable(path: &Path) { + use std::os::unix::fs::PermissionsExt; + let mut permissions = std::fs::metadata(path).unwrap().permissions(); + permissions.set_mode(0o755); + std::fs::set_permissions(path, permissions).unwrap(); +} + +#[cfg(not(unix))] +fn make_executable(_path: &Path) {} diff --git a/crates/sidecar/tests/coverage/server_tests.rs b/crates/sidecar/tests/coverage/server_tests.rs new file mode 100644 index 0000000..495e34b --- /dev/null +++ b/crates/sidecar/tests/coverage/server_tests.rs @@ -0,0 +1,294 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use axum::body::Body; +use axum::http::{Request, StatusCode, header}; +use axum::response::IntoResponse; +use bytes::Bytes; +use futures_util::stream; +use http_body_util::BodyExt; +use serde_json::{Value, json}; +use tokio::net::TcpListener; +use tower::ServiceExt; + +use super::*; + +fn test_config() -> SidecarConfig { + SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + } +} + +#[tokio::test] +async fn codex_hook_keeps_codex_response_shape() { + let app = router(test_config()); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/hooks/codex") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "session_id": "codex-1", + "hook_event_name": "sessionStart" + }) + .to_string(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body, json!({})); +} + +#[tokio::test] +async fn healthz_returns_ok() { + let app = router(test_config()); + let response = app + .oneshot( + Request::builder() + .method("GET") + .uri("/healthz") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body, json!({ "status": "ok" })); +} + +#[tokio::test] +async fn claude_code_hook_returns_continue_shape() { + let app = router(test_config()); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/hooks/claude-code") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "session_id": "claude-1", + "hook_event_name": "SessionStart" + }) + .to_string(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body["continue"], json!(true)); +} + +#[tokio::test] +async fn cursor_hook_returns_cursor_permission_fields() { + let app = router(test_config()); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/hooks/cursor") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "session_id": "cursor-1", + "hook_event_name": "beforeShellExecution", + "tool_call_id": "shell-1", + "tool_name": "shell", + "input": { "command": "pwd" } + }) + .to_string(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body["continue"], json!(true)); + assert_eq!(body["permission"], json!("allow")); +} + +#[tokio::test] +async fn gateway_forwards_openai_json_without_rewriting_payload() { + let upstream = spawn_upstream(false).await; + let mut config = test_config(); + config.openai_base_url = upstream; + let app = router(config); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/v1/chat/completions") + .header("content-type", "application/json") + .header("authorization", "Bearer test") + .header("connection", "close") + .body(Body::from( + json!({ + "model": "gpt-test", + "messages": [{ "role": "user", "content": "hello" }] + }) + .to_string(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body["model"], json!("gpt-test")); + assert_eq!(body["authorization"], json!("Bearer test")); + assert_eq!(body["connection"], Value::Null); +} + +#[tokio::test] +async fn gateway_preserves_streaming_body() { + let upstream = spawn_upstream(true).await; + let mut config = test_config(); + config.openai_base_url = upstream; + let app = router(config); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/v1/responses") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "model": "gpt-test", + "input": "hello", + "stream": true + }) + .to_string(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers().get(header::CONTENT_TYPE).unwrap(), + "text/event-stream" + ); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + assert_eq!(bytes, Bytes::from_static(b"data: one\n\ndata: two\n\n")); +} + +#[tokio::test] +async fn gateway_rejects_unsupported_paths() { + let app = router(test_config()); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/v1/unsupported") + .header("content-type", "application/json") + .body(Body::from("{}")) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn models_route_forwards_get_requests() { + let upstream = spawn_models_upstream().await; + let mut config = test_config(); + config.openai_base_url = upstream; + let app = router(config); + let response = app + .oneshot( + Request::builder() + .method("GET") + .uri("/v1/models?limit=1") + .header("authorization", "Bearer test") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body["path"], json!("/v1/models?limit=1")); + assert_eq!(body["authorization"], json!("Bearer test")); +} + +async fn spawn_upstream(streaming: bool) -> String { + async fn chat(headers: HeaderMap, body: Bytes) -> impl IntoResponse { + let payload: Value = serde_json::from_slice(&body).unwrap(); + Json(json!({ + "model": payload["model"], + "authorization": headers + .get(header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()), + "connection": headers + .get(header::CONNECTION) + .and_then(|value| value.to_str().ok()) + })) + } + + async fn stream_response() -> impl IntoResponse { + let chunks = stream::iter([ + Ok::<_, std::convert::Infallible>(Bytes::from_static(b"data: one\n\n")), + Ok(Bytes::from_static(b"data: two\n\n")), + ]); + ( + [(header::CONTENT_TYPE, "text/event-stream")], + Body::from_stream(chunks), + ) + } + + let app = if streaming { + Router::new().route("/v1/responses", post(stream_response)) + } else { + Router::new().route("/v1/chat/completions", post(chat)) + }; + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + format!("http://{address}") +} + +async fn spawn_models_upstream() -> String { + async fn models(headers: HeaderMap, request: Request) -> impl IntoResponse { + Json(json!({ + "path": request.uri().path_and_query().map(|value| value.as_str()), + "authorization": headers + .get(header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()) + })) + } + + let app = Router::new().route("/v1/models", get(models)); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + format!("http://{address}") +} diff --git a/crates/sidecar/tests/coverage/session_tests.rs b/crates/sidecar/tests/coverage/session_tests.rs new file mode 100644 index 0000000..828b81a --- /dev/null +++ b/crates/sidecar/tests/coverage/session_tests.rs @@ -0,0 +1,311 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use axum::http::HeaderMap; +use serde_json::json; + +use super::*; +use crate::model::{SessionEvent, ToolEvent}; + +#[tokio::test] +async fn nests_agent_subagent_and_tool_lifecycle() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + let headers = HeaderMap::new(); + let events = vec![ + NormalizedEvent::AgentStarted(SessionEvent { + session_id: "s1".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SessionStart".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "s1".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "worker-1".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::ToolStarted(ToolEvent { + session_id: "s1".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "PreToolUse".into(), + tool_call_id: "t1".into(), + tool_name: "Read".into(), + subagent_id: Some("worker-1".into()), + arguments: json!({ "file_path": "README.md" }), + result: Value::Null, + status: None, + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::ToolEnded(ToolEvent { + session_id: "s1".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "PostToolUse".into(), + tool_call_id: "t1".into(), + tool_name: "Read".into(), + subagent_id: Some("worker-1".into()), + arguments: Value::Null, + result: json!({ "ok": true }), + status: Some("success".into()), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentEnded(SubagentEvent { + session_id: "s1".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStop".into(), + subagent_id: "worker-1".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::AgentEnded(SessionEvent { + session_id: "s1".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SessionEnd".into(), + payload: json!({}), + metadata: json!({}), + }), + ]; + manager.apply_events(&headers, events).await.unwrap(); + assert!(manager.inner.lock().await.is_empty()); +} + +#[tokio::test] +async fn writes_atif_on_session_end_from_header_config() { + let temp = tempfile::tempdir().unwrap(); + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + let mut headers = HeaderMap::new(); + headers.insert( + "x-nemo-flow-atif-dir", + temp.path().to_string_lossy().parse().unwrap(), + ); + headers.insert( + "x-nemo-flow-session-metadata", + r#"{"team":"coverage"}"#.parse().unwrap(), + ); + headers.insert("x-nemo-flow-gateway-mode", "required".parse().unwrap()); + + manager + .apply_events( + &headers, + vec![ + NormalizedEvent::AgentStarted(SessionEvent { + session_id: "atif-session".into(), + agent_kind: AgentKind::Codex, + event_name: "sessionStart".into(), + payload: json!({ "start": true }), + metadata: json!({ "agent": "codex" }), + }), + NormalizedEvent::PromptSubmitted(SessionEvent { + session_id: "atif-session".into(), + agent_kind: AgentKind::Codex, + event_name: "UserPromptSubmit".into(), + payload: json!({ "prompt": "hello" }), + metadata: json!({}), + }), + NormalizedEvent::AgentEnded(SessionEvent { + session_id: "atif-session".into(), + agent_kind: AgentKind::Codex, + event_name: "sessionEnd".into(), + payload: json!({ "done": true }), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + let path = temp.path().join("atif-session.atif.json"); + let atif: Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); + assert_eq!(atif["agent"]["name"], json!("codex")); +} + +#[tokio::test] +async fn handles_out_of_order_subagent_and_tool_end_events() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + let headers = HeaderMap::new(); + + manager + .apply_events( + &headers, + vec![ + NormalizedEvent::SubagentEnded(SubagentEvent { + session_id: "out-of-order".into(), + agent_kind: AgentKind::Cursor, + event_name: "subagentStop".into(), + subagent_id: "missing".into(), + payload: json!({ "reason": "missing-start" }), + metadata: json!({}), + }), + NormalizedEvent::ToolEnded(ToolEvent { + session_id: "out-of-order".into(), + agent_kind: AgentKind::Cursor, + event_name: "postToolUse".into(), + tool_call_id: "tool-without-start".into(), + tool_name: "Shell".into(), + subagent_id: None, + arguments: json!({ "cmd": "pwd" }), + result: json!({ "stdout": "/repo" }), + status: Some("success".into()), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::AgentEnded(SessionEvent { + session_id: "out-of-order".into(), + agent_kind: AgentKind::Cursor, + event_name: "sessionEnd".into(), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + assert!(manager.inner.lock().await.is_empty()); +} + +#[tokio::test] +async fn llm_lifecycle_starts_implicit_gateway_session() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + let active = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: Some("llm-session".into()), + provider: "openai.responses".into(), + model_name: Some("gpt-test".into()), + request: LlmRequest { + headers: Map::new(), + content: json!({ "model": "gpt-test", "input": "hello" }), + }, + streaming: true, + metadata: json!({ "gateway_path": "/v1/responses" }), + }, + ) + .await + .unwrap(); + manager + .end_llm( + active, + json!({ "output_text": "hello" }), + json!({ "http_status": 200 }), + ) + .await + .unwrap(); + + let sessions = manager.inner.lock().await; + assert!(sessions.contains_key("llm-session")); +} + +#[tokio::test] +async fn llm_lifecycle_uses_single_active_hook_session_when_header_is_missing() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + manager + .apply_events( + &HeaderMap::new(), + vec![NormalizedEvent::AgentStarted(SessionEvent { + session_id: "hook-session".into(), + agent_kind: AgentKind::Codex, + event_name: "sessionStart".into(), + payload: json!({}), + metadata: json!({}), + })], + ) + .await + .unwrap(); + + let active = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: None, + provider: "openai.responses".into(), + model_name: Some("gpt-test".into()), + request: LlmRequest { + headers: Map::new(), + content: json!({ "model": "gpt-test", "input": "hello" }), + }, + streaming: false, + metadata: json!({ "gateway_path": "/v1/responses" }), + }, + ) + .await + .unwrap(); + manager + .end_llm(active, json!({ "output_text": "hello" }), json!({})) + .await + .unwrap(); + + let sessions = manager.inner.lock().await; + assert!(sessions.contains_key("hook-session")); + assert!(!sessions.contains_key("gateway-gateway")); +} + +#[test] +fn merge_metadata_handles_objects_nulls_and_scalars() { + assert_eq!( + merge_metadata(json!({ "a": 1 }), json!({ "b": 2, "c": null })), + json!({ "a": 1, "b": 2 }) + ); + assert_eq!( + merge_metadata(Value::Null, json!({ "a": 1 })), + json!({ "a": 1 }) + ); + assert_eq!( + merge_metadata(json!({ "a": 1 }), Value::Null), + json!({ "a": 1 }) + ); + assert_eq!( + merge_metadata(json!("left"), json!("right")), + json!({ "metadata": "left", "extra_metadata": "right" }) + ); +} diff --git a/docs/integrate-frameworks/about.md b/docs/integrate-frameworks/about.md index fc4e3d2..d88efed 100644 --- a/docs/integrate-frameworks/about.md +++ b/docs/integrate-frameworks/about.md @@ -38,9 +38,9 @@ Use these guide links to move from the overview into task-specific instructions. - [Basic Guide: Wrap Tool Calls](wrap-tool-calls.md) explains where to place managed tool wrappers and tool lifecycle fallbacks. - [Basic Guide: Wrap LLM Calls](wrap-llm-calls.md) explains where to place managed provider wrappers, model names, streaming behavior, and LLM lifecycle fallbacks. - [Advanced Guide: Coding-Agent Gateway Sidecar](coding-agent-sidecar.md) describes the Rust sidecar for observing Codex, Claude Code, and Cursor through canonical hooks plus a passthrough LLM gateway. -- [Claude Code Sidecar Guide](coding-agent-claude-code.md) covers Claude Code hook installation, Anthropic gateway routing, ATIF verification, and the unsupported Claude application modes. -- [Codex Sidecar Guide](coding-agent-codex.md) covers Codex CLI and local GUI/app setup, `codex_hooks = true`, model provider routing, and remote-task caveats. -- [Cursor Sidecar Guide](coding-agent-cursor.md) covers the Cursor hook bundle, GUI and CLI smoke tests, gateway routing limits, and hook-only operation. +- [Claude Code Sidecar Guide](coding-agent-claude-code.md) covers transparent Claude Code runs, Anthropic gateway routing, ATIF verification, and unsupported Claude application modes. +- [Codex Sidecar Guide](coding-agent-codex.md) covers transparent Codex CLI runs, local GUI/app caveats, model provider routing, and remote-task limits. +- [Cursor Sidecar Guide](coding-agent-cursor.md) covers transparent Cursor runs, temporary hook patching, GUI and CLI smoke tests, and gateway routing limits. - [Advanced Guide: Handle Non-Serializable Data](non-serializable-data.md) shows how to keep clients, streams, callbacks, and SDK objects outside JSON payloads. - [Advanced Guide: Using Codecs](using-codecs.md) explains typed value codecs for framework-facing wrappers. - [Advanced Guide: Provider Codecs](provider-codecs.md) explains provider request and response codecs for normalized middleware and event annotations. diff --git a/docs/integrate-frameworks/coding-agent-claude-code.md b/docs/integrate-frameworks/coding-agent-claude-code.md index d12c663..63c16a3 100644 --- a/docs/integrate-frameworks/coding-agent-claude-code.md +++ b/docs/integrate-frameworks/coding-agent-claude-code.md @@ -10,59 +10,79 @@ the supported integration target. The Claude application, Claude web, and Claude desktop sessions are unsupported unless they expose the same local hook and gateway controls as Claude Code. -## Install Hooks +## Transparent Run -Inspect the generated config first: +Use the wrapper for no-install local observability: ```bash -nemo-flow-sidecar install claude-code \ - --scope user \ - --target cli \ - --sidecar-url http://127.0.0.1:4040 \ +nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- claude +``` + +The wrapper infers Claude Code from `claude`, starts a sidecar on a dynamic +`127.0.0.1` port, creates a temporary Claude plugin directory with NeMo Flow +hooks, passes that plugin with `--plugin-dir`, and sets +`ANTHROPIC_BASE_URL` to the sidecar URL for the launched process. + +Inspect what would be launched without starting Claude Code: + +```bash +nemo-flow-sidecar run \ --atif-dir .nemo-flow/atif \ - --gateway-mode required \ + --openinference-endpoint http://127.0.0.1:4318/v1/traces \ --dry-run \ - --print + --print \ + -- claude ``` -Then install it: +If a launcher hides the command name, pass the agent explicitly: ```bash -nemo-flow-sidecar install claude-code \ - --scope user \ - --target cli \ - --sidecar-url http://127.0.0.1:4040 \ - --atif-dir .nemo-flow/atif \ - --gateway-mode required +nemo-flow-sidecar run --agent claude-code -- my-claude-wrapper ``` -The packaged hook files live in -`integrations/coding-agents/claude-code/`. The installer merges equivalent hook -entries into `.claude/settings.json` and backs up an existing file before -writing. +## Shared Config -## Start The Sidecar +Create `.nemo-flow/sidecar.toml` for project defaults or +`~/.config/nemo-flow/sidecar.toml` for user defaults: -```bash -NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ -nemo-flow-sidecar --bind 127.0.0.1:4040 +```toml +[session] +atif_dir = ".nemo-flow/atif" +metadata = { team = "agent-observability" } + +[export.openinference] +endpoint = "http://127.0.0.1:4318/v1/traces" + +[agents.claude-code] +command = "claude" ``` -Add `NEMO_FLOW_OPENINFERENCE_ENDPOINT` or `--openinference-endpoint` when the -session should also export OpenInference traces. +Then run `nemo-flow-sidecar run --agent claude-code` to use the configured +command. User config takes priority over project and global config. + +## Persistent Install + +Use persistent hooks only when you want Claude Code configured outside the +wrapper: -## Configure The Gateway +```bash +nemo-flow-sidecar install claude-code \ + --scope user \ + --target cli \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif +``` -Route Claude Code Anthropic traffic through the sidecar: +Then set `ANTHROPIC_BASE_URL` in the Claude Code process environment and start the +sidecar manually in another terminal: ```bash export ANTHROPIC_BASE_URL=http://127.0.0.1:4040 +NEMO_FLOW_ATIF_DIR=.nemo-flow/atif nemo-flow-sidecar --bind 127.0.0.1:4040 ``` The sidecar forwards Anthropic `/v1/messages`, `/v1/messages/count_tokens`, and -model routes without rewriting provider JSON. Hook-only mode observes agent, -subagent, and tool lifecycle, but it cannot prove complete LLM lifecycle without -this gateway routing. +model routes without rewriting provider JSON. ## Smoke Test @@ -70,8 +90,9 @@ Run a small Claude Code prompt that starts a session and uses one simple tool. Then check that hook forwarding reaches the sidecar: ```bash +curl -f http://127.0.0.1:4040/healthz printf '{"session_id":"smoke-claude","hook_event_name":"SessionStart"}' \ - | nemo-flow-sidecar hook-forward claude-code --sidecar-url http://127.0.0.1:4040 + | NEMO_FLOW_SIDECAR_URL=http://127.0.0.1:4040 nemo-flow-sidecar hook-forward claude-code --fail-closed ``` The response should be valid Claude Code hook JSON. For most lifecycle events it diff --git a/docs/integrate-frameworks/coding-agent-codex.md b/docs/integrate-frameworks/coding-agent-codex.md index 670c9f6..17be669 100644 --- a/docs/integrate-frameworks/coding-agent-codex.md +++ b/docs/integrate-frameworks/coding-agent-codex.md @@ -10,64 +10,73 @@ sessions that honor the same local config and gateway routing. Cloud or remote Codex tasks are partial or unsupported for local sidecar LLM capture because the local sidecar cannot observe provider traffic that never reaches the machine. -## Install Hooks +## Transparent Run -Inspect the generated config first: +Use the wrapper for no-install local observability: ```bash -nemo-flow-sidecar install codex \ - --scope user \ - --target both \ - --sidecar-url http://127.0.0.1:4040 \ +nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- codex +``` + +The wrapper infers Codex from `codex`, starts a sidecar on a dynamic +`127.0.0.1` port, enables Codex hooks with CLI config overrides, injects hook +commands that use `NEMO_FLOW_SIDECAR_URL`, and sets the active OpenAI provider +`base_url` to the sidecar URL. + +Inspect what would be launched without starting Codex: + +```bash +nemo-flow-sidecar run \ --atif-dir .nemo-flow/atif \ - --gateway-mode required \ + --openinference-endpoint http://127.0.0.1:4318/v1/traces \ --dry-run \ - --print + --print \ + -- codex ``` -Then install it: +If a launcher hides the command name, pass the agent explicitly: ```bash -nemo-flow-sidecar install codex \ - --scope user \ - --target both \ - --sidecar-url http://127.0.0.1:4040 \ - --atif-dir .nemo-flow/atif \ - --gateway-mode required +nemo-flow-sidecar run --agent codex -- my-codex-wrapper ``` -The packaged Codex plugin files live in `integrations/coding-agents/codex/`. -The installer merges hook entries into `.codex/hooks.json` and enables hooks in -`.codex/config.toml` with: +## Shared Config + +Create `.nemo-flow/sidecar.toml` for project defaults or +`~/.config/nemo-flow/sidecar.toml` for user defaults: ```toml -[features] -codex_hooks = true -``` +[server] +openai_base_url = "https://api.openai.com" -## Start The Sidecar +[session] +atif_dir = ".nemo-flow/atif" +metadata = { team = "agent-observability" } -```bash -NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ -nemo-flow-sidecar --bind 127.0.0.1:4040 +[agents.codex] +command = "codex" ``` -Use `--openai-base-url` if the sidecar should forward OpenAI-compatible traffic -to a provider other than `https://api.openai.com`. +Then run `nemo-flow-sidecar run --agent codex` to use the configured command. +User config takes priority over project and global config. -## Configure The Gateway +## Persistent Install -For Codex CLI, configure the model provider `base_url` to use the sidecar: +Use persistent hooks only when you want Codex configured outside the wrapper: -```toml -[model_providers.openai] -base_url = "http://127.0.0.1:4040" +```bash +nemo-flow-sidecar install codex \ + --scope user \ + --target both \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif ``` -Local Codex GUI or app sessions have the same support level only when they read -the same local hook/plugin config and provider routing. Cloud tasks may still -emit some lifecycle hooks, but complete LLM lifecycle capture requires model -traffic to pass through the sidecar. +Then start the sidecar manually and configure the local Codex provider +`base_url` to `http://127.0.0.1:4040`. Local Codex GUI or app sessions have the +same support level only when they read the same local hook/plugin config and +provider routing. Cloud tasks may still emit some lifecycle hooks, but complete +LLM lifecycle capture requires model traffic to pass through the sidecar. ## Smoke Test @@ -75,8 +84,9 @@ Run a small Codex prompt that starts a session and uses one simple tool. Then check hook forwarding directly: ```bash +curl -f http://127.0.0.1:4040/healthz printf '{"session_id":"smoke-codex","hook_event_name":"sessionStart"}' \ - | nemo-flow-sidecar hook-forward codex --sidecar-url http://127.0.0.1:4040 + | NEMO_FLOW_SIDECAR_URL=http://127.0.0.1:4040 nemo-flow-sidecar hook-forward codex --fail-closed ``` The response should match Codex hook semantics. For most lifecycle events it is diff --git a/docs/integrate-frameworks/coding-agent-cursor.md b/docs/integrate-frameworks/coding-agent-cursor.md index 25e8731..41f8831 100644 --- a/docs/integrate-frameworks/coding-agent-cursor.md +++ b/docs/integrate-frameworks/coding-agent-cursor.md @@ -19,54 +19,67 @@ Cursor CLI support must be verified separately with `cursor-agent`. If CLI hooks do not fire, treat Cursor CLI support as hook-limited and gateway-only where model routing is configurable. -## Install Hooks +## Transparent Run -Inspect the generated config first: +Use the wrapper for no-install local observability: ```bash -nemo-flow-sidecar install cursor \ - --scope project \ - --target gui \ - --sidecar-url http://127.0.0.1:4040 \ +nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- cursor-agent +``` + +The wrapper infers Cursor from `cursor` or `cursor-agent`, starts a sidecar on a +dynamic `127.0.0.1` port, temporarily merges NeMo Flow hook entries into the +project `.cursor/hooks.json`, launches Cursor, and restores the original hook +file after the agent exits. + +Inspect what would be launched without starting Cursor: + +```bash +nemo-flow-sidecar run \ --atif-dir .nemo-flow/atif \ - --gateway-mode passthrough \ --dry-run \ - --print + --print \ + -- cursor-agent ``` -Then install it: +If a launcher hides the command name, pass the agent explicitly: ```bash -nemo-flow-sidecar install cursor \ - --scope project \ - --target gui \ - --sidecar-url http://127.0.0.1:4040 \ - --atif-dir .nemo-flow/atif \ - --gateway-mode passthrough +nemo-flow-sidecar run --agent cursor -- my-cursor-wrapper ``` -The installer merges NeMo Flow entries into `.cursor/hooks.json` and backs up an -existing file before writing. +## Shared Config -## Start The Sidecar +Create `.nemo-flow/sidecar.toml` for project defaults or +`~/.config/nemo-flow/sidecar.toml` for user defaults: -```bash -NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ -nemo-flow-sidecar --bind 127.0.0.1:4040 +```toml +[session] +atif_dir = ".nemo-flow/atif" +metadata = { team = "agent-observability" } + +[agents.cursor] +command = "cursor-agent" +patch_restore_hooks = true ``` -Use `--openai-base-url` or `--anthropic-base-url` when the sidecar should -forward to non-default upstream providers. +Then run `nemo-flow-sidecar run --agent cursor` to use the configured command. +User config takes priority over project and global config. -## Configure The Gateway +## Persistent Install -If Cursor exposes provider base URL configuration, point OpenAI-compatible or -Anthropic-compatible traffic at: +Use persistent hooks only when you want Cursor configured outside the wrapper: -```text -http://127.0.0.1:4040 +```bash +nemo-flow-sidecar install cursor \ + --scope project \ + --target gui \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif ``` +Then start the sidecar manually and point Cursor provider traffic at +`http://127.0.0.1:4040` where Cursor exposes provider base URL configuration. Hook-only Cursor mode observes agent and tool lifecycle but cannot provide complete LLM lifecycle. Missing LLM spans are expected when Cursor sends model traffic directly to the provider or through a remote service. @@ -77,8 +90,9 @@ Run a small Cursor GUI session that starts an agent and uses one simple tool. Then check hook forwarding directly: ```bash +curl -f http://127.0.0.1:4040/healthz printf '{"session_id":"smoke-cursor","hook_event_name":"sessionStart"}' \ - | nemo-flow-sidecar hook-forward cursor --sidecar-url http://127.0.0.1:4040 + | NEMO_FLOW_SIDECAR_URL=http://127.0.0.1:4040 nemo-flow-sidecar hook-forward cursor --fail-closed ``` For Cursor CLI, run an equivalent `cursor-agent` session and verify the sidecar diff --git a/docs/integrate-frameworks/coding-agent-sidecar.md b/docs/integrate-frameworks/coding-agent-sidecar.md index cc481b8..46235ca 100644 --- a/docs/integrate-frameworks/coding-agent-sidecar.md +++ b/docs/integrate-frameworks/coding-agent-sidecar.md @@ -47,22 +47,73 @@ payload schemas. It removes only hop-by-hop transport headers, forwards streaming responses as streams, and emits NeMo Flow LLM start and end events under the active session scope. -## Session Configuration +## Transparent Run -Sidecar-specific configuration travels through hook registration settings, -headers, environment variables, or a referenced sidecar profile. It must not -replace the coding agent's canonical hook schema. +Use `nemo-flow-sidecar run` for no-install local observability. The wrapper +starts a sidecar on a dynamic `127.0.0.1` port, injects the resolved hook and +gateway configuration into the launched coding agent, and stops the sidecar +when the agent exits. -Common headers are: +```bash +nemo-flow-sidecar run -- codex +nemo-flow-sidecar run -- claude +nemo-flow-sidecar run -- cursor-agent +``` + +The wrapper infers the agent from the command basename. Use `--agent` when a +launcher or wrapper hides the real agent name: + +```bash +nemo-flow-sidecar run --agent codex -- my-codex-wrapper +``` + +Use `--dry-run --print` to inspect the generated hook config, gateway +environment, sidecar URL, and final command without launching the agent. + +## Shared Configuration + +Shared TOML config is optional. The sidecar loads defaults, then global config, +then project config, then user config. User config takes priority over global +and project config. CLI flags and environment variables override file config. + +Config file locations are: -- `x-nemo-flow-session-id` -- `x-nemo-flow-config-profile` -- `x-nemo-flow-session-metadata` -- `x-nemo-flow-plugin-config` -- `x-nemo-flow-openinference-endpoint` -- `x-nemo-flow-atif-dir` +- `/etc/nemo-flow/sidecar.toml` +- `.nemo-flow/sidecar.toml` +- `$XDG_CONFIG_HOME/nemo-flow/sidecar.toml` +- `~/.config/nemo-flow/sidecar.toml` -Common environment variables are: +Example: + +```toml +[server] +openai_base_url = "https://api.openai.com" +anthropic_base_url = "https://api.anthropic.com" + +[session] +atif_dir = ".nemo-flow/atif" +metadata = { team = "agent-observability" } +plugin_config = { components = [] } + +[export.openinference] +endpoint = "http://127.0.0.1:4318/v1/traces" + +[agents.claude-code] +command = "claude" + +[agents.codex] +command = "codex" + +[agents.cursor] +command = "cursor-agent" +patch_restore_hooks = true +``` + +Transparent runs always bind the managed sidecar to `127.0.0.1:0`. The selected +port is discovered by the wrapper and exposed to hooks through +`NEMO_FLOW_SIDECAR_URL`. + +Common environment variables for direct sidecar server use are: - `NEMO_FLOW_SIDECAR_BIND` - `NEMO_FLOW_OPENAI_BASE_URL` @@ -94,10 +145,11 @@ Cursor hook-only mode observes agent, subagent, and tool lifecycle. To observe Cursor LLM lifecycle completely, configure Cursor model traffic to use the sidecar gateway. -## Install Integrations +## Persistent Install -The repository includes installable integration packages under -`integrations/coding-agents/` and an installer in the sidecar binary. +The repository also includes installable integration packages under +`integrations/coding-agents/`. Use `install` when you want stable hook config +instead of the transparent wrapper. ```bash nemo-flow-sidecar install claude-code --scope user --target cli --sidecar-url http://127.0.0.1:4040 @@ -115,22 +167,25 @@ headers: - `--atif-dir` sets `x-nemo-flow-atif-dir`. - `--openinference-endpoint` sets `x-nemo-flow-openinference-endpoint`. -- `--profile` sets `x-nemo-flow-config-profile`. - `--session-metadata` sets `x-nemo-flow-session-metadata`. - `--plugin-config` sets `x-nemo-flow-plugin-config`. -- `--gateway-mode hook-only|passthrough|required` sets - `x-nemo-flow-gateway-mode`. -The generated hooks run: +Static integration bundles rely on the wrapper-provided +`NEMO_FLOW_SIDECAR_URL` and run: ```bash nemo-flow-sidecar hook-forward ``` +Persistent installer output embeds `--sidecar-url` and any selected export or +session options directly in the generated hook command. + `hook-forward` reads the canonical hook payload from standard input, sends it to -the matching endpoint, and prints the endpoint response. It fails open by -default so observability outages do not block the coding agent. Add -`--fail-closed` only when policy requires hook delivery to block the agent. +the matching endpoint, and prints the endpoint response. In transparent runs it +discovers the sidecar through `NEMO_FLOW_SIDECAR_URL`; in persistent installs +you can still pass `--sidecar-url`. It fails open by default so observability +outages do not block the coding agent. Add `--fail-closed` only when policy +requires hook delivery to block the agent. ## Agent Guides @@ -141,6 +196,6 @@ application-mode caveats. - [Codex Sidecar Guide](coding-agent-codex.md) - [Cursor Sidecar Guide](coding-agent-cursor.md) -Each guide covers plugin or hook installation, sidecar startup, gateway routing, -hook smoke tests, ATIF export verification on session end, and troubleshooting -missing LLM lifecycle data. +Each guide covers transparent run setup, persistent installation, gateway +routing, hook smoke tests, ATIF export verification on session end, and +troubleshooting missing LLM lifecycle data. diff --git a/integrations/coding-agents/README.md b/integrations/coding-agents/README.md index b4211f0..f558082 100644 --- a/integrations/coding-agents/README.md +++ b/integrations/coding-agents/README.md @@ -5,8 +5,8 @@ SPDX-License-Identifier: Apache-2.0 # NeMo Flow Coding-Agent Observability Integrations -This directory contains installable hook integrations for coding agents that -should be observed by `nemo-flow-sidecar`. +This directory contains hook integration bundles for coding agents that should +be observed by `nemo-flow-sidecar`. The sidecar combines two observability paths: @@ -17,7 +17,8 @@ The sidecar combines two observability paths: Hook integrations preserve each coding agent's canonical hook payload. They do not wrap the payload in a shared NeMo Flow envelope. Sidecar-specific settings -travel through hook command arguments and HTTP headers. +travel through the transparent wrapper, hook command arguments, HTTP headers, +environment variables, or shared TOML config. ## Packages @@ -28,18 +29,43 @@ travel through hook command arguments and HTTP headers. - `cursor/` installs a Cursor `.cursor/hooks.json` bundle targeting `POST /hooks/cursor`. -## Common Setup +## Transparent Setup Build or install the sidecar binary so `nemo-flow-sidecar` is on `PATH`. -Start the sidecar: +Prefer the wrapper. It starts a sidecar on a dynamic `127.0.0.1` port, injects +temporary hook and gateway configuration, runs the agent, and shuts the sidecar +down when the agent exits. ```bash -NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ -nemo-flow-sidecar --bind 127.0.0.1:4040 +nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- claude +nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- codex +nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- cursor-agent ``` -Install an integration: +Use `--agent claude-code|codex|cursor` when a wrapper hides the agent command +name. Use `--dry-run --print` to inspect generated config without launching. + +Shared TOML config is loaded from `/etc/nemo-flow/sidecar.toml`, then nearest +project `.nemo-flow/sidecar.toml`, then +`$XDG_CONFIG_HOME/nemo-flow/sidecar.toml` or +`~/.config/nemo-flow/sidecar.toml`. + +```toml +[session] +atif_dir = ".nemo-flow/atif" +metadata = { team = "agent-observability" } + +[export.openinference] +endpoint = "http://127.0.0.1:4318/v1/traces" + +[agents.codex] +command = "codex" +``` + +## Persistent Setup + +Use `install` only when you want persistent hook configuration: ```bash nemo-flow-sidecar install claude-code --scope user --target cli --sidecar-url http://127.0.0.1:4040 @@ -55,35 +81,37 @@ nemo-flow-sidecar install codex \ --target both \ --sidecar-url http://127.0.0.1:4040 \ --atif-dir .nemo-flow/atif \ - --gateway-mode required \ --dry-run \ --print ``` The installer backs up existing config files, merges only NeMo Flow hook -entries, and avoids adding duplicate NeMo Flow entries on repeated runs. +entries, and avoids adding duplicate NeMo Flow entries on repeated runs. In +persistent mode you start the sidecar yourself and pass `--sidecar-url` or set +`NEMO_FLOW_SIDECAR_URL` for hook forwarding. ## Common Options -The installer writes hook commands that call: +Static bundles rely on `NEMO_FLOW_SIDECAR_URL` from `nemo-flow-sidecar run` and +call: ```bash nemo-flow-sidecar hook-forward ``` +Persistent installer output includes `--sidecar-url` and any selected export or +session options in the generated command. + `hook-forward` reads the canonical hook JSON from standard input, forwards it to the matching sidecar endpoint, and prints the vendor-specific hook response. -Useful install options: +Useful wrapper and install options: - `--atif-dir ` writes ATIF trajectories on session end. - `--openinference-endpoint ` exports OpenInference traces. -- `--profile ` records a sidecar profile name in session metadata. - `--session-metadata ''` adds structured metadata to the agent begin event. - `--plugin-config ''` records scope-local plugin configuration metadata. -- `--gateway-mode hook-only|passthrough|required` records the intended gateway - mode for the session. - `--fail-closed` can be added to generated hook commands when the agent should block on hook delivery failures. The default is fail-open. @@ -102,8 +130,9 @@ The sidecar exposes these passthrough routes: - `POST /v1/messages/count_tokens` - `GET /v1/models` -Configure each coding agent's provider base URL to use -`http://127.0.0.1:4040` where that agent supports local provider routing. +Transparent runs configure provider routing automatically where the launched +agent supports local routing. Persistent installs require you to point the +agent's provider base URL at the sidecar manually. ## Verify Export diff --git a/integrations/coding-agents/claude-code/README.md b/integrations/coding-agents/claude-code/README.md index 5469e72..5f1f14f 100644 --- a/integrations/coding-agents/claude-code/README.md +++ b/integrations/coding-agents/claude-code/README.md @@ -5,8 +5,8 @@ SPDX-License-Identifier: Apache-2.0 # NeMo Flow Claude Code Observability -This package installs Claude Code hook entries that forward canonical Claude Code -hook JSON to `nemo-flow-sidecar` at `/hooks/claude-code`. +This package contains Claude Code hook entries that forward canonical Claude +Code hook JSON to `nemo-flow-sidecar` at `/hooks/claude-code`. Claude Code is the supported Claude integration target. Claude application, Claude web, and Claude desktop sessions are unsupported unless they expose the @@ -14,111 +14,89 @@ same local hook and gateway controls as Claude Code. ## Files -- `.claude-plugin/plugin.json` describes the installable Claude Code hook - package. +- `.claude-plugin/plugin.json` describes the Claude Code hook package. - `hooks/hooks.json` contains hook entries that run `nemo-flow-sidecar hook-forward claude-code`. -## Start The Sidecar +## Transparent Setup Build or install the sidecar binary so `nemo-flow-sidecar` is on `PATH`. -Start a local sidecar with ATIF export enabled: +Run Claude Code through the wrapper: ```bash -NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ -nemo-flow-sidecar --bind 127.0.0.1:4040 +nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- claude ``` -Add OpenInference export when needed: +The wrapper starts a per-invocation sidecar on a dynamic localhost port, +creates a temporary Claude plugin directory, passes it with `--plugin-dir`, sets +`ANTHROPIC_BASE_URL` for the launched process, and removes the temporary plugin +when Claude exits. + +Inspect the launch without starting Claude Code: ```bash -nemo-flow-sidecar \ - --bind 127.0.0.1:4040 \ +nemo-flow-sidecar run \ --atif-dir .nemo-flow/atif \ - --openinference-endpoint http://127.0.0.1:4318/v1/traces + --openinference-endpoint http://127.0.0.1:4318/v1/traces \ + --dry-run \ + --print \ + -- claude ``` -## Install Hooks +## Shared Config + +Use `.nemo-flow/sidecar.toml` for project defaults or +`~/.config/nemo-flow/sidecar.toml` for user defaults: + +```toml +[session] +atif_dir = ".nemo-flow/atif" +metadata = { team = "agent-observability" } -Inspect the generated config before writing: +[agents.claude-code] +command = "claude" +``` + +Then run: ```bash -nemo-flow-sidecar install claude-code \ - --scope user \ - --target cli \ - --sidecar-url http://127.0.0.1:4040 \ - --atif-dir .nemo-flow/atif \ - --gateway-mode required \ - --dry-run \ - --print +nemo-flow-sidecar run --agent claude-code ``` -Install for Claude Code: +## Persistent Setup + +Use persistent hooks only when you do not want to launch Claude Code through the +wrapper: ```bash nemo-flow-sidecar install claude-code \ --scope user \ --target cli \ --sidecar-url http://127.0.0.1:4040 \ - --atif-dir .nemo-flow/atif \ - --gateway-mode required -``` - -The installer merges NeMo Flow hook entries into `.claude/settings.json` and -backs up any existing file before writing. Sidecar-specific options are stored -in the generated hook command and forwarded as HTTP headers. + --atif-dir .nemo-flow/atif -## Configure LLM Gateway - -For complete LLM lifecycle observability, route Claude Code's Anthropic traffic -through the sidecar: - -```bash export ANTHROPIC_BASE_URL=http://127.0.0.1:4040 +NEMO_FLOW_ATIF_DIR=.nemo-flow/atif nemo-flow-sidecar --bind 127.0.0.1:4040 ``` -The sidecar forwards Anthropic `/v1/messages`, `/v1/messages/count_tokens`, and -model routes without rewriting provider request or response JSON. - -Hook-only mode observes Claude Code sessions, prompts, subagents, tools, -compaction, and stop events. It does not observe provider request and response -lifecycle unless model traffic goes through the sidecar gateway. +## Verify -## Smoke Test - -Verify the sidecar endpoint directly: +Run a Claude Code session that starts, uses one simple tool, and ends. Confirm +that ATIF was written: ```bash -printf '{"session_id":"smoke-claude","hook_event_name":"SessionStart"}' \ - | nemo-flow-sidecar hook-forward claude-code --sidecar-url http://127.0.0.1:4040 +ls .nemo-flow/atif ``` -The command should print a Claude-compatible continue response. - -Then run a small Claude Code prompt that starts a session and uses one simple -tool. The sidecar should receive hook requests for session and tool lifecycle -events. - -## Verify ATIF Export - -End the Claude Code session and confirm that ATIF was written: +For a direct endpoint smoke test against a manually started sidecar: ```bash -ls .nemo-flow/atif +curl -f http://127.0.0.1:4040/healthz +printf '{"session_id":"smoke-claude","hook_event_name":"SessionStart"}' \ + | NEMO_FLOW_SIDECAR_URL=http://127.0.0.1:4040 nemo-flow-sidecar hook-forward claude-code --fail-closed ``` -The sidecar writes `.atif.json` when it receives `SessionEnd` for a -session with ATIF enabled. - -## Troubleshooting - -If no hook events arrive, confirm `nemo-flow-sidecar` is on `PATH`, Claude Code -loaded `.claude/settings.json`, and the sidecar is listening on the configured -URL. - -If hooks arrive but LLM spans are missing, confirm `ANTHROPIC_BASE_URL` is set -in the Claude Code process environment and points to `http://127.0.0.1:4040`. - -If ATIF is missing, confirm `--atif-dir` or `NEMO_FLOW_ATIF_DIR` is configured -and that the sidecar process can write to the directory. +If hooks arrive but LLM spans are missing, confirm the Claude Code process was +started by `nemo-flow-sidecar run` or has `ANTHROPIC_BASE_URL` set to the +sidecar URL. diff --git a/integrations/coding-agents/codex/README.md b/integrations/coding-agents/codex/README.md index 6d98c88..dfec71f 100644 --- a/integrations/coding-agents/codex/README.md +++ b/integrations/coding-agents/codex/README.md @@ -5,128 +5,99 @@ SPDX-License-Identifier: Apache-2.0 # NeMo Flow Codex Observability -This package installs Codex hook entries that forward canonical Codex hook JSON +This package contains Codex hook entries that forward canonical Codex hook JSON to `nemo-flow-sidecar` at `/hooks/codex`. -Codex CLI is fully supported for local sessions when hooks and provider routing -are configured locally. Codex GUI or app sessions are supported only when they -run on the same machine and honor the same local hook/plugin config and provider -routing. Cloud or remote Codex tasks are partial or unsupported for local -sidecar LLM capture. +Codex CLI is fully supported for local sessions. Codex GUI or app sessions are +supported only when they run locally and honor the same hook/plugin config and +provider routing. Cloud or remote Codex tasks are partial or unsupported for +local sidecar LLM capture. ## Files -- `.codex-plugin/plugin.json` describes the installable Codex plugin package. +- `.codex-plugin/plugin.json` describes the Codex plugin package. - `hooks/hooks.json` contains hook entries that run `nemo-flow-sidecar hook-forward codex`. -## Start The Sidecar +## Transparent Setup Build or install the sidecar binary so `nemo-flow-sidecar` is on `PATH`. -Start a local sidecar with ATIF export enabled: +Run Codex through the wrapper: ```bash -NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ -nemo-flow-sidecar --bind 127.0.0.1:4040 +nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- codex ``` -Use a custom OpenAI-compatible upstream when needed: +The wrapper starts a per-invocation sidecar on a dynamic localhost port, +enables Codex hooks with CLI config overrides, injects hook commands that use +`NEMO_FLOW_SIDECAR_URL`, and sets the active OpenAI provider `base_url` to the +sidecar URL. -```bash -nemo-flow-sidecar \ - --bind 127.0.0.1:4040 \ - --openai-base-url https://api.openai.com \ - --atif-dir .nemo-flow/atif -``` - -## Install Hooks - -Inspect generated changes before writing: +Inspect the launch without starting Codex: ```bash -nemo-flow-sidecar install codex \ - --scope user \ - --target both \ - --sidecar-url http://127.0.0.1:4040 \ +nemo-flow-sidecar run \ --atif-dir .nemo-flow/atif \ - --gateway-mode required \ + --openinference-endpoint http://127.0.0.1:4318/v1/traces \ --dry-run \ - --print + --print \ + -- codex ``` -Install for Codex CLI and local GUI/app sessions: +## Shared Config -```bash -nemo-flow-sidecar install codex \ - --scope user \ - --target both \ - --sidecar-url http://127.0.0.1:4040 \ - --atif-dir .nemo-flow/atif \ - --gateway-mode required -``` - -The installer merges NeMo Flow hook entries into `.codex/hooks.json`, backs up -existing config files, and enables Codex hooks in `.codex/config.toml`: +Use `.nemo-flow/sidecar.toml` for project defaults or +`~/.config/nemo-flow/sidecar.toml` for user defaults: ```toml -[features] -codex_hooks = true -``` +[session] +atif_dir = ".nemo-flow/atif" +metadata = { team = "agent-observability" } -## Configure LLM Gateway - -For complete LLM lifecycle observability, configure the local Codex model -provider `base_url` to use the sidecar gateway: - -```toml -[model_providers.openai] -base_url = "http://127.0.0.1:4040" +[agents.codex] +command = "codex" ``` -The sidecar forwards OpenAI-compatible `/v1/responses`, -`/v1/chat/completions`, and model routes without rewriting provider request or -response JSON. +Then run: -Hook-only mode observes Codex sessions, prompts, subagents, tools, compaction, -and stop events. It does not observe provider request and response lifecycle -unless model traffic goes through the sidecar gateway. +```bash +nemo-flow-sidecar run --agent codex +``` -## Smoke Test +## Persistent Setup -Verify the sidecar endpoint directly: +Use persistent hooks only when you do not want to launch Codex through the +wrapper: ```bash -printf '{"session_id":"smoke-codex","hook_event_name":"sessionStart"}' \ - | nemo-flow-sidecar hook-forward codex --sidecar-url http://127.0.0.1:4040 +nemo-flow-sidecar install codex \ + --scope user \ + --target both \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif ``` -The command should print a Codex-compatible hook response. Most lifecycle events -return an empty JSON object. +Then start the sidecar manually and configure the local Codex provider +`base_url` to `http://127.0.0.1:4040`. -Then run a small Codex prompt that starts a session and uses one simple tool. -The sidecar should receive hook requests for session and tool lifecycle events. +## Verify -## Verify ATIF Export - -End the Codex session and confirm that ATIF was written: +Run a Codex session that starts, uses one simple tool, and ends. Confirm that +ATIF was written: ```bash ls .nemo-flow/atif ``` -The sidecar writes `.atif.json` when it receives a session-end hook -for a session with ATIF enabled. - -## Troubleshooting +For a direct endpoint smoke test against a manually started sidecar: -If no hook events arrive, confirm `codex_hooks = true`, Codex loaded the -expected `.codex/hooks.json`, `nemo-flow-sidecar` is on `PATH`, and the sidecar -is listening on the configured URL. - -If hooks arrive but LLM spans are missing, confirm the active Codex process uses -a provider `base_url` of `http://127.0.0.1:4040`. For GUI/app sessions, confirm -the session is local rather than remote. +```bash +curl -f http://127.0.0.1:4040/healthz +printf '{"session_id":"smoke-codex","hook_event_name":"sessionStart"}' \ + | NEMO_FLOW_SIDECAR_URL=http://127.0.0.1:4040 nemo-flow-sidecar hook-forward codex --fail-closed +``` -If ATIF is missing, confirm `--atif-dir` or `NEMO_FLOW_ATIF_DIR` is configured -and that the sidecar process can write to the directory. +If hooks arrive but LLM spans are missing, confirm Codex was started by +`nemo-flow-sidecar run` or that the active provider `base_url` points to the +sidecar URL. diff --git a/integrations/coding-agents/cursor/README.md b/integrations/coding-agents/cursor/README.md index 0484a7e..27205e3 100644 --- a/integrations/coding-agents/cursor/README.md +++ b/integrations/coding-agents/cursor/README.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # NeMo Flow Cursor Observability This package is a Cursor hook bundle, not an official Cursor plugin package. It -installs `.cursor/hooks.json` entries that forward canonical Cursor hook JSON to +contains `.cursor/hooks.json` entries that forward canonical Cursor hook JSON to `nemo-flow-sidecar` at `/hooks/cursor`. Cursor GUI or IDE sessions can provide agent, subagent, tool, shell, MCP, file, @@ -15,121 +15,89 @@ lifecycle observability additionally requires Cursor model traffic to route through the sidecar gateway if the active Cursor build exposes provider base URL configuration. -Cursor CLI support must be verified separately with `cursor-agent`. If CLI hooks -do not fire, treat Cursor CLI support as hook-limited and gateway-only where -model routing is configurable. - ## Files - `.cursor/hooks.json` contains hook entries that run `nemo-flow-sidecar hook-forward cursor`. -## Start The Sidecar +## Transparent Setup Build or install the sidecar binary so `nemo-flow-sidecar` is on `PATH`. -Start a local sidecar with ATIF export enabled: +Run Cursor through the wrapper: ```bash -NEMO_FLOW_ATIF_DIR=.nemo-flow/atif \ -nemo-flow-sidecar --bind 127.0.0.1:4040 +nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- cursor-agent ``` -Use custom upstreams when needed: - -```bash -nemo-flow-sidecar \ - --bind 127.0.0.1:4040 \ - --openai-base-url https://api.openai.com \ - --anthropic-base-url https://api.anthropic.com \ - --atif-dir .nemo-flow/atif -``` +The wrapper starts a per-invocation sidecar on a dynamic localhost port, +temporarily merges NeMo Flow hooks into project `.cursor/hooks.json`, launches +Cursor, and restores or removes the temporary hook file when Cursor exits. -## Install Hooks - -Inspect generated changes before writing: +Inspect the launch without starting Cursor: ```bash -nemo-flow-sidecar install cursor \ - --scope project \ - --target gui \ - --sidecar-url http://127.0.0.1:4040 \ +nemo-flow-sidecar run \ --atif-dir .nemo-flow/atif \ - --gateway-mode passthrough \ --dry-run \ - --print -``` - -Install for a project-local Cursor GUI or IDE session: - -```bash -nemo-flow-sidecar install cursor \ - --scope project \ - --target gui \ - --sidecar-url http://127.0.0.1:4040 \ - --atif-dir .nemo-flow/atif \ - --gateway-mode passthrough + --print \ + -- cursor-agent ``` -The installer merges NeMo Flow hook entries into `.cursor/hooks.json` and backs -up an existing file before writing. +## Shared Config -## Configure LLM Gateway +Use `.nemo-flow/sidecar.toml` for project defaults or +`~/.config/nemo-flow/sidecar.toml` for user defaults: -If Cursor exposes provider base URL configuration for the model path being used, -point OpenAI-compatible or Anthropic-compatible traffic at: +```toml +[session] +atif_dir = ".nemo-flow/atif" +metadata = { team = "agent-observability" } -```text -http://127.0.0.1:4040 +[agents.cursor] +command = "cursor-agent" +patch_restore_hooks = true ``` -The sidecar forwards OpenAI-compatible `/v1/responses`, -`/v1/chat/completions`, Anthropic-compatible `/v1/messages`, token-count, and -model routes without rewriting provider request or response JSON. +Then run: -Hook-only mode observes Cursor agent and tool lifecycle. Missing LLM spans are -expected when Cursor sends model traffic directly to the provider or through a -remote service. +```bash +nemo-flow-sidecar run --agent cursor +``` -## Smoke Test +## Persistent Setup -Verify the sidecar endpoint directly: +Use persistent hooks only when you do not want to launch Cursor through the +wrapper: ```bash -printf '{"session_id":"smoke-cursor","hook_event_name":"sessionStart"}' \ - | nemo-flow-sidecar hook-forward cursor --sidecar-url http://127.0.0.1:4040 +nemo-flow-sidecar install cursor \ + --scope project \ + --target gui \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif ``` -The command should print a Cursor-compatible continue response. - -Then run a small Cursor GUI session that starts an agent and uses one simple -tool. The sidecar should receive hook requests for session and tool lifecycle -events. +Then start the sidecar manually and point Cursor provider traffic at +`http://127.0.0.1:4040` where Cursor exposes provider base URL configuration. -For Cursor CLI, run an equivalent `cursor-agent` session and verify that the -sidecar receives hook requests. If no hook requests arrive, treat that CLI -version as hook-limited. +## Verify -## Verify ATIF Export - -End the Cursor session and confirm that ATIF was written: +Run a Cursor session that starts, uses one simple tool, and ends. Confirm that +ATIF was written: ```bash ls .nemo-flow/atif ``` -The sidecar writes `.atif.json` when it receives a session-end hook -for a session with ATIF enabled. - -## Troubleshooting +For a direct endpoint smoke test against a manually started sidecar: -If no hook events arrive, confirm Cursor loaded `.cursor/hooks.json`, -`nemo-flow-sidecar` is on `PATH`, and the sidecar is listening on the configured -URL. - -If hooks arrive but LLM spans are missing, confirm the active Cursor GUI or CLI -mode supports provider base URL configuration and points provider traffic to -`http://127.0.0.1:4040`. +```bash +curl -f http://127.0.0.1:4040/healthz +printf '{"session_id":"smoke-cursor","hook_event_name":"sessionStart"}' \ + | NEMO_FLOW_SIDECAR_URL=http://127.0.0.1:4040 nemo-flow-sidecar hook-forward cursor --fail-closed +``` -If ATIF is missing, confirm `--atif-dir` or `NEMO_FLOW_ATIF_DIR` is configured -and that the sidecar process can write to the directory. +If Cursor CLI hooks do not fire for the active `cursor-agent` version, treat +that CLI mode as hook-limited and rely on gateway observability where provider +routing is available. From c08743726cd13ad43e46b2722abcb41a66e37d90 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 6 May 2026 11:20:39 -0400 Subject: [PATCH 05/13] Fix sidecar review findings Signed-off-by: Will Killian --- .github/ci-path-filters.yml | 3 + .gitlab-ci.yml | 2 +- crates/sidecar/src/adapters/claude_code.rs | 14 +- crates/sidecar/src/adapters/codex.rs | 2 +- crates/sidecar/src/adapters/mod.rs | 28 +++- crates/sidecar/src/gateway.rs | 96 +++++++++---- crates/sidecar/src/installer.rs | 7 +- crates/sidecar/src/launcher.rs | 13 +- crates/sidecar/src/session.rs | 41 ++++-- .../sidecar/tests/coverage/adapters_tests.rs | 68 +++++++++- .../sidecar/tests/coverage/gateway_tests.rs | 67 +++++++++ .../sidecar/tests/coverage/launcher_tests.rs | 37 +++++ .../sidecar/tests/coverage/session_tests.rs | 128 ++++++++++++++++++ .../coding-agent-claude-code.md | 11 +- .../coding-agents/claude-code/README.md | 12 +- 15 files changed, 475 insertions(+), 54 deletions(-) diff --git a/.github/ci-path-filters.yml b/.github/ci-path-filters.yml index d99f816..016fe0a 100644 --- a/.github/ci-path-filters.yml +++ b/.github/ci-path-filters.yml @@ -12,6 +12,7 @@ shared: - 'crates/adaptive/src/**' - 'crates/core/Cargo.toml' - 'crates/core/src/**' + - 'integrations/coding-agents/**' - 'justfile' - 'rust-toolchain.toml' @@ -24,6 +25,7 @@ docs: - 'crates/node/*.js' - 'crates/**/*.md' - 'docs/**' + - 'integrations/coding-agents/**' - 'python/nemo_flow/**' - 'scripts/build-docs.sh' - 'scripts/docs/**' @@ -34,6 +36,7 @@ rust: - 'crates/**/Cargo.toml' - 'crates/**/*.rs' - 'crates/ffi/cbindgen.toml' + - 'integrations/coding-agents/**' go: - 'crates/ffi/Cargo.toml' diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1d3d2a1..8c54955 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -268,7 +268,7 @@ publish:artifactory:cargo: EOF chmod 600 "${cargo_home}/credentials.toml" - for crate in nemo-flow nemo-flow-adaptive nemo-flow-ffi; do + for crate in nemo-flow nemo-flow-adaptive nemo-flow-ffi nemo-flow-sidecar; do cargo publish --package "$crate" --registry artifactory --allow-dirty done diff --git a/crates/sidecar/src/adapters/claude_code.rs b/crates/sidecar/src/adapters/claude_code.rs index c5d6bd9..30346da 100644 --- a/crates/sidecar/src/adapters/claude_code.rs +++ b/crates/sidecar/src/adapters/claude_code.rs @@ -4,7 +4,7 @@ use axum::http::HeaderMap; use serde_json::{Value, json}; -use crate::adapters::{AdapterOutcome, ClassificationRules, classify}; +use crate::adapters::{AdapterOutcome, ClassificationRules, classify, event_name, normalize_name}; use crate::model::{AgentKind, NormalizedEvent}; pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { @@ -14,7 +14,7 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { &ClassificationRules { kind: AgentKind::ClaudeCode, agent_start: &["SessionStart", "sessionStart", "session_start"], - agent_end: &["SessionEnd", "sessionEnd", "session_end", "Stop", "stop"], + agent_end: &["SessionEnd", "sessionEnd", "session_end"], subagent_start: &["SubagentStart", "subagentStart"], subagent_end: &["SubagentStop", "subagentStop", "SubagentEnd"], tool_start: &["PreToolUse", "preToolUse"], @@ -25,9 +25,12 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { "postToolUseFailure", "ToolUseFailed", "toolUseFailed", + "PermissionDenied", + "permissionDenied", ], }, ); + let normalized_event = normalize_name(&event_name(&payload)); let response = match &event { NormalizedEvent::ToolStarted(_) => json!({ "continue": true, @@ -36,7 +39,12 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { "permissionDecision": "allow" } }), - NormalizedEvent::AgentEnded(_) => json!({ "continue": true, "stopReason": null }), + NormalizedEvent::AgentEnded(_) | NormalizedEvent::HookMark(_) + if normalized_event == "stop" => + { + json!({ "continue": true, "stopReason": null }) + } + NormalizedEvent::AgentEnded(_) => json!({ "continue": true }), _ => json!({ "continue": true }), }; AdapterOutcome { diff --git a/crates/sidecar/src/adapters/codex.rs b/crates/sidecar/src/adapters/codex.rs index 73846c8..ba98219 100644 --- a/crates/sidecar/src/adapters/codex.rs +++ b/crates/sidecar/src/adapters/codex.rs @@ -14,7 +14,7 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { &ClassificationRules { kind: AgentKind::Codex, agent_start: &["sessionStart", "session_start", "agentStarted"], - agent_end: &["sessionEnd", "session_end", "agentEnded", "stop"], + agent_end: &["sessionEnd", "session_end", "agentEnded"], subagent_start: &["subagentStart", "subagent_start"], subagent_end: &["subagentStop", "subagentEnd", "subagent_stop"], tool_start: &["preToolUse", "toolStarted", "tool_start"], diff --git a/crates/sidecar/src/adapters/mod.rs b/crates/sidecar/src/adapters/mod.rs index 4511c8d..055f208 100644 --- a/crates/sidecar/src/adapters/mod.rs +++ b/crates/sidecar/src/adapters/mod.rs @@ -62,6 +62,8 @@ fn metadata(payload: &Value, headers: &HeaderMap, kind: AgentKind, event_name: & ("project_dir", string_at(payload, &["project_dir"])), ("user_email", string_at(payload, &["user_email"])), ("model", string_at(payload, &["model"])), + ("agent_id", string_at(payload, &["agent_id"])), + ("agent_type", string_at(payload, &["agent_type"])), ] { if let Some(value) = value { object.insert(key.into(), json!(value)); @@ -98,6 +100,7 @@ fn common_subagent_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) fn common_tool_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> ToolEvent { let session = common_session_event(payload, headers, kind); + let normalized_event = normalize_name(&session.event_name); let tool_call_id = string_at(payload, &["tool_call_id"]) .or_else(|| string_at(payload, &["toolCallId"])) .or_else(|| string_at(payload, &["tool_use_id"])) @@ -121,8 +124,8 @@ fn common_tool_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> T .or_else(|| value_at(payload, &["tool_response"])) .or_else(|| value_at(payload, &["output"])) .or_else(|| value_at(payload, &["result"])) + .or_else(|| event_detail_result(payload, &normalized_event)) .unwrap_or(Value::Null); - let normalized_event = normalize_name(&session.event_name); ToolEvent { session_id: session.session_id, agent_kind: kind, @@ -139,6 +142,11 @@ fn common_tool_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> T .or_else(|| { (normalized_event.contains("failure") || normalized_event.contains("failed")) .then_some("error".to_string()) + }) + .or_else(|| { + normalized_event + .contains("permissiondenied") + .then_some("denied".to_string()) }), payload: session.payload, metadata: session.metadata, @@ -148,10 +156,28 @@ fn common_tool_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> T fn subagent_id(payload: &Value) -> Option { string_at(payload, &["subagent_id"]) .or_else(|| string_at(payload, &["subagentId"])) + .or_else(|| string_at(payload, &["agent_id"])) .or_else(|| string_at(payload, &["subagent", "id"])) .or_else(|| string_at(payload, &["agent", "id"])) } +fn event_detail_result(payload: &Value, normalized_event: &str) -> Option { + let include_details = normalized_event.contains("failure") + || normalized_event.contains("failed") + || normalized_event.contains("permissiondenied"); + if !include_details { + return None; + } + + let mut object = Map::new(); + for key in ["error", "reason", "is_interrupt", "duration_ms"] { + if let Some(value) = value_at(payload, &[key]) { + object.insert(key.into(), value); + } + } + (!object.is_empty()).then_some(Value::Object(object)) +} + fn string_at(payload: &Value, path: &[&str]) -> Option { value_at(payload, path).and_then(|value| match value { Value::String(value) => Some(value), diff --git a/crates/sidecar/src/gateway.rs b/crates/sidecar/src/gateway.rs index bec17ab..f2a4115 100644 --- a/crates/sidecar/src/gateway.rs +++ b/crates/sidecar/src/gateway.rs @@ -11,7 +11,7 @@ use serde_json::{Map, Value, json}; use crate::config::header_string; use crate::error::SidecarError; use crate::server::AppState; -use crate::session::LlmGatewayStart; +use crate::session::{ActiveLlm, LlmGatewayStart, SessionManager}; pub(crate) async fn passthrough( State(state): State, @@ -94,11 +94,10 @@ pub(crate) async fn passthrough( let is_stream = streaming || content_type.contains("text/event-stream"); if is_stream { - let sessions = state.sessions.clone(); let stream = upstream_response.bytes_stream(); let body = Body::from_stream(async_stream::stream! { let mut stream = stream; - let mut active = Some(active); + let mut llm = StreamingLlmGuard::new(state.sessions.clone(), active, status); let mut collected = Vec::new(); let mut truncated = false; while let Some(chunk) = stream.next().await { @@ -112,30 +111,14 @@ pub(crate) async fn passthrough( yield Ok::(bytes); } Err(error) => { - if let Some(active) = active.take() { - let _ = sessions - .end_llm( - active, - json!({ "error": error.to_string() }), - json!({ "http_status": status.as_u16(), "streaming": true, "gateway_error": true, "stage": "stream" }), - ) - .await; - } + llm.end_error("stream", error.to_string()).await; yield Err(error); return; } } } let response = stream_response_json(&collected, truncated); - if let Some(active) = active.take() { - let _ = sessions - .end_llm( - active, - response, - json!({ "http_status": status.as_u16(), "streaming": true, "stream_truncated": truncated }), - ) - .await; - } + llm.end_success(response, truncated).await; }); return build_response(status, headers, body); } @@ -167,6 +150,69 @@ pub(crate) async fn passthrough( build_response(status, headers, Body::from(bytes)) } +struct StreamingLlmGuard { + sessions: SessionManager, + active: Option, + status: StatusCode, +} + +impl StreamingLlmGuard { + fn new(sessions: SessionManager, active: ActiveLlm, status: StatusCode) -> Self { + Self { + sessions, + active: Some(active), + status, + } + } + + async fn end_success(&mut self, response: Value, truncated: bool) { + if let Some(active) = self.active.take() { + let _ = self + .sessions + .end_llm( + active, + response, + json!({ "http_status": self.status.as_u16(), "streaming": true, "stream_truncated": truncated }), + ) + .await; + } + } + + async fn end_error(&mut self, stage: &'static str, error: String) { + if let Some(active) = self.active.take() { + let _ = self + .sessions + .end_llm( + active, + json!({ "error": error }), + json!({ "http_status": self.status.as_u16(), "streaming": true, "gateway_error": true, "stage": stage }), + ) + .await; + } + } +} + +impl Drop for StreamingLlmGuard { + fn drop(&mut self) { + let Some(active) = self.active.take() else { + return; + }; + let sessions = self.sessions.clone(); + let status = self.status; + if let Ok(handle) = tokio::runtime::Handle::try_current() { + handle.spawn(async move { + let _ = sessions + .end_llm( + active, + json!({ "error": "stream body dropped before completion" }), + json!({ "http_status": status.as_u16(), "streaming": true, "gateway_error": true, "stage": "client_drop" }), + ) + .await; + }); + } + } +} + pub(crate) async fn models( State(state): State, request: Request, @@ -266,7 +312,7 @@ fn response_headers(headers: &HeaderMap) -> HeaderMap { let mut output = HeaderMap::new(); for (name, value) in headers { if !is_hop_by_hop(name) { - output.insert(name.clone(), value.clone()); + output.append(name.clone(), value.clone()); } } output @@ -278,10 +324,8 @@ fn build_response( body: Body, ) -> Result, SidecarError> { let mut builder = Response::builder().status(status); - for (name, value) in headers { - if let Some(name) = name { - builder = builder.header(name, value); - } + for (name, value) in &headers { + builder = builder.header(name, value); } Ok(builder.body(body)?) } diff --git a/crates/sidecar/src/installer.rs b/crates/sidecar/src/installer.rs index 1dd6f29..dfa15fb 100644 --- a/crates/sidecar/src/installer.rs +++ b/crates/sidecar/src/installer.rs @@ -3,7 +3,7 @@ use std::io::Read; use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue}; use serde_json::{Value, json}; @@ -43,6 +43,7 @@ const CURSOR_HOOK_EVENTS: &[&str] = &[ "stop", "sessionEnd", ]; +const HOOK_FORWARD_TIMEOUT: Duration = Duration::from_secs(2); #[derive(Debug, Clone)] struct PlannedFile { @@ -110,7 +111,9 @@ pub(crate) async fn hook_forward(command: HookForwardCommand) -> Result<(), Side sidecar_url.trim_end_matches('/'), command.agent.hook_path() ); - let response = reqwest::Client::new() + let response = reqwest::Client::builder() + .timeout(HOOK_FORWARD_TIMEOUT) + .build()? .post(url) .headers(sidecar_headers( command.atif_dir.as_deref(), diff --git a/crates/sidecar/src/launcher.rs b/crates/sidecar/src/launcher.rs index 01ce0f4..6c0892f 100644 --- a/crates/sidecar/src/launcher.rs +++ b/crates/sidecar/src/launcher.rs @@ -228,7 +228,7 @@ impl PreparedRun { } fn prepare_cursor(&mut self) -> Result<(), SidecarError> { - let path = std::env::current_dir()?.join(".cursor/hooks.json"); + let path = cursor_hooks_path()?; let had_original = path.exists(); let backup_path = if had_original { let backup = path.with_extension(format!("json.nemo-flow-run.bak.{}", timestamp()?)); @@ -258,7 +258,7 @@ impl PreparedRun { } fn prepare_cursor_dry(&mut self) -> Result<(), SidecarError> { - let path = std::env::current_dir()?.join(".cursor/hooks.json"); + let path = cursor_hooks_path()?; self.notes.push(format!( "would temporarily merge NeMo Flow hooks into {}", path.display() @@ -402,6 +402,15 @@ fn temp_dir(prefix: &str) -> Result { Ok(path) } +fn cursor_hooks_path() -> Result { + let cwd = std::env::current_dir()?; + let project = cwd + .ancestors() + .find(|ancestor| ancestor.join(".cursor").is_dir()) + .unwrap_or(cwd.as_path()); + Ok(project.join(".cursor/hooks.json")) +} + fn timestamp() -> Result { Ok(SystemTime::now() .duration_since(UNIX_EPOCH) diff --git a/crates/sidecar/src/session.rs b/crates/sidecar/src/session.rs index 6293a3b..cb20833 100644 --- a/crates/sidecar/src/session.rs +++ b/crates/sidecar/src/session.rs @@ -55,6 +55,7 @@ struct Session { scope_stack: ScopeStackHandle, agent_scope: Option, subagents: HashMap, + subagent_stack: Vec, tools: HashMap, config: SessionConfig, atif: Option, @@ -84,6 +85,7 @@ impl SessionManager { session.apply(event).await?; if session.agent_scope.is_none() && session.subagents.is_empty() + && session.subagent_stack.is_empty() && session.tools.is_empty() { sessions.remove(&session_id); @@ -139,6 +141,7 @@ impl Session { scope_stack: create_scope_stack(), agent_scope: None, subagents: HashMap::new(), + subagent_stack: Vec::new(), tools: HashMap::new(), config, atif: None, @@ -269,15 +272,17 @@ impl Session { .build(), )?; } - let active_subagents: Vec<_> = self.subagents.drain().map(|(_, handle)| handle).collect(); - for handle in active_subagents.into_iter().rev() { - let _ = pop_scope( - PopScopeParams::builder() - .handle_uuid(&handle.uuid) - .output(json!({ "status": "closed_by_agent_end" })) - .build(), - ); + while let Some(subagent_id) = self.subagent_stack.pop() { + if let Some(handle) = self.subagents.remove(&subagent_id) { + pop_scope( + PopScopeParams::builder() + .handle_uuid(&handle.uuid) + .output(json!({ "status": "closed_by_agent_end" })) + .build(), + )?; + } } + self.subagents.clear(); if let Some(scope) = self.agent_scope.take() { pop_scope( PopScopeParams::builder() @@ -303,13 +308,14 @@ impl Session { .input(event.payload) .build(), )?; + self.subagent_stack.push(event.subagent_id.clone()); self.subagents.insert(event.subagent_id, scope); Ok(()) } fn end_subagent(&mut self, event: SubagentEvent) -> Result<(), SidecarError> { self.ensure_agent_started(event.metadata.clone())?; - let Some(scope) = self.subagents.remove(&event.subagent_id) else { + let Some(scope) = self.subagents.get(&event.subagent_id).cloned() else { return self.mark( "subagent_end_without_start", SessionEvent { @@ -321,6 +327,16 @@ impl Session { }, ); }; + if self.subagent_stack.last() != Some(&event.subagent_id) { + return emit_mark_event( + EmitMarkEventParams::builder() + .name("subagent_end_not_top") + .data(event.payload) + .metadata(event.metadata) + .build(), + ) + .map_err(SidecarError::from); + } if pop_scope( PopScopeParams::builder() .handle_uuid(&scope.uuid) @@ -329,14 +345,17 @@ impl Session { ) .is_err() { - emit_mark_event( + return emit_mark_event( EmitMarkEventParams::builder() .name("subagent_end_not_top") .data(event.payload) .metadata(event.metadata) .build(), - )?; + ) + .map_err(SidecarError::from); } + self.subagent_stack.pop(); + self.subagents.remove(&event.subagent_id); Ok(()) } diff --git a/crates/sidecar/tests/coverage/adapters_tests.rs b/crates/sidecar/tests/coverage/adapters_tests.rs index 970a774..841ab65 100644 --- a/crates/sidecar/tests/coverage/adapters_tests.rs +++ b/crates/sidecar/tests/coverage/adapters_tests.rs @@ -55,7 +55,9 @@ fn maps_claude_post_tool_failure_with_canonical_fields() { "tool_use_id": "toolu-1", "tool_name": "Bash", "tool_input": { "command": "false" }, - "tool_response": { "stderr": "failed" } + "error": "failed", + "is_interrupt": false, + "duration_ms": 12 }), &headers, ); @@ -64,13 +66,63 @@ fn maps_claude_post_tool_failure_with_canonical_fields() { NormalizedEvent::ToolEnded(event) => { assert_eq!(event.tool_call_id, "toolu-1"); assert_eq!(event.tool_name, "Bash"); - assert_eq!(event.result, json!({ "stderr": "failed" })); + assert_eq!( + event.result, + json!({ "error": "failed", "is_interrupt": false, "duration_ms": 12 }) + ); assert_eq!(event.status.as_deref(), Some("error")); } event => panic!("unexpected event: {event:?}"), } } +#[test] +fn maps_claude_permission_denied_as_tool_end() { + let headers = HeaderMap::new(); + let outcome = claude_code::adapt( + json!({ + "session_id": "claude-session", + "hook_event_name": "PermissionDenied", + "tool_use_id": "toolu-denied", + "tool_name": "Bash", + "tool_input": { "command": "rm -rf /tmp/project" }, + "reason": "policy" + }), + &headers, + ); + + match &outcome.events[0] { + NormalizedEvent::ToolEnded(event) => { + assert_eq!(event.tool_call_id, "toolu-denied"); + assert_eq!(event.status.as_deref(), Some("denied")); + assert_eq!(event.result, json!({ "reason": "policy" })); + } + event => panic!("unexpected event: {event:?}"), + } +} + +#[test] +fn maps_claude_subagent_canonical_agent_id() { + let headers = HeaderMap::new(); + let outcome = claude_code::adapt( + json!({ + "session_id": "claude-session", + "hook_event_name": "SubagentStart", + "agent_id": "agent-worker-1", + "agent_type": "general-purpose" + }), + &headers, + ); + + match &outcome.events[0] { + NormalizedEvent::SubagentStarted(event) => { + assert_eq!(event.subagent_id, "agent-worker-1"); + assert_eq!(event.metadata["agent_type"], json!("general-purpose")); + } + event => panic!("unexpected event: {event:?}"), + } +} + #[test] fn maps_cursor_subagent_and_permission_response() { let headers = HeaderMap::new(); @@ -214,9 +266,19 @@ fn stop_responses_preserve_vendor_shapes() { }), &headers, ); - assert!(matches!(claude.events[0], NormalizedEvent::AgentEnded(_))); + assert!(matches!(claude.events[0], NormalizedEvent::HookMark(_))); assert_eq!(claude.response["stopReason"], Value::Null); + let codex = codex::adapt( + json!({ + "session_id": "codex-session", + "hook_event_name": "stop" + }), + &headers, + ); + assert!(matches!(codex.events[0], NormalizedEvent::HookMark(_))); + assert_eq!(codex.response, json!({})); + let cursor = cursor::adapt( json!({ "session_id": "cursor-session", diff --git a/crates/sidecar/tests/coverage/gateway_tests.rs b/crates/sidecar/tests/coverage/gateway_tests.rs index 7d3102d..8106b7f 100644 --- a/crates/sidecar/tests/coverage/gateway_tests.rs +++ b/crates/sidecar/tests/coverage/gateway_tests.rs @@ -3,6 +3,7 @@ use super::*; use crate::config::SidecarConfig; +use crate::model::{AgentKind, NormalizedEvent, SessionEvent}; use axum::http::{HeaderMap, HeaderValue}; #[test] @@ -115,6 +116,17 @@ fn observable_headers_omit_secrets_and_transport_headers() { assert!(!observed.contains_key("connection")); } +#[test] +fn response_headers_preserve_duplicates() { + let mut headers = HeaderMap::new(); + headers.append("set-cookie", HeaderValue::from_static("a=1")); + headers.append("set-cookie", HeaderValue::from_static("b=2")); + + let copied = response_headers(&headers); + + assert_eq!(copied.get_all("set-cookie").iter().count(), 2); +} + #[test] fn stream_response_records_preview_and_truncation() { assert_eq!( @@ -126,3 +138,58 @@ fn stream_response_records_preview_and_truncation() { json!({ "stream_preview": "partial", "stream_truncated": true }) ); } + +#[tokio::test] +async fn streaming_llm_guard_closes_on_drop() { + let temp = tempfile::tempdir().unwrap(); + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://openai".into(), + anthropic_base_url: "http://anthropic".into(), + atif_dir: Some(temp.path().to_path_buf()), + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let sessions = SessionManager::new(config); + let active = sessions + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: Some("drop-session".into()), + provider: "openai.responses".into(), + model_name: Some("gpt-test".into()), + request: LlmRequest { + headers: Map::new(), + content: json!({ "model": "gpt-test", "stream": true }), + }, + streaming: true, + metadata: json!({ "gateway_path": "/v1/responses" }), + }, + ) + .await + .unwrap(); + + drop(StreamingLlmGuard::new( + sessions.clone(), + active, + StatusCode::OK, + )); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + sessions + .apply_events( + &HeaderMap::new(), + vec![NormalizedEvent::AgentEnded(SessionEvent { + session_id: "drop-session".into(), + agent_kind: AgentKind::Gateway, + event_name: "SessionEnd".into(), + payload: json!({}), + metadata: json!({}), + })], + ) + .await + .unwrap(); + + let atif = std::fs::read_to_string(temp.path().join("drop-session.atif.json")).unwrap(); + assert!(atif.contains("stream body dropped before completion")); +} diff --git a/crates/sidecar/tests/coverage/launcher_tests.rs b/crates/sidecar/tests/coverage/launcher_tests.rs index d47d440..4afbeb0 100644 --- a/crates/sidecar/tests/coverage/launcher_tests.rs +++ b/crates/sidecar/tests/coverage/launcher_tests.rs @@ -189,6 +189,43 @@ fn cursor_patch_restore_restores_original_file() { std::env::set_current_dir(previous).unwrap(); } +#[test] +fn cursor_patch_restore_uses_nearest_project_cursor_dir() { + let _guard = current_dir_lock().lock().unwrap(); + let temp = tempfile::tempdir().unwrap(); + let previous = std::env::current_dir().unwrap(); + std::fs::create_dir_all(temp.path().join(".cursor")).unwrap(); + std::fs::create_dir_all(temp.path().join("nested")).unwrap(); + std::fs::write( + temp.path().join(".cursor/hooks.json"), + r#"{"hooks":{"sessionStart":[]}}"#, + ) + .unwrap(); + std::env::set_current_dir(temp.path().join("nested")).unwrap(); + let resolved = ResolvedConfig { + sidecar: SidecarConfig::default(), + agents: AgentConfigs::default(), + }; + + let prepared = PreparedRun::new( + CodingAgent::Cursor, + vec!["cursor-agent".into()], + "http://s", + &resolved, + false, + ) + .unwrap(); + + assert!( + std::fs::read_to_string(temp.path().join(".cursor/hooks.json")) + .unwrap() + .contains("hook-forward cursor") + ); + assert!(!Path::new(".cursor/hooks.json").exists()); + prepared.restore().unwrap(); + std::env::set_current_dir(previous).unwrap(); +} + #[test] fn cursor_patch_restore_removes_temporary_file() { let _guard = current_dir_lock().lock().unwrap(); diff --git a/crates/sidecar/tests/coverage/session_tests.rs b/crates/sidecar/tests/coverage/session_tests.rs index 828b81a..9e0656b 100644 --- a/crates/sidecar/tests/coverage/session_tests.rs +++ b/crates/sidecar/tests/coverage/session_tests.rs @@ -195,6 +195,134 @@ async fn handles_out_of_order_subagent_and_tool_end_events() { assert!(manager.inner.lock().await.is_empty()); } +#[tokio::test] +async fn out_of_order_started_subagent_end_does_not_leak_scope() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + let headers = HeaderMap::new(); + + manager + .apply_events( + &headers, + vec![ + NormalizedEvent::AgentStarted(SessionEvent { + session_id: "nested".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SessionStart".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "nested".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "parent".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "nested".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "child".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentEnded(SubagentEvent { + session_id: "nested".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStop".into(), + subagent_id: "parent".into(), + payload: json!({ "out_of_order": true }), + metadata: json!({}), + }), + NormalizedEvent::SubagentEnded(SubagentEvent { + session_id: "nested".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStop".into(), + subagent_id: "child".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::AgentEnded(SessionEvent { + session_id: "nested".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SessionEnd".into(), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + assert!(manager.inner.lock().await.is_empty()); +} + +#[tokio::test] +async fn agent_end_closes_nested_active_subagents_lifo() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + let headers = HeaderMap::new(); + + manager + .apply_events( + &headers, + vec![ + NormalizedEvent::AgentStarted(SessionEvent { + session_id: "cleanup".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SessionStart".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "cleanup".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "parent".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "cleanup".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "child".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::AgentEnded(SessionEvent { + session_id: "cleanup".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SessionEnd".into(), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + assert!(manager.inner.lock().await.is_empty()); +} + #[tokio::test] async fn llm_lifecycle_starts_implicit_gateway_session() { let config = SidecarConfig { diff --git a/docs/integrate-frameworks/coding-agent-claude-code.md b/docs/integrate-frameworks/coding-agent-claude-code.md index 63c16a3..8995b74 100644 --- a/docs/integrate-frameworks/coding-agent-claude-code.md +++ b/docs/integrate-frameworks/coding-agent-claude-code.md @@ -73,14 +73,19 @@ nemo-flow-sidecar install claude-code \ --atif-dir .nemo-flow/atif ``` -Then set `ANTHROPIC_BASE_URL` in the Claude Code process environment and start the -sidecar manually in another terminal: +Then start the sidecar manually: ```bash -export ANTHROPIC_BASE_URL=http://127.0.0.1:4040 NEMO_FLOW_ATIF_DIR=.nemo-flow/atif nemo-flow-sidecar --bind 127.0.0.1:4040 ``` +Launch Claude Code from another terminal with the gateway environment: + +```bash +export ANTHROPIC_BASE_URL=http://127.0.0.1:4040 +claude +``` + The sidecar forwards Anthropic `/v1/messages`, `/v1/messages/count_tokens`, and model routes without rewriting provider JSON. diff --git a/integrations/coding-agents/claude-code/README.md b/integrations/coding-agents/claude-code/README.md index 5f1f14f..39ba86f 100644 --- a/integrations/coding-agents/claude-code/README.md +++ b/integrations/coding-agents/claude-code/README.md @@ -75,11 +75,21 @@ nemo-flow-sidecar install claude-code \ --target cli \ --sidecar-url http://127.0.0.1:4040 \ --atif-dir .nemo-flow/atif +``` -export ANTHROPIC_BASE_URL=http://127.0.0.1:4040 +Start the sidecar in one terminal: + +```bash NEMO_FLOW_ATIF_DIR=.nemo-flow/atif nemo-flow-sidecar --bind 127.0.0.1:4040 ``` +Launch Claude Code from another terminal with the gateway environment: + +```bash +export ANTHROPIC_BASE_URL=http://127.0.0.1:4040 +claude +``` + ## Verify Run a Claude Code session that starts, uses one simple tool, and ends. Confirm From 841d5837f0d9fb1101391206f8e2290fe39efa69 Mon Sep 17 00:00:00 2001 From: Bryan Bednarski Date: Wed, 6 May 2026 10:22:37 -0700 Subject: [PATCH 06/13] Feature additions: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit   - Added Hermes as a first-class sidecar agent:     --agent hermes, config support, command inference for hermes / hermes-agent, and AgentKind::Hermes.   - Added a dedicated Hermes hook endpoint:     /hooks/hermes now routes Hermes shell-hook payloads into the sidecar session manager.   - Added a Hermes adapter:     Maps Hermes lifecycle, tool, and subagent shell hooks into normalized NeMo Flow sidecar events.   - Added Hermes installer support:     nemo-flow-sidecar install hermes writes/merges .hermes/config.yaml hook config using YAML instead of JSON/TOML.   - Added dynamic sidecar URL handling for Hermes:     Hermes installed hooks prefer runtime NEMO_FLOW_SIDECAR_URL, so nemo-flow-sidecar run --agent hermes can use an ephemeral sidecar URL without reinstalling hooks.   - Preserved Hermes hook consent semantics:     The runner exports the dynamic sidecar URL but does not auto-enable HERMES_ACCEPT_HOOKS.   - Improved ATIF LLM request fidelity:     ATIF user steps now preserve the full unwrapped LLM request payload under extra.llm_request.   - Improved ATIF token metrics extraction:     Supports provider-native usage objects in addition to NeMo Flow token_usage, including cached-token variants from OpenAI/Anthropic-style payloads.   - Expanded shared adapter extraction:     Adds support for Hermes-friendly payload fields like task_id, parent_session_id, extra.tool_call_id, and extra.result.   - Added test coverage:     New/updated tests cover Hermes adapter mapping, config parsing, installer generation/merge behavior, runtime launcher behavior, server hook response shape, and ATIF request/usage export. Signed-off-by: Bryan Bednarski Signed-off-by: Will Killian --- Cargo.lock | 20 +++++ crates/core/src/observability/atif.rs | 58 +++++++++++-- crates/core/tests/unit/atif_tests.rs | 59 ++++++++++++- crates/sidecar/Cargo.toml | 1 + crates/sidecar/src/adapters/hermes.rs | 28 +++++++ crates/sidecar/src/adapters/mod.rs | 23 ++++-- crates/sidecar/src/config.rs | 9 ++ crates/sidecar/src/installer.rs | 82 +++++++++++++++++-- crates/sidecar/src/launcher.rs | 10 ++- crates/sidecar/src/model.rs | 2 + crates/sidecar/src/server.rs | 16 +++- .../sidecar/tests/coverage/adapters_tests.rs | 58 ++++++++++++- crates/sidecar/tests/coverage/config_tests.rs | 9 ++ .../sidecar/tests/coverage/installer_tests.rs | 72 ++++++++++++++++ .../sidecar/tests/coverage/launcher_tests.rs | 57 +++++++++++++ crates/sidecar/tests/coverage/server_tests.rs | 26 ++++++ 16 files changed, 509 insertions(+), 21 deletions(-) create mode 100644 crates/sidecar/src/adapters/hermes.rs diff --git a/Cargo.lock b/Cargo.lock index 7070a48..61cb840 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1298,6 +1298,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serde_yaml", "tempfile", "thiserror", "tokio", @@ -2162,6 +2163,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1_smol" version = "1.0.1" @@ -2623,6 +2637,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/crates/core/src/observability/atif.rs b/crates/core/src/observability/atif.rs index 46033fb..9f2ca0d 100644 --- a/crates/core/src/observability/atif.rs +++ b/crates/core/src/observability/atif.rs @@ -233,6 +233,9 @@ pub struct AtifStepExtra { /// Step-level invocation timing. #[serde(skip_serializing_if = "Option::is_none")] pub invocation: Option, + /// Full unwrapped LLM request payload for request-level fidelity. + #[serde(skip_serializing_if = "Option::is_none")] + pub llm_request: Option, /// Per-tool callable lineage, aligned with `tool_calls`. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tool_ancestry: Vec, @@ -427,13 +430,21 @@ const TOKEN_USAGE_KNOWN_KEYS: &[&str] = &[ /// Try to extract `AtifMetrics` from a `token_usage` object in the LLM response. /// -/// Populates `extra` with any unknown token_usage keys (e.g. reasoning_tokens). -/// Returns `None` if the response has no `token_usage` or it is not an object. +/// Supports NeMo Flow `token_usage` and provider-native `usage` payloads. +/// Populates `extra` with any unknown usage keys (e.g. reasoning_tokens or total_tokens). +/// Returns `None` if the response has no recognized token counts. fn extract_metrics(output: &Json) -> Option { - let usage = output.as_object()?.get("token_usage")?.as_object()?; - let prompt = usage.get("prompt_tokens").and_then(Json::as_u64); - let completion = usage.get("completion_tokens").and_then(Json::as_u64); - let cached = usage.get("cached_tokens").and_then(Json::as_u64); + let usage = token_usage_object(output)?; + let prompt = usage_u64(usage, &["prompt_tokens", "input_tokens"]); + let completion = usage_u64(usage, &["completion_tokens", "output_tokens"]); + let cached = usage_u64(usage, &["cached_tokens"]) + .or_else(|| prompt_tokens_detail_u64(usage, "cached_tokens")) + .or_else(|| { + sum_usage_u64( + usage, + &["cache_read_input_tokens", "cache_creation_input_tokens"], + ) + }); let cost = usage.get("cost_usd").and_then(Json::as_f64); let prompt_ids = usage .get("prompt_token_ids") @@ -473,6 +484,39 @@ fn extract_metrics(output: &Json) -> Option { }) } +fn token_usage_object(output: &Json) -> Option<&serde_json::Map> { + let output = output.as_object()?; + output + .get("token_usage") + .or_else(|| output.get("usage")) + .and_then(Json::as_object) +} + +fn usage_u64(usage: &serde_json::Map, keys: &[&str]) -> Option { + keys.iter() + .find_map(|key| usage.get(*key).and_then(Json::as_u64)) +} + +fn sum_usage_u64(usage: &serde_json::Map, keys: &[&str]) -> Option { + let mut total = 0; + let mut found = false; + for key in keys { + if let Some(value) = usage.get(*key).and_then(Json::as_u64) { + total += value; + found = true; + } + } + found.then_some(total) +} + +fn prompt_tokens_detail_u64(usage: &serde_json::Map, key: &str) -> Option { + usage + .get("prompt_tokens_details") + .and_then(Json::as_object) + .and_then(|details| details.get(key)) + .and_then(Json::as_u64) +} + /// Extract `reasoning_effort` from an LLM request (string or number). /// /// The request content may have `reasoning_effort` (e.g. `"high"`, `"medium"`, @@ -696,6 +740,7 @@ impl PendingAgentStep { let extra = AtifStepExtra { ancestry, invocation: self.invocation.take(), + llm_request: None, tool_ancestry: std::mem::take(&mut self.tool_ancestry), tool_invocations: if self.tool_invocations.is_empty() { None @@ -803,6 +848,7 @@ impl StepConversionState { let extra = AtifStepExtra { ancestry: build_ancestry(event, &lookups.name_map), invocation: None, + llm_request: Some(content.clone()), tool_ancestry: Vec::new(), tool_invocations: None, }; diff --git a/crates/core/tests/unit/atif_tests.rs b/crates/core/tests/unit/atif_tests.rs index 795714e..a4e9d68 100644 --- a/crates/core/tests/unit/atif_tests.rs +++ b/crates/core/tests/unit/atif_tests.rs @@ -310,7 +310,22 @@ fn test_exporter_llm_lifecycle() { .name("gpt-4") .scope_type(ScopeType::Llm) .input(json!({ - "content": {"messages": [{"role": "user", "content": "hello"}]}, + "content": { + "messages": [{"role": "user", "content": "hello"}], + "temperature": 0.1, + "tools": [{ + "type": "function", + "function": { + "name": "read_file", + "parameters": { + "type": "object", + "properties": { + "path": { "type": "string" } + } + } + } + }] + }, "headers": {} })) .model_name("gpt-4") @@ -349,6 +364,13 @@ fn test_exporter_llm_lifecycle() { // extract_user_messages pulls out just the messages array assert_eq!(step1.message, json!([{"role": "user", "content": "hello"}])); assert_eq!(step1.model_name, None); + let extra: AtifStepExtra = serde_json::from_value(step1.extra.clone().unwrap()).unwrap(); + let llm_request = extra.llm_request.unwrap(); + assert_eq!(llm_request["temperature"], json!(0.1)); + assert_eq!( + llm_request["tools"][0]["function"]["name"], + json!("read_file") + ); // Second step: agent (LLM end with extracted content + metrics) let step2 = &trajectory.steps[1]; @@ -370,6 +392,41 @@ fn test_exporter_llm_lifecycle() { assert_eq!(fm.total_steps, Some(2)); } +#[test] +fn test_extract_metrics_supports_provider_usage_payloads() { + let openai_metrics = extract_metrics(&json!({ + "usage": { + "prompt_tokens": 10, + "completion_tokens": 20, + "total_tokens": 30, + "prompt_tokens_details": { + "cached_tokens": 4 + } + } + })) + .unwrap(); + assert_eq!(openai_metrics.prompt_tokens, Some(10)); + assert_eq!(openai_metrics.completion_tokens, Some(20)); + assert_eq!(openai_metrics.cached_tokens, Some(4)); + assert_eq!( + openai_metrics.extra.as_ref().unwrap()["total_tokens"], + json!(30) + ); + + let anthropic_metrics = extract_metrics(&json!({ + "usage": { + "input_tokens": 11, + "output_tokens": 22, + "cache_read_input_tokens": 3, + "cache_creation_input_tokens": 5 + } + })) + .unwrap(); + assert_eq!(anthropic_metrics.prompt_tokens, Some(11)); + assert_eq!(anthropic_metrics.completion_tokens, Some(22)); + assert_eq!(anthropic_metrics.cached_tokens, Some(8)); +} + #[test] fn test_exporter_llm_lifecycle_plain_input() { // Input without LlmRequest envelope — passed through unchanged. diff --git a/crates/sidecar/Cargo.toml b/crates/sidecar/Cargo.toml index 0020907..1c6a566 100644 --- a/crates/sidecar/Cargo.toml +++ b/crates/sidecar/Cargo.toml @@ -24,6 +24,7 @@ http-body-util = "0.1" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots", "stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_yaml = "0.9" thiserror = "2" tokio = { version = "1", features = ["macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"] } toml = "0.9" diff --git a/crates/sidecar/src/adapters/hermes.rs b/crates/sidecar/src/adapters/hermes.rs new file mode 100644 index 0000000..59b4d46 --- /dev/null +++ b/crates/sidecar/src/adapters/hermes.rs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use axum::http::HeaderMap; +use serde_json::{Value, json}; + +use crate::adapters::{AdapterOutcome, ClassificationRules, classify}; +use crate::model::AgentKind; + +pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { + let event = classify( + &payload, + headers, + &ClassificationRules { + kind: AgentKind::Hermes, + agent_start: &["on_session_start", "sessionStart"], + agent_end: &["on_session_finalize", "on_session_reset"], + subagent_start: &["subagent_start", "subagentStart"], + subagent_end: &["subagent_stop", "subagentStop"], + tool_start: &["pre_tool_call", "preToolCall"], + tool_end: &["post_tool_call", "postToolCall"], + }, + ); + AdapterOutcome { + events: vec![event], + response: json!({}), + } +} diff --git a/crates/sidecar/src/adapters/mod.rs b/crates/sidecar/src/adapters/mod.rs index 055f208..423c63c 100644 --- a/crates/sidecar/src/adapters/mod.rs +++ b/crates/sidecar/src/adapters/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod claude_code; pub(crate) mod codex; pub(crate) mod cursor; +pub(crate) mod hermes; use axum::http::HeaderMap; use serde_json::{Map, Value, json}; @@ -36,6 +37,10 @@ fn session_id(payload: &Value, headers: &HeaderMap) -> String { .or_else(|| string_at(payload, &["session", "id"])) .or_else(|| string_at(payload, &["conversation_id"])) .or_else(|| string_at(payload, &["conversationId"])) + .or_else(|| string_at(payload, &["parent_session_id"])) + .or_else(|| string_at(payload, &["task_id"])) + .or_else(|| string_at(payload, &["extra", "session_id"])) + .or_else(|| string_at(payload, &["extra", "task_id"])) .unwrap_or_else(|| format!("hook-{}", Uuid::now_v7())) } @@ -105,6 +110,8 @@ fn common_tool_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> T .or_else(|| string_at(payload, &["toolCallId"])) .or_else(|| string_at(payload, &["tool_use_id"])) .or_else(|| string_at(payload, &["call_id"])) + .or_else(|| string_at(payload, &["extra", "tool_call_id"])) + .or_else(|| string_at(payload, &["extra", "call_id"])) .or_else(|| string_at(payload, &["tool", "id"])) .or_else(|| string_at(payload, &["tool_input", "id"])) .or_else(|| string_at(payload, &["id"])) @@ -124,6 +131,8 @@ fn common_tool_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> T .or_else(|| value_at(payload, &["tool_response"])) .or_else(|| value_at(payload, &["output"])) .or_else(|| value_at(payload, &["result"])) + .or_else(|| value_at(payload, &["extra", "tool_output"])) + .or_else(|| value_at(payload, &["extra", "result"])) .or_else(|| event_detail_result(payload, &normalized_event)) .unwrap_or(Value::Null); ToolEvent { @@ -179,12 +188,14 @@ fn event_detail_result(payload: &Value, normalized_event: &str) -> Option } fn string_at(payload: &Value, path: &[&str]) -> Option { - value_at(payload, path).and_then(|value| match value { - Value::String(value) => Some(value), - Value::Number(value) => Some(value.to_string()), - Value::Bool(value) => Some(value.to_string()), - _ => None, - }) + value_at(payload, path) + .and_then(|value| match value { + Value::String(value) => Some(value), + Value::Number(value) => Some(value.to_string()), + Value::Bool(value) => Some(value.to_string()), + _ => None, + }) + .filter(|value| !value.is_empty()) } fn value_at(payload: &Value, path: &[&str]) -> Option { diff --git a/crates/sidecar/src/config.rs b/crates/sidecar/src/config.rs index af2b74e..42dcb87 100644 --- a/crates/sidecar/src/config.rs +++ b/crates/sidecar/src/config.rs @@ -141,6 +141,7 @@ pub(crate) enum CodingAgent { ClaudeCode, Codex, Cursor, + Hermes, } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] @@ -211,6 +212,7 @@ pub(crate) struct AgentConfigs { pub(crate) claude_code: AgentCommandConfig, pub(crate) codex: AgentCommandConfig, pub(crate) cursor: CursorAgentConfig, + pub(crate) hermes: AgentCommandConfig, } #[derive(Debug, Clone, Default)] @@ -270,6 +272,7 @@ struct FileAgentsConfig { claude_code: Option, codex: Option, cursor: Option, + hermes: Option, } #[derive(Debug, Clone, Default, Deserialize)] @@ -460,6 +463,9 @@ fn apply_file_config( resolved.agents.cursor.patch_restore_hooks = patch_restore_hooks; } } + if let Some(value) = agents.hermes { + resolved.agents.hermes.command = value.command; + } } Ok(()) } @@ -529,6 +535,7 @@ impl CodingAgent { Self::ClaudeCode => "/hooks/claude-code", Self::Codex => "/hooks/codex", Self::Cursor => "/hooks/cursor", + Self::Hermes => "/hooks/hermes", } } @@ -537,6 +544,7 @@ impl CodingAgent { Self::ClaudeCode => "claude-code", Self::Codex => "codex", Self::Cursor => "cursor", + Self::Hermes => "hermes", } } @@ -549,6 +557,7 @@ impl CodingAgent { "claude" | "claude-code" => Some(Self::ClaudeCode), "codex" => Some(Self::Codex), "cursor" | "cursor-agent" => Some(Self::Cursor), + "hermes" | "hermes-agent" => Some(Self::Hermes), _ => None, } } diff --git a/crates/sidecar/src/installer.rs b/crates/sidecar/src/installer.rs index dfa15fb..c60161e 100644 --- a/crates/sidecar/src/installer.rs +++ b/crates/sidecar/src/installer.rs @@ -45,6 +45,18 @@ const CURSOR_HOOK_EVENTS: &[&str] = &[ ]; const HOOK_FORWARD_TIMEOUT: Duration = Duration::from_secs(2); +const HERMES_HOOK_EVENTS: &[&str] = &[ + "on_session_start", + "on_session_end", + "on_session_finalize", + "on_session_reset", + "pre_llm_call", + "post_llm_call", + "pre_tool_call", + "post_tool_call", + "subagent_stop", +]; + #[derive(Debug, Clone)] struct PlannedFile { path: PathBuf, @@ -91,11 +103,11 @@ pub(crate) async fn hook_forward(command: HookForwardCommand) -> Result<(), Side input = "{}".to_string(); } - let Some(sidecar_url) = command - .sidecar_url - .clone() - .or_else(|| std::env::var("NEMO_FLOW_SIDECAR_URL").ok()) - else { + let Some(sidecar_url) = resolve_hook_sidecar_url( + command.agent, + command.sidecar_url.clone(), + std::env::var("NEMO_FLOW_SIDECAR_URL").ok(), + ) else { eprintln!( "nemo-flow-sidecar hook forward failed: missing sidecar URL; pass --sidecar-url or set NEMO_FLOW_SIDECAR_URL" ); @@ -157,6 +169,19 @@ pub(crate) async fn hook_forward(command: HookForwardCommand) -> Result<(), Side } } +fn resolve_hook_sidecar_url( + agent: CodingAgent, + command_url: Option, + env_url: Option, +) -> Option { + match agent { + // Hermes shell hooks are installed persistently, but `run --agent hermes` + // starts an ephemeral sidecar and passes the live URL through env. + CodingAgent::Hermes => env_url.or(command_url), + _ => command_url.or(env_url), + } +} + fn planned_files(command: &InstallCommand) -> Result, SidecarError> { let base = install_base(command)?; match command.agent { @@ -201,6 +226,19 @@ fn planned_files(command: &InstallCommand) -> Result, SidecarEr .map_err(|error| SidecarError::Install(error.to_string()))?; Ok(vec![PlannedFile { path, contents }]) } + CodingAgent::Hermes => { + let path = base.join(".hermes/config.yaml"); + let existing = match std::fs::read_to_string(&path) { + Ok(raw) => raw, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(), + Err(error) => return Err(SidecarError::Io(error)), + }; + let contents = merge_hermes_config( + &existing, + hermes_hooks(&hook_command(command, CodingAgent::Hermes)), + )?; + Ok(vec![PlannedFile { path, contents }]) + } } } @@ -289,6 +327,7 @@ pub(crate) fn generated_hooks(agent: CodingAgent, command: &str) -> Value { CodingAgent::ClaudeCode => claude_hooks(command), CodingAgent::Codex => codex_hooks(command), CodingAgent::Cursor => cursor_hooks(command), + CodingAgent::Hermes => hermes_hooks(command), } } @@ -308,6 +347,22 @@ fn cursor_hooks(command: &str) -> Value { hooks_for_events(CURSOR_HOOK_EVENTS, command, true) } +fn hermes_hooks(command: &str) -> Value { + let hooks: serde_json::Map = HERMES_HOOK_EVENTS + .iter() + .map(|event| { + ( + (*event).to_string(), + json!([{ + "command": command, + "timeout": 30 + }]), + ) + }) + .collect(); + json!({ "hooks": Value::Object(hooks) }) +} + fn hooks_for_events(events: &[&str], command: &str, matcher_for_tools: bool) -> Value { let hooks: serde_json::Map = events .iter() @@ -401,6 +456,18 @@ fn merge_codex_config(existing: &str) -> Result { Ok(document.to_string()) } +fn merge_hermes_config(existing: &str, generated: Value) -> Result { + let existing = if existing.trim().is_empty() { + Value::Null + } else { + serde_yaml::from_str(existing).map_err(|error| { + SidecarError::Install(format!("invalid YAML in Hermes config: {error}")) + })? + }; + let merged = merge_hooks(existing, generated)?; + serde_yaml::to_string(&merged).map_err(|error| SidecarError::Install(error.to_string())) +} + pub(crate) fn read_json_file(path: &Path) -> Result { match std::fs::read_to_string(path) { Ok(raw) => serde_json::from_str(&raw).map_err(|error| { @@ -525,6 +592,11 @@ fn print_target_note(agent: CodingAgent, target: InstallTarget) { "Note: run the Cursor CLI smoke test to confirm cursor-agent loads hooks in your version." ); } + (CodingAgent::Hermes, InstallTarget::Cli | InstallTarget::Both) => { + println!( + "Note: Hermes shell hooks prefer NEMO_FLOW_SIDECAR_URL at runtime when set; otherwise they use the installed sidecar URL. Hook consent is still required unless approved interactively or through Hermes configuration." + ); + } _ => {} } } diff --git a/crates/sidecar/src/launcher.rs b/crates/sidecar/src/launcher.rs index 6c0892f..1d58943 100644 --- a/crates/sidecar/src/launcher.rs +++ b/crates/sidecar/src/launcher.rs @@ -90,7 +90,7 @@ fn resolve_agent_and_argv( Some(agent) => agent, None => CodingAgent::infer(&argv[0]).ok_or_else(|| { SidecarError::Launch(format!( - "could not infer coding agent from command {:?}; pass --agent claude-code, --agent codex, or --agent cursor", + "could not infer coding agent from command {:?}; pass --agent claude-code, --agent codex, --agent cursor, or --agent hermes", argv[0] )) })?, @@ -103,6 +103,7 @@ fn configured_command(agent: CodingAgent, agents: &AgentConfigs) -> Option agents.claude_code.command.as_ref(), CodingAgent::Codex => agents.codex.command.as_ref(), CodingAgent::Cursor => agents.cursor.command.as_ref(), + CodingAgent::Hermes => agents.hermes.command.as_ref(), }?; let argv: Vec<_> = command.split_whitespace().map(ToOwned::to_owned).collect(); (!argv.is_empty()).then_some(argv) @@ -155,6 +156,7 @@ impl PreparedRun { } } } + CodingAgent::Hermes => run.prepare_hermes(), } Ok(run) } @@ -266,6 +268,12 @@ impl PreparedRun { Ok(()) } + fn prepare_hermes(&mut self) { + self.notes.push( + "Hermes shell hooks must be configured with `nemo-flow-sidecar install hermes`; this run exports the dynamic sidecar URL for approved hooks".into(), + ); + } + async fn spawn_and_wait(&self) -> Result { let mut command = Command::new(&self.argv[0]); command.args(&self.argv[1..]); diff --git a/crates/sidecar/src/model.rs b/crates/sidecar/src/model.rs index 5f53501..708a03b 100644 --- a/crates/sidecar/src/model.rs +++ b/crates/sidecar/src/model.rs @@ -8,6 +8,7 @@ pub(crate) enum AgentKind { Codex, ClaudeCode, Cursor, + Hermes, Gateway, } @@ -17,6 +18,7 @@ impl AgentKind { Self::Codex => "codex", Self::ClaudeCode => "claude-code", Self::Cursor => "cursor", + Self::Hermes => "hermes", Self::Gateway => "gateway", } } diff --git a/crates/sidecar/src/server.rs b/crates/sidecar/src/server.rs index 89b9197..5a12028 100644 --- a/crates/sidecar/src/server.rs +++ b/crates/sidecar/src/server.rs @@ -10,7 +10,7 @@ use serde_json::Value; use tokio::net::TcpListener; use tokio::sync::oneshot; -use crate::adapters::{claude_code, codex, cursor}; +use crate::adapters::{claude_code, codex, cursor, hermes}; use crate::config::SidecarConfig; use crate::error::SidecarError; use crate::gateway; @@ -61,6 +61,7 @@ pub(crate) fn router(config: SidecarConfig) -> Router { .route("/hooks/codex", post(codex_hook)) .route("/hooks/claude-code", post(claude_code_hook)) .route("/hooks/cursor", post(cursor_hook)) + .route("/hooks/hermes", post(hermes_hook)) .route("/v1/responses", post(gateway::passthrough)) .route("/v1/chat/completions", post(gateway::passthrough)) .route("/v1/messages", post(gateway::passthrough)) @@ -112,6 +113,19 @@ async fn cursor_hook( Ok(Json(outcome.response)) } +async fn hermes_hook( + State(state): State, + headers: HeaderMap, + Json(payload): Json, +) -> Result, SidecarError> { + let outcome = hermes::adapt(payload, &headers); + state + .sessions + .apply_events(&headers, outcome.events) + .await?; + Ok(Json(outcome.response)) +} + #[cfg(test)] #[path = "../tests/coverage/server_tests.rs"] mod tests; diff --git a/crates/sidecar/tests/coverage/adapters_tests.rs b/crates/sidecar/tests/coverage/adapters_tests.rs index 841ab65..a76401b 100644 --- a/crates/sidecar/tests/coverage/adapters_tests.rs +++ b/crates/sidecar/tests/coverage/adapters_tests.rs @@ -5,7 +5,7 @@ use axum::http::HeaderMap; use serde_json::json; use super::*; -use crate::adapters::{claude_code, codex, cursor}; +use crate::adapters::{claude_code, codex, cursor, hermes}; #[test] fn maps_claude_canonical_tool_payload() { @@ -168,6 +168,62 @@ fn keeps_codex_response_unwrapped() { assert_eq!(outcome.response, json!({})); } +#[test] +fn maps_hermes_shell_hook_tool_payload() { + let headers = HeaderMap::new(); + let outcome = hermes::adapt( + json!({ + "hook_event_name": "pre_tool_call", + "tool_name": "terminal", + "tool_input": { "command": "pwd" }, + "session_id": "", + "extra": { + "task_id": "hermes-session", + "tool_call_id": "tool-1" + } + }), + &headers, + ); + + match &outcome.events[0] { + NormalizedEvent::ToolStarted(event) => { + assert_eq!(event.agent_kind, AgentKind::Hermes); + assert_eq!(event.session_id, "hermes-session"); + assert_eq!(event.tool_call_id, "tool-1"); + assert_eq!(event.tool_name, "terminal"); + assert_eq!(event.arguments, json!({ "command": "pwd" })); + } + event => panic!("unexpected event: {event:?}"), + } + assert_eq!(outcome.response, json!({})); +} + +#[test] +fn maps_hermes_real_session_boundary_without_closing_per_turn_end() { + let headers = HeaderMap::new(); + + let per_turn = hermes::adapt( + json!({ + "hook_event_name": "on_session_end", + "session_id": "hermes-session" + }), + &headers, + ); + assert!(matches!(per_turn.events[0], NormalizedEvent::HookMark(_))); + + let finalized = hermes::adapt( + json!({ + "hook_event_name": "on_session_finalize", + "session_id": "hermes-session" + }), + &headers, + ); + assert!(matches!( + finalized.events[0], + NormalizedEvent::AgentEnded(_) + )); +} + #[test] fn normalizes_mark_style_events_and_header_session_ids() { let mut headers = HeaderMap::new(); diff --git a/crates/sidecar/tests/coverage/config_tests.rs b/crates/sidecar/tests/coverage/config_tests.rs index 5df33e9..ba4288b 100644 --- a/crates/sidecar/tests/coverage/config_tests.rs +++ b/crates/sidecar/tests/coverage/config_tests.rs @@ -83,6 +83,7 @@ fn agent_and_gateway_mode_arguments_are_stable() { assert_eq!(CodingAgent::ClaudeCode.hook_path(), "/hooks/claude-code"); assert_eq!(CodingAgent::Codex.hook_path(), "/hooks/codex"); assert_eq!(CodingAgent::Cursor.hook_path(), "/hooks/cursor"); + assert_eq!(CodingAgent::Hermes.hook_path(), "/hooks/hermes"); assert_eq!(GatewayMode::HookOnly.as_arg(), "hook-only"); assert_eq!(GatewayMode::Passthrough.as_arg(), "passthrough"); assert_eq!(GatewayMode::Required.as_arg(), "required"); @@ -99,6 +100,7 @@ fn agent_inference_uses_executable_basename() { CodingAgent::infer("cursor-agent"), Some(CodingAgent::Cursor) ); + assert_eq!(CodingAgent::infer("hermes"), Some(CodingAgent::Hermes)); assert_eq!(CodingAgent::infer("wrapper"), None); } @@ -130,6 +132,9 @@ command = "codex --approval-mode never" [agents.cursor] command = "cursor-agent" patch_restore_hooks = false + +[agents.hermes] +command = "hermes --yolo chat" "#, ) .unwrap(); @@ -162,6 +167,10 @@ patch_restore_hooks = false resolved.agents.codex.command.as_deref(), Some("codex --approval-mode never") ); + assert_eq!( + resolved.agents.hermes.command.as_deref(), + Some("hermes --yolo chat") + ); assert!(!resolved.agents.cursor.patch_restore_hooks); } diff --git a/crates/sidecar/tests/coverage/installer_tests.rs b/crates/sidecar/tests/coverage/installer_tests.rs index f2c340b..d5cc0f7 100644 --- a/crates/sidecar/tests/coverage/installer_tests.rs +++ b/crates/sidecar/tests/coverage/installer_tests.rs @@ -77,6 +77,78 @@ fn generates_cursor_hooks() { ); } +#[test] +fn generates_hermes_shell_hook_config() { + let temp = tempfile::tempdir().unwrap(); + let files = planned_files(&command(CodingAgent::Hermes, temp.path())).unwrap(); + assert_eq!(files.len(), 1); + assert!(files[0].path.ends_with(".hermes/config.yaml")); + let yaml: Value = serde_yaml::from_str(&files[0].contents).unwrap(); + assert!(yaml["hooks"]["on_session_start"].is_array()); + assert!(yaml["hooks"]["subagent_stop"].is_array()); + assert!(yaml["hooks"].get("subagent_start").is_none()); + assert!( + yaml["hooks"]["pre_tool_call"][0]["command"] + .as_str() + .unwrap() + .contains("hook-forward hermes") + ); +} + +#[test] +fn hermes_config_merge_preserves_existing_yaml() { + let existing = r#" +model: + provider: auto +hooks: + pre_tool_call: + - command: ~/.hermes/agent-hooks/audit.sh +"#; + let merged = merge_hermes_config( + existing, + hermes_hooks("nemo-flow-sidecar hook-forward hermes"), + ) + .unwrap(); + let yaml: Value = serde_yaml::from_str(&merged).unwrap(); + + assert_eq!(yaml["model"]["provider"], json!("auto")); + assert_eq!(yaml["hooks"]["pre_tool_call"].as_array().unwrap().len(), 2); + assert_eq!( + yaml["hooks"]["on_session_finalize"] + .as_array() + .unwrap() + .len(), + 1 + ); +} + +#[test] +fn hermes_hook_forward_prefers_dynamic_env_url() { + assert_eq!( + resolve_hook_sidecar_url( + CodingAgent::Hermes, + Some("http://installed".into()), + Some("http://dynamic".into()), + ) + .as_deref(), + Some("http://dynamic") + ); + assert_eq!( + resolve_hook_sidecar_url(CodingAgent::Hermes, Some("http://installed".into()), None,) + .as_deref(), + Some("http://installed") + ); + assert_eq!( + resolve_hook_sidecar_url( + CodingAgent::Codex, + Some("http://installed".into()), + Some("http://dynamic".into()), + ) + .as_deref(), + Some("http://installed") + ); +} + #[test] fn merge_hooks_is_idempotent_and_preserves_existing_entries() { let existing = json!({ diff --git a/crates/sidecar/tests/coverage/launcher_tests.rs b/crates/sidecar/tests/coverage/launcher_tests.rs index 4afbeb0..f50d94c 100644 --- a/crates/sidecar/tests/coverage/launcher_tests.rs +++ b/crates/sidecar/tests/coverage/launcher_tests.rs @@ -66,6 +66,34 @@ fn uses_configured_command_when_no_argv_is_supplied() { assert_eq!(argv, vec!["codex", "--full-auto"]); } +#[test] +fn uses_configured_hermes_command_when_no_argv_is_supplied() { + let agents = AgentConfigs { + hermes: AgentCommandConfig { + command: Some("hermes --yolo chat".into()), + }, + ..AgentConfigs::default() + }; + let command = RunCommand { + agent: Some(CodingAgent::Hermes), + config: None, + openai_base_url: None, + anthropic_base_url: None, + atif_dir: None, + openinference_endpoint: None, + session_metadata: None, + plugin_config: None, + dry_run: false, + print: false, + command: vec![], + }; + + let (agent, argv) = resolve_agent_and_argv(&command, &agents).unwrap(); + + assert_eq!(agent, CodingAgent::Hermes); + assert_eq!(argv, vec!["hermes", "--yolo", "chat"]); +} + #[test] fn inference_failure_has_actionable_message() { let command = RunCommand { @@ -119,6 +147,35 @@ fn prepares_codex_config_overrides() { ); } +#[test] +fn prepares_hermes_hook_environment() { + let resolved = ResolvedConfig { + sidecar: SidecarConfig::default(), + agents: AgentConfigs::default(), + }; + let prepared = PreparedRun::new( + CodingAgent::Hermes, + vec!["hermes".into(), "chat".into()], + "http://127.0.0.1:1234", + &resolved, + false, + ) + .unwrap(); + + assert_eq!(prepared.argv, vec!["hermes", "chat"]); + assert!(prepared.env.contains(&( + "NEMO_FLOW_SIDECAR_URL".into(), + "http://127.0.0.1:1234".into() + ))); + assert!( + !prepared + .env + .iter() + .any(|(name, _)| name == "HERMES_ACCEPT_HOOKS") + ); + assert!(prepared.notes[0].contains("approved hooks")); +} + #[test] fn prepares_claude_temp_plugin() { let resolved = ResolvedConfig { diff --git a/crates/sidecar/tests/coverage/server_tests.rs b/crates/sidecar/tests/coverage/server_tests.rs index 495e34b..024940e 100644 --- a/crates/sidecar/tests/coverage/server_tests.rs +++ b/crates/sidecar/tests/coverage/server_tests.rs @@ -127,6 +127,32 @@ async fn cursor_hook_returns_cursor_permission_fields() { assert_eq!(body["permission"], json!("allow")); } +#[tokio::test] +async fn hermes_hook_keeps_shell_hook_response_shape() { + let app = router(test_config()); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/hooks/hermes") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "session_id": "hermes-1", + "hook_event_name": "on_session_start" + }) + .to_string(), + )) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body, json!({})); +} + #[tokio::test] async fn gateway_forwards_openai_json_without_rewriting_payload() { let upstream = spawn_upstream(false).await; From 173bca9ffcaf6b5cac280105329c2ec195042027 Mon Sep 17 00:00:00 2001 From: GSD Agent Date: Wed, 6 May 2026 14:23:06 -0700 Subject: [PATCH 07/13] feat(sidecar): capture Hermes API hook token metrics Signed-off-by: GSD Agent --- crates/sidecar/src/adapters/hermes.rs | 138 +++++++++++++++++- crates/sidecar/src/installer.rs | 2 + crates/sidecar/src/model.rs | 16 ++ crates/sidecar/src/session.rs | 66 ++++++++- .../sidecar/tests/coverage/adapters_tests.rs | 70 +++++++++ .../sidecar/tests/coverage/installer_tests.rs | 2 + .../sidecar/tests/coverage/session_tests.rs | 82 ++++++++++- 7 files changed, 371 insertions(+), 5 deletions(-) diff --git a/crates/sidecar/src/adapters/hermes.rs b/crates/sidecar/src/adapters/hermes.rs index 59b4d46..7b94eda 100644 --- a/crates/sidecar/src/adapters/hermes.rs +++ b/crates/sidecar/src/adapters/hermes.rs @@ -2,12 +2,38 @@ // SPDX-License-Identifier: Apache-2.0 use axum::http::HeaderMap; -use serde_json::{Value, json}; +use serde_json::{Map, Value, json}; -use crate::adapters::{AdapterOutcome, ClassificationRules, classify}; -use crate::model::AgentKind; +use crate::adapters::{ + AdapterOutcome, ClassificationRules, classify, event_name, metadata, normalize_name, + session_id, value_at, +}; +use crate::model::{AgentKind, LlmEvent}; pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { + let event_name = event_name(&payload); + let normalized = normalize_name(&event_name); + if normalized == "preapirequest" { + return AdapterOutcome { + events: vec![crate::model::NormalizedEvent::LlmStarted(hermes_llm_event( + &payload, + headers, + &event_name, + ))], + response: json!({}), + }; + } + if normalized == "postapirequest" { + return AdapterOutcome { + events: vec![crate::model::NormalizedEvent::LlmEnded(hermes_llm_event( + &payload, + headers, + &event_name, + ))], + response: json!({}), + }; + } + let event = classify( &payload, headers, @@ -26,3 +52,109 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { response: json!({}), } } + +fn hermes_llm_event(payload: &Value, headers: &HeaderMap, event_name: &str) -> LlmEvent { + let session_id = session_id(payload, headers); + let api_call_id = hermes_api_call_id(payload, &session_id); + let provider = hermes_string_at(payload, "provider") + .or_else(|| hermes_string_at(payload, "api_mode")) + .unwrap_or_else(|| "hermes_api_request".to_string()); + let model_name = + hermes_string_at(payload, "response_model").or_else(|| hermes_string_at(payload, "model")); + let mut event_metadata = metadata(payload, headers, AgentKind::Hermes, event_name); + if let Value::Object(ref mut object) = event_metadata { + object.insert("api_call_id".into(), json!(api_call_id.clone())); + object.insert("provider_payload_exact".into(), json!(false)); + object.insert("fidelity_source".into(), json!("hermes_api_hooks")); + } + LlmEvent { + session_id, + agent_kind: AgentKind::Hermes, + event_name: event_name.to_string(), + api_call_id, + provider, + model_name, + request: hermes_llm_request(payload), + response: hermes_llm_response(payload), + metadata: event_metadata, + } +} + +fn hermes_api_call_id(payload: &Value, session_id: &str) -> String { + let task_id = hermes_string_at(payload, "task_id").unwrap_or_default(); + let api_call_count = hermes_string_at(payload, "api_call_count").unwrap_or_default(); + format!("{session_id}:{task_id}:{api_call_count}") +} + +fn hermes_llm_request(payload: &Value) -> Value { + let mut object = Map::new(); + for key in [ + "task_id", + "session_id", + "platform", + "model", + "provider", + "base_url", + "api_mode", + "api_call_count", + "message_count", + "tool_count", + "approx_input_tokens", + "request_char_count", + "max_tokens", + ] { + if let Some(value) = hermes_value_at(payload, key) { + object.insert(key.into(), value); + } + } + object.insert( + "fidelity".into(), + json!({ + "provider_payload_exact": false, + "source": "hermes_pre_api_request" + }), + ); + Value::Object(object) +} + +fn hermes_llm_response(payload: &Value) -> Value { + let mut object = Map::new(); + for key in [ + "task_id", + "session_id", + "platform", + "model", + "provider", + "base_url", + "api_mode", + "api_call_count", + "api_duration", + "finish_reason", + "message_count", + "response_model", + "usage", + "assistant_content_chars", + "assistant_tool_call_count", + ] { + if let Some(value) = hermes_value_at(payload, key) { + object.insert(key.into(), value); + } + } + Value::Object(object) +} + +fn hermes_string_at(payload: &Value, key: &str) -> Option { + value_at(payload, &[key]) + .or_else(|| value_at(payload, &["extra", key])) + .and_then(|value| match value { + Value::String(value) => Some(value), + Value::Number(value) => Some(value.to_string()), + Value::Bool(value) => Some(value.to_string()), + _ => None, + }) + .filter(|value| !value.is_empty()) +} + +fn hermes_value_at(payload: &Value, key: &str) -> Option { + value_at(payload, &[key]).or_else(|| value_at(payload, &["extra", key])) +} diff --git a/crates/sidecar/src/installer.rs b/crates/sidecar/src/installer.rs index c60161e..1fd0a10 100644 --- a/crates/sidecar/src/installer.rs +++ b/crates/sidecar/src/installer.rs @@ -52,6 +52,8 @@ const HERMES_HOOK_EVENTS: &[&str] = &[ "on_session_reset", "pre_llm_call", "post_llm_call", + "pre_api_request", + "post_api_request", "pre_tool_call", "post_tool_call", "subagent_stop", diff --git a/crates/sidecar/src/model.rs b/crates/sidecar/src/model.rs index 708a03b..7095a40 100644 --- a/crates/sidecar/src/model.rs +++ b/crates/sidecar/src/model.rs @@ -30,6 +30,8 @@ pub(crate) enum NormalizedEvent { AgentEnded(SessionEvent), SubagentStarted(SubagentEvent), SubagentEnded(SubagentEvent), + LlmStarted(LlmEvent), + LlmEnded(LlmEvent), ToolStarted(ToolEvent), ToolEnded(ToolEvent), PromptSubmitted(SessionEvent), @@ -49,6 +51,7 @@ impl NormalizedEvent { | Self::Compaction(event) | Self::Notification(event) | Self::HookMark(event) => &event.session_id, + Self::LlmStarted(event) | Self::LlmEnded(event) => &event.session_id, Self::SubagentStarted(event) | Self::SubagentEnded(event) => &event.session_id, Self::ToolStarted(event) | Self::ToolEnded(event) => &event.session_id, } @@ -74,6 +77,19 @@ pub(crate) struct SubagentEvent { pub(crate) metadata: Value, } +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct LlmEvent { + pub(crate) session_id: String, + pub(crate) agent_kind: AgentKind, + pub(crate) event_name: String, + pub(crate) api_call_id: String, + pub(crate) provider: String, + pub(crate) model_name: Option, + pub(crate) request: Value, + pub(crate) response: Value, + pub(crate) metadata: Value, +} + #[derive(Debug, Clone, PartialEq)] pub(crate) struct ToolEvent { pub(crate) session_id: String, diff --git a/crates/sidecar/src/session.rs b/crates/sidecar/src/session.rs index cb20833..8ce5145 100644 --- a/crates/sidecar/src/session.rs +++ b/crates/sidecar/src/session.rs @@ -25,7 +25,7 @@ use tokio::sync::Mutex; use crate::config::{SessionConfig, SidecarConfig}; use crate::error::SidecarError; -use crate::model::{AgentKind, NormalizedEvent, SessionEvent, SubagentEvent, ToolEvent}; +use crate::model::{AgentKind, LlmEvent, NormalizedEvent, SessionEvent, SubagentEvent, ToolEvent}; #[derive(Clone)] pub(crate) struct SessionManager { @@ -56,6 +56,7 @@ struct Session { agent_scope: Option, subagents: HashMap, subagent_stack: Vec, + llms: HashMap, tools: HashMap, config: SessionConfig, atif: Option, @@ -86,6 +87,7 @@ impl SessionManager { if session.agent_scope.is_none() && session.subagents.is_empty() && session.subagent_stack.is_empty() + && session.llms.is_empty() && session.tools.is_empty() { sessions.remove(&session_id); @@ -142,6 +144,7 @@ impl Session { agent_scope: None, subagents: HashMap::new(), subagent_stack: Vec::new(), + llms: HashMap::new(), tools: HashMap::new(), config, atif: None, @@ -158,6 +161,8 @@ impl Session { NormalizedEvent::AgentEnded(event) => self.end_agent(event), NormalizedEvent::SubagentStarted(event) => self.start_subagent(event), NormalizedEvent::SubagentEnded(event) => self.end_subagent(event), + NormalizedEvent::LlmStarted(event) => self.start_hook_llm(event), + NormalizedEvent::LlmEnded(event) => self.end_hook_llm(event), NormalizedEvent::ToolStarted(event) => self.start_tool(event), NormalizedEvent::ToolEnded(event) => self.end_tool(event), NormalizedEvent::PromptSubmitted(event) => self.mark("prompt_submitted", event), @@ -262,6 +267,16 @@ impl Session { fn end_agent(&mut self, event: SessionEvent) -> Result<(), SidecarError> { self.ensure_agent_started(event.metadata.clone())?; + let active_llms: Vec<_> = self.llms.drain().map(|(_, handle)| handle).collect(); + for handle in active_llms { + llm_call_end( + LlmCallEndParams::builder() + .handle(&handle) + .response(json!({ "status": "closed_by_agent_end" })) + .metadata(json!({ "status": "closed_by_agent_end" })) + .build(), + )?; + } let active_tools: Vec<_> = self.tools.drain().map(|(_, handle)| handle).collect(); for handle in active_tools { tool_call_end( @@ -359,6 +374,54 @@ impl Session { Ok(()) } + fn start_hook_llm(&mut self, event: LlmEvent) -> Result<(), SidecarError> { + self.ensure_agent_started(event.metadata.clone())?; + if self.llms.contains_key(&event.api_call_id) { + return Ok(()); + } + let handle = llm_call( + LlmCallParams::builder() + .name(event.provider.as_str()) + .request(&LlmRequest { + headers: Map::new(), + content: event.request, + }) + .attributes(LlmAttributes::empty()) + .metadata(event.metadata) + .model_name_opt(event.model_name) + .build(), + )?; + self.llms.insert(event.api_call_id, handle); + Ok(()) + } + + fn end_hook_llm(&mut self, event: LlmEvent) -> Result<(), SidecarError> { + self.ensure_agent_started(event.metadata.clone())?; + let handle = match self.llms.remove(&event.api_call_id) { + Some(handle) => handle, + None => llm_call( + LlmCallParams::builder() + .name(event.provider.as_str()) + .request(&LlmRequest { + headers: Map::new(), + content: event.request, + }) + .attributes(LlmAttributes::empty()) + .metadata(event.metadata.clone()) + .model_name_opt(event.model_name.clone()) + .build(), + )?, + }; + llm_call_end( + LlmCallEndParams::builder() + .handle(&handle) + .response(event.response) + .metadata(event.metadata) + .build(), + )?; + Ok(()) + } + fn start_tool(&mut self, event: ToolEvent) -> Result<(), SidecarError> { self.ensure_agent_started(event.metadata.clone())?; if self.tools.contains_key(&event.tool_call_id) { @@ -466,6 +529,7 @@ fn event_agent_kind(event: &NormalizedEvent) -> AgentKind { NormalizedEvent::SubagentStarted(event) | NormalizedEvent::SubagentEnded(event) => { event.agent_kind } + NormalizedEvent::LlmStarted(event) | NormalizedEvent::LlmEnded(event) => event.agent_kind, NormalizedEvent::ToolStarted(event) | NormalizedEvent::ToolEnded(event) => event.agent_kind, } } diff --git a/crates/sidecar/tests/coverage/adapters_tests.rs b/crates/sidecar/tests/coverage/adapters_tests.rs index a76401b..276486d 100644 --- a/crates/sidecar/tests/coverage/adapters_tests.rs +++ b/crates/sidecar/tests/coverage/adapters_tests.rs @@ -224,6 +224,76 @@ fn maps_hermes_real_session_boundary_without_closing_per_turn_end() { )); } +#[test] +fn maps_hermes_api_hooks_to_llm_lifecycle() { + let headers = HeaderMap::new(); + + let started = hermes::adapt( + json!({ + "hook_event_name": "pre_api_request", + "session_id": "hermes-session", + "extra": { + "task_id": "task-1", + "api_call_count": 2, + "model": "qwen", + "provider": "custom", + "base_url": "http://localhost:11434/v1", + "api_mode": "chat_completions", + "message_count": 3, + "tool_count": 1, + "approx_input_tokens": 12, + "request_char_count": 456, + "max_tokens": 1024 + } + }), + &headers, + ); + match &started.events[0] { + NormalizedEvent::LlmStarted(event) => { + assert_eq!(event.session_id, "hermes-session"); + assert_eq!(event.api_call_id, "hermes-session:task-1:2"); + assert_eq!(event.provider, "custom"); + assert_eq!(event.model_name.as_deref(), Some("qwen")); + assert_eq!(event.request["message_count"], json!(3)); + assert_eq!( + event.request["fidelity"]["provider_payload_exact"], + json!(false) + ); + } + event => panic!("unexpected event: {event:?}"), + } + + let ended = hermes::adapt( + json!({ + "hook_event_name": "post_api_request", + "session_id": "hermes-session", + "extra": { + "task_id": "task-1", + "api_call_count": 2, + "model": "qwen", + "response_model": "qwen", + "provider": "custom", + "api_duration": 0.25, + "finish_reason": "stop", + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "prompt_tokens_details": { "cached_tokens": 3 } + } + } + }), + &headers, + ); + match &ended.events[0] { + NormalizedEvent::LlmEnded(event) => { + assert_eq!(event.api_call_id, "hermes-session:task-1:2"); + assert_eq!(event.response["usage"]["prompt_tokens"], json!(10)); + assert_eq!(event.response["usage"]["completion_tokens"], json!(5)); + } + event => panic!("unexpected event: {event:?}"), + } +} + #[test] fn normalizes_mark_style_events_and_header_session_ids() { let mut headers = HeaderMap::new(); diff --git a/crates/sidecar/tests/coverage/installer_tests.rs b/crates/sidecar/tests/coverage/installer_tests.rs index d5cc0f7..b83bfd6 100644 --- a/crates/sidecar/tests/coverage/installer_tests.rs +++ b/crates/sidecar/tests/coverage/installer_tests.rs @@ -85,6 +85,8 @@ fn generates_hermes_shell_hook_config() { assert!(files[0].path.ends_with(".hermes/config.yaml")); let yaml: Value = serde_yaml::from_str(&files[0].contents).unwrap(); assert!(yaml["hooks"]["on_session_start"].is_array()); + assert!(yaml["hooks"]["pre_api_request"].is_array()); + assert!(yaml["hooks"]["post_api_request"].is_array()); assert!(yaml["hooks"]["subagent_stop"].is_array()); assert!(yaml["hooks"].get("subagent_start").is_none()); assert!( diff --git a/crates/sidecar/tests/coverage/session_tests.rs b/crates/sidecar/tests/coverage/session_tests.rs index 9e0656b..2bbb7fd 100644 --- a/crates/sidecar/tests/coverage/session_tests.rs +++ b/crates/sidecar/tests/coverage/session_tests.rs @@ -5,7 +5,7 @@ use axum::http::HeaderMap; use serde_json::json; use super::*; -use crate::model::{SessionEvent, ToolEvent}; +use crate::model::{LlmEvent, SessionEvent, ToolEvent}; #[tokio::test] async fn nests_agent_subagent_and_tool_lifecycle() { @@ -141,6 +141,86 @@ async fn writes_atif_on_session_end_from_header_config() { assert_eq!(atif["agent"]["name"], json!("codex")); } +#[tokio::test] +async fn writes_hermes_api_hook_usage_to_atif_metrics() { + let temp = tempfile::tempdir().unwrap(); + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + let mut headers = HeaderMap::new(); + headers.insert( + "x-nemo-flow-atif-dir", + temp.path().to_string_lossy().parse().unwrap(), + ); + + manager + .apply_events( + &headers, + vec![ + NormalizedEvent::AgentStarted(SessionEvent { + session_id: "hermes-usage".into(), + agent_kind: AgentKind::Hermes, + event_name: "on_session_start".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::LlmStarted(LlmEvent { + session_id: "hermes-usage".into(), + agent_kind: AgentKind::Hermes, + event_name: "pre_api_request".into(), + api_call_id: "hermes-usage:task-1:1".into(), + provider: "custom".into(), + model_name: Some("qwen".into()), + request: json!({ "model": "qwen" }), + response: Value::Null, + metadata: json!({}), + }), + NormalizedEvent::LlmEnded(LlmEvent { + session_id: "hermes-usage".into(), + agent_kind: AgentKind::Hermes, + event_name: "post_api_request".into(), + api_call_id: "hermes-usage:task-1:1".into(), + provider: "custom".into(), + model_name: Some("qwen".into()), + request: json!({}), + response: json!({ + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "prompt_tokens_details": { "cached_tokens": 3 } + } + }), + metadata: json!({}), + }), + NormalizedEvent::AgentEnded(SessionEvent { + session_id: "hermes-usage".into(), + agent_kind: AgentKind::Hermes, + event_name: "on_session_finalize".into(), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + let path = temp.path().join("hermes-usage.atif.json"); + let atif: Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); + assert_eq!(atif["steps"][1]["metrics"]["prompt_tokens"], json!(10)); + assert_eq!(atif["steps"][1]["metrics"]["completion_tokens"], json!(5)); + assert_eq!(atif["steps"][1]["metrics"]["cached_tokens"], json!(3)); + assert_eq!(atif["final_metrics"]["total_prompt_tokens"], json!(10)); + assert_eq!(atif["final_metrics"]["total_completion_tokens"], json!(5)); + assert_eq!(atif["final_metrics"]["total_cached_tokens"], json!(3)); +} + #[tokio::test] async fn handles_out_of_order_subagent_and_tool_end_events() { let config = SidecarConfig { From c1be4eea41781242546f6ff655e89a1d045c0b8e Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 6 May 2026 18:58:57 -0400 Subject: [PATCH 08/13] feat: add LLM and Tool best-effort correlation; Hermes docs Signed-off-by: Will Killian --- ATTRIBUTIONS-Rust.md | 112 ++ codecov.yml | 10 + crates/sidecar/src/adapters/claude_code.rs | 10 +- crates/sidecar/src/adapters/codex.rs | 5 + crates/sidecar/src/adapters/cursor.rs | 5 + crates/sidecar/src/adapters/hermes.rs | 5 + crates/sidecar/src/adapters/mod.rs | 266 +++-- crates/sidecar/src/config.rs | 200 +++- crates/sidecar/src/error.rs | 3 + crates/sidecar/src/gateway.rs | 303 +++++- crates/sidecar/src/installer.rs | 395 +++++-- crates/sidecar/src/launcher.rs | 313 ++++-- crates/sidecar/src/main.rs | 5 + crates/sidecar/src/model.rs | 25 +- crates/sidecar/src/server.rs | 20 + crates/sidecar/src/session.rs | 962 ++++++++++++++++-- crates/sidecar/tests/cli_tests.rs | 325 ++++++ .../sidecar/tests/coverage/adapters_tests.rs | 109 +- crates/sidecar/tests/coverage/config_tests.rs | 81 ++ .../sidecar/tests/coverage/gateway_tests.rs | 133 ++- .../sidecar/tests/coverage/installer_tests.rs | 77 +- .../sidecar/tests/coverage/launcher_tests.rs | 158 +++ crates/sidecar/tests/coverage/server_tests.rs | 90 ++ .../sidecar/tests/coverage/session_tests.rs | 953 ++++++++++++++++- docs/index.md | 1 + docs/integrate-frameworks/about.md | 3 +- .../coding-agent-claude-code.md | 18 + .../coding-agent-codex.md | 19 + .../coding-agent-cursor.md | 20 + .../coding-agent-hermes.md | 136 +++ .../coding-agent-sidecar.md | 78 +- docs/reference/api/rust/index.md | 5 +- integrations/coding-agents/README.md | 20 +- .../coding-agents/claude-code/README.md | 13 + .../claude-code/hooks/hooks.json | 33 + integrations/coding-agents/codex/README.md | 17 + .../coding-agents/codex/hooks/hooks.json | 33 + .../coding-agents/cursor/.cursor/hooks.json | 11 + integrations/coding-agents/cursor/README.md | 17 + 39 files changed, 4558 insertions(+), 431 deletions(-) create mode 100644 crates/sidecar/tests/cli_tests.rs create mode 100644 docs/integrate-frameworks/coding-agent-hermes.md diff --git a/ATTRIBUTIONS-Rust.md b/ATTRIBUTIONS-Rust.md index 2aa3bcd..5e7d69f 100644 --- a/ATTRIBUTIONS-Rust.md +++ b/ATTRIBUTIONS-Rust.md @@ -34924,6 +34924,87 @@ limitations under the License. ``` +## serde_yaml - 0.9.34+deprecated +**Repository URL**: https://github.com/dtolnay/serde-yaml +**License Type(s)**: Apache-2.0 +### License: https://spdx.org/licenses/Apache-2.0.html +``` +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. + +``` + ## syn - 2.0.117 **Repository URL**: https://github.com/dtolnay/syn **License Type(s)**: Apache-2.0 @@ -37425,6 +37506,37 @@ SOFTWARE. ``` +## unsafe-libyaml - 0.2.11 +**Repository URL**: https://github.com/dtolnay/unsafe-libyaml +**License Type(s)**: MIT +### License: https://spdx.org/licenses/MIT.html +``` +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +``` + ## zmij - 1.0.21 **Repository URL**: https://github.com/dtolnay/zmij **License Type(s)**: MIT diff --git a/codecov.yml b/codecov.yml index f044574..f5a0b8e 100644 --- a/codecov.yml +++ b/codecov.yml @@ -55,6 +55,16 @@ component_management: threshold: 0.5% base: auto if_ci_failed: error + - component_id: sidecar_runtime + name: Sidecar Runtime + paths: + - "crates/sidecar/src" + statuses: + - type: project + target: 95% + threshold: 0.5% + base: auto + if_ci_failed: error - component_id: go_binding name: Go Binding paths: diff --git a/crates/sidecar/src/adapters/claude_code.rs b/crates/sidecar/src/adapters/claude_code.rs index 30346da..95a079b 100644 --- a/crates/sidecar/src/adapters/claude_code.rs +++ b/crates/sidecar/src/adapters/claude_code.rs @@ -7,6 +7,12 @@ use serde_json::{Value, json}; use crate::adapters::{AdapterOutcome, ClassificationRules, classify, event_name, normalize_name}; use crate::model::{AgentKind, NormalizedEvent}; +/// Normalizes Claude Code hook payloads and returns the hook response Claude expects. +/// +/// Claude Code uses permission-bearing tool hooks, so pre-tool events are explicitly allowed +/// instead of returning the generic `{ continue: true }` shape. Stop hooks can arrive as either +/// terminal events or LLM-style marks; both are acknowledged with a null stop reason so the +/// sidecar remains observational and never blocks Claude's lifecycle by default. pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { let event = classify( &payload, @@ -39,7 +45,9 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { "permissionDecision": "allow" } }), - NormalizedEvent::AgentEnded(_) | NormalizedEvent::HookMark(_) + NormalizedEvent::AgentEnded(_) + | NormalizedEvent::HookMark(_) + | NormalizedEvent::LlmHint(_) if normalized_event == "stop" => { json!({ "continue": true, "stopReason": null }) diff --git a/crates/sidecar/src/adapters/codex.rs b/crates/sidecar/src/adapters/codex.rs index ba98219..14a0bc5 100644 --- a/crates/sidecar/src/adapters/codex.rs +++ b/crates/sidecar/src/adapters/codex.rs @@ -7,6 +7,11 @@ use serde_json::{Value, json}; use crate::adapters::{AdapterOutcome, ClassificationRules, classify}; use crate::model::AgentKind; +/// Normalizes Codex hook payloads while leaving Codex hook control flow untouched. +/// +/// Codex receives an empty response body from this adapter because the sidecar currently records +/// hooks instead of making allow/deny decisions. Event spelling is accepted in both camelCase and +/// snake_case forms so installed hooks and inline `run` hook configuration share one path. pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { let event = classify( &payload, diff --git a/crates/sidecar/src/adapters/cursor.rs b/crates/sidecar/src/adapters/cursor.rs index d7fb569..337a5cf 100644 --- a/crates/sidecar/src/adapters/cursor.rs +++ b/crates/sidecar/src/adapters/cursor.rs @@ -7,6 +7,11 @@ use serde_json::{Value, json}; use crate::adapters::{AdapterOutcome, ClassificationRules, classify}; use crate::model::{AgentKind, NormalizedEvent}; +/// Normalizes Cursor hook payloads and returns Cursor-compatible continuation decisions. +/// +/// Cursor has separate shell and MCP hook names, both of which are collapsed into normal tool +/// start/end events. Tool starts are fail-open with an explicit `allow` permission response so +/// the sidecar records activity without becoming a policy engine for Cursor executions. pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { let event = classify( &payload, diff --git a/crates/sidecar/src/adapters/hermes.rs b/crates/sidecar/src/adapters/hermes.rs index 59b4d46..057eeb7 100644 --- a/crates/sidecar/src/adapters/hermes.rs +++ b/crates/sidecar/src/adapters/hermes.rs @@ -7,6 +7,11 @@ use serde_json::{Value, json}; use crate::adapters::{AdapterOutcome, ClassificationRules, classify}; use crate::model::AgentKind; +/// Normalizes Hermes shell hook payloads without emitting control directives. +/// +/// Hermes hooks are installed as shell commands and may run outside `run`, so this adapter keeps +/// responses minimal and relies on the forwarder fail-open/fail-closed setting to decide whether +/// hook delivery problems affect the invoking agent. pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { let event = classify( &payload, diff --git a/crates/sidecar/src/adapters/mod.rs b/crates/sidecar/src/adapters/mod.rs index 423c63c..33fd83b 100644 --- a/crates/sidecar/src/adapters/mod.rs +++ b/crates/sidecar/src/adapters/mod.rs @@ -11,7 +11,9 @@ use serde_json::{Map, Value, json}; use uuid::Uuid; use crate::config::header_string; -use crate::model::{AgentKind, NormalizedEvent, SessionEvent, SubagentEvent, ToolEvent}; +use crate::model::{ + AgentKind, LlmHintEvent, NormalizedEvent, SessionEvent, SubagentEvent, ToolEvent, +}; #[derive(Debug, Clone, PartialEq)] pub(crate) struct AdapterOutcome { @@ -29,21 +31,37 @@ pub(super) struct ClassificationRules<'a> { tool_end: &'a [&'a str], } +// Derives a stable session identifier from sidecar headers first, then common agent payload +// fields, and finally a v7 UUID. Header precedence lets gateway and hook-forward callers +// correlate events even when agent payload schemas omit or rename their native session field. fn session_id(payload: &Value, headers: &HeaderMap) -> String { header_string(headers, "x-nemo-flow-session-id") .or_else(|| header_string(headers, "x-claude-code-session-id")) - .or_else(|| string_at(payload, &["session_id"])) - .or_else(|| string_at(payload, &["sessionId"])) - .or_else(|| string_at(payload, &["session", "id"])) - .or_else(|| string_at(payload, &["conversation_id"])) - .or_else(|| string_at(payload, &["conversationId"])) - .or_else(|| string_at(payload, &["parent_session_id"])) - .or_else(|| string_at(payload, &["task_id"])) - .or_else(|| string_at(payload, &["extra", "session_id"])) - .or_else(|| string_at(payload, &["extra", "task_id"])) + .or_else(|| session_id_from_payload(payload)) .unwrap_or_else(|| format!("hook-{}", Uuid::now_v7())) } +// Reads the first known session identifier payload path. Keeping the path list in one place makes +// adapter precedence explicit without nesting a long `or_else` chain in `session_id`. +fn session_id_from_payload(payload: &Value) -> Option { + [ + &["session_id"][..], + &["sessionId"], + &["session", "id"], + &["conversation_id"], + &["conversationId"], + &["parent_session_id"], + &["task_id"], + &["extra", "session_id"], + &["extra", "task_id"], + ] + .into_iter() + .find_map(|path| string_at(payload, path)) +} + +// Reads the agent's event name from the known hook fields in order and falls back to `unknown`. +// This deliberately keeps unknown payloads observable instead of rejecting them at the adapter +// boundary, allowing the session layer to emit a generic mark event. fn event_name(payload: &Value) -> String { string_at(payload, &["hook_event_name"]) .or_else(|| string_at(payload, &["event_name"])) @@ -54,6 +72,9 @@ fn event_name(payload: &Value) -> String { .unwrap_or_else(|| "unknown".to_string()) } +// Builds shared metadata for every normalized hook event. Only stable, low-cardinality fields and +// sidecar configuration hints are lifted out; the full payload remains on the event for consumers +// that need agent-specific detail. fn metadata(payload: &Value, headers: &HeaderMap, kind: AgentKind, event_name: &str) -> Value { let mut object = Map::new(); object.insert("agent_kind".into(), json!(kind.as_str())); @@ -77,6 +98,8 @@ fn metadata(payload: &Value, headers: &HeaderMap, kind: AgentKind, event_name: & Value::Object(object) } +// Creates a root session event using the common session-id and metadata extraction rules so +// lifecycle, marks, notifications, and compaction events all carry identical correlation fields. fn common_session_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> SessionEvent { let event_name = event_name(payload); SessionEvent { @@ -88,6 +111,9 @@ fn common_session_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) - } } +// Creates a subagent event and tolerates sparse agent payloads by using the sidecar subagent +// header and then a synthetic `subagent` id. The fallback keeps unmatched start/end events visible +// rather than dropping them when an integration lacks explicit nested-agent IDs. fn common_subagent_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> SubagentEvent { let session = common_session_event(payload, headers, kind); let subagent_id = subagent_id(payload) @@ -103,65 +129,173 @@ fn common_subagent_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) } } +// Captures hook payloads that can help correlate nearby gateway LLM calls to the right agent or +// subagent. Multiple naming conventions are accepted because integrations expose conversation, +// generation, request, and model identifiers under different shapes. +fn common_llm_hint_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> LlmHintEvent { + let session = common_session_event(payload, headers, kind); + LlmHintEvent { + session_id: session.session_id, + agent_kind: kind, + event_name: session.event_name, + subagent_id: hook_subagent_id(payload, headers), + agent_id: first_string_at(payload, &[&["agent_id"][..], &["agent", "id"][..]]), + agent_type: first_string_at( + payload, + &[ + &["agent_type"][..], + &["agent", "type"][..], + &["agent", "name"][..], + ], + ), + conversation_id: first_string_at( + payload, + &[ + &["conversation_id"][..], + &["conversationId"][..], + &["conversation", "id"][..], + ], + ), + generation_id: first_string_at( + payload, + &[ + &["generation_id"][..], + &["generationId"][..], + &["generation", "id"][..], + ], + ), + request_id: first_string_at( + payload, + &[ + &["request_id"][..], + &["requestId"][..], + &["request", "id"][..], + &["extra", "request_id"][..], + ], + ), + model: first_string_at( + payload, + &[&["model"][..], &["model_name"][..], &["modelName"][..]], + ), + payload: session.payload, + metadata: session.metadata, + } +} + +// Converts agent tool hooks into the runtime tool event shape while preserving missing fields. +// Tool IDs and names are synthesized when absent, arguments/results are searched across known +// payload shapes, and failure or permission-denied event names are reflected in status metadata. fn common_tool_event(payload: &Value, headers: &HeaderMap, kind: AgentKind) -> ToolEvent { let session = common_session_event(payload, headers, kind); let normalized_event = normalize_name(&session.event_name); - let tool_call_id = string_at(payload, &["tool_call_id"]) - .or_else(|| string_at(payload, &["toolCallId"])) - .or_else(|| string_at(payload, &["tool_use_id"])) - .or_else(|| string_at(payload, &["call_id"])) - .or_else(|| string_at(payload, &["extra", "tool_call_id"])) - .or_else(|| string_at(payload, &["extra", "call_id"])) - .or_else(|| string_at(payload, &["tool", "id"])) - .or_else(|| string_at(payload, &["tool_input", "id"])) - .or_else(|| string_at(payload, &["id"])) - .unwrap_or_else(|| format!("tool-{}", Uuid::now_v7())); - let tool_name = string_at(payload, &["tool_name"]) - .or_else(|| string_at(payload, &["toolName"])) - .or_else(|| string_at(payload, &["tool", "name"])) - .or_else(|| string_at(payload, &["tool_input", "name"])) - .or_else(|| string_at(payload, &["name"])) - .unwrap_or_else(|| "unknown_tool".to_string()); - let arguments = value_at(payload, &["tool_input"]) + ToolEvent { + session_id: session.session_id, + agent_kind: kind, + event_name: session.event_name, + tool_call_id: tool_call_id(payload), + tool_name: tool_name(payload), + subagent_id: hook_subagent_id(payload, headers), + arguments: tool_arguments(payload), + result: tool_result(payload, &normalized_event), + status: tool_status(payload, &normalized_event), + payload: session.payload, + metadata: session.metadata, + } +} + +// Looks up the first string across a list of payload paths. Keeping this fallback mechanic in one +// helper makes event-specific extraction code read as schema precedence rather than control flow. +fn first_string_at(payload: &Value, paths: &[&[&str]]) -> Option { + paths.iter().find_map(|path| string_at(payload, path)) +} + +// Resolves a subagent id from payload shape first and the sidecar header second. The payload wins +// because it is the agent's native ownership signal; the header exists for gateway correlation and +// sparse hook systems. +fn hook_subagent_id(payload: &Value, headers: &HeaderMap) -> Option { + subagent_id(payload).or_else(|| header_string(headers, "x-nemo-flow-subagent-id")) +} + +// Resolves a tool call identifier from all known agent payload conventions before synthesizing a +// UUID-backed id. The synthetic id keeps lifecycle events recordable even when hooks omit IDs. +fn tool_call_id(payload: &Value) -> String { + first_string_at( + payload, + &[ + &["tool_call_id"][..], + &["toolCallId"][..], + &["tool_use_id"][..], + &["call_id"][..], + &["extra", "tool_call_id"][..], + &["extra", "call_id"][..], + &["tool", "id"][..], + &["tool_input", "id"][..], + &["id"][..], + ], + ) + .unwrap_or_else(|| format!("tool-{}", Uuid::now_v7())) +} + +// Resolves a human-readable tool name from the common top-level, nested tool, and tool-input +// shapes. Missing names are kept explicit as `unknown_tool` rather than inheriting event names. +fn tool_name(payload: &Value) -> String { + first_string_at( + payload, + &[ + &["tool_name"][..], + &["toolName"][..], + &["tool", "name"][..], + &["tool_input", "name"][..], + &["name"][..], + ], + ) + .unwrap_or_else(|| "unknown_tool".to_string()) +} + +// Extracts tool input from the agent-specific fields that represent call arguments. A missing +// argument payload remains JSON null so downstream consumers can distinguish it from `{}`. +fn tool_arguments(payload: &Value) -> Value { + value_at(payload, &["tool_input"]) .or_else(|| value_at(payload, &["input"])) .or_else(|| value_at(payload, &["arguments"])) .or_else(|| value_at(payload, &["args"])) - .unwrap_or(Value::Null); - let result = value_at(payload, &["tool_output"]) + .unwrap_or(Value::Null) +} + +// Extracts tool output from success payloads first and then failure diagnostics. Failure detail +// synthesis is last so an explicit result always wins over sidecar-built diagnostic metadata. +fn tool_result(payload: &Value, normalized_event: &str) -> Value { + value_at(payload, &["tool_output"]) .or_else(|| value_at(payload, &["tool_response"])) .or_else(|| value_at(payload, &["output"])) .or_else(|| value_at(payload, &["result"])) .or_else(|| value_at(payload, &["extra", "tool_output"])) .or_else(|| value_at(payload, &["extra", "result"])) - .or_else(|| event_detail_result(payload, &normalized_event)) - .unwrap_or(Value::Null); - ToolEvent { - session_id: session.session_id, - agent_kind: kind, - event_name: session.event_name, - tool_call_id, - tool_name, - subagent_id: subagent_id(payload) - .or_else(|| header_string(headers, "x-nemo-flow-subagent-id")), - arguments, - result, - status: string_at(payload, &["status"]) - .or_else(|| string_at(payload, &["decision"])) - .or_else(|| string_at(payload, &["permission"])) - .or_else(|| { - (normalized_event.contains("failure") || normalized_event.contains("failed")) - .then_some("error".to_string()) - }) - .or_else(|| { - normalized_event - .contains("permissiondenied") - .then_some("denied".to_string()) - }), - payload: session.payload, - metadata: session.metadata, - } + .or_else(|| event_detail_result(payload, normalized_event)) + .unwrap_or(Value::Null) +} + +// Resolves explicit status fields before deriving error/denied status from event names. Derived +// status is intentionally conservative and only covers known failure or permission-denial spellings. +fn tool_status(payload: &Value, normalized_event: &str) -> Option { + first_string_at( + payload, + &[&["status"][..], &["decision"][..], &["permission"][..]], + ) + .or_else(|| { + (normalized_event.contains("failure") || normalized_event.contains("failed")) + .then_some("error".to_string()) + }) + .or_else(|| { + normalized_event + .contains("permissiondenied") + .then_some("denied".to_string()) + }) } +// Finds the most specific nested-agent identifier the sidecar knows how to interpret. Agent IDs +// are accepted as subagent IDs because several hook systems use `agent` terminology for spawned +// workers rather than for the top-level coding agent. fn subagent_id(payload: &Value) -> Option { string_at(payload, &["subagent_id"]) .or_else(|| string_at(payload, &["subagentId"])) @@ -170,6 +304,9 @@ fn subagent_id(payload: &Value) -> Option { .or_else(|| string_at(payload, &["agent", "id"])) } +// Extracts detail fields as a synthetic tool result only for failure-like hooks. Successful tool +// events without explicit output remain `null` so observers can distinguish "no output supplied" +// from "the sidecar assembled diagnostic details". fn event_detail_result(payload: &Value, normalized_event: &str) -> Option { let include_details = normalized_event.contains("failure") || normalized_event.contains("failed") @@ -187,6 +324,9 @@ fn event_detail_result(payload: &Value, normalized_event: &str) -> Option (!object.is_empty()).then_some(Value::Object(object)) } +// Reads a nested value as a string, accepting numbers and booleans for agent schemas that encode +// identifiers or flags without string types. Empty strings are treated as absent to preserve +// fallback ordering. fn string_at(payload: &Value, path: &[&str]) -> Option { value_at(payload, path) .and_then(|value| match value { @@ -198,6 +338,8 @@ fn string_at(payload: &Value, path: &[&str]) -> Option { .filter(|value| !value.is_empty()) } +// Returns a cloned nested JSON value using exact object-key traversal. Missing intermediate keys +// stop the lookup without error so callers can chain schema fallbacks cheaply. fn value_at(payload: &Value, path: &[&str]) -> Option { let mut current = payload; for key in path { @@ -206,6 +348,9 @@ fn value_at(payload: &Value, path: &[&str]) -> Option { Some(current.clone()) } +// Classifies a raw hook event using adapter-specific lifecycle names first and generic sidecar +// names second. Unknown events are intentionally converted to hook marks, not errors, so new agent +// hook types remain observable until first-class normalization rules are added. fn classify( payload: &Value, headers: &HeaderMap, @@ -252,10 +397,11 @@ fn classify( } else { match normalized.as_str() { "beforesubmitprompt" | "promptsubmitted" | "userpromptsubmit" => { - NormalizedEvent::PromptSubmitted(common_session_event(payload, headers, rules.kind)) + NormalizedEvent::LlmHint(common_llm_hint_event(payload, headers, rules.kind)) } - "afteragentresponse" | "agentresponse" | "assistantresponse" => { - NormalizedEvent::AgentResponse(common_session_event(payload, headers, rules.kind)) + "afteragentresponse" | "agentresponse" | "assistantresponse" | "afteragentthought" + | "prellmcall" | "postllmcall" | "stop" => { + NormalizedEvent::LlmHint(common_llm_hint_event(payload, headers, rules.kind)) } "precompact" | "compaction" => { NormalizedEvent::Compaction(common_session_event(payload, headers, rules.kind)) @@ -268,6 +414,8 @@ fn classify( } } +// Removes separators and case differences before comparing hook names. The sidecar uses this for +// agent-specific aliases so `PostToolUse`, `post_tool_use`, and `postToolUse` converge. fn normalize_name(name: &str) -> String { name.chars() .filter(|character| character.is_ascii_alphanumeric()) diff --git a/crates/sidecar/src/config.rs b/crates/sidecar/src/config.rs index 42dcb87..3fb7121 100644 --- a/crates/sidecar/src/config.rs +++ b/crates/sidecar/src/config.rs @@ -178,6 +178,9 @@ pub(crate) struct SessionConfig { } impl SidecarConfig { + // Resolves per-session settings from hook/gateway headers with process config as fallback. + // Header JSON fields are parsed opportunistically; invalid JSON is treated as absent here + // because install and hook-forward validate generated header values before sending them. pub(crate) fn session_config_from_headers(&self, headers: &HeaderMap) -> SessionConfig { let atif_dir = header_string(headers, "x-nemo-flow-atif-dir") .map(PathBuf::from) @@ -227,6 +230,9 @@ pub(crate) struct CursorAgentConfig { } impl Default for CursorAgentConfig { + // Keeps Cursor run-mode patching enabled unless a config file opts out. Cursor's CLI discovers + // hooks from project files, so the launcher needs permission to temporarily patch and restore + // `.cursor/hooks.json` by default. fn default() -> Self { Self { command: None, @@ -287,6 +293,9 @@ struct FileCursorAgentConfig { } impl Default for SidecarConfig { + // Supplies conservative local gateway defaults: bind only to loopback, route OpenAI and + // Anthropic requests to their public bases, and leave exporters/plugins disabled until config, + // environment, or headers explicitly opt in. fn default() -> Self { Self { bind: "127.0.0.1:4040" @@ -302,12 +311,21 @@ impl Default for SidecarConfig { } } +/// Resolves server-mode configuration from shared config files plus server CLI/environment overrides. +/// +/// File discovery and merge behavior live in `load_shared_config`; this function only applies the +/// server-facing command-line layer so launcher-only settings cannot leak into daemon mode. pub(crate) fn resolve_server_config(args: &ServerArgs) -> Result { let mut resolved = load_shared_config(args.config.as_ref())?; apply_server_overrides(&mut resolved.sidecar, args); Ok(resolved) } +/// Resolves transparent `run` configuration and switches the sidecar to an ephemeral bind address. +/// +/// Explicit run arguments override inherited top-level server flags, which override shared config. +/// Session metadata and plugin config are parsed as JSON here so malformed CLI values fail before +/// the child agent is spawned. pub(crate) fn resolve_run_config( command: &RunCommand, inherited: Option<&ServerArgs>, @@ -320,30 +338,58 @@ pub(crate) fn resolve_run_config( if let Some(args) = inherited { apply_server_overrides(&mut resolved.sidecar, args); } + apply_run_overrides(&mut resolved.sidecar, command)?; + resolved.sidecar.bind = "127.0.0.1:0" + .parse() + .expect("valid transparent bind address"); + Ok(resolved) +} + +// Applies subcommand-specific `run` overrides after inherited top-level flags. JSON-bearing fields +// are parsed here so invalid metadata or plugin config fails before the sidecar binds a port. +fn apply_run_overrides( + config: &mut SidecarConfig, + command: &RunCommand, +) -> Result<(), SidecarError> { + apply_run_url_overrides(config, command); + apply_run_json_overrides(config, command)?; + Ok(()) +} + +// Applies plain string/path run overrides. These fields do not need parsing, so they stay separate +// from JSON options whose errors should include field context. +fn apply_run_url_overrides(config: &mut SidecarConfig, command: &RunCommand) { if let Some(value) = &command.openai_base_url { - resolved.sidecar.openai_base_url = value.clone(); + config.openai_base_url = value.clone(); } if let Some(value) = &command.anthropic_base_url { - resolved.sidecar.anthropic_base_url = value.clone(); + config.anthropic_base_url = value.clone(); } if let Some(value) = &command.atif_dir { - resolved.sidecar.atif_dir = Some(value.clone()); + config.atif_dir = Some(value.clone()); } if let Some(value) = &command.openinference_endpoint { - resolved.sidecar.openinference_endpoint = Some(value.clone()); + config.openinference_endpoint = Some(value.clone()); } +} + +// Parses JSON-bearing run overrides after simple values. Invalid metadata or plugin config fails +// before transparent run mode binds its ephemeral sidecar listener. +fn apply_run_json_overrides( + config: &mut SidecarConfig, + command: &RunCommand, +) -> Result<(), SidecarError> { if let Some(value) = &command.session_metadata { - resolved.sidecar.metadata = Some(parse_json_option("session metadata", value)?); + config.metadata = Some(parse_json_option("session metadata", value)?); } if let Some(value) = &command.plugin_config { - resolved.sidecar.plugin_config = Some(parse_json_option("plugin config", value)?); + config.plugin_config = Some(parse_json_option("plugin config", value)?); } - resolved.sidecar.bind = "127.0.0.1:0" - .parse() - .expect("valid transparent bind address"); - Ok(resolved) + Ok(()) } +// Applies direct server flags on top of already-merged configuration. Only present options mutate +// the config so lower-priority file values survive when a flag was omitted. fn apply_server_overrides(config: &mut SidecarConfig, args: &ServerArgs) { if let Some(value) = args.bind { config.bind = value; @@ -362,6 +408,9 @@ fn apply_server_overrides(config: &mut SidecarConfig, args: &ServerArgs) { } } +// Loads config from the ordered shared locations, deep-merges TOML tables, maps the typed file +// shape onto runtime structs, then lets environment variables override file values. Invalid TOML +// or typed shapes fail closed because they indicate an operator configuration error. fn load_shared_config(explicit: Option<&PathBuf>) -> Result { let mut merged = toml::Value::Table(toml::map::Map::new()); for path in config_paths(explicit) { @@ -385,6 +434,8 @@ fn load_shared_config(explicit: Option<&PathBuf>) -> Result) -> Vec { if let Some(path) = explicit { return vec![path.clone()]; @@ -401,6 +452,8 @@ fn config_paths(explicit: Option<&PathBuf>) -> Vec { paths } +// Walks upward from the current directory and returns the nearest project-local sidecar config. +// The first hit wins so nested projects can override parent workspace defaults. fn find_project_config(start: &std::path::Path) -> Option { for ancestor in start.ancestors() { let path = ancestor.join(".nemo-flow/sidecar.toml"); @@ -411,6 +464,8 @@ fn find_project_config(start: &std::path::Path) -> Option { None } +// Resolves the user config using XDG first and HOME/USERPROFILE second. Returning `None` keeps +// config loading portable in minimal environments where no home directory is visible. fn user_config_path() -> Option { if let Some(base) = std::env::var_os("XDG_CONFIG_HOME") { return Some(PathBuf::from(base).join("nemo-flow/sidecar.toml")); @@ -418,6 +473,9 @@ fn user_config_path() -> Option { home_dir().map(|home| home.join(".config/nemo-flow/sidecar.toml")) } +// Applies the typed TOML config model to the resolved runtime config. Missing sections and fields +// are ignored, preserving defaults and prior merge layers; Cursor's patch-restore flag is only +// changed when explicitly present. fn apply_file_config( resolved: &mut ResolvedConfig, value: toml::Value, @@ -425,51 +483,83 @@ fn apply_file_config( let config: FileConfig = value.try_into().map_err(|error| { SidecarError::Config(format!("invalid sidecar configuration shape: {error}")) })?; - if let Some(server) = config.server { - if let Some(value) = server.openai_base_url { - resolved.sidecar.openai_base_url = value; - } - if let Some(value) = server.anthropic_base_url { - resolved.sidecar.anthropic_base_url = value; - } + apply_file_server_config(&mut resolved.sidecar, config.server); + apply_file_session_config(&mut resolved.sidecar, config.session); + apply_file_export_config(&mut resolved.sidecar, config.export); + apply_file_agents_config(&mut resolved.agents, config.agents); + Ok(()) +} + +// Applies provider upstream defaults from file config. These values are the upstream targets used +// by direct sidecar server mode; transparent `run` mode can still override them per invocation. +fn apply_file_server_config(sidecar: &mut SidecarConfig, server: Option) { + let Some(server) = server else { + return; + }; + if let Some(value) = server.openai_base_url { + sidecar.openai_base_url = value; } - if let Some(session) = config.session { - if let Some(value) = session.atif_dir { - resolved.sidecar.atif_dir = Some(value); - } - if let Some(value) = session.metadata { - resolved.sidecar.metadata = Some(value); - } - if let Some(value) = session.plugin_config { - resolved.sidecar.plugin_config = Some(value); - } + if let Some(value) = server.anthropic_base_url { + sidecar.anthropic_base_url = value; } - if let Some(export) = config.export - && let Some(openinference) = export.openinference +} + +// Applies session-level exporter and metadata defaults. Missing optional fields leave earlier +// merge layers intact, which preserves global or project defaults when user config is partial. +fn apply_file_session_config(sidecar: &mut SidecarConfig, session: Option) { + let Some(session) = session else { + return; + }; + if let Some(value) = session.atif_dir { + sidecar.atif_dir = Some(value); + } + if let Some(value) = session.metadata { + sidecar.metadata = Some(value); + } + if let Some(value) = session.plugin_config { + sidecar.plugin_config = Some(value); + } +} + +// Applies optional OpenInference export config. The nested shape mirrors the docs and leaves room +// for future exporter-specific fields without changing the top-level config parser. +fn apply_file_export_config(sidecar: &mut SidecarConfig, export: Option) { + let Some(export) = export else { + return; + }; + if let Some(openinference) = export.openinference && let Some(value) = openinference.endpoint { - resolved.sidecar.openinference_endpoint = Some(value); + sidecar.openinference_endpoint = Some(value); } - if let Some(agents) = config.agents { - if let Some(value) = agents.claude_code { - resolved.agents.claude_code.command = value.command; - } - if let Some(value) = agents.codex { - resolved.agents.codex.command = value.command; - } - if let Some(value) = agents.cursor { - resolved.agents.cursor.command = value.command; - if let Some(patch_restore_hooks) = value.patch_restore_hooks { - resolved.agents.cursor.patch_restore_hooks = patch_restore_hooks; - } - } - if let Some(value) = agents.hermes { - resolved.agents.hermes.command = value.command; +} + +// Applies configured agent commands and Cursor's temporary-hook behavior. Cursor's +// `patch_restore_hooks` flag is intentionally tri-state in file config so omitted values preserve +// the safe default while explicit `false` disables temporary hook mutation. +fn apply_file_agents_config(agents: &mut AgentConfigs, file_agents: Option) { + let Some(file_agents) = file_agents else { + return; + }; + if let Some(value) = file_agents.claude_code { + agents.claude_code.command = value.command; + } + if let Some(value) = file_agents.codex { + agents.codex.command = value.command; + } + if let Some(value) = file_agents.cursor { + agents.cursor.command = value.command; + if let Some(patch_restore_hooks) = value.patch_restore_hooks { + agents.cursor.patch_restore_hooks = patch_restore_hooks; } } - Ok(()) + if let Some(value) = file_agents.hermes { + agents.hermes.command = value.command; + } } +// Applies environment variables after file configuration. Invalid bind values are ignored here to +// preserve existing startup behavior, while string/path values replace earlier layers when present. fn apply_env_config(config: &mut SidecarConfig) { if let Ok(value) = std::env::var("NEMO_FLOW_SIDECAR_BIND") && let Ok(value) = value.parse() @@ -490,6 +580,8 @@ fn apply_env_config(config: &mut SidecarConfig) { } } +// Recursively merges TOML tables and replaces scalar/array values from the higher-priority side. +// This lets user/project configs override individual nested keys without restating whole sections. fn merge_toml(left: &mut toml::Value, right: toml::Value) { match (left, right) { (toml::Value::Table(left), toml::Value::Table(right)) => { @@ -506,17 +598,25 @@ fn merge_toml(left: &mut toml::Value, right: toml::Value) { } } +// Parses JSON-valued CLI options into runtime metadata/config values and labels errors with the +// user-facing option name so callers can report which structured argument was malformed. fn parse_json_option(name: &str, value: &str) -> Result { serde_json::from_str::(value) .map_err(|error| SidecarError::Config(format!("invalid {name}: {error}"))) } +// Resolves a cross-platform home directory from environment only. The sidecar avoids extra OS +// lookups here so tests can control install/config locations by setting env variables. fn home_dir() -> Option { std::env::var_os("HOME") .or_else(|| std::env::var_os("USERPROFILE")) .map(PathBuf::from) } +/// Reads a non-empty UTF-8 header value as an owned string. +/// +/// Invalid header bytes and empty strings are treated as absent so callers can preserve their +/// explicit fallback order without surfacing HTTP parsing details as sidecar errors. pub(crate) fn header_string(headers: &HeaderMap, name: &str) -> Option { headers .get(name) @@ -530,6 +630,8 @@ fn header_json(headers: &HeaderMap, name: &str) -> Option { } impl CodingAgent { + // Returns the sidecar hook endpoint for the agent. These paths are stable integration surface + // because installed hook commands persist them in user or project configuration. pub(crate) const fn hook_path(self) -> &'static str { match self { Self::ClaudeCode => "/hooks/claude-code", @@ -539,6 +641,8 @@ impl CodingAgent { } } + // Returns the CLI spelling used in generated commands and diagnostics. The value intentionally + // matches clap's kebab-case enum names so install/run output can be copied back into commands. pub(crate) const fn as_arg(self) -> &'static str { match self { Self::ClaudeCode => "claude-code", @@ -548,6 +652,8 @@ impl CodingAgent { } } + // Infers an agent from the executable basename, accepting both canonical project names and + // common command aliases. Path components are stripped so configured absolute commands work. pub(crate) fn infer(command: &str) -> Option { let name = std::path::Path::new(command) .file_name() @@ -564,6 +670,8 @@ impl CodingAgent { } impl GatewayMode { + // Returns the installed hook-forward spelling for gateway mode headers. Keeping this separate + // from debug output prevents enum formatting changes from affecting persisted hook commands. pub(crate) const fn as_arg(self) -> &'static str { match self { Self::HookOnly => "hook-only", diff --git a/crates/sidecar/src/error.rs b/crates/sidecar/src/error.rs index 4d1451f..b591ae5 100644 --- a/crates/sidecar/src/error.rs +++ b/crates/sidecar/src/error.rs @@ -29,6 +29,9 @@ pub(crate) enum SidecarError { } impl IntoResponse for SidecarError { + // Maps sidecar errors into a compact JSON HTTP response. Bad hook payloads are client errors, + // upstream gateway failures are bad gateway responses, and local install/config/runtime faults + // remain internal errors so callers do not mistake them for agent policy decisions. fn into_response(self) -> Response { let status = match self { Self::InvalidPayload(_) => StatusCode::BAD_REQUEST, diff --git a/crates/sidecar/src/gateway.rs b/crates/sidecar/src/gateway.rs index f2a4115..7f5e7b1 100644 --- a/crates/sidecar/src/gateway.rs +++ b/crates/sidecar/src/gateway.rs @@ -13,10 +13,52 @@ use crate::error::SidecarError; use crate::server::AppState; use crate::session::{ActiveLlm, LlmGatewayStart, SessionManager}; +/// Proxies supported LLM API requests while recording a NeMo Flow LLM call around the upstream work. +/// +/// The gateway reads the full request body once so it can both forward exact bytes and derive +/// observable metadata. Upstream send/body failures close the active LLM with gateway-error +/// metadata before surfacing an HTTP error. Streaming responses are forwarded chunk-by-chunk while +/// collecting at most 1 MiB for the end event, so client-visible streaming is not delayed by +/// observability capture. pub(crate) async fn passthrough( State(state): State, request: Request, ) -> Result, SidecarError> { + let prepared = prepare_gateway_request(&state.config, request).await?; + let active = start_gateway_llm(&state.sessions, &prepared).await?; + let upstream_response = send_upstream_or_end(&state, &prepared, active.clone()).await?; + let status = upstream_response.status(); + let headers = response_headers(upstream_response.headers()); + if is_stream_response(prepared.streaming, upstream_response.headers()) { + return streaming_gateway_response( + state.sessions, + active, + status, + headers, + upstream_response, + ); + } + buffered_gateway_response(state.sessions, active, status, headers, upstream_response).await +} + +struct PreparedGatewayRequest { + method: Method, + headers: HeaderMap, + path: String, + provider: ProviderRoute, + upstream_url: String, + body_bytes: Bytes, + request_json: Value, + streaming: bool, +} + +// Validates the gateway route, buffers the request body exactly once, and derives the metadata used +// for both upstream forwarding and NeMo Flow LLM start events. Provider JSON parse failures are not +// request failures because the gateway still forwards raw bytes unchanged. +async fn prepare_gateway_request( + config: &crate::config::SidecarConfig, + request: Request, +) -> Result { let (parts, body) = request.into_parts(); let provider = ProviderRoute::from_path(parts.uri.path()).ok_or_else(|| { SidecarError::InvalidPayload(format!("unsupported gateway path {}", parts.uri.path())) @@ -26,7 +68,7 @@ pub(crate) async fn passthrough( .map_err(|error| SidecarError::InvalidPayload(error.to_string()))?; let request_json = serde_json::from_slice::(&body_bytes).unwrap_or(Value::Null); let upstream_url = provider.upstream_url( - &state.config, + config, parts .uri .path_and_query() @@ -37,40 +79,94 @@ pub(crate) async fn passthrough( .get("stream") .and_then(Value::as_bool) .unwrap_or(false); - let session_id = gateway_session_id(&parts.headers); + Ok(PreparedGatewayRequest { + method: parts.method, + headers: parts.headers, + path: parts.uri.path().to_string(), + provider, + upstream_url, + body_bytes, + request_json, + streaming, + }) +} + +// Starts the NeMo Flow LLM lifecycle for a prepared gateway request. Session and subagent +// correlation identifiers are read from headers first and then from provider body fields. +async fn start_gateway_llm( + sessions: &SessionManager, + request: &PreparedGatewayRequest, +) -> Result { let llm_request = LlmRequest { - headers: observable_headers(&parts.headers), - content: request_json.clone(), + headers: observable_headers(&request.headers), + content: request.request_json.clone(), }; - let active = state - .sessions + sessions .start_llm( - &parts.headers, + &request.headers, LlmGatewayStart { - session_id, - provider: provider.name().to_string(), - model_name: request_json + session_id: gateway_session_id(&request.headers), + provider: request.provider.name().to_string(), + model_name: request + .request_json .get("model") .and_then(Value::as_str) .map(ToOwned::to_owned), + subagent_id: gateway_subagent_id(&request.headers), + conversation_id: gateway_identifier( + &request.headers, + &request.request_json, + "x-nemo-flow-conversation-id", + &[ + &["conversation_id"], + &["conversationId"], + &["conversation", "id"], + ], + ), + generation_id: gateway_identifier( + &request.headers, + &request.request_json, + "x-nemo-flow-generation-id", + &[&["generation_id"], &["generationId"], &["generation", "id"]], + ), + request_id: gateway_identifier( + &request.headers, + &request.request_json, + "x-nemo-flow-request-id", + &[ + &["request_id"], + &["requestId"], + &["request", "id"], + &["metadata", "request_id"], + ], + ) + .or_else(|| header_string(&request.headers, "x-request-id")), request: llm_request, - streaming, - metadata: json!({ "gateway_path": parts.uri.path() }), + streaming: request.streaming, + metadata: json!({ "gateway_path": request.path }), }, ) - .await?; + .await +} +// Builds and sends the upstream request, copying only safe request headers. Send failures close the +// active LLM immediately because no response path will later own that lifecycle. +async fn send_upstream_or_end( + state: &AppState, + request: &PreparedGatewayRequest, + active: ActiveLlm, +) -> Result { let mut upstream = state .http - .request(parts.method.clone(), upstream_url) - .body(body_bytes.clone()); - for (name, value) in &parts.headers { + .request(request.method.clone(), request.upstream_url.clone()) + .body(request.body_bytes.clone()); + for (name, value) in &request.headers { if should_forward_request_header(name) { upstream = upstream.header(name, value); } } - let upstream_response = match upstream.send().await { - Ok(response) => response, + match upstream.send().await { + Ok(response) => Ok(response), Err(error) => { state .sessions @@ -80,54 +176,74 @@ pub(crate) async fn passthrough( json!({ "gateway_error": true, "stage": "send" }), ) .await?; - return Err(SidecarError::Upstream(error)); + Err(SidecarError::Upstream(error)) } - }; - let status = upstream_response.status(); - let headers = response_headers(upstream_response.headers()); - let content_type = upstream_response - .headers() + } +} + +// Determines whether the response should be proxied as a stream. The explicit request `stream` +// flag wins, but upstream SSE content type is also respected for providers that infer streaming. +fn is_stream_response(request_streaming: bool, headers: &HeaderMap) -> bool { + let content_type = headers .get(http::header::CONTENT_TYPE) .and_then(|value| value.to_str().ok()) .unwrap_or_default() .to_ascii_lowercase(); - let is_stream = streaming || content_type.contains("text/event-stream"); - - if is_stream { - let stream = upstream_response.bytes_stream(); - let body = Body::from_stream(async_stream::stream! { - let mut stream = stream; - let mut llm = StreamingLlmGuard::new(state.sessions.clone(), active, status); - let mut collected = Vec::new(); - let mut truncated = false; - while let Some(chunk) = stream.next().await { - match chunk { - Ok(bytes) => { - if collected.len() + bytes.len() <= 1_048_576 { - collected.extend_from_slice(&bytes); - } else { - truncated = true; - } - yield Ok::(bytes); - } - Err(error) => { - llm.end_error("stream", error.to_string()).await; - yield Err(error); - return; + request_streaming || content_type.contains("text/event-stream") +} + +// Builds a streaming response body that forwards chunks as they arrive while retaining a bounded +// preview for the LLM end event. Stream errors end the LLM with gateway-error metadata before the +// client sees the propagated stream error. +fn streaming_gateway_response( + sessions: SessionManager, + active: ActiveLlm, + status: StatusCode, + headers: HeaderMap, + upstream_response: reqwest::Response, +) -> Result, SidecarError> { + let stream = upstream_response.bytes_stream(); + let body = Body::from_stream(async_stream::stream! { + let mut stream = stream; + let mut llm = StreamingLlmGuard::new(sessions, active, status); + let mut collected = Vec::new(); + let mut truncated = false; + while let Some(chunk) = stream.next().await { + match chunk { + Ok(bytes) => { + if collected.len() + bytes.len() <= 1_048_576 { + collected.extend_from_slice(&bytes); + } else { + truncated = true; } + yield Ok::(bytes); + } + Err(error) => { + llm.end_error("stream", error.to_string()).await; + yield Err(error); + return; } } - let response = stream_response_json(&collected, truncated); - llm.end_success(response, truncated).await; - }); - return build_response(status, headers, body); - } + } + let response = stream_response_json(&collected, truncated); + llm.end_success(response, truncated).await; + }); + build_response(status, headers, body) +} +// Buffers a non-streaming upstream response, records its JSON body or byte count, and then returns +// the original bytes to the client. Body read errors close the LLM before surfacing upstream error. +async fn buffered_gateway_response( + sessions: SessionManager, + active: ActiveLlm, + status: StatusCode, + headers: HeaderMap, + upstream_response: reqwest::Response, +) -> Result, SidecarError> { let bytes = match upstream_response.bytes().await { Ok(bytes) => bytes, Err(error) => { - state - .sessions + sessions .end_llm( active, json!({ "error": error.to_string() }), @@ -139,8 +255,7 @@ pub(crate) async fn passthrough( }; let response_json = serde_json::from_slice::(&bytes) .unwrap_or_else(|_| json!({ "body_bytes": bytes.len() })); - state - .sessions + sessions .end_llm( active, response_json, @@ -157,6 +272,9 @@ struct StreamingLlmGuard { } impl StreamingLlmGuard { + // Creates a guard that owns the active LLM until a stream reaches exactly one terminal path. + // The option prevents double-ending when success, stream error, or drop cleanup races with + // normal control flow. fn new(sessions: SessionManager, active: ActiveLlm, status: StatusCode) -> Self { Self { sessions, @@ -165,6 +283,9 @@ impl StreamingLlmGuard { } } + // Ends a completed streaming LLM with the collected stream preview and truncation marker. + // Errors from the observability layer are swallowed because the response body has already been + // delivered to the client and the sidecar must not retroactively fail the stream. async fn end_success(&mut self, response: Value, truncated: bool) { if let Some(active) = self.active.take() { let _ = self @@ -178,6 +299,8 @@ impl StreamingLlmGuard { } } + // Ends a streaming LLM after an upstream stream error. The stage is preserved in metadata so + // observers can distinguish mid-body failures from client drops or initial send failures. async fn end_error(&mut self, stage: &'static str, error: String) { if let Some(active) = self.active.take() { let _ = self @@ -193,6 +316,9 @@ impl StreamingLlmGuard { } impl Drop for StreamingLlmGuard { + // Best-effort cleanup for streams abandoned before success or error handling runs. Drop cannot + // block, so it spawns onto the current Tokio runtime when one is available and otherwise leaves + // cleanup to process shutdown. fn drop(&mut self) { let Some(active) = self.active.take() else { return; @@ -213,6 +339,10 @@ impl Drop for StreamingLlmGuard { } } +/// Proxies OpenAI model-list requests without creating LLM runtime events. +/// +/// The route is registered as GET-only but still verifies the method so direct tests or future +/// router changes return a 405 instead of forwarding a nonsensical request upstream. pub(crate) async fn models( State(state): State, request: Request, @@ -257,6 +387,8 @@ enum ProviderRoute { } impl ProviderRoute { + // Maps public sidecar paths to known upstream provider routes. Unsupported paths return `None` + // so the caller can fail as a bad hook/gateway payload instead of constructing arbitrary URLs. fn from_path(path: &str) -> Option { match path { "/v1/responses" => Some(Self::OpenAiResponses), @@ -268,6 +400,8 @@ impl ProviderRoute { } } + // Returns the provider route name recorded in LLM event metadata. These names split OpenAI API + // variants because their request/response schemas differ even when they share a base URL. const fn name(self) -> &'static str { match self { Self::OpenAiResponses => "openai.responses", @@ -278,6 +412,9 @@ impl ProviderRoute { } } + // Builds the upstream URL by combining the configured provider base with the original path and + // query string. Trailing slashes are stripped from the base to avoid double-slash variants in + // configured enterprise or local proxy endpoints. fn upstream_url(self, config: &crate::config::SidecarConfig, path_and_query: &str) -> String { let base = match self { Self::OpenAiResponses | Self::OpenAiChatCompletions | Self::OpenAiModels => { @@ -291,11 +428,51 @@ impl ProviderRoute { } } +// Reads the gateway session id from explicit sidecar headers first, with Claude's session header +// accepted for compatibility with Claude Code environments that already propagate it. fn gateway_session_id(headers: &HeaderMap) -> Option { header_string(headers, "x-nemo-flow-session-id") .or_else(|| header_string(headers, "x-claude-code-session-id")) } +fn gateway_subagent_id(headers: &HeaderMap) -> Option { + header_string(headers, "x-nemo-flow-subagent-id") +} + +// Resolves a correlation identifier from a dedicated header before trying known JSON body paths. +// Header precedence lets callers disambiguate requests even when provider payloads contain stale +// or differently scoped identifiers. +fn gateway_identifier( + headers: &HeaderMap, + body: &Value, + header_name: &'static str, + body_paths: &[&[&str]], +) -> Option { + header_string(headers, header_name).or_else(|| { + body_paths + .iter() + .find_map(|path| string_at(body, path)) + .filter(|value| !value.is_empty()) + }) +} + +// Reads nested JSON as a string, accepting scalar numeric and boolean forms for provider metadata +// fields that are not consistently serialized as strings. Arrays and objects are ignored. +fn string_at(payload: &Value, path: &[&str]) -> Option { + let mut current = payload; + for key in path { + current = current.get(*key)?; + } + match current { + Value::String(value) => Some(value.clone()), + Value::Number(value) => Some(value.to_string()), + Value::Bool(value) => Some(value.to_string()), + _ => None, + } +} + +// Copies only non-sensitive, forwardable request headers into LLM request metadata. This preserves +// correlation headers while excluding credentials and hop-by-hop transport details. fn observable_headers(headers: &HeaderMap) -> Map { let mut output = Map::new(); for (name, value) in headers { @@ -308,6 +485,8 @@ fn observable_headers(headers: &HeaderMap) -> Map { output } +// Copies upstream response headers except hop-by-hop transport headers that Axum/hyper must manage +// for the downstream connection. Multiple values are appended to preserve provider behavior. fn response_headers(headers: &HeaderMap) -> HeaderMap { let mut output = HeaderMap::new(); for (name, value) in headers { @@ -318,6 +497,8 @@ fn response_headers(headers: &HeaderMap) -> HeaderMap { output } +// Reconstructs an Axum response from upstream status, filtered headers, and the selected body. All +// builder errors are converted into sidecar HTTP errors rather than panics. fn build_response( status: StatusCode, headers: HeaderMap, @@ -330,10 +511,16 @@ fn build_response( Ok(builder.body(body)?) } +// Allows provider request headers through unless they are transport-owned or must be recalculated +// for the forwarded body. Host and content length are intentionally excluded because reqwest sets +// them for the upstream connection. fn should_forward_request_header(name: &HeaderName) -> bool { !is_hop_by_hop(name) && name != http::header::HOST && name != http::header::CONTENT_LENGTH } +// Allows headers into observability metadata only after removing credentials and provider API keys. +// The forwarding filter runs first so hop-by-hop transport headers are also excluded from recorded +// LLM request attributes. fn should_record_header(name: &HeaderName) -> bool { should_forward_request_header(name) && name != http::header::AUTHORIZATION @@ -341,6 +528,8 @@ fn should_record_header(name: &HeaderName) -> bool { && name.as_str() != "anthropic-api-key" } +// Identifies headers that describe a single transport hop and therefore must not be proxied across +// the client-sidecar-upstream boundary. fn is_hop_by_hop(name: &HeaderName) -> bool { matches!( name.as_str(), @@ -355,6 +544,8 @@ fn is_hop_by_hop(name: &HeaderName) -> bool { ) } +// Builds the streaming end-event payload from the collected prefix. Truncated streams are marked +// explicitly so downstream analysis does not mistake the preview for a complete provider response. fn stream_response_json(collected: &[u8], truncated: bool) -> Value { if truncated { return json!({ diff --git a/crates/sidecar/src/installer.rs b/crates/sidecar/src/installer.rs index c60161e..35de379 100644 --- a/crates/sidecar/src/installer.rs +++ b/crates/sidecar/src/installer.rs @@ -20,8 +20,11 @@ const HOOK_EVENTS: &[&str] = &[ "PreToolUse", "PostToolUse", "PostToolUseFailure", + "AfterAgentResponse", + "AfterAgentThought", "SubagentStart", "SubagentStop", + "Notification", "Stop", "PreCompact", "SessionEnd", @@ -39,6 +42,7 @@ const CURSOR_HOOK_EVENTS: &[&str] = &[ "subagentStart", "subagentStop", "afterAgentResponse", + "afterAgentThought", "preCompact", "stop", "sessionEnd", @@ -54,6 +58,7 @@ const HERMES_HOOK_EVENTS: &[&str] = &[ "post_llm_call", "pre_tool_call", "post_tool_call", + "subagent_start", "subagent_stop", ]; @@ -63,46 +68,92 @@ struct PlannedFile { contents: String, } +/// Plans and optionally writes persistent hook configuration for the selected agent. +/// +/// Structured JSON options are validated before any filesystem writes, `--print` shows the exact +/// planned contents, and `--dry-run` stops before mutation. Existing files are merged rather than +/// replaced, with per-file backups created by `write_planned_file`. pub(crate) fn install(command: InstallCommand) -> Result<(), SidecarError> { validate_optional_json("session metadata", command.session_metadata.as_deref())?; validate_optional_json("plugin config", command.plugin_config.as_deref())?; let files = planned_files(&command)?; if command.print { - for file in &files { - println!("--- {}", file.path.display()); - print!("{}", file.contents); - if !file.contents.ends_with('\n') { - println!(); - } - } + print_planned_files(&files); } if command.dry_run { - println!( - "Dry run: would install {} integration for {:?} {:?}.", - command.agent.as_arg(), - command.scope, - command.target - ); + print_dry_run_summary(&command); return Ok(()); } - for file in &files { + write_planned_files(&files)?; + print_target_note(command.agent, command.target); + Ok(()) +} + +// Prints planned file contents in the same format used by installer dry-run tests. The trailing +// newline fix keeps concatenated file previews readable even when serialized contents lack one. +fn print_planned_files(files: &[PlannedFile]) { + for file in files { + println!("--- {}", file.path.display()); + print!("{}", file.contents); + if !file.contents.ends_with('\n') { + println!(); + } + } +} + +// Prints the install summary without touching the filesystem. Keeping this separate from the write +// path makes the `install` control flow read as validate, plan, preview, then mutate-or-return. +fn print_dry_run_summary(command: &InstallCommand) { + println!( + "Dry run: would install {} integration for {:?} {:?}.", + command.agent.as_arg(), + command.scope, + command.target + ); +} + +// Writes every planned file with backup behavior handled by `write_planned_file`. This helper +// centralizes the success output so per-file write semantics stay consistent across agents. +fn write_planned_files(files: &[PlannedFile]) -> Result<(), SidecarError> { + for file in files { write_planned_file(file)?; println!("Installed {}", file.path.display()); } - print_target_note(command.agent, command.target); Ok(()) } +/// Forwards a hook payload from an installed shell command to a running sidecar. +/// +/// Empty stdin is normalized to `{}` so hooks that provide no payload still generate observable +/// marks. Delivery failures are fail-open by default to avoid blocking coding agents, but +/// `--fail-closed` converts missing URLs, HTTP failures, and upstream errors into process errors. pub(crate) async fn hook_forward(command: HookForwardCommand) -> Result<(), SidecarError> { validate_optional_json("session metadata", command.session_metadata.as_deref())?; validate_optional_json("plugin config", command.plugin_config.as_deref())?; + let input = read_hook_payload()?; + let Some(url) = hook_forward_url(&command)? else { + return Ok(()); + }; + let response = send_hook_forward_request(&command, url, input).await?; + handle_hook_forward_response(response, command.fail_closed).await +} + +// Reads the native hook payload from stdin and normalizes empty payloads to JSON object syntax. +// This keeps hook commands observable even for agents or events that invoke hooks without input. +fn read_hook_payload() -> Result { let mut input = String::new(); std::io::stdin().read_to_string(&mut input)?; if input.trim().is_empty() { - input = "{}".to_string(); + Ok("{}".to_string()) + } else { + Ok(input) } +} +// Builds the target sidecar hook URL and applies fail-open/fail-closed behavior for missing +// sidecar discovery. Returning `Ok(None)` is the fail-open path used by default hook commands. +fn hook_forward_url(command: &HookForwardCommand) -> Result, SidecarError> { let Some(sidecar_url) = resolve_hook_sidecar_url( command.agent, command.sidecar_url.clone(), @@ -116,14 +167,23 @@ pub(crate) async fn hook_forward(command: HookForwardCommand) -> Result<(), Side "missing sidecar URL; pass --sidecar-url or set NEMO_FLOW_SIDECAR_URL".into(), )); } - return Ok(()); + return Ok(None); }; - let url = format!( + Ok(Some(format!( "{}{}", sidecar_url.trim_end_matches('/'), command.agent.hook_path() - ); - let response = reqwest::Client::builder() + ))) +} + +// Sends the hook payload with sidecar-specific headers translated from CLI flags. The reqwest +// transport result is returned separately so response handling can preserve fail-open semantics. +async fn send_hook_forward_request( + command: &HookForwardCommand, + url: String, + input: String, +) -> Result, SidecarError> { + Ok(reqwest::Client::builder() .timeout(HOOK_FORWARD_TIMEOUT) .build()? .post(url) @@ -138,15 +198,22 @@ pub(crate) async fn hook_forward(command: HookForwardCommand) -> Result<(), Side .header(CONTENT_TYPE, "application/json") .body(input) .send() - .await; + .await) +} +// Handles hook delivery results without changing agent control flow unless `--fail-closed` was +// requested. Successful non-empty endpoint bodies are printed verbatim for the invoking hook API. +async fn handle_hook_forward_response( + response: Result, + fail_closed: bool, +) -> Result<(), SidecarError> { match response { Ok(response) => { let status = response.status(); let body = response.text().await.unwrap_or_default(); if !status.is_success() { eprintln!("nemo-flow-sidecar hook forward failed with HTTP {status}"); - if command.fail_closed { + if fail_closed { return Err(SidecarError::Install(format!( "hook forward failed with HTTP {status}" ))); @@ -160,7 +227,7 @@ pub(crate) async fn hook_forward(command: HookForwardCommand) -> Result<(), Side } Err(error) => { eprintln!("nemo-flow-sidecar hook forward failed: {error}"); - if command.fail_closed { + if fail_closed { Err(SidecarError::Upstream(error)) } else { Ok(()) @@ -169,6 +236,9 @@ pub(crate) async fn hook_forward(command: HookForwardCommand) -> Result<(), Side } } +// Chooses the sidecar URL for hook-forward. Hermes prefers the runtime environment URL because +// its hooks are commonly installed persistently but reused by `run --agent hermes` with an +// ephemeral sidecar; other agents prefer the installed command URL for stable configuration. fn resolve_hook_sidecar_url( agent: CodingAgent, command_url: Option, @@ -182,66 +252,103 @@ fn resolve_hook_sidecar_url( } } +// Builds the exact files that would be written for an install command. Each agent keeps its native +// config format: Claude/Cursor/Codex hook JSON, Codex feature TOML, and Hermes YAML translated +// through the shared JSON hook merge logic. fn planned_files(command: &InstallCommand) -> Result, SidecarError> { let base = install_base(command)?; match command.agent { - CodingAgent::ClaudeCode => { - let path = base.join(".claude/settings.json"); - let existing = read_json_file(&path)?; - let contents = serde_json::to_string_pretty(&merge_hooks( - existing, - claude_hooks(&hook_command(command, CodingAgent::ClaudeCode)), - )?) - .map_err(|error| SidecarError::Install(error.to_string()))?; - Ok(vec![PlannedFile { path, contents }]) - } - CodingAgent::Codex => { - let config_path = base.join(".codex/config.toml"); - let hooks_path = base.join(".codex/hooks.json"); - let config = - merge_codex_config(&std::fs::read_to_string(&config_path).unwrap_or_default())?; - let hooks = serde_json::to_string_pretty(&merge_hooks( - read_json_file(&hooks_path)?, - codex_hooks(&hook_command(command, CodingAgent::Codex)), - )?) - .map_err(|error| SidecarError::Install(error.to_string()))?; - Ok(vec![ - PlannedFile { - path: config_path, - contents: config, - }, - PlannedFile { - path: hooks_path, - contents: hooks, - }, - ]) - } - CodingAgent::Cursor => { - let path = base.join(".cursor/hooks.json"); - let existing = read_json_file(&path)?; - let contents = serde_json::to_string_pretty(&merge_hooks( - existing, - cursor_hooks(&hook_command(command, CodingAgent::Cursor)), - )?) - .map_err(|error| SidecarError::Install(error.to_string()))?; - Ok(vec![PlannedFile { path, contents }]) - } - CodingAgent::Hermes => { - let path = base.join(".hermes/config.yaml"); - let existing = match std::fs::read_to_string(&path) { - Ok(raw) => raw, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(), - Err(error) => return Err(SidecarError::Io(error)), - }; - let contents = merge_hermes_config( - &existing, - hermes_hooks(&hook_command(command, CodingAgent::Hermes)), - )?; - Ok(vec![PlannedFile { path, contents }]) - } + CodingAgent::ClaudeCode => planned_claude_file(command, &base), + CodingAgent::Codex => planned_codex_files(command, &base), + CodingAgent::Cursor => planned_cursor_file(command, &base), + CodingAgent::Hermes => planned_hermes_file(command, &base), } } +// Plans the Claude settings file by merging generated hook groups into existing JSON settings. +// Claude's plugin-dir transparent mode uses a separate temporary file path handled by launcher. +fn planned_claude_file( + command: &InstallCommand, + base: &Path, +) -> Result, SidecarError> { + let path = base.join(".claude/settings.json"); + Ok(vec![planned_json_hooks_file( + path, + claude_hooks(&hook_command(command, CodingAgent::ClaudeCode)), + )?]) +} + +// Plans both Codex files: feature enablement in TOML and generated hook groups in JSON. The TOML +// merge intentionally leaves unrelated provider configuration untouched. +fn planned_codex_files( + command: &InstallCommand, + base: &Path, +) -> Result, SidecarError> { + let config_path = base.join(".codex/config.toml"); + let hooks_path = base.join(".codex/hooks.json"); + Ok(vec![ + PlannedFile { + path: config_path.clone(), + contents: merge_codex_config( + &std::fs::read_to_string(&config_path).unwrap_or_default(), + )?, + }, + planned_json_hooks_file( + hooks_path, + codex_hooks(&hook_command(command, CodingAgent::Codex)), + )?, + ]) +} + +// Plans Cursor's project hook file using the shared JSON hook merge behavior. Cursor transparent +// runs patch and restore this same path dynamically instead of writing persistent config. +fn planned_cursor_file( + command: &InstallCommand, + base: &Path, +) -> Result, SidecarError> { + let path = base.join(".cursor/hooks.json"); + Ok(vec![planned_json_hooks_file( + path, + cursor_hooks(&hook_command(command, CodingAgent::Cursor)), + )?]) +} + +// Plans Hermes YAML config by translating through the shared hook map format. Missing files are +// treated as empty config, while unreadable files fail rather than overwriting user state. +fn planned_hermes_file( + command: &InstallCommand, + base: &Path, +) -> Result, SidecarError> { + let path = base.join(".hermes/config.yaml"); + let existing = read_optional_text_file(&path)?; + let contents = merge_hermes_config( + &existing, + hermes_hooks(&hook_command(command, CodingAgent::Hermes)), + )?; + Ok(vec![PlannedFile { path, contents }]) +} + +// Reads an optional text file for config formats where missing files are valid install targets. +// Non-not-found I/O errors still propagate to avoid losing existing user configuration. +fn read_optional_text_file(path: &Path) -> Result { + match std::fs::read_to_string(path) { + Ok(raw) => Ok(raw), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(String::new()), + Err(error) => Err(SidecarError::Io(error)), + } +} + +// Produces a planned JSON hook file by reading existing JSON, merging generated hooks, and +// formatting the result consistently with the package hook bundles. +fn planned_json_hooks_file(path: PathBuf, generated: Value) -> Result { + let existing = read_json_file(&path)?; + let contents = serde_json::to_string_pretty(&merge_hooks(existing, generated)?) + .map_err(|error| SidecarError::Install(error.to_string()))?; + Ok(PlannedFile { path, contents }) +} + +// Resolves the installation root according to user or project scope. Hidden test-only overrides +// take precedence so coverage can avoid touching real home/project directories. fn install_base(command: &InstallCommand) -> Result { match command.scope { InstallScope::User => command @@ -258,6 +365,9 @@ fn install_base(command: &InstallCommand) -> Result { } } +// Builds the shell command persisted into hook configuration. Optional sidecar settings are turned +// into hook-forward flags and every argument is shell-quoted because most target hook systems store +// the command as a single shell string. fn hook_command(command: &InstallCommand, agent: CodingAgent) -> String { let mut args = vec![ "nemo-flow-sidecar".to_string(), @@ -290,6 +400,8 @@ fn hook_command(command: &InstallCommand, agent: CodingAgent) -> String { .join(" ") } +// Appends a flag/value pair only when a string option is present, preserving omission semantics in +// generated hook commands instead of serializing empty values. fn push_optional(args: &mut Vec, flag: &str, value: Option<&str>) { if let Some(value) = value { args.push(flag.to_string()); @@ -297,6 +409,8 @@ fn push_optional(args: &mut Vec, flag: &str, value: Option<&str>) { } } +// Appends optional path flags using display formatting because installed commands are read by a +// shell, not by Rust path parsers. fn push_optional_path(args: &mut Vec, flag: &str, value: Option<&Path>) { if let Some(value) = value { args.push(flag.to_string()); @@ -304,6 +418,8 @@ fn push_optional_path(args: &mut Vec, flag: &str, value: Option<&Path>) } } +// Serializes the gateway-mode enum into the generated hook-forward command only when explicitly +// configured, leaving default runtime behavior under the sidecar's normal config resolution. fn push_optional_gateway_mode(args: &mut Vec, gateway_mode: Option) { if let Some(gateway_mode) = gateway_mode { args.push("--gateway-mode".to_string()); @@ -311,6 +427,8 @@ fn push_optional_gateway_mode(args: &mut Vec, gateway_mode: Option String { if value .chars() @@ -322,6 +440,10 @@ fn shell_quote(value: &str) -> String { } } +/// Generates native hook configuration for the selected agent. +/// +/// The returned value always has a top-level `hooks` object, but Hermes uses its simpler command +/// group shape while Claude/Codex/Cursor use command hook groups with optional tool matchers. pub(crate) fn generated_hooks(agent: CodingAgent, command: &str) -> Value { match agent { CodingAgent::ClaudeCode => claude_hooks(command), @@ -347,6 +469,8 @@ fn cursor_hooks(command: &str) -> Value { hooks_for_events(CURSOR_HOOK_EVENTS, command, true) } +// Generates Hermes YAML-compatible hook groups. Hermes expects direct command entries rather than +// the nested `type = command` group format used by Claude, Codex, and Cursor. fn hermes_hooks(command: &str) -> Value { let hooks: serde_json::Map = HERMES_HOOK_EVENTS .iter() @@ -363,6 +487,9 @@ fn hermes_hooks(command: &str) -> Value { json!({ "hooks": Value::Object(hooks) }) } +// Generates hook groups for all requested events and adds a wildcard matcher to tool events when +// the target agent requires matcher-scoped tool hooks. Non-tool events omit matchers so they fire +// for the full lifecycle. fn hooks_for_events(events: &[&str], command: &str, matcher_for_tools: bool) -> Value { let hooks: serde_json::Map = events .iter() @@ -388,6 +515,8 @@ fn hooks_for_events(events: &[&str], command: &str, matcher_for_tools: bool) -> json!({ "hooks": Value::Object(hooks) }) } +// Identifies hook events that should receive wildcard tool matchers. The list includes current +// Claude/Codex spellings plus Cursor shell/MCP names so generated config stays agent-compatible. fn event_matches_tools(event: &str) -> bool { matches!( event, @@ -404,43 +533,79 @@ fn event_matches_tools(event: &str) -> bool { ) } +/// Merges generated hook groups into an existing hook configuration without duplicating groups. +/// +/// Missing files are represented by `Null` and become empty objects. Existing non-object roots, +/// non-object `hooks`, non-array event hooks, or malformed generated hooks fail closed because +/// writing through those shapes would corrupt user configuration. pub(crate) fn merge_hooks(existing: Value, generated: Value) -> Result { - let mut root = match existing { - Value::Null => json!({}), - Value::Object(object) => Value::Object(object), - _ => { - return Err(SidecarError::Install( - "hook config must be a JSON object".into(), - )); - } - }; - let root_object = root.as_object_mut().expect("root checked as object"); - let hooks = root_object + let mut root = hook_config_root(existing)?; + let hooks = hooks_object_mut(&mut root)?; + let generated_hooks = generated_hooks_object(&generated)?; + for (event, groups) in generated_hooks { + merge_event_hook_groups(hooks, event, groups)?; + } + Ok(root) +} + +// Normalizes an existing hook config root. Missing files arrive as `Null`, valid JSON/YAML config +// roots remain objects, and other shapes are rejected before any install write can occur. +fn hook_config_root(existing: Value) -> Result { + match existing { + Value::Null => Ok(json!({})), + Value::Object(object) => Ok(Value::Object(object)), + _ => Err(SidecarError::Install( + "hook config must be a JSON object".into(), + )), + } +} + +// Returns the mutable `hooks` object from a config root, creating it when absent. A non-object +// `hooks` field is considered user config corruption and is not overwritten. +fn hooks_object_mut(root: &mut Value) -> Result<&mut serde_json::Map, SidecarError> { + root.as_object_mut() + .expect("root checked as object") .entry("hooks") .or_insert_with(|| json!({})) .as_object_mut() - .ok_or_else(|| SidecarError::Install("hooks must be a JSON object".into()))?; - let generated_hooks = generated + .ok_or_else(|| SidecarError::Install("hooks must be a JSON object".into())) +} + +// Validates generated hook shape before merging. Generated hooks are internal data, but checking +// here keeps test failures localized if an agent bundle generator regresses. +fn generated_hooks_object( + generated: &Value, +) -> Result<&serde_json::Map, SidecarError> { + generated .get("hooks") .and_then(Value::as_object) - .ok_or_else(|| SidecarError::Install("generated hooks were malformed".into()))?; - for (event, groups) in generated_hooks { - let groups = groups - .as_array() - .ok_or_else(|| SidecarError::Install("generated hook groups were malformed".into()))?; - let event_groups = hooks.entry(event.clone()).or_insert_with(|| json!([])); - let event_groups = event_groups - .as_array_mut() - .ok_or_else(|| SidecarError::Install(format!("{event} hooks must be an array")))?; - for group in groups { - if !event_groups.iter().any(|existing| existing == group) { - event_groups.push(group.clone()); - } + .ok_or_else(|| SidecarError::Install("generated hooks were malformed".into())) +} + +// Appends missing generated groups for one hook event. Equality comparison is exact so repeated +// installs are idempotent without trying to interpret vendor-specific hook group schemas. +fn merge_event_hook_groups( + hooks: &mut serde_json::Map, + event: &str, + groups: &Value, +) -> Result<(), SidecarError> { + let groups = groups + .as_array() + .ok_or_else(|| SidecarError::Install("generated hook groups were malformed".into()))?; + let event_groups = hooks.entry(event.to_string()).or_insert_with(|| json!([])); + let event_groups = event_groups + .as_array_mut() + .ok_or_else(|| SidecarError::Install(format!("{event} hooks must be an array")))?; + for group in groups { + if !event_groups.iter().any(|existing| existing == group) { + event_groups.push(group.clone()); } } - Ok(root) + Ok(()) } +// Enables Codex hook support in TOML without rewriting unrelated config. Empty config creates a +// new document; malformed TOML fails before any install writes occur. fn merge_codex_config(existing: &str) -> Result { let mut document = if existing.trim().is_empty() { DocumentMut::new() @@ -456,6 +621,8 @@ fn merge_codex_config(existing: &str) -> Result { Ok(document.to_string()) } +// Parses Hermes YAML, merges generated hooks through the shared JSON hook merger, and serializes +// back to YAML. Empty files are treated as no existing configuration. fn merge_hermes_config(existing: &str, generated: Value) -> Result { let existing = if existing.trim().is_empty() { Value::Null @@ -468,6 +635,10 @@ fn merge_hermes_config(existing: &str, generated: Value) -> Result Result { match std::fs::read_to_string(path) { Ok(raw) => serde_json::from_str(&raw).map_err(|error| { @@ -478,6 +649,8 @@ pub(crate) fn read_json_file(path: &Path) -> Result { } } +// Writes one planned file, creating parents and backing up any existing file first. Backup naming +// is delegated to `backup_path` so the original extension is preserved in the backup filename. fn write_planned_file(file: &PlannedFile) -> Result<(), SidecarError> { if let Some(parent) = file.path.parent() { std::fs::create_dir_all(parent)?; @@ -489,6 +662,8 @@ fn write_planned_file(file: &PlannedFile) -> Result<(), SidecarError> { Ok(()) } +// Builds a timestamped backup path beside the original file. If a file has no extension, `config` +// is used so backup names remain recognizable. fn backup_path(path: &Path) -> Result { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -502,12 +677,16 @@ fn backup_path(path: &Path) -> Result { ))) } +// Resolves a cross-platform home directory from environment variables only, matching config +// resolution and keeping installer tests isolated through env/test overrides. fn home_dir() -> Option { std::env::var_os("HOME") .or_else(|| std::env::var_os("USERPROFILE")) .map(PathBuf::from) } +// Validates optional JSON strings before they are embedded into generated hook-forward commands or +// headers. This catches quoting/config mistakes during install rather than during a later hook run. fn validate_optional_json(name: &str, value: Option<&str>) -> Result<(), SidecarError> { if let Some(value) = value { serde_json::from_str::(value) @@ -516,6 +695,8 @@ fn validate_optional_json(name: &str, value: Option<&str>) -> Result<(), Sidecar Ok(()) } +// Converts optional session/export/gateway settings into sidecar headers for hook-forward. Each +// absent value is omitted so the server can fall back to file, environment, or default config. fn sidecar_headers( atif_dir: Option<&Path>, openinference_endpoint: Option<&str>, @@ -546,6 +727,8 @@ fn sidecar_headers( Ok(headers) } +// Inserts one optional header after validating it is legal HTTP header text. Invalid values are +// reported as installer errors because they came from generated or user-provided hook options. fn insert_header( headers: &mut HeaderMap, name: &'static str, @@ -562,6 +745,8 @@ fn insert_header( Ok(()) } +// Converts an optional filesystem path to a header value using loss-tolerant display text. This +// mirrors installed shell command behavior, where paths are passed as strings. fn insert_header_path( headers: &mut HeaderMap, name: &'static str, @@ -575,6 +760,8 @@ fn insert_header_path( } } +// Prints agent/target-specific follow-up notes for limitations that cannot be encoded directly in +// hook files, such as GUI/cloud behavior or Hermes consent requirements. fn print_target_note(agent: CodingAgent, target: InstallTarget) { match (agent, target) { (CodingAgent::ClaudeCode, InstallTarget::Gui | InstallTarget::Both) => { diff --git a/crates/sidecar/src/launcher.rs b/crates/sidecar/src/launcher.rs index 1d58943..62c6af2 100644 --- a/crates/sidecar/src/launcher.rs +++ b/crates/sidecar/src/launcher.rs @@ -10,94 +10,164 @@ use serde_json::{Value, json}; use tokio::net::TcpListener; use tokio::process::Command; use tokio::sync::oneshot; +use tokio::task::JoinHandle; use crate::config::{ - AgentConfigs, CodingAgent, ResolvedConfig, RunCommand, ServerArgs, resolve_run_config, + AgentConfigs, CodingAgent, ResolvedConfig, RunCommand, ServerArgs, SidecarConfig, + resolve_run_config, }; use crate::error::SidecarError; use crate::installer::{generated_hooks, hook_forward_command, merge_hooks, read_json_file}; use crate::server; +/// Runs a child coding-agent command behind an ephemeral local sidecar. +/// +/// The sidecar binds to an OS-assigned loopback port, prepares agent-specific hook/gateway wiring, +/// waits for health before spawning the child, and restores temporary files after the child and +/// server shut down. The child's exit status is preserved when it fits in `ExitCode`; otherwise the +/// launcher reports generic failure. pub(crate) async fn run( command: RunCommand, inherited: Option<&ServerArgs>, ) -> Result { - let mut resolved = resolve_run_config(&command, inherited)?; - let (agent, argv) = resolve_agent_and_argv(&command, &resolved.agents)?; - let listener = TcpListener::bind("127.0.0.1:0").await?; - let address = listener.local_addr()?; - let sidecar_url = format!("http://{address}"); - resolved.sidecar.bind = address; - - let prepared = PreparedRun::new(agent, argv, &sidecar_url, &resolved, command.dry_run)?; - if command.print || command.dry_run { - prepared.print(agent, &sidecar_url, &resolved); + let run = TransparentRun::new(command, inherited).await?; + run.print_if_requested(); + run.execute().await +} + +struct TransparentRun { + agent: CodingAgent, + prepared: PreparedRun, + resolved: ResolvedConfig, + listener: TcpListener, + sidecar_url: String, + dry_run: bool, + print: bool, +} + +impl TransparentRun { + // Resolves configuration, binds the ephemeral listener, and builds agent-specific launch wiring + // without starting the sidecar or spawning the child command. + async fn new( + command: RunCommand, + inherited: Option<&ServerArgs>, + ) -> Result { + let dry_run = command.dry_run; + let print = command.print; + let mut resolved = resolve_run_config(&command, inherited)?; + let (agent, argv) = resolve_agent_and_argv(&command, &resolved.agents)?; + let listener = TcpListener::bind("127.0.0.1:0").await?; + let address = listener.local_addr()?; + let sidecar_url = format!("http://{address}"); + resolved.sidecar.bind = address; + + let prepared = PreparedRun::new(agent, argv, &sidecar_url, &resolved, dry_run)?; + Ok(Self { + agent, + prepared, + resolved, + listener, + sidecar_url, + dry_run, + print, + }) } - if command.dry_run { - return Ok(ExitCode::SUCCESS); + + // Emits the resolved run plan when requested. Dry runs always print because inspection is their + // primary behavior; live runs print only when `--print` was passed. + fn print_if_requested(&self) { + if self.print || self.dry_run { + self.prepared + .print(self.agent, &self.sidecar_url, &self.resolved); + } } - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let server_config = resolved.sidecar.clone(); - let server_task = tokio::spawn(async move { - server::serve_listener(listener, server_config, Some(shutdown_rx)).await - }); - if let Err(error) = wait_for_health(&sidecar_url).await { - let _ = shutdown_tx.send(()); - let _ = server_task.await; - return Err(error); + // Runs the prepared child command unless this is an inspection-only dry run. + async fn execute(self) -> Result { + if self.dry_run { + return Ok(ExitCode::SUCCESS); + } + execute_live_run( + self.listener, + self.resolved.sidecar, + &self.sidecar_url, + self.prepared, + ) + .await } +} +// Starts the sidecar, waits for readiness, runs the child command, restores temporary state, and then +// maps the child process status to the launcher's exit code. +async fn execute_live_run( + listener: TcpListener, + sidecar_config: SidecarConfig, + sidecar_url: &str, + prepared: PreparedRun, +) -> Result { + let running_server = RunningSidecar::start(listener, sidecar_config); + if let Err(error) = wait_for_health(sidecar_url).await { + let _ = running_server.stop().await; + return Err(error); + } let status = prepared.spawn_and_wait().await; let restore = prepared.restore(); - let _ = shutdown_tx.send(()); - let server_result = server_task - .await - .map_err(|error| SidecarError::Launch(format!("sidecar task failed: {error}")))?; + let server_result = running_server.stop().await; restore?; server_result?; - let status = status?; - Ok(status - .code() - .and_then(|code| u8::try_from(code).ok()) - .map(ExitCode::from) - .unwrap_or(ExitCode::FAILURE)) + Ok(exit_code(status?)) } +// Resolves the launched agent and argv from either an explicit command or a configured per-agent +// command. Agent inference only happens from argv[0] when `--agent` was omitted, so explicit agent +// selection can wrap commands whose executable name is not recognizable. fn resolve_agent_and_argv( command: &RunCommand, agents: &AgentConfigs, ) -> Result<(CodingAgent, Vec), SidecarError> { - let argv = if command.command.is_empty() { - let agent = command.agent.ok_or_else(|| { - SidecarError::Launch( - "missing command; pass -- or --agent with a configured command" - .into(), - ) - })?; - configured_command(agent, agents).ok_or_else(|| { - SidecarError::Launch(format!( - "no configured command for {}; pass -- ", - agent.as_arg() - )) - })? - } else { - command.command.clone() - }; - - let agent = match command.agent { - Some(agent) => agent, - None => CodingAgent::infer(&argv[0]).ok_or_else(|| { - SidecarError::Launch(format!( - "could not infer coding agent from command {:?}; pass --agent claude-code, --agent codex, --agent cursor, or --agent hermes", - argv[0] - )) - })?, - }; + let argv = resolved_argv(command, agents)?; + let agent = resolved_agent(command, &argv)?; Ok((agent, argv)) } +// Returns the command argv supplied on the CLI, or the configured command for an explicitly selected +// agent. Empty CLI argv without `--agent` is rejected before inference because there is no executable +// name to inspect. +fn resolved_argv(command: &RunCommand, agents: &AgentConfigs) -> Result, SidecarError> { + if !command.command.is_empty() { + return Ok(command.command.clone()); + } + let agent = command.agent.ok_or_else(|| { + SidecarError::Launch( + "missing command; pass -- or --agent with a configured command".into(), + ) + })?; + configured_command(agent, agents).ok_or_else(|| { + SidecarError::Launch(format!( + "no configured command for {}; pass -- ", + agent.as_arg() + )) + }) +} + +// Uses an explicit `--agent` when present and otherwise infers the agent from argv[0]. Inference is +// intentionally late so configured commands and direct CLI commands share the same validation path. +fn resolved_agent(command: &RunCommand, argv: &[String]) -> Result { + if let Some(agent) = command.agent { + return Ok(agent); + } + CodingAgent::infer(&argv[0]).ok_or_else(|| { + SidecarError::Launch(format!( + "could not infer coding agent from command {:?}; pass --agent claude-code, --agent codex, --agent cursor, or --agent hermes", + argv[0] + )) + }) +} + +// Splits a configured command string into argv words for run mode. This intentionally uses simple +// whitespace splitting because config command values are a convenience fallback; complex shell +// commands should be passed after `--` by the caller. fn configured_command(agent: CodingAgent, agents: &AgentConfigs) -> Option> { let command = match agent { CodingAgent::ClaudeCode => agents.claude_code.command.as_ref(), @@ -123,7 +193,36 @@ struct CursorRestore { had_original: bool, } +struct RunningSidecar { + shutdown_tx: oneshot::Sender<()>, + task: JoinHandle>, +} + +impl RunningSidecar { + // Starts the sidecar listener on a background task and keeps the shutdown sender paired with the + // task handle so health failures and normal exits use identical cleanup semantics. + fn start(listener: TcpListener, config: crate::config::SidecarConfig) -> Self { + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let task = tokio::spawn(async move { + server::serve_listener(listener, config, Some(shutdown_rx)).await + }); + Self { shutdown_tx, task } + } + + // Requests shutdown and joins the server task. The send can fail only if the task already exited; + // the join result still captures whether serving ended cleanly. + async fn stop(self) -> Result<(), SidecarError> { + let _ = self.shutdown_tx.send(()); + self.task + .await + .map_err(|error| SidecarError::Launch(format!("sidecar task failed: {error}")))? + } +} + impl PreparedRun { + // Builds the launch plan and applies only the preparation needed by the selected agent. + // Dry-run preparation records equivalent notes and argv/env changes without writing temporary + // hook files or patching user/project configuration. fn new( agent: CodingAgent, argv: Vec, @@ -161,6 +260,8 @@ impl PreparedRun { Ok(run) } + // Records the Claude Code argv/env changes that would be made during a real run. The temporary + // plugin path is symbolic so printed dry-run output is deterministic and non-mutating. fn prepare_claude_dry(&mut self, sidecar_url: &str) { insert_after_agent( &mut self.argv, @@ -176,6 +277,8 @@ impl PreparedRun { .push("would generate a temporary Claude Code plugin directory".into()); } + // Creates a temporary Claude Code plugin containing sidecar hooks and points Claude at both + // that plugin directory and the sidecar Anthropic-compatible gateway URL. fn prepare_claude(&mut self, sidecar_url: &str) -> Result<(), SidecarError> { let root = temp_dir("nemo-flow-claude-plugin")?; std::fs::create_dir_all(root.join(".claude-plugin"))?; @@ -207,6 +310,9 @@ impl PreparedRun { Ok(()) } + // Injects Codex hook and provider-base configuration through repeated `--config` flags. The + // generated TOML hook groups are passed inline so transparent run mode does not edit the user's + // persistent Codex config. fn prepare_codex(&mut self, sidecar_url: &str) { let hook_command = hook_forward_command(CodingAgent::Codex); let mut args = vec![ @@ -229,28 +335,13 @@ impl PreparedRun { insert_after_agent(&mut self.argv, CodingAgent::Codex, args); } + // Temporarily merges Cursor hooks into the nearest project `.cursor/hooks.json`, backing up the + // original if it exists. Cursor discovers hooks from files, so run mode patches and later + // restores project state rather than passing hook config on the command line. fn prepare_cursor(&mut self) -> Result<(), SidecarError> { let path = cursor_hooks_path()?; - let had_original = path.exists(); - let backup_path = if had_original { - let backup = path.with_extension(format!("json.nemo-flow-run.bak.{}", timestamp()?)); - std::fs::copy(&path, &backup)?; - Some(backup) - } else { - None - }; - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - let contents = serde_json::to_string_pretty(&merge_hooks( - read_json_file(&path)?, - generated_hooks( - CodingAgent::Cursor, - &hook_forward_command(CodingAgent::Cursor), - ), - )?) - .map_err(|error| SidecarError::Launch(error.to_string()))?; - std::fs::write(&path, contents)?; + let (had_original, backup_path) = backup_existing_cursor_hooks(&path)?; + write_merged_cursor_hooks(&path)?; self.cursor_restore = Some(CursorRestore { path, backup_path, @@ -259,6 +350,8 @@ impl PreparedRun { Ok(()) } + // Records the Cursor hook file that would be patched during a real run without touching the + // filesystem, preserving dry-run as an inspection-only operation. fn prepare_cursor_dry(&mut self) -> Result<(), SidecarError> { let path = cursor_hooks_path()?; self.notes.push(format!( @@ -268,12 +361,16 @@ impl PreparedRun { Ok(()) } + // Notes Hermes' persistent-hook requirement. Hermes hook approval is outside this launcher, so + // run mode only exports the live sidecar URL for hooks that are already installed and approved. fn prepare_hermes(&mut self) { self.notes.push( "Hermes shell hooks must be configured with `nemo-flow-sidecar install hermes`; this run exports the dynamic sidecar URL for approved hooks".into(), ); } + // Spawns the prepared child process with injected environment and waits for its exit status. + // Stdio is inherited by default so agent interaction remains unchanged in transparent mode. async fn spawn_and_wait(&self) -> Result { let mut command = Command::new(&self.argv[0]); command.args(&self.argv[1..]); @@ -284,6 +381,8 @@ impl PreparedRun { child.wait().await.map_err(SidecarError::from) } + // Removes temporary directories and restores Cursor hook files after the child exits. Restore + // errors are surfaced after the child status is collected so cleanup problems are not hidden. fn restore(&self) -> Result<(), SidecarError> { for dir in &self.temp_dirs { let _ = std::fs::remove_dir_all(dir); @@ -316,6 +415,8 @@ impl PreparedRun { Ok(()) } + // Prints the resolved transparent-run plan, including dynamic sidecar URL, upstream base URLs, + // argv/env injection, and any agent-specific notes or temporary files. fn print(&self, agent: CodingAgent, sidecar_url: &str, resolved: &ResolvedConfig) { println!("agent = {}", agent.as_arg()); println!("sidecar_url = {sidecar_url}"); @@ -343,6 +444,18 @@ impl PreparedRun { } } +// Converts a process status into the launcher status code while preserving normal 0-255 exits. Signal +// exits and platform-specific out-of-range codes become generic failure. +fn exit_code(status: std::process::ExitStatus) -> ExitCode { + status + .code() + .and_then(|code| u8::try_from(code).ok()) + .map(ExitCode::from) + .unwrap_or(ExitCode::FAILURE) +} + +// Polls the ephemeral sidecar health endpoint for roughly one second before launching the agent. +// Startup failures return a launcher error so the child command is never run against a dead proxy. async fn wait_for_health(sidecar_url: &str) -> Result<(), SidecarError> { let client = Client::new(); let url = format!("{}/healthz", sidecar_url.trim_end_matches('/')); @@ -359,6 +472,9 @@ async fn wait_for_health(sidecar_url: &str) -> Result<(), SidecarError> { ))) } +// Inserts generated agent flags immediately after the last argv element that looks like the agent +// executable. Falling back to index 0 keeps wrapper commands usable by inserting after the first +// word when the agent cannot be found later in argv. fn insert_after_agent( argv: &mut Vec, agent: CodingAgent, @@ -373,6 +489,8 @@ fn insert_after_agent( argv.splice(index + 1..index + 1, args); } +// Writes pretty JSON hook config to a path whose parent has already been created by the caller. +// Serialization errors are converted to launch errors to keep temporary setup failures contextual. fn write_hooks(path: &Path, hooks: Value) -> Result<(), SidecarError> { std::fs::write( path, @@ -382,6 +500,38 @@ fn write_hooks(path: &Path, hooks: Value) -> Result<(), SidecarError> { Ok(()) } +// Backs up an existing Cursor hook file before run-mode patching. The return value records both the +// original-file state and backup path so restore can either copy back or remove the generated file. +fn backup_existing_cursor_hooks(path: &Path) -> Result<(bool, Option), SidecarError> { + let had_original = path.exists(); + if !had_original { + return Ok((false, None)); + } + let backup = path.with_extension(format!("json.nemo-flow-run.bak.{}", timestamp()?)); + std::fs::copy(path, &backup)?; + Ok((true, Some(backup))) +} + +// Creates the Cursor hooks parent directory when needed, merges generated sidecar hooks with any +// existing hook file, and writes the patched JSON used for this transparent run. +fn write_merged_cursor_hooks(path: &Path) -> Result<(), SidecarError> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let contents = serde_json::to_string_pretty(&merge_hooks( + read_json_file(path)?, + generated_hooks( + CodingAgent::Cursor, + &hook_forward_command(CodingAgent::Cursor), + ), + )?) + .map_err(|error| SidecarError::Launch(error.to_string()))?; + std::fs::write(path, contents)?; + Ok(()) +} + +// Converts JSON hook groups into inline TOML arrays for Codex `--config` flags. The function +// preserves matchers when present and assumes generated hook groups contain one command hook. fn hook_groups_toml(value: &Value) -> String { let mut groups = Vec::new(); for group in value.as_array().into_iter().flatten() { @@ -399,17 +549,22 @@ fn hook_groups_toml(value: &Value) -> String { format!("[{}]", groups.join(",")) } +// Escapes a Rust string as a TOML basic string for inline Codex configuration values. fn toml_string(value: &str) -> String { let escaped = value.replace('\\', "\\\\").replace('"', "\\\""); format!("\"{escaped}\"") } +// Creates a timestamped directory under the OS temp directory. The timestamp suffix avoids +// collisions between concurrent transparent runs without keeping persistent state. fn temp_dir(prefix: &str) -> Result { let path = std::env::temp_dir().join(format!("{prefix}-{}", timestamp()?)); std::fs::create_dir_all(&path)?; Ok(path) } +// Locates Cursor's project hook file by walking up to the nearest ancestor that already contains a +// `.cursor` directory, falling back to the current directory for first-time project setup. fn cursor_hooks_path() -> Result { let cwd = std::env::current_dir()?; let project = cwd @@ -419,6 +574,8 @@ fn cursor_hooks_path() -> Result { Ok(project.join(".cursor/hooks.json")) } +// Returns a monotonic-enough wall-clock nanosecond stamp for temp and backup names. System time +// errors become launcher errors because paths cannot be safely generated without a timestamp. fn timestamp() -> Result { Ok(SystemTime::now() .duration_since(UNIX_EPOCH) diff --git a/crates/sidecar/src/main.rs b/crates/sidecar/src/main.rs index a16b8ea..75a97b2 100644 --- a/crates/sidecar/src/main.rs +++ b/crates/sidecar/src/main.rs @@ -20,6 +20,9 @@ use clap::Parser; use crate::config::{Cli, Command}; #[tokio::main] +// Runs the async CLI entrypoint and converts any surfaced sidecar error into a non-zero process +// exit. Errors are printed once here so subcommands can return structured errors without also +// owning process-level reporting. async fn main() -> ExitCode { match run().await { Ok(code) => code, @@ -30,6 +33,8 @@ async fn main() -> ExitCode { } } +// Dispatches CLI subcommands while keeping the no-subcommand path as server mode. `run` inherits +// top-level server flags so transparent launch can share config parsing with daemon startup. async fn run() -> Result { let cli = Cli::parse(); match cli.command { diff --git a/crates/sidecar/src/model.rs b/crates/sidecar/src/model.rs index 708a03b..012305b 100644 --- a/crates/sidecar/src/model.rs +++ b/crates/sidecar/src/model.rs @@ -13,6 +13,8 @@ pub(crate) enum AgentKind { } impl AgentKind { + // Returns the canonical metadata spelling for runtime events. These strings are consumed by + // observability exporters and therefore avoid deriving from enum debug names. pub(crate) const fn as_str(self) -> &'static str { match self { Self::Codex => "codex", @@ -30,25 +32,28 @@ pub(crate) enum NormalizedEvent { AgentEnded(SessionEvent), SubagentStarted(SubagentEvent), SubagentEnded(SubagentEvent), + LlmHint(LlmHintEvent), ToolStarted(ToolEvent), ToolEnded(ToolEvent), + #[allow(dead_code)] PromptSubmitted(SessionEvent), - AgentResponse(SessionEvent), Compaction(SessionEvent), Notification(SessionEvent), HookMark(SessionEvent), } impl NormalizedEvent { + // Extracts the routing session id regardless of normalized event kind. Keeping this on the + // enum lets the session manager group events before it needs to inspect lifecycle semantics. pub(crate) fn session_id(&self) -> &str { match self { Self::AgentStarted(event) | Self::AgentEnded(event) | Self::PromptSubmitted(event) - | Self::AgentResponse(event) | Self::Compaction(event) | Self::Notification(event) | Self::HookMark(event) => &event.session_id, + Self::LlmHint(event) => &event.session_id, Self::SubagentStarted(event) | Self::SubagentEnded(event) => &event.session_id, Self::ToolStarted(event) | Self::ToolEnded(event) => &event.session_id, } @@ -74,6 +79,22 @@ pub(crate) struct SubagentEvent { pub(crate) metadata: Value, } +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct LlmHintEvent { + pub(crate) session_id: String, + pub(crate) agent_kind: AgentKind, + pub(crate) event_name: String, + pub(crate) subagent_id: Option, + pub(crate) agent_id: Option, + pub(crate) agent_type: Option, + pub(crate) conversation_id: Option, + pub(crate) generation_id: Option, + pub(crate) request_id: Option, + pub(crate) model: Option, + pub(crate) payload: Value, + pub(crate) metadata: Value, +} + #[derive(Debug, Clone, PartialEq)] pub(crate) struct ToolEvent { pub(crate) session_id: String, diff --git a/crates/sidecar/src/server.rs b/crates/sidecar/src/server.rs index 5a12028..fb46967 100644 --- a/crates/sidecar/src/server.rs +++ b/crates/sidecar/src/server.rs @@ -23,11 +23,19 @@ pub(crate) struct AppState { pub(crate) sessions: SessionManager, } +/// Binds the configured address and serves until the process is stopped. +/// +/// Tests and transparent run mode use `serve_listener` directly so they can supply an already +/// bound ephemeral listener and optional shutdown channel. pub(crate) async fn serve(config: SidecarConfig) -> Result<(), SidecarError> { let listener = TcpListener::bind(config.bind).await?; serve_listener(listener, config, None).await } +/// Serves the sidecar router on a caller-owned listener with optional graceful shutdown. +/// +/// A provided shutdown receiver is best-effort: the send side may be dropped after the child agent +/// exits, and either receiving or channel closure is enough to let Axum drain the listener. pub(crate) async fn serve_listener( listener: TcpListener, config: SidecarConfig, @@ -49,6 +57,10 @@ pub(crate) async fn serve_listener( Ok(()) } +/// Builds the sidecar HTTP router and shared state. +/// +/// Hook endpoints normalize agent-specific payloads into session events, while gateway endpoints +/// proxy model traffic and emit LLM runtime events against the same `SessionManager`. pub(crate) fn router(config: SidecarConfig) -> Router { let sessions = SessionManager::new(config.clone()); let state = AppState { @@ -74,6 +86,8 @@ async fn healthz() -> Json { Json(serde_json::json!({ "status": "ok" })) } +// Normalizes a Codex hook payload, applies all resulting events before responding, and returns the +// adapter's pass-through response body so hook delivery stays causally ordered with observability. async fn codex_hook( State(state): State, headers: HeaderMap, @@ -87,6 +101,8 @@ async fn codex_hook( Ok(Json(outcome.response)) } +// Handles Claude Code hooks with the adapter's explicit continuation/permission response. Events +// are committed before the response so Claude lifecycle hooks can close scopes deterministically. async fn claude_code_hook( State(state): State, headers: HeaderMap, @@ -100,6 +116,8 @@ async fn claude_code_hook( Ok(Json(outcome.response)) } +// Handles Cursor hook payloads and preserves Cursor's fail-open response shape. Shell and MCP hook +// names are already normalized by the adapter before session state is updated. async fn cursor_hook( State(state): State, headers: HeaderMap, @@ -113,6 +131,8 @@ async fn cursor_hook( Ok(Json(outcome.response)) } +// Handles Hermes hook payloads from persistent shell integration. The adapter returns a minimal +// body because hook-forward owns the fail-open/fail-closed behavior for Hermes command execution. async fn hermes_hook( State(state): State, headers: HeaderMap, diff --git a/crates/sidecar/src/session.rs b/crates/sidecar/src/session.rs index cb20833..ece5323 100644 --- a/crates/sidecar/src/session.rs +++ b/crates/sidecar/src/session.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; +use std::time::{Duration, Instant}; use axum::http::HeaderMap; use nemo_flow::api::llm::{ @@ -25,7 +26,13 @@ use tokio::sync::Mutex; use crate::config::{SessionConfig, SidecarConfig}; use crate::error::SidecarError; -use crate::model::{AgentKind, NormalizedEvent, SessionEvent, SubagentEvent, ToolEvent}; +use crate::model::{ + AgentKind, LlmHintEvent, NormalizedEvent, SessionEvent, SubagentEvent, ToolEvent, +}; + +const LLM_HINT_TTL: Duration = Duration::from_secs(300); +const TOOL_HINT_TTL: Duration = Duration::from_secs(300); +const LAST_OWNER_TTL: Duration = Duration::from_secs(300); #[derive(Clone)] pub(crate) struct SessionManager { @@ -38,6 +45,10 @@ pub(crate) struct LlmGatewayStart { pub(crate) session_id: Option, pub(crate) provider: String, pub(crate) model_name: Option, + pub(crate) subagent_id: Option, + pub(crate) conversation_id: Option, + pub(crate) generation_id: Option, + pub(crate) request_id: Option, pub(crate) request: LlmRequest, pub(crate) streaming: bool, pub(crate) metadata: Value, @@ -47,6 +58,8 @@ pub(crate) struct LlmGatewayStart { pub(crate) struct ActiveLlm { stack: ScopeStackHandle, handle: LlmHandle, + session_id: String, + owner_subagent_id: Option, } struct Session { @@ -57,12 +70,62 @@ struct Session { subagents: HashMap, subagent_stack: Vec, tools: HashMap, + pending_llm_hints: Vec, + pending_tool_hints: Vec, + last_llm_owner: Option, config: SessionConfig, atif: Option, openinference: Option, } +#[derive(Debug, Clone)] +struct PendingLlmHint { + hint: LlmHintEvent, + inserted_at: Instant, +} + +#[derive(Debug, Clone)] +struct PendingToolHint { + hint: ToolHint, + inserted_at: Instant, +} + +#[derive(Debug, Clone)] +struct ToolHint { + tool_call_id: Option, + tool_name: Option, + subagent_id: Option, + arguments: Value, + source: String, +} + +#[derive(Debug, Clone)] +struct LastLlmOwner { + subagent_id: Option, + updated_at: Instant, +} + +struct LlmOwnerResolution { + parent: Option, + subagent_id: Option, + status: &'static str, + source: Option, + hint: Option, +} + +struct ToolOwnerResolution { + parent: Option, + subagent_id: Option, + status: &'static str, + source: Option, + hint: Option, +} + impl SessionManager { + /// Creates an empty manager that uses the supplied sidecar config as the header fallback layer. + /// + /// Sessions are stored behind a shared async mutex because hook requests and gateway requests + /// may arrive concurrently and need to resolve LLM ownership against the same in-memory state. pub(crate) fn new(default_config: SidecarConfig) -> Self { Self { inner: Arc::new(Mutex::new(HashMap::new())), @@ -70,6 +133,11 @@ impl SessionManager { } } + /// Applies normalized hook events to their owning sessions in arrival order. + /// + /// Session configuration is re-read from headers for each request so installed hook commands can + /// override exporters or metadata per invocation. Empty sessions are removed after lifecycle + /// closure to avoid retaining stale correlation state. pub(crate) async fn apply_events( &self, headers: &HeaderMap, @@ -94,6 +162,11 @@ impl SessionManager { Ok(()) } + /// Starts a gateway-observed LLM call and correlates it with the best available session. + /// + /// Explicit session IDs win, a single active hook session is reused as a convenience fallback, + /// and otherwise a synthetic gateway session is created so pure proxy use still emits runtime + /// events. pub(crate) async fn start_llm( &self, headers: &HeaderMap, @@ -112,12 +185,19 @@ impl SessionManager { session.start_llm(start).await } + /// Ends an active gateway-observed LLM call on the scope stack that created it. + /// + /// The captured stack is restored around `llm_call_end` so asynchronous gateway body handling + /// closes the correct scoped event even after the original request task has moved on. pub(crate) async fn end_llm( &self, active: ActiveLlm, response: Value, metadata: Value, ) -> Result<(), SidecarError> { + let response_for_hints = response.clone(); + let session_id = active.session_id.clone(); + let owner_subagent_id = active.owner_subagent_id.clone(); TASK_SCOPE_STACK .scope(active.stack, async move { llm_call_end( @@ -129,11 +209,19 @@ impl SessionManager { ) .map_err(SidecarError::from) }) - .await + .await?; + let mut sessions = self.inner.lock().await; + if let Some(session) = sessions.get_mut(&session_id) { + session.add_tool_hints_from_llm_response(response_for_hints, owner_subagent_id); + } + Ok(()) } } impl Session { + // Constructs per-session runtime state without creating a scope yet. The root agent scope is + // opened lazily on the first event or gateway LLM call so sessions created from hints and pure + // gateway traffic share the same initialization path. fn new(session_id: String, agent_kind: AgentKind, config: SessionConfig) -> Self { Self { agent_kind, @@ -143,12 +231,17 @@ impl Session { subagents: HashMap::new(), subagent_stack: Vec::new(), tools: HashMap::new(), + pending_llm_hints: Vec::new(), + pending_tool_hints: Vec::new(), + last_llm_owner: None, config, atif: None, openinference: None, } } + // Runs one normalized hook event inside this session's scope stack. Dispatch stays synchronous + // inside the scoped closure so lifecycle ordering from each hook request is preserved exactly. async fn apply(&mut self, event: NormalizedEvent) -> Result<(), SidecarError> { let stack = self.scope_stack.clone(); TASK_SCOPE_STACK @@ -158,10 +251,10 @@ impl Session { NormalizedEvent::AgentEnded(event) => self.end_agent(event), NormalizedEvent::SubagentStarted(event) => self.start_subagent(event), NormalizedEvent::SubagentEnded(event) => self.end_subagent(event), + NormalizedEvent::LlmHint(event) => self.add_llm_hint(event), NormalizedEvent::ToolStarted(event) => self.start_tool(event), NormalizedEvent::ToolEnded(event) => self.end_tool(event), NormalizedEvent::PromptSubmitted(event) => self.mark("prompt_submitted", event), - NormalizedEvent::AgentResponse(event) => self.mark("agent_response", event), NormalizedEvent::Compaction(event) => self.mark("compaction", event), NormalizedEvent::Notification(event) => self.mark("notification", event), NormalizedEvent::HookMark(event) => self.mark("hook_mark", event), @@ -170,6 +263,9 @@ impl Session { .await } + // Opens an LLM call for gateway traffic, creating the agent scope if needed and resolving the + // parent scope from headers, pending hints, sticky ownership, active subagents, or agent fallback + // in that order. async fn start_llm(&mut self, start: LlmGatewayStart) -> Result { let stack = self.scope_stack.clone(); TASK_SCOPE_STACK @@ -179,25 +275,44 @@ impl Session { if start.streaming { attributes |= LlmAttributes::STREAMING; } + let owner = self.resolve_llm_owner(&start); + let metadata = llm_correlation_metadata( + start.metadata, + owner.status, + owner.source.as_deref(), + owner.subagent_id.as_deref(), + owner.hint.as_ref(), + ); let handle = llm_call( LlmCallParams::builder() .name(start.provider.as_str()) .request(&start.request) + .parent_opt(owner.parent.as_ref()) .attributes(attributes) - .metadata(start.metadata) + .metadata(metadata) .model_name_opt(start.model_name) .build(), )?; - Ok(ActiveLlm { stack, handle }) + Ok(ActiveLlm { + stack, + handle, + session_id: self.session_id.clone(), + owner_subagent_id: owner.subagent_id, + }) }) .await } + // Records an explicit top-level agent start. Repeated start hooks are idempotent because + // `ensure_agent_started` leaves an existing agent scope open and only updates agent kind first. fn start_agent(&mut self, event: SessionEvent) -> Result<(), SidecarError> { self.agent_kind = event.agent_kind; self.ensure_agent_started(event.metadata) } + // Lazily opens the root agent scope, installs observers on the root handle, and merges metadata + // from config, event payload, and sidecar headers. Later calls are no-ops to keep duplicate + // hooks from nesting agent scopes. fn ensure_agent_started(&mut self, event_metadata: Value) -> Result<(), SidecarError> { if self.agent_scope.is_some() { return Ok(()); @@ -227,41 +342,71 @@ impl Session { Ok(()) } + // Installs configured exporters exactly once per session root. ATIF and OpenInference are + // scope-local subscribers so they disappear with the session and do not affect unrelated + // concurrent agent runs. fn install_observers(&mut self, root: &ScopeHandle) -> Result<(), SidecarError> { - if self.atif.is_none() && self.config.atif_dir.is_some() { - let exporter = AtifExporter::new( - self.session_id.clone(), - AtifAgentInfo { - name: self.agent_kind.as_str().to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - model_name: None, - tool_definitions: None, - extra: self.config.metadata.clone(), - }, - ); - scope_register_subscriber(&root.uuid, "sidecar-atif", exporter.subscriber())?; - self.atif = Some(exporter); + self.install_atif_observer(root)?; + self.install_openinference_observer(root)?; + Ok(()) + } + + // Registers the ATIF exporter once when a session has ATIF output configured. The exporter keeps + // the session agent metadata so downstream trajectory files can be attributed to this run. + fn install_atif_observer(&mut self, root: &ScopeHandle) -> Result<(), SidecarError> { + if self.atif.is_some() || self.config.atif_dir.is_none() { + return Ok(()); } - if self.openinference.is_none() - && let Some(endpoint) = &self.config.openinference_endpoint - { - let subscriber = OpenInferenceSubscriber::new( - OpenInferenceConfig::new() - .with_endpoint(endpoint.clone()) - .with_service_name("nemo-flow-sidecar"), - )?; - scope_register_subscriber( - &root.uuid, - "sidecar-openinference", - subscriber.subscriber(), - )?; - self.openinference = Some(subscriber); + let exporter = AtifExporter::new( + self.session_id.clone(), + AtifAgentInfo { + name: self.agent_kind.as_str().to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + model_name: None, + tool_definitions: None, + extra: self.config.metadata.clone(), + }, + ); + scope_register_subscriber(&root.uuid, "sidecar-atif", exporter.subscriber())?; + self.atif = Some(exporter); + Ok(()) + } + + // Registers the OpenInference subscriber once when an endpoint is configured. Endpoint ownership + // remains on the session config so repeated start events cannot duplicate subscribers. + fn install_openinference_observer(&mut self, root: &ScopeHandle) -> Result<(), SidecarError> { + if self.openinference.is_some() { + return Ok(()); } + let Some(endpoint) = &self.config.openinference_endpoint else { + return Ok(()); + }; + let subscriber = OpenInferenceSubscriber::new( + OpenInferenceConfig::new() + .with_endpoint(endpoint.clone()) + .with_service_name("nemo-flow-sidecar"), + )?; + scope_register_subscriber(&root.uuid, "sidecar-openinference", subscriber.subscriber())?; + self.openinference = Some(subscriber); Ok(()) } + // Closes the session in a fail-safe order: active tools first, nested subagents from the top + // down, correlation state, then the root agent scope. Observer flush/export happens after the + // root scope ends so terminal events are included. fn end_agent(&mut self, event: SessionEvent) -> Result<(), SidecarError> { self.ensure_agent_started(event.metadata.clone())?; + self.close_active_tools_for_agent_end()?; + self.close_active_subagents_for_agent_end()?; + self.clear_correlation_state(); + self.close_agent_scope(event.payload)?; + self.flush_observers()?; + Ok(()) + } + + // Ends all active tool calls with a synthetic close result before ending their containing scopes. + // Draining first avoids holding mutable map state while the runtime emits lifecycle events. + fn close_active_tools_for_agent_end(&mut self) -> Result<(), SidecarError> { let active_tools: Vec<_> = self.tools.drain().map(|(_, handle)| handle).collect(); for handle in active_tools { tool_call_end( @@ -272,6 +417,12 @@ impl Session { .build(), )?; } + Ok(()) + } + + // Pops active subagent scopes in stack order so nested subagents close from child to parent. The + // map is cleared afterward to discard any out-of-order stale handles not present in the stack. + fn close_active_subagents_for_agent_end(&mut self) -> Result<(), SidecarError> { while let Some(subagent_id) = self.subagent_stack.pop() { if let Some(handle) = self.subagents.remove(&subagent_id) { pop_scope( @@ -283,18 +434,33 @@ impl Session { } } self.subagents.clear(); - if let Some(scope) = self.agent_scope.take() { - pop_scope( - PopScopeParams::builder() - .handle_uuid(&scope.uuid) - .output(event.payload) - .build(), - )?; - } - self.flush_observers()?; Ok(()) } + // Clears sticky LLM/tool ownership hints that should not survive an agent root shutdown. + fn clear_correlation_state(&mut self) { + self.pending_llm_hints.clear(); + self.pending_tool_hints.clear(); + self.last_llm_owner = None; + } + + // Ends the root agent scope when present. Duplicate agent-end hooks can reach this path after the + // scope is already gone, so absence is treated as a no-op. + fn close_agent_scope(&mut self, payload: Value) -> Result<(), SidecarError> { + let Some(scope) = self.agent_scope.take() else { + return Ok(()); + }; + pop_scope( + PopScopeParams::builder() + .handle_uuid(&scope.uuid) + .output(payload) + .build(), + )?; + Ok(()) + } + + // Starts a subagent scope under the current session. Duplicate subagent starts are ignored so + // integrations that retry or emit both "start" and "created" style hooks do not double-nest. fn start_subagent(&mut self, event: SubagentEvent) -> Result<(), SidecarError> { self.ensure_agent_started(event.metadata.clone())?; if self.subagents.contains_key(&event.subagent_id) { @@ -313,6 +479,9 @@ impl Session { Ok(()) } + // Ends a subagent only when it is the current top of the subagent stack. Unknown or out-of-order + // endings become mark events instead of corrupting the scope stack, preserving evidence of the + // mismatch for observability consumers. fn end_subagent(&mut self, event: SubagentEvent) -> Result<(), SidecarError> { self.ensure_agent_started(event.metadata.clone())?; let Some(scope) = self.subagents.get(&event.subagent_id).cloned() else { @@ -356,25 +525,63 @@ impl Session { } self.subagent_stack.pop(); self.subagents.remove(&event.subagent_id); + self.pending_tool_hints + .retain(|pending| pending.hint.subagent_id.as_ref() != Some(&event.subagent_id)); + if self + .last_llm_owner + .as_ref() + .is_some_and(|owner| owner.subagent_id.as_ref() == Some(&event.subagent_id)) + { + self.last_llm_owner = None; + } Ok(()) } + // Stores an LLM correlation hint from hook activity after pruning expired hints. Hints do not + // emit runtime events themselves; they are consumed by the next matching gateway LLM call. + fn add_llm_hint(&mut self, event: LlmHintEvent) -> Result<(), SidecarError> { + self.ensure_agent_started(event.metadata.clone())?; + self.cleanup_correlation_state(); + let owner_subagent_id = event.subagent_id.clone().or_else(|| event.agent_id.clone()); + self.add_tool_hints_from_llm_response(event.payload.clone(), owner_subagent_id); + self.pending_llm_hints.push(PendingLlmHint { + hint: event, + inserted_at: Instant::now(), + }); + Ok(()) + } + + // Starts a tool call under an explicit subagent when available, otherwise under the agent + // scope. Duplicate tool IDs are ignored so repeated pre-tool hooks do not create parallel + // handles for one agent tool invocation. fn start_tool(&mut self, event: ToolEvent) -> Result<(), SidecarError> { self.ensure_agent_started(event.metadata.clone())?; if self.tools.contains_key(&event.tool_call_id) { return Ok(()); } - let parent = event - .subagent_id - .as_ref() - .and_then(|id| self.subagents.get(id)) - .or(self.agent_scope.as_ref()); + let owner = self.resolve_tool_owner(&event); + let arguments = if event.arguments.is_null() { + owner + .hint + .as_ref() + .map(|hint| hint.arguments.clone()) + .unwrap_or(event.arguments) + } else { + event.arguments + }; + let metadata = tool_correlation_metadata( + event.metadata, + owner.status, + owner.source.as_deref(), + owner.subagent_id.as_deref(), + owner.hint.as_ref(), + ); let handle = tool_call( ToolCallParams::builder() .name(event.tool_name.as_str()) - .args(event.arguments) - .parent_opt(parent) - .metadata(event.metadata) + .args(arguments) + .parent_opt(owner.parent.as_ref()) + .metadata(metadata) .tool_call_id(event.tool_call_id.clone()) .build(), )?; @@ -382,22 +589,36 @@ impl Session { Ok(()) } + // Ends a tool call, synthesizing a start if no matching handle exists. This keeps post-only + // hooks observable and preserves the final result/status instead of dropping orphaned endings. fn end_tool(&mut self, event: ToolEvent) -> Result<(), SidecarError> { self.ensure_agent_started(event.metadata.clone())?; let handle = match self.tools.remove(&event.tool_call_id) { Some(handle) => handle, None => { - let parent = event - .subagent_id - .as_ref() - .and_then(|id| self.subagents.get(id)) - .or(self.agent_scope.as_ref()); + let owner = self.resolve_tool_owner(&event); + let arguments = if event.arguments.is_null() { + owner + .hint + .as_ref() + .map(|hint| hint.arguments.clone()) + .unwrap_or(event.arguments) + } else { + event.arguments + }; + let metadata = tool_correlation_metadata( + event.metadata.clone(), + owner.status, + owner.source.as_deref(), + owner.subagent_id.as_deref(), + owner.hint.as_ref(), + ); tool_call( ToolCallParams::builder() .name(event.tool_name.as_str()) - .args(event.arguments) - .parent_opt(parent) - .metadata(event.metadata.clone()) + .args(arguments) + .parent_opt(owner.parent.as_ref()) + .metadata(metadata) .tool_call_id(event.tool_call_id.clone()) .build(), )? @@ -416,6 +637,8 @@ impl Session { Ok(()) } + // Emits a mark event after ensuring the agent scope exists. Generic and unknown hooks use this + // path so unsupported agent events remain visible without changing scope structure. fn mark(&mut self, name: &str, event_payload: SessionEvent) -> Result<(), SidecarError> { self.ensure_agent_started(event_payload.metadata.clone())?; emit_mark_event( @@ -428,6 +651,8 @@ impl Session { Ok(()) } + // Flushes and shuts down configured observers, then writes ATIF output if requested. This runs + // only on agent end, so long-lived sessions keep subscribers active across intermediate hooks. fn flush_observers(&mut self) -> Result<(), SidecarError> { if let Some(subscriber) = &self.openinference { subscriber.force_flush()?; @@ -438,8 +663,319 @@ impl Session { } Ok(()) } + + // Prunes expired LLM hints and sticky owner state. The TTLs prevent old hook activity from + // incorrectly capturing later gateway calls when agents reuse a process or session id. + fn cleanup_correlation_state(&mut self) { + let now = Instant::now(); + self.pending_llm_hints + .retain(|hint| now.duration_since(hint.inserted_at) <= LLM_HINT_TTL); + self.pending_tool_hints + .retain(|hint| now.duration_since(hint.inserted_at) <= TOOL_HINT_TTL); + if self + .last_llm_owner + .as_ref() + .is_some_and(|owner| now.duration_since(owner.updated_at) > LAST_OWNER_TTL) + { + self.last_llm_owner = None; + } + } + + // Resolves the parent scope for a gateway LLM call. The precedence is explicit subagent header, + // single pending hint, uniquely matched hint, sticky last owner, sole active subagent, then agent + // fallback; ambiguous hints intentionally fall back to the agent and are reported in metadata. + fn resolve_llm_owner(&mut self, start: &LlmGatewayStart) -> LlmOwnerResolution { + self.cleanup_correlation_state(); + + if let Some(resolution) = self.explicit_llm_owner(start) { + return resolution; + } + if let Some(resolution) = self.single_hint_owner() { + return resolution; + } + if let Some(resolution) = self.matched_hint_owner(start) { + return resolution; + } + if let Some(resolution) = self.sticky_llm_owner() { + return resolution; + } + if let Some(resolution) = self.sole_subagent_owner() { + return resolution; + } + + self.fallback_llm_owner() + } + + // Uses an explicit gateway subagent id when it names an active subagent. Unknown ids do not + // produce an explicit result because the caller should still have a chance to use hint-based + // or fallback ownership. + fn explicit_llm_owner(&mut self, start: &LlmGatewayStart) -> Option { + if let Some(subagent_id) = &start.subagent_id + && let Some(scope) = self.subagents.get(subagent_id).cloned() + { + self.set_last_llm_owner(Some(subagent_id.clone())); + return Some(LlmOwnerResolution { + parent: Some(scope), + subagent_id: Some(subagent_id.clone()), + status: "explicit", + source: Some("gateway_header".to_string()), + hint: None, + }); + } + None + } + + // Consumes a sole pending hint without scoring. A single hint is unambiguous even when it only + // contains model or event context, and retaining it would incorrectly affect later LLM calls. + fn single_hint_owner(&mut self) -> Option { + if self.pending_llm_hints.len() == 1 { + let hint = self.pending_llm_hints.remove(0).hint; + return Some(self.resolution_from_hint(hint, "single_hint")); + } + None + } + + // Consumes the unique best-scoring hint for this gateway request. Tied scores are treated as + // ambiguous by `matching_hint_index` so this helper only returns defensible correlations. + fn matched_hint_owner(&mut self, start: &LlmGatewayStart) -> Option { + if let Some(index) = self.matching_hint_index(start) { + let hint = self.pending_llm_hints.remove(index).hint; + return Some(self.resolution_from_hint(hint, "matched_hint")); + } + None + } + + // Reuses the previous LLM owner while its TTL is valid and its scope can still be resolved. + // This covers agents that emit one hint followed by a cluster of related provider calls. + fn sticky_llm_owner(&self) -> Option { + if let Some(owner) = self.last_llm_owner.as_ref() + && let Some(parent) = self.scope_for_owner(owner.subagent_id.as_deref()) + { + return Some(LlmOwnerResolution { + parent: Some(parent), + subagent_id: owner.subagent_id.clone(), + status: "sticky_last_owner", + source: None, + hint: None, + }); + } + None + } + + // Assigns an unhinted gateway call to the only active subagent. Multiple active subagents are + // deliberately not guessed here; those cases fall back to the agent scope with ambiguity + // metadata. + fn sole_subagent_owner(&mut self) -> Option { + if self.subagents.len() == 1 + && let Some((subagent_id, scope)) = self.subagents.iter().next() + { + let subagent_id = subagent_id.clone(); + let scope = scope.clone(); + self.set_last_llm_owner(Some(subagent_id.clone())); + return Some(LlmOwnerResolution { + parent: Some(scope), + subagent_id: Some(subagent_id), + status: "active_subagent", + source: None, + hint: None, + }); + } + None + } + + // Final fallback for gateway calls that cannot be correlated to a subagent. Pending hints are + // left intact in ambiguous cases so later calls with stronger identifiers can still match them. + fn fallback_llm_owner(&self) -> LlmOwnerResolution { + LlmOwnerResolution { + parent: self.agent_scope.clone(), + subagent_id: None, + status: if self.pending_llm_hints.is_empty() { + "agent_fallback" + } else { + "ambiguous_fallback" + }, + source: None, + hint: None, + } + } + + // Converts a consumed hint into an ownership resolution. If the hinted subagent is not currently + // active, the LLM is attached to the agent scope but the hint metadata is still preserved for + // correlation diagnostics. + fn resolution_from_hint( + &mut self, + hint: LlmHintEvent, + status: &'static str, + ) -> LlmOwnerResolution { + let hinted_subagent_id = hint.subagent_id.clone().or_else(|| hint.agent_id.clone()); + let (parent, subagent_id) = match hinted_subagent_id.as_deref() { + Some(id) => match self.subagents.get(id).cloned() { + Some(scope) => (Some(scope), Some(id.to_string())), + None => (self.agent_scope.clone(), None), + }, + None => (self.agent_scope.clone(), None), + }; + if parent.is_some() { + self.set_last_llm_owner(subagent_id.clone()); + } + LlmOwnerResolution { + parent, + subagent_id, + status, + source: Some(hint.event_name.clone()), + hint: Some(hint), + } + } + + // Finds a single best pending hint for a gateway call. Ties are treated as ambiguous and return + // `None`, causing the caller to use fallback behavior rather than guessing between subagents. + fn matching_hint_index(&self, start: &LlmGatewayStart) -> Option { + let matches: Vec<_> = self + .pending_llm_hints + .iter() + .enumerate() + .filter_map(|(index, pending)| { + let score = hint_match_score(&pending.hint, start); + (score > 0).then_some((index, score)) + }) + .collect(); + let best_score = matches.iter().map(|(_, score)| *score).max()?; + let best: Vec<_> = matches + .into_iter() + .filter(|(_, score)| *score == best_score) + .collect(); + (best.len() == 1).then_some(best[0].0) + } + + // Resolves a stored owner back to a live scope, falling back to the agent scope when no subagent + // id is present or the subagent has already ended. + fn scope_for_owner(&self, subagent_id: Option<&str>) -> Option { + subagent_id + .and_then(|id| self.subagents.get(id).cloned()) + .or_else(|| self.agent_scope.clone()) + } + + // Records the most recent LLM owner with a timestamp so nearby gateway calls can inherit the + // same parent scope when explicit IDs and hints are absent. + fn set_last_llm_owner(&mut self, subagent_id: Option) { + self.last_llm_owner = Some(LastLlmOwner { + subagent_id, + updated_at: Instant::now(), + }); + } + + // Records tool-call suggestions from LLM responses as private correlation hints. These hints + // are not emitted as events; they only help later tool hooks choose the same subagent scope as + // the LLM that proposed the call. + fn add_tool_hints_from_llm_response( + &mut self, + response: Value, + owner_subagent_id: Option, + ) { + self.cleanup_correlation_state(); + let hints = tool_hints_from_llm_response(&response, owner_subagent_id); + self.pending_tool_hints + .extend(hints.into_iter().map(|hint| PendingToolHint { + hint, + inserted_at: Instant::now(), + })); + } + + // Resolves tool hook ownership from explicit subagent data first, then private tool hints + // extracted from LLM responses, and finally the agent scope. + fn resolve_tool_owner(&mut self, event: &ToolEvent) -> ToolOwnerResolution { + self.cleanup_correlation_state(); + + if let Some(subagent_id) = &event.subagent_id + && let Some(scope) = self.subagents.get(subagent_id).cloned() + { + self.consume_matching_tool_hint(event); + return ToolOwnerResolution { + parent: Some(scope), + subagent_id: Some(subagent_id.clone()), + status: "explicit", + source: Some("hook_payload".to_string()), + hint: None, + }; + } + + if self.pending_tool_hints.len() == 1 { + let hint = self.pending_tool_hints.remove(0).hint; + return self.tool_resolution_from_hint(hint, "single_hint"); + } + + if let Some(index) = self.matching_tool_hint_index(event) { + let hint = self.pending_tool_hints.remove(index).hint; + return self.tool_resolution_from_hint(hint, "matched_hint"); + } + + ToolOwnerResolution { + parent: self.agent_scope.clone(), + subagent_id: None, + status: if self.pending_tool_hints.is_empty() { + "agent_fallback" + } else { + "ambiguous_fallback" + }, + source: None, + hint: None, + } + } + + // Converts a consumed tool hint into a live parent scope, falling back to the root agent if the + // hinted subagent has already ended or never existed. + fn tool_resolution_from_hint( + &mut self, + hint: ToolHint, + status: &'static str, + ) -> ToolOwnerResolution { + let (parent, subagent_id) = match hint.subagent_id.as_deref() { + Some(id) => match self.subagents.get(id).cloned() { + Some(scope) => (Some(scope), Some(id.to_string())), + None => (self.agent_scope.clone(), None), + }, + None => (self.agent_scope.clone(), None), + }; + ToolOwnerResolution { + parent, + subagent_id, + status, + source: Some(hint.source.clone()), + hint: Some(hint), + } + } + + // Removes a stale matching hint when a hook already carried an explicit subagent owner. + fn consume_matching_tool_hint(&mut self, event: &ToolEvent) { + if let Some(index) = self.matching_tool_hint_index(event) { + self.pending_tool_hints.remove(index); + } + } + + // Finds a unique best-scoring tool hint by call id, name, and argument equality. Ties remain + // ambiguous and are not consumed. + fn matching_tool_hint_index(&self, event: &ToolEvent) -> Option { + let matches: Vec<_> = self + .pending_tool_hints + .iter() + .enumerate() + .filter_map(|(index, pending)| { + let score = tool_hint_match_score(&pending.hint, event); + (score > 0).then_some((index, score)) + }) + .collect(); + let best_score = matches.iter().map(|(_, score)| *score).max()?; + let best: Vec<_> = matches + .into_iter() + .filter(|(_, score)| *score == best_score) + .collect(); + (best.len() == 1).then_some(best[0].0) + } } +// Writes the complete ATIF trajectory for a finished session to `{session_id}.atif.json`, creating +// the target directory lazily. Serialization failures are reported as invalid payloads because they +// indicate exporter output could not be represented as JSON. fn write_atif( directory: &PathBuf, session_id: &str, @@ -454,15 +990,320 @@ fn write_atif( Ok(()) } +// Scores how strongly a pending hint matches a gateway LLM request. Subagent/agent identity is +// weighted highest, request/conversation/generation identifiers are equal, and model match is only +// a low-confidence tie breaker. +fn hint_match_score(hint: &LlmHintEvent, start: &LlmGatewayStart) -> u8 { + let mut score = 0; + if same_optional(hint.subagent_id.as_deref(), start.subagent_id.as_deref()) + || same_optional(hint.agent_id.as_deref(), start.subagent_id.as_deref()) + { + score += 8; + } + if same_optional( + hint.conversation_id.as_deref(), + start.conversation_id.as_deref(), + ) { + score += 4; + } + if same_optional( + hint.generation_id.as_deref(), + start.generation_id.as_deref(), + ) { + score += 4; + } + if same_optional(hint.request_id.as_deref(), start.request_id.as_deref()) { + score += 4; + } + if same_optional(hint.model.as_deref(), start.model_name.as_deref()) { + score += 1; + } + score +} + +// Extracts tool-call hints from common provider response shapes. These private hints let later +// hook-only tool events attach to the subagent that received the LLM response proposing the tool. +fn tool_hints_from_llm_response( + response: &Value, + owner_subagent_id: Option, +) -> Vec { + let mut hints = Vec::new(); + collect_openai_chat_tool_hints(response, owner_subagent_id.as_deref(), &mut hints); + collect_openai_response_tool_hints(response, owner_subagent_id.as_deref(), &mut hints); + collect_anthropic_tool_hints(response, owner_subagent_id.as_deref(), &mut hints); + hints +} + +// Collects OpenAI Chat Completions `choices[].message.tool_calls[]` entries and preserves +// stringified function arguments as parsed JSON when possible. +fn collect_openai_chat_tool_hints( + response: &Value, + owner_subagent_id: Option<&str>, + hints: &mut Vec, +) { + let Some(choices) = response.get("choices").and_then(Value::as_array) else { + return; + }; + for choice in choices { + let Some(tool_calls) = choice + .get("message") + .and_then(|message| message.get("tool_calls")) + .and_then(Value::as_array) + else { + continue; + }; + for call in tool_calls { + push_tool_hint( + hints, + call, + owner_subagent_id, + "openai_chat_tool_call", + &[&["id"][..], &["call_id"][..]], + &[&["function", "name"][..], &["name"][..]], + &[&["function", "arguments"][..], &["arguments"][..]], + ); + } + } +} + +// Collects OpenAI Responses output items where function-call data is usually direct on each item. +// Items without an id or name are ignored because they are too weak for ownership correlation. +fn collect_openai_response_tool_hints( + response: &Value, + owner_subagent_id: Option<&str>, + hints: &mut Vec, +) { + let Some(output) = response.get("output").and_then(Value::as_array) else { + return; + }; + for item in output { + push_tool_hint( + hints, + item, + owner_subagent_id, + "openai_response_tool_call", + &[&["call_id"][..], &["id"][..]], + &[&["name"][..], &["tool_name"][..]], + &[&["arguments"][..], &["input"][..]], + ); + } +} + +// Collects Anthropic `tool_use` blocks from top-level or nested message content arrays. Other +// content block types are skipped so text and thinking blocks never become tool hints. +fn collect_anthropic_tool_hints( + response: &Value, + owner_subagent_id: Option<&str>, + hints: &mut Vec, +) { + for content in [ + response.get("content"), + response + .get("message") + .and_then(|message| message.get("content")), + ] + .into_iter() + .flatten() + .filter_map(Value::as_array) + { + for block in content { + if json_string_at(block, &[&["type"][..]]).as_deref() == Some("tool_use") { + push_tool_hint( + hints, + block, + owner_subagent_id, + "anthropic_tool_use", + &[&["id"][..], &["tool_use_id"][..]], + &[&["name"][..], &["tool_name"][..]], + &[&["input"][..], &["arguments"][..]], + ); + } + } + } +} + +// Appends one provider tool hint when an object carries at least a tool-call id or tool name. +// Argument-only hints are intentionally skipped because they over-match across unrelated tools. +fn push_tool_hint( + hints: &mut Vec, + object: &Value, + owner_subagent_id: Option<&str>, + source: &str, + id_paths: &[&[&str]], + name_paths: &[&[&str]], + argument_paths: &[&[&str]], +) { + let tool_call_id = json_string_at(object, id_paths); + let tool_name = json_string_at(object, name_paths); + if tool_call_id.is_none() && tool_name.is_none() { + return; + } + hints.push(ToolHint { + tool_call_id, + tool_name, + subagent_id: owner_subagent_id.map(ToOwned::to_owned), + arguments: json_value_at(object, argument_paths) + .map(normalize_tool_arguments) + .unwrap_or(Value::Null), + source: source.to_string(), + }); +} + +// Scores how strongly a pending provider tool hint matches an observed hook event. Tool-call id is +// strongest, tool name is secondary, and exact argument equality is only a tie breaker. +fn tool_hint_match_score(hint: &ToolHint, event: &ToolEvent) -> u8 { + let mut score = 0; + if same_optional( + hint.tool_call_id.as_deref(), + Some(event.tool_call_id.as_str()), + ) { + score += 12; + } + if same_optional(hint.tool_name.as_deref(), Some(event.tool_name.as_str())) { + score += 4; + } + if !hint.arguments.is_null() && !event.arguments.is_null() && hint.arguments == event.arguments + { + score += 1; + } + score +} + +fn same_optional(left: Option<&str>, right: Option<&str>) -> bool { + matches!((left, right), (Some(left), Some(right)) if left == right) +} + +// Reads the first string-like value from any candidate JSON path. Scalar numbers and booleans are +// accepted for IDs because provider payloads are not always strict about identifier types. +fn json_string_at(payload: &Value, paths: &[&[&str]]) -> Option { + json_value_at(payload, paths) + .and_then(|value| match value { + Value::String(value) => Some(value), + Value::Number(value) => Some(value.to_string()), + Value::Bool(value) => Some(value.to_string()), + _ => None, + }) + .filter(|value| !value.is_empty()) +} + +// Reads the first JSON value from any candidate path. The clone is intentional because extracted +// hint data must live independently of the response body stored on the LLM end event. +fn json_value_at(payload: &Value, paths: &[&[&str]]) -> Option { + paths.iter().find_map(|path| { + let mut current = payload; + for key in *path { + current = current.get(*key)?; + } + Some(current.clone()) + }) +} + +// Parses stringified tool arguments when providers encode them as JSON text. Non-JSON strings are +// preserved as strings so metadata still reflects what the provider actually returned. +fn normalize_tool_arguments(arguments: Value) -> Value { + match arguments { + Value::String(raw) => serde_json::from_str(&raw).unwrap_or(Value::String(raw)), + value => value, + } +} + +// Adds correlation status and consumed-hint identifiers to the LLM event metadata. Caller metadata +// is merged first so correlation keys win when names collide. +fn llm_correlation_metadata( + metadata: Value, + status: &str, + source: Option<&str>, + subagent_id: Option<&str>, + hint: Option<&LlmHintEvent>, +) -> Value { + let mut correlation = Map::new(); + correlation.insert("llm_correlation_status".into(), json!(status)); + if let Some(source) = source { + correlation.insert("llm_correlation_source".into(), json!(source)); + } + if let Some(subagent_id) = subagent_id { + correlation.insert("llm_correlation_subagent_id".into(), json!(subagent_id)); + } + if let Some(hint) = hint { + insert_optional( + &mut correlation, + "llm_correlation_conversation_id", + hint.conversation_id.as_deref(), + ); + insert_optional( + &mut correlation, + "llm_correlation_generation_id", + hint.generation_id.as_deref(), + ); + insert_optional( + &mut correlation, + "llm_correlation_request_id", + hint.request_id.as_deref(), + ); + insert_optional( + &mut correlation, + "llm_correlation_agent_type", + hint.agent_type.as_deref(), + ); + } + merge_metadata(metadata, Value::Object(correlation)) +} + +// Adds correlation metadata to tool spans created from hook events. Consumed hints preserve the +// provider-side tool id/name and extracted arguments so ambiguous or fallback ownership can be +// debugged from emitted events. +fn tool_correlation_metadata( + metadata: Value, + status: &str, + source: Option<&str>, + subagent_id: Option<&str>, + hint: Option<&ToolHint>, +) -> Value { + let mut correlation = Map::new(); + correlation.insert("tool_correlation_status".into(), json!(status)); + if let Some(source) = source { + correlation.insert("tool_correlation_source".into(), json!(source)); + } + if let Some(subagent_id) = subagent_id { + correlation.insert("tool_correlation_subagent_id".into(), json!(subagent_id)); + } + if let Some(hint) = hint { + insert_optional( + &mut correlation, + "tool_correlation_tool_call_id", + hint.tool_call_id.as_deref(), + ); + insert_optional( + &mut correlation, + "tool_correlation_tool_name", + hint.tool_name.as_deref(), + ); + if !hint.arguments.is_null() { + correlation.insert("tool_correlation_arguments".into(), hint.arguments.clone()); + } + } + merge_metadata(metadata, Value::Object(correlation)) +} + +// Inserts an optional string value into a JSON object while omitting absent fields entirely. This +// keeps correlation metadata compact and avoids serializing nulls as meaningful observations. +fn insert_optional(object: &mut Map, key: &str, value: Option<&str>) { + if let Some(value) = value { + object.insert(key.to_string(), json!(value)); + } +} + +// Extracts the source agent kind from any normalized event variant so newly created sessions can +// inherit the correct agent identity before an explicit agent-start hook arrives. fn event_agent_kind(event: &NormalizedEvent) -> AgentKind { match event { NormalizedEvent::AgentStarted(event) | NormalizedEvent::AgentEnded(event) | NormalizedEvent::PromptSubmitted(event) - | NormalizedEvent::AgentResponse(event) | NormalizedEvent::Compaction(event) | NormalizedEvent::Notification(event) | NormalizedEvent::HookMark(event) => event.agent_kind, + NormalizedEvent::LlmHint(event) => event.agent_kind, NormalizedEvent::SubagentStarted(event) | NormalizedEvent::SubagentEnded(event) => { event.agent_kind } @@ -470,12 +1311,17 @@ fn event_agent_kind(event: &NormalizedEvent) -> AgentKind { } } +// Returns a session id only when exactly one session is active. Gateway requests without explicit +// session headers use this narrow fallback to avoid cross-correlating concurrent agents. fn single_active_session_id(sessions: &HashMap) -> Option { (sessions.len() == 1) .then(|| sessions.keys().next().cloned()) .flatten() } +// Merges metadata objects with right-hand values taking precedence and null right-hand fields +// ignored. Non-object values are preserved under separate keys so callers do not lose unusual +// metadata shapes supplied by configuration or hooks. fn merge_metadata(left: Value, right: Value) -> Value { match (left, right) { (Value::Object(mut left), Value::Object(right)) => { diff --git a/crates/sidecar/tests/cli_tests.rs b/crates/sidecar/tests/cli_tests.rs new file mode 100644 index 0000000..eb0bbad --- /dev/null +++ b/crates/sidecar/tests/cli_tests.rs @@ -0,0 +1,325 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! CLI-level sidecar coverage tests. + +use std::io::{Read, Write}; +use std::net::TcpListener; +use std::process::{Command, Stdio}; +use std::sync::mpsc; +use std::thread; + +fn sidecar_bin() -> &'static str { + env!("CARGO_BIN_EXE_nemo-flow-sidecar") +} + +#[test] +fn cli_help_exits_successfully() { + let output = Command::new(sidecar_bin()).arg("--help").output().unwrap(); + + assert!(output.status.success()); + assert!(String::from_utf8_lossy(&output.stdout).contains("Gateway sidecar")); +} + +#[test] +fn cli_install_dry_run_plans_without_writing() { + let temp = tempfile::tempdir().unwrap(); + let output = Command::new(sidecar_bin()) + .env("HOME", temp.path()) + .args([ + "install", + "codex", + "--dry-run", + "--print", + "--target", + "both", + "--sidecar-url", + "http://127.0.0.1:4040", + "--session-metadata", + r#"{"team":"cli"}"#, + "--plugin-config", + r#"{"components":[]}"#, + "--gateway-mode", + "required", + ]) + .output() + .unwrap(); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Dry run: would install")); + assert!(stdout.contains("hook-forward codex")); + assert!(!temp.path().join(".codex/hooks.json").exists()); +} + +#[test] +fn cli_run_dry_run_resolves_config_and_command() { + let temp = tempfile::tempdir().unwrap(); + let config = temp.path().join("sidecar.toml"); + std::fs::write( + &config, + r#" +[server] +openai_base_url = "http://file-openai" +anthropic_base_url = "http://file-anthropic" + +[session] +atif_dir = "file-atif" + +[export.openinference] +endpoint = "http://otel" + +[agents.hermes] +command = "hermes --yolo chat" +"#, + ) + .unwrap(); + + let output = Command::new(sidecar_bin()) + .args([ + "--config", + config.to_str().unwrap(), + "run", + "--agent", + "hermes", + "--dry-run", + ]) + .output() + .unwrap(); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("agent = hermes")); + assert!(stdout.contains("openai_base_url = http://file-openai")); + assert!(stdout.contains("argv = hermes --yolo chat")); +} + +#[test] +fn cli_run_dry_run_uses_project_user_and_env_config_layers() { + let temp = tempfile::tempdir().unwrap(); + let project = temp.path().join("project"); + let nested = project.join("nested"); + let xdg = temp.path().join("xdg/nemo-flow"); + std::fs::create_dir_all(project.join(".nemo-flow")).unwrap(); + std::fs::create_dir_all(&nested).unwrap(); + std::fs::create_dir_all(&xdg).unwrap(); + std::fs::write( + project.join(".nemo-flow/sidecar.toml"), + r#" +[server] +openai_base_url = "http://project-openai" +"#, + ) + .unwrap(); + std::fs::write( + xdg.join("sidecar.toml"), + r#" +[server] +anthropic_base_url = "http://user-anthropic" + +[agents.codex] +command = "codex --full-auto" +"#, + ) + .unwrap(); + + let output = Command::new(sidecar_bin()) + .current_dir(&nested) + .env("XDG_CONFIG_HOME", temp.path().join("xdg")) + .env("NEMO_FLOW_SIDECAR_BIND", "127.0.0.1:0") + .env("NEMO_FLOW_OPENAI_BASE_URL", "http://env-openai") + .env("NEMO_FLOW_ANTHROPIC_BASE_URL", "http://env-anthropic") + .env("NEMO_FLOW_ATIF_DIR", "env-atif") + .env("NEMO_FLOW_OPENINFERENCE_ENDPOINT", "http://env-otel") + .args(["run", "--agent", "codex", "--dry-run"]) + .output() + .unwrap(); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("openai_base_url = http://env-openai")); + assert!(stdout.contains("anthropic_base_url = http://env-anthropic")); + assert!(stdout.contains("atif_dir = env-atif")); + assert!(stdout.contains("openinference_endpoint = http://env-otel")); + assert!(stdout.contains("argv = codex")); +} + +#[test] +fn cli_hook_forward_fails_open_without_sidecar_url() { + let mut child = Command::new(sidecar_bin()) + .env_remove("NEMO_FLOW_SIDECAR_URL") + .args(["hook-forward", "codex"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child.stdin.take().unwrap().write_all(b"").unwrap(); + let output = child.wait_with_output().unwrap(); + + assert!(output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("missing sidecar URL")); +} + +#[test] +fn cli_hook_forward_fails_closed_without_sidecar_url() { + let mut child = Command::new(sidecar_bin()) + .env_remove("NEMO_FLOW_SIDECAR_URL") + .args(["hook-forward", "codex", "--fail-closed"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child.stdin.take().unwrap().write_all(b"{}").unwrap(); + let output = child.wait_with_output().unwrap(); + + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("missing sidecar URL")); +} + +#[test] +fn cli_hook_forward_posts_payload_headers_and_prints_response() { + let (server_url, received) = spawn_single_request_server(200, r#"{"continue":true}"#); + let mut child = Command::new(sidecar_bin()) + .args([ + "hook-forward", + "codex", + "--sidecar-url", + &server_url, + "--atif-dir", + "atif", + "--openinference-endpoint", + "http://otel", + "--profile", + "coverage", + "--session-metadata", + r#"{"team":"cli"}"#, + "--plugin-config", + r#"{"components":[]}"#, + "--gateway-mode", + "passthrough", + "--fail-closed", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child + .stdin + .take() + .unwrap() + .write_all(br#"{"hook_event_name":"sessionStart"}"#) + .unwrap(); + let output = child.wait_with_output().unwrap(); + let request = received.recv().unwrap(); + + assert!(output.status.success()); + assert_eq!( + String::from_utf8_lossy(&output.stdout).trim(), + r#"{"continue":true}"# + ); + assert!(request.contains("POST /hooks/codex HTTP/1.1")); + assert!(request.contains("x-nemo-flow-atif-dir: atif")); + assert!(request.contains("x-nemo-flow-openinference-endpoint: http://otel")); + assert!(request.contains("x-nemo-flow-config-profile: coverage")); + assert!(request.contains("x-nemo-flow-gateway-mode: passthrough")); + assert!(request.contains(r#"{"hook_event_name":"sessionStart"}"#)); +} + +#[test] +fn cli_hook_forward_reports_http_failure_when_fail_closed() { + let (server_url, received) = spawn_single_request_server(503, "unavailable"); + let mut child = Command::new(sidecar_bin()) + .args([ + "hook-forward", + "cursor", + "--sidecar-url", + &server_url, + "--fail-closed", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child.stdin.take().unwrap().write_all(b"{}").unwrap(); + let output = child.wait_with_output().unwrap(); + let request = received.recv().unwrap(); + + assert!(!output.status.success()); + assert!(request.contains("POST /hooks/cursor HTTP/1.1")); + assert!(String::from_utf8_lossy(&output.stderr).contains("HTTP 503")); +} + +#[test] +fn cli_hook_forward_reports_transport_failure_when_fail_closed() { + let mut child = Command::new(sidecar_bin()) + .args([ + "hook-forward", + "codex", + "--sidecar-url", + "http://127.0.0.1:1", + "--fail-closed", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child.stdin.take().unwrap().write_all(b"{}").unwrap(); + let output = child.wait_with_output().unwrap(); + + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("hook forward failed")); +} + +fn spawn_single_request_server( + status: u16, + body: &'static str, +) -> (String, mpsc::Receiver) { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let (sender, receiver) = mpsc::channel(); + thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let request = read_http_request(&mut stream); + sender.send(request).unwrap(); + let response = format!( + "HTTP/1.1 {status} OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}", + body.len() + ); + stream.write_all(response.as_bytes()).unwrap(); + }); + (format!("http://{address}"), receiver) +} + +fn read_http_request(stream: &mut std::net::TcpStream) -> String { + let mut buffer = Vec::new(); + let mut scratch = [0; 1024]; + loop { + let read = stream.read(&mut scratch).unwrap(); + assert_ne!(read, 0); + buffer.extend_from_slice(&scratch[..read]); + if let Some(header_end) = find_header_end(&buffer) { + let headers = String::from_utf8_lossy(&buffer[..header_end]); + let content_length = headers + .lines() + .find_map(|line| line.strip_prefix("content-length: ")) + .and_then(|value| value.trim().parse::().ok()) + .unwrap_or(0); + let expected = header_end + 4 + content_length; + while buffer.len() < expected { + let read = stream.read(&mut scratch).unwrap(); + assert_ne!(read, 0); + buffer.extend_from_slice(&scratch[..read]); + } + return String::from_utf8(buffer).unwrap(); + } + } +} + +fn find_header_end(buffer: &[u8]) -> Option { + buffer.windows(4).position(|window| window == b"\r\n\r\n") +} diff --git a/crates/sidecar/tests/coverage/adapters_tests.rs b/crates/sidecar/tests/coverage/adapters_tests.rs index a76401b..4a5e2ef 100644 --- a/crates/sidecar/tests/coverage/adapters_tests.rs +++ b/crates/sidecar/tests/coverage/adapters_tests.rs @@ -123,6 +123,54 @@ fn maps_claude_subagent_canonical_agent_id() { } } +#[test] +fn maps_claude_subagent_stop() { + let outcome = claude_code::adapt( + json!({ + "session_id": "claude-session", + "hook_event_name": "SubagentStop", + "agent_id": "agent-worker-1" + }), + &HeaderMap::new(), + ); + + match &outcome.events[0] { + NormalizedEvent::SubagentEnded(event) => { + assert_eq!(event.subagent_id, "agent-worker-1"); + } + event => panic!("unexpected event: {event:?}"), + } +} + +#[test] +fn maps_claude_stop_response_shape() { + let outcome = claude_code::adapt( + json!({ + "session_id": "claude-session", + "hook_event_name": "Stop" + }), + &HeaderMap::new(), + ); + + assert_eq!( + outcome.response, + json!({ "continue": true, "stopReason": null }) + ); +} + +#[test] +fn adapter_string_lookup_accepts_scalar_values_only() { + let payload = json!({ + "number": 7, + "boolean": false, + "object": { "nested": true } + }); + + assert_eq!(string_at(&payload, &["number"]).as_deref(), Some("7")); + assert_eq!(string_at(&payload, &["boolean"]).as_deref(), Some("false")); + assert_eq!(string_at(&payload, &["object"]), None); +} + #[test] fn maps_cursor_subagent_and_permission_response() { let headers = HeaderMap::new(); @@ -245,21 +293,52 @@ fn normalizes_mark_style_events_and_header_session_ids() { }), &headers, ); - let session = match &outcome.events[0] { - NormalizedEvent::PromptSubmitted(event) if expected == "prompt" => event, - NormalizedEvent::AgentResponse(event) if expected == "response" => event, - NormalizedEvent::Compaction(event) if expected == "compact" => event, - NormalizedEvent::Notification(event) if expected == "notification" => event, - NormalizedEvent::HookMark(event) if expected == "hook" => event, + let (session_id, metadata) = match &outcome.events[0] { + NormalizedEvent::LlmHint(event) if expected == "prompt" => { + (event.session_id.as_str(), &event.metadata) + } + NormalizedEvent::LlmHint(event) if expected == "response" => { + (event.session_id.as_str(), &event.metadata) + } + NormalizedEvent::Compaction(event) if expected == "compact" => { + (event.session_id.as_str(), &event.metadata) + } + NormalizedEvent::Notification(event) if expected == "notification" => { + (event.session_id.as_str(), &event.metadata) + } + NormalizedEvent::HookMark(event) if expected == "hook" => { + (event.session_id.as_str(), &event.metadata) + } event => panic!("unexpected event for {event_name}: {event:?}"), }; - assert_eq!(session.session_id, "header-session"); - assert_eq!(session.metadata["model"], json!("model-a")); - assert_eq!(session.metadata["cwd"], json!("/repo")); - assert_eq!( - session.metadata["sidecar_config_profile"], - json!("coverage") - ); + assert_eq!(session_id, "header-session"); + assert_eq!(metadata["model"], json!("model-a")); + assert_eq!(metadata["cwd"], json!("/repo")); + assert_eq!(metadata["sidecar_config_profile"], json!("coverage")); + } +} + +#[test] +fn maps_hermes_llm_hooks_to_private_hints() { + let headers = HeaderMap::new(); + let outcome = hermes::adapt( + json!({ + "hook_event_name": "pre_llm_call", + "session_id": "hermes-session", + "model": "anthropic/claude-sonnet", + "request_id": "req-1" + }), + &headers, + ); + + match &outcome.events[0] { + NormalizedEvent::LlmHint(event) => { + assert_eq!(event.session_id, "hermes-session"); + assert_eq!(event.event_name, "pre_llm_call"); + assert_eq!(event.model.as_deref(), Some("anthropic/claude-sonnet")); + assert_eq!(event.request_id.as_deref(), Some("req-1")); + } + event => panic!("unexpected event: {event:?}"), } } @@ -322,7 +401,7 @@ fn stop_responses_preserve_vendor_shapes() { }), &headers, ); - assert!(matches!(claude.events[0], NormalizedEvent::HookMark(_))); + assert!(matches!(claude.events[0], NormalizedEvent::LlmHint(_))); assert_eq!(claude.response["stopReason"], Value::Null); let codex = codex::adapt( @@ -332,7 +411,7 @@ fn stop_responses_preserve_vendor_shapes() { }), &headers, ); - assert!(matches!(codex.events[0], NormalizedEvent::HookMark(_))); + assert!(matches!(codex.events[0], NormalizedEvent::LlmHint(_))); assert_eq!(codex.response, json!({})); let cursor = cursor::adapt( diff --git a/crates/sidecar/tests/coverage/config_tests.rs b/crates/sidecar/tests/coverage/config_tests.rs index ba4288b..10bea3e 100644 --- a/crates/sidecar/tests/coverage/config_tests.rs +++ b/crates/sidecar/tests/coverage/config_tests.rs @@ -247,6 +247,87 @@ openai_base_url = "http://file-openai" assert_eq!(resolved.sidecar.openai_base_url, "http://top-level-openai"); } +#[test] +fn server_resolution_applies_all_server_overrides() { + let args = ServerArgs { + config: None, + bind: Some("127.0.0.1:0".parse().unwrap()), + openai_base_url: Some("http://cli-openai".into()), + anthropic_base_url: Some("http://cli-anthropic".into()), + atif_dir: Some(PathBuf::from("cli-atif")), + openinference_endpoint: Some("http://cli-otel".into()), + }; + + let resolved = resolve_server_config(&args).unwrap(); + + assert_eq!(resolved.sidecar.bind.to_string(), "127.0.0.1:0"); + assert_eq!(resolved.sidecar.openai_base_url, "http://cli-openai"); + assert_eq!(resolved.sidecar.anthropic_base_url, "http://cli-anthropic"); + assert_eq!(resolved.sidecar.atif_dir, Some(PathBuf::from("cli-atif"))); + assert_eq!( + resolved.sidecar.openinference_endpoint.as_deref(), + Some("http://cli-otel") + ); +} + +#[test] +fn run_resolution_applies_all_run_overrides() { + let command = RunCommand { + agent: Some(CodingAgent::Codex), + config: None, + openai_base_url: Some("http://run-openai".into()), + anthropic_base_url: Some("http://run-anthropic".into()), + atif_dir: Some(PathBuf::from("run-atif")), + openinference_endpoint: Some("http://run-otel".into()), + session_metadata: Some(r#"{"team":"run"}"#.into()), + plugin_config: Some(r#"{"components":["x"]}"#.into()), + dry_run: false, + print: false, + command: vec!["codex".into()], + }; + + let resolved = resolve_run_config(&command, None).unwrap(); + + assert_eq!(resolved.sidecar.openai_base_url, "http://run-openai"); + assert_eq!(resolved.sidecar.anthropic_base_url, "http://run-anthropic"); + assert_eq!(resolved.sidecar.atif_dir, Some(PathBuf::from("run-atif"))); + assert_eq!( + resolved.sidecar.openinference_endpoint.as_deref(), + Some("http://run-otel") + ); + assert_eq!(resolved.sidecar.metadata, Some(json!({ "team": "run" }))); + assert_eq!( + resolved.sidecar.plugin_config, + Some(json!({ "components": ["x"] })) + ); +} + +#[test] +fn malformed_shared_config_reports_context() { + let temp = tempfile::tempdir().unwrap(); + let invalid_toml = temp.path().join("invalid.toml"); + std::fs::write(&invalid_toml, "server = [").unwrap(); + let args = ServerArgs { + config: Some(invalid_toml), + ..ServerArgs::default() + }; + + let error = resolve_server_config(&args).unwrap_err().to_string(); + + assert!(error.contains("invalid TOML")); + + let invalid_shape = temp.path().join("invalid-shape.toml"); + std::fs::write(&invalid_shape, "server = \"not-a-table\"").unwrap(); + let args = ServerArgs { + config: Some(invalid_shape), + ..ServerArgs::default() + }; + + let error = resolve_server_config(&args).unwrap_err().to_string(); + + assert!(error.contains("invalid sidecar configuration shape")); +} + #[test] fn recursive_toml_merge_replaces_scalars_and_preserves_tables() { let mut left: toml::Value = r#" diff --git a/crates/sidecar/tests/coverage/gateway_tests.rs b/crates/sidecar/tests/coverage/gateway_tests.rs index 8106b7f..d0001c1 100644 --- a/crates/sidecar/tests/coverage/gateway_tests.rs +++ b/crates/sidecar/tests/coverage/gateway_tests.rs @@ -4,7 +4,12 @@ use super::*; use crate::config::SidecarConfig; use crate::model::{AgentKind, NormalizedEvent, SessionEvent}; -use axum::http::{HeaderMap, HeaderValue}; +use crate::server::AppState; +use axum::body::Body; +use axum::extract::State; +use axum::http::{HeaderMap, HeaderValue, Method, Request, StatusCode}; +use http_body_util::BodyExt; +use reqwest::Client; #[test] fn removes_hop_by_hop_headers() { @@ -45,6 +50,15 @@ fn selects_provider_routes() { .name(), "openai.chat_completions" ); + assert_eq!(ProviderRoute::OpenAiModels.name(), "openai.models"); + assert_eq!( + ProviderRoute::AnthropicMessages.name(), + "anthropic.messages" + ); + assert_eq!( + ProviderRoute::AnthropicCountTokens.name(), + "anthropic.count_tokens" + ); assert_eq!(ProviderRoute::from_path("/unsupported"), None); } @@ -100,6 +114,56 @@ fn gateway_session_id_prefers_headers_and_has_fallbacks() { assert_eq!(gateway_session_id(&HeaderMap::new()), None); } +#[test] +fn gateway_identifiers_accept_headers_and_scalar_body_values() { + let mut headers = HeaderMap::new(); + headers.insert( + "x-nemo-flow-request-id", + HeaderValue::from_static("req-header"), + ); + let body = json!({ + "conversation": { "id": 42 }, + "generation": { "id": true }, + "request": { "id": "req-body" }, + "object": { "id": { "nested": true } } + }); + + assert_eq!( + gateway_identifier( + &headers, + &body, + "x-nemo-flow-request-id", + &[&["request", "id"]] + ) + .as_deref(), + Some("req-header") + ); + assert_eq!( + gateway_identifier( + &HeaderMap::new(), + &body, + "missing", + &[&["conversation", "id"]] + ) + .as_deref(), + Some("42") + ); + assert_eq!( + gateway_identifier( + &HeaderMap::new(), + &body, + "missing", + &[&["generation", "id"]] + ) + .as_deref(), + Some("true") + ); + assert_eq!( + gateway_identifier(&HeaderMap::new(), &body, "missing", &[&["object", "id"]]), + None + ); +} + #[test] fn observable_headers_omit_secrets_and_transport_headers() { let mut headers = HeaderMap::new(); @@ -116,6 +180,69 @@ fn observable_headers_omit_secrets_and_transport_headers() { assert!(!observed.contains_key("connection")); } +#[tokio::test] +async fn passthrough_rejects_unsupported_provider_path_directly() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://openai".into(), + anthropic_base_url: "http://anthropic".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let state = AppState { + config: config.clone(), + http: Client::new(), + sessions: SessionManager::new(config), + }; + let request = Request::builder() + .method(Method::POST) + .uri("/unsupported") + .body(Body::empty()) + .unwrap(); + + let error = passthrough(State(state), request).await.unwrap_err(); + + assert!(error.to_string().contains("unsupported gateway path")); +} + +#[tokio::test] +async fn models_rejects_non_get_requests_directly() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://openai".into(), + anthropic_base_url: "http://anthropic".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let state = AppState { + config: config.clone(), + http: Client::new(), + sessions: SessionManager::new(config), + }; + let request = Request::builder() + .method(Method::POST) + .uri("/v1/models") + .body(Body::empty()) + .unwrap(); + + let response = models(State(state), request).await.unwrap(); + + assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); + assert!( + response + .into_body() + .collect() + .await + .unwrap() + .to_bytes() + .is_empty() + ); +} + #[test] fn response_headers_preserve_duplicates() { let mut headers = HeaderMap::new(); @@ -159,6 +286,10 @@ async fn streaming_llm_guard_closes_on_drop() { session_id: Some("drop-session".into()), provider: "openai.responses".into(), model_name: Some("gpt-test".into()), + subagent_id: None, + conversation_id: None, + generation_id: None, + request_id: None, request: LlmRequest { headers: Map::new(), content: json!({ "model": "gpt-test", "stream": true }), diff --git a/crates/sidecar/tests/coverage/installer_tests.rs b/crates/sidecar/tests/coverage/installer_tests.rs index d5cc0f7..a576265 100644 --- a/crates/sidecar/tests/coverage/installer_tests.rs +++ b/crates/sidecar/tests/coverage/installer_tests.rs @@ -38,6 +38,15 @@ fn generates_claude_install_file() { assert!(files[0].path.ends_with(".claude/settings.json")); let json: Value = serde_json::from_str(&files[0].contents).unwrap(); assert!(json["hooks"]["SessionStart"].is_array()); + assert!(json["hooks"]["UserPromptSubmit"].is_array()); + assert!(json["hooks"]["AfterAgentResponse"].is_array()); + assert!(json["hooks"]["AfterAgentThought"].is_array()); + assert!(json["hooks"]["Notification"].is_array()); + assert!( + json["hooks"]["AfterAgentResponse"][0] + .get("matcher") + .is_none() + ); assert!( json["hooks"]["PreToolUse"][0]["hooks"][0]["command"] .as_str() @@ -54,6 +63,15 @@ fn generates_codex_config_and_hooks() { assert!(files[0].contents.contains("codex_hooks = true")); let json: Value = serde_json::from_str(&files[1].contents).unwrap(); assert!(json["hooks"]["Stop"].is_array()); + assert!(json["hooks"]["UserPromptSubmit"].is_array()); + assert!(json["hooks"]["AfterAgentResponse"].is_array()); + assert!(json["hooks"]["AfterAgentThought"].is_array()); + assert!(json["hooks"]["Notification"].is_array()); + assert!( + json["hooks"]["AfterAgentThought"][0] + .get("matcher") + .is_none() + ); assert!( json["hooks"]["PreToolUse"][0]["hooks"][0]["command"] .as_str() @@ -69,6 +87,14 @@ fn generates_cursor_hooks() { assert_eq!(files.len(), 1); let json: Value = serde_json::from_str(&files[0].contents).unwrap(); assert!(json["hooks"]["beforeShellExecution"].is_array()); + assert!(json["hooks"]["beforeSubmitPrompt"].is_array()); + assert!(json["hooks"]["afterAgentResponse"].is_array()); + assert!(json["hooks"]["afterAgentThought"].is_array()); + assert!( + json["hooks"]["afterAgentThought"][0] + .get("matcher") + .is_none() + ); assert!( json["hooks"]["beforeShellExecution"][0]["hooks"][0]["command"] .as_str() @@ -85,8 +111,10 @@ fn generates_hermes_shell_hook_config() { assert!(files[0].path.ends_with(".hermes/config.yaml")); let yaml: Value = serde_yaml::from_str(&files[0].contents).unwrap(); assert!(yaml["hooks"]["on_session_start"].is_array()); + assert!(yaml["hooks"]["pre_llm_call"].is_array()); + assert!(yaml["hooks"]["post_llm_call"].is_array()); + assert!(yaml["hooks"]["subagent_start"].is_array()); assert!(yaml["hooks"]["subagent_stop"].is_array()); - assert!(yaml["hooks"].get("subagent_start").is_none()); assert!( yaml["hooks"]["pre_tool_call"][0]["command"] .as_str() @@ -122,6 +150,18 @@ hooks: ); } +#[test] +fn hermes_config_merge_rejects_invalid_yaml() { + let error = merge_hermes_config( + "hooks: [not valid", + hermes_hooks("nemo-flow-sidecar hook-forward hermes"), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains("invalid YAML in Hermes config")); +} + #[test] fn hermes_hook_forward_prefers_dynamic_env_url() { assert_eq!( @@ -202,6 +242,22 @@ fn install_writes_file_and_backs_up_existing_config() { assert_eq!(backups.len(), 1); } +#[test] +fn install_prints_target_notes_for_non_claude_agents() { + for agent in [CodingAgent::Codex, CodingAgent::Cursor, CodingAgent::Hermes] { + let temp = tempfile::tempdir().unwrap(); + let mut command = command(agent, temp.path()); + command.target = InstallTarget::Both; + + install(command).unwrap(); + } +} + +#[test] +fn target_note_noops_for_unmatched_agent_target_pairs() { + print_target_note(CodingAgent::Codex, InstallTarget::Cli); +} + #[test] fn install_dry_run_does_not_write_files() { let temp = tempfile::tempdir().unwrap(); @@ -287,6 +343,25 @@ fn helper_formatting_and_headers_cover_optional_paths() { ) .is_err() ); + + let headers = sidecar_headers(None, None, None, None, None, None).unwrap(); + assert!(headers.is_empty()); +} + +#[test] +fn generated_hook_dispatch_covers_all_agents() { + for agent in [ + CodingAgent::ClaudeCode, + CodingAgent::Codex, + CodingAgent::Cursor, + CodingAgent::Hermes, + ] { + assert!(generated_hooks(agent, "cmd")["hooks"].is_object()); + } + assert_eq!( + hook_forward_command(CodingAgent::Hermes), + "nemo-flow-sidecar hook-forward hermes" + ); } #[test] diff --git a/crates/sidecar/tests/coverage/launcher_tests.rs b/crates/sidecar/tests/coverage/launcher_tests.rs index f50d94c..7a4afd7 100644 --- a/crates/sidecar/tests/coverage/launcher_tests.rs +++ b/crates/sidecar/tests/coverage/launcher_tests.rs @@ -117,6 +117,39 @@ fn inference_failure_has_actionable_message() { assert!(error.contains("pass --agent claude-code")); } +#[test] +fn missing_configured_command_has_actionable_messages() { + let command = RunCommand { + agent: None, + config: None, + openai_base_url: None, + anthropic_base_url: None, + atif_dir: None, + openinference_endpoint: None, + session_metadata: None, + plugin_config: None, + dry_run: false, + print: false, + command: vec![], + }; + + let error = resolve_agent_and_argv(&command, &AgentConfigs::default()) + .unwrap_err() + .to_string(); + + assert!(error.contains("missing command")); + + let command = RunCommand { + agent: Some(CodingAgent::Cursor), + ..command + }; + let error = resolve_agent_and_argv(&command, &AgentConfigs::default()) + .unwrap_err() + .to_string(); + + assert!(error.contains("no configured command for cursor")); +} + #[test] fn prepares_codex_config_overrides() { let resolved = ResolvedConfig { @@ -147,6 +180,62 @@ fn prepares_codex_config_overrides() { ); } +#[test] +fn prepares_claude_dry_run_without_writing_plugin() { + let resolved = ResolvedConfig { + sidecar: SidecarConfig::default(), + agents: AgentConfigs::default(), + }; + let prepared = PreparedRun::new( + CodingAgent::ClaudeCode, + vec!["claude".into()], + "http://127.0.0.1:1234", + &resolved, + true, + ) + .unwrap(); + + assert_eq!(prepared.argv[1], "--plugin-dir"); + assert_eq!(prepared.argv[2], ""); + assert!( + prepared + .env + .contains(&("ANTHROPIC_BASE_URL".into(), "http://127.0.0.1:1234".into())) + ); + assert!(prepared.notes[0].contains("would generate")); +} + +#[test] +fn cursor_patching_can_be_disabled() { + let _guard = current_dir_lock().lock().unwrap(); + let temp = tempfile::tempdir().unwrap(); + let previous = std::env::current_dir().unwrap(); + std::env::set_current_dir(temp.path()).unwrap(); + let resolved = ResolvedConfig { + sidecar: SidecarConfig::default(), + agents: AgentConfigs { + cursor: CursorAgentConfig { + command: None, + patch_restore_hooks: false, + }, + ..AgentConfigs::default() + }, + }; + + let prepared = PreparedRun::new( + CodingAgent::Cursor, + vec!["cursor-agent".into()], + "http://s", + &resolved, + false, + ) + .unwrap(); + + assert!(prepared.cursor_restore.is_none()); + assert!(!Path::new(".cursor/hooks.json").exists()); + std::env::set_current_dir(previous).unwrap(); +} + #[test] fn prepares_hermes_hook_environment() { let resolved = ResolvedConfig { @@ -308,6 +397,65 @@ fn cursor_patch_restore_removes_temporary_file() { std::env::set_current_dir(previous).unwrap(); } +#[test] +fn cursor_restore_reports_failed_backup_restore() { + let temp = tempfile::tempdir().unwrap(); + let prepared = PreparedRun { + argv: vec![], + env: vec![], + temp_dirs: vec![], + cursor_restore: Some(CursorRestore { + path: temp.path().join("hooks.json"), + backup_path: Some(temp.path().join("missing-backup.json")), + had_original: true, + }), + notes: vec![], + }; + + let error = prepared.restore().unwrap_err().to_string(); + + assert!(error.contains("failed to restore Cursor hooks")); +} + +#[test] +fn cursor_restore_reports_failed_temporary_hook_removal() { + let temp = tempfile::tempdir().unwrap(); + let hooks_path = temp.path().join("hooks.json"); + std::fs::create_dir(&hooks_path).unwrap(); + let prepared = PreparedRun { + argv: vec![], + env: vec![], + temp_dirs: vec![], + cursor_restore: Some(CursorRestore { + path: hooks_path, + backup_path: None, + had_original: false, + }), + notes: vec![], + }; + + let error = prepared.restore().unwrap_err().to_string(); + + assert!(error.contains("failed to remove temporary Cursor hooks")); +} + +#[test] +fn cursor_restore_noops_when_original_was_declared_without_backup() { + let prepared = PreparedRun { + argv: vec![], + env: vec![], + temp_dirs: vec![], + cursor_restore: Some(CursorRestore { + path: PathBuf::from("unused"), + backup_path: None, + had_original: true, + }), + notes: vec![], + }; + + prepared.restore().unwrap(); +} + #[test] fn cursor_dry_run_does_not_write_hooks() { let _guard = current_dir_lock().lock().unwrap(); @@ -390,6 +538,16 @@ async fn dry_run_does_not_spawn_agent() { assert_eq!(code, ExitCode::SUCCESS); } +#[tokio::test] +async fn wait_for_health_reports_unready_sidecar() { + let error = wait_for_health("http://127.0.0.1:1") + .await + .unwrap_err() + .to_string(); + + assert!(error.contains("sidecar did not become ready")); +} + #[cfg(unix)] fn make_executable(path: &Path) { use std::os::unix::fs::PermissionsExt; diff --git a/crates/sidecar/tests/coverage/server_tests.rs b/crates/sidecar/tests/coverage/server_tests.rs index 024940e..2a63028 100644 --- a/crates/sidecar/tests/coverage/server_tests.rs +++ b/crates/sidecar/tests/coverage/server_tests.rs @@ -12,6 +12,7 @@ use tokio::net::TcpListener; use tower::ServiceExt; use super::*; +use crate::error::SidecarError; fn test_config() -> SidecarConfig { SidecarConfig { @@ -71,6 +72,26 @@ async fn healthz_returns_ok() { assert_eq!(body, json!({ "status": "ok" })); } +#[tokio::test] +async fn sidecar_errors_render_structured_json_responses() { + let response = SidecarError::InvalidPayload("bad input".into()).into_response(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(body["error"]["type"], json!("nemo_flow_sidecar_error")); + assert!( + body["error"]["message"] + .as_str() + .unwrap() + .contains("bad input") + ); + + let response = SidecarError::Config("bad config".into()).into_response(); + + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); +} + #[tokio::test] async fn claude_code_hook_returns_continue_shape() { let app = router(test_config()); @@ -219,6 +240,34 @@ async fn gateway_preserves_streaming_body() { assert_eq!(bytes, Bytes::from_static(b"data: one\n\ndata: two\n\n")); } +#[tokio::test] +async fn gateway_surfaces_streaming_upstream_errors() { + let upstream = spawn_failing_stream_upstream().await; + let mut config = test_config(); + config.openai_base_url = upstream; + let app = router(config); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/v1/responses") + .header("content-type", "application/json") + .body(Body::from( + json!({ + "model": "gpt-test", + "input": "hello", + "stream": true + }) + .to_string(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_GATEWAY); +} + #[tokio::test] async fn gateway_rejects_unsupported_paths() { let app = router(test_config()); @@ -237,6 +286,26 @@ async fn gateway_rejects_unsupported_paths() { assert_eq!(response.status(), StatusCode::NOT_FOUND); } +#[tokio::test] +async fn gateway_returns_bad_gateway_when_upstream_is_unreachable() { + let mut config = test_config(); + config.openai_base_url = "http://127.0.0.1:1".into(); + let app = router(config); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/v1/chat/completions") + .header("content-type", "application/json") + .body(Body::from(json!({ "model": "gpt-test" }).to_string())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_GATEWAY); +} + #[tokio::test] async fn models_route_forwards_get_requests() { let upstream = spawn_models_upstream().await; @@ -300,6 +369,27 @@ async fn spawn_upstream(streaming: bool) -> String { format!("http://{address}") } +async fn spawn_failing_stream_upstream() -> String { + async fn stream_response() -> impl IntoResponse { + let chunks = stream::iter([ + Ok::<_, std::io::Error>(Bytes::from_static(b"data: one\n\n")), + Err(std::io::Error::other("stream failed")), + ]); + ( + [(header::CONTENT_TYPE, "text/event-stream")], + Body::from_stream(chunks), + ) + } + + let app = Router::new().route("/v1/responses", post(stream_response)); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + format!("http://{address}") +} + async fn spawn_models_upstream() -> String { async fn models(headers: HeaderMap, request: Request) -> impl IntoResponse { Json(json!({ diff --git a/crates/sidecar/tests/coverage/session_tests.rs b/crates/sidecar/tests/coverage/session_tests.rs index 9e0656b..09e0983 100644 --- a/crates/sidecar/tests/coverage/session_tests.rs +++ b/crates/sidecar/tests/coverage/session_tests.rs @@ -5,7 +5,7 @@ use axum::http::HeaderMap; use serde_json::json; use super::*; -use crate::model::{SessionEvent, ToolEvent}; +use crate::model::{LlmHintEvent, SessionEvent, ToolEvent}; #[tokio::test] async fn nests_agent_subagent_and_tool_lifecycle() { @@ -342,6 +342,10 @@ async fn llm_lifecycle_starts_implicit_gateway_session() { session_id: Some("llm-session".into()), provider: "openai.responses".into(), model_name: Some("gpt-test".into()), + subagent_id: None, + conversation_id: None, + generation_id: None, + request_id: None, request: LlmRequest { headers: Map::new(), content: json!({ "model": "gpt-test", "input": "hello" }), @@ -398,6 +402,10 @@ async fn llm_lifecycle_uses_single_active_hook_session_when_header_is_missing() session_id: None, provider: "openai.responses".into(), model_name: Some("gpt-test".into()), + subagent_id: None, + conversation_id: None, + generation_id: None, + request_id: None, request: LlmRequest { headers: Map::new(), content: json!({ "model": "gpt-test", "input": "hello" }), @@ -418,6 +426,909 @@ async fn llm_lifecycle_uses_single_active_hook_session_when_header_is_missing() assert!(!sessions.contains_key("gateway-gateway")); } +#[tokio::test] +async fn single_pending_llm_hint_claims_next_gateway_llm() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + manager + .apply_events( + &HeaderMap::new(), + vec![ + NormalizedEvent::AgentStarted(SessionEvent { + session_id: "hint-session".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SessionStart".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "hint-session".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "worker-1".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::LlmHint(LlmHintEvent { + session_id: "hint-session".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "UserPromptSubmit".into(), + subagent_id: Some("worker-1".into()), + agent_id: None, + agent_type: Some("Explore".into()), + conversation_id: Some("conv-1".into()), + generation_id: None, + request_id: None, + model: Some("gpt-test".into()), + payload: json!({ "prompt": "hello" }), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + let subagent_uuid = { + let sessions = manager.inner.lock().await; + sessions + .get("hint-session") + .unwrap() + .subagents + .get("worker-1") + .unwrap() + .uuid + }; + let active = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: Some("hint-session".into()), + provider: "openai.responses".into(), + model_name: Some("gpt-test".into()), + subagent_id: None, + conversation_id: None, + generation_id: None, + request_id: None, + request: LlmRequest { + headers: Map::new(), + content: json!({ "model": "gpt-test", "input": "hello" }), + }, + streaming: false, + metadata: json!({}), + }, + ) + .await + .unwrap(); + + assert_eq!(active.handle.parent_uuid, Some(subagent_uuid)); + assert_eq!( + active.handle.metadata.as_ref().unwrap()["llm_correlation_status"], + json!("single_hint") + ); + assert_eq!( + active.handle.metadata.as_ref().unwrap()["llm_correlation_subagent_id"], + json!("worker-1") + ); + manager + .end_llm(active, json!({ "output_text": "hello" }), json!({})) + .await + .unwrap(); +} + +#[tokio::test] +async fn multiple_llm_hints_resolve_by_generation_id() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + manager + .apply_events( + &HeaderMap::new(), + vec![ + NormalizedEvent::AgentStarted(SessionEvent { + session_id: "multi-session".into(), + agent_kind: AgentKind::Cursor, + event_name: "sessionStart".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "multi-session".into(), + agent_kind: AgentKind::Cursor, + event_name: "subagentStart".into(), + subagent_id: "worker-1".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "multi-session".into(), + agent_kind: AgentKind::Cursor, + event_name: "subagentStart".into(), + subagent_id: "worker-2".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::LlmHint(LlmHintEvent { + session_id: "multi-session".into(), + agent_kind: AgentKind::Cursor, + event_name: "afterAgentThought".into(), + subagent_id: Some("worker-1".into()), + agent_id: None, + agent_type: None, + conversation_id: Some("conv-1".into()), + generation_id: Some("gen-1".into()), + request_id: None, + model: Some("gpt-test".into()), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::LlmHint(LlmHintEvent { + session_id: "multi-session".into(), + agent_kind: AgentKind::Cursor, + event_name: "afterAgentThought".into(), + subagent_id: Some("worker-2".into()), + agent_id: None, + agent_type: None, + conversation_id: Some("conv-1".into()), + generation_id: Some("gen-2".into()), + request_id: None, + model: Some("gpt-test".into()), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + let worker_2_uuid = { + let sessions = manager.inner.lock().await; + sessions + .get("multi-session") + .unwrap() + .subagents + .get("worker-2") + .unwrap() + .uuid + }; + let active = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: Some("multi-session".into()), + provider: "openai.responses".into(), + model_name: Some("gpt-test".into()), + subagent_id: None, + conversation_id: Some("conv-1".into()), + generation_id: Some("gen-2".into()), + request_id: None, + request: LlmRequest { + headers: Map::new(), + content: json!({ "model": "gpt-test", "input": "hello" }), + }, + streaming: false, + metadata: json!({}), + }, + ) + .await + .unwrap(); + + assert_eq!(active.handle.parent_uuid, Some(worker_2_uuid)); + assert_eq!( + active.handle.metadata.as_ref().unwrap()["llm_correlation_status"], + json!("matched_hint") + ); + manager + .end_llm(active, json!({ "output_text": "hello" }), json!({})) + .await + .unwrap(); +} + +#[tokio::test] +async fn ambiguous_llm_hints_fall_back_to_agent_scope() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + manager + .apply_events( + &HeaderMap::new(), + vec![ + NormalizedEvent::AgentStarted(SessionEvent { + session_id: "ambiguous-session".into(), + agent_kind: AgentKind::Cursor, + event_name: "sessionStart".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::LlmHint(LlmHintEvent { + session_id: "ambiguous-session".into(), + agent_kind: AgentKind::Cursor, + event_name: "afterAgentThought".into(), + subagent_id: None, + agent_id: None, + agent_type: None, + conversation_id: Some("conv-1".into()), + generation_id: None, + request_id: None, + model: Some("gpt-test".into()), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::LlmHint(LlmHintEvent { + session_id: "ambiguous-session".into(), + agent_kind: AgentKind::Cursor, + event_name: "afterAgentResponse".into(), + subagent_id: None, + agent_id: None, + agent_type: None, + conversation_id: Some("conv-1".into()), + generation_id: None, + request_id: None, + model: Some("gpt-test".into()), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + let agent_uuid = { + let sessions = manager.inner.lock().await; + sessions + .get("ambiguous-session") + .unwrap() + .agent_scope + .as_ref() + .unwrap() + .uuid + }; + let active = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: Some("ambiguous-session".into()), + provider: "openai.responses".into(), + model_name: Some("gpt-test".into()), + subagent_id: None, + conversation_id: Some("conv-1".into()), + generation_id: None, + request_id: None, + request: LlmRequest { + headers: Map::new(), + content: json!({ "model": "gpt-test", "input": "hello" }), + }, + streaming: false, + metadata: json!({}), + }, + ) + .await + .unwrap(); + + assert_eq!(active.handle.parent_uuid, Some(agent_uuid)); + assert_eq!( + active.handle.metadata.as_ref().unwrap()["llm_correlation_status"], + json!("ambiguous_fallback") + ); + manager + .end_llm(active, json!({ "output_text": "hello" }), json!({})) + .await + .unwrap(); +} + +#[tokio::test] +async fn no_active_hint_reuses_last_llm_owner() { + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + manager + .apply_events( + &HeaderMap::new(), + vec![ + NormalizedEvent::AgentStarted(SessionEvent { + session_id: "sticky-session".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SessionStart".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "sticky-session".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "worker-1".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::LlmHint(LlmHintEvent { + session_id: "sticky-session".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "UserPromptSubmit".into(), + subagent_id: Some("worker-1".into()), + agent_id: None, + agent_type: None, + conversation_id: Some("conv-1".into()), + generation_id: None, + request_id: None, + model: Some("gpt-test".into()), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + let first = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: Some("sticky-session".into()), + provider: "openai.responses".into(), + model_name: Some("gpt-test".into()), + subagent_id: None, + conversation_id: None, + generation_id: None, + request_id: None, + request: LlmRequest { + headers: Map::new(), + content: json!({ "model": "gpt-test", "input": "hello" }), + }, + streaming: false, + metadata: json!({}), + }, + ) + .await + .unwrap(); + let worker_uuid = first.handle.parent_uuid; + manager + .end_llm(first, json!({ "output_text": "hello" }), json!({})) + .await + .unwrap(); + + let second = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: Some("sticky-session".into()), + provider: "openai.responses".into(), + model_name: Some("gpt-test".into()), + subagent_id: None, + conversation_id: None, + generation_id: None, + request_id: None, + request: LlmRequest { + headers: Map::new(), + content: json!({ "model": "gpt-test", "input": "again" }), + }, + streaming: false, + metadata: json!({}), + }, + ) + .await + .unwrap(); + + assert_eq!(second.handle.parent_uuid, worker_uuid); + assert_eq!( + second.handle.metadata.as_ref().unwrap()["llm_correlation_status"], + json!("sticky_last_owner") + ); + manager + .end_llm(second, json!({ "output_text": "again" }), json!({})) + .await + .unwrap(); +} + +#[tokio::test] +async fn session_marks_cover_compaction_notifications_and_hook_marks() { + let temp = tempfile::tempdir().unwrap(); + let mut config = session_test_config(); + config.atif_dir = Some(temp.path().to_path_buf()); + let manager = SessionManager::new(config); + let headers = HeaderMap::new(); + + manager + .apply_events( + &headers, + vec![ + NormalizedEvent::AgentStarted(session_event("marks", "SessionStart")), + NormalizedEvent::Compaction(session_event("marks", "PreCompact")), + NormalizedEvent::Notification(session_event("marks", "Notification")), + NormalizedEvent::HookMark(session_event("marks", "CustomHook")), + NormalizedEvent::AgentEnded(session_event("marks", "SessionEnd")), + ], + ) + .await + .unwrap(); + + let atif = std::fs::read_to_string(temp.path().join("marks.atif.json")).unwrap(); + assert!(atif.contains("PreCompact")); + assert!(atif.contains("Notification")); + assert!(atif.contains("CustomHook")); +} + +#[tokio::test] +async fn agent_end_closes_active_tools_and_duplicate_starts_are_ignored() { + let manager = SessionManager::new(session_test_config()); + let headers = HeaderMap::new(); + + manager + .apply_events( + &headers, + vec![ + NormalizedEvent::AgentStarted(session_event("active-tool-cleanup", "SessionStart")), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "active-tool-cleanup".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "worker".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "active-tool-cleanup".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "worker".into(), + payload: json!({ "duplicate": true }), + metadata: json!({}), + }), + NormalizedEvent::ToolStarted(ToolEvent { + session_id: "active-tool-cleanup".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "PreToolUse".into(), + tool_call_id: "tool-1".into(), + tool_name: "Read".into(), + subagent_id: Some("worker".into()), + arguments: json!({ "file_path": "README.md" }), + result: Value::Null, + status: None, + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::ToolStarted(ToolEvent { + session_id: "active-tool-cleanup".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "PreToolUse".into(), + tool_call_id: "tool-1".into(), + tool_name: "Read".into(), + subagent_id: Some("worker".into()), + arguments: json!({ "file_path": "README.md" }), + result: Value::Null, + status: None, + payload: json!({ "duplicate": true }), + metadata: json!({}), + }), + NormalizedEvent::AgentEnded(session_event("active-tool-cleanup", "SessionEnd")), + ], + ) + .await + .unwrap(); + + assert!(manager.inner.lock().await.is_empty()); +} + +#[tokio::test] +async fn explicit_gateway_subagent_header_sets_llm_parent() { + let manager = SessionManager::new(session_test_config()); + let headers = HeaderMap::new(); + manager + .apply_events( + &headers, + vec![ + NormalizedEvent::AgentStarted(session_event("explicit-owner", "SessionStart")), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "explicit-owner".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "worker".into(), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + let subagent_uuid = { + let sessions = manager.inner.lock().await; + sessions + .get("explicit-owner") + .unwrap() + .subagents + .get("worker") + .unwrap() + .uuid + }; + let active = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: Some("explicit-owner".into()), + subagent_id: Some("worker".into()), + ..llm_start() + }, + ) + .await + .unwrap(); + + assert_eq!(active.handle.parent_uuid, Some(subagent_uuid)); + assert_eq!( + active.handle.metadata.as_ref().unwrap()["llm_correlation_status"], + json!("explicit") + ); + assert_eq!( + active.handle.metadata.as_ref().unwrap()["llm_correlation_source"], + json!("gateway_header") + ); +} + +#[tokio::test] +async fn single_active_subagent_claims_unhinted_gateway_llm() { + let manager = SessionManager::new(session_test_config()); + let headers = HeaderMap::new(); + manager + .apply_events( + &headers, + vec![ + NormalizedEvent::AgentStarted(session_event("single-subagent", "SessionStart")), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "single-subagent".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "worker".into(), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + let subagent_uuid = { + let sessions = manager.inner.lock().await; + sessions + .get("single-subagent") + .unwrap() + .subagents + .get("worker") + .unwrap() + .uuid + }; + let active = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: Some("single-subagent".into()), + ..llm_start() + }, + ) + .await + .unwrap(); + + assert_eq!(active.handle.parent_uuid, Some(subagent_uuid)); + assert_eq!( + active.handle.metadata.as_ref().unwrap()["llm_correlation_status"], + json!("active_subagent") + ); +} + +#[tokio::test] +async fn llm_response_tool_hint_claims_next_tool_hook() { + let manager = SessionManager::new(session_test_config()); + manager + .apply_events( + &HeaderMap::new(), + vec![ + NormalizedEvent::AgentStarted(session_event("tool-hints", "SessionStart")), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "tool-hints".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "worker".into(), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + let subagent_uuid = { + let sessions = manager.inner.lock().await; + sessions + .get("tool-hints") + .unwrap() + .subagents + .get("worker") + .unwrap() + .uuid + }; + let active = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: Some("tool-hints".into()), + subagent_id: Some("worker".into()), + ..llm_start() + }, + ) + .await + .unwrap(); + manager + .end_llm( + active, + json!({ + "output": [ + { + "type": "function_call", + "call_id": "call-1", + "name": "Read", + "arguments": "{\"file_path\":\"README.md\"}" + } + ] + }), + json!({}), + ) + .await + .unwrap(); + + manager + .apply_events( + &HeaderMap::new(), + vec![NormalizedEvent::ToolStarted(ToolEvent { + session_id: "tool-hints".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "PreToolUse".into(), + tool_call_id: "call-1".into(), + tool_name: "Read".into(), + subagent_id: None, + arguments: Value::Null, + result: Value::Null, + status: None, + payload: json!({}), + metadata: json!({}), + })], + ) + .await + .unwrap(); + + let sessions = manager.inner.lock().await; + let handle = sessions + .get("tool-hints") + .unwrap() + .tools + .get("call-1") + .unwrap(); + assert_eq!(handle.parent_uuid, Some(subagent_uuid)); + assert_eq!( + handle.metadata.as_ref().unwrap()["tool_correlation_status"], + json!("single_hint") + ); + assert_eq!( + handle.metadata.as_ref().unwrap()["tool_correlation_subagent_id"], + json!("worker") + ); +} + +#[tokio::test] +async fn multiple_tool_hints_resolve_by_tool_call_id() { + let manager = SessionManager::new(session_test_config()); + manager + .apply_events( + &HeaderMap::new(), + vec![ + NormalizedEvent::AgentStarted(session_event("multi-tool-hints", "SessionStart")), + NormalizedEvent::SubagentStarted(SubagentEvent { + session_id: "multi-tool-hints".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SubagentStart".into(), + subagent_id: "worker".into(), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + let active = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: Some("multi-tool-hints".into()), + subagent_id: Some("worker".into()), + ..llm_start() + }, + ) + .await + .unwrap(); + manager + .end_llm( + active, + json!({ + "choices": [{ + "message": { + "tool_calls": [ + { "id": "call-a", "function": { "name": "Read", "arguments": "{}" } }, + { "id": "call-b", "function": { "name": "Bash", "arguments": "{\"command\":\"pwd\"}" } } + ] + } + }] + }), + json!({}), + ) + .await + .unwrap(); + + manager + .apply_events( + &HeaderMap::new(), + vec![NormalizedEvent::ToolStarted(ToolEvent { + session_id: "multi-tool-hints".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "PreToolUse".into(), + tool_call_id: "call-b".into(), + tool_name: "Bash".into(), + subagent_id: None, + arguments: json!({ "command": "pwd" }), + result: Value::Null, + status: None, + payload: json!({}), + metadata: json!({}), + })], + ) + .await + .unwrap(); + + let sessions = manager.inner.lock().await; + let handle = sessions + .get("multi-tool-hints") + .unwrap() + .tools + .get("call-b") + .unwrap(); + assert_eq!( + handle.metadata.as_ref().unwrap()["tool_correlation_status"], + json!("matched_hint") + ); + assert_eq!( + handle.metadata.as_ref().unwrap()["tool_correlation_tool_call_id"], + json!("call-b") + ); +} + +#[tokio::test] +async fn hint_for_missing_subagent_falls_back_to_agent_scope() { + let manager = SessionManager::new(session_test_config()); + let headers = HeaderMap::new(); + manager + .apply_events( + &headers, + vec![ + NormalizedEvent::AgentStarted(session_event("missing-hint-owner", "SessionStart")), + NormalizedEvent::LlmHint(LlmHintEvent { + session_id: "missing-hint-owner".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "UserPromptSubmit".into(), + subagent_id: Some("missing-worker".into()), + agent_id: None, + agent_type: None, + conversation_id: None, + generation_id: None, + request_id: None, + model: Some("gpt-test".into()), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + let agent_uuid = { + let sessions = manager.inner.lock().await; + sessions + .get("missing-hint-owner") + .unwrap() + .agent_scope + .as_ref() + .unwrap() + .uuid + }; + let active = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: Some("missing-hint-owner".into()), + ..llm_start() + }, + ) + .await + .unwrap(); + + assert_eq!(active.handle.parent_uuid, Some(agent_uuid)); + assert_eq!( + active.handle.metadata.as_ref().unwrap()["llm_correlation_status"], + json!("single_hint") + ); + assert!( + active + .handle + .metadata + .as_ref() + .unwrap() + .get("llm_correlation_subagent_id") + .is_none() + ); +} + +#[test] +fn llm_hint_scoring_and_event_accessors_cover_all_variants() { + let hint = LlmHintEvent { + session_id: "score".into(), + agent_kind: AgentKind::Codex, + event_name: "afterAgentThought".into(), + subagent_id: Some("worker".into()), + agent_id: None, + agent_type: None, + conversation_id: Some("conv".into()), + generation_id: Some("gen".into()), + request_id: Some("req".into()), + model: Some("gpt-test".into()), + payload: json!({}), + metadata: json!({}), + }; + let start = LlmGatewayStart { + session_id: Some("score".into()), + subagent_id: Some("worker".into()), + conversation_id: Some("conv".into()), + generation_id: Some("gen".into()), + request_id: Some("req".into()), + ..llm_start() + }; + + assert_eq!(hint_match_score(&hint, &start), 21); + + for event in [ + NormalizedEvent::PromptSubmitted(session_event("variant", "UserPromptSubmit")), + NormalizedEvent::Compaction(session_event("variant", "PreCompact")), + NormalizedEvent::Notification(session_event("variant", "Notification")), + NormalizedEvent::HookMark(session_event("variant", "Custom")), + ] { + assert_eq!(event.session_id(), "variant"); + assert_eq!(event_agent_kind(&event), AgentKind::ClaudeCode); + } +} + #[test] fn merge_metadata_handles_objects_nulls_and_scalars() { assert_eq!( @@ -437,3 +1348,43 @@ fn merge_metadata_handles_objects_nulls_and_scalars() { json!({ "metadata": "left", "extra_metadata": "right" }) ); } + +fn session_test_config() -> SidecarConfig { + SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + } +} + +fn session_event(session_id: &str, event_name: &str) -> SessionEvent { + SessionEvent { + session_id: session_id.into(), + agent_kind: AgentKind::ClaudeCode, + event_name: event_name.into(), + payload: json!({ "event": event_name }), + metadata: json!({}), + } +} + +fn llm_start() -> LlmGatewayStart { + LlmGatewayStart { + session_id: Some("llm".into()), + provider: "openai.responses".into(), + model_name: Some("gpt-test".into()), + subagent_id: None, + conversation_id: None, + generation_id: None, + request_id: None, + request: LlmRequest { + headers: Map::new(), + content: json!({ "model": "gpt-test", "input": "hello" }), + }, + streaming: false, + metadata: json!({}), + } +} diff --git a/docs/index.md b/docs/index.md index aa33c73..11453a5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -165,6 +165,7 @@ Advanced Guide: Coding-Agent Gateway Sidecar Codex Sidecar Guide Cursor Sidecar Guide +Hermes Sidecar Guide Advanced Guide: Handle Non-Serializable Data Advanced Guide: Using Codecs Advanced Guide: Provider Codecs diff --git a/docs/integrate-frameworks/about.md b/docs/integrate-frameworks/about.md index d88efed..385282a 100644 --- a/docs/integrate-frameworks/about.md +++ b/docs/integrate-frameworks/about.md @@ -37,10 +37,11 @@ Use these guide links to move from the overview into task-specific instructions. - [Basic Guide: Adding Scopes](adding-scopes.md) shows how framework request and run hooks become NeMo Flow ownership boundaries. - [Basic Guide: Wrap Tool Calls](wrap-tool-calls.md) explains where to place managed tool wrappers and tool lifecycle fallbacks. - [Basic Guide: Wrap LLM Calls](wrap-llm-calls.md) explains where to place managed provider wrappers, model names, streaming behavior, and LLM lifecycle fallbacks. -- [Advanced Guide: Coding-Agent Gateway Sidecar](coding-agent-sidecar.md) describes the Rust sidecar for observing Codex, Claude Code, and Cursor through canonical hooks plus a passthrough LLM gateway. +- [Advanced Guide: Coding-Agent Gateway Sidecar](coding-agent-sidecar.md) describes the Rust sidecar for observing Codex, Claude Code, Cursor, and Hermes through canonical hooks plus a passthrough LLM gateway. - [Claude Code Sidecar Guide](coding-agent-claude-code.md) covers transparent Claude Code runs, Anthropic gateway routing, ATIF verification, and unsupported Claude application modes. - [Codex Sidecar Guide](coding-agent-codex.md) covers transparent Codex CLI runs, local GUI/app caveats, model provider routing, and remote-task limits. - [Cursor Sidecar Guide](coding-agent-cursor.md) covers transparent Cursor runs, temporary hook patching, GUI and CLI smoke tests, and gateway routing limits. +- [Hermes Sidecar Guide](coding-agent-hermes.md) covers Hermes shell hook installation, dynamic sidecar URL handling, session-finalize behavior, and hook consent caveats. - [Advanced Guide: Handle Non-Serializable Data](non-serializable-data.md) shows how to keep clients, streams, callbacks, and SDK objects outside JSON payloads. - [Advanced Guide: Using Codecs](using-codecs.md) explains typed value codecs for framework-facing wrappers. - [Advanced Guide: Provider Codecs](provider-codecs.md) explains provider request and response codecs for normalized middleware and event annotations. diff --git a/docs/integrate-frameworks/coding-agent-claude-code.md b/docs/integrate-frameworks/coding-agent-claude-code.md index 8995b74..49506cb 100644 --- a/docs/integrate-frameworks/coding-agent-claude-code.md +++ b/docs/integrate-frameworks/coding-agent-claude-code.md @@ -89,6 +89,19 @@ claude The sidecar forwards Anthropic `/v1/messages`, `/v1/messages/count_tokens`, and model routes without rewriting provider JSON. +## Captured Events + +Generated Claude Code hooks include `SessionStart`, `SessionEnd`, +`SubagentStart`, `SubagentStop`, `PreToolUse`, `PostToolUse`, +`PostToolUseFailure`, `Notification`, and `PreCompact` for scope, tool, and +mark events. `UserPromptSubmit`, `AfterAgentResponse`, `AfterAgentThought`, and +`Stop` are retained as private LLM correlation hints and are not emitted as +standalone NeMo Flow events. + +Tool hooks preserve canonical fields such as `tool_use_id`, `tool_name`, +`tool_input`, `error`, `duration_ms`, and `is_interrupt`. Subagent hooks use +`agent_id` as the subagent identifier and preserve `agent_type` in metadata. + ## Smoke Test Run a small Claude Code prompt that starts a session and uses one simple tool. @@ -124,3 +137,8 @@ the `nemo-flow-sidecar` binary is not on `PATH`. Missing LLM spans with present hook spans means Anthropic traffic is not routed through the sidecar. Verify `ANTHROPIC_BASE_URL` in the Claude Code process environment and confirm that requests hit `/v1/messages`. + +If LLM spans exist but attach to the session instead of a subagent, pass +`x-nemo-flow-subagent-id` on gateway requests or include shared +`conversation_id`, `generation_id`, or `request_id` values in both hook payloads +and provider requests. diff --git a/docs/integrate-frameworks/coding-agent-codex.md b/docs/integrate-frameworks/coding-agent-codex.md index 17be669..419d3a7 100644 --- a/docs/integrate-frameworks/coding-agent-codex.md +++ b/docs/integrate-frameworks/coding-agent-codex.md @@ -78,6 +78,20 @@ same support level only when they read the same local hook/plugin config and provider routing. Cloud tasks may still emit some lifecycle hooks, but complete LLM lifecycle capture requires model traffic to pass through the sidecar. +## Captured Events + +Generated Codex hooks include `SessionStart`, `SessionEnd`, `SubagentStart`, +`SubagentStop`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, +`Notification`, and `PreCompact` for scope, tool, and mark events. +`UserPromptSubmit`, `AfterAgentResponse`, `AfterAgentThought`, and `Stop` are +retained as private LLM correlation hints and are not emitted as standalone +NeMo Flow events. + +The transparent wrapper passes hook entries as Codex CLI config overrides and +sets `features.codex_hooks=true` for that launched process. Persistent install +writes `.codex/config.toml` with `codex_hooks = true` and merges generated hook +entries into `.codex/hooks.json`. + ## Smoke Test Run a small Codex prompt that starts a session and uses one simple tool. Then @@ -110,3 +124,8 @@ If agent/tool events exist but LLM spans are missing, the provider `base_url` is not pointing at the sidecar for the active Codex process. If only GUI sessions are missing spans, confirm the GUI is using local provider configuration rather than a remote execution path. + +If LLM spans exist but attach to the session instead of a subagent, pass +`x-nemo-flow-subagent-id` on gateway requests or include shared +`conversation_id`, `generation_id`, or `request_id` values in both hook payloads +and provider requests. diff --git a/docs/integrate-frameworks/coding-agent-cursor.md b/docs/integrate-frameworks/coding-agent-cursor.md index 41f8831..eac9875 100644 --- a/docs/integrate-frameworks/coding-agent-cursor.md +++ b/docs/integrate-frameworks/coding-agent-cursor.md @@ -84,6 +84,21 @@ Hook-only Cursor mode observes agent and tool lifecycle but cannot provide complete LLM lifecycle. Missing LLM spans are expected when Cursor sends model traffic directly to the provider or through a remote service. +## Captured Events + +Generated Cursor hooks include `sessionStart`, `sessionEnd`, `subagentStart`, +`subagentStop`, `preToolUse`, `postToolUse`, `beforeShellExecution`, +`afterShellExecution`, `beforeMCPExecution`, `afterMCPExecution`, `preCompact`, +and `stop` for scope, tool, and mark events. `beforeSubmitPrompt`, +`afterAgentResponse`, and `afterAgentThought` are retained as private LLM +correlation hints and are not emitted as standalone NeMo Flow events. + +Tool events preserve Cursor shell and MCP payloads in metadata and use the +active `subagent.id`, `subagent_id`, or `x-nemo-flow-subagent-id` when present. +The transparent wrapper backs up the project hook file, merges NeMo Flow hook +entries for the run, and restores or removes the temporary file when the agent +exits. + ## Smoke Test Run a small Cursor GUI session that starts an agent and uses one simple tool. @@ -117,3 +132,8 @@ missing, confirm Cursor loaded `.cursor/hooks.json`, the sidecar binary is on If Cursor hook events appear but LLM spans are missing, provider traffic is not routed through the sidecar. Confirm the active Cursor GUI or CLI mode supports provider base URL configuration for the model path being used. + +If LLM spans exist but attach to the session instead of a subagent, pass +`x-nemo-flow-subagent-id` on gateway requests or include shared +`conversation_id`, `generation_id`, or `request_id` values in both hook payloads +and provider requests. diff --git a/docs/integrate-frameworks/coding-agent-hermes.md b/docs/integrate-frameworks/coding-agent-hermes.md new file mode 100644 index 0000000..150e053 --- /dev/null +++ b/docs/integrate-frameworks/coding-agent-hermes.md @@ -0,0 +1,136 @@ + + +# Hermes Sidecar Guide + +Use this guide to observe local Hermes Agent sessions with NeMo Flow through +Hermes shell hooks and the `nemo-flow-sidecar` gateway. This sidecar path is +separate from the Hermes third-party patch set under `patches/hermes-agent/`; +use the sidecar when you want hook forwarding without rebuilding a patched +Hermes checkout. + +Hermes shell hooks provide session, subagent, tool, and LLM hint lifecycle +events. Complete LLM request and response observability still requires model +traffic to route through the sidecar gateway. + +## Transparent Run + +Use the wrapper when you want the sidecar lifetime managed for a local Hermes +process: + +```bash +nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- hermes +``` + +The wrapper infers Hermes from `hermes` or `hermes-agent`, starts a sidecar on a +dynamic `127.0.0.1` port, and exports `NEMO_FLOW_SIDECAR_URL` for the launched +process. Hermes hook configuration is not temporary in this mode. Install hooks +first, or configure equivalent Hermes shell hooks, so approved hook commands can +discover the dynamic sidecar URL. + +Inspect what would be launched without starting Hermes: + +```bash +nemo-flow-sidecar run \ + --atif-dir .nemo-flow/atif \ + --openinference-endpoint http://127.0.0.1:4318/v1/traces \ + --dry-run \ + --print \ + -- hermes +``` + +If a launcher hides the command name, pass the agent explicitly: + +```bash +nemo-flow-sidecar run --agent hermes -- my-hermes-wrapper +``` + +## Shared Config + +Create `.nemo-flow/sidecar.toml` for project defaults or +`~/.config/nemo-flow/sidecar.toml` for user defaults: + +```toml +[session] +atif_dir = ".nemo-flow/atif" +metadata = { team = "agent-observability" } + +[export.openinference] +endpoint = "http://127.0.0.1:4318/v1/traces" + +[agents.hermes] +command = "hermes" +``` + +Then run `nemo-flow-sidecar run --agent hermes` to use the configured command. +User config takes priority over project and global config. + +## Persistent Install + +Use persistent hooks to merge NeMo Flow hook commands into +`~/.hermes/config.yaml` or the project `.hermes/config.yaml`: + +```bash +nemo-flow-sidecar install hermes \ + --scope user \ + --target cli \ + --sidecar-url http://127.0.0.1:4040 \ + --atif-dir .nemo-flow/atif +``` + +The installer preserves existing YAML config, appends missing NeMo Flow hook +entries, and backs up the file before writing. The generated Hermes hooks cover +`on_session_start`, `on_session_end`, `on_session_finalize`, +`on_session_reset`, `pre_llm_call`, `post_llm_call`, `pre_tool_call`, +`post_tool_call`, `subagent_start`, and `subagent_stop`. + +Hermes hook forwarding prefers `NEMO_FLOW_SIDECAR_URL` when it is set, even if +the installed command also includes `--sidecar-url`. This lets persistent hook +config work with `nemo-flow-sidecar run`, where each run uses a dynamic local +port. Without `NEMO_FLOW_SIDECAR_URL`, the installed `--sidecar-url` is used. + +Then start the sidecar manually for persistent mode: + +```bash +NEMO_FLOW_ATIF_DIR=.nemo-flow/atif nemo-flow-sidecar --bind 127.0.0.1:4040 +``` + +Point Hermes provider traffic at `http://127.0.0.1:4040` for any provider mode +that exposes a local OpenAI-compatible or Anthropic-compatible base URL. + +## Smoke Test + +Run a small Hermes session that starts, invokes one tool, and exits. Then check +hook forwarding directly: + +```bash +curl -f http://127.0.0.1:4040/healthz +printf '{"session_id":"smoke-hermes","hook_event_name":"on_session_start"}' \ + | NEMO_FLOW_SIDECAR_URL=http://127.0.0.1:4040 nemo-flow-sidecar hook-forward hermes --fail-closed +``` + +The response should be `{}`. If Hermes prompts for hook consent, approve the +NeMo Flow hook command interactively or through Hermes configuration before +relying on unattended capture. + +## Verify Export + +End or finalize the Hermes session and confirm ATIF exists: + +```bash +ls .nemo-flow/atif +``` + +The sidecar writes `.atif.json` when it receives +`on_session_finalize` or `on_session_reset`. `on_session_end` is treated as a +per-turn mark and does not close the NeMo Flow session by itself. + +## Troubleshoot LLM Lifecycle + +If hook events appear but LLM spans are missing, Hermes model traffic is not +routed through the sidecar. If LLM spans exist but attach to the top-level agent +instead of a subagent, include shared identifiers in Hermes hook payloads and +gateway requests, such as `conversation_id`, `generation_id`, `request_id`, or +`x-nemo-flow-subagent-id`. diff --git a/docs/integrate-frameworks/coding-agent-sidecar.md b/docs/integrate-frameworks/coding-agent-sidecar.md index 46235ca..28a652d 100644 --- a/docs/integrate-frameworks/coding-agent-sidecar.md +++ b/docs/integrate-frameworks/coding-agent-sidecar.md @@ -11,7 +11,8 @@ passthrough LLM gateway so NeMo Flow owns both the agent lifecycle and the model request lifecycle. Use the sidecar when you need one observability boundary for OpenAI Codex, -Claude Code, and Cursor without replacing each agent's canonical hook payload. +Claude Code, Cursor, and Hermes without replacing each agent's canonical hook +payload. ## Hook Endpoints @@ -26,6 +27,8 @@ the payload in a shared sidecar envelope. - `POST /hooks/cursor` accepts Cursor hook JSON and returns Cursor-compatible fields such as `continue`, `permission`, `user_message`, and `agent_message` when the hook event supports them. +- `POST /hooks/hermes` accepts Hermes shell hook JSON and returns the empty JSON + object expected by Hermes hook commands. The adapters preserve vendor fields such as session IDs, working directories, transcript paths, model names, tool payloads, shell payloads, MCP payloads, file @@ -58,6 +61,7 @@ when the agent exits. nemo-flow-sidecar run -- codex nemo-flow-sidecar run -- claude nemo-flow-sidecar run -- cursor-agent +nemo-flow-sidecar run -- hermes ``` The wrapper infers the agent from the command basename. Use `--agent` when a @@ -67,6 +71,10 @@ launcher or wrapper hides the real agent name: nemo-flow-sidecar run --agent codex -- my-codex-wrapper ``` +Hermes is different from the other transparent modes: `run --agent hermes` +starts the sidecar and exports the dynamic `NEMO_FLOW_SIDECAR_URL`, but Hermes +shell hooks still need to be installed or otherwise approved in Hermes config. + Use `--dry-run --print` to inspect the generated hook config, gateway environment, sidecar URL, and final command without launching the agent. @@ -107,6 +115,9 @@ command = "codex" [agents.cursor] command = "cursor-agent" patch_restore_hooks = true + +[agents.hermes] +command = "hermes" ``` Transparent runs always bind the managed sidecar to `127.0.0.1:0`. The selected @@ -125,6 +136,20 @@ Per-session configuration controls the scope-local OpenInference subscriber, the ATIF exporter, structured metadata on the top-level agent begin event, and the plugin configuration metadata associated with the session. +`hook-forward` can also pass per-session configuration through headers: + +- `x-nemo-flow-atif-dir` +- `x-nemo-flow-openinference-endpoint` +- `x-nemo-flow-config-profile` +- `x-nemo-flow-session-metadata` +- `x-nemo-flow-plugin-config` +- `x-nemo-flow-gateway-mode` + +The accepted gateway mode values are `hook-only`, `passthrough`, and +`required`. The sidecar records this value as session metadata so downstream +exporters and review tooling can distinguish hook-only traces from sessions +where provider traffic was expected to pass through the gateway. + ## Runtime Mapping The sidecar normalizes vendor hook payloads into private internal events before @@ -136,10 +161,51 @@ calling NeMo Flow APIs. that scope when it is still active. - Tool pre-use starts a NeMo Flow tool span. Tool post-use, denial, or failure closes it. -- Prompt, response, compaction, notification, and unknown hook events become - mark events under the active session scope. +- Prompt, response, agent-thought, and Hermes LLM hooks are retained as + private correlation hints. They are not emitted as NeMo Flow events. +- Compaction, notification, and unknown hook events become mark events under + the active session scope. - Gateway requests emit NeMo Flow LLM start and end events under the active - session scope. + session scope. Before each LLM start, the sidecar uses explicit subagent + headers, pending hints, shared conversation/generation/request identifiers, + and the previous correlated owner to choose the parent scope. +- LLM responses that contain future tool-use suggestions are retained as + private tool-call hints. The next matching tool hook can then inherit the + subagent scope that owned the LLM response, even when the hook payload does + not include a subagent id. + +Gateway requests can provide explicit correlation identifiers with these +headers: + +- `x-nemo-flow-session-id` +- `x-nemo-flow-subagent-id` +- `x-nemo-flow-conversation-id` +- `x-nemo-flow-generation-id` +- `x-nemo-flow-request-id` + +When those headers are absent, the sidecar also looks for +`conversation_id`/`conversationId`/`conversation.id`, +`generation_id`/`generationId`/`generation.id`, and +`request_id`/`requestId`/`request.id` fields in the provider request body. +Correlation hints expire after five minutes. If the sidecar cannot select one +unambiguous hint, it falls back to the previous LLM owner, then to the only +active subagent, then to the top-level agent scope. + +Every gateway LLM event includes `llm_correlation_status` metadata. Possible +values are `explicit`, `single_hint`, `matched_hint`, `sticky_last_owner`, +`active_subagent`, `agent_fallback`, and `ambiguous_fallback`. Matched hints can +also add `llm_correlation_source`, `llm_correlation_subagent_id`, +`llm_correlation_conversation_id`, `llm_correlation_generation_id`, +`llm_correlation_request_id`, and `llm_correlation_agent_type`. + +Generated hook bundles subscribe to the events needed for that mapping: + +| Agent | Correlation hint hooks | Scope, tool, and mark hooks | +| --- | --- | --- | +| Claude Code | `UserPromptSubmit`, `AfterAgentResponse`, `AfterAgentThought`, `Stop` | `SessionStart`, `SessionEnd`, `SubagentStart`, `SubagentStop`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `Notification`, `PreCompact` | +| Codex | `UserPromptSubmit`, `AfterAgentResponse`, `AfterAgentThought`, `Stop` | `SessionStart`, `SessionEnd`, `SubagentStart`, `SubagentStop`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `Notification`, `PreCompact` | +| Cursor | `beforeSubmitPrompt`, `afterAgentResponse`, `afterAgentThought` | `sessionStart`, `sessionEnd`, `subagentStart`, `subagentStop`, `preToolUse`, `postToolUse`, `beforeShellExecution`, `afterShellExecution`, `beforeMCPExecution`, `afterMCPExecution`, `preCompact`, `stop` | +| Hermes | `pre_llm_call`, `post_llm_call` | `on_session_start`, `on_session_end`, `on_session_finalize`, `on_session_reset`, `subagent_start`, `subagent_stop`, `pre_tool_call`, `post_tool_call` | Cursor hook-only mode observes agent, subagent, and tool lifecycle. To observe Cursor LLM lifecycle completely, configure Cursor model traffic to use the @@ -155,6 +221,7 @@ instead of the transparent wrapper. nemo-flow-sidecar install claude-code --scope user --target cli --sidecar-url http://127.0.0.1:4040 nemo-flow-sidecar install codex --scope user --target both --sidecar-url http://127.0.0.1:4040 nemo-flow-sidecar install cursor --scope project --target gui --sidecar-url http://127.0.0.1:4040 +nemo-flow-sidecar install hermes --scope user --target cli --sidecar-url http://127.0.0.1:4040 ``` Use `--dry-run` to see which files would be changed. Use `--print` to print the @@ -169,6 +236,8 @@ headers: - `--openinference-endpoint` sets `x-nemo-flow-openinference-endpoint`. - `--session-metadata` sets `x-nemo-flow-session-metadata`. - `--plugin-config` sets `x-nemo-flow-plugin-config`. +- `--profile` sets `x-nemo-flow-config-profile`. +- `--gateway-mode` sets `x-nemo-flow-gateway-mode`. Static integration bundles rely on the wrapper-provided `NEMO_FLOW_SIDECAR_URL` and run: @@ -195,6 +264,7 @@ application-mode caveats. - [Claude Code Sidecar Guide](coding-agent-claude-code.md) - [Codex Sidecar Guide](coding-agent-codex.md) - [Cursor Sidecar Guide](coding-agent-cursor.md) +- [Hermes Sidecar Guide](coding-agent-hermes.md) Each guide covers transparent run setup, persistent installation, gateway routing, hook smoke tests, ATIF export verification on session end, and diff --git a/docs/reference/api/rust/index.md b/docs/reference/api/rust/index.md index a675dc9..5b97b1e 100644 --- a/docs/reference/api/rust/index.md +++ b/docs/reference/api/rust/index.md @@ -27,9 +27,8 @@ These entry points are the primary APIs to use from this binding. - `nemo-flow`: core runtime APIs for scopes, tools, LLMs, registries, subscribers, codecs, streams, and observability - `nemo-flow-adaptive`: adaptive runtime helpers, learner implementations, storage backends, and adaptive configuration +- `nemo-flow-sidecar`: binary gateway sidecar for coding-agent hooks and passthrough LLM observability - `nemo-flow-ffi`: raw C ABI used by downstream native bindings -- `nemo-flow-sidecar`: binary gateway sidecar for coding-agent hooks and - passthrough LLM observability Within `nemo-flow`, most integrations start in `api`, especially the `scope`, `tool`, `llm`, `registry`, and `subscriber` modules. Other important public @@ -62,6 +61,7 @@ Use the generated crate entry points when you need symbol-level detail: nemo-flow <_generated/nemo-flow/src> nemo-flow-adaptive <_generated/nemo-flow-adaptive/src> +nemo-flow-adaptive <_generated/nemo-flow-sidecar/src> ``` ## Related Guides @@ -77,3 +77,4 @@ Use these links to continue from the API reference into task-focused guides. - [Adaptive Optimization](../../../use-adaptive-optimization/about.md) - [Typed Wrappers and Codecs](../../../integrate-frameworks/using-codecs.md) - [Framework Integration Surfaces](../../../integrate-frameworks/about.md) +- [Coding-Agent Gateway Sidecar](../../../integrate-frameworks/coding-agent-sidecar.md) diff --git a/integrations/coding-agents/README.md b/integrations/coding-agents/README.md index f558082..b83c165 100644 --- a/integrations/coding-agents/README.md +++ b/integrations/coding-agents/README.md @@ -28,6 +28,9 @@ environment variables, or shared TOML config. `codex_hooks = true`. - `cursor/` installs a Cursor `.cursor/hooks.json` bundle targeting `POST /hooks/cursor`. +- Hermes does not require a static bundle in this directory. Use + `nemo-flow-sidecar install hermes` to merge hook commands into + `.hermes/config.yaml`. ## Transparent Setup @@ -41,10 +44,16 @@ down when the agent exits. nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- claude nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- codex nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- cursor-agent +nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- hermes ``` -Use `--agent claude-code|codex|cursor` when a wrapper hides the agent command -name. Use `--dry-run --print` to inspect generated config without launching. +Use `--agent claude-code|codex|cursor|hermes` when a wrapper hides the agent +command name. Use `--dry-run --print` to inspect generated config without +launching. + +Hermes transparent runs export the dynamic `NEMO_FLOW_SIDECAR_URL`, but Hermes +hooks still need to be installed or approved in Hermes configuration before +they can call the sidecar. Shared TOML config is loaded from `/etc/nemo-flow/sidecar.toml`, then nearest project `.nemo-flow/sidecar.toml`, then @@ -61,6 +70,9 @@ endpoint = "http://127.0.0.1:4318/v1/traces" [agents.codex] command = "codex" + +[agents.hermes] +command = "hermes" ``` ## Persistent Setup @@ -71,6 +83,7 @@ Use `install` only when you want persistent hook configuration: nemo-flow-sidecar install claude-code --scope user --target cli --sidecar-url http://127.0.0.1:4040 nemo-flow-sidecar install codex --scope user --target both --sidecar-url http://127.0.0.1:4040 nemo-flow-sidecar install cursor --scope project --target gui --sidecar-url http://127.0.0.1:4040 +nemo-flow-sidecar install hermes --scope user --target cli --sidecar-url http://127.0.0.1:4040 ``` Inspect generated changes before writing: @@ -112,6 +125,9 @@ Useful wrapper and install options: - `--session-metadata ''` adds structured metadata to the agent begin event. - `--plugin-config ''` records scope-local plugin configuration metadata. +- `--profile ` records a configuration profile in session metadata. +- `--gateway-mode hook-only|passthrough|required` records the expected gateway + behavior in session metadata. - `--fail-closed` can be added to generated hook commands when the agent should block on hook delivery failures. The default is fail-open. diff --git a/integrations/coding-agents/claude-code/README.md b/integrations/coding-agents/claude-code/README.md index 39ba86f..3ef9937 100644 --- a/integrations/coding-agents/claude-code/README.md +++ b/integrations/coding-agents/claude-code/README.md @@ -18,6 +18,14 @@ same local hook and gateway controls as Claude Code. - `hooks/hooks.json` contains hook entries that run `nemo-flow-sidecar hook-forward claude-code`. +## Captured Events + +The bundle forwards `SessionStart`, `SessionEnd`, `SubagentStart`, +`SubagentStop`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, +`Notification`, and `PreCompact` as scope, tool, or mark events. +`UserPromptSubmit`, `AfterAgentResponse`, `AfterAgentThought`, and `Stop` +provide private LLM correlation hints for gateway requests. + ## Transparent Setup Build or install the sidecar binary so `nemo-flow-sidecar` is on `PATH`. @@ -110,3 +118,8 @@ printf '{"session_id":"smoke-claude","hook_event_name":"SessionStart"}' \ If hooks arrive but LLM spans are missing, confirm the Claude Code process was started by `nemo-flow-sidecar run` or has `ANTHROPIC_BASE_URL` set to the sidecar URL. + +If LLM spans are present but attached to the top-level agent instead of a +subagent, include `x-nemo-flow-subagent-id` on gateway requests or share +`conversation_id`, `generation_id`, or `request_id` values between hook payloads +and provider requests. diff --git a/integrations/coding-agents/claude-code/hooks/hooks.json b/integrations/coding-agents/claude-code/hooks/hooks.json index 873df11..82ac68e 100644 --- a/integrations/coding-agents/claude-code/hooks/hooks.json +++ b/integrations/coding-agents/claude-code/hooks/hooks.json @@ -58,6 +58,28 @@ ] } ], + "AfterAgentResponse": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward claude-code", + "timeout": 30 + } + ] + } + ], + "AfterAgentThought": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward claude-code", + "timeout": 30 + } + ] + } + ], "SubagentStart": [ { "hooks": [ @@ -80,6 +102,17 @@ ] } ], + "Notification": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward claude-code", + "timeout": 30 + } + ] + } + ], "Stop": [ { "hooks": [ diff --git a/integrations/coding-agents/codex/README.md b/integrations/coding-agents/codex/README.md index dfec71f..ce2db35 100644 --- a/integrations/coding-agents/codex/README.md +++ b/integrations/coding-agents/codex/README.md @@ -19,6 +19,18 @@ local sidecar LLM capture. - `hooks/hooks.json` contains hook entries that run `nemo-flow-sidecar hook-forward codex`. +## Captured Events + +The bundle forwards `SessionStart`, `SessionEnd`, `SubagentStart`, +`SubagentStop`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, +`Notification`, and `PreCompact` as scope, tool, or mark events. +`UserPromptSubmit`, `AfterAgentResponse`, `AfterAgentThought`, and `Stop` +provide private LLM correlation hints for gateway requests. + +Transparent setup injects these hooks with CLI config overrides. Persistent +setup writes `codex_hooks = true` in `.codex/config.toml` and merges the hook +entries into `.codex/hooks.json`. + ## Transparent Setup Build or install the sidecar binary so `nemo-flow-sidecar` is on `PATH`. @@ -101,3 +113,8 @@ printf '{"session_id":"smoke-codex","hook_event_name":"sessionStart"}' \ If hooks arrive but LLM spans are missing, confirm Codex was started by `nemo-flow-sidecar run` or that the active provider `base_url` points to the sidecar URL. + +If LLM spans are present but attached to the top-level agent instead of a +subagent, include `x-nemo-flow-subagent-id` on gateway requests or share +`conversation_id`, `generation_id`, or `request_id` values between hook payloads +and provider requests. diff --git a/integrations/coding-agents/codex/hooks/hooks.json b/integrations/coding-agents/codex/hooks/hooks.json index a2541a7..d735537 100644 --- a/integrations/coding-agents/codex/hooks/hooks.json +++ b/integrations/coding-agents/codex/hooks/hooks.json @@ -58,6 +58,28 @@ ] } ], + "AfterAgentResponse": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward codex", + "timeout": 30 + } + ] + } + ], + "AfterAgentThought": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward codex", + "timeout": 30 + } + ] + } + ], "SubagentStart": [ { "hooks": [ @@ -80,6 +102,17 @@ ] } ], + "Notification": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward codex", + "timeout": 30 + } + ] + } + ], "Stop": [ { "hooks": [ diff --git a/integrations/coding-agents/cursor/.cursor/hooks.json b/integrations/coding-agents/cursor/.cursor/hooks.json index c44b2d9..bc5ff09 100644 --- a/integrations/coding-agents/cursor/.cursor/hooks.json +++ b/integrations/coding-agents/cursor/.cursor/hooks.json @@ -127,6 +127,17 @@ ] } ], + "afterAgentThought": [ + { + "hooks": [ + { + "type": "command", + "command": "nemo-flow-sidecar hook-forward cursor", + "timeout": 30 + } + ] + } + ], "preCompact": [ { "hooks": [ diff --git a/integrations/coding-agents/cursor/README.md b/integrations/coding-agents/cursor/README.md index 27205e3..d15c3c8 100644 --- a/integrations/coding-agents/cursor/README.md +++ b/integrations/coding-agents/cursor/README.md @@ -20,6 +20,18 @@ configuration. - `.cursor/hooks.json` contains hook entries that run `nemo-flow-sidecar hook-forward cursor`. +## Captured Events + +The bundle forwards `sessionStart`, `sessionEnd`, `subagentStart`, +`subagentStop`, `preToolUse`, `postToolUse`, `beforeShellExecution`, +`afterShellExecution`, `beforeMCPExecution`, `afterMCPExecution`, `preCompact`, +and `stop` as scope, tool, or mark events. `beforeSubmitPrompt`, +`afterAgentResponse`, and `afterAgentThought` provide private LLM correlation +hints for gateway requests. + +Tool events preserve shell and MCP payloads in metadata and attach to +`subagent.id`, `subagent_id`, or `x-nemo-flow-subagent-id` when one is present. + ## Transparent Setup Build or install the sidecar binary so `nemo-flow-sidecar` is on `PATH`. @@ -101,3 +113,8 @@ printf '{"session_id":"smoke-cursor","hook_event_name":"sessionStart"}' \ If Cursor CLI hooks do not fire for the active `cursor-agent` version, treat that CLI mode as hook-limited and rely on gateway observability where provider routing is available. + +If LLM spans are present but attached to the top-level agent instead of a +subagent, include `x-nemo-flow-subagent-id` on gateway requests or share +`conversation_id`, `generation_id`, or `request_id` values between hook payloads +and provider requests. From bfd9f73a1307011f0e989d259d84c31e467d2fb5 Mon Sep 17 00:00:00 2001 From: GSD Agent Date: Wed, 6 May 2026 14:23:06 -0700 Subject: [PATCH 09/13] feat(sidecar): capture Hermes API hook token metrics Signed-off-by: GSD Agent --- crates/sidecar/src/adapters/hermes.rs | 138 +++++++++++++++++- crates/sidecar/src/installer.rs | 2 + crates/sidecar/src/model.rs | 16 ++ crates/sidecar/src/session.rs | 80 +++++++++- .../sidecar/tests/coverage/adapters_tests.rs | 70 +++++++++ .../sidecar/tests/coverage/installer_tests.rs | 2 + .../sidecar/tests/coverage/session_tests.rs | 82 ++++++++++- 7 files changed, 382 insertions(+), 8 deletions(-) diff --git a/crates/sidecar/src/adapters/hermes.rs b/crates/sidecar/src/adapters/hermes.rs index 057eeb7..4fcf212 100644 --- a/crates/sidecar/src/adapters/hermes.rs +++ b/crates/sidecar/src/adapters/hermes.rs @@ -2,10 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use axum::http::HeaderMap; -use serde_json::{Value, json}; +use serde_json::{Map, Value, json}; -use crate::adapters::{AdapterOutcome, ClassificationRules, classify}; -use crate::model::AgentKind; +use crate::adapters::{ + AdapterOutcome, ClassificationRules, classify, event_name, metadata, normalize_name, + session_id, value_at, +}; +use crate::model::{AgentKind, LlmEvent}; /// Normalizes Hermes shell hook payloads without emitting control directives. /// @@ -13,6 +16,29 @@ use crate::model::AgentKind; /// responses minimal and relies on the forwarder fail-open/fail-closed setting to decide whether /// hook delivery problems affect the invoking agent. pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { + let event_name = event_name(&payload); + let normalized = normalize_name(&event_name); + if normalized == "preapirequest" { + return AdapterOutcome { + events: vec![crate::model::NormalizedEvent::LlmStarted(hermes_llm_event( + &payload, + headers, + &event_name, + ))], + response: json!({}), + }; + } + if normalized == "postapirequest" { + return AdapterOutcome { + events: vec![crate::model::NormalizedEvent::LlmEnded(hermes_llm_event( + &payload, + headers, + &event_name, + ))], + response: json!({}), + }; + } + let event = classify( &payload, headers, @@ -31,3 +57,109 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { response: json!({}), } } + +fn hermes_llm_event(payload: &Value, headers: &HeaderMap, event_name: &str) -> LlmEvent { + let session_id = session_id(payload, headers); + let api_call_id = hermes_api_call_id(payload, &session_id); + let provider = hermes_string_at(payload, "provider") + .or_else(|| hermes_string_at(payload, "api_mode")) + .unwrap_or_else(|| "hermes_api_request".to_string()); + let model_name = + hermes_string_at(payload, "response_model").or_else(|| hermes_string_at(payload, "model")); + let mut event_metadata = metadata(payload, headers, AgentKind::Hermes, event_name); + if let Value::Object(ref mut object) = event_metadata { + object.insert("api_call_id".into(), json!(api_call_id.clone())); + object.insert("provider_payload_exact".into(), json!(false)); + object.insert("fidelity_source".into(), json!("hermes_api_hooks")); + } + LlmEvent { + session_id, + agent_kind: AgentKind::Hermes, + event_name: event_name.to_string(), + api_call_id, + provider, + model_name, + request: hermes_llm_request(payload), + response: hermes_llm_response(payload), + metadata: event_metadata, + } +} + +fn hermes_api_call_id(payload: &Value, session_id: &str) -> String { + let task_id = hermes_string_at(payload, "task_id").unwrap_or_default(); + let api_call_count = hermes_string_at(payload, "api_call_count").unwrap_or_default(); + format!("{session_id}:{task_id}:{api_call_count}") +} + +fn hermes_llm_request(payload: &Value) -> Value { + let mut object = Map::new(); + for key in [ + "task_id", + "session_id", + "platform", + "model", + "provider", + "base_url", + "api_mode", + "api_call_count", + "message_count", + "tool_count", + "approx_input_tokens", + "request_char_count", + "max_tokens", + ] { + if let Some(value) = hermes_value_at(payload, key) { + object.insert(key.into(), value); + } + } + object.insert( + "fidelity".into(), + json!({ + "provider_payload_exact": false, + "source": "hermes_pre_api_request" + }), + ); + Value::Object(object) +} + +fn hermes_llm_response(payload: &Value) -> Value { + let mut object = Map::new(); + for key in [ + "task_id", + "session_id", + "platform", + "model", + "provider", + "base_url", + "api_mode", + "api_call_count", + "api_duration", + "finish_reason", + "message_count", + "response_model", + "usage", + "assistant_content_chars", + "assistant_tool_call_count", + ] { + if let Some(value) = hermes_value_at(payload, key) { + object.insert(key.into(), value); + } + } + Value::Object(object) +} + +fn hermes_string_at(payload: &Value, key: &str) -> Option { + value_at(payload, &[key]) + .or_else(|| value_at(payload, &["extra", key])) + .and_then(|value| match value { + Value::String(value) => Some(value), + Value::Number(value) => Some(value.to_string()), + Value::Bool(value) => Some(value.to_string()), + _ => None, + }) + .filter(|value| !value.is_empty()) +} + +fn hermes_value_at(payload: &Value, key: &str) -> Option { + value_at(payload, &[key]).or_else(|| value_at(payload, &["extra", key])) +} diff --git a/crates/sidecar/src/installer.rs b/crates/sidecar/src/installer.rs index 35de379..9695076 100644 --- a/crates/sidecar/src/installer.rs +++ b/crates/sidecar/src/installer.rs @@ -56,6 +56,8 @@ const HERMES_HOOK_EVENTS: &[&str] = &[ "on_session_reset", "pre_llm_call", "post_llm_call", + "pre_api_request", + "post_api_request", "pre_tool_call", "post_tool_call", "subagent_start", diff --git a/crates/sidecar/src/model.rs b/crates/sidecar/src/model.rs index 012305b..d220e16 100644 --- a/crates/sidecar/src/model.rs +++ b/crates/sidecar/src/model.rs @@ -33,6 +33,8 @@ pub(crate) enum NormalizedEvent { SubagentStarted(SubagentEvent), SubagentEnded(SubagentEvent), LlmHint(LlmHintEvent), + LlmStarted(LlmEvent), + LlmEnded(LlmEvent), ToolStarted(ToolEvent), ToolEnded(ToolEvent), #[allow(dead_code)] @@ -54,6 +56,7 @@ impl NormalizedEvent { | Self::Notification(event) | Self::HookMark(event) => &event.session_id, Self::LlmHint(event) => &event.session_id, + Self::LlmStarted(event) | Self::LlmEnded(event) => &event.session_id, Self::SubagentStarted(event) | Self::SubagentEnded(event) => &event.session_id, Self::ToolStarted(event) | Self::ToolEnded(event) => &event.session_id, } @@ -95,6 +98,19 @@ pub(crate) struct LlmHintEvent { pub(crate) metadata: Value, } +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct LlmEvent { + pub(crate) session_id: String, + pub(crate) agent_kind: AgentKind, + pub(crate) event_name: String, + pub(crate) api_call_id: String, + pub(crate) provider: String, + pub(crate) model_name: Option, + pub(crate) request: Value, + pub(crate) response: Value, + pub(crate) metadata: Value, +} + #[derive(Debug, Clone, PartialEq)] pub(crate) struct ToolEvent { pub(crate) session_id: String, diff --git a/crates/sidecar/src/session.rs b/crates/sidecar/src/session.rs index ece5323..2dd58dd 100644 --- a/crates/sidecar/src/session.rs +++ b/crates/sidecar/src/session.rs @@ -27,7 +27,7 @@ use tokio::sync::Mutex; use crate::config::{SessionConfig, SidecarConfig}; use crate::error::SidecarError; use crate::model::{ - AgentKind, LlmHintEvent, NormalizedEvent, SessionEvent, SubagentEvent, ToolEvent, + AgentKind, LlmEvent, LlmHintEvent, NormalizedEvent, SessionEvent, SubagentEvent, ToolEvent, }; const LLM_HINT_TTL: Duration = Duration::from_secs(300); @@ -69,6 +69,7 @@ struct Session { agent_scope: Option, subagents: HashMap, subagent_stack: Vec, + llms: HashMap, tools: HashMap, pending_llm_hints: Vec, pending_tool_hints: Vec, @@ -154,6 +155,7 @@ impl SessionManager { if session.agent_scope.is_none() && session.subagents.is_empty() && session.subagent_stack.is_empty() + && session.llms.is_empty() && session.tools.is_empty() { sessions.remove(&session_id); @@ -230,6 +232,7 @@ impl Session { agent_scope: None, subagents: HashMap::new(), subagent_stack: Vec::new(), + llms: HashMap::new(), tools: HashMap::new(), pending_llm_hints: Vec::new(), pending_tool_hints: Vec::new(), @@ -252,6 +255,8 @@ impl Session { NormalizedEvent::SubagentStarted(event) => self.start_subagent(event), NormalizedEvent::SubagentEnded(event) => self.end_subagent(event), NormalizedEvent::LlmHint(event) => self.add_llm_hint(event), + NormalizedEvent::LlmStarted(event) => self.start_hook_llm(event), + NormalizedEvent::LlmEnded(event) => self.end_hook_llm(event), NormalizedEvent::ToolStarted(event) => self.start_tool(event), NormalizedEvent::ToolEnded(event) => self.end_tool(event), NormalizedEvent::PromptSubmitted(event) => self.mark("prompt_submitted", event), @@ -391,11 +396,12 @@ impl Session { Ok(()) } - // Closes the session in a fail-safe order: active tools first, nested subagents from the top - // down, correlation state, then the root agent scope. Observer flush/export happens after the - // root scope ends so terminal events are included. + // Closes the session in a fail-safe order: active LLMs/tools first, nested subagents from the + // top down, correlation state, then the root agent scope. Observer flush/export happens after + // the root scope ends so terminal events are included. fn end_agent(&mut self, event: SessionEvent) -> Result<(), SidecarError> { self.ensure_agent_started(event.metadata.clone())?; + self.close_active_llms_for_agent_end()?; self.close_active_tools_for_agent_end()?; self.close_active_subagents_for_agent_end()?; self.clear_correlation_state(); @@ -404,6 +410,21 @@ impl Session { Ok(()) } + // Ends all active hook-observed LLM calls before closing their containing scopes. + fn close_active_llms_for_agent_end(&mut self) -> Result<(), SidecarError> { + let active_llms: Vec<_> = self.llms.drain().map(|(_, handle)| handle).collect(); + for handle in active_llms { + llm_call_end( + LlmCallEndParams::builder() + .handle(&handle) + .response(json!({ "status": "closed_by_agent_end" })) + .metadata(json!({ "status": "closed_by_agent_end" })) + .build(), + )?; + } + Ok(()) + } + // Ends all active tool calls with a synthetic close result before ending their containing scopes. // Draining first avoids holding mutable map state while the runtime emits lifecycle events. fn close_active_tools_for_agent_end(&mut self) -> Result<(), SidecarError> { @@ -551,6 +572,56 @@ impl Session { Ok(()) } + // Starts an LLM call from hook activity such as Hermes API request hooks. Duplicate call IDs are + // ignored so repeated pre hooks do not create parallel handles for one provider call. + fn start_hook_llm(&mut self, event: LlmEvent) -> Result<(), SidecarError> { + self.ensure_agent_started(event.metadata.clone())?; + if self.llms.contains_key(&event.api_call_id) { + return Ok(()); + } + let handle = llm_call( + LlmCallParams::builder() + .name(event.provider.as_str()) + .request(&LlmRequest { + headers: Map::new(), + content: event.request, + }) + .attributes(LlmAttributes::empty()) + .metadata(event.metadata) + .model_name_opt(event.model_name) + .build(), + )?; + self.llms.insert(event.api_call_id, handle); + Ok(()) + } + + fn end_hook_llm(&mut self, event: LlmEvent) -> Result<(), SidecarError> { + self.ensure_agent_started(event.metadata.clone())?; + let handle = match self.llms.remove(&event.api_call_id) { + Some(handle) => handle, + None => llm_call( + LlmCallParams::builder() + .name(event.provider.as_str()) + .request(&LlmRequest { + headers: Map::new(), + content: event.request, + }) + .attributes(LlmAttributes::empty()) + .metadata(event.metadata.clone()) + .model_name_opt(event.model_name.clone()) + .build(), + )?, + }; + llm_call_end( + LlmCallEndParams::builder() + .handle(&handle) + .response(event.response) + .metadata(event.metadata) + .build(), + )?; + Ok(()) + } + // Starts a tool call under an explicit subagent when available, otherwise under the agent // scope. Duplicate tool IDs are ignored so repeated pre-tool hooks do not create parallel // handles for one agent tool invocation. @@ -1307,6 +1378,7 @@ fn event_agent_kind(event: &NormalizedEvent) -> AgentKind { NormalizedEvent::SubagentStarted(event) | NormalizedEvent::SubagentEnded(event) => { event.agent_kind } + NormalizedEvent::LlmStarted(event) | NormalizedEvent::LlmEnded(event) => event.agent_kind, NormalizedEvent::ToolStarted(event) | NormalizedEvent::ToolEnded(event) => event.agent_kind, } } diff --git a/crates/sidecar/tests/coverage/adapters_tests.rs b/crates/sidecar/tests/coverage/adapters_tests.rs index 4a5e2ef..2f12128 100644 --- a/crates/sidecar/tests/coverage/adapters_tests.rs +++ b/crates/sidecar/tests/coverage/adapters_tests.rs @@ -272,6 +272,76 @@ fn maps_hermes_real_session_boundary_without_closing_per_turn_end() { )); } +#[test] +fn maps_hermes_api_hooks_to_llm_lifecycle() { + let headers = HeaderMap::new(); + + let started = hermes::adapt( + json!({ + "hook_event_name": "pre_api_request", + "session_id": "hermes-session", + "extra": { + "task_id": "task-1", + "api_call_count": 2, + "model": "qwen", + "provider": "custom", + "base_url": "http://localhost:11434/v1", + "api_mode": "chat_completions", + "message_count": 3, + "tool_count": 1, + "approx_input_tokens": 12, + "request_char_count": 456, + "max_tokens": 1024 + } + }), + &headers, + ); + match &started.events[0] { + NormalizedEvent::LlmStarted(event) => { + assert_eq!(event.session_id, "hermes-session"); + assert_eq!(event.api_call_id, "hermes-session:task-1:2"); + assert_eq!(event.provider, "custom"); + assert_eq!(event.model_name.as_deref(), Some("qwen")); + assert_eq!(event.request["message_count"], json!(3)); + assert_eq!( + event.request["fidelity"]["provider_payload_exact"], + json!(false) + ); + } + event => panic!("unexpected event: {event:?}"), + } + + let ended = hermes::adapt( + json!({ + "hook_event_name": "post_api_request", + "session_id": "hermes-session", + "extra": { + "task_id": "task-1", + "api_call_count": 2, + "model": "qwen", + "response_model": "qwen", + "provider": "custom", + "api_duration": 0.25, + "finish_reason": "stop", + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "prompt_tokens_details": { "cached_tokens": 3 } + } + } + }), + &headers, + ); + match &ended.events[0] { + NormalizedEvent::LlmEnded(event) => { + assert_eq!(event.api_call_id, "hermes-session:task-1:2"); + assert_eq!(event.response["usage"]["prompt_tokens"], json!(10)); + assert_eq!(event.response["usage"]["completion_tokens"], json!(5)); + } + event => panic!("unexpected event: {event:?}"), + } +} + #[test] fn normalizes_mark_style_events_and_header_session_ids() { let mut headers = HeaderMap::new(); diff --git a/crates/sidecar/tests/coverage/installer_tests.rs b/crates/sidecar/tests/coverage/installer_tests.rs index a576265..b46db8b 100644 --- a/crates/sidecar/tests/coverage/installer_tests.rs +++ b/crates/sidecar/tests/coverage/installer_tests.rs @@ -114,6 +114,8 @@ fn generates_hermes_shell_hook_config() { assert!(yaml["hooks"]["pre_llm_call"].is_array()); assert!(yaml["hooks"]["post_llm_call"].is_array()); assert!(yaml["hooks"]["subagent_start"].is_array()); + assert!(yaml["hooks"]["pre_api_request"].is_array()); + assert!(yaml["hooks"]["post_api_request"].is_array()); assert!(yaml["hooks"]["subagent_stop"].is_array()); assert!( yaml["hooks"]["pre_tool_call"][0]["command"] diff --git a/crates/sidecar/tests/coverage/session_tests.rs b/crates/sidecar/tests/coverage/session_tests.rs index 09e0983..c9c688d 100644 --- a/crates/sidecar/tests/coverage/session_tests.rs +++ b/crates/sidecar/tests/coverage/session_tests.rs @@ -5,7 +5,7 @@ use axum::http::HeaderMap; use serde_json::json; use super::*; -use crate::model::{LlmHintEvent, SessionEvent, ToolEvent}; +use crate::model::{LlmEvent, LlmHintEvent, SessionEvent, ToolEvent}; #[tokio::test] async fn nests_agent_subagent_and_tool_lifecycle() { @@ -141,6 +141,86 @@ async fn writes_atif_on_session_end_from_header_config() { assert_eq!(atif["agent"]["name"], json!("codex")); } +#[tokio::test] +async fn writes_hermes_api_hook_usage_to_atif_metrics() { + let temp = tempfile::tempdir().unwrap(); + let config = SidecarConfig { + bind: "127.0.0.1:0".parse().unwrap(), + openai_base_url: "http://127.0.0.1".into(), + anthropic_base_url: "http://127.0.0.1".into(), + atif_dir: None, + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + let mut headers = HeaderMap::new(); + headers.insert( + "x-nemo-flow-atif-dir", + temp.path().to_string_lossy().parse().unwrap(), + ); + + manager + .apply_events( + &headers, + vec![ + NormalizedEvent::AgentStarted(SessionEvent { + session_id: "hermes-usage".into(), + agent_kind: AgentKind::Hermes, + event_name: "on_session_start".into(), + payload: json!({}), + metadata: json!({}), + }), + NormalizedEvent::LlmStarted(LlmEvent { + session_id: "hermes-usage".into(), + agent_kind: AgentKind::Hermes, + event_name: "pre_api_request".into(), + api_call_id: "hermes-usage:task-1:1".into(), + provider: "custom".into(), + model_name: Some("qwen".into()), + request: json!({ "model": "qwen" }), + response: Value::Null, + metadata: json!({}), + }), + NormalizedEvent::LlmEnded(LlmEvent { + session_id: "hermes-usage".into(), + agent_kind: AgentKind::Hermes, + event_name: "post_api_request".into(), + api_call_id: "hermes-usage:task-1:1".into(), + provider: "custom".into(), + model_name: Some("qwen".into()), + request: json!({}), + response: json!({ + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "prompt_tokens_details": { "cached_tokens": 3 } + } + }), + metadata: json!({}), + }), + NormalizedEvent::AgentEnded(SessionEvent { + session_id: "hermes-usage".into(), + agent_kind: AgentKind::Hermes, + event_name: "on_session_finalize".into(), + payload: json!({}), + metadata: json!({}), + }), + ], + ) + .await + .unwrap(); + + let path = temp.path().join("hermes-usage.atif.json"); + let atif: Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); + assert_eq!(atif["steps"][1]["metrics"]["prompt_tokens"], json!(10)); + assert_eq!(atif["steps"][1]["metrics"]["completion_tokens"], json!(5)); + assert_eq!(atif["steps"][1]["metrics"]["cached_tokens"], json!(3)); + assert_eq!(atif["final_metrics"]["total_prompt_tokens"], json!(10)); + assert_eq!(atif["final_metrics"]["total_completion_tokens"], json!(5)); + assert_eq!(atif["final_metrics"]["total_cached_tokens"], json!(3)); +} + #[tokio::test] async fn handles_out_of_order_subagent_and_tool_end_events() { let config = SidecarConfig { From 1d896bf806e78a0e7b57b106e4088adf8782b16c Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 6 May 2026 19:13:51 -0400 Subject: [PATCH 10/13] ci: add sidecar binary publishing Signed-off-by: Will Killian --- .github/workflows/ci_rust.yml | 73 +++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/.github/workflows/ci_rust.yml b/.github/workflows/ci_rust.yml index 541c781..928480a 100644 --- a/.github/workflows/ci_rust.yml +++ b/.github/workflows/ci_rust.yml @@ -146,3 +146,76 @@ jobs: flags: rust-${{ matrix.platform }} name: rust-${{ matrix.platform }} verbose: true + + Package: + name: Package (${{ matrix.platform }}) + needs: [Test] + if: ${{ !cancelled() && needs.Test.result == 'success' }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - platform: linux-amd64 + runner: ubuntu-latest + - platform: linux-arm64 + runner: ubuntu-24.04-arm + - platform: macos-arm64 + runner: macos-15 + - platform: windows-amd64 + runner: windows-2022 + - platform: windows-arm64 + runner: windows-11-arm + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Load CI tool versions + id: ci-config + uses: ./.github/actions/load-ci-tool-versions + + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + with: + cache: false + toolchain: ${{ steps.ci-config.outputs.rust_version }} + + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + shared-key: nemo-flow-rust-${{ runner.os }}-${{ runner.arch }}-${{ steps.ci-config.outputs.rust_version }} + workspaces: . -> target + cache-all-crates: true + cache-bin: false + save-if: false + + - name: Build sidecar release binary + working-directory: ${{ env.NEMO_FLOW_CI_WORKSPACE }} + run: | + set -e + cargo build --release -p nemo-flow-sidecar + + - name: Stage sidecar binary artifact + working-directory: ${{ env.NEMO_FLOW_CI_WORKSPACE }} + run: | + set -euo pipefail + binary="nemo-flow-sidecar" + if [ "${{ runner.os }}" = "Windows" ]; then + binary="${binary}.exe" + fi + source="${NEMO_FLOW_CI_WORKSPACE}/target/release/${binary}" + if [ ! -f "$source" ]; then + echo "Error: expected sidecar binary at ${source}" >&2 + exit 1 + fi + mkdir -p "${NEMO_FLOW_CI_WORKSPACE_TMP}/sidecar" + cp "$source" "${NEMO_FLOW_CI_WORKSPACE_TMP}/sidecar/${binary}" + + - name: Upload sidecar binary artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sidecar-${{ matrix.platform }} + path: ${{ env.NEMO_FLOW_CI_WORKSPACE_TMP }}/sidecar/* + if-no-files-found: error From 23315c09ff52dd7c19174fa0a542b65c4cbc7091 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 6 May 2026 19:29:24 -0400 Subject: [PATCH 11/13] test: make sidecar launcher test portable Signed-off-by: Will Killian --- .../sidecar/tests/coverage/launcher_tests.rs | 42 ++++++++++++++----- docs/reference/api/rust/index.md | 1 - 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/crates/sidecar/tests/coverage/launcher_tests.rs b/crates/sidecar/tests/coverage/launcher_tests.rs index 7a4afd7..4e38f78 100644 --- a/crates/sidecar/tests/coverage/launcher_tests.rs +++ b/crates/sidecar/tests/coverage/launcher_tests.rs @@ -484,17 +484,8 @@ fn cursor_dry_run_does_not_write_hooks() { #[tokio::test] async fn run_starts_sidecar_injects_env_and_returns_agent_exit_code() { let temp = tempfile::tempdir().unwrap(); - let script = temp.path().join("fake-agent.sh"); let output = temp.path().join("env.txt"); - std::fs::write( - &script, - format!( - "#!/bin/sh\nprintf '%s' \"$NEMO_FLOW_SIDECAR_URL\" > {}\nexit 7\n", - output.display() - ), - ) - .unwrap(); - make_executable(&script); + let command_argv = fake_agent_command(temp.path(), &output); let command = RunCommand { agent: Some(CodingAgent::Codex), config: None, @@ -506,7 +497,7 @@ async fn run_starts_sidecar_injects_env_and_returns_agent_exit_code() { plugin_config: None, dry_run: false, print: false, - command: vec![script.display().to_string()], + command: command_argv, }; let code = run(command, None).await.unwrap(); @@ -517,6 +508,35 @@ async fn run_starts_sidecar_injects_env_and_returns_agent_exit_code() { assert!(!url.ends_with(":0")); } +#[cfg(unix)] +fn fake_agent_command(temp: &Path, output: &Path) -> Vec { + let script = temp.join("fake-agent.sh"); + std::fs::write( + &script, + format!( + "#!/bin/sh\nprintf '%s' \"$NEMO_FLOW_SIDECAR_URL\" > \"{}\"\nexit 7\n", + output.display() + ), + ) + .unwrap(); + make_executable(&script); + vec![script.display().to_string()] +} + +#[cfg(windows)] +fn fake_agent_command(temp: &Path, output: &Path) -> Vec { + let script = temp.join("fake-agent.cmd"); + std::fs::write( + &script, + format!( + "@echo off\r\n \"{}\"\r\nexit /b 7\r\n", + output.display() + ), + ) + .unwrap(); + vec!["cmd.exe".into(), "/C".into(), script.display().to_string()] +} + #[tokio::test] async fn dry_run_does_not_spawn_agent() { let command = RunCommand { diff --git a/docs/reference/api/rust/index.md b/docs/reference/api/rust/index.md index 5b97b1e..e000dbc 100644 --- a/docs/reference/api/rust/index.md +++ b/docs/reference/api/rust/index.md @@ -61,7 +61,6 @@ Use the generated crate entry points when you need symbol-level detail: nemo-flow <_generated/nemo-flow/src> nemo-flow-adaptive <_generated/nemo-flow-adaptive/src> -nemo-flow-adaptive <_generated/nemo-flow-sidecar/src> ``` ## Related Guides From 220196236e6ad24d2d72b802f2684fa639b51b3a Mon Sep 17 00:00:00 2001 From: Will Killian Date: Wed, 6 May 2026 20:27:49 -0400 Subject: [PATCH 12/13] ci(windows): get build to pass Signed-off-by: Will Killian --- crates/sidecar/tests/coverage/launcher_tests.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/sidecar/tests/coverage/launcher_tests.rs b/crates/sidecar/tests/coverage/launcher_tests.rs index 4e38f78..faf0f2b 100644 --- a/crates/sidecar/tests/coverage/launcher_tests.rs +++ b/crates/sidecar/tests/coverage/launcher_tests.rs @@ -575,6 +575,3 @@ fn make_executable(path: &Path) { permissions.set_mode(0o755); std::fs::set_permissions(path, permissions).unwrap(); } - -#[cfg(not(unix))] -fn make_executable(_path: &Path) {} From 78798859373f2e307a29d12cc4fc4fa6beff9c1f Mon Sep 17 00:00:00 2001 From: Bryan Bednarski Date: Thu, 7 May 2026 13:08:32 -0700 Subject: [PATCH 13/13] feat(python): support response annotations on manual LLM span end Signed-off-by: Bryan Bednarski --- crates/python/src/py_api/mod.rs | 109 ++++++++++++------ .../tests/coverage/py_api_coverage_tests.rs | 2 + integrations/coding-agents/README.md | 2 + python/nemo_flow/_native.pyi | 8 ++ python/nemo_flow/llm.py | 37 +++++- python/tests/test_builtin_codecs.py | 83 +++++++++++++ 6 files changed, 200 insertions(+), 41 deletions(-) diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index cadb851..c044b1d 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -25,6 +25,7 @@ use nemo_flow::api::scope::ScopeAttributes; use nemo_flow::api::subscriber as core_subscriber_api; use nemo_flow::api::tool as core_tool_api; use nemo_flow::api::tool::ToolAttributes; +use nemo_flow::codec::response::AnnotatedLlmResponse; use nemo_flow::codec::traits::{LlmCodec, LlmResponseCodec}; use nemo_flow::error::{FlowError, Result as FlowResult}; use pyo3::prelude::*; @@ -34,9 +35,9 @@ use uuid::Uuid; use crate::convert::{json_to_py, opt_py_to_json, opt_py_to_timestamp, py_to_json}; use crate::py_callable; use crate::py_types::{ - PyAnthropicMessagesCodec, PyLLMAttributes, PyLLMHandle, PyLLMRequest, PyLlmStream, - PyOpenAIChatCodec, PyOpenAIResponsesCodec, PyScopeAttributes, PyScopeHandle, PyScopeStack, - PyScopeType, PyToolAttributes, PyToolHandle, + PyAnnotatedLLMResponse, PyAnthropicMessagesCodec, PyLLMAttributes, PyLLMHandle, PyLLMRequest, + PyLlmStream, PyOpenAIChatCodec, PyOpenAIResponsesCodec, PyScopeAttributes, PyScopeHandle, + PyScopeStack, PyScopeType, PyToolAttributes, PyToolHandle, }; pub(crate) type RustJsonStream = @@ -47,6 +48,55 @@ fn to_py_err(e: FlowError) -> PyErr { PyErr::new::(e.to_string()) } +fn py_llm_response_codec( + response_codec: Option<&Bound<'_, PyAny>>, +) -> Option> { + response_codec.and_then(|c| -> Option> { + if c.is_none() { + return None; + } + // Try to extract as a built-in codec first (avoids Python method dispatch overhead) + if let Ok(builtin) = c.extract::>() { + return Some(builtin.inner_response_codec.clone()); + } + if let Ok(builtin) = c.extract::>() { + return Some(builtin.inner_response_codec.clone()); + } + if let Ok(builtin) = c.extract::>() { + return Some(builtin.inner_response_codec.clone()); + } + // Fall back to wrapping the Python object as a custom response codec + Some(Arc::new(py_callable::PyLlmResponseCodecWrapper { + py_codec: c.clone().unbind(), + })) + }) +} + +fn py_annotated_llm_response( + annotated_response: Option<&Bound<'_, PyAny>>, +) -> PyResult>> { + let Some(annotated_response) = annotated_response else { + return Ok(None); + }; + if annotated_response.is_none() { + return Ok(None); + } + + if let Ok(response) = annotated_response.cast::() { + let response = response.borrow(); + return Ok(Some(Arc::new(response.inner.clone()))); + } + + let value = py_to_json(annotated_response)?; + serde_json::from_value::(value) + .map(|response| Some(Arc::new(response))) + .map_err(|error| { + PyErr::new::(format!( + "invalid annotated_response: {error}" + )) + }) +} + pub(crate) async fn forward_stream_to_channel( mut stream: RustJsonStream, tx: tokio::sync::mpsc::Sender>, @@ -558,6 +608,10 @@ fn llm_call( /// sanitize-response guardrails unless it sanitizes to JSON null. /// data: Optional JSON-serializable payload used when the sanitized response is JSON null. /// metadata: Optional JSON-serializable metadata recorded on the end event. +/// annotated_response: Optional normalized response annotation, either as an +/// ``AnnotatedLLMResponse`` instance or a JSON object matching that schema. +/// response_codec: Optional response codec used to decode ``response`` into +/// an annotated response for observability when ``annotated_response`` is omitted. /// timestamp: Optional timezone-aware ``datetime.datetime`` for the emitted end event. /// When omitted, the runtime default end timestamp is used. /// @@ -571,18 +625,30 @@ fn llm_call( *, data: "object | None"=None, metadata: "object | None"=None, + annotated_response: "AnnotatedLLMResponse | object | None"=None, + response_codec: "object | None"=None, timestamp: "datetime.datetime | None"=None -) -> "None", text_signature = "(handle: LlmHandle, response: object, *, data: object | None = None, metadata: object | None = None, timestamp: datetime.datetime | None = None) -> None")] +) -> "None", text_signature = "(handle: LlmHandle, response: object, *, data: object | None = None, metadata: object | None = None, annotated_response: AnnotatedLLMResponse | object | None = None, response_codec: object | None = None, timestamp: datetime.datetime | None = None) -> None")] fn llm_call_end( handle: &PyLLMHandle, response: &Bound<'_, PyAny>, data: Option<&Bound<'_, PyAny>>, metadata: Option<&Bound<'_, PyAny>>, + annotated_response: Option<&Bound<'_, PyAny>>, + response_codec: Option<&Bound<'_, PyAny>>, timestamp: Option<&Bound<'_, PyAny>>, ) -> PyResult<()> { let response_json = py_to_json(response)?; let data = opt_py_to_json(data)?; let metadata = opt_py_to_json(metadata)?; + let response_codec = py_llm_response_codec(response_codec); + let annotated_response = match py_annotated_llm_response(annotated_response)? { + Some(annotated_response) => Some(annotated_response), + None => response_codec + .as_ref() + .and_then(|codec| codec.decode_response(&response_json).ok()) + .map(Arc::new), + }; let timestamp = opt_py_to_timestamp(timestamp)?; core_llm_api::llm_call_end( core_llm_api::LlmCallEndParams::builder() @@ -590,6 +656,7 @@ fn llm_call_end( .response(response_json) .data_opt(data) .metadata_opt(metadata) + .annotated_response_opt(annotated_response) .timestamp_opt(timestamp) .build(), ) @@ -663,23 +730,7 @@ fn llm_call_execute<'py>( py_codec: c.clone().unbind(), }) as Arc }); - let response_codec_arc: Option> = - response_codec.map(|c| -> Arc { - // Try to extract as a built-in codec first (avoids Python method dispatch overhead) - if let Ok(builtin) = c.extract::>() { - return builtin.inner_response_codec.clone(); - } - if let Ok(builtin) = c.extract::>() { - return builtin.inner_response_codec.clone(); - } - if let Ok(builtin) = c.extract::>() { - return builtin.inner_response_codec.clone(); - } - // Fall back to wrapping the Python object as a custom response codec - Arc::new(py_callable::PyLlmResponseCodecWrapper { - py_codec: c.clone().unbind(), - }) - }); + let response_codec_arc = py_llm_response_codec(response_codec); let scope_stack = current_scope_stack_handle(); pyo3_async_runtimes::tokio::future_into_py(py, async move { @@ -780,21 +831,7 @@ fn llm_stream_call_execute<'py>( py_codec: c.clone().unbind(), }) as Arc }); - let response_codec_arc: Option> = - response_codec.map(|c| -> Arc { - if let Ok(builtin) = c.extract::>() { - return builtin.inner_response_codec.clone(); - } - if let Ok(builtin) = c.extract::>() { - return builtin.inner_response_codec.clone(); - } - if let Ok(builtin) = c.extract::>() { - return builtin.inner_response_codec.clone(); - } - Arc::new(py_callable::PyLlmResponseCodecWrapper { - py_codec: c.clone().unbind(), - }) - }); + let response_codec_arc = py_llm_response_codec(response_codec); let scope_stack = current_scope_stack_handle(); pyo3_async_runtimes::tokio::future_into_py(py, async move { diff --git a/crates/python/tests/coverage/py_api_coverage_tests.rs b/crates/python/tests/coverage/py_api_coverage_tests.rs index c43db52..828e5dc 100644 --- a/crates/python/tests/coverage/py_api_coverage_tests.rs +++ b/crates/python/tests/coverage/py_api_coverage_tests.rs @@ -129,6 +129,8 @@ fn py_api_helpers_and_scope_lifecycle_round_trip() { Some(&py_dict(py, json!({"tokens": 10}))), Some(&py_dict(py, json!({"finish_reason": "stop"}))), None, + None, + None, ) .unwrap(); diff --git a/integrations/coding-agents/README.md b/integrations/coding-agents/README.md index b83c165..c2d8e31 100644 --- a/integrations/coding-agents/README.md +++ b/integrations/coding-agents/README.md @@ -31,6 +31,8 @@ environment variables, or shared TOML config. - Hermes does not require a static bundle in this directory. Use `nemo-flow-sidecar install hermes` to merge hook commands into `.hermes/config.yaml`. +- `hermes/` contains a native Hermes Python plugin prototype that writes ATIF + from Hermes plugin middleware without running the sidecar HTTP process. ## Transparent Setup diff --git a/python/nemo_flow/_native.pyi b/python/nemo_flow/_native.pyi index 03cacbb..4a9cd4a 100644 --- a/python/nemo_flow/_native.pyi +++ b/python/nemo_flow/_native.pyi @@ -1284,6 +1284,8 @@ def llm_call_end( *, data: _Json | None = None, metadata: _Json | None = None, + annotated_response: AnnotatedLLMResponse | Mapping[str, _JsonValue] | None = None, + response_codec: object | None = None, timestamp: datetime | None = None, ) -> None: """End a manual LLM lifecycle span. @@ -1294,6 +1296,12 @@ def llm_call_end( sanitize-response guardrails unless it sanitizes to JSON null. data: Optional JSON payload used when the sanitized response is JSON null. metadata: Optional JSON metadata recorded on the end event. + annotated_response: Optional normalized response annotation attached to + the end event. Accepts an ``AnnotatedLLMResponse`` instance or a + JSON-compatible mapping matching that schema. + response_codec: Optional object implementing ``decode_response`` used + to derive ``annotated_response`` from ``response`` for observability + when ``annotated_response`` is omitted. timestamp: Optional timezone-aware datetime recorded on the end event. When omitted, the runtime default end timestamp is used. diff --git a/python/nemo_flow/llm.py b/python/nemo_flow/llm.py index cd39dc9..9eec59f 100644 --- a/python/nemo_flow/llm.py +++ b/python/nemo_flow/llm.py @@ -29,6 +29,7 @@ async def impl(req): from __future__ import annotations +from collections.abc import Mapping from datetime import datetime from typing import TYPE_CHECKING @@ -56,6 +57,8 @@ async def impl(req): ) if TYPE_CHECKING: + from nemo_flow import Json + from nemo_flow._native import AnnotatedLLMResponse from nemo_flow.codecs import LlmCodec, LlmResponseCodec @@ -128,7 +131,16 @@ def call( ) -def call_end(handle, response, *, data=None, metadata=None, timestamp: datetime | None = None) -> None: +def call_end( + handle, + response, + *, + data=None, + metadata=None, + annotated_response: AnnotatedLLMResponse | Mapping[str, Json] | None = None, + response_codec: LlmResponseCodec | None = None, + timestamp: datetime | None = None, +) -> None: """Finish a manual LLM span started by ``call()``. Args: @@ -136,6 +148,12 @@ def call_end(handle, response, *, data=None, metadata=None, timestamp: datetime response: Raw JSON-compatible response to record on the end event. data: Optional JSON payload used when the sanitized ``response`` is JSON null. metadata: Optional JSON metadata recorded on the emitted end event. + annotated_response: Optional normalized response annotation attached to + the emitted end event. Accepts an ``AnnotatedLLMResponse`` returned + by a codec, or a JSON-compatible mapping matching that schema. + response_codec: Optional response codec used to derive + ``annotated_response`` from ``response`` for observability. Ignored + when ``annotated_response`` is provided. timestamp: Optional timezone-aware ``datetime`` recorded on the emitted end event. When omitted, the runtime default end timestamp is used. @@ -144,11 +162,20 @@ def call_end(handle, response, *, data=None, metadata=None, timestamp: datetime Notes: ``call_end()`` applies sanitize-response guardrails to the emitted - end-event payload but does not normalize or decode the response - automatically. ``timestamp`` must be a timezone-aware ``datetime``; - strings and naive datetimes are rejected. + end-event payload. ``response_codec`` and ``annotated_response`` enrich + observability output only and do not rewrite the recorded response. + ``timestamp`` must be a timezone-aware ``datetime``; strings and naive + datetimes are rejected. """ - return _native_llm_call_end(handle, response, data=data, metadata=metadata, timestamp=timestamp) + return _native_llm_call_end( + handle, + response, + data=data, + metadata=metadata, + annotated_response=annotated_response, + response_codec=response_codec, + timestamp=timestamp, + ) def execute( diff --git a/python/tests/test_builtin_codecs.py b/python/tests/test_builtin_codecs.py index 246ffdb..34df7ec 100644 --- a/python/tests/test_builtin_codecs.py +++ b/python/tests/test_builtin_codecs.py @@ -185,6 +185,89 @@ def test_builtin_codecs_satisfy_protocol(self): class TestResponseCodecObjectParam: + def test_manual_call_end_response_codec_attaches_annotation(self): + """manual llm.call_end() accepts response_codec for end-event annotations.""" + captured_events = [] + + def capture(event): + captured_events.append(event) + + subscribers.register("test-manual-call-end-response-codec", capture) + + try: + handle = llm.call( + "manual-codec-llm", + LLMRequest( + {}, + {"model": "gpt-4", "messages": [{"role": "user", "content": "hi"}]}, + ), + ) + llm.call_end( + handle, + { + "id": "chatcmpl-manual", + "model": "gpt-4", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Hello!"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 7, "completion_tokens": 4, "total_tokens": 11}, + }, + response_codec=OpenAIChatCodec(), + ) + + end_events = [ + e for e in captured_events if e.kind == "scope" and e.category == "llm" and e.scope_category == "end" + ] + assert len(end_events) == 1 + + annotated = end_events[0].annotated_response + assert annotated is not None + assert annotated.usage == {"prompt_tokens": 7, "completion_tokens": 4, "total_tokens": 11} + assert annotated.response_text() == "Hello!" + + finally: + subscribers.deregister("test-manual-call-end-response-codec") + + def test_manual_call_end_accepts_annotated_response_mapping(self): + """manual llm.call_end() accepts an explicit JSON annotation mapping.""" + captured_events = [] + + def capture(event): + captured_events.append(event) + + subscribers.register("test-manual-call-end-annotated-response", capture) + + try: + handle = llm.call( + "manual-annotated-llm", + LLMRequest({}, {"model": "gpt-4", "messages": []}), + ) + llm.call_end( + handle, + {"status": "ok"}, + annotated_response={ + "model": "gpt-4", + "usage": {"prompt_tokens": 3, "completion_tokens": 2, "total_tokens": 5}, + }, + ) + + end_events = [ + e for e in captured_events if e.kind == "scope" and e.category == "llm" and e.scope_category == "end" + ] + assert len(end_events) == 1 + + annotated = end_events[0].annotated_response + assert annotated is not None + assert annotated.model == "gpt-4" + assert annotated.usage == {"prompt_tokens": 3, "completion_tokens": 2, "total_tokens": 5} + + finally: + subscribers.deregister("test-manual-call-end-annotated-response") + async def test_response_codec_accepts_builtin_object(self): """response_codec= accepts a built-in codec object, not a string.""" captured_events = []