From 5f35146b979e3c506c7a6d2fad55556b420a0210 Mon Sep 17 00:00:00 2001 From: Will Killian Date: Tue, 5 May 2026 15:51:01 -0400 Subject: [PATCH 01/27] 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 2d0b6141..ca0023c7 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 fec08ebf..0fd06054 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 1cb96b23..0c79b57e 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 00000000..096027a5 --- /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 00000000..6edc6c3f --- /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 00000000..73846c8e --- /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 00000000..d7fb5695 --- /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 00000000..210cdf08 --- /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 00000000..620d1aa0 --- /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 00000000..4cdfb535 --- /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 00000000..7b84e9ae --- /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 00000000..95fa6655 --- /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 00000000..ec5512fc --- /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 00000000..5f535011 --- /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 00000000..62a92a2b --- /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 00000000..1bc8a085 --- /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 9048ad9d..aa33c73b 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 a12a1b77..fc4e3d27 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 00000000..d12c6637 --- /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 00000000..670c9f6a --- /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 00000000..25e87316 --- /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 00000000..cc481b88 --- /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 00000000..b4211f0e --- /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 00000000..3baf0162 --- /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 00000000..5469e725 --- /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 00000000..873df11b --- /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 00000000..556e50ab --- /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 00000000..6d98c881 --- /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 00000000..a2541a72 --- /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 00000000..c44b2d97 --- /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 00000000..0484a7e4 --- /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/27] 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 d2691d1b..15de8670 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 0c79b57e..b8c0708a 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 9682f6de..b50bdcf9 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 1b3cd16e..a652996e 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 eac059fc..2d3b480b 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 ced57a88..855bdd7e 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 65402f8d..a675dc97 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 c30b9411..57004197 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/27] 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 210cdf08..dc015f3b 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 620d1aa0..47136462 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 7b84e9ae..24b92b6b 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 95fa6655..f21caab5 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 62a92a2b..36fe36f7 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 1bc8a085..96237fc3 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/27] 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 ca0023c7..2aa3bcd4 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 0fd06054..7070a488 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 29581c4c..f044574c 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 096027a5..00209072 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 6edc6c3f..c5d6bd96 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 dc015f3b..4511c8df 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 47136462..af2b74ea 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 4cdfb535..4d1451f1 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 24b92b6b..bec17ab3 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 f21caab5..1dd6f297 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 00000000..01ce0f4f --- /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 ec5512fc..a16b8ea4 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 36fe36f7..89b9197e 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 96237fc3..6293a3b3 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 00000000..970a7747 --- /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 00000000..5df33e92 --- /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 00000000..7d3102d2 --- /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 00000000..f2c340b9 --- /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 00000000..d47d4400 --- /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 00000000..495e34b2 --- /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 00000000..828b81a6 --- /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 fc4e3d27..d88efed8 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 d12c6637..63c16a39 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 670c9f6a..17be6695 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 25e87316..41f88314 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 cc481b88..46235ca8 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 b4211f0e..f5580822 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 5469e725..5f1f14f4 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 6d98c881..dfec71fd 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 0484a7e4..27205e38 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/27] 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 d99f8160..016fe0ad 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 1d3d2a13..8c549558 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 c5d6bd96..30346dac 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 73846c8e..ba982192 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 4511c8df..055f208b 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 bec17ab3..f2a4115a 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 1dd6f297..dfa15fb9 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 01ce0f4f..6c0892f6 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 6293a3b3..cb208330 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 970a7747..841ab650 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 7d3102d2..8106b7f9 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 d47d4400..4afbeb0a 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 828b81a6..9e0656b8 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 63c16a39..8995b74a 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 5f1f14f4..39ba86f5 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/27] 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 7070a488..61cb840f 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 46033fbe..9f2ca0d8 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 795714e7..a4e9d686 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 00209072..1c6a5665 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 00000000..59b4d46b --- /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 055f208b..423c63ca 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 af2b74ea..42dcb87f 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 dfa15fb9..c60161e2 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 6c0892f6..1d589430 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 5f535011..708a03be 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 89b9197e..5a120282 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 841ab650..a76401b9 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 5df33e92..ba4288bb 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 f2c340b9..d5cc0f75 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 4afbeb0a..f50d94ce 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 495e34b2..024940ef 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/27] 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 59b4d46b..7b94edab 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 c60161e2..1fd0a10f 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 708a03be..7095a40a 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 cb208330..8ce51459 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 a76401b9..276486dd 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 d5cc0f75..b83bfd6d 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 9e0656b8..2bbb7fd8 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/27] 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 2aa3bcd4..5e7d69f1 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 f044574c..f5a0b8e8 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 30346dac..95a079bf 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 ba982192..14a0bc58 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 d7fb5695..337a5cfa 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 59b4d46b..057eeb74 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 423c63ca..33fd83bc 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 42dcb87f..3fb71216 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 4d1451f1..b591ae53 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 f2a4115a..7f5e7b16 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 c60161e2..35de3798 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 1d589430..62c6af24 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 a16b8ea4..75a97b2e 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 708a03be..012305bf 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 5a120282..fb46967d 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 cb208330..ece5323d 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 00000000..eb0bbad6 --- /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 a76401b9..4a5e2efe 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 ba4288bb..10bea3eb 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 8106b7f9..d0001c16 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 d5cc0f75..a576265f 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 f50d94ce..7a4afd76 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 024940ef..2a63028d 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 9e0656b8..09e09837 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 aa33c73b..11453a57 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 d88efed8..385282ae 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 8995b74a..49506cb3 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 17be6695..419d3a7f 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 41f88314..eac98752 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 00000000..150e053d --- /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 46235ca8..28a652db 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 a675dc97..5b97b1ee 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 f5580822..b83c1658 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 39ba86f5..3ef99376 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 873df11b..82ac68e4 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 dfec71fd..ce2db35c 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 a2541a72..d7355374 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 c44b2d97..bc5ff091 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 27205e38..d15c3c87 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/27] 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 057eeb74..4fcf2122 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 35de3798..96950767 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 012305bf..d220e160 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 ece5323d..2dd58ddf 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 4a5e2efe..2f121283 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 a576265f..b46db8b9 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 09e09837..c9c688d0 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/27] 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 541c7818..928480a4 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/27] 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 7a4afd76..4e38f784 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 5b97b1ee..e000dbc6 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/27] 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 4e38f784..faf0f2be 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/27] 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 cadb8513..c044b1df 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 c43db52b..828e5dc3 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 b83c1658..c2d8e318 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 03cacbb0..4a9cd4aa 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 cd39dc9d..9eec59fd 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 246ffdbf..34df7ecf 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 = [] From 57421644bbfe311190aea7873162b6e19cd7b86a Mon Sep 17 00:00:00 2001 From: Will Killian Date: Fri, 8 May 2026 15:59:28 -0400 Subject: [PATCH 14/27] fix: address sidecar and codec review findings Signed-off-by: Will Killian --- .github/ci-path-filters.yml | 1 - codecov.yml | 1 - crates/core/src/api/llm.rs | 53 ++++++-- crates/core/src/codec/traits.rs | 8 +- crates/python/src/py_api/mod.rs | 9 +- crates/sidecar/src/error.rs | 5 +- crates/sidecar/src/gateway.rs | 4 +- crates/sidecar/src/installer.rs | 5 +- crates/sidecar/src/model.rs | 7 + crates/sidecar/src/server.rs | 14 +- crates/sidecar/src/session.rs | 49 ++++++- .../sidecar/tests/coverage/gateway_tests.rs | 53 +++++++- crates/sidecar/tests/coverage/server_tests.rs | 53 ++++++-- .../sidecar/tests/coverage/session_tests.rs | 121 ++++++++++++++++++ .../coding-agents/codex/hooks/hooks.json | 1 + .../coding-agents/cursor/.cursor/hooks.json | 1 + python/nemo_flow/llm.py | 6 +- python/tests/test_builtin_codecs.py | 73 +++++++++++ 18 files changed, 413 insertions(+), 51 deletions(-) diff --git a/.github/ci-path-filters.yml b/.github/ci-path-filters.yml index 3897b715..3a1c7590 100644 --- a/.github/ci-path-filters.yml +++ b/.github/ci-path-filters.yml @@ -12,7 +12,6 @@ shared: - 'crates/adaptive/src/**' - 'crates/core/Cargo.toml' - 'crates/core/src/**' - - 'integrations/coding-agents/**' - 'justfile' - 'rust-toolchain.toml' diff --git a/codecov.yml b/codecov.yml index f5a0b8e8..edbd67e5 100644 --- a/codecov.yml +++ b/codecov.yml @@ -48,7 +48,6 @@ component_management: paths: - "crates/core/src" - "crates/adaptive/src" - - "crates/sidecar/src" statuses: - type: project target: 95% diff --git a/crates/core/src/api/llm.rs b/crates/core/src/api/llm.rs index a4e65cb5..e95242b6 100644 --- a/crates/core/src/api/llm.rs +++ b/crates/core/src/api/llm.rs @@ -250,6 +250,9 @@ pub struct LlmCallEndParams<'a> { /// Optional normalized response annotation produced by a response codec. #[builder(default)] pub annotated_response: Option>, + /// Optional response codec used to produce an annotation from sanitized event data. + #[builder(default)] + pub response_codec: Option>, /// Optional timestamp recorded on the emitted end event. When omitted, the /// runtime records the current UTC time, or one microsecond after the /// handle start time if the current time is not later. @@ -364,7 +367,10 @@ pub fn llm_call(params: LlmCallParams<'_>) -> Result { /// JSON null, in which case this payload is used. /// - `metadata`: Optional JSON metadata recorded on the end event. /// - `annotated_response`: Optional normalized response annotation produced by -/// a response codec. +/// a response codec. When omitted and `response_codec` is supplied, the +/// annotation is decoded from the sanitized end-event payload. +/// - `response_codec`: Optional response codec used to produce a normalized +/// response annotation from the sanitized end-event payload. /// - `timestamp`: Optional timestamp recorded on the emitted end event. When /// `None`, the runtime uses the current UTC time, or one microsecond after /// the handle start time if the current time is not later. @@ -373,14 +379,24 @@ pub fn llm_call(params: LlmCallParams<'_>) -> Result { /// A [`Result`] that is `Ok(())` when the end event has been emitted. /// /// # Errors -/// Returns an error when the runtime owner check fails or when internal state -/// cannot be read safely. +/// Returns an error when the runtime owner check fails, internal state cannot be +/// read safely, or response codec decoding fails. /// /// # Notes /// Sanitize-response guardrails affect only the emitted end-event payload, not /// the caller-owned `response` value. pub fn llm_call_end(params: LlmCallEndParams<'_>) -> Result<()> { + let LlmCallEndParams { + handle, + response, + data, + metadata, + annotated_response, + response_codec, + timestamp, + } = params; ensure_runtime_owner()?; + let mut decode_error = None; let (event, subscribers) = { let scope_stack = current_scope_stack(); let scope_guard = scope_stack.read().expect("scope stack lock poisoned"); @@ -394,25 +410,42 @@ pub fn llm_call_end(params: LlmCallEndParams<'_>) -> Result<()> { .read() .map_err(|error| FlowError::Internal(error.to_string()))?; - let sanitized_response = state.llm_sanitize_response_chain(params.response, &scope_locals); + let sanitized_response = state.llm_sanitize_response_chain(response, &scope_locals); let data = if sanitized_response.is_null() { - params.data + data } else { Some(sanitized_response) }; + let annotated_response = match annotated_response { + Some(annotated_response) => Some(annotated_response), + None => match (response_codec.as_ref(), data.as_ref()) { + (Some(codec), Some(response)) => match codec.decode_response(response) { + Ok(decoded) => Some(Arc::new(decoded)), + Err(error) => { + decode_error = Some(error); + None + } + }, + _ => None, + }, + }; let event = state.build_llm_end_event( EndLlmHandleParams::builder() - .handle(params.handle) + .handle(handle) .data_opt(data) - .metadata_opt(params.metadata) - .annotated_response_opt(params.annotated_response) - .timestamp_opt(params.timestamp) + .metadata_opt(metadata) + .annotated_response_opt(annotated_response) + .timestamp_opt(timestamp) .build(), ); (event, subscribers) }; NemoFlowContextState::emit_event(&event, &subscribers); - Ok(()) + if let Some(error) = decode_error { + Err(error) + } else { + Ok(()) + } } fn emit_llm_end_without_output(handle: &LlmHandle, metadata: Option) -> Result<()> { diff --git a/crates/core/src/codec/traits.rs b/crates/core/src/codec/traits.rs index 608d5208..a4236409 100644 --- a/crates/core/src/codec/traits.rs +++ b/crates/core/src/codec/traits.rs @@ -62,8 +62,8 @@ pub trait LlmCodec: Send + Sync { /// - **`Send + Sync`**: Required for storage in `Arc` behind `RwLock`. /// - **Trait object**: Codecs are registered at runtime, stored as /// `Arc`. -/// - **Non-fatal**: Returns `Result` but callers treat errors as -/// "no annotation available" rather than pipeline failure. +/// - **Fallible**: Returns `Result`; managed call sites may omit annotations on +/// decode failure, while manual lifecycle bindings may surface the error. /// /// # Two-Phase Decode /// @@ -73,8 +73,6 @@ pub trait LlmCodec: Send + Sync { pub trait LlmResponseCodec: Send + Sync { /// Parse a raw JSON response into normalized structured form. /// - /// Callers treat errors as "no annotation available" (pipeline continues - /// with `annotated_response: None`), so implementations should return - /// `Err` only for genuinely unparseable input. + /// Implementations should return `Err` only for genuinely unparseable input. fn decode_response(&self, response: &Json) -> Result; } diff --git a/crates/python/src/py_api/mod.rs b/crates/python/src/py_api/mod.rs index c044b1df..532f18cf 100644 --- a/crates/python/src/py_api/mod.rs +++ b/crates/python/src/py_api/mod.rs @@ -642,13 +642,7 @@ fn llm_call_end( 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 annotated_response = py_annotated_llm_response(annotated_response)?; let timestamp = opt_py_to_timestamp(timestamp)?; core_llm_api::llm_call_end( core_llm_api::LlmCallEndParams::builder() @@ -657,6 +651,7 @@ fn llm_call_end( .data_opt(data) .metadata_opt(metadata) .annotated_response_opt(annotated_response) + .response_codec_opt(response_codec) .timestamp_opt(timestamp) .build(), ) diff --git a/crates/sidecar/src/error.rs b/crates/sidecar/src/error.rs index b591ae53..c29051b9 100644 --- a/crates/sidecar/src/error.rs +++ b/crates/sidecar/src/error.rs @@ -33,7 +33,8 @@ impl IntoResponse for SidecarError { // 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 { + let message = self.to_string(); + let status = match &self { Self::InvalidPayload(_) => StatusCode::BAD_REQUEST, Self::Upstream(_) => StatusCode::BAD_GATEWAY, Self::Http(_) @@ -46,7 +47,7 @@ impl IntoResponse for SidecarError { }; let body = Json(json!({ "error": { - "message": self.to_string(), + "message": message, "type": "nemo_flow_sidecar_error" } })); diff --git a/crates/sidecar/src/gateway.rs b/crates/sidecar/src/gateway.rs index 7f5e7b16..7652c4ed 100644 --- a/crates/sidecar/src/gateway.rs +++ b/crates/sidecar/src/gateway.rs @@ -13,6 +13,8 @@ use crate::error::SidecarError; use crate::server::AppState; use crate::session::{ActiveLlm, LlmGatewayStart, SessionManager}; +const MAX_BODY_BYTES: usize = 100 * 1024 * 1024; + /// 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 @@ -63,7 +65,7 @@ async fn prepare_gateway_request( 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) + let body_bytes = axum::body::to_bytes(body, MAX_BODY_BYTES) .await .map_err(|error| SidecarError::InvalidPayload(error.to_string()))?; let request_json = serde_json::from_slice::(&body_bytes).unwrap_or(Value::Null); diff --git a/crates/sidecar/src/installer.rs b/crates/sidecar/src/installer.rs index 96950767..69096796 100644 --- a/crates/sidecar/src/installer.rs +++ b/crates/sidecar/src/installer.rs @@ -288,12 +288,11 @@ fn planned_codex_files( ) -> Result, SidecarError> { let config_path = base.join(".codex/config.toml"); let hooks_path = base.join(".codex/hooks.json"); + let existing_config = read_optional_text_file(&config_path)?; Ok(vec![ PlannedFile { path: config_path.clone(), - contents: merge_codex_config( - &std::fs::read_to_string(&config_path).unwrap_or_default(), - )?, + contents: merge_codex_config(&existing_config)?, }, planned_json_hooks_file( hooks_path, diff --git a/crates/sidecar/src/model.rs b/crates/sidecar/src/model.rs index d220e160..a21e627e 100644 --- a/crates/sidecar/src/model.rs +++ b/crates/sidecar/src/model.rs @@ -61,6 +61,13 @@ impl NormalizedEvent { Self::ToolStarted(event) | Self::ToolEnded(event) => &event.session_id, } } + + pub(crate) fn is_terminal(&self) -> bool { + matches!( + self, + Self::AgentEnded(_) | Self::SubagentEnded(_) | Self::LlmEnded(_) | Self::ToolEnded(_) + ) + } } #[derive(Debug, Clone, PartialEq)] diff --git a/crates/sidecar/src/server.rs b/crates/sidecar/src/server.rs index fb46967d..a4bf9c4f 100644 --- a/crates/sidecar/src/server.rs +++ b/crates/sidecar/src/server.rs @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +use std::time::Duration; + use axum::extract::State; use axum::http::HeaderMap; use axum::routing::{get, post}; @@ -16,6 +18,10 @@ use crate::error::SidecarError; use crate::gateway; use crate::session::SessionManager; +const HTTP_CONNECT_TIMEOUT: Duration = Duration::from_secs(30); +const HTTP_REQUEST_TIMEOUT: Duration = Duration::from_secs(300); +const HTTP_READ_TIMEOUT: Duration = Duration::from_secs(300); + #[derive(Clone)] pub(crate) struct AppState { pub(crate) config: SidecarConfig, @@ -63,9 +69,15 @@ pub(crate) async fn serve_listener( /// 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 http = Client::builder() + .connect_timeout(HTTP_CONNECT_TIMEOUT) + .timeout(HTTP_REQUEST_TIMEOUT) + .read_timeout(HTTP_READ_TIMEOUT) + .build() + .expect("sidecar HTTP client configuration is valid"); let state = AppState { config, - http: Client::new(), + http, sessions, }; Router::new() diff --git a/crates/sidecar/src/session.rs b/crates/sidecar/src/session.rs index 2dd58ddf..053aa406 100644 --- a/crates/sidecar/src/session.rs +++ b/crates/sidecar/src/session.rs @@ -147,6 +147,9 @@ impl SessionManager { let mut sessions = self.inner.lock().await; for event in events { let session_id = event.session_id().to_string(); + if event.is_terminal() && !sessions.contains_key(&session_id) { + continue; + } 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()) @@ -199,7 +202,17 @@ impl SessionManager { ) -> Result<(), SidecarError> { let response_for_hints = response.clone(); let session_id = active.session_id.clone(); + let llm_id = active.handle.uuid.to_string(); let owner_subagent_id = active.owner_subagent_id.clone(); + { + let mut sessions = self.inner.lock().await; + let Some(session) = sessions.get_mut(&session_id) else { + return Ok(()); + }; + if session.llms.remove(&llm_id).is_none() { + return Ok(()); + } + } TASK_SCOPE_STACK .scope(active.stack, async move { llm_call_end( @@ -218,6 +231,16 @@ impl SessionManager { } Ok(()) } + + #[cfg(test)] + pub(crate) async fn session_llms_empty(&self, session_id: &str) -> bool { + self.inner + .lock() + .await + .get(session_id) + .map(|session| session.llms.is_empty()) + .unwrap_or(true) + } } impl Session { @@ -298,12 +321,15 @@ impl Session { .model_name_opt(start.model_name) .build(), )?; - Ok(ActiveLlm { + let active = ActiveLlm { stack, handle, session_id: self.session_id.clone(), owner_subagent_id: owner.subagent_id, - }) + }; + self.llms + .insert(active.handle.uuid.to_string(), active.handle.clone()); + Ok(active) }) .await } @@ -1053,6 +1079,7 @@ fn write_atif( exporter: &AtifExporter, ) -> Result<(), SidecarError> { std::fs::create_dir_all(directory)?; + validate_atif_session_id(session_id)?; let path = directory.join(format!("{session_id}.atif.json")); let trajectory = exporter.export(); let serialized = serde_json::to_vec_pretty(&trajectory) @@ -1061,6 +1088,21 @@ fn write_atif( Ok(()) } +fn validate_atif_session_id(session_id: &str) -> Result<(), SidecarError> { + if session_id.is_empty() + || session_id == "." + || session_id == ".." + || !session_id + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) + { + return Err(SidecarError::InvalidPayload( + "session id is not safe for ATIF export filename".into(), + )); + } + 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. @@ -1148,6 +1190,9 @@ fn collect_openai_response_tool_hints( return; }; for item in output { + if item.get("type").and_then(Value::as_str) != Some("function_call") { + continue; + } push_tool_hint( hints, item, diff --git a/crates/sidecar/tests/coverage/gateway_tests.rs b/crates/sidecar/tests/coverage/gateway_tests.rs index d0001c16..7cf1d6ec 100644 --- a/crates/sidecar/tests/coverage/gateway_tests.rs +++ b/crates/sidecar/tests/coverage/gateway_tests.rs @@ -11,6 +11,42 @@ use axum::http::{HeaderMap, HeaderValue, Method, Request, StatusCode}; use http_body_util::BodyExt; use reqwest::Client; +async fn wait_for_file_contains( + path: &std::path::Path, + needle: &str, + timeout: std::time::Duration, +) -> bool { + let deadline = std::time::Instant::now() + timeout; + loop { + if let Ok(contents) = std::fs::read_to_string(path) + && contents.contains(needle) + { + return true; + } + if std::time::Instant::now() >= deadline { + return false; + } + tokio::time::sleep(std::time::Duration::from_millis(25)).await; + } +} + +async fn wait_for_session_llms_empty( + sessions: &SessionManager, + session_id: &str, + timeout: std::time::Duration, +) -> bool { + let deadline = std::time::Instant::now() + timeout; + loop { + if sessions.session_llms_empty(session_id).await { + return true; + } + if std::time::Instant::now() >= deadline { + return false; + } + tokio::time::sleep(std::time::Duration::from_millis(25)).await; + } +} + #[test] fn removes_hop_by_hop_headers() { assert!(!should_forward_request_header(&HeaderName::from_static( @@ -306,7 +342,11 @@ async fn streaming_llm_guard_closes_on_drop() { active, StatusCode::OK, )); - tokio::time::sleep(std::time::Duration::from_millis(100)).await; + // Drop cleanup runs in a spawned task, so poll the session state instead of sleeping. + assert!( + wait_for_session_llms_empty(&sessions, "drop-session", std::time::Duration::from_secs(5)) + .await + ); sessions .apply_events( &HeaderMap::new(), @@ -321,6 +361,13 @@ async fn streaming_llm_guard_closes_on_drop() { .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")); + let atif_path = temp.path().join("drop-session.atif.json"); + assert!( + wait_for_file_contains( + &atif_path, + "stream body dropped before completion", + std::time::Duration::from_secs(5), + ) + .await + ); } diff --git a/crates/sidecar/tests/coverage/server_tests.rs b/crates/sidecar/tests/coverage/server_tests.rs index 2a63028d..94105c2b 100644 --- a/crates/sidecar/tests/coverage/server_tests.rs +++ b/crates/sidecar/tests/coverage/server_tests.rs @@ -9,11 +9,29 @@ use futures_util::stream; use http_body_util::BodyExt; use serde_json::{Value, json}; use tokio::net::TcpListener; +use tokio::task::JoinHandle; use tower::ServiceExt; use super::*; use crate::error::SidecarError; +struct TestServer { + url: String, + handle: JoinHandle<()>, +} + +impl TestServer { + fn url(&self) -> String { + self.url.clone() + } +} + +impl Drop for TestServer { + fn drop(&mut self) { + self.handle.abort(); + } +} + fn test_config() -> SidecarConfig { SidecarConfig { bind: "127.0.0.1:0".parse().unwrap(), @@ -178,7 +196,7 @@ async fn hermes_hook_keeps_shell_hook_response_shape() { 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; + config.openai_base_url = upstream.url(); let app = router(config); let response = app .oneshot( @@ -211,7 +229,7 @@ async fn gateway_forwards_openai_json_without_rewriting_payload() { async fn gateway_preserves_streaming_body() { let upstream = spawn_upstream(true).await; let mut config = test_config(); - config.openai_base_url = upstream; + config.openai_base_url = upstream.url(); let app = router(config); let response = app .oneshot( @@ -244,7 +262,7 @@ async fn gateway_preserves_streaming_body() { async fn gateway_surfaces_streaming_upstream_errors() { let upstream = spawn_failing_stream_upstream().await; let mut config = test_config(); - config.openai_base_url = upstream; + config.openai_base_url = upstream.url(); let app = router(config); let response = app .oneshot( @@ -310,7 +328,7 @@ async fn gateway_returns_bad_gateway_when_upstream_is_unreachable() { async fn models_route_forwards_get_requests() { let upstream = spawn_models_upstream().await; let mut config = test_config(); - config.openai_base_url = upstream; + config.openai_base_url = upstream.url(); let app = router(config); let response = app .oneshot( @@ -331,7 +349,7 @@ async fn models_route_forwards_get_requests() { assert_eq!(body["authorization"], json!("Bearer test")); } -async fn spawn_upstream(streaming: bool) -> String { +async fn spawn_upstream(streaming: bool) -> TestServer { async fn chat(headers: HeaderMap, body: Bytes) -> impl IntoResponse { let payload: Value = serde_json::from_slice(&body).unwrap(); Json(json!({ @@ -363,13 +381,16 @@ async fn spawn_upstream(streaming: bool) -> String { }; let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let address = listener.local_addr().unwrap(); - tokio::spawn(async move { + let handle = tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); - format!("http://{address}") + TestServer { + url: format!("http://{address}"), + handle, + } } -async fn spawn_failing_stream_upstream() -> String { +async fn spawn_failing_stream_upstream() -> TestServer { async fn stream_response() -> impl IntoResponse { let chunks = stream::iter([ Ok::<_, std::io::Error>(Bytes::from_static(b"data: one\n\n")), @@ -384,13 +405,16 @@ async fn spawn_failing_stream_upstream() -> String { 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 { + let handle = tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); - format!("http://{address}") + TestServer { + url: format!("http://{address}"), + handle, + } } -async fn spawn_models_upstream() -> String { +async fn spawn_models_upstream() -> TestServer { async fn models(headers: HeaderMap, request: Request) -> impl IntoResponse { Json(json!({ "path": request.uri().path_and_query().map(|value| value.as_str()), @@ -403,8 +427,11 @@ async fn spawn_models_upstream() -> String { 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 { + let handle = tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); - format!("http://{address}") + TestServer { + url: format!("http://{address}"), + handle, + } } diff --git a/crates/sidecar/tests/coverage/session_tests.rs b/crates/sidecar/tests/coverage/session_tests.rs index c9c688d0..4b25fbad 100644 --- a/crates/sidecar/tests/coverage/session_tests.rs +++ b/crates/sidecar/tests/coverage/session_tests.rs @@ -275,6 +275,31 @@ async fn handles_out_of_order_subagent_and_tool_end_events() { assert!(manager.inner.lock().await.is_empty()); } +#[tokio::test] +async fn terminal_retry_for_unknown_session_is_ignored() { + 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); + + manager + .apply_events( + &HeaderMap::new(), + vec![NormalizedEvent::AgentEnded(SessionEvent { + session_id: "retry-session".into(), + agent_kind: AgentKind::Codex, + event_name: "sessionEnd".into(), + payload: json!({}), + metadata: json!({}), + })], + ) + .await + .unwrap(); + + assert!(manager.inner.lock().await.is_empty()); + assert!(!temp.path().join("retry-session.atif.json").exists()); +} + #[tokio::test] async fn out_of_order_started_subagent_end_does_not_leak_scope() { let config = SidecarConfig { @@ -449,6 +474,53 @@ async fn llm_lifecycle_starts_implicit_gateway_session() { assert!(sessions.contains_key("llm-session")); } +#[tokio::test] +async fn agent_end_closes_in_flight_gateway_llm() { + 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 _active = manager + .start_llm( + &HeaderMap::new(), + LlmGatewayStart { + session_id: Some("gateway-cleanup".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: true, + metadata: json!({ "gateway_path": "/v1/responses" }), + }, + ) + .await + .unwrap(); + + manager + .apply_events( + &HeaderMap::new(), + vec![NormalizedEvent::AgentEnded(SessionEvent { + session_id: "gateway-cleanup".into(), + agent_kind: AgentKind::Gateway, + event_name: "SessionEnd".into(), + payload: json!({}), + metadata: json!({}), + })], + ) + .await + .unwrap(); + + assert!(manager.inner.lock().await.is_empty()); + let atif = std::fs::read_to_string(temp.path().join("gateway-cleanup.atif.json")).unwrap(); + assert!(atif.contains("closed_by_agent_end")); +} + #[tokio::test] async fn llm_lifecycle_uses_single_active_hook_session_when_header_is_missing() { let config = SidecarConfig { @@ -1219,6 +1291,55 @@ async fn llm_response_tool_hint_claims_next_tool_hook() { ); } +#[test] +fn openai_response_tool_hints_ignore_non_tool_output_items() { + let mut hints = Vec::new(); + + collect_openai_response_tool_hints( + &json!({ + "output": [ + { + "type": "message", + "id": "msg-1", + "name": "Read", + "arguments": "{\"file_path\":\"README.md\"}" + }, + { + "type": "function_call", + "call_id": "call-1", + "name": "Read", + "arguments": "{\"file_path\":\"README.md\"}" + } + ] + }), + Some("worker"), + &mut hints, + ); + + assert_eq!(hints.len(), 1); + assert_eq!(hints[0].tool_call_id.as_deref(), Some("call-1")); +} + +#[test] +fn write_atif_rejects_unsafe_session_id_filename() { + let temp = tempfile::tempdir().unwrap(); + let exporter = AtifExporter::new( + "safe-session".to_string(), + AtifAgentInfo { + name: "test-agent".to_string(), + version: "1.0.0".to_string(), + model_name: None, + tool_definitions: None, + extra: None, + }, + ); + + let error = write_atif(&temp.path().to_path_buf(), "../escape", &exporter).unwrap_err(); + + assert!(matches!(error, SidecarError::InvalidPayload(_))); + assert!(!temp.path().join("../escape.atif.json").exists()); +} + #[tokio::test] async fn multiple_tool_hints_resolve_by_tool_call_id() { let manager = SessionManager::new(session_test_config()); diff --git a/integrations/coding-agents/codex/hooks/hooks.json b/integrations/coding-agents/codex/hooks/hooks.json index d7355374..c25b825e 100644 --- a/integrations/coding-agents/codex/hooks/hooks.json +++ b/integrations/coding-agents/codex/hooks/hooks.json @@ -1,4 +1,5 @@ { + "SPDX-License-Identifier": "Apache-2.0", "hooks": { "SessionStart": [ { diff --git a/integrations/coding-agents/cursor/.cursor/hooks.json b/integrations/coding-agents/cursor/.cursor/hooks.json index bc5ff091..0196f7da 100644 --- a/integrations/coding-agents/cursor/.cursor/hooks.json +++ b/integrations/coding-agents/cursor/.cursor/hooks.json @@ -1,4 +1,5 @@ { + "SPDX-License-Identifier": "Apache-2.0", "hooks": { "sessionStart": [ { diff --git a/python/nemo_flow/llm.py b/python/nemo_flow/llm.py index 9eec59fd..fba23bcb 100644 --- a/python/nemo_flow/llm.py +++ b/python/nemo_flow/llm.py @@ -152,8 +152,8 @@ def call_end( 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. + ``annotated_response`` from the sanitized end-event payload 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. @@ -164,6 +164,8 @@ def call_end( ``call_end()`` applies sanitize-response guardrails to the emitted end-event payload. ``response_codec`` and ``annotated_response`` enrich observability output only and do not rewrite the recorded response. + Response codec failures are raised after the end event is emitted + without an annotation. ``timestamp`` must be a timezone-aware ``datetime``; strings and naive datetimes are rejected. """ diff --git a/python/tests/test_builtin_codecs.py b/python/tests/test_builtin_codecs.py index 34df7ecf..2e0fd099 100644 --- a/python/tests/test_builtin_codecs.py +++ b/python/tests/test_builtin_codecs.py @@ -13,11 +13,13 @@ from typing import cast import nemo_flow +import pytest from nemo_flow import ( AnnotatedLLMRequest, AnnotatedLLMResponse, JsonObject, LLMRequest, + guardrails, llm, subscribers, ) @@ -268,6 +270,77 @@ def capture(event): finally: subscribers.deregister("test-manual-call-end-annotated-response") + def test_manual_call_end_response_codec_uses_sanitized_payload(self): + """manual llm.call_end() decodes response annotations from sanitized event data.""" + captured_events = [] + + def capture(event): + captured_events.append(event) + + def sanitize_response(response): + return { + "id": "chatcmpl-sanitized", + "model": "gpt-4", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Sanitized"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3}, + } + + guardrails.register_llm_sanitize_response("test-call-end-codec-sanitizer", 1, sanitize_response) + subscribers.register("test-manual-call-end-sanitized-response-codec", capture) + + try: + handle = llm.call( + "manual-codec-sanitized-llm", + LLMRequest({}, {"model": "gpt-4", "messages": []}), + ) + llm.call_end(handle, "raw response", 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 + assert end_events[0].data["id"] == "chatcmpl-sanitized" + + annotated = end_events[0].annotated_response + assert annotated is not None + assert annotated.response_text() == "Sanitized" + + finally: + subscribers.deregister("test-manual-call-end-sanitized-response-codec") + guardrails.deregister_llm_sanitize_response("test-call-end-codec-sanitizer") + + def test_manual_call_end_response_codec_failure_raises_after_end_event(self): + """manual llm.call_end() surfaces response codec failures instead of dropping them.""" + captured_events = [] + + def capture(event): + captured_events.append(event) + + subscribers.register("test-manual-call-end-response-codec-error", capture) + + try: + handle = llm.call( + "manual-codec-error-llm", + LLMRequest({}, {"model": "gpt-4", "messages": []}), + ) + with pytest.raises(RuntimeError, match="OpenAI Chat response decode"): + llm.call_end(handle, "malformed response", 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 + assert end_events[0].annotated_response is None + + finally: + subscribers.deregister("test-manual-call-end-response-codec-error") + async def test_response_codec_accepts_builtin_object(self): """response_codec= accepts a built-in codec object, not a string.""" captured_events = [] From d336a47e8c326ad762a69db58c80ec44dc92d78f Mon Sep 17 00:00:00 2001 From: Will Killian Date: Fri, 8 May 2026 16:17:52 -0400 Subject: [PATCH 15/27] fix(sidecar): route Codex through provider alias Signed-off-by: Will Killian --- crates/sidecar/src/gateway.rs | 11 ++++++ crates/sidecar/src/launcher.rs | 20 +++++++---- crates/sidecar/src/server.rs | 3 ++ .../sidecar/tests/coverage/gateway_tests.rs | 16 +++++++++ .../sidecar/tests/coverage/launcher_tests.rs | 17 ++++++++- crates/sidecar/tests/coverage/server_tests.rs | 36 ++++++++++++++++++- .../coding-agent-codex.md | 36 +++++++++++++------ integrations/coding-agents/README.md | 3 +- .../codex/.codex-plugin/plugin.json | 2 +- integrations/coding-agents/codex/README.md | 23 ++++++++---- 10 files changed, 139 insertions(+), 28 deletions(-) diff --git a/crates/sidecar/src/gateway.rs b/crates/sidecar/src/gateway.rs index 7652c4ed..9bab80d1 100644 --- a/crates/sidecar/src/gateway.rs +++ b/crates/sidecar/src/gateway.rs @@ -393,8 +393,11 @@ impl ProviderRoute { // so the caller can fail as a bad hook/gateway payload instead of constructing arbitrary URLs. fn from_path(path: &str) -> Option { match path { + "/responses" => Some(Self::OpenAiResponses), "/v1/responses" => Some(Self::OpenAiResponses), + "/chat/completions" => Some(Self::OpenAiChatCompletions), "/v1/chat/completions" => Some(Self::OpenAiChatCompletions), + "/models" => Some(Self::OpenAiModels), "/v1/models" => Some(Self::OpenAiModels), "/v1/messages" => Some(Self::AnthropicMessages), "/v1/messages/count_tokens" => Some(Self::AnthropicCountTokens), @@ -426,6 +429,14 @@ impl ProviderRoute { config.anthropic_base_url.trim_end_matches('/') } }; + let path_and_query = match self { + Self::OpenAiResponses | Self::OpenAiChatCompletions | Self::OpenAiModels + if !path_and_query.starts_with("/v1/") => + { + format!("/v1{path_and_query}") + } + _ => path_and_query.to_string(), + }; format!("{base}{path_and_query}") } } diff --git a/crates/sidecar/src/launcher.rs b/crates/sidecar/src/launcher.rs index 62c6af24..48a9ccf2 100644 --- a/crates/sidecar/src/launcher.rs +++ b/crates/sidecar/src/launcher.rs @@ -310,19 +310,18 @@ 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. + // Injects Codex hook and provider configuration through repeated `--config` flags. Codex + // reserves built-in provider IDs, so run mode installs a temporary provider alias instead of + // overriding `model_providers.openai`. 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) - ), + "model_provider=\"nemo-flow-openai\"".to_string(), + "--config".to_string(), + codex_sidecar_provider_config(sidecar_url), ]; for (event, groups) in generated_hooks(CodingAgent::Codex, &hook_command)["hooks"] .as_object() @@ -472,6 +471,13 @@ async fn wait_for_health(sidecar_url: &str) -> Result<(), SidecarError> { ))) } +fn codex_sidecar_provider_config(sidecar_url: &str) -> String { + format!( + "model_providers.nemo-flow-openai={{name=\"Nemo Flow OpenAI\",base_url={},wire_api=\"responses\",requires_openai_auth=true,supports_websockets=false}}", + toml_string(sidecar_url) + ) +} + // 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. diff --git a/crates/sidecar/src/server.rs b/crates/sidecar/src/server.rs index a4bf9c4f..30340bea 100644 --- a/crates/sidecar/src/server.rs +++ b/crates/sidecar/src/server.rs @@ -86,6 +86,9 @@ pub(crate) fn router(config: SidecarConfig) -> Router { .route("/hooks/claude-code", post(claude_code_hook)) .route("/hooks/cursor", post(cursor_hook)) .route("/hooks/hermes", post(hermes_hook)) + .route("/responses", post(gateway::passthrough)) + .route("/chat/completions", post(gateway::passthrough)) + .route("/models", get(gateway::models)) .route("/v1/responses", post(gateway::passthrough)) .route("/v1/chat/completions", post(gateway::passthrough)) .route("/v1/messages", post(gateway::passthrough)) diff --git a/crates/sidecar/tests/coverage/gateway_tests.rs b/crates/sidecar/tests/coverage/gateway_tests.rs index 7cf1d6ec..76344225 100644 --- a/crates/sidecar/tests/coverage/gateway_tests.rs +++ b/crates/sidecar/tests/coverage/gateway_tests.rs @@ -72,6 +72,10 @@ fn removes_hop_by_hop_headers() { #[test] fn selects_provider_routes() { + assert_eq!( + ProviderRoute::from_path("/responses"), + Some(ProviderRoute::OpenAiResponses) + ); assert_eq!( ProviderRoute::from_path("/v1/responses"), Some(ProviderRoute::OpenAiResponses) @@ -86,6 +90,10 @@ fn selects_provider_routes() { .name(), "openai.chat_completions" ); + assert_eq!( + ProviderRoute::from_path("/models"), + Some(ProviderRoute::OpenAiModels) + ); assert_eq!(ProviderRoute::OpenAiModels.name(), "openai.models"); assert_eq!( ProviderRoute::AnthropicMessages.name(), @@ -114,6 +122,14 @@ fn provider_routes_preserve_path_query_and_choose_upstream() { ProviderRoute::OpenAiResponses.upstream_url(&config, "/v1/responses?x=1"), "http://openai/v1/responses?x=1" ); + assert_eq!( + ProviderRoute::OpenAiResponses.upstream_url(&config, "/responses?x=1"), + "http://openai/v1/responses?x=1" + ); + assert_eq!( + ProviderRoute::OpenAiModels.upstream_url(&config, "/models"), + "http://openai/v1/models" + ); assert_eq!( ProviderRoute::AnthropicMessages.upstream_url(&config, "/v1/messages"), "http://anthropic/v1/messages" diff --git a/crates/sidecar/tests/coverage/launcher_tests.rs b/crates/sidecar/tests/coverage/launcher_tests.rs index faf0f2be..c02bec96 100644 --- a/crates/sidecar/tests/coverage/launcher_tests.rs +++ b/crates/sidecar/tests/coverage/launcher_tests.rs @@ -170,7 +170,22 @@ fn prepares_codex_config_overrides() { prepared .argv .iter() - .any(|arg| arg.contains("model_providers.openai.base_url")) + .any(|arg| arg == "model_provider=\"nemo-flow-openai\"") + ); + assert!( + prepared + .argv + .iter() + .any(|arg| arg.contains("model_providers.nemo-flow-openai") + && arg.contains("base_url=\"http://127.0.0.1:1234\"") + && arg.contains("requires_openai_auth=true") + && arg.contains("supports_websockets=false")) + ); + assert!( + !prepared + .argv + .iter() + .any(|arg| arg.contains("model_providers.openai")) ); assert!( prepared diff --git a/crates/sidecar/tests/coverage/server_tests.rs b/crates/sidecar/tests/coverage/server_tests.rs index 94105c2b..fb073df1 100644 --- a/crates/sidecar/tests/coverage/server_tests.rs +++ b/crates/sidecar/tests/coverage/server_tests.rs @@ -225,6 +225,38 @@ async fn gateway_forwards_openai_json_without_rewriting_payload() { assert_eq!(body["connection"], Value::Null); } +#[tokio::test] +async fn gateway_accepts_codex_responses_path() { + let upstream = spawn_upstream(false).await; + let mut config = test_config(); + config.openai_base_url = upstream.url(); + let app = router(config); + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/responses") + .header("content-type", "application/json") + .header("authorization", "Bearer test") + .body(Body::from( + json!({ + "model": "gpt-test", + "input": "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")); +} + #[tokio::test] async fn gateway_preserves_streaming_body() { let upstream = spawn_upstream(true).await; @@ -377,7 +409,9 @@ async fn spawn_upstream(streaming: bool) -> TestServer { let app = if streaming { Router::new().route("/v1/responses", post(stream_response)) } else { - Router::new().route("/v1/chat/completions", post(chat)) + Router::new() + .route("/v1/chat/completions", post(chat)) + .route("/v1/responses", post(chat)) }; let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let address = listener.local_addr().unwrap(); diff --git a/docs/integrate-frameworks/coding-agent-codex.md b/docs/integrate-frameworks/coding-agent-codex.md index 419d3a7f..dedbbd62 100644 --- a/docs/integrate-frameworks/coding-agent-codex.md +++ b/docs/integrate-frameworks/coding-agent-codex.md @@ -20,8 +20,9 @@ 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. +commands that use `NEMO_FLOW_SIDECAR_URL`, and points Codex at a temporary +`nemo-flow-openai` provider alias that uses the sidecar URL while preserving +Codex's OpenAI auth path. Inspect what would be launched without starting Codex: @@ -72,11 +73,24 @@ nemo-flow-sidecar install codex \ --atif-dir .nemo-flow/atif ``` -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. +Then start the sidecar manually and configure local Codex to use a sidecar +provider alias instead of overriding the reserved built-in `openai` provider: + +```toml +model_provider = "nemo-flow-openai" + +[model_providers.nemo-flow-openai] +name = "Nemo Flow OpenAI" +base_url = "http://127.0.0.1:4040" +wire_api = "responses" +requires_openai_auth = true +supports_websockets = false +``` + +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. ## Captured Events @@ -120,10 +134,10 @@ missing, confirm `codex_hooks = true`, hook config loading, and `--atif-dir` or ## 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. +If agent/tool events exist but LLM spans are missing, the active Codex provider +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 diff --git a/integrations/coding-agents/README.md b/integrations/coding-agents/README.md index c2d8e318..225258cb 100644 --- a/integrations/coding-agents/README.md +++ b/integrations/coding-agents/README.md @@ -25,7 +25,8 @@ environment variables, or shared TOML config. - `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`. + `codex_hooks = true`. Use `nemo-flow-sidecar run` or a sidecar provider alias + for Codex LLM gateway routing. - `cursor/` installs a Cursor `.cursor/hooks.json` bundle targeting `POST /hooks/cursor`. - Hermes does not require a static bundle in this directory. Use diff --git a/integrations/coding-agents/codex/.codex-plugin/plugin.json b/integrations/coding-agents/codex/.codex-plugin/plugin.json index 556e50ab..77a2eb57 100644 --- a/integrations/coding-agents/codex/.codex-plugin/plugin.json +++ b/integrations/coding-agents/codex/.codex-plugin/plugin.json @@ -19,7 +19,7 @@ "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.", + "longDescription": "Installs command hooks that preserve Codex hook payloads and forward them to nemo-flow-sidecar for agent, subagent, tool, and lifecycle observability. Full LLM capture also requires sidecar provider routing.", "developerName": "NVIDIA", "category": "Coding", "capabilities": [ diff --git a/integrations/coding-agents/codex/README.md b/integrations/coding-agents/codex/README.md index ce2db35c..71c1c194 100644 --- a/integrations/coding-agents/codex/README.md +++ b/integrations/coding-agents/codex/README.md @@ -43,8 +43,9 @@ nemo-flow-sidecar run --atif-dir .nemo-flow/atif -- codex 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. +`NEMO_FLOW_SIDECAR_URL`, and points Codex at a temporary `nemo-flow-openai` +provider alias that uses the sidecar URL while preserving Codex's OpenAI auth +path. Inspect the launch without starting Codex: @@ -90,8 +91,19 @@ nemo-flow-sidecar install codex \ --atif-dir .nemo-flow/atif ``` -Then start the sidecar manually and configure the local Codex provider -`base_url` to `http://127.0.0.1:4040`. +Then start the sidecar manually and configure local Codex to use a sidecar +provider alias instead of overriding the reserved built-in `openai` provider: + +```toml +model_provider = "nemo-flow-openai" + +[model_providers.nemo-flow-openai] +name = "Nemo Flow OpenAI" +base_url = "http://127.0.0.1:4040" +wire_api = "responses" +requires_openai_auth = true +supports_websockets = false +``` ## Verify @@ -111,8 +123,7 @@ 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. +`nemo-flow-sidecar run` or that the active provider 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 From bf4a82da0a7a81fda545a982b717ac817d8dc4cb Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 8 May 2026 15:41:59 -0700 Subject: [PATCH 16/27] fix(sidecar): update Codex configuration to use new hooks flag Updated the Codex configuration to replace the deprecated `features.codex_hooks` with `features.hooks`, requiring `codex-cli >= 0.129.0`. Adjusted related tests and documentation to reflect this change. Signed-off-by: Ajay Thorve --- crates/sidecar/src/launcher.rs | 5 +++-- crates/sidecar/tests/coverage/launcher_tests.rs | 2 +- docs/integrate-frameworks/coding-agent-codex.md | 7 +++++++ integrations/coding-agents/codex/README.md | 3 +++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/sidecar/src/launcher.rs b/crates/sidecar/src/launcher.rs index 48a9ccf2..4ba41c96 100644 --- a/crates/sidecar/src/launcher.rs +++ b/crates/sidecar/src/launcher.rs @@ -312,12 +312,13 @@ impl PreparedRun { // Injects Codex hook and provider configuration through repeated `--config` flags. Codex // reserves built-in provider IDs, so run mode installs a temporary provider alias instead of - // overriding `model_providers.openai`. + // overriding `model_providers.openai`. Uses `features.hooks=true` introduced in codex-cli + // 0.129; the older `features.codex_hooks` is deprecated. Requires codex-cli >= 0.129.0. 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(), + "features.hooks=true".to_string(), "--config".to_string(), "model_provider=\"nemo-flow-openai\"".to_string(), "--config".to_string(), diff --git a/crates/sidecar/tests/coverage/launcher_tests.rs b/crates/sidecar/tests/coverage/launcher_tests.rs index c02bec96..a5f1524f 100644 --- a/crates/sidecar/tests/coverage/launcher_tests.rs +++ b/crates/sidecar/tests/coverage/launcher_tests.rs @@ -165,7 +165,7 @@ fn prepares_codex_config_overrides() { ) .unwrap(); - assert!(prepared.argv.contains(&"features.codex_hooks=true".into())); + assert!(prepared.argv.contains(&"features.hooks=true".into())); assert!( prepared .argv diff --git a/docs/integrate-frameworks/coding-agent-codex.md b/docs/integrate-frameworks/coding-agent-codex.md index dedbbd62..31101633 100644 --- a/docs/integrate-frameworks/coding-agent-codex.md +++ b/docs/integrate-frameworks/coding-agent-codex.md @@ -10,6 +10,13 @@ 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. +## Requirements + +`codex-cli >= 0.129.0`. The sidecar uses the `features.hooks` flag and the +`nemo-flow-openai` provider alias, both of which require this version. Earlier +versions either reject the provider override or do not recognize the hooks +feature flag. + ## Transparent Run Use the wrapper for no-install local observability: diff --git a/integrations/coding-agents/codex/README.md b/integrations/coding-agents/codex/README.md index 71c1c194..11b5c837 100644 --- a/integrations/coding-agents/codex/README.md +++ b/integrations/coding-agents/codex/README.md @@ -13,6 +13,9 @@ 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. +Requires `codex-cli >= 0.129.0` (introduced the `features.hooks` flag and the +provider alias surface the sidecar relies on). + ## Files - `.codex-plugin/plugin.json` describes the Codex plugin package. From eb16758085700839ba5fe1cc36dd80722168e53f Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 8 May 2026 18:58:05 -0700 Subject: [PATCH 17/27] fix(sidecar): strip Accept-Encoding from forwarded gateway requests Without this, upstream providers honored the client's Accept-Encoding and returned gzip/br/zstd-encoded bodies. The bytes were forwarded to the client unchanged (which decoded normally), but the sidecar's observability capture stored the still-compressed bytes in `output.value` on LLM spans and in ATIF trajectory bodies, leaving both unreadable for any downstream consumer (Phoenix, ATIF, ATOF). Stripping the header forces upstreams to return identity-encoded bodies. The bandwidth cost is paid only on the sidecar-upstream hop; the client never asked for the encoding it would have received, so its decoders never trigger. Closes NMF-84. Signed-off-by: Ajay Thorve --- crates/sidecar/src/gateway.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/sidecar/src/gateway.rs b/crates/sidecar/src/gateway.rs index 9bab80d1..f08cdf4e 100644 --- a/crates/sidecar/src/gateway.rs +++ b/crates/sidecar/src/gateway.rs @@ -528,7 +528,15 @@ fn build_response( // 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 + !is_hop_by_hop(name) + && name != http::header::HOST + && name != http::header::CONTENT_LENGTH + // Strip Accept-Encoding so upstreams return identity-encoded bodies; otherwise the + // observability capture (`output.value` on LLM spans, ATIF trajectory bodies) records + // gzip/br/zstd bytes that downstream consumers can't read. Bandwidth cost is paid only + // on the sidecar-upstream hop. The client never asked for the encoding it would have + // received from upstream, so its decoders never trigger. + && name != http::header::ACCEPT_ENCODING } // Allows headers into observability metadata only after removing credentials and provider API keys. From 08b4b61bad8b4126c546ad49da551599167555af Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 8 May 2026 19:51:20 -0700 Subject: [PATCH 18/27] fix(sidecar): unblock transparent-run hooks for claude (and partial codex) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three independent issues silently disabled transparent-run hooks for Claude Code; this commit fixes all three: 1. Hook subprocesses couldn't find the sidecar binary on PATH. `hook_forward_command` returned the bare name `nemo-flow-sidecar`, which Claude/Codex/Cursor passed to their hook subprocesses. Those subprocesses inherit the agent's environment, where the sidecar's install location (e.g. target/debug) typically isn't on PATH, so every hook exited with status 127 (command not found). Add an `executable` parameter so transparent-run callers can pass `current_exe()`; persistent install keeps the bare name. 2. Claude Code 2.1.x's hook loader strict-rejects unknown event names — any single unknown event causes the ENTIRE hooks file to be rejected silently, disabling all hooks. We were injecting `AfterAgentResponse` and `AfterAgentThought`, neither in Claude's whitelist. Drop both. Also add `PermissionRequest` and `PostCompact`, which are in Claude's whitelist and in Codex 0.129's event surface. 3. Claude's hook output validator rejects null for optional string fields. The Claude adapter's Stop response carried `stopReason: null` and was rejected with a runtime validation error. Omit the field entirely; the response collapses to the generic `{ continue: true }`. Also regenerate the static integration bundles for claude-code and codex with the corrected event list. The same whitelist mismatch broke persistent install for Claude users. Verified end-to-end against Claude Code 2.1.137: - SessionStart, UserPromptSubmit, PreToolUse:WebSearch, PostToolUse:WebSearch, Stop, SubagentStop all fire successfully - Phoenix shows [AGENT] claude-code root span with tool spans - ATIF written on session-end (1.7 MB / 1.3 MB across two sessions) Codex 0.129 lifecycle (SessionStart through PreToolUse/PostToolUse) now also fires correctly — Phoenix shows [AGENT] codex root with Bash tool spans. Codex transparent-run ATIF flush still pending: codex 0.129 has no SessionEnd-equivalent hook, so end_agent never runs without a wrapper-driven flush. Tracking that piece separately. Signed-off-by: Ajay Thorve --- crates/sidecar/src/adapters/claude_code.rs | 18 +++----- crates/sidecar/src/installer.rs | 19 ++++++-- crates/sidecar/src/launcher.rs | 19 ++++++-- .../sidecar/tests/coverage/adapters_tests.rs | 14 ++++-- .../sidecar/tests/coverage/installer_tests.rs | 44 ++++++++++++++----- .../claude-code/hooks/hooks.json | 15 ++++--- .../coding-agents/codex/hooks/hooks.json | 15 ++++--- 7 files changed, 95 insertions(+), 49 deletions(-) diff --git a/crates/sidecar/src/adapters/claude_code.rs b/crates/sidecar/src/adapters/claude_code.rs index 95a079bf..cb4ea2d2 100644 --- a/crates/sidecar/src/adapters/claude_code.rs +++ b/crates/sidecar/src/adapters/claude_code.rs @@ -4,15 +4,16 @@ use axum::http::HeaderMap; use serde_json::{Value, json}; -use crate::adapters::{AdapterOutcome, ClassificationRules, classify, event_name, normalize_name}; +use crate::adapters::{AdapterOutcome, ClassificationRules, classify}; 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. +/// instead of returning the generic `{ continue: true }` shape. All other hooks acknowledge with +/// `{ continue: true }` so the sidecar remains observational and never blocks Claude's lifecycle +/// by default. Note: Claude's hook output schema rejects `null` for optional string fields like +/// `stopReason`; omit them entirely instead. pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { let event = classify( &payload, @@ -36,7 +37,6 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { ], }, ); - let normalized_event = normalize_name(&event_name(&payload)); let response = match &event { NormalizedEvent::ToolStarted(_) => json!({ "continue": true, @@ -45,14 +45,6 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { "permissionDecision": "allow" } }), - NormalizedEvent::AgentEnded(_) - | NormalizedEvent::HookMark(_) - | NormalizedEvent::LlmHint(_) - if normalized_event == "stop" => - { - json!({ "continue": true, "stopReason": null }) - } - NormalizedEvent::AgentEnded(_) => json!({ "continue": true }), _ => json!({ "continue": true }), }; AdapterOutcome { diff --git a/crates/sidecar/src/installer.rs b/crates/sidecar/src/installer.rs index 69096796..df140472 100644 --- a/crates/sidecar/src/installer.rs +++ b/crates/sidecar/src/installer.rs @@ -14,19 +14,24 @@ use crate::config::{ }; use crate::error::SidecarError; +// Claude Code's hook loader strictly whitelists event names — any unknown event causes the +// entire hooks file to be rejected (no hooks register). Only events present in Claude Code's +// whitelist as of 2.1.x belong here. Codex 0.129 has a smaller subset (SessionStart, +// UserPromptSubmit, PreToolUse, PostToolUse, Stop, PreCompact, PostCompact, PermissionRequest) +// and silently ignores events it doesn't recognize, so the union list is safe for both agents. const HOOK_EVENTS: &[&str] = &[ "SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", - "AfterAgentResponse", - "AfterAgentThought", + "PermissionRequest", "SubagentStart", "SubagentStop", "Notification", "Stop", "PreCompact", + "PostCompact", "SessionEnd", ]; @@ -454,8 +459,14 @@ pub(crate) fn generated_hooks(agent: CodingAgent, command: &str) -> Value { } } -pub(crate) fn hook_forward_command(agent: CodingAgent) -> String { - format!("nemo-flow-sidecar hook-forward {}", agent.as_arg()) +// Returns the shell command a hook should run to forward an event to the sidecar. Callers must +// pass the executable they want hooks to invoke. Transparent-run callers should pass the absolute +// path of the currently running sidecar binary so spawned hook subprocesses do not depend on the +// user's `PATH` (which Codex/Claude/Cursor inherit but which typically does not include +// `target/debug` or other dev locations); persistent-install callers can pass the bare name +// `"nemo-flow-sidecar"` because the user is expected to have the binary on `PATH` after install. +pub(crate) fn hook_forward_command(executable: &str, agent: CodingAgent) -> String { + format!("{executable} hook-forward {}", agent.as_arg()) } fn claude_hooks(command: &str) -> Value { diff --git a/crates/sidecar/src/launcher.rs b/crates/sidecar/src/launcher.rs index 4ba41c96..51a3e2ba 100644 --- a/crates/sidecar/src/launcher.rs +++ b/crates/sidecar/src/launcher.rs @@ -296,7 +296,7 @@ impl PreparedRun { &root.join("hooks/hooks.json"), generated_hooks( CodingAgent::ClaudeCode, - &hook_forward_command(CodingAgent::ClaudeCode), + &hook_forward_command(&transparent_hook_executable(), CodingAgent::ClaudeCode), ), )?; insert_after_agent( @@ -315,7 +315,7 @@ impl PreparedRun { // overriding `model_providers.openai`. Uses `features.hooks=true` introduced in codex-cli // 0.129; the older `features.codex_hooks` is deprecated. Requires codex-cli >= 0.129.0. fn prepare_codex(&mut self, sidecar_url: &str) { - let hook_command = hook_forward_command(CodingAgent::Codex); + let hook_command = hook_forward_command(&transparent_hook_executable(), CodingAgent::Codex); let mut args = vec![ "--config".to_string(), "features.hooks=true".to_string(), @@ -479,6 +479,19 @@ fn codex_sidecar_provider_config(sidecar_url: &str) -> String { ) } +// Returns the absolute path of the running sidecar binary so injected hooks can find it +// without relying on the user's `PATH`. Spawned hook subprocesses inherit the agent's +// environment; in transparent run, the dev/install location of the sidecar is rarely on +// `PATH`, which would cause hooks to exit with status 127 (command not found). Falls back +// to the bare name when `current_exe` is unavailable so behavior degrades to the previous +// install-style assumption rather than failing to launch. +fn transparent_hook_executable() -> String { + std::env::current_exe() + .ok() + .and_then(|path| path.to_str().map(str::to_owned)) + .unwrap_or_else(|| "nemo-flow-sidecar".to_string()) +} + // 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. @@ -529,7 +542,7 @@ fn write_merged_cursor_hooks(path: &Path) -> Result<(), SidecarError> { read_json_file(path)?, generated_hooks( CodingAgent::Cursor, - &hook_forward_command(CodingAgent::Cursor), + &hook_forward_command(&transparent_hook_executable(), CodingAgent::Cursor), ), )?) .map_err(|error| SidecarError::Launch(error.to_string()))?; diff --git a/crates/sidecar/tests/coverage/adapters_tests.rs b/crates/sidecar/tests/coverage/adapters_tests.rs index 2f121283..be0444d7 100644 --- a/crates/sidecar/tests/coverage/adapters_tests.rs +++ b/crates/sidecar/tests/coverage/adapters_tests.rs @@ -152,9 +152,12 @@ fn maps_claude_stop_response_shape() { &HeaderMap::new(), ); - assert_eq!( - outcome.response, - json!({ "continue": true, "stopReason": null }) + // Claude's hook output schema rejects `null` for optional string fields like stopReason — + // the adapter must omit them entirely (return only `{ continue: true }`). + assert_eq!(outcome.response, json!({ "continue": true })); + assert!( + outcome.response.get("stopReason").is_none(), + "stopReason must not appear in the response (Claude rejects null)" ); } @@ -472,7 +475,10 @@ fn stop_responses_preserve_vendor_shapes() { &headers, ); assert!(matches!(claude.events[0], NormalizedEvent::LlmHint(_))); - assert_eq!(claude.response["stopReason"], Value::Null); + assert!( + claude.response.get("stopReason").is_none(), + "stopReason must not be present (Claude rejects null per its hook schema)" + ); let codex = codex::adapt( json!({ diff --git a/crates/sidecar/tests/coverage/installer_tests.rs b/crates/sidecar/tests/coverage/installer_tests.rs index b46db8b9..6c08f5db 100644 --- a/crates/sidecar/tests/coverage/installer_tests.rs +++ b/crates/sidecar/tests/coverage/installer_tests.rs @@ -39,14 +39,23 @@ fn generates_claude_install_file() { 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"]["SessionEnd"].is_array()); + assert!(json["hooks"]["Stop"].is_array()); assert!(json["hooks"]["Notification"].is_array()); assert!( - json["hooks"]["AfterAgentResponse"][0] - .get("matcher") - .is_none() + json["hooks"]["PermissionRequest"].is_array(), + "PermissionRequest must be injected (Claude + Codex both support it)" + ); + assert!(json["hooks"]["PostCompact"].is_array()); + assert!( + json["hooks"]["AfterAgentResponse"].is_null(), + "AfterAgentResponse is not in Claude's hook whitelist; it must not be injected (would cause Claude to reject the entire hooks file)" + ); + assert!( + json["hooks"]["AfterAgentThought"].is_null(), + "AfterAgentThought is not in Claude's hook whitelist; it must not be injected" ); + assert!(json["hooks"]["SessionEnd"][0].get("matcher").is_none()); assert!( json["hooks"]["PreToolUse"][0]["hooks"][0]["command"] .as_str() @@ -64,14 +73,23 @@ fn generates_codex_config_and_hooks() { 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"]["SessionStart"].is_array()); + assert!(json["hooks"]["SessionEnd"].is_array()); assert!(json["hooks"]["Notification"].is_array()); assert!( - json["hooks"]["AfterAgentThought"][0] - .get("matcher") - .is_none() + json["hooks"]["PermissionRequest"].is_array(), + "PermissionRequest must be injected for Codex" + ); + assert!(json["hooks"]["PostCompact"].is_array()); + assert!( + json["hooks"]["AfterAgentResponse"].is_null(), + "AfterAgentResponse must not be injected — not part of the supported event surface" ); + assert!( + json["hooks"]["AfterAgentThought"].is_null(), + "AfterAgentThought must not be injected — not part of the supported event surface" + ); + assert!(json["hooks"]["Stop"][0].get("matcher").is_none()); assert!( json["hooks"]["PreToolUse"][0]["hooks"][0]["command"] .as_str() @@ -361,9 +379,13 @@ fn generated_hook_dispatch_covers_all_agents() { assert!(generated_hooks(agent, "cmd")["hooks"].is_object()); } assert_eq!( - hook_forward_command(CodingAgent::Hermes), + hook_forward_command("nemo-flow-sidecar", CodingAgent::Hermes), "nemo-flow-sidecar hook-forward hermes" ); + assert_eq!( + hook_forward_command("/abs/path/to/nemo-flow-sidecar", CodingAgent::Codex), + "/abs/path/to/nemo-flow-sidecar hook-forward codex" + ); } #[test] diff --git a/integrations/coding-agents/claude-code/hooks/hooks.json b/integrations/coding-agents/claude-code/hooks/hooks.json index 82ac68e4..fe39482e 100644 --- a/integrations/coding-agents/claude-code/hooks/hooks.json +++ b/integrations/coding-agents/claude-code/hooks/hooks.json @@ -58,8 +58,9 @@ ] } ], - "AfterAgentResponse": [ + "PermissionRequest": [ { + "matcher": "*", "hooks": [ { "type": "command", @@ -69,7 +70,7 @@ ] } ], - "AfterAgentThought": [ + "SubagentStart": [ { "hooks": [ { @@ -80,7 +81,7 @@ ] } ], - "SubagentStart": [ + "SubagentStop": [ { "hooks": [ { @@ -91,7 +92,7 @@ ] } ], - "SubagentStop": [ + "Notification": [ { "hooks": [ { @@ -102,7 +103,7 @@ ] } ], - "Notification": [ + "Stop": [ { "hooks": [ { @@ -113,7 +114,7 @@ ] } ], - "Stop": [ + "PreCompact": [ { "hooks": [ { @@ -124,7 +125,7 @@ ] } ], - "PreCompact": [ + "PostCompact": [ { "hooks": [ { diff --git a/integrations/coding-agents/codex/hooks/hooks.json b/integrations/coding-agents/codex/hooks/hooks.json index c25b825e..a027655c 100644 --- a/integrations/coding-agents/codex/hooks/hooks.json +++ b/integrations/coding-agents/codex/hooks/hooks.json @@ -59,8 +59,9 @@ ] } ], - "AfterAgentResponse": [ + "PermissionRequest": [ { + "matcher": "*", "hooks": [ { "type": "command", @@ -70,7 +71,7 @@ ] } ], - "AfterAgentThought": [ + "SubagentStart": [ { "hooks": [ { @@ -81,7 +82,7 @@ ] } ], - "SubagentStart": [ + "SubagentStop": [ { "hooks": [ { @@ -92,7 +93,7 @@ ] } ], - "SubagentStop": [ + "Notification": [ { "hooks": [ { @@ -103,7 +104,7 @@ ] } ], - "Notification": [ + "Stop": [ { "hooks": [ { @@ -114,7 +115,7 @@ ] } ], - "Stop": [ + "PreCompact": [ { "hooks": [ { @@ -125,7 +126,7 @@ ] } ], - "PreCompact": [ + "PostCompact": [ { "hooks": [ { From 17dbf65de7f3186c75585949d07f0dfd8a41b2f0 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 8 May 2026 21:09:54 -0700 Subject: [PATCH 19/27] fix(sidecar): label gateway-first sessions by their real agent, not "gateway" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, when a real agent's gateway request beat its SessionStart hook, `SessionManager::start_llm` created the session with `AgentKind::Gateway`. Observer identities (ATIF agent name, Phoenix scope name) are baked at scope-open time inside `ensure_agent_started`, so the session stayed labeled "gateway" for its entire lifetime even after SessionStart arrived. Result: Phoenix root spans for real Claude/Codex sessions read as `[AGENT] gateway` instead of `[AGENT] claude-code` / `[AGENT] codex`. Two targeted fixes: 1. In `SessionManager::start_llm`, infer the agent kind from the gateway provider when creating a new session: `anthropic.messages` and `anthropic.count_tokens` → `ClaudeCode`; `openai.responses` → `Codex`. Unknown providers (e.g., `openai.chat_completions`, `openai.models`) keep the legacy `Gateway` label so synthetic gateway-only sessions for unattributed proxy traffic remain semantically intact. 2. In `SessionManager::apply_events`, when an `AgentStarted` event arrives for an existing session whose `agent_kind` is still `Gateway` (i.e., the gateway path created the session before the hook), upgrade the session's `agent_kind` to the event's real kind. This fixes session-level metadata for the rare case where provider inference fell back to `Gateway` (e.g., chat-completions traffic) but a SessionStart hook later identifies the agent. Tests cover all three branches: anthropic gateway → claude-code label, openai.responses gateway → codex label, ambiguous provider → gateway label preserved. Signed-off-by: Ajay Thorve --- crates/sidecar/src/session.rs | 45 +++++- .../sidecar/tests/coverage/session_tests.rs | 138 ++++++++++++++++++ 2 files changed, 178 insertions(+), 5 deletions(-) diff --git a/crates/sidecar/src/session.rs b/crates/sidecar/src/session.rs index 053aa406..8be980f4 100644 --- a/crates/sidecar/src/session.rs +++ b/crates/sidecar/src/session.rs @@ -139,6 +139,13 @@ impl SessionManager { /// 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. + /// + /// When an `AgentStarted` event arrives for a session that was already created by the gateway + /// path (i.e., agent_kind is still `Gateway` because an LLM call beat the SessionStart hook), + /// upgrade the session's agent_kind to the real one carried in the event so subsequent + /// metadata reflects the actual agent. Note: agent-scope and observer identities are baked at + /// scope-open time, so this upgrade applies to session metadata only — the + /// provider-inferred kind set in `start_llm` is the primary defense. pub(crate) async fn apply_events( &self, headers: &HeaderMap, @@ -151,9 +158,16 @@ impl SessionManager { continue; } 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()) - }); + let event_kind = event_agent_kind(&event); + let session = sessions + .entry(session_id.clone()) + .or_insert_with(|| Session::new(session_id.clone(), event_kind, config.clone())); + if matches!(&event, NormalizedEvent::AgentStarted(_)) + && session.agent_kind == AgentKind::Gateway + && event_kind != AgentKind::Gateway + { + session.agent_kind = event_kind; + } session.apply(event).await?; if session.agent_scope.is_none() && session.subagents.is_empty() @@ -171,7 +185,13 @@ impl SessionManager { /// /// 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. + /// events. When this path creates a brand-new session (i.e., a real agent's gateway request + /// beat its SessionStart hook), the session's agent_kind is inferred from the gateway provider + /// rather than defaulting to `Gateway`. Without this inference, the session's exported agent + /// name (in ATIF and Phoenix scope spans) would freeze as "gateway" for the lifetime of the + /// session, even after a SessionStart hook arrives, because observer identities are baked at + /// scope-open time. With it, an Anthropic Messages call before SessionStart still labels the + /// trace as `claude-code`, an OpenAI Responses call as `codex`, etc. pub(crate) async fn start_llm( &self, headers: &HeaderMap, @@ -184,9 +204,10 @@ impl SessionManager { .clone() .or_else(|| single_active_session_id(&sessions)) .unwrap_or_else(|| format!("{}-gateway", AgentKind::Gateway.as_str())); + let inferred_agent_kind = agent_kind_for_gateway_provider(&start.provider); let session = sessions .entry(session_id.clone()) - .or_insert_with(|| Session::new(session_id, AgentKind::Gateway, config)); + .or_insert_with(|| Session::new(session_id, inferred_agent_kind, config)); session.start_llm(start).await } @@ -1436,6 +1457,20 @@ fn single_active_session_id(sessions: &HashMap) -> Option AgentKind { + match provider { + "anthropic.messages" | "anthropic.count_tokens" => AgentKind::ClaudeCode, + "openai.responses" => AgentKind::Codex, + _ => AgentKind::Gateway, + } +} + // 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. diff --git a/crates/sidecar/tests/coverage/session_tests.rs b/crates/sidecar/tests/coverage/session_tests.rs index 4b25fbad..c039d0b2 100644 --- a/crates/sidecar/tests/coverage/session_tests.rs +++ b/crates/sidecar/tests/coverage/session_tests.rs @@ -1562,6 +1562,144 @@ fn session_test_config() -> SidecarConfig { } } +// Regression: an Anthropic Messages gateway request that arrives before SessionStart used to +// freeze the session label as "gateway" (default agent_kind) for the rest of the session, +// because observer identities are baked at scope-open time. The session must instead be labeled +// `claude-code` from the provider, so ATIF and Phoenix root spans reflect the real agent. +#[tokio::test] +async fn gateway_first_anthropic_call_labels_session_as_claude_code() { + 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: Some(temp.path().to_path_buf()), + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + let mut start = llm_start(); + start.session_id = Some("claude-uuid".into()); + start.provider = "anthropic.messages".into(); + let active = manager.start_llm(&HeaderMap::new(), start).await.unwrap(); + manager + .end_llm(active, json!({ "ok": true }), json!({})) + .await + .unwrap(); + // Drive an explicit AgentEnded so flush_observers writes ATIF. + manager + .apply_events( + &HeaderMap::new(), + vec![NormalizedEvent::AgentEnded(SessionEvent { + session_id: "claude-uuid".into(), + agent_kind: AgentKind::ClaudeCode, + event_name: "SessionEnd".into(), + payload: json!({}), + metadata: json!({}), + })], + ) + .await + .unwrap(); + + let atif: Value = serde_json::from_str( + &std::fs::read_to_string(temp.path().join("claude-uuid.atif.json")).unwrap(), + ) + .unwrap(); + assert_eq!( + atif["agent"]["name"], + json!("claude-code"), + "session created from anthropic.messages gateway request must be labeled claude-code, not gateway" + ); +} + +// OpenAI Responses gateway requests (codex's API path) must label the session as `codex`. +#[tokio::test] +async fn gateway_first_openai_responses_call_labels_session_as_codex() { + 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: Some(temp.path().to_path_buf()), + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + let mut start = llm_start(); + start.session_id = Some("codex-uuid".into()); + start.provider = "openai.responses".into(); + let active = manager.start_llm(&HeaderMap::new(), start).await.unwrap(); + manager + .end_llm(active, json!({ "ok": true }), json!({})) + .await + .unwrap(); + manager + .apply_events( + &HeaderMap::new(), + vec![NormalizedEvent::AgentEnded(SessionEvent { + session_id: "codex-uuid".into(), + agent_kind: AgentKind::Codex, + event_name: "SessionEnd".into(), + payload: json!({}), + metadata: json!({}), + })], + ) + .await + .unwrap(); + + let atif: Value = serde_json::from_str( + &std::fs::read_to_string(temp.path().join("codex-uuid.atif.json")).unwrap(), + ) + .unwrap(); + assert_eq!(atif["agent"]["name"], json!("codex")); +} + +// Synthetic gateway-only sessions (pure proxy traffic, unknown provider) keep the legacy +// `gateway` label so existing observability semantics for unattributed traffic are preserved. +#[tokio::test] +async fn synthetic_gateway_session_keeps_gateway_label() { + 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: Some(temp.path().to_path_buf()), + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + let mut start = llm_start(); + start.session_id = None; + start.provider = "openai.chat_completions".into(); // ambiguous → Gateway + let active = manager.start_llm(&HeaderMap::new(), start).await.unwrap(); + manager + .end_llm(active, json!({ "ok": true }), json!({})) + .await + .unwrap(); + manager + .apply_events( + &HeaderMap::new(), + vec![NormalizedEvent::AgentEnded(SessionEvent { + session_id: "gateway-gateway".into(), + agent_kind: AgentKind::Gateway, + event_name: "SessionEnd".into(), + payload: json!({}), + metadata: json!({}), + })], + ) + .await + .unwrap(); + + let atif: Value = serde_json::from_str( + &std::fs::read_to_string(temp.path().join("gateway-gateway.atif.json")).unwrap(), + ) + .unwrap(); + assert_eq!(atif["agent"]["name"], json!("gateway")); +} + fn session_event(session_id: &str, event_name: &str) -> SessionEvent { SessionEvent { session_id: session_id.into(), From 5aaccb4be03de65d55eecd42c8d7e0140475a66c Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 8 May 2026 22:01:04 -0700 Subject: [PATCH 20/27] docs(sidecar): address CodeRabbit feedback on codex provider naming and hook flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small fixes from CodeRabbit's pass on PR #63: 1. `Nemo Flow OpenAI` → `NeMo Flow OpenAI` in the codex transparent-run provider config (`launcher.rs:477`) and matching docs in `docs/integrate-frameworks/coding-agent-codex.md` and `integrations/coding-agents/codex/README.md`. Use the official NeMo Flow capitalization in user-facing TOML config and docs. 2. Update codex hook configuration documentation to use the canonical `features.hooks` / `hooks = true` keys instead of the legacy `features.codex_hooks` / `codex_hooks = true`. The legacy spelling is still accepted as an alias on codex 0.129+ but the canonical form is what new users should configure. Retain a one-line note that the old spelling is the legacy alias so users on older codex still understand the deprecation path. Resolves CodeRabbit comments 3211156569, 3211760910, 3211760919, 3211156599. No code behavior changes; 128 sidecar tests still pass. Signed-off-by: Ajay Thorve --- crates/sidecar/src/launcher.rs | 2 +- docs/integrate-frameworks/coding-agent-codex.md | 11 ++++++----- integrations/coding-agents/codex/README.md | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/sidecar/src/launcher.rs b/crates/sidecar/src/launcher.rs index 51a3e2ba..7063730f 100644 --- a/crates/sidecar/src/launcher.rs +++ b/crates/sidecar/src/launcher.rs @@ -474,7 +474,7 @@ async fn wait_for_health(sidecar_url: &str) -> Result<(), SidecarError> { fn codex_sidecar_provider_config(sidecar_url: &str) -> String { format!( - "model_providers.nemo-flow-openai={{name=\"Nemo Flow OpenAI\",base_url={},wire_api=\"responses\",requires_openai_auth=true,supports_websockets=false}}", + "model_providers.nemo-flow-openai={{name=\"NeMo Flow OpenAI\",base_url={},wire_api=\"responses\",requires_openai_auth=true,supports_websockets=false}}", toml_string(sidecar_url) ) } diff --git a/docs/integrate-frameworks/coding-agent-codex.md b/docs/integrate-frameworks/coding-agent-codex.md index 31101633..4933ec39 100644 --- a/docs/integrate-frameworks/coding-agent-codex.md +++ b/docs/integrate-frameworks/coding-agent-codex.md @@ -87,7 +87,7 @@ provider alias instead of overriding the reserved built-in `openai` provider: model_provider = "nemo-flow-openai" [model_providers.nemo-flow-openai] -name = "Nemo Flow OpenAI" +name = "NeMo Flow OpenAI" base_url = "http://127.0.0.1:4040" wire_api = "responses" requires_openai_auth = true @@ -109,9 +109,10 @@ 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`. +sets `features.hooks=true` for that launched process. Persistent install writes +`.codex/config.toml` with `hooks = true` and merges generated hook entries into +`.codex/hooks.json`. (`features.codex_hooks` is the legacy alias of +`features.hooks`; new docs and configurations should prefer the canonical name.) ## Smoke Test @@ -136,7 +137,7 @@ 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 +missing, confirm `hooks = true`, hook config loading, and `--atif-dir` or `NEMO_FLOW_ATIF_DIR`. ## Troubleshoot LLM Lifecycle diff --git a/integrations/coding-agents/codex/README.md b/integrations/coding-agents/codex/README.md index 11b5c837..505ab6cd 100644 --- a/integrations/coding-agents/codex/README.md +++ b/integrations/coding-agents/codex/README.md @@ -31,7 +31,7 @@ The bundle forwards `SessionStart`, `SessionEnd`, `SubagentStart`, 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 +setup writes `hooks = true` in `.codex/config.toml` and merges the hook entries into `.codex/hooks.json`. ## Transparent Setup @@ -101,7 +101,7 @@ provider alias instead of overriding the reserved built-in `openai` provider: model_provider = "nemo-flow-openai" [model_providers.nemo-flow-openai] -name = "Nemo Flow OpenAI" +name = "NeMo Flow OpenAI" base_url = "http://127.0.0.1:4040" wire_api = "responses" requires_openai_auth = true From cd3377bdb6537a5c891d4a3b433e91197d2d24a6 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 8 May 2026 23:09:36 -0700 Subject: [PATCH 21/27] fix(sidecar): snapshot ATIF on per-turn Stop hooks for codex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex 0.129's hook surface has no `SessionEnd`-equivalent event — its closest session-termination signal, `Stop`, fires per-conversation-turn rather than at session end. As a result, codex transparent runs through `nemo-flow-sidecar run -- codex` produced empty `.nemo-flow/atif/` directories: the existing `flush_observers` path (`end_agent` → `write_atif`) never fired. Add a new `NormalizedEvent::TurnEnded` variant. When `classify` sees a `Stop` hook AND the primary classification is not already terminal, it emits both the existing `LlmHint` (preserving correlation behavior for subsequent LLM calls) AND a `TurnEnded` event. `Session::apply` routes `TurnEnded` to a new `snapshot_atif` method that writes the cumulative ATIF without closing the agent scope or shutting down the OpenInference subscriber. The next turn keeps adding events; the final Stop's snapshot is the complete trajectory. The `is_terminal()` guard is critical: Cursor's adapter classifies `stop` as `AgentEnded`, so without the guard we'd emit `[AgentEnded, TurnEnded]` — the first call tears down the session and writes ATIF, the second creates an empty synthetic session and overwrites the freshly-written ATIF with an empty trajectory. Safety verified by reading `AtifExporter`: `export()` takes `&self`, documented as "does not clear the buffered events." Multiple snapshot writes are cumulative supersets, last-write-wins yields the complete trajectory. E2E verified against codex 0.129: - Real codex session writes a 1.6 MB ATIF on Ctrl+D exit (was 0 bytes before) - Multi-turn captured (5 steps: 2 user, 2 agent, 1 system) - `agent.name = "codex"` (from earlier provider-inference fix) - Phoenix root remains `[AGENT] codex` with proper nested LLM/tool spans Hermes is unaffected — it has no `Stop` hook event. Partially closes NMF-91. The remaining cross-process subagent unification gap in NMF-91 stays upstream-blocked: codex spawns subagents as separate codex processes with no `SubagentStart` hook, so each subagent's gateway-only synthetic session is still its own Phoenix trace and its own ATIF (or no ATIF when no Stop fires for it). Tests: - adapter: codex Stop emits TurnEnded - adapter: claude Stop emits TurnEnded - adapter: cursor stop does NOT also emit TurnEnded (regression: would wipe ATIF) - session: TurnEnded snapshots ATIF without closing scope; cumulative across turns - session: TurnEnded is a no-op when the session has no agent scope Signed-off-by: Ajay Thorve --- crates/sidecar/src/adapters/claude_code.rs | 13 +- crates/sidecar/src/adapters/codex.rs | 4 +- crates/sidecar/src/adapters/cursor.rs | 14 +-- crates/sidecar/src/adapters/hermes.rs | 4 +- crates/sidecar/src/adapters/mod.rs | 30 ++++- crates/sidecar/src/model.rs | 9 ++ crates/sidecar/src/session.rs | 23 ++++ .../sidecar/tests/coverage/adapters_tests.rs | 57 +++++++++ .../sidecar/tests/coverage/session_tests.rs | 116 ++++++++++++++++++ .../coding-agent-codex.md | 10 +- 10 files changed, 257 insertions(+), 23 deletions(-) diff --git a/crates/sidecar/src/adapters/claude_code.rs b/crates/sidecar/src/adapters/claude_code.rs index cb4ea2d2..b436b5c8 100644 --- a/crates/sidecar/src/adapters/claude_code.rs +++ b/crates/sidecar/src/adapters/claude_code.rs @@ -15,7 +15,7 @@ use crate::model::{AgentKind, NormalizedEvent}; /// by default. Note: Claude's hook output schema rejects `null` for optional string fields like /// `stopReason`; omit them entirely instead. pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { - let event = classify( + let events = classify( &payload, headers, &ClassificationRules { @@ -37,8 +37,10 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { ], }, ); - let response = match &event { - NormalizedEvent::ToolStarted(_) => json!({ + // Response shape is decided by the primary event (first in the vec); secondary events like + // `TurnEnded` are observability-only and don't influence the hook response Claude gets back. + let response = match events.first() { + Some(NormalizedEvent::ToolStarted(_)) => json!({ "continue": true, "hookSpecificOutput": { "hookEventName": "PreToolUse", @@ -47,8 +49,5 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { }), _ => json!({ "continue": true }), }; - AdapterOutcome { - events: vec![event], - response, - } + AdapterOutcome { events, response } } diff --git a/crates/sidecar/src/adapters/codex.rs b/crates/sidecar/src/adapters/codex.rs index 14a0bc58..755de77c 100644 --- a/crates/sidecar/src/adapters/codex.rs +++ b/crates/sidecar/src/adapters/codex.rs @@ -13,7 +13,7 @@ use crate::model::AgentKind; /// 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( + let events = classify( &payload, headers, &ClassificationRules { @@ -27,7 +27,7 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { }, ); AdapterOutcome { - events: vec![event], + events, response: json!({}), } } diff --git a/crates/sidecar/src/adapters/cursor.rs b/crates/sidecar/src/adapters/cursor.rs index 337a5cfa..1a4bfd5c 100644 --- a/crates/sidecar/src/adapters/cursor.rs +++ b/crates/sidecar/src/adapters/cursor.rs @@ -13,7 +13,7 @@ use crate::model::{AgentKind, NormalizedEvent}; /// 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( + let events = classify( &payload, headers, &ClassificationRules { @@ -31,18 +31,16 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { ], }, ); - let response = match &event { - NormalizedEvent::ToolStarted(_) => json!({ + // Response shape is determined by the primary event (first in the vec). + let response = match events.first() { + Some(NormalizedEvent::ToolStarted(_)) => json!({ "continue": true, "permission": "allow", "user_message": null, "agent_message": null }), - NormalizedEvent::AgentEnded(_) => json!({ "continue": true }), + Some(NormalizedEvent::AgentEnded(_)) => json!({ "continue": true }), _ => json!({ "continue": true }), }; - AdapterOutcome { - events: vec![event], - response, - } + AdapterOutcome { events, response } } diff --git a/crates/sidecar/src/adapters/hermes.rs b/crates/sidecar/src/adapters/hermes.rs index 4fcf2122..38ac43ac 100644 --- a/crates/sidecar/src/adapters/hermes.rs +++ b/crates/sidecar/src/adapters/hermes.rs @@ -39,7 +39,7 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { }; } - let event = classify( + let events = classify( &payload, headers, &ClassificationRules { @@ -53,7 +53,7 @@ pub(crate) fn adapt(payload: Value, headers: &HeaderMap) -> AdapterOutcome { }, ); AdapterOutcome { - events: vec![event], + events, response: json!({}), } } diff --git a/crates/sidecar/src/adapters/mod.rs b/crates/sidecar/src/adapters/mod.rs index 33fd83bc..110a890e 100644 --- a/crates/sidecar/src/adapters/mod.rs +++ b/crates/sidecar/src/adapters/mod.rs @@ -348,10 +348,38 @@ fn value_at(payload: &Value, path: &[&str]) -> Option { Some(current.clone()) } +// Classifies a raw hook event into one or more normalized events. +// +// Most hook events produce a single normalized event from `classify_primary`. The exception is +// `Stop` (Claude/Codex): it emits both the existing `LlmHint` (preserving correlation for +// subsequent LLM calls) AND a `TurnEnded` so the session manager can snapshot ATIF without +// closing the agent scope. Codex 0.129 has no `SessionEnd`-equivalent hook — without this dual +// emission, codex transparent runs would never trigger an ATIF write. +// +// If the primary event is already terminal (e.g., Cursor classifies `stop` as `AgentEnded`), +// the snapshot is skipped to avoid double-writing — `flush_observers` already writes ATIF on +// agent-end, and a follow-up `TurnEnded` on a removed session would recreate an empty session +// and overwrite the freshly-written ATIF with an empty trajectory. +fn classify( + payload: &Value, + headers: &HeaderMap, + rules: &ClassificationRules<'_>, +) -> Vec { + let primary = classify_primary(payload, headers, rules); + let normalized = normalize_name(&event_name(payload)); + if normalized == "stop" && !primary.is_terminal() { + return vec![ + primary, + NormalizedEvent::TurnEnded(common_session_event(payload, headers, rules.kind)), + ]; + } + vec![primary] +} + // 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( +fn classify_primary( payload: &Value, headers: &HeaderMap, rules: &ClassificationRules<'_>, diff --git a/crates/sidecar/src/model.rs b/crates/sidecar/src/model.rs index a21e627e..afddfe5b 100644 --- a/crates/sidecar/src/model.rs +++ b/crates/sidecar/src/model.rs @@ -30,6 +30,13 @@ impl AgentKind { pub(crate) enum NormalizedEvent { AgentStarted(SessionEvent), AgentEnded(SessionEvent), + /// Conversation-turn boundary that the sidecar uses to snapshot ATIF without closing the + /// agent scope. Emitted alongside `LlmHint` for `Stop` hooks (Claude/Codex/Cursor). + /// Required for codex 0.129 transparent runs because codex has no `SessionEnd`-equivalent + /// event — the last `Stop` of the session leaves an up-to-date ATIF on disk. Multi-turn + /// sessions write progressively complete trajectories; the underlying `AtifExporter::export()` + /// is non-destructive so each snapshot is a cumulative superset of prior writes. + TurnEnded(SessionEvent), SubagentStarted(SubagentEvent), SubagentEnded(SubagentEvent), LlmHint(LlmHintEvent), @@ -51,6 +58,7 @@ impl NormalizedEvent { match self { Self::AgentStarted(event) | Self::AgentEnded(event) + | Self::TurnEnded(event) | Self::PromptSubmitted(event) | Self::Compaction(event) | Self::Notification(event) @@ -63,6 +71,7 @@ impl NormalizedEvent { } pub(crate) fn is_terminal(&self) -> bool { + // TurnEnded is intentionally NOT terminal — the agent scope stays open across turns. matches!( self, Self::AgentEnded(_) | Self::SubagentEnded(_) | Self::LlmEnded(_) | Self::ToolEnded(_) diff --git a/crates/sidecar/src/session.rs b/crates/sidecar/src/session.rs index 8be980f4..9a90b63b 100644 --- a/crates/sidecar/src/session.rs +++ b/crates/sidecar/src/session.rs @@ -262,6 +262,11 @@ impl SessionManager { .map(|session| session.llms.is_empty()) .unwrap_or(true) } + + #[cfg(test)] + pub(crate) async fn open_session_count(&self) -> usize { + self.inner.lock().await.len() + } } impl Session { @@ -296,6 +301,7 @@ impl Session { match event { NormalizedEvent::AgentStarted(event) => self.start_agent(event), NormalizedEvent::AgentEnded(event) => self.end_agent(event), + NormalizedEvent::TurnEnded(_) => self.snapshot_atif(), NormalizedEvent::SubagentStarted(event) => self.start_subagent(event), NormalizedEvent::SubagentEnded(event) => self.end_subagent(event), NormalizedEvent::LlmHint(event) => self.add_llm_hint(event), @@ -312,6 +318,22 @@ impl Session { .await } + /// Writes ATIF for the current session without closing the agent scope or shutting observers + /// down. Triggered by `TurnEnded` (per-turn `Stop` hooks). Each turn produces a cumulative + /// snapshot — `AtifExporter::export()` is documented as non-destructive, so subsequent turns + /// add events on top and last-write-wins semantics yield a complete trajectory by the final + /// turn. No-op when `agent_scope` was never opened or when the session has no ATIF observer + /// installed (e.g., `atif_dir` not configured). + fn snapshot_atif(&mut self) -> Result<(), SidecarError> { + if self.agent_scope.is_none() { + return Ok(()); + } + if let (Some(exporter), Some(directory)) = (&self.atif, &self.config.atif_dir) { + write_atif(directory, &self.session_id, exporter)?; + } + Ok(()) + } + // 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. @@ -1436,6 +1458,7 @@ fn event_agent_kind(event: &NormalizedEvent) -> AgentKind { match event { NormalizedEvent::AgentStarted(event) | NormalizedEvent::AgentEnded(event) + | NormalizedEvent::TurnEnded(event) | NormalizedEvent::PromptSubmitted(event) | NormalizedEvent::Compaction(event) | NormalizedEvent::Notification(event) diff --git a/crates/sidecar/tests/coverage/adapters_tests.rs b/crates/sidecar/tests/coverage/adapters_tests.rs index be0444d7..eab766d4 100644 --- a/crates/sidecar/tests/coverage/adapters_tests.rs +++ b/crates/sidecar/tests/coverage/adapters_tests.rs @@ -161,6 +161,63 @@ fn maps_claude_stop_response_shape() { ); } +// Stop hook on Claude/Codex/Cursor (per-turn boundary) must yield a TurnEnded event so the +// session manager can snapshot ATIF without closing the agent scope. Codex needs this because +// it has no SessionEnd hook; Claude/Cursor get it for free for resilience. +#[test] +fn stop_hook_emits_turn_ended_for_codex() { + let outcome = codex::adapt( + json!({ "session_id": "codex-session", "hook_event_name": "Stop" }), + &HeaderMap::new(), + ); + assert!( + outcome + .events + .iter() + .any(|e| matches!(e, NormalizedEvent::TurnEnded(_))), + "codex Stop must produce a TurnEnded event for ATIF snapshot. events: {:?}", + outcome.events + ); +} + +#[test] +fn stop_hook_emits_turn_ended_for_claude() { + let outcome = claude_code::adapt( + json!({ "session_id": "claude-session", "hook_event_name": "Stop" }), + &HeaderMap::new(), + ); + assert!( + outcome + .events + .iter() + .any(|e| matches!(e, NormalizedEvent::TurnEnded(_))), + "claude Stop must produce a TurnEnded event for ATIF snapshot" + ); +} + +// Cursor classifies `stop` as AgentEnded (its existing per-adapter rule). The TurnEnded path +// must NOT also fire there — flush_observers already writes ATIF on agent-end, and a follow-up +// snapshot on a removed session would recreate an empty session and overwrite the freshly +// written file with an empty trajectory. +#[test] +fn stop_hook_does_not_double_emit_for_cursor_agent_end() { + let outcome = cursor::adapt( + json!({ "session_id": "cursor-session", "hook_event_name": "stop" }), + &HeaderMap::new(), + ); + assert!( + matches!(outcome.events.first(), Some(NormalizedEvent::AgentEnded(_))), + "cursor stop must classify as AgentEnded" + ); + assert!( + !outcome + .events + .iter() + .any(|e| matches!(e, NormalizedEvent::TurnEnded(_))), + "cursor stop must NOT also produce TurnEnded — would double-write ATIF then wipe it" + ); +} + #[test] fn adapter_string_lookup_accepts_scalar_values_only() { let payload = json!({ diff --git a/crates/sidecar/tests/coverage/session_tests.rs b/crates/sidecar/tests/coverage/session_tests.rs index c039d0b2..092db274 100644 --- a/crates/sidecar/tests/coverage/session_tests.rs +++ b/crates/sidecar/tests/coverage/session_tests.rs @@ -1700,6 +1700,122 @@ async fn synthetic_gateway_session_keeps_gateway_label() { assert_eq!(atif["agent"]["name"], json!("gateway")); } +// `TurnEnded` (synthesized from per-turn `Stop` hooks) writes ATIF without closing the agent +// scope. This is the codex-0.129 workaround: codex has no `SessionEnd` hook, so per-turn +// snapshots are how its ATIF gets written. After several turns the agent scope must remain open +// and the trajectory file must reflect cumulative state. +#[tokio::test] +async fn turn_ended_snapshots_atif_without_closing_scope() { + 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: Some(temp.path().to_path_buf()), + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + let headers = HeaderMap::new(); + + // Open a codex session. + manager + .apply_events( + &headers, + vec![NormalizedEvent::AgentStarted(SessionEvent { + session_id: "codex-multi-turn".into(), + agent_kind: AgentKind::Codex, + event_name: "SessionStart".into(), + payload: json!({}), + metadata: json!({}), + })], + ) + .await + .unwrap(); + assert_eq!(manager.open_session_count().await, 1); + + // First turn ends — ATIF should be written even though SessionEnd never arrived. + manager + .apply_events( + &headers, + vec![NormalizedEvent::TurnEnded(SessionEvent { + session_id: "codex-multi-turn".into(), + agent_kind: AgentKind::Codex, + event_name: "Stop".into(), + payload: json!({}), + metadata: json!({}), + })], + ) + .await + .unwrap(); + + let atif_path = temp.path().join("codex-multi-turn.atif.json"); + assert!( + atif_path.exists(), + "TurnEnded must produce an ATIF file during an open session" + ); + // Session is still open — TurnEnded must not have torn it down. + assert_eq!( + manager.open_session_count().await, + 1, + "TurnEnded must NOT close the agent scope or remove the session" + ); + + // Second turn ends — file should be overwritten with a cumulative trajectory. + manager + .apply_events( + &headers, + vec![NormalizedEvent::TurnEnded(SessionEvent { + session_id: "codex-multi-turn".into(), + agent_kind: AgentKind::Codex, + event_name: "Stop".into(), + payload: json!({}), + metadata: json!({}), + })], + ) + .await + .unwrap(); + assert!(atif_path.exists()); + assert_eq!(manager.open_session_count().await, 1); + + let trajectory: Value = serde_json::from_slice(&std::fs::read(&atif_path).unwrap()).unwrap(); + assert_eq!(trajectory["session_id"], json!("codex-multi-turn")); + assert_eq!(trajectory["agent"]["name"], json!("codex")); +} + +// TurnEnded for a session that was never opened (no AgentStarted, no gateway LLM) is a no-op — +// no observers were ever installed, so there's nothing to flush. +#[tokio::test] +async fn turn_ended_is_noop_for_session_with_no_agent_scope() { + 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: Some(temp.path().to_path_buf()), + openinference_endpoint: None, + metadata: None, + plugin_config: None, + }; + let manager = SessionManager::new(config); + manager + .apply_events( + &HeaderMap::new(), + vec![NormalizedEvent::TurnEnded(SessionEvent { + session_id: "no-agent".into(), + agent_kind: AgentKind::Codex, + event_name: "Stop".into(), + payload: json!({}), + metadata: json!({}), + })], + ) + .await + .unwrap(); + // No file should be created — the snapshot needs an active session with installed observers. + assert!(std::fs::read_dir(temp.path()).unwrap().next().is_none()); +} + fn session_event(session_id: &str, event_name: &str) -> SessionEvent { SessionEvent { session_id: session_id.into(), diff --git a/docs/integrate-frameworks/coding-agent-codex.md b/docs/integrate-frameworks/coding-agent-codex.md index 4933ec39..704d2caa 100644 --- a/docs/integrate-frameworks/coding-agent-codex.md +++ b/docs/integrate-frameworks/coding-agent-codex.md @@ -136,9 +136,13 @@ End the Codex session and confirm ATIF exists: ls .nemo-flow/atif ``` -The sidecar writes `.atif.json` on session end. If the file is -missing, confirm `hooks = true`, hook config loading, and `--atif-dir` or -`NEMO_FLOW_ATIF_DIR`. +The sidecar writes `.atif.json` after every conversation turn for +Codex sessions (Codex's hook surface has no `SessionEnd`-equivalent event, so +the sidecar uses each per-turn `Stop` hook to snapshot the trajectory; the file +grows cumulatively across turns and the final write reflects the full session). +For agents that do emit a session-end hook, the same file is written once on +session close. If the file is missing, confirm `hooks = true`, hook config +loading, and `--atif-dir` or `NEMO_FLOW_ATIF_DIR`. ## Troubleshoot LLM Lifecycle From 45a3277d13c6b2c8123fff8ce499be0f347354ad Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 8 May 2026 20:02:28 -0700 Subject: [PATCH 22/27] feat(core): add StreamingCodec trait and SSE event decoder Defines the streaming counterpart to LlmResponseCodec, used with llm_stream_call_execute. Each provider's StreamingCodec impl owns the incremental state needed to assemble per-chunk events into a single non-streaming-shape JSON payload, which the matching LlmResponseCodec then decodes into AnnotatedLlmResponse. Per Will's design suggestion in NMF-90: trait StreamingCodec { fn collector(&self) -> LlmCollectorFn; fn finalizer(&self) -> LlmFinalizerFn; } Includes an SseEventDecoder utility that incrementally splits a text/event-stream byte stream into parsed SSE events. Anthropic Messages, OpenAI Responses, and OpenAI Chat Completions all emit one JSON object per data: line, so the decoder is shared infrastructure for the per-provider StreamingCodec impls landing in follow-up commits. Refs NMF-90. Signed-off-by: Ajay Thorve --- crates/core/src/codec/mod.rs | 8 +- crates/core/src/codec/streaming.rs | 209 +++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 crates/core/src/codec/streaming.rs diff --git a/crates/core/src/codec/mod.rs b/crates/core/src/codec/mod.rs index 3088fc01..82e46198 100644 --- a/crates/core/src/codec/mod.rs +++ b/crates/core/src/codec/mod.rs @@ -4,13 +4,17 @@ //! LLM codec types, traits, and built-in implementations. //! //! This module provides the type system and traits for bidirectional -//! request codecs ([`traits::LlmCodec`] / [`request::AnnotatedLlmRequest`]) and +//! request codecs ([`traits::LlmCodec`] / [`request::AnnotatedLlmRequest`]), //! the decode-only response codec -//! ([`traits::LlmResponseCodec`] / [`response::AnnotatedLlmResponse`]). +//! ([`traits::LlmResponseCodec`] / [`response::AnnotatedLlmResponse`]), and +//! the streaming response codec +//! ([`streaming::StreamingCodec`]) used with the managed +//! streaming LLM execution pipeline. pub mod anthropic; pub mod openai_chat; pub mod openai_responses; pub mod request; pub mod response; +pub mod streaming; pub mod traits; diff --git a/crates/core/src/codec/streaming.rs b/crates/core/src/codec/streaming.rs new file mode 100644 index 00000000..c9c29881 --- /dev/null +++ b/crates/core/src/codec/streaming.rs @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Streaming response codecs for the managed LLM execution pipeline. +//! +//! [`LlmResponseCodec`] (in [`crate::codec::traits`]) decodes a complete provider response into a +//! normalized [`AnnotatedLlmResponse`]. For streaming providers, the analogous job is to: +//! +//! 1. consume per-chunk events as they arrive on a streaming HTTP response, and +//! 2. assemble a single non-streaming-shape JSON payload at end of stream. +//! +//! Once assembled, the payload can be fed back through the matching [`LlmResponseCodec`] to produce +//! an [`AnnotatedLlmResponse`] — meaning streaming and non-streaming requests converge on the same +//! observability output without per-route shape duplication. +//! +//! [`StreamingCodec`] is the trait that bundles the two functions +//! ([`LlmCollectorFn`](crate::api::runtime::LlmCollectorFn), +//! [`LlmFinalizerFn`](crate::api::runtime::LlmFinalizerFn)) used by +//! [`crate::api::llm::llm_stream_call_execute`]. Each provider supplies one impl whose internal +//! state holds whatever incremental information is needed to materialize the final payload. +//! +//! [`AnnotatedLlmResponse`]: crate::codec::response::AnnotatedLlmResponse + +use crate::api::runtime::{LlmCollectorFn, LlmFinalizerFn}; +use crate::error::{FlowError, Result}; +use crate::json::Json; + +/// Per-provider streaming codec used with [`crate::api::llm::llm_stream_call_execute`]. +/// +/// `collector()` and `finalizer()` produce owned closures that share the codec's internal +/// accumulation state. Implementations typically wrap that state in `Arc>` so each +/// `&self`-produced closure captures a clone of the handle. +/// +/// [`LlmFinalizerFn`] is `FnOnce`, so a [`StreamingCodec`] instance is single-use: callers +/// construct a fresh instance per managed-lifecycle call and discard it after the stream +/// completes. +pub trait StreamingCodec: Send + Sync { + /// Returns a closure that consumes one decoded provider event per call. + fn collector(&self) -> LlmCollectorFn; + + /// Returns a closure that, when called once at end of stream, produces the assembled response + /// payload in the shape the matching [`crate::codec::traits::LlmResponseCodec`] can decode. + fn finalizer(&self) -> LlmFinalizerFn; +} + +/// Incremental decoder for `text/event-stream` byte streams that yields one JSON object per +/// complete `data:` payload. +/// +/// SSE frames are separated by blank lines (`\n\n`); each frame may contain `event:` and `data:` +/// lines. Anthropic Messages, OpenAI Responses, and OpenAI Chat Completions all emit one JSON +/// object per `data:` line, so the decoder buffers received bytes, splits on frame boundaries, +/// parses the JSON payload, and tags it with the frame's event name when present. +/// +/// The decoder is byte-stream-friendly: it accumulates partial frames across chunks and emits +/// completed frames only when their terminating blank line arrives. Bytes after the last +/// terminator are retained for the next call. +#[derive(Default)] +pub struct SseEventDecoder { + buffer: String, +} + +/// One decoded SSE frame, paired with the parsed `data:` payload. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SseEvent { + /// Value of the `event:` line if present. + pub event: Option, + /// Parsed JSON payload from the `data:` line(s). + pub data: Json, +} + +impl SseEventDecoder { + /// Creates a new decoder with an empty buffer. + pub fn new() -> Self { + Self::default() + } + + /// Appends `bytes` to the internal buffer and returns every now-complete SSE event. + /// + /// Bytes are interpreted as UTF-8 with replacement characters for invalid sequences; provider + /// SSE streams are well-formed UTF-8 in practice, but lossy decoding keeps the decoder honest + /// rather than failing on a single corrupt chunk. + /// + /// Returns `Ok(events)` containing zero or more events whose `data:` payloads parsed + /// successfully. Frames whose `data:` line is non-empty but does not parse as JSON are + /// surfaced as [`FlowError::Internal`] so the caller can decide whether to abort the stream + /// or skip the frame; frames with no `data:` line at all (e.g. SSE heartbeats) are silently + /// dropped. + pub fn push_bytes(&mut self, bytes: &[u8]) -> Result> { + // Normalize CRLF to LF on append so the framing search only needs to find `\n\n`. Some + // providers emit mixed line endings on the wire; normalizing once here keeps the inner + // loop cheap. + let chunk = String::from_utf8_lossy(bytes).replace("\r\n", "\n"); + self.buffer.push_str(&chunk); + let mut events = Vec::new(); + while let Some(cut) = self.buffer.find("\n\n") { + let frame: String = self.buffer.drain(..cut).collect(); + // Drop the `\n\n` terminator itself. + self.buffer.drain(..2); + if let Some(event) = parse_sse_frame(&frame)? { + events.push(event); + } + } + Ok(events) + } + + /// Drains any remaining buffered frame at end of stream. + /// + /// Most well-formed SSE streams end with a terminating blank line, in which case this returns + /// `Ok(None)`. Stops with no terminator are surfaced as a final partial frame so observability + /// captures the last bytes the upstream sent before disconnect. + pub fn finish(mut self) -> Result> { + let trailing = std::mem::take(&mut self.buffer); + if trailing.trim().is_empty() { + Ok(None) + } else { + parse_sse_frame(&trailing) + } + } +} + +// Parses a single SSE frame. Returns `None` for frames without a `data:` line, `Some(event)` for +// frames whose `data:` JSON parsed successfully. +fn parse_sse_frame(frame: &str) -> Result> { + let mut event_name: Option = None; + let mut data_parts: Vec<&str> = Vec::new(); + for line in frame.split('\n') { + if let Some(rest) = line.strip_prefix("event:") { + event_name = Some(rest.trim().to_string()); + } else if let Some(rest) = line.strip_prefix("data:") { + // SSE allows a single space after the colon by convention; strip it lazily. + data_parts.push(rest.strip_prefix(' ').unwrap_or(rest)); + } + // Other lines (`id:`, `retry:`, comments starting with `:`) are ignored. + } + if data_parts.is_empty() { + return Ok(None); + } + let payload = data_parts.join("\n"); + let data: Json = serde_json::from_str(payload.trim()).map_err(|error| { + FlowError::Internal(format!( + "streaming codec failed to parse SSE data payload: {error}: {payload}" + )) + })?; + Ok(Some(SseEvent { + event: event_name, + data, + })) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn decodes_complete_frames_in_one_push() { + let mut decoder = SseEventDecoder::new(); + let events = decoder + .push_bytes(b"event: ping\ndata: {\"type\":\"ping\"}\n\nevent: msg\ndata: {\"text\":\"hi\"}\n\n") + .unwrap(); + assert_eq!(events.len(), 2); + assert_eq!(events[0].event.as_deref(), Some("ping")); + assert_eq!(events[0].data, json!({"type": "ping"})); + assert_eq!(events[1].event.as_deref(), Some("msg")); + assert_eq!(events[1].data, json!({"text": "hi"})); + } + + #[test] + fn buffers_partial_frames_across_pushes() { + let mut decoder = SseEventDecoder::new(); + assert!(decoder.push_bytes(b"event: m\ndata: ").unwrap().is_empty()); + assert!(decoder.push_bytes(b"{\"a\":1").unwrap().is_empty()); + let events = decoder.push_bytes(b"}\n\n").unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0].data, json!({"a": 1})); + } + + #[test] + fn drops_frames_without_data_lines() { + let mut decoder = SseEventDecoder::new(); + // A heartbeat-style comment frame plus a real one. + let events = decoder + .push_bytes(b": keepalive\n\nevent: real\ndata: {\"v\":2}\n\n") + .unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0].data, json!({"v": 2})); + } + + #[test] + fn surfaces_final_partial_frame_on_finish() { + let mut decoder = SseEventDecoder::new(); + decoder + .push_bytes(b"event: tail\ndata: {\"end\":true}") + .unwrap(); + let trailing = decoder.finish().unwrap().expect("trailing frame present"); + assert_eq!(trailing.data, json!({"end": true})); + } + + #[test] + fn surfaces_parse_errors_with_payload_context() { + let mut decoder = SseEventDecoder::new(); + let error = decoder + .push_bytes(b"event: bad\ndata: {not valid json}\n\n") + .unwrap_err(); + let message = error.to_string(); + assert!(message.contains("SSE data payload"), "{message}"); + assert!(message.contains("not valid json"), "{message}"); + } +} From 21466e26f8892b5f25e151245081c271142a7dfb Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 8 May 2026 21:05:54 -0700 Subject: [PATCH 23/27] feat(core): AnthropicMessagesStreamingCodec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements StreamingCodec for the Anthropic Messages SSE event sequence. The collector observes message_start, content_block_start/delta/stop, message_delta, and message_stop events, accumulating per-block state (text, tool input partial_json, citations) keyed by content_block index. The finalizer produces a JSON object wire-compatible with RawAnthropicResponse — the same shape Anthropic returns for a non-streaming Messages request — so the assembled output can be fed back through AnthropicMessagesCodec::decode_response to produce the canonical AnnotatedLlmResponse. Streaming and non-streaming Anthropic requests now converge on the same observability output without per-route shape duplication. Internal state lives behind Arc> so the &self-produced collector and finalizer closures share access. Each instance is single-use because LlmFinalizerFn consumes the finalize step. Tests cover: - text-only response with usage and stop_reason mapping - tool_use input assembled from partial_json fragments - web_search_tool_result blocks (full content delivered at start) - text blocks with citations_delta accumulation - truncated streams falling back to raw partial_json string Refs NMF-90. Signed-off-by: Ajay Thorve --- crates/core/src/codec/anthropic.rs | 262 ++++++++++++++++++ .../core/tests/unit/codec/anthropic_tests.rs | 255 +++++++++++++++++ 2 files changed, 517 insertions(+) diff --git a/crates/core/src/codec/anthropic.rs b/crates/core/src/codec/anthropic.rs index 8ffb83ee..4380ec32 100644 --- a/crates/core/src/codec/anthropic.rs +++ b/crates/core/src/codec/anthropic.rs @@ -509,6 +509,268 @@ impl LlmCodec for AnthropicMessagesCodec { } } +// --------------------------------------------------------------------------- +// Streaming codec +// --------------------------------------------------------------------------- + +/// Streaming counterpart to [`AnthropicMessagesCodec`]. +/// +/// Replays the Anthropic Messages SSE event sequence into the same JSON shape Anthropic returns +/// for a non-streaming request (`{id, type, role, model, content, stop_reason, stop_sequence, +/// usage}`). Once finalized, the assembled JSON can be fed back through +/// [`AnthropicMessagesCodec::decode_response`] to produce an +/// [`AnnotatedLlmResponse`](crate::codec::response::AnnotatedLlmResponse) — meaning streaming and +/// non-streaming Anthropic requests converge on the same observability output. +/// +/// Internal state lives behind `Arc>` so the `&self`-produced collector and finalizer +/// closures share access. Each instance is single-use because [`LlmFinalizerFn`] consumes the +/// finalize step. +/// +/// [`LlmFinalizerFn`]: crate::api::runtime::LlmFinalizerFn +pub struct AnthropicMessagesStreamingCodec { + state: std::sync::Arc>, +} + +impl AnthropicMessagesStreamingCodec { + /// Creates a fresh streaming codec with empty accumulator state. + pub fn new() -> Self { + Self { + state: std::sync::Arc::new(std::sync::Mutex::new( + AnthropicMessagesStreamingState::default(), + )), + } + } +} + +impl Default for AnthropicMessagesStreamingCodec { + fn default() -> Self { + Self::new() + } +} + +impl super::streaming::StreamingCodec for AnthropicMessagesStreamingCodec { + fn collector(&self) -> crate::api::runtime::LlmCollectorFn { + let state = std::sync::Arc::clone(&self.state); + Box::new(move |event: Json| -> Result<()> { + let mut guard = state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + guard.observe(&event); + Ok(()) + }) + } + + fn finalizer(&self) -> crate::api::runtime::LlmFinalizerFn { + let state = std::sync::Arc::clone(&self.state); + Box::new(move || -> Json { + let mut guard = state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + // Move state out so finalize can consume it; the codec is single-use, so leaving a + // default behind is intentional and never observed by another caller. + std::mem::take(&mut *guard).finalize() + }) + } +} + +#[derive(Debug, Default)] +struct AnthropicMessagesStreamingState { + id: Option, + type_: Option, + role: Option, + model: Option, + /// Latest usage snapshot. `message_start` carries an initial value (input tokens, zero output + /// so far); `message_delta` updates it cumulatively. Last write wins. + usage: Option, + stop_reason: Option, + /// Stored as raw `Json` to preserve `null` (Anthropic's wire shape) versus omitted. + stop_sequence: Option, + /// Indexed by the SSE event's `index` field. `None` slots accommodate sparse indices though + /// Anthropic emits them in order today. + blocks: Vec>, +} + +#[derive(Debug, Default, Clone)] +struct StreamingBlock { + /// The `content_block` JSON captured at `content_block_start`. Deltas mutate fields directly + /// for blocks Anthropic delivers incrementally (text, tool_use input, citations); other block + /// types (server_tool_use results) ship complete at start and pass through unchanged. + skeleton: serde_json::Map, + text: String, + has_text: bool, + partial_json: String, + has_partial_json: bool, + citations: Vec, + has_citations: bool, +} + +impl AnthropicMessagesStreamingState { + fn observe(&mut self, event: &Json) { + let event_type = event.get("type").and_then(Json::as_str).unwrap_or(""); + match event_type { + "message_start" => self.observe_message_start(event), + "content_block_start" => self.observe_content_block_start(event), + "content_block_delta" => self.observe_content_block_delta(event), + "message_delta" => self.observe_message_delta(event), + // content_block_stop, message_stop, ping, and any unknown event type carry no + // accumulator-relevant payload. Unknown types are ignored rather than erroring so a + // future Anthropic event addition does not break observability. + _ => {} + } + } + + fn observe_message_start(&mut self, event: &Json) { + let Some(message) = event.get("message") else { + return; + }; + if let Some(id) = message.get("id").and_then(Json::as_str) { + self.id = Some(id.to_string()); + } + if let Some(model) = message.get("model").and_then(Json::as_str) { + self.model = Some(model.to_string()); + } + if let Some(role) = message.get("role").and_then(Json::as_str) { + self.role = Some(role.to_string()); + } + if let Some(t) = message.get("type").and_then(Json::as_str) { + self.type_ = Some(t.to_string()); + } + if let Some(usage) = message.get("usage") { + self.usage = Some(usage.clone()); + } + } + + fn observe_content_block_start(&mut self, event: &Json) { + let Some(index) = event.get("index").and_then(Json::as_u64) else { + return; + }; + let Some(content_block) = event.get("content_block") else { + return; + }; + let skeleton = match content_block { + Json::Object(map) => map.clone(), + _ => return, + }; + let index = index as usize; + while self.blocks.len() <= index { + self.blocks.push(None); + } + self.blocks[index] = Some(StreamingBlock { + skeleton, + ..StreamingBlock::default() + }); + } + + fn observe_content_block_delta(&mut self, event: &Json) { + let Some(index) = event.get("index").and_then(Json::as_u64) else { + return; + }; + let index = index as usize; + let Some(delta) = event.get("delta") else { + return; + }; + let delta_type = delta.get("type").and_then(Json::as_str).unwrap_or(""); + let Some(slot) = self.blocks.get_mut(index) else { + return; + }; + let Some(block) = slot.as_mut() else { return }; + match delta_type { + "text_delta" => { + if let Some(text) = delta.get("text").and_then(Json::as_str) { + block.text.push_str(text); + block.has_text = true; + } + } + "input_json_delta" => { + if let Some(partial) = delta.get("partial_json").and_then(Json::as_str) { + block.partial_json.push_str(partial); + block.has_partial_json = true; + } + } + "citations_delta" => { + if let Some(citation) = delta.get("citation") { + block.citations.push(citation.clone()); + block.has_citations = true; + } + } + // thinking_delta, signature_delta, and any future delta types fall through; the block + // skeleton retains whatever shape was set at content_block_start. + _ => {} + } + } + + fn observe_message_delta(&mut self, event: &Json) { + if let Some(delta) = event.get("delta") { + if let Some(reason) = delta.get("stop_reason").and_then(Json::as_str) { + self.stop_reason = Some(reason.to_string()); + } + if let Some(seq) = delta.get("stop_sequence") { + self.stop_sequence = Some(seq.clone()); + } + } + if let Some(usage) = event.get("usage") { + self.usage = Some(usage.clone()); + } + } + + fn finalize(self) -> Json { + let mut output = serde_json::Map::new(); + if let Some(id) = self.id { + output.insert("id".to_string(), Json::String(id)); + } + if let Some(t) = self.type_ { + output.insert("type".to_string(), Json::String(t)); + } + if let Some(role) = self.role { + output.insert("role".to_string(), Json::String(role)); + } + if let Some(model) = self.model { + output.insert("model".to_string(), Json::String(model)); + } + let content: Vec = self + .blocks + .into_iter() + .filter_map(|block| block.map(StreamingBlock::finalize)) + .collect(); + output.insert("content".to_string(), Json::Array(content)); + if let Some(reason) = self.stop_reason { + output.insert("stop_reason".to_string(), Json::String(reason)); + } + if let Some(seq) = self.stop_sequence { + output.insert("stop_sequence".to_string(), seq); + } + if let Some(usage) = self.usage { + output.insert("usage".to_string(), usage); + } + Json::Object(output) + } +} + +impl StreamingBlock { + fn finalize(mut self) -> Json { + if self.has_text { + self.skeleton + .insert("text".to_string(), Json::String(self.text)); + } + if self.has_partial_json { + // Concatenated `partial_json` fragments are expected to parse as a JSON object — that's + // the assembled tool input. If parsing fails (Anthropic emits malformed deltas, stream + // truncated mid-block), surface the raw concatenation so observability still captures + // something rather than dropping the call. + let parsed = match serde_json::from_str::(&self.partial_json) { + Ok(value) => value, + Err(_) => Json::String(self.partial_json), + }; + self.skeleton.insert("input".to_string(), parsed); + } + if self.has_citations { + self.skeleton + .insert("citations".to_string(), Json::Array(self.citations)); + } + Json::Object(self.skeleton) + } +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- diff --git a/crates/core/tests/unit/codec/anthropic_tests.rs b/crates/core/tests/unit/codec/anthropic_tests.rs index e814bcae..796bd96d 100644 --- a/crates/core/tests/unit/codec/anthropic_tests.rs +++ b/crates/core/tests/unit/codec/anthropic_tests.rs @@ -756,3 +756,258 @@ fn test_encode_tool_choice_specific_to_anthropic() { Some(&json!({"type": "tool", "name": "my_func"})) ); } + +// =================================================================== +// Streaming codec tests +// =================================================================== + +use super::super::streaming::StreamingCodec; + +#[test] +fn anthropic_streaming_codec_assembles_text_response() { + let codec = AnthropicMessagesStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + collector(json!({ + "type": "message_start", + "message": { + "id": "msg_01ABC", + "type": "message", + "role": "assistant", + "model": "claude-haiku-4-5-20251001", + "content": [], + "stop_reason": null, + "stop_sequence": null, + "usage": {"input_tokens": 100, "output_tokens": 0} + } + })) + .unwrap(); + collector(json!({ + "type": "content_block_start", + "index": 0, + "content_block": {"type": "text", "text": ""} + })) + .unwrap(); + collector(json!({ + "type": "content_block_delta", + "index": 0, + "delta": {"type": "text_delta", "text": "Hello, "} + })) + .unwrap(); + collector(json!({ + "type": "content_block_delta", + "index": 0, + "delta": {"type": "text_delta", "text": "world."} + })) + .unwrap(); + collector(json!({"type": "content_block_stop", "index": 0})).unwrap(); + collector(json!({ + "type": "message_delta", + "delta": {"stop_reason": "end_turn", "stop_sequence": null}, + "usage": {"input_tokens": 100, "output_tokens": 5} + })) + .unwrap(); + collector(json!({"type": "message_stop"})).unwrap(); + + let assembled = finalizer(); + // Wire-compatible with RawAnthropicResponse — feed it back through the existing decoder. + let annotated = AnthropicMessagesCodec + .decode_response(&assembled) + .expect("assembled response should decode"); + assert_eq!(annotated.id.as_deref(), Some("msg_01ABC")); + assert_eq!( + annotated.model.as_deref(), + Some("claude-haiku-4-5-20251001") + ); + assert_eq!(annotated.finish_reason, Some(FinishReason::Complete)); + assert_eq!( + annotated.message, + Some(MessageContent::Text("Hello, world.".to_string())) + ); + let usage = annotated.usage.as_ref().unwrap(); + assert_eq!(usage.prompt_tokens, Some(100)); + assert_eq!(usage.completion_tokens, Some(5)); +} + +#[test] +fn anthropic_streaming_codec_assembles_tool_use_input_from_partial_json() { + let codec = AnthropicMessagesStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + collector(json!({ + "type": "message_start", + "message": { + "id": "msg_tool", + "type": "message", + "role": "assistant", + "model": "claude-haiku-4-5-20251001", + "content": [], + "usage": {"input_tokens": 50, "output_tokens": 0} + } + })) + .unwrap(); + collector(json!({ + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "tool_use", + "id": "toolu_01", + "name": "lookup", + "input": {} + } + })) + .unwrap(); + for fragment in &["{\"q", "uery\":", " \"weath", "er\"}"] { + collector(json!({ + "type": "content_block_delta", + "index": 0, + "delta": {"type": "input_json_delta", "partial_json": fragment} + })) + .unwrap(); + } + collector(json!({"type": "content_block_stop", "index": 0})).unwrap(); + collector(json!({ + "type": "message_delta", + "delta": {"stop_reason": "tool_use"}, + "usage": {"input_tokens": 50, "output_tokens": 12} + })) + .unwrap(); + + let assembled = finalizer(); + let annotated = AnthropicMessagesCodec + .decode_response(&assembled) + .expect("assembled response should decode"); + assert_eq!(annotated.finish_reason, Some(FinishReason::ToolUse)); + let tool_calls = annotated.tool_calls.expect("tool_calls present"); + assert_eq!(tool_calls.len(), 1); + assert_eq!(tool_calls[0].id, "toolu_01"); + assert_eq!(tool_calls[0].name, "lookup"); + assert_eq!(tool_calls[0].arguments, json!({"query": "weather"})); +} + +#[test] +fn anthropic_streaming_codec_preserves_unknown_block_types() { + // Server-side tool blocks (web_search_tool_result) ship full content at content_block_start + // and have no deltas; the codec must preserve them in the assembled content array. + let codec = AnthropicMessagesStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + collector(json!({ + "type": "message_start", + "message": { + "id": "msg_ws", + "type": "message", + "role": "assistant", + "model": "claude-haiku-4-5-20251001", + "usage": {"input_tokens": 1, "output_tokens": 0} + } + })) + .unwrap(); + collector(json!({ + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "web_search_tool_result", + "tool_use_id": "srvtoolu_42", + "content": [ + {"type": "web_search_result", "title": "First", "url": "https://a"}, + {"type": "web_search_result", "title": "Second", "url": "https://b"} + ] + } + })) + .unwrap(); + collector(json!({"type": "content_block_stop", "index": 0})).unwrap(); + + let assembled = finalizer(); + let block = &assembled["content"][0]; + assert_eq!(block["type"], json!("web_search_tool_result")); + assert_eq!(block["tool_use_id"], json!("srvtoolu_42")); + assert_eq!(block["content"][0]["title"], json!("First")); + assert_eq!(block["content"][1]["url"], json!("https://b")); +} + +#[test] +fn anthropic_streaming_codec_attaches_citations_to_text_blocks() { + let codec = AnthropicMessagesStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + collector(json!({ + "type": "message_start", + "message": { + "id": "msg_c", "type": "message", "role": "assistant", + "model": "claude-haiku-4-5-20251001", "usage": {} + } + })) + .unwrap(); + collector(json!({ + "type": "content_block_start", + "index": 0, + "content_block": {"type": "text", "text": ""} + })) + .unwrap(); + collector(json!({ + "type": "content_block_delta", + "index": 0, + "delta": {"type": "citations_delta", "citation": { + "type": "web_search_result_location", + "cited_text": "Hello", + "url": "https://example.com", + "title": "Source" + }} + })) + .unwrap(); + collector(json!({ + "type": "content_block_delta", + "index": 0, + "delta": {"type": "text_delta", "text": "Hello"} + })) + .unwrap(); + + let assembled = finalizer(); + let block = &assembled["content"][0]; + assert_eq!(block["text"], json!("Hello")); + let citations = block["citations"].as_array().expect("citations array"); + assert_eq!(citations.len(), 1); + assert_eq!(citations[0]["url"], json!("https://example.com")); +} + +#[test] +fn anthropic_streaming_codec_keeps_partial_json_when_unparseable() { + // Truncated stream: input_json_delta fragments don't form valid JSON. Codec must not drop + // the tool_use block; surface the raw concatenation as a string fallback so observability + // captures partial intent. + let codec = AnthropicMessagesStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + collector(json!({ + "type": "message_start", + "message": { + "id": "msg_p", "type": "message", "role": "assistant", + "model": "claude-haiku-4-5-20251001", "usage": {} + } + })) + .unwrap(); + collector(json!({ + "type": "content_block_start", + "index": 0, + "content_block": {"type": "tool_use", "id": "toolu_p", "name": "go", "input": {}} + })) + .unwrap(); + collector(json!({ + "type": "content_block_delta", + "index": 0, + "delta": {"type": "input_json_delta", "partial_json": "{\"q\": \"trun"} + })) + .unwrap(); + + let assembled = finalizer(); + let block = &assembled["content"][0]; + assert_eq!(block["type"], json!("tool_use")); + assert_eq!(block["id"], json!("toolu_p")); + assert_eq!(block["input"], json!("{\"q\": \"trun")); +} From d3676e46d3d000d8acbaa8e6a3930c20d9eace8b Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 8 May 2026 21:13:14 -0700 Subject: [PATCH 24/27] feat(core): OpenAIResponsesStreamingCodec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements StreamingCodec for the OpenAI Responses SSE event sequence. The Responses API streams either complete `response` snapshots (response.created, response.in_progress, response.completed, response.failed, response.incomplete) or final-state output items (response.output_item.done), so the codec: 1. Tracks the latest response snapshot — terminal events typically carry the complete state including `output`. 2. Tracks output items by output_index from output_item.done events, used as a fallback when the terminal snapshot lacks `output`. 3. Ignores per-token deltas (output_text.delta, function_call_arguments.delta) because their content is redelivered in the matching output_item.done. The finalizer produces a JSON object wire-compatible with RawResponsesResponse — feed it back through OpenAIResponsesCodec:: decode_response to produce the canonical AnnotatedLlmResponse. Tests cover: - terminal response.completed carries full state (common case) - terminal completed lacks output → fall back to output_item.done items - response.incomplete with reason maps to FinishReason::Length - per-token deltas don't double-accumulate Refs NMF-90. Signed-off-by: Ajay Thorve --- crates/core/src/codec/openai_responses.rs | 144 +++++++++++++ .../unit/codec/openai_responses_tests.rs | 192 ++++++++++++++++++ 2 files changed, 336 insertions(+) diff --git a/crates/core/src/codec/openai_responses.rs b/crates/core/src/codec/openai_responses.rs index 16e44cfb..62691df0 100644 --- a/crates/core/src/codec/openai_responses.rs +++ b/crates/core/src/codec/openai_responses.rs @@ -427,6 +427,150 @@ impl LlmCodec for OpenAIResponsesCodec { } } +// --------------------------------------------------------------------------- +// Streaming codec +// --------------------------------------------------------------------------- + +/// Streaming counterpart to [`OpenAIResponsesCodec`]. +/// +/// Replays the OpenAI Responses SSE event sequence into the same JSON shape the API returns for a +/// non-streaming request (`{id, model, status, output, usage, incomplete_details, ...}`). Once +/// finalized, the assembled JSON can be fed back through [`OpenAIResponsesCodec::decode_response`] +/// to produce the canonical [`AnnotatedLlmResponse`]. +/// +/// # Strategy +/// +/// The Responses API is a relatively forgiving streaming target because every event carries +/// either the full `response` snapshot (`response.created`, `response.in_progress`, +/// `response.completed`, `response.failed`, `response.incomplete`) or the final-state output item +/// (`response.output_item.done`). We: +/// +/// 1. Track the latest `response` snapshot — terminal events (`completed`/`failed`/`incomplete`) +/// typically carry the complete state including `output`, so we prefer those when present. +/// 2. Track output items by `output_index` — `output_item.done` events deliver the final per-item +/// state, used as a fallback when the terminal `response.output` is missing or empty. +/// 3. Per-token `output_text.delta` and `function_call_arguments.delta` events are ignored +/// because their content is redelivered in the matching `output_item.done` event. Skipping +/// deltas keeps the codec resilient to schema additions and avoids double-accumulation. +/// +/// Internal state lives behind `Arc>` so the `&self`-produced collector and finalizer +/// closures share access. Each instance is single-use because [`LlmFinalizerFn`] consumes the +/// finalize step. +/// +/// [`AnnotatedLlmResponse`]: crate::codec::response::AnnotatedLlmResponse +/// [`LlmFinalizerFn`]: crate::api::runtime::LlmFinalizerFn +pub struct OpenAIResponsesStreamingCodec { + state: std::sync::Arc>, +} + +impl OpenAIResponsesStreamingCodec { + /// Creates a fresh streaming codec with empty accumulator state. + pub fn new() -> Self { + Self { + state: std::sync::Arc::new(std::sync::Mutex::new( + OpenAIResponsesStreamingState::default(), + )), + } + } +} + +impl Default for OpenAIResponsesStreamingCodec { + fn default() -> Self { + Self::new() + } +} + +impl super::streaming::StreamingCodec for OpenAIResponsesStreamingCodec { + fn collector(&self) -> crate::api::runtime::LlmCollectorFn { + let state = std::sync::Arc::clone(&self.state); + Box::new(move |event: Json| -> Result<()> { + let mut guard = state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + guard.observe(&event); + Ok(()) + }) + } + + fn finalizer(&self) -> crate::api::runtime::LlmFinalizerFn { + let state = std::sync::Arc::clone(&self.state); + Box::new(move || -> Json { + let mut guard = state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + std::mem::take(&mut *guard).finalize() + }) + } +} + +#[derive(Debug, Default)] +struct OpenAIResponsesStreamingState { + /// Latest `response` snapshot from any event that carries one. Last write wins, so terminal + /// events with the complete state will end up here when they fire. + response: Option>, + /// Items keyed by `output_index`. Captured from `response.output_item.added` (initial) and + /// replaced on `response.output_item.done` (final). Used as a fallback for `output` when the + /// terminal `response` snapshot lacks it. + items: std::collections::BTreeMap, +} + +impl OpenAIResponsesStreamingState { + fn observe(&mut self, event: &Json) { + let event_type = event.get("type").and_then(Json::as_str).unwrap_or(""); + match event_type { + "response.created" + | "response.in_progress" + | "response.completed" + | "response.failed" + | "response.incomplete" => self.observe_response_snapshot(event), + "response.output_item.added" | "response.output_item.done" => { + self.observe_output_item(event); + } + // response.output_text.delta, response.function_call_arguments.delta, + // response.content_part.added/done — content is redelivered in output_item.done, so we + // don't accumulate deltas. Unknown events are ignored. + _ => {} + } + } + + fn observe_response_snapshot(&mut self, event: &Json) { + let Some(response) = event.get("response") else { + return; + }; + if let Json::Object(map) = response { + self.response = Some(map.clone()); + } + } + + fn observe_output_item(&mut self, event: &Json) { + let Some(index) = event.get("output_index").and_then(Json::as_u64) else { + return; + }; + let Some(item) = event.get("item") else { + return; + }; + self.items.insert(index as usize, item.clone()); + } + + fn finalize(self) -> Json { + let mut output = self.response.unwrap_or_default(); + // If the latest snapshot lacked `output` (or has an empty array because it came from an + // early `response.created` event), backfill from per-item accumulator. Terminal events + // typically carry the complete output, so this branch is a safety net for truncated + // streams or schemas that drop output from terminal events. + let snapshot_output_empty = output + .get("output") + .and_then(Json::as_array) + .map(|arr| arr.is_empty()) + .unwrap_or(true); + if snapshot_output_empty && !self.items.is_empty() { + let items: Vec = self.items.into_values().collect(); + output.insert("output".to_string(), Json::Array(items)); + } + Json::Object(output) + } +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- diff --git a/crates/core/tests/unit/codec/openai_responses_tests.rs b/crates/core/tests/unit/codec/openai_responses_tests.rs index 87558e3f..c10ed308 100644 --- a/crates/core/tests/unit/codec/openai_responses_tests.rs +++ b/crates/core/tests/unit/codec/openai_responses_tests.rs @@ -562,3 +562,195 @@ fn test_helper_and_error_paths_cover_remaining_responses_branches() { other => panic!("unexpected encode result: {other:?}"), } } + +// =================================================================== +// Streaming codec tests +// =================================================================== + +use super::super::streaming::StreamingCodec; + +#[test] +fn openai_responses_streaming_codec_uses_terminal_snapshot() { + // Common case: response.completed carries the full final state. Streaming codec emits that + // verbatim; per-item accumulator is unused. + let codec = OpenAIResponsesStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + collector(json!({ + "type": "response.created", + "response": {"id": "resp_1", "model": "gpt-5.5", "status": "in_progress", + "output": [], "usage": null} + })) + .unwrap(); + collector(json!({ + "type": "response.completed", + "response": { + "id": "resp_1", + "model": "gpt-5.5", + "status": "completed", + "output": [ + {"type": "message", "content": [ + {"type": "output_text", "text": "Hello, world."} + ]} + ], + "usage": {"input_tokens": 10, "output_tokens": 4, "total_tokens": 14} + } + })) + .unwrap(); + + let assembled = finalizer(); + let annotated = OpenAIResponsesCodec + .decode_response(&assembled) + .expect("assembled response should decode through the existing codec"); + assert_eq!(annotated.id.as_deref(), Some("resp_1")); + assert_eq!(annotated.model.as_deref(), Some("gpt-5.5")); + assert_eq!(annotated.finish_reason, Some(FinishReason::Complete)); + assert_eq!( + annotated.message, + Some(MessageContent::Text("Hello, world.".to_string())) + ); + let usage = annotated.usage.as_ref().unwrap(); + assert_eq!(usage.prompt_tokens, Some(10)); + assert_eq!(usage.completion_tokens, Some(4)); +} + +#[test] +fn openai_responses_streaming_codec_assembles_from_output_item_done_when_terminal_lacks_output() { + // Schema variant: terminal `response.completed` event omits `output` (or sends empty array). + // Codec falls back to per-item accumulator populated by output_item.done. + let codec = OpenAIResponsesStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + collector(json!({ + "type": "response.created", + "response": {"id": "resp_x", "model": "gpt-5.5", "status": "in_progress", "output": []} + })) + .unwrap(); + collector(json!({ + "type": "response.output_item.done", + "output_index": 0, + "item": {"type": "message", "content": [ + {"type": "output_text", "text": "Hi from item 0."} + ]} + })) + .unwrap(); + collector(json!({ + "type": "response.output_item.done", + "output_index": 1, + "item": { + "type": "function_call", + "call_id": "call_42", + "name": "lookup", + "arguments": "{\"q\": \"weather\"}" + } + })) + .unwrap(); + collector(json!({ + "type": "response.completed", + "response": { + "id": "resp_x", + "model": "gpt-5.5", + "status": "completed", + "usage": {"input_tokens": 5, "output_tokens": 3} + } + })) + .unwrap(); + + let assembled = finalizer(); + let annotated = OpenAIResponsesCodec + .decode_response(&assembled) + .expect("assembled response should decode"); + assert_eq!( + annotated.message, + Some(MessageContent::Text("Hi from item 0.".to_string())) + ); + let tool_calls = annotated.tool_calls.expect("function call extracted"); + assert_eq!(tool_calls.len(), 1); + assert_eq!(tool_calls[0].id, "call_42"); + assert_eq!(tool_calls[0].name, "lookup"); + assert_eq!(tool_calls[0].arguments, json!({"q": "weather"})); +} + +#[test] +fn openai_responses_streaming_codec_preserves_incomplete_terminal_state() { + // response.incomplete with `reason: max_output_tokens` should map to FinishReason::Length + // through the existing decoder. The streaming codec must surface incomplete_details intact. + let codec = OpenAIResponsesStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + collector(json!({ + "type": "response.incomplete", + "response": { + "id": "resp_inc", + "model": "gpt-5.5", + "status": "incomplete", + "incomplete_details": {"reason": "max_output_tokens"}, + "output": [ + {"type": "message", "content": [ + {"type": "output_text", "text": "partial..."} + ]} + ] + } + })) + .unwrap(); + + let assembled = finalizer(); + let annotated = OpenAIResponsesCodec + .decode_response(&assembled) + .expect("assembled response should decode"); + assert_eq!(annotated.finish_reason, Some(FinishReason::Length)); + assert_eq!( + annotated.message, + Some(MessageContent::Text("partial...".to_string())) + ); +} + +#[test] +fn openai_responses_streaming_codec_ignores_per_token_deltas() { + // output_text.delta events are intentionally not accumulated — their content is redelivered + // in output_item.done. Codec must not double-count or insert delta-only state. + let codec = OpenAIResponsesStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + collector(json!({ + "type": "response.created", + "response": {"id": "resp_d", "model": "gpt-5.5", "status": "in_progress", "output": []} + })) + .unwrap(); + collector(json!({ + "type": "response.output_text.delta", + "output_index": 0, "content_index": 0, "delta": "Hel" + })) + .unwrap(); + collector(json!({ + "type": "response.output_text.delta", + "output_index": 0, "content_index": 0, "delta": "lo" + })) + .unwrap(); + collector(json!({ + "type": "response.output_item.done", + "output_index": 0, + "item": {"type": "message", "content": [ + {"type": "output_text", "text": "Hello"} + ]} + })) + .unwrap(); + collector(json!({ + "type": "response.completed", + "response": {"id": "resp_d", "model": "gpt-5.5", "status": "completed", "output": []} + })) + .unwrap(); + + let assembled = finalizer(); + let annotated = OpenAIResponsesCodec + .decode_response(&assembled) + .expect("assembled response should decode"); + assert_eq!( + annotated.message, + Some(MessageContent::Text("Hello".to_string())) + ); +} From 24e245143c6f1ac345ceebca0155c17babd88a24 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 8 May 2026 21:51:18 -0700 Subject: [PATCH 25/27] feat(core): OpenAIChatStreamingCodec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements StreamingCodec for the OpenAI Chat Completions SSE chunk sequence. Each delta chunk may carry role (typically only the first), incremental content fragments, partial tool_calls whose function.arguments stream as a string, or a finish_reason terminator. Top-level fields (id, object, created, model) are repeated on every chunk; we capture them once. Final-chunk usage is preserved when emitted (only sent when stream_options.include_usage is set). The finalizer produces a JSON object wire-compatible with a non-streaming Chat Completions response — feed it back through OpenAIChatCodec::decode_response to produce the canonical AnnotatedLlmResponse. The `object` field is normalized from `chat.completion.chunk` to `chat.completion` so the assembled JSON matches what a non-streaming response would carry. Also: SseEventDecoder now drops the `data: [DONE]` end-of-stream sentinel that OpenAI Chat Completions emits. It's a wire-level marker, not a JSON payload, and would otherwise fail JSON parsing. Tests cover: - text-only response with usage and finish_reason mapping - tool_call arguments assembled from string fragments - content: null preserved when only tool calls streamed - multiple choices (n > 1 requests) tracked separately - intermediate null usage chunks don't overwrite real usage Refs NMF-90. Signed-off-by: Ajay Thorve --- crates/core/src/codec/openai_chat.rs | 271 ++++++++++++++++++ crates/core/src/codec/streaming.rs | 23 +- .../tests/unit/codec/openai_chat_tests.rs | 223 ++++++++++++++ 3 files changed, 516 insertions(+), 1 deletion(-) diff --git a/crates/core/src/codec/openai_chat.rs b/crates/core/src/codec/openai_chat.rs index 72ddd836..ad1bcbc7 100644 --- a/crates/core/src/codec/openai_chat.rs +++ b/crates/core/src/codec/openai_chat.rs @@ -367,6 +367,277 @@ fn overlay_generation_params( Ok(()) } +// --------------------------------------------------------------------------- +// Streaming codec +// --------------------------------------------------------------------------- + +/// Streaming counterpart to [`OpenAIChatCodec`]. +/// +/// Replays the OpenAI Chat Completions SSE chunk sequence into the same JSON shape returned for a +/// non-streaming request (`{id, object, created, model, choices: [{message, finish_reason}], +/// usage}`). Once finalized, the assembled JSON can be fed back through +/// [`OpenAIChatCodec::decode_response`] to produce the canonical +/// [`AnnotatedLlmResponse`](crate::codec::response::AnnotatedLlmResponse). +/// +/// # Strategy +/// +/// Chat Completions streams untyped SSE chunks of `{choices: [{index, delta: {...}, +/// finish_reason: ...}]}`. Each delta may carry a `role` (typically only on the first chunk), +/// incremental `content` text, or partial `tool_calls` whose `function.arguments` stream as a +/// JSON-encoded string fragment-by-fragment. Top-level fields (`id`, `model`, `created`) are +/// repeated on every chunk; we capture them once. Final-chunk `usage` is preserved when emitted +/// (only sent when `stream_options.include_usage` is set on the request). +/// +/// The OpenAI `[DONE]` end-of-stream sentinel is dropped by the SSE event decoder before +/// reaching the collector, so this codec never sees it. +/// +/// Internal state lives behind `Arc>` so the `&self`-produced collector and finalizer +/// closures share access. Each instance is single-use because [`LlmFinalizerFn`] consumes the +/// finalize step. +/// +/// [`LlmFinalizerFn`]: crate::api::runtime::LlmFinalizerFn +pub struct OpenAIChatStreamingCodec { + state: std::sync::Arc>, +} + +impl OpenAIChatStreamingCodec { + /// Creates a fresh streaming codec with empty accumulator state. + pub fn new() -> Self { + Self { + state: std::sync::Arc::new(std::sync::Mutex::new(OpenAIChatStreamingState::default())), + } + } +} + +impl Default for OpenAIChatStreamingCodec { + fn default() -> Self { + Self::new() + } +} + +impl super::streaming::StreamingCodec for OpenAIChatStreamingCodec { + fn collector(&self) -> crate::api::runtime::LlmCollectorFn { + let state = std::sync::Arc::clone(&self.state); + Box::new(move |event: Json| -> Result<()> { + let mut guard = state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + guard.observe(&event); + Ok(()) + }) + } + + fn finalizer(&self) -> crate::api::runtime::LlmFinalizerFn { + let state = std::sync::Arc::clone(&self.state); + Box::new(move || -> Json { + let mut guard = state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + std::mem::take(&mut *guard).finalize() + }) + } +} + +#[derive(Debug, Default)] +struct OpenAIChatStreamingState { + id: Option, + object: Option, + created: Option, + model: Option, + /// Per-choice accumulator keyed by `choice.index`. BTreeMap so finalize emits choices in + /// stable order. + choices: std::collections::BTreeMap, + /// Top-level usage from the final chunk (when `stream_options.include_usage` is set). + usage: Option, +} + +#[derive(Debug, Default)] +struct ChoiceState { + role: Option, + content: String, + has_content: bool, + /// Tool calls keyed by their `index` within the choice. Each tool call's `arguments` is + /// streamed as a JSON-encoded string accumulated fragment-by-fragment. + tool_calls: std::collections::BTreeMap, + finish_reason: Option, +} + +#[derive(Debug, Default)] +struct ToolCallState { + id: Option, + type_: Option, + name: Option, + arguments: String, +} + +impl OpenAIChatStreamingState { + fn observe(&mut self, chunk: &Json) { + // Top-level fields (id, object, created, model) are repeated on every chunk; capture once + // each so unrelated later chunks can't overwrite the canonical values. + if self.id.is_none() + && let Some(id) = chunk.get("id").and_then(Json::as_str) + { + self.id = Some(id.to_string()); + } + if self.object.is_none() + && let Some(obj) = chunk.get("object").and_then(Json::as_str) + { + self.object = Some(obj.to_string()); + } + if self.created.is_none() + && let Some(c) = chunk.get("created").and_then(Json::as_u64) + { + self.created = Some(c); + } + if self.model.is_none() + && let Some(m) = chunk.get("model").and_then(Json::as_str) + { + self.model = Some(m.to_string()); + } + if let Some(usage) = chunk.get("usage") { + // Some streams emit `usage: null` on every chunk and the real usage only on the + // final chunk; only capture non-null usage objects. + if !usage.is_null() { + self.usage = Some(usage.clone()); + } + } + let Some(choices) = chunk.get("choices").and_then(Json::as_array) else { + return; + }; + for choice in choices { + self.observe_choice(choice); + } + } + + fn observe_choice(&mut self, choice: &Json) { + let index = choice.get("index").and_then(Json::as_u64).unwrap_or(0); + let entry = self.choices.entry(index).or_default(); + if let Some(reason) = choice.get("finish_reason").and_then(Json::as_str) { + entry.finish_reason = Some(reason.to_string()); + } + let Some(delta) = choice.get("delta") else { + return; + }; + if let Some(role) = delta.get("role").and_then(Json::as_str) { + entry.role = Some(role.to_string()); + } + if let Some(content) = delta.get("content").and_then(Json::as_str) { + entry.content.push_str(content); + entry.has_content = true; + } + if let Some(tool_calls) = delta.get("tool_calls").and_then(Json::as_array) { + for tc in tool_calls { + let tc_index = tc.get("index").and_then(Json::as_u64).unwrap_or(0); + let tc_state = entry.tool_calls.entry(tc_index).or_default(); + if let Some(id) = tc.get("id").and_then(Json::as_str) { + tc_state.id = Some(id.to_string()); + } + if let Some(t) = tc.get("type").and_then(Json::as_str) { + tc_state.type_ = Some(t.to_string()); + } + if let Some(function) = tc.get("function") { + if let Some(name) = function.get("name").and_then(Json::as_str) { + tc_state.name = Some(name.to_string()); + } + if let Some(args) = function.get("arguments").and_then(Json::as_str) { + tc_state.arguments.push_str(args); + } + } + } + } + } + + fn finalize(self) -> Json { + let mut output = serde_json::Map::new(); + if let Some(id) = self.id { + output.insert("id".to_string(), Json::String(id)); + } + // After streaming, the final shape is `chat.completion`, not `chat.completion.chunk`. + // Strip the `.chunk` suffix so the assembled JSON round-trips through + // OpenAIChatCodec::decode_response with the same `object` field a non-streaming response + // would carry. + if let Some(object) = self.object { + let normalized = object + .strip_suffix(".chunk") + .map(str::to_string) + .unwrap_or(object); + output.insert("object".to_string(), Json::String(normalized)); + } + if let Some(created) = self.created { + output.insert("created".to_string(), Json::Number(created.into())); + } + if let Some(model) = self.model { + output.insert("model".to_string(), Json::String(model)); + } + let choices: Vec = self + .choices + .into_iter() + .map(|(index, choice)| choice.finalize(index)) + .collect(); + output.insert("choices".to_string(), Json::Array(choices)); + if let Some(usage) = self.usage { + output.insert("usage".to_string(), usage); + } + Json::Object(output) + } +} + +impl ChoiceState { + fn finalize(self, index: u64) -> Json { + let mut message = serde_json::Map::new(); + message.insert( + "role".to_string(), + Json::String(self.role.unwrap_or_else(|| "assistant".to_string())), + ); + // OpenAI's wire format uses `content: null` when the model only emitted tool calls. + // Preserve that distinction: empty-string content when the model said something, null + // when it didn't. + if self.has_content { + message.insert("content".to_string(), Json::String(self.content)); + } else { + message.insert("content".to_string(), Json::Null); + } + if !self.tool_calls.is_empty() { + let tool_calls: Vec = self + .tool_calls + .into_values() + .map(ToolCallState::finalize) + .collect(); + message.insert("tool_calls".to_string(), Json::Array(tool_calls)); + } + let mut choice = serde_json::Map::new(); + choice.insert("index".to_string(), Json::Number(index.into())); + choice.insert("message".to_string(), Json::Object(message)); + if let Some(reason) = self.finish_reason { + choice.insert("finish_reason".to_string(), Json::String(reason)); + } else { + choice.insert("finish_reason".to_string(), Json::Null); + } + Json::Object(choice) + } +} + +impl ToolCallState { + fn finalize(self) -> Json { + let mut function = serde_json::Map::new(); + function.insert( + "name".to_string(), + Json::String(self.name.unwrap_or_default()), + ); + function.insert("arguments".to_string(), Json::String(self.arguments)); + let mut call = serde_json::Map::new(); + if let Some(id) = self.id { + call.insert("id".to_string(), Json::String(id)); + } + call.insert( + "type".to_string(), + Json::String(self.type_.unwrap_or_else(|| "function".to_string())), + ); + call.insert("function".to_string(), Json::Object(function)); + Json::Object(call) + } +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- diff --git a/crates/core/src/codec/streaming.rs b/crates/core/src/codec/streaming.rs index c9c29881..0cd1bd26 100644 --- a/crates/core/src/codec/streaming.rs +++ b/crates/core/src/codec/streaming.rs @@ -136,7 +136,14 @@ fn parse_sse_frame(frame: &str) -> Result> { return Ok(None); } let payload = data_parts.join("\n"); - let data: Json = serde_json::from_str(payload.trim()).map_err(|error| { + let trimmed = payload.trim(); + // OpenAI Chat Completions emits a `data: [DONE]` terminator as a wire-level end-of-stream + // sentinel. It's not a JSON payload — drop it like a heartbeat. Other providers (Anthropic, + // OpenAI Responses) have proper terminal events instead, so this only fires for OpenAI Chat. + if trimmed == "[DONE]" { + return Ok(None); + } + let data: Json = serde_json::from_str(trimmed).map_err(|error| { FlowError::Internal(format!( "streaming codec failed to parse SSE data payload: {error}: {payload}" )) @@ -196,6 +203,20 @@ mod tests { assert_eq!(trailing.data, json!({"end": true})); } + #[test] + fn drops_openai_chat_done_sentinel() { + let mut decoder = SseEventDecoder::new(); + let events = decoder + .push_bytes( + b"data: {\"id\":\"chatcmpl-1\"}\n\ndata: [DONE]\n\ndata: {\"id\":\"chatcmpl-2\"}\n\n", + ) + .unwrap(); + // [DONE] is dropped; surrounding JSON events still come through. + assert_eq!(events.len(), 2); + assert_eq!(events[0].data, json!({"id": "chatcmpl-1"})); + assert_eq!(events[1].data, json!({"id": "chatcmpl-2"})); + } + #[test] fn surfaces_parse_errors_with_payload_context() { let mut decoder = SseEventDecoder::new(); diff --git a/crates/core/tests/unit/codec/openai_chat_tests.rs b/crates/core/tests/unit/codec/openai_chat_tests.rs index ea14dff8..78b66509 100644 --- a/crates/core/tests/unit/codec/openai_chat_tests.rs +++ b/crates/core/tests/unit/codec/openai_chat_tests.rs @@ -809,3 +809,226 @@ fn test_encode_does_not_inject_stream_options_on_non_streaming() { "stream_options must not be injected when stream: false", ); } + +// =================================================================== +// Streaming codec tests +// =================================================================== + +use super::super::streaming::StreamingCodec; + +#[test] +fn openai_chat_streaming_codec_assembles_text_response() { + let codec = OpenAIChatStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + // First chunk: top-level fields + role-only delta. + collector(json!({ + "id": "chatcmpl-1", + "object": "chat.completion.chunk", + "created": 1_700_000_000, + "model": "gpt-4o", + "choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": null}] + })) + .unwrap(); + // Content deltas. + for part in &["Hello, ", "world", "."] { + collector(json!({ + "id": "chatcmpl-1", "object": "chat.completion.chunk", + "choices": [{"index": 0, "delta": {"content": part}, "finish_reason": null}] + })) + .unwrap(); + } + // Final chunk with finish_reason and usage (when stream_options.include_usage was set). + collector(json!({ + "id": "chatcmpl-1", "object": "chat.completion.chunk", + "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}], + "usage": {"prompt_tokens": 10, "completion_tokens": 4, "total_tokens": 14} + })) + .unwrap(); + + let assembled = finalizer(); + // Verify the assembled object is wire-compatible with non-streaming Chat Completions and + // round-trips through the existing decoder. + assert_eq!(assembled["object"], json!("chat.completion")); + let annotated = OpenAIChatCodec + .decode_response(&assembled) + .expect("assembled response should decode"); + assert_eq!(annotated.id.as_deref(), Some("chatcmpl-1")); + assert_eq!(annotated.model.as_deref(), Some("gpt-4o")); + assert_eq!(annotated.finish_reason, Some(FinishReason::Complete)); + assert_eq!( + annotated.message, + Some(MessageContent::Text("Hello, world.".to_string())) + ); + let usage = annotated.usage.as_ref().unwrap(); + assert_eq!(usage.prompt_tokens, Some(10)); + assert_eq!(usage.completion_tokens, Some(4)); + assert_eq!(usage.total_tokens, Some(14)); +} + +#[test] +fn openai_chat_streaming_codec_assembles_tool_call_arguments_from_fragments() { + let codec = OpenAIChatStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + // Initial chunk: role + tool_call header (id, type, function.name). + collector(json!({ + "id": "chatcmpl-tc", "object": "chat.completion.chunk", "model": "gpt-4o", + "choices": [{ + "index": 0, + "delta": { + "role": "assistant", + "tool_calls": [{ + "index": 0, + "id": "call_a", + "type": "function", + "function": {"name": "lookup", "arguments": ""} + }] + }, + "finish_reason": null + }] + })) + .unwrap(); + // Argument fragments arrive over multiple chunks. + for fragment in &["{\"q", "uery\":", " \"weath", "er\"}"] { + collector(json!({ + "id": "chatcmpl-tc", "object": "chat.completion.chunk", + "choices": [{ + "index": 0, + "delta": {"tool_calls": [{ + "index": 0, + "function": {"arguments": fragment} + }]}, + "finish_reason": null + }] + })) + .unwrap(); + } + collector(json!({ + "id": "chatcmpl-tc", "object": "chat.completion.chunk", + "choices": [{"index": 0, "delta": {}, "finish_reason": "tool_calls"}] + })) + .unwrap(); + + let assembled = finalizer(); + let annotated = OpenAIChatCodec + .decode_response(&assembled) + .expect("assembled response should decode"); + assert_eq!(annotated.finish_reason, Some(FinishReason::ToolUse)); + let tool_calls = annotated.tool_calls.expect("tool_calls present"); + assert_eq!(tool_calls.len(), 1); + assert_eq!(tool_calls[0].id, "call_a"); + assert_eq!(tool_calls[0].name, "lookup"); + assert_eq!(tool_calls[0].arguments, json!({"query": "weather"})); +} + +#[test] +fn openai_chat_streaming_codec_emits_null_content_when_only_tool_calls_streamed() { + // OpenAI's non-streaming wire format uses `content: null` when the assistant only emitted + // tool calls. The streaming codec must preserve that distinction so downstream consumers + // (or anyone manually inspecting the assembled JSON) match what a non-streaming response + // would have shown. + let codec = OpenAIChatStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + collector(json!({ + "id": "chatcmpl-nc", "object": "chat.completion.chunk", "model": "gpt-4o", + "choices": [{ + "index": 0, + "delta": { + "role": "assistant", + "tool_calls": [{ + "index": 0, "id": "call_x", "type": "function", + "function": {"name": "go", "arguments": "{}"} + }] + }, + "finish_reason": null + }] + })) + .unwrap(); + collector(json!({ + "id": "chatcmpl-nc", "object": "chat.completion.chunk", + "choices": [{"index": 0, "delta": {}, "finish_reason": "tool_calls"}] + })) + .unwrap(); + + let assembled = finalizer(); + let message = &assembled["choices"][0]["message"]; + assert_eq!(message["content"], json!(null)); + assert!(message["tool_calls"].is_array()); +} + +#[test] +fn openai_chat_streaming_codec_handles_multiple_choices() { + // OpenAI Chat Completions supports `n > 1` requesting multiple completions; each gets its + // own choice index. Streaming codec must keep them separate. + let codec = OpenAIChatStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + collector(json!({ + "id": "chatcmpl-multi", "object": "chat.completion.chunk", "model": "gpt-4o", + "choices": [ + {"index": 0, "delta": {"role": "assistant"}, "finish_reason": null}, + {"index": 1, "delta": {"role": "assistant"}, "finish_reason": null} + ] + })) + .unwrap(); + collector(json!({ + "id": "chatcmpl-multi", "object": "chat.completion.chunk", + "choices": [ + {"index": 0, "delta": {"content": "First"}, "finish_reason": null}, + {"index": 1, "delta": {"content": "Second"}, "finish_reason": null} + ] + })) + .unwrap(); + collector(json!({ + "id": "chatcmpl-multi", "object": "chat.completion.chunk", + "choices": [ + {"index": 0, "delta": {}, "finish_reason": "stop"}, + {"index": 1, "delta": {}, "finish_reason": "stop"} + ] + })) + .unwrap(); + + let assembled = finalizer(); + let choices = assembled["choices"].as_array().expect("choices array"); + assert_eq!(choices.len(), 2); + assert_eq!(choices[0]["index"], json!(0)); + assert_eq!(choices[0]["message"]["content"], json!("First")); + assert_eq!(choices[1]["index"], json!(1)); + assert_eq!(choices[1]["message"]["content"], json!("Second")); +} + +#[test] +fn openai_chat_streaming_codec_skips_null_usage_chunks() { + // Some streams emit `usage: null` on every chunk and the real usage only on the final chunk. + // Codec must not let intermediate nulls overwrite a captured usage object. + let codec = OpenAIChatStreamingCodec::new(); + let mut collector = codec.collector(); + let finalizer = codec.finalizer(); + + collector(json!({ + "id": "chatcmpl-u", "object": "chat.completion.chunk", "model": "gpt-4o", "usage": null, + "choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": null}] + })) + .unwrap(); + collector(json!({ + "id": "chatcmpl-u", "object": "chat.completion.chunk", "usage": null, + "choices": [{"index": 0, "delta": {"content": "hi"}, "finish_reason": null}] + })) + .unwrap(); + collector(json!({ + "id": "chatcmpl-u", "object": "chat.completion.chunk", + "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2} + })) + .unwrap(); + + let assembled = finalizer(); + assert_eq!(assembled["usage"]["prompt_tokens"], json!(1)); + assert_eq!(assembled["usage"]["total_tokens"], json!(2)); +} From 4c92ffd21c33cc26a80ad8ef4b84a3c71d424a2b Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 8 May 2026 22:34:49 -0700 Subject: [PATCH 26/27] refactor(core): move streaming codec tests to tests/unit/codec/ Match the project's existing test layout: every other codec file in crates/core/src/codec/ pulls its tests from crates/core/tests/unit/codec/_tests.rs via #[path]. The streaming codec tests were inlined in src/codec/streaming.rs in the initial commit; move them out to follow the same convention so source files stay slim and all codec test files cluster under tests/unit/codec/. No behavior change. 283 core lib tests still passing. Refs NMF-90. Signed-off-by: Ajay Thorve --- crates/core/src/codec/streaming.rs | 75 +----------------- .../core/tests/unit/codec/streaming_tests.rs | 78 +++++++++++++++++++ 2 files changed, 80 insertions(+), 73 deletions(-) create mode 100644 crates/core/tests/unit/codec/streaming_tests.rs diff --git a/crates/core/src/codec/streaming.rs b/crates/core/src/codec/streaming.rs index 0cd1bd26..35ebcab1 100644 --- a/crates/core/src/codec/streaming.rs +++ b/crates/core/src/codec/streaming.rs @@ -155,76 +155,5 @@ fn parse_sse_frame(frame: &str) -> Result> { } #[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn decodes_complete_frames_in_one_push() { - let mut decoder = SseEventDecoder::new(); - let events = decoder - .push_bytes(b"event: ping\ndata: {\"type\":\"ping\"}\n\nevent: msg\ndata: {\"text\":\"hi\"}\n\n") - .unwrap(); - assert_eq!(events.len(), 2); - assert_eq!(events[0].event.as_deref(), Some("ping")); - assert_eq!(events[0].data, json!({"type": "ping"})); - assert_eq!(events[1].event.as_deref(), Some("msg")); - assert_eq!(events[1].data, json!({"text": "hi"})); - } - - #[test] - fn buffers_partial_frames_across_pushes() { - let mut decoder = SseEventDecoder::new(); - assert!(decoder.push_bytes(b"event: m\ndata: ").unwrap().is_empty()); - assert!(decoder.push_bytes(b"{\"a\":1").unwrap().is_empty()); - let events = decoder.push_bytes(b"}\n\n").unwrap(); - assert_eq!(events.len(), 1); - assert_eq!(events[0].data, json!({"a": 1})); - } - - #[test] - fn drops_frames_without_data_lines() { - let mut decoder = SseEventDecoder::new(); - // A heartbeat-style comment frame plus a real one. - let events = decoder - .push_bytes(b": keepalive\n\nevent: real\ndata: {\"v\":2}\n\n") - .unwrap(); - assert_eq!(events.len(), 1); - assert_eq!(events[0].data, json!({"v": 2})); - } - - #[test] - fn surfaces_final_partial_frame_on_finish() { - let mut decoder = SseEventDecoder::new(); - decoder - .push_bytes(b"event: tail\ndata: {\"end\":true}") - .unwrap(); - let trailing = decoder.finish().unwrap().expect("trailing frame present"); - assert_eq!(trailing.data, json!({"end": true})); - } - - #[test] - fn drops_openai_chat_done_sentinel() { - let mut decoder = SseEventDecoder::new(); - let events = decoder - .push_bytes( - b"data: {\"id\":\"chatcmpl-1\"}\n\ndata: [DONE]\n\ndata: {\"id\":\"chatcmpl-2\"}\n\n", - ) - .unwrap(); - // [DONE] is dropped; surrounding JSON events still come through. - assert_eq!(events.len(), 2); - assert_eq!(events[0].data, json!({"id": "chatcmpl-1"})); - assert_eq!(events[1].data, json!({"id": "chatcmpl-2"})); - } - - #[test] - fn surfaces_parse_errors_with_payload_context() { - let mut decoder = SseEventDecoder::new(); - let error = decoder - .push_bytes(b"event: bad\ndata: {not valid json}\n\n") - .unwrap_err(); - let message = error.to_string(); - assert!(message.contains("SSE data payload"), "{message}"); - assert!(message.contains("not valid json"), "{message}"); - } -} +#[path = "../../tests/unit/codec/streaming_tests.rs"] +mod tests; diff --git a/crates/core/tests/unit/codec/streaming_tests.rs b/crates/core/tests/unit/codec/streaming_tests.rs new file mode 100644 index 00000000..cb534a9e --- /dev/null +++ b/crates/core/tests/unit/codec/streaming_tests.rs @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Unit tests for streaming in the NeMo Flow core crate. + +use super::*; +use serde_json::json; + +#[test] +fn decodes_complete_frames_in_one_push() { + let mut decoder = SseEventDecoder::new(); + let events = decoder + .push_bytes( + b"event: ping\ndata: {\"type\":\"ping\"}\n\nevent: msg\ndata: {\"text\":\"hi\"}\n\n", + ) + .unwrap(); + assert_eq!(events.len(), 2); + assert_eq!(events[0].event.as_deref(), Some("ping")); + assert_eq!(events[0].data, json!({"type": "ping"})); + assert_eq!(events[1].event.as_deref(), Some("msg")); + assert_eq!(events[1].data, json!({"text": "hi"})); +} + +#[test] +fn buffers_partial_frames_across_pushes() { + let mut decoder = SseEventDecoder::new(); + assert!(decoder.push_bytes(b"event: m\ndata: ").unwrap().is_empty()); + assert!(decoder.push_bytes(b"{\"a\":1").unwrap().is_empty()); + let events = decoder.push_bytes(b"}\n\n").unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0].data, json!({"a": 1})); +} + +#[test] +fn drops_frames_without_data_lines() { + let mut decoder = SseEventDecoder::new(); + // A heartbeat-style comment frame plus a real one. + let events = decoder + .push_bytes(b": keepalive\n\nevent: real\ndata: {\"v\":2}\n\n") + .unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0].data, json!({"v": 2})); +} + +#[test] +fn surfaces_final_partial_frame_on_finish() { + let mut decoder = SseEventDecoder::new(); + decoder + .push_bytes(b"event: tail\ndata: {\"end\":true}") + .unwrap(); + let trailing = decoder.finish().unwrap().expect("trailing frame present"); + assert_eq!(trailing.data, json!({"end": true})); +} + +#[test] +fn drops_openai_chat_done_sentinel() { + let mut decoder = SseEventDecoder::new(); + let events = decoder + .push_bytes( + b"data: {\"id\":\"chatcmpl-1\"}\n\ndata: [DONE]\n\ndata: {\"id\":\"chatcmpl-2\"}\n\n", + ) + .unwrap(); + // [DONE] is dropped; surrounding JSON events still come through. + assert_eq!(events.len(), 2); + assert_eq!(events[0].data, json!({"id": "chatcmpl-1"})); + assert_eq!(events[1].data, json!({"id": "chatcmpl-2"})); +} + +#[test] +fn surfaces_parse_errors_with_payload_context() { + let mut decoder = SseEventDecoder::new(); + let error = decoder + .push_bytes(b"event: bad\ndata: {not valid json}\n\n") + .unwrap_err(); + let message = error.to_string(); + assert!(message.contains("SSE data payload"), "{message}"); + assert!(message.contains("not valid json"), "{message}"); +} From a5f01057b4e4ca1c24869c34328035819a6b76c8 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Fri, 8 May 2026 23:35:40 -0700 Subject: [PATCH 27/27] feat(sidecar): route gateway through managed LLM execution Refactors the gateway proxy to use llm_call_execute and llm_stream_call_execute so the runtime owns LLM start/end events, codec annotation, and stream lifecycle. Per-route dispatch wires Anthropic Messages, OpenAI Responses, and OpenAI Chat Completions to their existing typed codecs and the new streaming codecs landed earlier in this PR. A new SessionManager::prepare_gateway_call helper resolves the right session, opens the agent scope, and computes correlation metadata without holding the session lock during the upstream HTTP work. The buffered path returns the captured upstream bytes verbatim; the streaming path re-encodes parsed events back into SSE frames for the client (Option B). Upstream connection failures are still surfaced as 502 Bad Gateway via a side channel that captures the original reqwest error before the runtime collapses it to FlowError::Internal. Trade-off A vs B for streaming response forwarding: A. Byte tee: split the raw upstream byte stream between the client and a side capture that feeds the runtime collector. Bytes pass through unchanged, but observability hooks see the literal wire bytes (gzip/identity) rather than parsed events, and the collector code has to duplicate per-provider SSE framing logic just for capture. B. Re-encode (chosen): parse SSE on the upstream side, feed parsed JSON events to the runtime collector, then re-encode events as SSE for the client. Adds one parse + one serialize per chunk, but the runtime sees identical event shapes for streaming and non-streaming requests, and chunk-level observability matches the codec's annotated response. The legacy SessionManager::start_llm / end_llm path is retained behind cfg(test) so existing correlation unit tests continue to exercise resolve_llm_owner. Streaming tool-hint extraction is intentionally deferred because the runtime synthesizes the aggregate response inside LlmStreamWrapper and does not surface it back to the gateway. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Ajay Thorve --- crates/sidecar/src/gateway.rs | 729 ++++++++++++------ crates/sidecar/src/session.rs | 133 +++- .../sidecar/tests/coverage/gateway_tests.rs | 123 +-- crates/sidecar/tests/coverage/server_tests.rs | 36 +- 4 files changed, 642 insertions(+), 379 deletions(-) diff --git a/crates/sidecar/src/gateway.rs b/crates/sidecar/src/gateway.rs index f08cdf4e..3ee1a6a2 100644 --- a/crates/sidecar/src/gateway.rs +++ b/crates/sidecar/src/gateway.rs @@ -1,46 +1,58 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +use std::sync::{Arc, Mutex}; + +use async_stream::stream; 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 nemo_flow::api::llm::{ + LlmCallExecuteParams, LlmRequest, LlmStreamCallExecuteParams, llm_call_execute, + llm_stream_call_execute, +}; +use nemo_flow::api::runtime::{ + LlmExecutionNextFn, LlmJsonStream, LlmStreamExecutionNextFn, TASK_SCOPE_STACK, +}; +use nemo_flow::codec::anthropic::{AnthropicMessagesCodec, AnthropicMessagesStreamingCodec}; +use nemo_flow::codec::openai_chat::{OpenAIChatCodec, OpenAIChatStreamingCodec}; +use nemo_flow::codec::openai_responses::{OpenAIResponsesCodec, OpenAIResponsesStreamingCodec}; +use nemo_flow::codec::streaming::StreamingCodec; +use nemo_flow::codec::traits::LlmResponseCodec; +use nemo_flow::error::FlowError; use serde_json::{Map, Value, json}; use crate::config::header_string; use crate::error::SidecarError; use crate::server::AppState; -use crate::session::{ActiveLlm, LlmGatewayStart, SessionManager}; +use crate::session::{GatewayCallPrep, LlmGatewayStart}; const MAX_BODY_BYTES: usize = 100 * 1024 * 1024; -/// Proxies supported LLM API requests while recording a NeMo Flow LLM call around the upstream work. +/// Proxies supported LLM API requests through NeMo Flow's managed execution pipeline. +/// +/// The gateway buffers the inbound body once, opens a managed LLM call against the resolved +/// session, and lets the runtime own the start/end events. Provider routes that have a built-in +/// codec round-trip the response through the codec so observability records the same annotated +/// response shape as direct in-process calls; routes without a codec still emit raw JSON to the +/// runtime so the LLM scope is preserved. /// -/// 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. +/// Streaming responses are decoded into per-event JSON values, fed through the runtime collector, +/// and re-encoded as SSE frames for the client. This Option B approach (re-encode) keeps the +/// runtime in the streaming hot path so chunk-level observability matches non-streaming output; +/// the trade-off is one extra JSON parse + serialize per chunk versus the alternative byte-tee +/// design that splits a raw byte stream between client and runtime. 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 + let prep = state + .sessions + .prepare_gateway_call(&prepared.headers, build_llm_gateway_start(&prepared)) + .await?; + run_managed_gateway(state, prepared, prep).await } struct PreparedGatewayRequest { @@ -93,251 +105,490 @@ async fn prepare_gateway_request( }) } -// 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(&request.headers), - content: request.request_json.clone(), - }; - sessions - .start_llm( +// Builds the [`LlmGatewayStart`] payload from a prepared request. Identifier resolution is shared +// across streaming and non-streaming paths so correlation behavior is consistent for every route. +fn build_llm_gateway_start(request: &PreparedGatewayRequest) -> LlmGatewayStart { + LlmGatewayStart { + 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, - LlmGatewayStart { - 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: request.streaming, - metadata: json!({ "gateway_path": request.path }), - }, + &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"], + ], ) - .await + .or_else(|| header_string(&request.headers, "x-request-id")), + request: LlmRequest { + headers: observable_headers(&request.headers), + content: request.request_json.clone(), + }, + streaming: request.streaming, + metadata: json!({ "gateway_path": request.path }), + } } -// 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(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); - } +// Captures upstream HTTP status and response headers from inside the managed `func`. The runtime's +// LLM execution callback returns only a Json (or Json stream), so the outer gateway needs a side +// channel to recover the bytes the client expects. +type UpstreamResponseInfo = Arc>>; + +// Captures the original `reqwest::Error` from an upstream send failure so the gateway can return +// a 502 Bad Gateway on connection-level failures. The runtime collapses every callback failure to +// `FlowError::Internal`, which would otherwise map to a generic 400. +type UpstreamErrorSlot = Arc>>; + +// Runs the managed pipeline for a prepared gateway request. Streaming and non-streaming branches +// share the same prep + codec dispatch but diverge in how the runtime drives the upstream call. +async fn run_managed_gateway( + state: AppState, + prepared: PreparedGatewayRequest, + prep: GatewayCallPrep, +) -> Result, SidecarError> { + let codecs = codecs_for_route(prepared.provider); + if prepared.streaming { + run_managed_streaming(state, prepared, prep, codecs).await + } else { + run_managed_buffered(state, prepared, prep, codecs).await } - match upstream.send().await { - Ok(response) => Ok(response), - Err(error) => { +} + +// Codecs registered for each managed provider route. Routes that emit LLM events but lack a typed +// codec (count_tokens) return `None` so the runtime still wraps the call but skips annotation. +struct RouteCodecs { + streaming: Option>, + response: Option>, +} + +fn codecs_for_route(route: ProviderRoute) -> RouteCodecs { + match route { + ProviderRoute::AnthropicMessages => RouteCodecs { + streaming: Some(Box::new(AnthropicMessagesStreamingCodec::new())), + response: Some(Arc::new(AnthropicMessagesCodec) as Arc), + }, + ProviderRoute::OpenAiResponses => RouteCodecs { + streaming: Some(Box::new(OpenAIResponsesStreamingCodec::new())), + response: Some(Arc::new(OpenAIResponsesCodec) as Arc), + }, + ProviderRoute::OpenAiChatCompletions => RouteCodecs { + streaming: Some(Box::new(OpenAIChatStreamingCodec::new())), + response: Some(Arc::new(OpenAIChatCodec) as Arc), + }, + ProviderRoute::AnthropicCountTokens | ProviderRoute::OpenAiModels => RouteCodecs { + streaming: None, + response: None, + }, + } +} + +// Runs a non-streaming gateway request through `llm_call_execute`. The runtime handles start/end +// events and codec annotation; the gateway only sends the upstream request, parses bytes, and +// forwards the captured status/headers back to the client. +async fn run_managed_buffered( + state: AppState, + prepared: PreparedGatewayRequest, + prep: GatewayCallPrep, + codecs: RouteCodecs, +) -> Result, SidecarError> { + let upstream_info: UpstreamResponseInfo = Arc::new(Mutex::new(None)); + let upstream_error: UpstreamErrorSlot = Arc::new(Mutex::new(None)); + let response_bytes: Arc>> = Arc::new(Mutex::new(None)); + let func = build_buffered_func( + state.clone(), + &prepared, + upstream_info.clone(), + upstream_error.clone(), + response_bytes.clone(), + ); + let GatewayCallPrep { + scope_stack, + session_id, + provider_name, + request, + parent, + attributes, + metadata, + model_name, + owner_subagent_id, + } = prep; + let provider_for_event = provider_name.clone(); + let params = LlmCallExecuteParams::builder() + .name(provider_for_event) + .request(request) + .func(func) + .parent_opt(parent) + .attributes(attributes) + .metadata(metadata) + .model_name_opt(model_name) + .response_codec_opt(codecs.response) + .build(); + let result = TASK_SCOPE_STACK + .scope(scope_stack, async move { llm_call_execute(params).await }) + .await; + match result { + Ok(response_json) => { state .sessions - .end_llm( - active, - json!({ "error": error.to_string() }), - json!({ "gateway_error": true, "stage": "send" }), - ) - .await?; - Err(SidecarError::Upstream(error)) + .record_gateway_response_hints(&session_id, owner_subagent_id, response_json) + .await; + let (status, headers) = upstream_info + .lock() + .expect("upstream info lock poisoned") + .take() + .unwrap_or((StatusCode::OK, HeaderMap::new())); + let bytes = response_bytes + .lock() + .expect("response bytes lock poisoned") + .take() + .unwrap_or_default(); + build_response(status, headers, Body::from(bytes)) } + Err(error) => Err(translate_runtime_error(error, &upstream_error)), } } -// 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(); - request_streaming || content_type.contains("text/event-stream") +// Builds the managed-execution callback for a non-streaming route. The closure forwards the +// buffered request bytes upstream, captures the status and headers into `upstream_info` so the +// outer code can rebuild the client response, and returns the upstream JSON payload to the runtime. +fn build_buffered_func( + state: AppState, + prepared: &PreparedGatewayRequest, + upstream_info: UpstreamResponseInfo, + upstream_error: UpstreamErrorSlot, + response_bytes: Arc>>, +) -> LlmExecutionNextFn { + let http = state.http.clone(); + let method = prepared.method.clone(); + let url = prepared.upstream_url.clone(); + let body_bytes = prepared.body_bytes.clone(); + let headers = prepared.headers.clone(); + Arc::new(move |_request| { + let http = http.clone(); + let method = method.clone(); + let url = url.clone(); + let body_bytes = body_bytes.clone(); + let headers = headers.clone(); + let upstream_info = upstream_info.clone(); + let upstream_error = upstream_error.clone(); + let response_bytes = response_bytes.clone(); + Box::pin(async move { + let response = + match forward_upstream_request(&http, &method, &url, &body_bytes, &headers).await { + Ok(response) => response, + Err(error) => { + let message = error.to_string(); + *upstream_error.lock().expect("upstream error lock poisoned") = Some(error); + return Err(FlowError::Internal(message)); + } + }; + let status = response.status(); + let response_headers = response_headers(response.headers()); + let bytes = match response.bytes().await { + Ok(bytes) => bytes, + Err(error) => { + let message = error.to_string(); + *upstream_error.lock().expect("upstream error lock poisoned") = Some(error); + return Err(FlowError::Internal(message)); + } + }; + let json = serde_json::from_slice::(&bytes) + .unwrap_or_else(|_| json!({ "body_bytes": bytes.len() })); + *upstream_info.lock().expect("upstream info lock poisoned") = + Some((status, response_headers)); + *response_bytes.lock().expect("response bytes lock poisoned") = Some(bytes); + Ok(json) + }) + }) } -// 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, +// Runs a streaming gateway request through `llm_stream_call_execute`. The runtime wraps the +// upstream byte stream as `LlmJsonStream`; the gateway then re-encodes the parsed events back into +// SSE frames for the client (Option B trade-off: simpler chunk-level observability, one extra +// JSON parse/serialize per chunk). +async fn run_managed_streaming( + state: AppState, + prepared: PreparedGatewayRequest, + prep: GatewayCallPrep, + codecs: RouteCodecs, ) -> 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 { + let upstream_info: UpstreamResponseInfo = Arc::new(Mutex::new(None)); + let upstream_error: UpstreamErrorSlot = Arc::new(Mutex::new(None)); + let func = build_streaming_func( + state.clone(), + &prepared, + upstream_info.clone(), + upstream_error.clone(), + ); + let provider_route = prepared.provider; + + // Streaming routes that lack a codec fall back to byte passthrough. The runtime requires a + // collector and finalizer for managed streaming, so without a codec we cannot use the managed + // pipeline. This keeps non-LLM streaming paths working while typed codecs remain optional. + let Some(streaming_codec) = codecs.streaming else { + return passthrough_streaming(state, prepared).await; + }; + let collector = streaming_codec.collector(); + let finalizer = streaming_codec.finalizer(); + + let GatewayCallPrep { + scope_stack, + session_id, + provider_name, + request, + parent, + attributes, + metadata, + model_name, + owner_subagent_id, + } = prep; + let params = LlmStreamCallExecuteParams::builder() + .name(provider_name) + .request(request) + .func(func) + .collector(collector) + .finalizer(finalizer) + .parent_opt(parent) + .attributes(attributes) + .metadata(metadata) + .model_name_opt(model_name) + .response_codec_opt(codecs.response) + .build(); + let json_stream_result = TASK_SCOPE_STACK + .scope( + scope_stack, + async move { llm_stream_call_execute(params).await }, + ) + .await; + let json_stream = + json_stream_result.map_err(|error| translate_runtime_error(error, &upstream_error))?; + let (status, headers) = upstream_info + .lock() + .expect("upstream info lock poisoned") + .take() + .unwrap_or((StatusCode::OK, HeaderMap::new())); + let body = client_sse_body(json_stream, provider_route); + + // Tool hint extraction from streamed responses is intentionally deferred: the runtime + // synthesizes the aggregate response inside `LlmStreamWrapper::emit_end_event` and does not + // surface it back to the caller. Non-streamed responses continue to feed + // `record_gateway_response_hints` from `run_managed_buffered`. Wiring streamed hints back would + // require either a runtime hook or a finalizer-tap channel; neither is in scope for the + // initial managed-execution refactor. + let _ = (session_id, owner_subagent_id); + + build_response(status, headers, body) +} + +// Builds the streaming managed-execution callback. The runtime drives the returned future, which +// fires the upstream request, captures the status + headers into `upstream_info`, and yields a +// stream of parsed SSE event JSON values for the runtime collector. +fn build_streaming_func( + state: AppState, + prepared: &PreparedGatewayRequest, + upstream_info: UpstreamResponseInfo, + upstream_error: UpstreamErrorSlot, +) -> LlmStreamExecutionNextFn { + let http = state.http.clone(); + let method = prepared.method.clone(); + let url = prepared.upstream_url.clone(); + let body_bytes = prepared.body_bytes.clone(); + let headers = prepared.headers.clone(); + Arc::new(move |_request| { + let http = http.clone(); + let method = method.clone(); + let url = url.clone(); + let body_bytes = body_bytes.clone(); + let headers = headers.clone(); + let upstream_info = upstream_info.clone(); + let upstream_error = upstream_error.clone(); + Box::pin(async move { + let response = + match forward_upstream_request(&http, &method, &url, &body_bytes, &headers).await { + Ok(response) => response, + Err(error) => { + let message = error.to_string(); + *upstream_error.lock().expect("upstream error lock poisoned") = Some(error); + return Err(FlowError::Internal(message)); + } + }; + let status = response.status(); + let response_headers = response_headers(response.headers()); + *upstream_info.lock().expect("upstream info lock poisoned") = + Some((status, response_headers)); + let json_stream = sse_json_stream(response); + Ok(json_stream) + }) + }) +} + +// Decodes an upstream SSE byte stream into a stream of parsed `data:` JSON payloads. Frames with no +// `data:` line (heartbeats), comments, and the `data: [DONE]` sentinel are filtered out by the +// shared `SseEventDecoder`. Trailing partial frames are surfaced to the runtime so the collector +// observes whatever the upstream sent before disconnect. +fn sse_json_stream(response: reqwest::Response) -> LlmJsonStream { + use nemo_flow::codec::streaming::SseEventDecoder; + let mut decoder = SseEventDecoder::new(); + let mut bytes = response.bytes_stream(); + let stream = stream! { + while let Some(chunk) = bytes.next().await { match chunk { - Ok(bytes) => { - if collected.len() + bytes.len() <= 1_048_576 { - collected.extend_from_slice(&bytes); - } else { - truncated = true; + Ok(buffer) => { + match decoder.push_bytes(&buffer) { + Ok(events) => { + for event in events { + yield Ok(event.data); + } + } + Err(error) => { + yield Err(error); + return; + } } - yield Ok::(bytes); } Err(error) => { - llm.end_error("stream", error.to_string()).await; - yield Err(error); + yield Err(FlowError::Internal(error.to_string())); return; } } } - 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) => { - 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)); + match decoder.finish() { + Ok(Some(event)) => yield Ok(event.data), + Ok(None) => {} + Err(error) => yield Err(error), } }; - let response_json = serde_json::from_slice::(&bytes) - .unwrap_or_else(|_| json!({ "body_bytes": bytes.len() })); - sessions - .end_llm( - active, - response_json, - json!({ "http_status": status.as_u16(), "streaming": false }), - ) - .await?; - build_response(status, headers, Body::from(bytes)) + Box::pin(stream) } -struct StreamingLlmGuard { - sessions: SessionManager, - active: Option, - status: StatusCode, +// Re-encodes a runtime JSON stream as `text/event-stream` frames for the downstream client. Event +// names are reconstructed from the JSON `type` field where providers populate it (Anthropic +// Messages, OpenAI Responses); OpenAI Chat omits the `event:` line and appends the original +// `data: [DONE]` terminator after the runtime stream completes. +fn client_sse_body(json_stream: LlmJsonStream, route: ProviderRoute) -> Body { + let mut json_stream = json_stream; + let stream = stream! { + while let Some(item) = json_stream.next().await { + match item { + Ok(event_json) => { + let frame = encode_sse_frame(&event_json, route); + yield Ok::(Bytes::from(frame)); + } + Err(error) => { + yield Err(SidecarError::InvalidPayload(error.to_string())); + return; + } + } + } + if matches!(route, ProviderRoute::OpenAiChatCompletions) { + yield Ok::(Bytes::from_static(b"data: [DONE]\n\n")); + } + }; + Body::from_stream(stream) } -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, - active: Some(active), - status, - } +// Formats one SSE frame from a parsed event payload. Anthropic and OpenAI Responses events carry +// the event name in the `type` field, so it is mirrored back onto the `event:` line; OpenAI Chat +// chunks have no event name and emit only `data:`. +fn encode_sse_frame(event_json: &Value, route: ProviderRoute) -> String { + let serialized = serde_json::to_string(event_json).unwrap_or_else(|_| "null".to_string()); + let event_name = match route { + ProviderRoute::AnthropicMessages | ProviderRoute::OpenAiResponses => event_json + .get("type") + .and_then(Value::as_str) + .map(ToOwned::to_owned), + _ => None, + }; + match event_name { + Some(name) => format!("event: {name}\ndata: {serialized}\n\n"), + None => format!("data: {serialized}\n\n"), } +} - // 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 - .sessions - .end_llm( - active, - response, - json!({ "http_status": self.status.as_u16(), "streaming": true, "stream_truncated": truncated }), - ) - .await; +// Forwards the buffered request to the upstream provider with only the safe request headers. This +// is shared by the buffered and streaming managed funcs so header filtering stays consistent. +async fn forward_upstream_request( + http: &reqwest::Client, + method: &Method, + url: &str, + body_bytes: &Bytes, + headers: &HeaderMap, +) -> Result { + let mut upstream = http.request(method.clone(), url).body(body_bytes.clone()); + for (name, value) in headers { + if should_forward_request_header(name) { + upstream = upstream.header(name, value); } } + upstream.send().await +} - // 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 - .sessions - .end_llm( - active, - json!({ "error": error }), - json!({ "http_status": self.status.as_u16(), "streaming": true, "gateway_error": true, "stage": stage }), - ) - .await; +// Plain byte passthrough used for streaming routes that lack a typed codec. The managed pipeline +// requires a collector + finalizer, so without a codec we keep the simpler proxy behavior and skip +// the LLM lifecycle event for that single request. +async fn passthrough_streaming( + state: AppState, + prepared: PreparedGatewayRequest, +) -> Result, SidecarError> { + let response = forward_upstream_request( + &state.http, + &prepared.method, + &prepared.upstream_url, + &prepared.body_bytes, + &prepared.headers, + ) + .await?; + let status = response.status(); + let headers = response_headers(response.headers()); + let mut bytes = response.bytes_stream(); + let body = Body::from_stream(stream! { + while let Some(chunk) = bytes.next().await { + yield chunk; } - } + }); + build_response(status, headers, body) } -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; - }; - 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; - }); - } +// Translates a runtime [`FlowError`] from managed execution into a sidecar HTTP error. When the +// failure originated from upstream send/body work, the captured `reqwest::Error` is preferred so +// the response status reflects 502 Bad Gateway rather than the generic 400 from a guardrail or +// internal sidecar error. +fn translate_runtime_error(error: FlowError, upstream_error: &UpstreamErrorSlot) -> SidecarError { + if let Some(upstream) = upstream_error + .lock() + .expect("upstream error lock poisoned") + .take() + { + return SidecarError::Upstream(upstream); + } + match error { + FlowError::GuardrailRejected(reason) => SidecarError::InvalidPayload(reason), + other => SidecarError::InvalidPayload(other.to_string()), } } @@ -500,10 +751,12 @@ fn observable_headers(headers: &HeaderMap) -> Map { // 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. +// Content-Length is also dropped because the gateway re-encodes streaming responses and the +// upstream-reported length will not match the bytes the client sees. fn response_headers(headers: &HeaderMap) -> HeaderMap { let mut output = HeaderMap::new(); for (name, value) in headers { - if !is_hop_by_hop(name) { + if !is_hop_by_hop(name) && name != http::header::CONTENT_LENGTH { output.append(name.clone(), value.clone()); } } @@ -565,18 +818,6 @@ 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!({ - "stream_preview": String::from_utf8_lossy(collected), - "stream_truncated": true - }); - } - json!({ "stream": String::from_utf8_lossy(collected) }) -} - #[cfg(test)] #[path = "../tests/coverage/gateway_tests.rs"] mod tests; diff --git a/crates/sidecar/src/session.rs b/crates/sidecar/src/session.rs index 9a90b63b..4bf37134 100644 --- a/crates/sidecar/src/session.rs +++ b/crates/sidecar/src/session.rs @@ -54,6 +54,10 @@ pub(crate) struct LlmGatewayStart { pub(crate) metadata: Value, } +/// Legacy active-LLM record kept for tests that exercise the manual `llm_call` / +/// `llm_call_end` correlation path. Production gateway traffic now uses managed execution via +/// [`SessionManager::prepare_gateway_call`]. +#[cfg(test)] #[derive(Debug, Clone)] pub(crate) struct ActiveLlm { stack: ScopeStackHandle, @@ -62,6 +66,25 @@ pub(crate) struct ActiveLlm { owner_subagent_id: Option, } +/// Inputs prepared by [`SessionManager::prepare_gateway_call`] for invoking the +/// runtime's managed LLM execution pipeline outside the session lock. +/// +/// The session lock is released after the prep is built, so the gateway can run +/// the upstream HTTP work without blocking unrelated session activity. The +/// preserved `scope_stack` is what restores the agent/subagent scope context +/// the call was opened against when the runtime emits start/end events. +pub(crate) struct GatewayCallPrep { + pub(crate) scope_stack: ScopeStackHandle, + pub(crate) session_id: String, + pub(crate) provider_name: String, + pub(crate) request: LlmRequest, + pub(crate) parent: Option, + pub(crate) attributes: LlmAttributes, + pub(crate) metadata: Value, + pub(crate) model_name: Option, + pub(crate) owner_subagent_id: Option, +} + struct Session { agent_kind: AgentKind, session_id: String, @@ -181,7 +204,9 @@ impl SessionManager { Ok(()) } - /// Starts a gateway-observed LLM call and correlates it with the best available session. + /// Legacy manual-lifecycle entry point retained for tests that drive correlation behavior + /// directly. Production gateway traffic uses [`Self::prepare_gateway_call`] + + /// `llm_call_execute` / `llm_stream_call_execute` so the runtime owns start/end events. /// /// 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 @@ -192,6 +217,7 @@ impl SessionManager { /// session, even after a SessionStart hook arrives, because observer identities are baked at /// scope-open time. With it, an Anthropic Messages call before SessionStart still labels the /// trace as `claude-code`, an OpenAI Responses call as `codex`, etc. + #[cfg(test)] pub(crate) async fn start_llm( &self, headers: &HeaderMap, @@ -211,10 +237,44 @@ impl SessionManager { session.start_llm(start).await } - /// Ends an active gateway-observed LLM call on the scope stack that created it. + /// Prepares a managed LLM execution against the right session and scope context. + /// + /// Resolves the session, opens the agent scope if needed, computes the parent scope and + /// correlation metadata, and returns a [`GatewayCallPrep`]. The returned prep carries the + /// `ScopeStackHandle` that callers must restore around `llm_call_execute` / + /// `llm_stream_call_execute` so the runtime emits start/end events under the same agent or + /// subagent scope the prep was opened under. + /// + /// The session manager lock is held only long enough to build the prep; the actual upstream + /// HTTP and managed pipeline run outside the lock. + pub(crate) async fn prepare_gateway_call( + &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_id = start + .session_id + .clone() + .or_else(|| single_active_session_id(&sessions)) + .unwrap_or_else(|| format!("{}-gateway", AgentKind::Gateway.as_str())); + // Match `start_llm`: when this path creates a brand-new session (real agent's gateway + // request beats its SessionStart hook), label the session by the provider so ATIF and + // Phoenix scopes carry the agent identity instead of freezing on "gateway". + let inferred_agent_kind = agent_kind_for_gateway_provider(&start.provider); + let session = sessions + .entry(session_id.clone()) + .or_insert_with(|| Session::new(session_id, inferred_agent_kind, config)); + session.prepare_gateway_call(start).await + } + + /// Legacy manual-lifecycle close paired with [`Self::start_llm`]. Production gateway traffic + /// no longer needs this helper because managed execution emits the end event automatically. /// /// 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. + #[cfg(test)] pub(crate) async fn end_llm( &self, active: ActiveLlm, @@ -253,14 +313,22 @@ impl SessionManager { Ok(()) } - #[cfg(test)] - pub(crate) async fn session_llms_empty(&self, session_id: &str) -> bool { - self.inner - .lock() - .await - .get(session_id) - .map(|session| session.llms.is_empty()) - .unwrap_or(true) + /// Records tool-call hints from a completed gateway response onto the owning session. + /// + /// The runtime owns the LLM lifecycle when the gateway uses managed execution, so the + /// per-response tool-hint extraction that `end_llm` would normally do has to be triggered + /// explicitly after the managed pipeline returns. Missing or already-removed sessions are + /// silently skipped because hints are advisory. + pub(crate) async fn record_gateway_response_hints( + &self, + session_id: &str, + owner_subagent_id: Option, + response: Value, + ) { + let mut sessions = self.inner.lock().await; + if let Some(session) = sessions.get_mut(session_id) { + session.add_tool_hints_from_llm_response(response, owner_subagent_id); + } } #[cfg(test)] @@ -334,9 +402,9 @@ impl Session { Ok(()) } - // 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. + // Legacy manual-lifecycle gateway start used by tests. Production code uses + // `prepare_gateway_call` + managed execution. + #[cfg(test)] async fn start_llm(&mut self, start: LlmGatewayStart) -> Result { let stack = self.scope_stack.clone(); TASK_SCOPE_STACK @@ -377,6 +445,45 @@ impl Session { .await } + // Builds a managed-execution prep without creating an LlmHandle. The agent scope is opened if + // needed and ownership/correlation metadata is computed exactly as the manual `start_llm` path + // does. The handle and start/end events are emitted later by `llm_call_execute` / + // `llm_stream_call_execute`, which the gateway runs outside the session lock. + async fn prepare_gateway_call( + &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 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(), + ); + Ok(GatewayCallPrep { + scope_stack: stack, + session_id: self.session_id.clone(), + provider_name: start.provider, + request: start.request, + parent: owner.parent, + attributes, + metadata, + model_name: start.model_name, + 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> { diff --git a/crates/sidecar/tests/coverage/gateway_tests.rs b/crates/sidecar/tests/coverage/gateway_tests.rs index 76344225..03474803 100644 --- a/crates/sidecar/tests/coverage/gateway_tests.rs +++ b/crates/sidecar/tests/coverage/gateway_tests.rs @@ -3,50 +3,14 @@ use super::*; use crate::config::SidecarConfig; -use crate::model::{AgentKind, NormalizedEvent, SessionEvent}; use crate::server::AppState; +use crate::session::SessionManager; use axum::body::Body; use axum::extract::State; use axum::http::{HeaderMap, HeaderValue, Method, Request, StatusCode}; use http_body_util::BodyExt; use reqwest::Client; -async fn wait_for_file_contains( - path: &std::path::Path, - needle: &str, - timeout: std::time::Duration, -) -> bool { - let deadline = std::time::Instant::now() + timeout; - loop { - if let Ok(contents) = std::fs::read_to_string(path) - && contents.contains(needle) - { - return true; - } - if std::time::Instant::now() >= deadline { - return false; - } - tokio::time::sleep(std::time::Duration::from_millis(25)).await; - } -} - -async fn wait_for_session_llms_empty( - sessions: &SessionManager, - session_id: &str, - timeout: std::time::Duration, -) -> bool { - let deadline = std::time::Instant::now() + timeout; - loop { - if sessions.session_llms_empty(session_id).await { - return true; - } - if std::time::Instant::now() >= deadline { - return false; - } - tokio::time::sleep(std::time::Duration::from_millis(25)).await; - } -} - #[test] fn removes_hop_by_hop_headers() { assert!(!should_forward_request_header(&HeaderName::from_static( @@ -306,84 +270,7 @@ fn response_headers_preserve_duplicates() { assert_eq!(copied.get_all("set-cookie").iter().count(), 2); } -#[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 }) - ); -} - -#[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()), - subagent_id: None, - conversation_id: None, - generation_id: None, - request_id: None, - 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, - )); - // Drop cleanup runs in a spawned task, so poll the session state instead of sleeping. - assert!( - wait_for_session_llms_empty(&sessions, "drop-session", std::time::Duration::from_secs(5)) - .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_path = temp.path().join("drop-session.atif.json"); - assert!( - wait_for_file_contains( - &atif_path, - "stream body dropped before completion", - std::time::Duration::from_secs(5), - ) - .await - ); -} +// `stream_response_records_preview_and_truncation` and `streaming_llm_guard_closes_on_drop` were +// removed when the gateway moved to `llm_stream_call_execute`. The runtime now owns stream-end +// lifecycle (start/end events emitted by `LlmStreamWrapper`); core tests cover that contract, +// and the gateway no longer carries a stream preview/truncation helper or a separate guard struct. diff --git a/crates/sidecar/tests/coverage/server_tests.rs b/crates/sidecar/tests/coverage/server_tests.rs index fb073df1..b4ea9181 100644 --- a/crates/sidecar/tests/coverage/server_tests.rs +++ b/crates/sidecar/tests/coverage/server_tests.rs @@ -287,7 +287,24 @@ async fn gateway_preserves_streaming_body() { "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")); + let body_str = std::str::from_utf8(&bytes).unwrap(); + // Managed execution re-encodes each parsed event with the OpenAI Responses event name on + // its own `event:` line, so the wire shape is closer to the spec but not byte-identical to + // the upstream feed. Both event payloads should appear in order. + assert!( + body_str.contains("event: response.created"), + "missing response.created event: {body_str}", + ); + assert!( + body_str.contains("event: response.completed"), + "missing response.completed event: {body_str}", + ); + let created_idx = body_str.find("response.created").unwrap(); + let completed_idx = body_str.find("response.completed").unwrap(); + assert!( + created_idx < completed_idx, + "events out of order: {body_str}" + ); } #[tokio::test] @@ -396,9 +413,16 @@ async fn spawn_upstream(streaming: bool) -> TestServer { } async fn stream_response() -> impl IntoResponse { + // OpenAI Responses managed pipeline parses each `data:` payload as JSON; emit minimally + // valid response.created / response.completed events so the runtime collector + finalizer + // assemble a well-formed end-event payload. 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")), + Ok::<_, std::convert::Infallible>(Bytes::from_static( + b"data: {\"type\":\"response.created\",\"response\":{\"id\":\"r1\"}}\n\n", + )), + Ok(Bytes::from_static( + b"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"r1\"}}\n\n", + )), ]); ( [(header::CONTENT_TYPE, "text/event-stream")], @@ -426,8 +450,12 @@ async fn spawn_upstream(streaming: bool) -> TestServer { async fn spawn_failing_stream_upstream() -> TestServer { async fn stream_response() -> impl IntoResponse { + // First chunk is a valid JSON SSE event so the managed pipeline opens cleanly; the + // following IO error simulates the upstream socket dropping mid-stream. let chunks = stream::iter([ - Ok::<_, std::io::Error>(Bytes::from_static(b"data: one\n\n")), + Ok::<_, std::io::Error>(Bytes::from_static( + b"data: {\"type\":\"response.created\",\"response\":{\"id\":\"r1\"}}\n\n", + )), Err(std::io::Error::other("stream failed")), ]); (