diff --git a/.gitattributes b/.gitattributes index a41210a715..7eb4e67d7e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,7 @@ *.ico binary *.zip binary *.gz binary + +# This vendored donor asset is intentionally byte-identical to its pinned source, +# including two trailing-space lines. Keep whitespace diagnostics enabled elsewhere. +src/guardrails/rules/guardrails_regex_rules.yaml -whitespace diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00abe5ec24..cf357bf984 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,7 @@ on: - "tests/**" - "scripts/**" - "gui/**" + - "docs-site/**" - "assets/**" - ".gitattributes" - ".npmignore" @@ -43,6 +44,8 @@ on: - "tsconfig.json" - "README.md" - "LICENSE" + - "THIRD_PARTY_NOTICES.md" + - "LICENSES/**" - ".github/workflows/ci.yml" - ".github/workflows/release.yml" - ".github/workflows/enforce-pr-target.yml" @@ -154,6 +157,7 @@ jobs: # step. A missing or malformed filter output must fail this job instead # of silently making every expensive job skip. ci: ${{ steps.scope.outputs.ci }} + docs: ${{ steps.scope.outputs.docs }} gui: ${{ steps.filter.outputs.gui }} packaging: ${{ steps.filter.outputs.packaging }} steps: @@ -193,6 +197,7 @@ jobs: - 'tests/**' - 'scripts/**' - 'gui/**' + - 'docs-site/**' - 'assets/**' - '.gitattributes' - '.npmignore' @@ -201,12 +206,17 @@ jobs: - 'tsconfig.json' - 'README.md' - 'LICENSE' + - 'THIRD_PARTY_NOTICES.md' + - 'LICENSES/**' - '.github/workflows/ci.yml' - '.github/workflows/release.yml' - '.github/workflows/enforce-pr-target.yml' - '.github/workflows/stale-needs-info.yml' gui: - 'gui/**' + docs: + - 'docs-site/**' + - '.github/workflows/ci.yml' # Everything that ends up inside `npm pack`, or that decides what # does. `src/**` belongs here because package.json ships `src` and # bin/ocx.mjs executes it: without that entry an ordinary source PR @@ -226,13 +236,18 @@ jobs: - '.gitattributes' - 'README.md' - 'LICENSE' + - 'THIRD_PARTY_NOTICES.md' + - 'LICENSES/**' + - 'scripts/guardrails-provenance.ts' - 'scripts/prepare-package.ts' + - 'scripts/verify-guardrails-package.ts' - name: Assert the scope output is usable id: scope shell: bash env: CI_SCOPE: ${{ steps.filter.outputs.ci }} + DOCS_SCOPE: ${{ steps.filter.outputs.docs }} run: | set -euo pipefail case "$CI_SCOPE" in @@ -244,6 +259,15 @@ jobs: exit 1 ;; esac + case "$DOCS_SCOPE" in + true|false) + printf 'docs=%s\n' "$DOCS_SCOPE" >> "$GITHUB_OUTPUT" + ;; + *) + printf '::error::changes.outputs.docs was %q, expected true or false\n' "$DOCS_SCOPE" + exit 1 + ;; + esac # The suite, split by file across four Linux runners. # @@ -472,6 +496,29 @@ jobs: - name: CLI help smoke run: bun run src/cli/index.ts help + docs-build: + name: docs build + needs: changes + if: needs.changes.outputs.docs == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup project Bun + uses: ./.github/actions/setup-project-bun + + - name: Install documentation dependencies + working-directory: docs-site + run: bun install --frozen-lockfile + + - name: Build documentation + working-directory: docs-site + run: bun run build + platform-macos: name: macos ${{ matrix.shard }}/2 needs: changes @@ -979,6 +1026,9 @@ jobs: - name: Build package assets run: npm run build:gui + - name: Verify Guardrails package assets and WASM scanner + run: npx --no-install bun scripts/verify-guardrails-package.ts + - name: Pack shell: bash run: npm pack --json > pack.json @@ -1015,7 +1065,7 @@ jobs: # direct dependencies only, so a failing `select-windows-runner` would # otherwise reach this gate as nothing at all while its dependents report # `skipped` — which the gate is required to read as a deliberate skip. - needs: [changes, select-windows-runner, test, storage-policy, api-usage, gates, platform-macos, macos-control, platform-windows, keyring-smoke, docker-smoke, npm-global-smoke] + needs: [changes, select-windows-runner, test, storage-policy, api-usage, gates, docs-build, platform-macos, macos-control, platform-windows, keyring-smoke, docker-smoke, npm-global-smoke] runs-on: ubuntu-latest timeout-minutes: 5 steps: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08db5a6bbb..d4b7259836 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,6 +56,23 @@ A ready-for-review PR is the author's claim that the change is complete, underst stated. A closed PR can be reopened once the stated reason is resolved, or replaced with a clean one. +## Guardrails synthetic test fixtures + +Guardrails detector tests intentionally contain non-credential strings shaped +like provider keys. The donor fixture corpus is pinned by SHA-256 and provenance; +the local privacy exemption stops applying when its bytes change. During the +first #4022 fork push on 2026-09-08, GitHub reported 33 pattern matches, including +duplicate detections. Each displayed match was checked against its historical +synthetic test source and individually allowed as test data with contributor +approval. Repository-wide scanning remained unchanged and history was not +rewritten. This record is not a blanket allowance for `tests/` or future values. + +For a new detection, verify its exact value, path and provenance before asking +the authorized repository owner to accept a test-only exception. Never classify +an unknown or real credential as test data to unblock a push. Fixture changes, +their local hash exemptions and upstream-repository allowances require fresh +review; a fork allowance is not maintainer security sponsorship. + ## Pre-push hook After cloning, run once to install a local pre-push hook that runs the typecheck, diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000000..8dada3edaf --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSES/Gitleaks-MIT.txt b/LICENSES/Gitleaks-MIT.txt new file mode 100644 index 0000000000..3c270b36c5 --- /dev/null +++ b/LICENSES/Gitleaks-MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Zachary Rice + +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. diff --git a/LICENSES/Google-RE2-BSD-3-Clause.txt b/LICENSES/Google-RE2-BSD-3-Clause.txt new file mode 100644 index 0000000000..09e5ec1c74 --- /dev/null +++ b/LICENSES/Google-RE2-BSD-3-Clause.txt @@ -0,0 +1,27 @@ +// Copyright (c) 2009 The RE2 Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. 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 +// OWNER 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. diff --git a/LICENSES/node-re2-BSD-3-Clause.txt b/LICENSES/node-re2-BSD-3-Clause.txt new file mode 100644 index 0000000000..b909eb6078 --- /dev/null +++ b/LICENSES/node-re2-BSD-3-Clause.txt @@ -0,0 +1,34 @@ +This library is available under the terms of the modified BSD license. No external contributions +are allowed under licenses which are fundamentally incompatible with the BSD license that this library is distributed under. + +The text of the BSD license is reproduced below. + +------------------------------------------------------------------------------- +The "New" BSD License: +********************** + +Copyright (c) 2005-2020, Eugene Lazutkin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * 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. + * Neither the name of Eugene Lazutkin nor the names of other 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 OWNER 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. diff --git a/LICENSES/re2-wasm-Apache-2.0.txt b/LICENSES/re2-wasm-Apache-2.0.txt new file mode 100644 index 0000000000..7a4a3ea242 --- /dev/null +++ b/LICENSES/re2-wasm-Apache-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/LICENSES/yaml-ISC.txt b/LICENSES/yaml-ISC.txt new file mode 100644 index 0000000000..e060aaa1fe --- /dev/null +++ b/LICENSES/yaml-ISC.txt @@ -0,0 +1,13 @@ +Copyright Eemeli Aro + +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. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000000..8f6e1443d2 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,103 @@ +# Third-Party Notices + +This distribution contains the following third-party materials. These notices +describe origin and attribution only; they do not imply endorsement by any +named project or organization. + +## guardrails-llm-filter rule assets + +- Disposition: copied unchanged. +- Source: `cloud-ru-tech/guardrails-llm-filter@bbd6f27467a53ff3869b59449edf4209f85ae675`. +- Source paths: `configs/guardrails_regex_rules.yaml` and + `configs/guardrails_regex_rules.gitleaks.generated.yaml`. +- Distributed paths: `src/guardrails/rules/guardrails_regex_rules.yaml` and + `src/guardrails/rules/guardrails_regex_rules.gitleaks.generated.yaml`. +- License source path: `LICENSE`; distributed full text: + `LICENSES/Apache-2.0.txt`. +- NOTICE source path: `NOTICE`; the complete notice text relevant to these + assets is reproduced below. + +Original NOTICE attribution: + +> guardrails-llm-filter +> Copyright 2026 Cloud.ru +> +> This product includes detection rules derived from the gitleaks project +> (https://github.com/gitleaks/gitleaks), licensed under the MIT License. +> See configs/gitleaks.toml and the generated +> configs/guardrails_regex_rules.gitleaks.generated.yaml. + +### Donor rule parity fixture + +- Disposition: mechanically transformed. +- Source: + `cloud-ru-tech/guardrails-llm-filter@bbd6f27467a53ff3869b59449edf4209f85ae675`. +- Source path: `tests/rules/rules_cases_test.go`; SHA-256 + `b7eae600c5a1658a69f948ab355cbc25f2b48bdc9f64d3ad3793115711d91214`. +- Distributed test path: `tests/fixtures/guardrails-donor-rule-cases.json`; + the Go table fields were converted to versioned JSON without changing case values. +- License: Apache-2.0; the full text is distributed at + `LICENSES/Apache-2.0.txt`. + +## Gitleaks rule configuration + +- Disposition: upstream-derived configuration represented in the copied + generated rule asset; the source configuration itself is not distributed. +- Source: `gitleaks/gitleaks@09242ce9c8a60d9b051fc2d166f9e849b88c7ac0`. +- Source configuration path: `config/gitleaks.toml`. +- Generated distributed path: + `src/guardrails/rules/guardrails_regex_rules.gitleaks.generated.yaml`. +- License source path: `LICENSE`; MIT, Copyright (c) 2019 Zachary Rice. + Distributed full text: `LICENSES/Gitleaks-MIT.txt`. + +## re2-wasm runtime dependency + +- Disposition: direct runtime dependency, not bundled in the OpenCodex tarball; + OpenCodex loads the exact dependency-owned artifacts described below. +- npm package: `re2-wasm@1.0.2`. +- Source commit: `63796eaa20e1eea74466c0c56e2785a64f7ae372`. +- npm integrity: + `sha512-VXUdgSiUrE/WZXn6gUIVVIsg0+Hp6VPZPOaHCay+OuFKy6u/8ktmeNEf+U5qSA8jzGGFsg8jrDNu1BeHpz2pJA==`. +- npm tarball SHA-1: `78c09dc651b8962aa814b55ae7fe5e472ec15bbb`. +- Installed path: dependency-owned `node_modules/re2-wasm`; no file from that + directory is copied into the OpenCodex package tarball. +- OpenCodex modification: `src/guardrails/re2-runtime.ts` verifies the stock + `build/wasm/re2.js` SHA-256, then modifies that loader only in process memory + before evaluation. It raises the fixed initial WebAssembly heap from 16 MiB + to 64 MiB and raises the validated imported-memory maximum from 256 to 1024 + pages. This prevents exhaustion while retaining all 266 reviewed rules. The + installed dependency files and npm tarball remain unchanged. +- Adapted source path at the pinned commit: `src/re2.ts` + (`escapeRegExp` and `translateRegExp`); Copyright 2021 Google LLC. +- Distributed adaptation path: `src/guardrails/registry.ts` + (`escapeRe2Pattern` and `translateRe2Pattern`). +- License source path at tag `v1.0.2`: `LICENSE`; Apache License 2.0. + Distributed exact full text: `LICENSES/re2-wasm-Apache-2.0.txt`. +- The package's `build/wasm/re2.js` is pinned by SHA-256 + `4bfa5d6a8dd0052da8d06baf171078a392dc9c70592d5dca90c9aefa9006336e`. +- The package's `build/wasm/re2.wasm` is pinned by SHA-256 + `79e025a30d20157807add5e7d01acefe700f06721f4a56669b0bad5d00995e72`. + +## yaml runtime dependency + +- Disposition: direct runtime dependency, not bundled in the OpenCodex tarball. +- npm package: `yaml@2.8.3`. +- Source repository and tag: `eemeli/yaml@v2.8.3`. +- npm integrity: + `sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==`. +- Installed path: dependency-owned `node_modules/yaml`; not copied into the + OpenCodex package tarball. +- License source path at tag `v2.8.3`: `LICENSE`; ISC, Copyright Eemeli Aro. + Distributed full text: `LICENSES/yaml-ISC.txt`. + +## RE2 and node-re2 attribution + +- RE2 source: `google/re2@166dbbeb3b0ab7e733b278e8f42a84f6882b8a25`, + embedded by `re2-wasm` under `third_party/re2`; standalone license source + path `LICENSE`, BSD-3-Clause; distributed exact full text: + `LICENSES/Google-RE2-BSD-3-Clause.txt`. +- node-re2 attribution: `google/re2-wasm@63796eaa20e1eea74466c0c56e2785a64f7ae372`, + adapted source under `third_party/node-re2`; license source path + `third_party/node-re2/LICENSE`, BSD-3-Clause, + Copyright (c) 2005-2020 Eugene Lazutkin; distributed full text: + `LICENSES/node-re2-BSD-3-Clause.txt`. diff --git a/bun.lock b/bun.lock index c4619c2866..78366b89cc 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,8 @@ "@modelcontextprotocol/sdk": "^1.30.0", "@napi-rs/keyring": "1.3.0", "bun": "1.4.0", + "re2-wasm": "1.0.2", + "yaml": "2.8.3", "zod": "4.4.3", }, "devDependencies": { @@ -268,6 +270,8 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "re2-wasm": ["re2-wasm@1.0.2", "", {}, "sha512-VXUdgSiUrE/WZXn6gUIVVIsg0+Hp6VPZPOaHCay+OuFKy6u/8ktmeNEf+U5qSA8jzGGFsg8jrDNu1BeHpz2pJA=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -310,6 +314,8 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs index b25586f7a6..766242233a 100644 --- a/docs-site/astro.config.mjs +++ b/docs-site/astro.config.mjs @@ -105,6 +105,7 @@ export default defineConfig({ { label: "Image Bridge", translations: { fr: "Pont d’images", ko: "이미지 브릿지", "zh-CN": "图像桥接", "zh-TW": "圖像橋接", ru: "Image Bridge", ja: "画像ブリッジ", tr: "Image Bridge" }, slug: "guides/image-bridge" }, { label: "Video Bridge", translations: { fr: "Pont vidéo", ko: "비디오 브릿지", "zh-CN": "视频桥接", "zh-TW": "影片橋接", ru: "Video Bridge", ja: "動画ブリッジ", tr: "Video Bridge" }, slug: "guides/video-bridge" }, { label: "Web Dashboard", translations: { fr: "Tableau de bord web", ko: "웹 대시보드", "zh-CN": "网页控制台", "zh-TW": "網頁儀表板", ru: "Веб-дашборд", ja: "ウェブダッシュボード", tr: "Web Kontrol Paneli" }, slug: "guides/web-dashboard" }, + { label: "Guardrails", translations: { fr: "Protection des données", ko: "데이터 보호", "zh-CN": "数据防护", "zh-TW": "資料防護", ru: "Защита данных", ja: "データ保護", tr: "Veri Koruması" }, slug: "guides/guardrails" }, { label: "Sub-agent Surface", translations: { fr: "Interface des sous-agents", ko: "서브에이전트 서피스", "zh-CN": "子代理界面", "zh-TW": "子代理介面", ru: "Интерфейс подагентов", ja: "サブエージェントサーフェス", tr: "Alt Ajan Arayüzü" }, slug: "guides/sub-agent-surface" }, ], }, diff --git a/docs-site/src/content/docs/guides/guardrails.md b/docs-site/src/content/docs/guides/guardrails.md new file mode 100644 index 0000000000..dd79626d62 --- /dev/null +++ b/docs-site/src/content/docs/guides/guardrails.md @@ -0,0 +1,350 @@ +--- +title: Guardrails +description: Opt-in sensitive-data detection and reversible placeholders inside the opencodex proxy. +--- + +Guardrails is an optional data-protection layer built into opencodex. It scans supported text fields +before an LLM request leaves the proxy. In enforcement mode it replaces detected values with +per-turn placeholders such as `` and restores only eligible assistant prose +on the way back to the same client. + +Guardrails is disabled by default. It adds no separate server, port, database, dashboard, or +monitoring stack. + +:::caution +Guardrails reduces accidental disclosure through supported proxy fields. It is not a general DLP +system, and it cannot prevent a model from inferring or rephrasing information that was otherwise +available to it. +::: + +## Enable it from the dashboard + +Open **Guardrails** in the opencodex sidebar. The workspace has five bookmarkable sections: + +| Section | Hash | Purpose | +| --- | --- | --- | +| Overview | `#guardrails` | Enablement, registry health, counters, top rules/categories, and recent metadata-only activity | +| Rules | `#guardrails/rules` | Search, filter, enable/disable, import/export, and manage custom rules | +| Tester | `#guardrails/tester` | Scan synthetic sample text through this OpenCodex Management API without sending it to an LLM provider or storing it server-side | +| Activity | `#guardrails/activity` | Inspect the bounded in-memory metadata ring | +| Settings | `#guardrails/settings` | Provider coverage, mode, failure policy, data types, and keyword prefilter | + +The first switch is an explicit opt-in. The safe initial settings are: + +- mode: **Enforce masking**; +- processing failure: **Block request**; +- all current and future providers protected; +- all six data types enabled; +- keyword prefilter off. + +Changes apply to newly admitted logical turns. A request or stream that has already started keeps +the immutable settings and placeholder mapping it began with. + +Action feedback appears inline above the section tabs. It can be dismissed with **Close** without +blocking navigation to another Guardrails section. + +### CLI and Management API + +This contribution is dashboard and Management API first. The 13 Guardrails endpoints are +documented in the [Management API reference](/reference/management-api/#guardrails), but a +dedicated non-interactive `ocx guardrails` command is intentionally deferred as one complete +follow-up. The existing `ocx gui` command only opens the dashboard and is not presented as a CLI +client for those endpoints. + +## Choose the mode and failure policy + +Mode and failure policy are separate controls: + +| Setting | Behavior | +| --- | --- | +| `enforce` | Masks detected values before upstream I/O and restores issued placeholders only in non-executable assistant text. | +| `detect` | Counts request findings but preserves the baseline wire payload produced by the normal opencodex protocol translation and leaves the provider response unchanged. It does **not** protect data sent to the provider and does not persist the request for Responses continuation replay. | +| `block` | Default failure policy. A request-side scanner, registry, traversal, or capacity failure stops the request before upstream I/O. | +| `passthrough` | Explicit fail-open policy. A Guardrails processing failure may send the original, unmasked request upstream. The dashboard keeps a permanent warning while this policy is selected and records a high-severity metadata event when it is used. | + +The selected failure policy applies consistently to Responses, Chat Completions, native and routed +Messages, and `messages/count_tokens`. Request-side block failures stop before upstream I/O; +request-side passthrough failures forward the admitted original request rather than a partially +transformed body. Response-side demasking happens after upstream I/O and always fails safely: +if restoration cannot complete within its bounds, the client receives the still-masked response +with a metadata-only warning, regardless of the selected failure policy. + +Switching Guardrails off, selecting detect-only mode, or enabling passthrough requires a consequence +confirmation in the dashboard. + +## Choose protected providers + +An enabled Guardrails policy protects all providers by default. In **Settings**, clear a provider +checkbox to switch to selected-provider mode. Requests routed directly to unchecked providers are +not scanned or masked. This is a security-relevant reduction in coverage, so the dashboard shows a +consequence confirmation and a persistent warning. + +The selection uses canonical provider IDs from `config.providers`, not model names, account labels, +or display names. The built-in native Anthropic credential path has the stable ID +`anthropic-native` and can be selected independently from a routed provider named `anthropic`. +That synthetic ID is reserved and cannot also be used as a configured provider name. +Selected-provider mode never auto-enables a newly configured provider; switch back to all-provider +mode if new providers should inherit protection automatically. + +Removed provider IDs remain visible as **not configured** so stale intent can be repaired. If a +selected scope contains only removed or disabled configured providers, the dashboard reports +**No providers protected**: scanning may still work in the local Tester, but current provider +traffic is not covered. + +Each concrete policy-fallback attempt is evaluated against its provider ID. A combo is unprotected +only when every physical target is unchecked; a mixed combo is conservatively protected as one +logical turn so its fallback attempts cannot mix raw and placeholder state. A protected attempt +keeps one immutable policy, masked request body, and placeholder mapping for its lifetime. A +policy attempt that starts on an unchecked provider is stopped locally if fallback would cross +to a checked provider with raw input. A continuation that +already carries a Guardrails enforce marker cannot resume through an unchecked provider; opencodex +returns HTTP `409 guardrails_policy_changed` before upstream I/O. + +## Data types and rules + +The built-in registry contains 272 rules. Of those, 266 are pinned donor rules: 46 manually +curated rules from `guardrails-llm-filter` and 220 rules generated from a pinned Gitleaks +configuration. Six additional MIT-licensed OpenCodex rules cover strongly labelled API-key, +password, secret/keyring, private-key, infrastructure-URI assignments, and ASCII email addresses +with punycode domains without modifying the donor assets or their provenance hashes. The scanner +uses RE2 through exact-pinned +`re2-wasm@1.0.2`; it never falls back to JavaScript `RegExp`. +Infrastructure URI detection also accepts a bounded angle-bracket placeholder in the host position. +This lets a repeated scan protect remaining userinfo credentials in partially masked text without +treating an ordinary URI that has no password as a secret. +OpenCodex verifies the stock loader and WASM hashes, then raises the fixed WASM heap to 64 MiB only +in process memory so all reviewed matchers and repeated scans fit without modifying installed +dependency files. The release package gate repeats 300 full-registry scans after a fresh npm +install. + +| ID | Data type | +| --- | --- | +| `1` | Credentials | +| `2` | API keys | +| `3` | Access tokens | +| `4` | IP addresses | +| `5` | Personal data | +| `6` | Custom | + +The Rules section shows metadata and IDs, not the built-in regex source. You can disable a built-in +rule or add up to 100 local custom rules. A custom rule is declarative and bounded: + +- rule ID: `^[a-z0-9_.-]{1,128}$`; +- RE2 pattern: at most 4096 UTF-8 bytes; +- placeholder type: `^[A-Z][A-Z0-9_]{0,63}$`; +- required bounded arrays for `keywords`, `banlist`, `validators`, and + `masking.captureGroups` (use `[]` when unused), plus a required `groupPriority`. + `groupPriority` is reserved compatibility metadata and does not currently change rule precedence; +- optional `minLength` and `entropy` constraints. Capture-group numbers are unique ordered + alternatives: the scanner masks the first non-empty group, not every listed group. + +The complete effective registry is validated and compiled before a write is committed. A malformed +rule, conflicting placeholder type, or failed import leaves the prior configuration active. + +The keyword prefilter is off by default. When enabled, it skips only built-in rules whose parsed +RE2 expression proves that every possible match contains one of the rule's declared keywords. +Custom rules and built-in rules without that proof always execute, so the optimization preserves +detection recall. + +## Test a rule safely + +The Tester accepts at most 128 KiB of UTF-8 text. It sends the sample to the Management API of the +OpenCodex instance shown in the dashboard, where the effective registry returns a masked preview +plus metadata-only findings. The sample is never sent to an upstream LLM provider and is not stored +in configuration, request logs, telemetry, or browser storage. In connected mode it can traverse +the configured OpenCodex hub, so use synthetic values and never paste real secrets. + +Clearing the Tester cancels an in-flight scan. Reloading the page also discards the input and result. +The custom-rule editor can send its current unsaved rule to the Tester. The draft is included only +in that local simulation request; it is not added to the active registry until **Save rule** succeeds. + +Import uses a versioned JSON bundle and requires **Merge** or **Replace**. opencodex performs a dry +run before applying it. Merge adds only new, non-conflicting custom rules and preserves the current +enabled/mode/failure policy, provider scope, data types, built-in toggles, and keyword-prefilter setting. Replace +applies the bundle settings and custom-rule list as a whole and requires a consequence confirmation. +The Replace preview distinguishes rules that will be created or replaced from byte-equivalent rules +that remain unchanged, so its security diff does not overstate the mutation. +Export contains only safe settings and declarative custom rules, never findings, prompt text, +originals, or placeholder maps. + +## Covered request and response fields + +Guardrails covers the current opencodex LLM surfaces: + +- `POST /v1/responses` over JSON, SSE, and the client-facing WebSocket upgrade; +- `POST /v1/chat/completions` over JSON and SSE; +- routed and native `POST /v1/messages` over JSON and Anthropic SSE; +- `POST /v1/messages/count_tokens` request text; +- native and routed `POST /v1/responses/compact`; +- supported text material introduced by local compaction, recovery, web-search, vision, and + additional upstream rounds. + +For Responses history, hosted `web_search_call.action.query` and string entries in +`web_search_call.action.queries` are treated as semantic text and scanned before replay. + +Only semantic text leaves understood by the current parser are scanned. Model IDs, URLs, headers, +metadata, JSON Schema definitions, tool definitions, images, audio/video payloads, file IDs, +reasoning/thinking/signatures, genuine ciphertext, and unknown opaque item types are not scanned. +Model-visible `input_file.filename` text is scanned; opaque `input_file.file_id` and file bytes are +not. +For supported structured tool inputs and outputs, Guardrails also uses a bounded property name as +detection context. For example, a string value under an `API_KEY`, `password`, `SECRET_KEYS`, or +`PrivateKey` property can match a contextual rule even when the value has no provider-specific +prefix. Only the original string value is replaced and retained in the per-turn mapping; the +property name is never added to that mapping. Control properties that merely contain words such as +`TOKEN` in a longer mode/enablement name are not treated as secrets by these supplemental rules. +One semantic text leaf is limited to 128 KiB and one logical turn to 2 MiB across leaves. A separate +128 MiB regex-work budget multiplies UTF-8 bytes by the number of rules that actually execute after +keyword prefiltering, so a turn below 2 MiB can still be rejected when many always-scan rules apply. +Exceeding any limit is a whole-request capacity failure, never a partial mask. + +Successful client-facing JSON and SSE restore issued placeholders in assistant text. Executable +output stays masked, including function-call arguments, custom-tool input, Anthropic `tool_use` +input, shell/computer actions, and tool-search payloads. This is deliberate: an upstream model must +not be able to turn a placeholder into a locally executable copy of the original secret. + +Streaming restoration is terminal-gated for HTTP/SSE and client-facing WebSocket responses. A +stream passes through normally until Guardrails actually restores an issued placeholder. From that +block onward, opencodex retains both the masked and restored forms within a combined 2 MiB / 4096 +block limit. It releases the restored form only after `response.completed` (Responses), `[DONE]` +(Chat Completions), or `message_stop` (Anthropic Messages). A failed or incomplete terminal, +malformed event, premature EOF, or staging-capacity failure releases the retained masked form and +continues fail-closed. Streams that never restore a placeholder keep their normal incremental +delivery. + +Response restoration is fail-safe and bounded. If a successful response cannot be classified as +JSON or SSE, contains malformed JSON/UTF-8, or exceeds the 32 MiB JSON output limit, opencodex +returns it with placeholders still masked and records a metadata-only demask warning. It never +partially restores an oversized or malformed response. + +If a placeholder remains inside a tool call, pass only that placeholder or obtain the value through +an independently authorized secret mechanism. Guardrails does not offer an “unmask tool arguments” +option. + +## Debugging with placeholders + +Source snippets, configuration, logs, and tool output may contain placeholders such as ``, +``, or ``. These tokens can be produced by local Guardrails +processing and do not prove that the literal token exists in the source file. + +When a placeholder limits debugging: + +- do not infer the original value or edit the source merely to replace the token; +- use placeholder identity where possible: the same placeholder refers to the same original value + within the current mapping; +- continue with visible structure and safe derived properties such as address family, + loopback/private/public classification, or subnet membership; +- if the exact value is required, temporarily disable only the relevant data type or built-in rule, + such as `ip-addrs.*`, for an isolated check, then enable it again; +- do not switch to `detect` or `passthrough`, or disable credential rules, solely to simplify + debugging. Detect mode sends the original text upstream, and passthrough may fail open. + +See [Proxy API Formats](/reference/proxy-formats/) for protocol-specific framing. + +## Continuations and compacted history + +The reversible mapping exists only in process memory and is bounded by time, entry count, and bytes. +Responses continuation inheritance requires all three: + +- a valid `previous_response_id`; and +- the same non-empty continuation lane derived from `x-codex-parent-thread-id`, `thread-id`, or + `session_id`/`session-id` (a parent and a more specific child/session ID are paired when both + are present); and +- the same admission identity: configured-key ID, process-wide environment credential identity, or + trusted loopback admission. + +An unscoped or cross-thread request cannot retrieve originals. Mappings expire in at most one hour, +and chain activity does not extend the original absolute expiry. A proxy restart, expiry, or memory +eviction removes the mapping; an unknown placeholder then stays a placeholder. +Configured keys are isolated from one another. Environment admission is intentionally one +process-wide identity, while loopback admission relies on the proxy's local-process trust boundary; +neither form makes a mapping available to another thread. + +Compact responses intentionally remain masked machine state. opencodex associates the exact returned +compact artifact with a thread-scoped fingerprint in memory so the next request can inherit the same +mapping without writing originals into the artifact or to disk. + +An enforced Responses continuation can resume after the enforce registry or settings change. It +keeps previously issued mappings until their original expiry and scans new values with the current +enforce registry. Switching that continuation to detect or disabled returns HTTP `409` with code +`guardrails_policy_changed`; start a new session before weakening the policy. + +## Activity and privacy boundary + +Overview and Activity use a bounded, one-hour, in-memory metadata ring. It contains only: + +- timestamp, protocol surface, mode, result, and registry generation; +- counts, data-type/rule IDs, aggregate latency, and severity; +- retention size and eviction counts. + +It never stores request/response text, masked text, originals, placeholder mappings, headers, +credentials, or Tester input. It is operational feedback, not a durable audit log. Request logging +and usage diagnostics also remain on the masked side of the client-facing restore boundary for an +enforced turn. + +The Responses state snapshot and spill store contain masked canonical state plus, when needed, only +the safe marker `{ "enforced": true, "policyRevision": "…" }`. Originals and mappings are never +written there. Guardrails makes no extra disk-privacy claim for traffic processed while the feature +is disabled. + +## Configuration example + +```json +{ + "guardrails": { + "enabled": true, + "mode": "enforce", + "failurePolicy": "block", + "providerScope": { "mode": "all" }, + "enabledDataTypes": [1, 2, 3, 4, 5, 6], + "disabledBuiltinRuleIds": [], + "customRules": [], + "keywordPrefilterEnabled": false + } +} +``` + +Prefer the dashboard or scoped Management API so the effective registry is validated before the +write commits. A malformed optional `guardrails` section found during startup is ignored with a +warning when it was not explicitly enabled. If the malformed section contains `enabled: true`, +opencodex preserves the opt-in and falls back to the built-in `enforce`/`block` policy. Unrelated +provider/account configuration is preserved in both cases. + +See [Configuration](/reference/configuration/#guardrails-sensitive-data-placeholders) and the +[Management API](/reference/management-api/#guardrails) for field and endpoint details. + +## Troubleshooting + +### When values stay masked + +A placeholder in a reply does not necessarily mean the request failed: + +- In tool arguments or compacted machine state, it is intentional. Guardrails + never restores executable fields automatically. Do not run a command with a + guessed substitution; use the local source of truth for the required value. +- After a proxy restart, expiry or memory eviction, the mapping is gone. + Restarting again or switching protection off will not recover it. Start a new + session and provide the necessary source context again through the protected + proxy, with Guardrails still enabled for the selected provider. Do not send + only the old placeholder, and do not borrow another thread's mapping. +- After a late stream error, premature disconnect or restoration limit, masked + output is the safe fallback. Check the request's completion status and the + metadata-only Activity view. Retry only after considering whether the previous + tool work had side effects. Do not disable credential rules, select detect + mode or enable passthrough just to make the placeholder disappear. + +Successful streaming prose may pause from its first restored placeholder until +the protocol's success terminal. If the turn does not finish successfully, +Guardrails releases the held text with placeholders instead. Unknown formats +also receive no speculative restoration. Activity cannot reconstruct originals +and is cleared on restart; reproduce a problem with synthetic values when +reporting it. + +| Symptom | What to do | +| --- | --- | +| A known placeholder remains after restart or a long pause | The in-memory mapping is unavailable. Resend full text in a new turn rather than trying another thread's mapping. | +| HTTP `409 guardrails_policy_changed` | Start a new session after changing from enforce to detect/off or routing a protected continuation through an unchecked provider. | +| A rule or import is rejected | Fix the reported RE2/schema conflict; the previous active registry is still in use. | +| A request is blocked with `guardrails_capacity_exceeded` | Reduce the semantic text/findings or split the work. Do not switch to passthrough unless unmasked fail-open behavior is acceptable. | +| A tool call contains a placeholder | Expected security behavior. Executable fields are never restored. | +| Activity is empty after restart | Activity is an in-memory operational view, not an audit log. | diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index 72adcdcd70..e73c7825b9 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -1,6 +1,6 @@ --- title: Web Dashboard -description: The opencodex GUI for proxy health, providers, models, delegation guidance, auth pools, usage, and logs. +description: The opencodex GUI for proxy health, providers, models, Guardrails, delegation guidance, auth pools, usage, and logs. --- opencodex ships a local web dashboard (a Vite/React app under `gui/`) served from the proxy. It is the @@ -91,6 +91,7 @@ badge or the version value to read the full value. | **Codex Auth** | Add ChatGPT/Codex pool accounts, select the next-session account, refresh 5h / weekly / 30d quotas, enable or disable quota auto-switch, set its 1–100% threshold, and configure transient-failure failover. | | **Subagents** | Feature up to five bare native or namespaced routed models in the `spawn_agent` override list. | | **Models** | Toggle native GPT and routed models, set provider allowlists and context caps, choose v1/base/v2, and configure the v2 thread limit. Configured providers stay visible as zero-model groups when discovery is off or returns no rows. | +| **Guardrails** | Opt in to Guardrails, choose `detect` or `enforce`, choose the safe scanner-failure policy and data types, enable/disable built-in rule IDs, and create/edit/delete bounded local custom rules. | | **Logs** | Auto-refresh recent requests with tokens, requested effort and (when available) effective outbound effort, resolved model, provider, status, request id, duration, and error details. The detail view includes the exact reasoning wire field when the adapter emits one. Filter by opaque conversation/session id (when the client sends one) to total tokens and estimated list-price cost for the currently loaded Logs ring. | | **Usage / Debug** | Inspect token-usage coverage and trends, or enable opt-in provider transport and usage-extraction diagnostics. | | **Storage** | Read-only CODEX_HOME disk breakdown (sessions, archives, DBs, attachments). Optional archived cleanup: preview the oldest N%, then quarantine to `CODEX_HOME/.trash` (default) or permanently delete behind an explicit checkbox. **Auto-cleanup policy** is opt-in and **default OFF** (`storageCleanupPolicy.enabled`); configure threshold/target/schedule/mode on the Storage page, or trigger **Run now**. Quarantined entries can be restored from the Storage page (JSONL + threads). Active sessions stay read-only. Cleanup and restore are refused while Codex holds the newest/active `state_*.sqlite` locked. | @@ -130,8 +131,10 @@ the surface selector. These controls do not query historical records beyond the There is a single layout, so there is no layout switch to configure. Dashboard sections are addressable instead: `#dashboard` opens Overview, and `#dashboard/providers` and `#dashboard/models` open the other two. Reload, bookmark, and Back all keep the section you were -on. **Logs** works the same way with `#logs` and `#logs/debug`. An older `#providers/workspace` -bookmark now lands on `#providers`. +on. **Logs** works the same way with `#logs` and `#logs/debug`; **Guardrails** is `#guardrails`. +Its Rules, Tester, Activity, and Settings tabs use `#guardrails/rules`, `#guardrails/tester`, +`#guardrails/activity`, and `#guardrails/settings`. An older `#providers/workspace` bookmark now +lands on `#providers`. Cost values in **Logs** and **Usage** are API list-price equivalents calculated from reported tokens. For a custom usage interval, the server must confirm the exact requested start and end times. @@ -157,6 +160,30 @@ observation yet, loading, failed lookup with last-known values, and measured zer states. **Quota check completed** means the read settled—not that a passive observation became new or that every upstream measurement was refreshed. +## Guardrails + +Open **Guardrails** in the sidebar to configure sensitive-data placeholders. The initial state is +off, so the first switch is an explicit opt-in. Use **detect** to confirm that scanning is compatible +with your traffic without changing it; use **enforce** to replace supported textual values before an +outbound provider call and restore matching placeholders in the successful reply to the same client. + +The five-tab workspace provides an operational Overview, rule management, a non-persistent Tester +served by the current OpenCodex Management API, bounded metadata-only Activity, and Settings. +Tester samples are never sent to an LLM provider, but in connected mode they can traverse the +configured OpenCodex hub; use synthetic values only. The workspace deliberately shows only +built-in rule metadata, IDs, and source; it never displays the bundled regex matchers. Custom rules +are local declarative RE2 rules, and a failed save keeps the last active configuration. + +Settings also controls provider coverage. All current and future providers are protected by +default. Clearing a provider switches to selected-provider mode and requires a consequence +confirmation; unchecked providers receive unchanged request text. Removed IDs are marked as not +configured, and a selection containing no active configured provider is reported as +**No providers protected** rather than ordinary reduced coverage. + +See the [Guardrails guide](/guides/guardrails/) for the complete workflow and the +[Guardrails configuration reference](/reference/configuration/#guardrails-sensitive-data-placeholders) +for the precise transport, continuation, and logging boundary. + ## Model visibility The **Models** switches show final Codex visibility: a routed model is on only when its provider allowlist includes it (or no allowlist is set) and it is not disabled. Turning a model on reconciles both filters atomically; **All on** clears the provider allowlist so newly discovered models are also on. @@ -317,6 +344,8 @@ The GUI is a thin client over the proxy's JSON management API. Useful endpoints | `POST /api/sync` | Rebuild the shared model catalog and stale the Codex model cache. | | `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | Check, run, and monitor self-update jobs. Worker PIDs are persisted so a crashed job recovers automatically; legacy no-PID jobs recover after ten minutes. | | `GET` / `PUT /api/sidecar-settings` | Read or set search/vision sidecar model settings. | +| `GET /api/guardrails` · `PUT /api/guardrails/settings` | Read or update the effective Guardrails settings. | +| `GET /api/guardrails/catalog` · `GET` / `POST /api/guardrails/rules` · `PUT` / `DELETE /api/guardrails/rules/:ruleId` | List built-in metadata and manage bounded local custom rules. | | `GET` / `PUT /api/injection-model` | Read or set the shared sub-agent model/effort selection and the independent guidance/native-default switches. | | `GET` / `PUT /api/v2` | Read or set the surface mode, Codex feature flag, and v2 thread limit. | | `GET /api/providers` · `POST /api/providers` · `PATCH /api/providers?name=...` · `DELETE /api/providers?name=...` | List, add/replace, enable/disable, set the default, or remove providers. `PATCH` uses standalone `{ "setDefault": true }` on an enabled provider; `POST` may include `setDefault` when creating/replacing (also enabled-only). Deleting the current default reassigns to the first remaining enabled provider when one exists; otherwise the API returns `409` with `code: "last_provider"` and keeps the current default. | diff --git a/docs-site/src/content/docs/reference/configuration.md b/docs-site/src/content/docs/reference/configuration.md index b1136ee86e..9d7317a8ae 100644 --- a/docs-site/src/content/docs/reference/configuration.md +++ b/docs-site/src/content/docs/reference/configuration.md @@ -110,6 +110,108 @@ Routing has its own ordered resolution rules; see [Routing](/reference/configura - [Server and runtime](/reference/configuration/server/) — listener and remote access, admission keys, timeouts, storage, sidecars, startup behavior, and shadow calls. +## Guardrails: sensitive-data placeholders + +`guardrails` is disabled by default. Enabling it makes opencodex scan supported textual request fields +for the Responses, Chat Completions, Anthropic Messages/token-count, and Responses compact paths. +In `enforce` mode, detected values are replaced before an outbound provider call with per-turn placeholders such as +``. Exact placeholders, plus bounded normalized variants produced by a +model, are restored in a successful JSON, SSE, or Responses-over-WebSocket reply only for the +original client and only in non-executable assistant prose. Normalization is limited to ASCII case, +hyphen-versus-underscore separators, embedded ASCII whitespace, and leading zeroes in the numeric +suffix; the candidate token remains capped at 256 UTF-16 code units. +Function/tool arguments, Anthropic `tool_use` input, and shell/computer/tool-search actions remain +masked even in the client-facing response. + +Response restoration is bounded and fail-safe. Successful responses that cannot be classified as +JSON or SSE, malformed JSON/UTF-8, and JSON bodies above 32 MiB are returned with placeholders still +masked and produce a metadata-only demask warning; opencodex never partially restores such output. + +Images, binary data, and unsupported opaque values are left unchanged. Guardrails is a transport +privacy control, not a general DLP system: it cannot prevent a model from independently inferring or +rephrasing information that was otherwise available to it. + +Each semantic text leaf is capped at 128 KiB and the aggregate logical turn at 2 MiB. A separate +128 MiB regex-work budget multiplies UTF-8 bytes by the number of rules that actually execute after +keyword prefiltering. Crossing any limit follows `failurePolicy` for the entire request; opencodex +never sends a partially masked body. + +```json +{ + "guardrails": { + "enabled": true, + "mode": "enforce", + "failurePolicy": "block", + "providerScope": { "mode": "all" }, + "enabledDataTypes": [1, 2, 3, 4, 5, 6], + "keywordPrefilterEnabled": false + } +} +``` + +| Field | Meaning and default | +| --- | --- | +| `enabled` | Explicit opt-in. Only `true` activates the scanner; absence and `false` leave existing traffic unchanged. | +| `mode` | `enforce` (default) replaces detected values on the provider-facing request and restores issued placeholders in eligible response prose. `detect` scans the request but preserves the baseline provider wire payload produced by normal protocol translation and leaves the provider response unchanged; it does not protect upstream data or persist the request for Responses replay. | +| `failurePolicy` | `block` (default) rejects a request when the scanner cannot process it safely, including a traversal limit. `passthrough` may send that request unchanged instead and records a high-severity metadata event. It is an explicit fail-open policy, not a rule-match policy. | +| `providerScope` | Optional closed object. Omitted or `{ "mode": "all" }` protects every current and future provider. `{ "mode": "selected", "providerIds": ["openai", "anthropic-native"] }` protects only those canonical provider IDs. Selected mode requires a non-empty, unique list of valid IDs. `anthropic-native` is reserved for the built-in native Anthropic path and cannot name a configured provider. | +| `enabledDataTypes` | Optional non-empty subset of the numeric categories below; omitted means all six. | +| `disabledBuiltinRuleIds` | Optional built-in rule IDs to disable. Obtain IDs from `GET /api/guardrails/catalog`; the endpoint does not disclose matchers. | +| `customRules` | Up to 100 declarative local rules. Rule IDs match `^[a-z0-9_.-]{1,128}$`, patterns are RE2-compatible and at most 4096 UTF-8 bytes, and placeholder types match `^[A-Z][A-Z0-9_]{0,63}$`. Executable plugins are not supported. | +| `keywordPrefilterEnabled` | Optional recall-preserving performance prefilter, off by default. It skips only built-in rules whose parsed RE2 expression proves that every match contains a declared keyword; custom and unproven rules always execute. | + +| Value | Data type | +| --- | --- | +| `1` | Credentials | +| `2` | API keys | +| `3` | Access tokens | +| `4` | IP addresses | +| `5` | Personal data | +| `6` | Custom | + +Changes made through the dashboard or Management API are validated and compiled before the active +runtime snapshot is replaced. An invalid custom pattern or conflicting rule leaves the previous +configuration and scanner in place. + +Provider scope is evaluated from the canonical routed provider ID. `anthropic-native` identifies +the native Anthropic credential path; routed `anthropic` remains a separate provider. Policy-fallback +attempts are evaluated by concrete provider. A combo is unprotected only when all targets are +excluded; a mixed combo remains protected as one logical turn. A protected continuation cannot +resume through an unchecked provider and receives HTTP `409 guardrails_policy_changed` before +upstream I/O. + +If startup finds a malformed optional `guardrails` section, opencodex warns and ignores it when the +section was not explicitly enabled. A malformed section containing `enabled: true` preserves that +opt-in by falling back to the built-in `enforce`/`block` policy. Unrelated provider/account +configuration is preserved in both cases. Live writes are strict and fail without changing the +active or persisted registry. + +### Continuations and telemetry boundary + +When a Responses request uses `previous_response_id`, the placeholder mapping is retained only in +process memory for up to one hour, with bounded entry count and memory. It is keyed to a normalized +continuation lane derived from `x-codex-parent-thread-id`, `thread-id`, or +`session_id`/`session-id`, plus the admission identity. A parent and a more specific child/session +ID are paired when both are present. Configured credentials use their key ID; +environment admission is one process-wide identity; loopback relies on the local-process trust +boundary. Unscoped, cross-thread, or cross-key requests never inherit a mapping. It is never +written to the ordinary response replay cache or its snapshots. After a restart or expiry, resend the +full text rather than relying on a prior placeholder being restored. + +An enforced continuation can resume after enforce rules or settings change: existing mappings keep +their original expiry, while new values use the current enforce registry. Switching the continuation +to detect, disabled, or an unchecked provider returns HTTP `409` with code +`guardrails_policy_changed`; start a new session before weakening the policy. Compact output stays masked and inherits its mapping only through an +in-memory, scope-bound fingerprint of the exact returned compact artifact. + +For an enforced turn, request logs and usage-debug capture retain structural metadata such as token +counts and status, but not response bodies or upstream error text. This prevents a downstream +WebSocket or debug logger from retaining values restored for the client. + +Use the [Guardrails Management API](/reference/management-api/#guardrails) for automation or the +[Guardrails guide](/guides/guardrails/) for the dashboard workflow, protocol coverage, and +non-covered payloads. + ## Keep secrets out of the file Prefer `${ENV_VAR}` references for API keys. Literal `apiKey`, `apiKeyPool[].key`, and `apiKeys[].key` diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index c0dd38f1fb..d1c34c778d 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -164,6 +164,51 @@ See [Combos](/guides/combos/) for target strategies, cooldowns, aliases, and rou | `GET, PUT /api/sidecar-settings` | Read or update web-search and vision sidecar model/backend settings | 400 invalid shape, backend, or limit | | `GET, PUT /api/shadow-call-settings` | Read or update shadow-call interception settings | 400 invalid shape or value | +### Guardrails + +All Guardrails endpoints require the normal management credential or loopback dashboard session. +They expose configuration and rule metadata, not the values matched in a request or response. + +| Method and path | Purpose | Notable errors | +| --- | --- | --- | +| `GET /api/guardrails` | Read safe settings, registry health/generation, rule counts, counters, top rules/categories, recent metadata-only events, and retention state | 503 only when rule assets required for the DTO are unavailable | +| `GET /api/guardrails/settings` | Read the safe effective settings projection and provider IDs available to the dashboard | — | +| `PUT /api/guardrails/settings` | Atomically update one or more settings: `enabled`, `mode`, `failurePolicy`, `providerScope`, `enabledDataTypes`, `disabledBuiltinRuleIds`, or `keywordPrefilterEnabled` | 400 invalid, unsupported, or empty patch; 428 missing `If-Match`; 412 stale revision; 409 writer busy or unavailable config state | +| `GET /api/guardrails/rules` | List safe summaries for all effective rules plus declarative custom-rule definitions; built-in regexes are omitted | 503 invalid/unavailable rule assets | +| `GET /api/guardrails/catalog` | List built-in rule metadata and IDs without regex matchers | 503 invalid/unavailable rule assets | +| `POST /api/guardrails/rules` | Create one custom rule | 400 invalid rule; 428 missing `If-Match`; 412 stale revision; 409 duplicate ID, writer busy, or unavailable config state | +| `PUT, DELETE /api/guardrails/rules/:ruleId` | Replace a local rule without changing its ID, or delete it | 400 invalid replacement; 404 unknown rule; 428 missing `If-Match`; 412 stale revision; 409 writer busy or unavailable config state | +| `PUT /api/guardrails/rules/:ruleId/enabled` | Enable or disable one built-in rule | 400 invalid body; 404 unknown built-in ID; 503 invalid/unavailable rule assets; 428 missing `If-Match`; 412 stale revision; 409 writer/config conflict | +| `GET /api/guardrails/activity` | Read bounded metadata events and filtered summaries; optional `limit=1..200`, `mode`, `surface`, `result`, and `category=1..6` filters | 400 invalid or unknown filter | +| `GET /api/guardrails/export` | Download a versioned safe settings/custom-rule bundle | — | +| `POST /api/guardrails/test` | Scan at most 128 KiB of text locally and return masked preview plus finding offsets/IDs/types; input is not retained or sent upstream | 400 invalid input/draft registry; 413 body, Tester, or regex-work limit | +| `POST /api/guardrails/import` | Validate a versioned `merge` or `replace` bundle; `dryRun: true` reports counts/conflicts, and apply is one atomic mutation | 400 schema/RE2 error; 428 missing `If-Match`; 412 stale revision; 409 import/config mutation conflict | + +`merge` adds only non-conflicting custom rules and preserves the current enabled state, mode, +failure policy, enabled data types, built-in rule toggles, and keyword-prefilter setting. A changed +definition with an existing rule ID is a conflict. Replace previews also report provider-scope +changes, list the selected provider IDs, and flag reduced coverage. `replace` applies all bundle settings and replaces +the custom-rule list. + +The safe settings projection marks retained removed IDs with `configured: false`. When selected +scope contains no enabled configured provider and does not select `anthropic-native`, +`trafficProtection` is `no-provider-coverage`; the dashboard renders this as **No providers +protected**, not as ordinary reduced coverage. + +The revision-bearing `GET /api/guardrails`, `GET /api/guardrails/settings`, and +`GET /api/guardrails/rules` responses include an `ETag` based on the current Guardrails revision. +Mutation clients must return it in `If-Match`. A missing precondition receives +`428 guardrails_revision_required`; a stale value receives `412 guardrails_revision_conflict` plus +the current revision. The dashboard refetches instead of overwriting a newer configuration. + +Custom rules are declarative and bounded: `ruleId`, display metadata, data type, RE2 pattern, +placeholder type/capture groups, and optional listed validators. The service validates the complete +resulting registry before publishing it, so a failed update preserves the prior active rule set. + +See the [Guardrails guide](/guides/guardrails/) and +[Guardrails configuration](/reference/configuration/#guardrails-sensitive-data-placeholders) for +mode, continuation, executable-field, and telemetry semantics. + ### Logs, usage, and storage `GET /api/logs` accepts an optional opaque `cursor` from its previous response. The envelope preserves diff --git a/docs-site/src/content/docs/reference/proxy-formats.md b/docs-site/src/content/docs/reference/proxy-formats.md index 1f2e589252..8de6b33b3d 100644 --- a/docs-site/src/content/docs/reference/proxy-formats.md +++ b/docs-site/src/content/docs/reference/proxy-formats.md @@ -32,6 +32,58 @@ should select among several targets. | Voice and Realtime | `POST /v1/live`, `POST /v1/realtime/calls` | Relayed call-creation response | A separate sideband WebSocket relays frames in both directions | | Responses compaction | `POST /v1/responses/compact` | Replacement-history JSON | Not applicable | +## Guardrails transport boundary + +When the optional `guardrails.enabled` setting is `true`, opencodex captures one immutable policy +intent before reading the request body, then activates its runtime only when the canonical routed +provider is covered by `providerScope`. Absence and `mode: "all"` cover every provider; selected +mode covers only its non-empty `providerIds` list. Supported semantic text is scanned after +Responses continuation expansion/local encrypted-content sanitation and before upstream I/O. +Protected retries and later web-search/vision/tool rounds reuse the same logical mapping. + +| Mode | Provider-facing request | Client-facing response | +| --- | --- | --- | +| `enforce` | Supported detected values become issued placeholders | Eligible assistant prose restores issued placeholders; executable fields stay masked | +| `detect` | Original bytes/translated semantics are sent unchanged | Identity response; provider output is not scanned for new values | +| disabled/absent | Baseline opencodex path; Guardrails does not load the registry or scan | Baseline opencodex path | + +Covered request text includes current parser-supported Responses message/tool history, +Chat message/function-call arguments, Anthropic system/message/tool input/result text, token-count +request text, and local plaintext materialized for compact/recovery/additional rounds. Headers, +model IDs, metadata, schemas/tool definitions, images/binary media, file IDs, reasoning/signatures, +genuine ciphertext, and unknown opaque items are not scanned. +Model-visible `input_file.filename` is scanned; opaque `input_file.file_id` and file bytes are not. +Each semantic text leaf is capped at 128 KiB and the aggregate logical turn at 2 MiB. +The turn also has a 128 MiB regex-work budget, calculated as UTF-8 bytes multiplied by the +number of rules that actually execute after keyword prefiltering. + +The last client-facing transform restores only placeholders issued by that logical turn. Function +and custom-tool arguments, Anthropic `tool_use` input, shell/computer actions, and tool-search +payloads deliberately remain placeholders; the proxy never turns model-controlled executable +output into an original secret. + +For SSE and client-facing Responses WebSocket streams, any block that actually restores an issued +placeholder starts a bounded terminal gate. OpenCodex retains masked and restored variants up to a +combined 2 MiB / 4096 blocks, releases restored output only after the protocol's successful terminal +event, and releases masked output on failure, malformed data, premature EOF, or capacity fallback. +Blocks before the first restoration and streams without restored placeholders are relayed normally. + +Responses continuation mapping is in memory for at most one hour and requires the same non-empty +continuation lane and admission identity with `previous_response_id`. The lane is derived from +`x-codex-parent-thread-id`, `thread-id`, or `session_id`/`session-id`; a parent and a more specific +child/session ID are paired when both are present. Configured-key IDs are isolated; environment +admission is process-wide; loopback relies on the local-process trust boundary. +Cross-scope and unscoped requests cannot inherit originals. Enforce registry/settings changes keep +old mappings at their original expiry and scan new values with the current registry. Switching the +continuation to detect or disabled returns HTTP `409` with code `guardrails_policy_changed`. +The same fail-closed response applies when a protected continuation resolves to a provider excluded +by selected-provider mode. + +With the default `failurePolicy: "block"`, scanner/registry failures return +`guardrails_scan_failed` or `guardrails_capacity_exceeded` before upstream I/O. The explicit +`passthrough` policy may send the original unmasked request and records a high-severity metadata +event. See the [Guardrails guide](/guides/guardrails/) for the full privacy and continuation model. + ## `POST /v1/responses` This is the native opencodex data-plane shape. The request body must be a JSON object with a @@ -398,7 +450,9 @@ points at it; `ocx start` injects that key next to `openai_base_url` (see ## `POST /v1/responses/compact` Compaction returns replacement history for clients that need to shorten a long Responses -conversation. +conversation. With Guardrails enforcement, returned compact items remain masked machine state. +Their mapping is associated only with an in-memory, thread-scoped fingerprint of the exact returned +artifact; originals are not embedded in compact output or written to disk. | Route type | Behavior | | --- | --- | diff --git a/gui/src/App.tsx b/gui/src/App.tsx index 91890ce664..b9c95f12cc 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -9,10 +9,11 @@ import Usage from "./pages/Usage"; import Storage from "./pages/Storage"; import CodexSet from "./pages/CodexSet"; import Integrations from "./pages/Integrations"; +import Guardrails from "./pages/Guardrails"; import Startup from "./pages/Startup"; import ErrorBoundary from "./components/ErrorBoundary"; import { SidebarGithubRow } from "./components/sidebar-github-row"; -import { IconGrid, IconServer, IconBoxes, IconBot, IconList, IconActivity, IconHardDrive, IconCodex, IconMenu, IconSun, IconMoon, IconMonitor, IconGlobe, IconPower, IconX, IconRefresh} from "./icons"; +import { IconGrid, IconServer, IconBoxes, IconBot, IconList, IconActivity, IconHardDrive, IconCodex, IconLock, IconMenu, IconSun, IconMoon, IconMonitor, IconGlobe, IconPower, IconX, IconRefresh} from "./icons"; import { useI18n, useT, LOCALES, localeDisplayName, type Locale, type TKey } from "./i18n/shared"; import { Select } from "./ui"; import { configureApiTargets, hasApiSession, installApiAuthFetch, installApiSessionFromHtml, logoutApiSession } from "./api"; @@ -36,6 +37,7 @@ const PAGE_TKEY: Record = { usage: "nav.usage", storage: "nav.storage", "codex-set": "nav.codexSet", + guardrails: "nav.guardrails", integrations: "nav.integrations", }; @@ -64,6 +66,7 @@ const NAV: NavEntry[] = [ { id: "codex-set", tkey: "nav.codexSet", Icon: IconCodex }, { id: "providers", tkey: "nav.providers", Icon: IconServer }, { id: "models", tkey: "nav.models", Icon: IconBoxes }, + { id: "guardrails", tkey: "nav.guardrails", Icon: IconLock }, { id: "subagents", tkey: "nav.subagents", Icon: IconBot }, { id: "logs", tkey: "nav.logs", Icon: IconList }, { id: "usage", tkey: "nav.usage", Icon: IconActivity }, @@ -433,6 +436,7 @@ export default function App() { {page === "usage" && } {page === "storage" && } {page === "codex-set" && } + {page === "guardrails" && } {page === "integrations" && } )} diff --git a/gui/src/app-routing.ts b/gui/src/app-routing.ts index c5971ffb6b..6615570857 100644 --- a/gui/src/app-routing.ts +++ b/gui/src/app-routing.ts @@ -12,6 +12,7 @@ export type Page = | "usage" | "storage" | "codex-set" + | "guardrails" | "integrations"; export const VALID_PAGES = new Set([ @@ -24,6 +25,7 @@ export const VALID_PAGES = new Set([ "usage", "storage", "codex-set", + "guardrails", "integrations", ]); @@ -68,6 +70,14 @@ export const DASHBOARD_TAB_HASHES = ["dashboard/providers", "dashboard/models"] */ export const MODELS_TAB_HASHES = ["models/combos", "models/routing", "models/compatibility"] as const; +/** Guardrails owns five workspace tabs; Overview uses the bare `#guardrails` hash. */ +export const GUARDRAILS_TAB_HASHES = [ + "guardrails/rules", + "guardrails/tester", + "guardrails/activity", + "guardrails/settings", +] as const; + /** * `#dashboard/update` is an action deep link, not a tab: the sidebar update button uses * it to open the maintenance update dialog over the Overview section. It is listed as a @@ -108,6 +118,7 @@ export function hashBelongsToPage(rawHash: string, page: Page): boolean { || (page === "logs" && rawHash === "logs/debug") || (page === "codex-set" && rawHash === "codex-set/prompt") || (page === "models" && (MODELS_TAB_HASHES as readonly string[]).includes(rawHash)) + || (page === "guardrails" && (GUARDRAILS_TAB_HASHES as readonly string[]).includes(rawHash)) || (page === "dashboard" && (rawHash === DASHBOARD_UPDATE_HASH || (DASHBOARD_TAB_HASHES as readonly string[]).includes(rawHash))) || (page === "integrations" diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 40ee6ea36f..c19532df28 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -2570,6 +2570,278 @@ export const de: Record = { "integrations.cursor.colReasoning": "Reasoning-Aufwand", "integrations.cursor.colContext": "Kontext", "integrations.cursor.guide": "Anleitung zu Cursor Private Inference öffnen", + "nav.guardrails": "Guardrails", + "guardrails.title": "Schutz sensibler Daten", + "guardrails.subtitle": "Erkannte sensible Werte werden durch umkehrbare Platzhalter ersetzt, bevor Anfragen diesen Proxy verlassen.", + "guardrails.loading": "Guardrails-Einstellungen werden geladen…", + "guardrails.loadFailed": "Guardrails-Einstellungen konnten nicht geladen werden.", + "guardrails.saveFailed": "Guardrails-Einstellungen konnten nicht gespeichert werden.", + "guardrails.saved": "Guardrails-Einstellungen gespeichert.", + "guardrails.statusActive": "Aktiv", + "guardrails.statusDisabled": "Deaktiviert", + "guardrails.settingsTitle": "Anfrageschutz", + "guardrails.settingsHint": "Änderungen werden atomar auf neue Anfragen angewendet. Bestehende Streams behalten ihre aktuelle Zuordnung.", + "guardrails.enabled": "Guardrails aktivieren", + "guardrails.enabledHint": "Zugelassene Prompt- und Tool-Felder maskieren, bevor sie einen Upstream-Anbieter erreichen.", + "guardrails.mode": "Modus", + "guardrails.modeEnforce": "Maskierung erzwingen", + "guardrails.modeDetect": "Nur erkennen", + "guardrails.failurePolicy": "Verarbeitungsfehler", + "guardrails.failureBlock": "Anfrage blockieren", + "guardrails.failurePassthrough": "Anfrage durchlassen", + "guardrails.keywordPrefilter": "Schlüsselwort-Vorfilter", + "guardrails.keywordPrefilterHint": "Nur integrierte Regeln überspringen, deren Regex nachweislich in jedem Treffer ein Schlüsselwort enthält. Eigene und nicht beweisbare Regeln werden immer geprüft.", + "guardrails.dataTypes": "Aktivierte Datentypen", + "guardrails.dataType": "Datentyp", + "guardrails.dataType.credentials": "Zugangsdaten", + "guardrails.groupCredentialUrls": "URLs mit Zugangsdaten", + "guardrails.dataType.apiKeys": "API-Schlüssel", + "guardrails.dataType.accessTokens": "Zugriffstoken", + "guardrails.dataType.ipAddresses": "IP-Adressen", + "guardrails.dataType.personal": "Personenbezogene Daten", + "guardrails.dataType.custom": "Benutzerdefiniert", + "guardrails.rulesTitle": "Regelverwaltung", + "guardrails.rulesHint": "Deaktivieren Sie eine integrierte Regel oder verwalten Sie lokale benutzerdefinierte Regeln. Integrierte reguläre Ausdrücke werden hier nie offengelegt.", + "guardrails.selectBuiltin": "Integrierte Regel auswählen", + "guardrails.disableBuiltin": "Regel deaktivieren", + "guardrails.enableBuiltin": "Regel aktivieren", + "guardrails.noDisabledBuiltins": "Keine integrierten Regeln sind deaktiviert.", + "guardrails.customRules": "Benutzerdefinierte Regeln", + "guardrails.noCustomRules": "Noch keine benutzerdefinierten Regeln.", + "guardrails.addRule": "Regel hinzufügen", + "guardrails.editRule": "Regel bearbeiten", + "guardrails.deleteRule": "Regel löschen", + "guardrails.removeRuleConfirm": "Benutzerdefinierte Regel {ruleId} löschen?", + "guardrails.ruleSaveFailed": "Die benutzerdefinierte Regel konnte nicht gespeichert werden.", + "guardrails.ruleDeleteFailed": "Die benutzerdefinierte Regel konnte nicht gelöscht werden.", + "guardrails.ruleSaved": "Benutzerdefinierte Regel gespeichert.", + "guardrails.ruleDeleted": "Benutzerdefinierte Regel gelöscht.", + "guardrails.ruleId": "Regel-ID", + "guardrails.ruleName": "Regelname", + "guardrails.group": "Gruppe", + "guardrails.groupPriority": "Gruppenpriorität", + "guardrails.displayName": "Anzeigename", + "guardrails.description": "Beschreibung", + "guardrails.regex": "RE2-Muster", + "guardrails.placeholderType": "Platzhaltertyp", + "guardrails.captureGroups": "Erfassungsgruppen", + "guardrails.captureGroupsInvalid": "Verwenden Sie eine durch Kommas getrennte Liste positiver ganzzahliger Gruppennummern.", + "guardrails.minLength": "Mindestlänge", + "guardrails.entropy": "Entropieschwelle", + "guardrails.keywords": "Schlüsselwörter", + "guardrails.banlist": "Sperrliste", + "guardrails.validators": "Validatoren", + "guardrails.saveRule": "Regel speichern", + "guardrails.cancelEdit": "Abbrechen", + "guardrails.tabsLabel": "Guardrails-Bereiche", + "guardrails.tab.overview": "Übersicht", + "guardrails.tab.rules": "Regeln", + "guardrails.tab.tester": "Tester", + "guardrails.tab.activity": "Aktivität", + "guardrails.tab.settings": "Einstellungen", + "guardrails.detectWarning": "Im Nur-erkennen-Modus werden Treffer gemeldet, sensible Daten jedoch nicht maskiert, bevor sie den Proxy verlassen.", + "guardrails.passthroughWarning": "Beim Durchlassen können unmaskierte Eingaben an den Upstream gesendet werden, wenn die Guardrails-Verarbeitung fehlschlägt.", + "guardrails.metric.scanned": "Gescannte Anfragen", + "guardrails.metric.masked": "Maskierte Werte", + "guardrails.metric.detected": "Erkannte Werte", + "guardrails.metric.errors": "Warnungen und Blockierungen", + "guardrails.lastPassthrough": "Letzter Durchlassvorfall: {date}", + "guardrails.topRules": "Häufigste Regeln", + "guardrails.topCategories": "Häufigste Datenkategorien", + "guardrails.noActivity": "Noch keine Guardrails-Aktivität", + "guardrails.noActivityHint": "Metadaten werden hier angezeigt, nachdem geschützte Anfragen verarbeitet wurden.", + "guardrails.documentation": "Guardrails-Dokumentation lesen", + "guardrails.searchRules": "Regeln durchsuchen", + "guardrails.source": "Quelle", + "guardrails.sourceOpenCodex": "OpenCodex-Regeln", + "guardrails.sourceManual": "Integriert", + "guardrails.allSources": "Alle Quellen", + "guardrails.status": "Status", + "guardrails.allStatuses": "Alle Status", + "guardrails.allDataTypes": "Alle Datentypen", + "guardrails.filteredRules": "Gefilterte Regeln: {count}", + "guardrails.bulkEnable": "Auswahl aktivieren ({count})", + "guardrails.bulkDisable": "Auswahl deaktivieren ({count})", + "guardrails.export": "Exportieren", + "guardrails.exported": "Guardrails-Paket exportiert.", + "guardrails.exportFailed": "Das Guardrails-Paket konnte nicht exportiert werden.", + "guardrails.import": "Importieren", + "guardrails.importMode": "Importmodus", + "guardrails.importMerge": "Zusammenführen", + "guardrails.importReplace": "Ersetzen", + "guardrails.importFailed": "Guardrails-Regeln konnten nicht importiert werden.", + "guardrails.importTooLarge": "Guardrails-Importdateien dürfen höchstens 4 MiB groß sein.", + "guardrails.imported": "Guardrails-Regeln importiert.", + "guardrails.noRules": "Keine Regeln entsprechen diesen Filtern.", + "guardrails.previous": "Zurück", + "guardrails.next": "Weiter", + "guardrails.page": "Seite {current} von {total}", + "guardrails.testerTitle": "Tester für sensible Daten", + "guardrails.testerHint": "Mit dem wirksamen Regelregister dieser OpenCodex-Instanz scannen. Der Text wird an deren Management API, niemals an einen LLM-Anbieter, gesendet und serverseitig nicht gespeichert.", + "guardrails.testerInput": "Zu scannender Text", + "guardrails.testerPlaceholder": "Beispieltext hier einfügen…", + "guardrails.byteCounter": "{current} / {max} Byte", + "guardrails.clear": "Leeren", + "guardrails.test": "Scannen", + "guardrails.testing": "Wird gescannt…", + "guardrails.testerFailed": "Der Text konnte nicht gescannt werden.", + "guardrails.testerResult": "Scanergebnis", + "guardrails.effective": "Wirksame Einstellungen", + "guardrails.draft": "Entwurfseinstellungen", + "guardrails.findings": "Treffer", + "guardrails.noFindings": "Die aktivierten Regeln fanden keine Treffer; dies garantiert nicht, dass der Text keine sensiblen Daten enthält.", + "guardrails.placeholder": "Platzhalter", + "guardrails.offsets": "Positionen", + "guardrails.activityTitle": "Guardrails-Aktivität", + "guardrails.activityPrivacy": "Nur Metadaten im Arbeitsspeicher; dies ist kein Auditprotokoll. Prompttext und Platzhalterzuordnungen werden hier nie gespeichert.", + "guardrails.surface": "Schnittstelle", + "guardrails.allSurfaces": "Alle Schnittstellen", + "guardrails.allModes": "Alle Modi", + "guardrails.result": "Ergebnis", + "guardrails.allResults": "Alle Ergebnisse", + "guardrails.result.scanned": "Gescannt", + "guardrails.result.masked": "Maskiert", + "guardrails.result.detected": "Erkannt", + "guardrails.result.blocked": "Blockiert", + "guardrails.result.passthrough": "Durchgelassen", + "guardrails.result.demaskWarning": "Warnung beim Demaskieren", + "guardrails.result.toolSkipped": "Tool-Wiederherstellung übersprungen", + "guardrails.refresh": "Aktualisieren", + "guardrails.retentionEvents": "Gespeicherte Ereignisse: {current} von {max}", + "guardrails.retentionEvicted": "Entfernte Ereignisse: {count}", + "guardrails.lastRefresh": "Letzte Aktualisierung: {date}", + "guardrails.time": "Zeit", + "guardrails.count": "Anzahl", + "guardrails.rules": "Regeln", + "guardrails.latency": "Latenz", + "guardrails.dialog.disable.title": "Guardrails deaktivieren?", + "guardrails.dialog.disable.changes": "Neue Anfragen werden nicht mehr gescannt oder maskiert.", + "guardrails.dialog.disable.breakage": "Sensible Werte können diesen Proxy unverändert verlassen.", + "guardrails.dialog.disable.undo": "Aktivieren Sie Guardrails unter „Übersicht“ oder „Einstellungen“ erneut.", + "guardrails.dialog.disable.confirm": "Deaktivieren", + "guardrails.dialog.detect.title": "In den Nur-erkennen-Modus wechseln?", + "guardrails.dialog.detect.changes": "Treffer werden gezählt, der Anfragetext wird jedoch nicht maskiert.", + "guardrails.dialog.detect.breakage": "Der Nur-erkennen-Modus schützt keine an den Upstream gesendeten Daten.", + "guardrails.dialog.detect.undo": "Wechseln Sie zurück zum Modus „Maskierung erzwingen“.", + "guardrails.dialog.detect.confirm": "Nur-erkennen-Modus verwenden", + "guardrails.dialog.passthrough.title": "Durchlassen bei Fehlern erlauben?", + "guardrails.dialog.passthrough.changes": "Bei einem Guardrails-Verarbeitungsfehler wird die ursprüngliche Anfrage weitergeleitet.", + "guardrails.dialog.passthrough.breakage": "Die fehlgeschlagene Anfrage kann unmaskierte sensible Werte enthalten.", + "guardrails.dialog.passthrough.undo": "Setzen Sie die Fehlerbehandlung zurück auf „Anfrage blockieren“.", + "guardrails.dialog.passthrough.confirm": "Durchlassen erlauben", + "guardrails.dialog.delete.title": "Benutzerdefinierte Regel {ruleId} löschen?", + "guardrails.dialog.delete.changes": "Die benutzerdefinierte Regel wird aus dem wirksamen Regelregister entfernt.", + "guardrails.dialog.delete.breakage": "Werte, die nur von dieser Regel erkannt werden, sind nicht mehr geschützt.", + "guardrails.dialog.delete.undo": "Importieren oder erstellen Sie die Regel erneut, um sie wiederherzustellen.", + "guardrails.dialog.delete.confirm": "Regel löschen", + "guardrails.dialog.import.title": "Alle Guardrails-Regeln ersetzen?", + "guardrails.dialog.import.changes": "Das importierte Paket ersetzt die aktuellen Guardrails-Einstellungen und benutzerdefinierten Regeln.", + "guardrails.dialog.import.breakage": "Lokale Regeln, die nicht im Paket enthalten sind, werden entfernt.", + "guardrails.dialog.import.undo": "Exportieren Sie das aktuelle Paket, bevor Sie es ersetzen.", + "guardrails.dialog.import.confirm": "Regeln ersetzen", + "guardrails.sourceGitleaks": "Gitleaks-Regeln", + "guardrails.surfaceCompact": "Kompakt", + "guardrails.registryReady": "Regelregister bereit", + "guardrails.registryFailed": "Regelregister fehlerhaft", + "guardrails.registryDisabled": "Regelregister deaktiviert", + "guardrails.effectiveRules": "Wirksame Regeln: {count}", + "guardrails.registryGeneration": "Generation {generation}", + "guardrails.testerDraftSettings": "Entwurfseinstellungen verwenden", + "guardrails.testerDraftSettingsHint": "Eine andere Auswahl von Datentypen und Schlüsselwort-Vorfilter testen, ohne sie zu speichern.", + "guardrails.showingEvents": "{shown} von {total} passenden Ereignissen werden angezeigt", + "guardrails.oldestEvent": "Ältestes gespeichertes Ereignis: {date}", + "guardrails.tableScrollHint": "Scrollen Sie horizontal durch die Tabelle, um alle Spalten zu sehen.", + "guardrails.importPreviewTitle": "Importvorschau", + "guardrails.importCreateCount": "Neu: {count}", + "guardrails.importReplaceCount": "Ersetzen: {count}", + "guardrails.importUnchangedCount": "Unverändert: {count}", + "guardrails.importConflicts": "Konflikte bei Regel-IDs: {count}. Beheben Sie sie vor dem Import.", + "guardrails.importApply": "Import anwenden", + "guardrails.category": "Kategorie", + "guardrails.allCategories": "Alle Kategorien", + "guardrails.generation": "Generation", + "guardrails.activitySummary": "Zusammenfassung der gefilterten Aktivität", + "guardrails.activityEvents": "Ereignisse: {count}", + "guardrails.activityFindings": "Treffer: {count}", + "guardrails.activityAverageLatency": "Durchschnittliche Latenz: {value} ms", + "guardrails.activityTopRule": "Häufigste Regel: {rule}", + "guardrails.recentActivity": "Letzte Aktivität", + "guardrails.dialog.ruleDisable.title": "Regel {ruleId} deaktivieren?", + "guardrails.dialog.ruleDisable.changes": "Diese integrierte Regel scannt keine neuen Anfragen mehr.", + "guardrails.dialog.ruleDisable.breakage": "Werte, die nur von dieser Regel erkannt werden, sind nicht mehr geschützt.", + "guardrails.dialog.ruleDisable.undo": "Aktivieren Sie die Regel auf der Registerkarte „Regeln“ erneut.", + "guardrails.dialog.ruleDisable.confirm": "Regel deaktivieren", + "guardrails.dialog.bulkDisable.title": "{count} Regeln deaktivieren?", + "guardrails.dialog.bulkDisable.changes": "Alle gefilterten integrierten Regeln in dieser Auswahl werden deaktiviert.", + "guardrails.dialog.bulkDisable.breakage": "Sensible Werte, die nur von diesen Regeln abgedeckt werden, können den Proxy unverändert verlassen.", + "guardrails.dialog.bulkDisable.undo": "Aktivieren Sie die Regeln auf der Registerkarte „Regeln“ erneut oder importieren Sie einen früheren Export.", + "guardrails.dialog.bulkDisable.confirm": "Regeln deaktivieren", + "guardrails.statusProtected": "Geschützt", + "guardrails.statusDetectOnly": "Nur Erkennung", + "guardrails.statusReducedCoverage": "Reduzierte Abdeckung", + "guardrails.statusUnavailable": "Register nicht verfügbar", + "guardrails.statusNoRules": "Keine aktiven Regeln", + "guardrails.statusNoProviderCoverage": "Keine Anbieter geschützt", + "guardrails.valueOn": "Ein", + "guardrails.valueOff": "Aus", + "guardrails.reducedCoverageWarning": "Reduzierte Abdeckung: Datentypen oder integrierte Regeln sind deaktiviert, die Anbieterabdeckung ist begrenzt oder Verarbeitungsfehler werden durchgereicht.", + "guardrails.providerScope": "Geschützte Anbieter", + "guardrails.providerScopeAllValue": "Alle Anbieter", + "guardrails.providerScopeSelectedValue": "Ausgewählt ({count})", + "guardrails.importSecurityProviderScope": "Anbieterabdeckung: {before} → {after}", + "guardrails.providerScopeAllHint": "Alle aktuellen und zukünftigen Anbieter sind geschützt.", + "guardrails.providerScopeSelectedHint": "Nur ausgewählte Anbieter sind geschützt. Neue Anbieter bleiben ungeschützt, bis sie ausgewählt werden.", + "guardrails.providerAnthropicNative": "Anthropic (nativ)", + "guardrails.providerDisabled": "deaktiviert", + "guardrails.providerNotConfigured": "nicht konfiguriert", + "guardrails.lastProviderRequired": "Mindestens einen Anbieter auswählen oder Guardrails vollständig deaktivieren.", + "guardrails.providerScopeWarning": "Die Anbieterabdeckung ist eingeschränkt. Anfragen an nicht ausgewählte Anbieter werden weder geprüft noch maskiert.", + "guardrails.noProviderCoverageWarning": "Kein aktiver Anbieter ist ausgewählt. Der aktuelle Anbieter-Datenverkehr wird weder geprüft noch maskiert.", + "guardrails.dialog.providerScope.title": "Anbieterabdeckung einschränken?", + "guardrails.dialog.providerScope.changes": "Neue direkte Anfragen an den abgewählten Anbieter werden nicht mehr geprüft oder maskiert.", + "guardrails.dialog.providerScope.breakage": "Sensible Werte können diesen Anbieter unverändert erreichen. Geschützte Fortsetzungen überschreiten diese Grenze nicht.", + "guardrails.dialog.providerScope.undo": "Wählen Sie den Anbieter in den Guardrails-Einstellungen erneut aus, um den Schutz wiederherzustellen.", + "guardrails.dialog.providerScope.confirm": "Abdeckung einschränken", + "guardrails.lastDataTypeRequired": "Mindestens ein Datentyp muss aktiviert bleiben.", + "guardrails.importSecurityTitle": "Sicherheitseinstellungen", + "guardrails.importSecurityWeakening": "Dieser Import reduziert den Schutz. Prüfen Sie jede hervorgehobene Änderung, bevor Sie fortfahren.", + "guardrails.importSecurityPotential": "Dieser Import ändert sicherheitsrelevante benutzerdefinierte Regeln und kann den Schutz reduzieren. Prüfen Sie sie vor dem Fortfahren.", + "guardrails.importSecurityUnchanged": "Die Guardrails-Sicherheitseinstellungen ändern sich nicht.", + "guardrails.importSecurityEnabled": "Datenverkehrsschutz: {before} → {after}", + "guardrails.importSecurityMode": "Modus: {before} → {after}", + "guardrails.importSecurityFailure": "Fehlerbehandlung: {before} → {after}", + "guardrails.importSecurityDataTypes": "Aktivierte Datentypen: {before} → {after}", + "guardrails.importSecurityDisabledRules": "Deaktivierte integrierte Regeln: {before} → {after} (neu deaktiviert: {disabled}, reaktiviert: {reenabled})", + "guardrails.importSecurityCustomRules": "Benutzerdefinierte Regeln: {before} → {after} (entfernt: {removed}, geändert: {changed})", + "guardrails.importSecurityChangedRuleIds": "Geänderte Regeldefinitionen:", + "guardrails.importSecurityRemovedRuleIds": "Entfernte Regel-IDs:", + "guardrails.importSecurityPrefilter": "Schlüsselwort-Vorfilter: {before} → {after}", + "guardrails.importSecurityReduced": "Reduzierter Schutz", + "guardrails.importSecurityReviewRequired": "Sicherheitsprüfung erforderlich", + "guardrails.importSecurityRemovedTypes": "Nicht mehr abgedeckt: {types}", + "guardrails.testerSimulation": "Nur Tester-Simulation. Verwenden Sie synthetische Werte; fügen Sie niemals echte Geheimnisse ein. Im verbundenen Modus kann der Text Ihren OpenCodex-Hub durchlaufen.", + "guardrails.testerTrafficDisabled": "Der Schutz des realen Datenverkehrs ist deaktiviert. Anfragen verlassen den Proxy derzeit ohne Guardrails-Maskierung.", + "guardrails.testerTrafficDetect": "Der reale Datenverkehr wird nur erkannt und vor dem Anbieter nicht maskiert.", + "guardrails.testerTrafficUnknown": "Der Schutzstatus des realen Datenverkehrs ist noch unbekannt. Warten Sie auf den Guardrails-Status.", + "guardrails.testerTrafficUnavailable": "Der Schutz des realen Datenverkehrs ist nicht verfügbar. Beheben Sie den Aktivierungs- oder Registry-Fehler.", + "guardrails.testerTrafficNoRules": "Für den realen Datenverkehr sind keine Guardrails-Regeln aktiv. Aktivieren Sie mindestens eine wirksame Regel.", + "guardrails.testerTrafficNoProviderCoverage": "Die Simulation kann Treffer finden, aber derzeit ist kein aktiver Anbieter für den Schutz ausgewählt.", + "guardrails.testerTrafficReduced": "Der reale Datenverkehr ist nur reduziert geschützt. Prüfen Sie deaktivierte Regeln, Datentypen, Anbieterabdeckung und Fehlerpolitik.", + "guardrails.dialog.categoryDisable.title": "Datenkategorie deaktivieren?", + "guardrails.dialog.categoryDisable.changes": "Regeln dieser Kategorie prüfen neue Anfragen nicht mehr.", + "guardrails.dialog.categoryDisable.breakage": "Sensible Werte, die nur diese Kategorie abdeckt, können den Proxy unverändert verlassen.", + "guardrails.dialog.categoryDisable.undo": "Aktivieren Sie die Kategorie in den Guardrails-Einstellungen erneut.", + "guardrails.dialog.categoryDisable.confirm": "Kategorie deaktivieren", + "guardrails.dialog.prefilter.title": "Schlüsselwort-Vorfilter aktivieren?", + "guardrails.dialog.prefilter.changes": "Regeln mit Schlüsselwörtern können übersprungen werden, wenn diese im zu prüfenden Text fehlen.", + "guardrails.dialog.prefilter.breakage": "Dieser Leistungskompromiss kann die Erkennungsabdeckung verringern.", + "guardrails.dialog.prefilter.undo": "Deaktivieren Sie den Schlüsselwort-Vorfilter in den Guardrails-Einstellungen.", + "guardrails.dialog.prefilter.confirm": "Vorfilter aktivieren", + "guardrails.dialog.importWeakening.title": "Import anwenden, der den Schutz reduziert?", + "guardrails.dialog.importWeakening.changes": "Das importierte Paket ersetzt Guardrails-Einstellungen und benutzerdefinierte Regeln durch die angezeigten Werte.", + "guardrails.dialog.importWeakening.breakage": "Mindestens eine hervorgehobene Einstellung lässt zusätzliche sensible Werte unverändert Anbieter erreichen.", + "guardrails.dialog.importWeakening.undo": "Exportieren Sie das aktuelle Paket, bevor Sie diesen Import anwenden.", + "guardrails.dialog.importWeakening.confirm": "Reduzierten Schutz anwenden", "models.displayNameSavedRefreshFailed": "Die Änderung wurde gespeichert, aber die Modellliste konnte nicht aktualisiert werden. Versuchen Sie es erneut.", "models.displayNameOutcomeUnknown": "Die Anfrage wurde nicht abgeschlossen. Die Änderung wurde möglicherweise gespeichert. Prüfen Sie den aktuellen Namen durch erneutes Versuchen, bevor Sie ihn weiter ändern.", "models.displayNameCurrentUnavailable": "Aktueller Name erst nach Aktualisierung verfügbar", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 53d940d7e6..b05c966351 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -2604,6 +2604,278 @@ export const en = { "usage.scope.machine": "This machine", "usage.scope.hub": "Hub-wide", "usage.hubOffline": "Hub usage is unavailable. Local usage was not substituted.", + "nav.guardrails": "Guardrails", + "guardrails.title": "Sensitive data guardrails", + "guardrails.subtitle": "Replace detected sensitive values with reversible placeholders before requests leave this proxy.", + "guardrails.loading": "Loading Guardrails settings…", + "guardrails.loadFailed": "Could not load Guardrails settings.", + "guardrails.saveFailed": "Could not save Guardrails settings.", + "guardrails.saved": "Guardrails settings saved.", + "guardrails.statusActive": "Active", + "guardrails.statusProtected": "Protected", + "guardrails.statusDetectOnly": "Detect only", + "guardrails.statusReducedCoverage": "Reduced coverage", + "guardrails.statusUnavailable": "Registry unavailable", + "guardrails.statusNoRules": "No active rules", + "guardrails.statusNoProviderCoverage": "No providers protected", + "guardrails.statusDisabled": "Disabled", + "guardrails.valueOn": "On", + "guardrails.valueOff": "Off", + "guardrails.settingsTitle": "Request protection", + "guardrails.settingsHint": "Changes are applied atomically to new requests. Existing streams keep their current mapping.", + "guardrails.enabled": "Enable Guardrails", + "guardrails.enabledHint": "Mask allowlisted prompt and tool fields before they reach an upstream provider.", + "guardrails.mode": "Mode", + "guardrails.modeEnforce": "Enforce masking", + "guardrails.modeDetect": "Detect only", + "guardrails.failurePolicy": "Processing failure", + "guardrails.failureBlock": "Block request", + "guardrails.failurePassthrough": "Pass through request", + "guardrails.keywordPrefilter": "Keyword prefilter", + "guardrails.keywordPrefilterHint": "Skip only built-in rules whose regex proves every match contains a declared keyword. Custom and unproven rules are always scanned.", + "guardrails.reducedCoverageWarning": "Reduced coverage: some data types or built-in rules are disabled, provider coverage is limited, or processing failures may pass through.", + "guardrails.providerScope": "Protected providers", + "guardrails.providerScopeAllValue": "All providers", + "guardrails.providerScopeSelectedValue": "Selected ({count})", + "guardrails.importSecurityProviderScope": "Provider coverage: {before} → {after}", + "guardrails.providerScopeAllHint": "All current and future providers are protected.", + "guardrails.providerScopeSelectedHint": "Only checked providers are protected. New providers remain unprotected until selected.", + "guardrails.providerAnthropicNative": "Anthropic (native)", + "guardrails.providerDisabled": "disabled", + "guardrails.providerNotConfigured": "not configured", + "guardrails.lastProviderRequired": "Keep at least one provider selected, or disable Guardrails globally.", + "guardrails.providerScopeWarning": "Provider coverage is limited. Requests routed to unchecked providers are not scanned or masked.", + "guardrails.noProviderCoverageWarning": "No active provider is selected. Current provider traffic is not scanned or masked.", + "guardrails.dialog.providerScope.title": "Limit provider coverage?", + "guardrails.dialog.providerScope.changes": "New requests routed directly to the unchecked provider will no longer be scanned or masked.", + "guardrails.dialog.providerScope.breakage": "Sensitive values may reach that provider unchanged. Protected continuations cannot cross this boundary.", + "guardrails.dialog.providerScope.undo": "Re-select the provider in Guardrails Settings to restore protection.", + "guardrails.dialog.providerScope.confirm": "Limit coverage", + "guardrails.lastDataTypeRequired": "At least one data type must remain enabled.", + "guardrails.dataTypes": "Enabled data types", + "guardrails.dataType": "Data type", + "guardrails.dataType.credentials": "Credentials", + "guardrails.groupCredentialUrls": "Credential URLs", + "guardrails.dataType.apiKeys": "API keys", + "guardrails.dataType.accessTokens": "Access tokens", + "guardrails.dataType.ipAddresses": "IP addresses", + "guardrails.dataType.personal": "Personal data", + "guardrails.dataType.custom": "Custom", + "guardrails.rulesTitle": "Rule management", + "guardrails.rulesHint": "Disable a built-in rule or manage local custom rules. Built-in regexes are never exposed here.", + "guardrails.selectBuiltin": "Select a built-in rule", + "guardrails.disableBuiltin": "Disable rule", + "guardrails.enableBuiltin": "Enable rule", + "guardrails.noDisabledBuiltins": "No built-in rules are disabled.", + "guardrails.customRules": "Custom rules", + "guardrails.noCustomRules": "No custom rules yet.", + "guardrails.addRule": "Add rule", + "guardrails.editRule": "Edit rule", + "guardrails.deleteRule": "Delete rule", + "guardrails.removeRuleConfirm": "Delete custom rule {ruleId}?", + "guardrails.ruleSaveFailed": "Could not save the custom rule.", + "guardrails.ruleDeleteFailed": "Could not delete the custom rule.", + "guardrails.ruleSaved": "Custom rule saved.", + "guardrails.ruleDeleted": "Custom rule deleted.", + "guardrails.ruleId": "Rule ID", + "guardrails.ruleName": "Rule name", + "guardrails.group": "Group", + "guardrails.groupPriority": "Group priority", + "guardrails.displayName": "Display name", + "guardrails.description": "Description", + "guardrails.regex": "RE2 pattern", + "guardrails.placeholderType": "Placeholder type", + "guardrails.captureGroups": "Capture groups", + "guardrails.captureGroupsInvalid": "Use a comma-separated list of positive integer group numbers.", + "guardrails.minLength": "Minimum length", + "guardrails.entropy": "Entropy threshold", + "guardrails.keywords": "Keywords", + "guardrails.banlist": "Banlist", + "guardrails.validators": "Validators", + "guardrails.saveRule": "Save rule", + "guardrails.cancelEdit": "Cancel", + "guardrails.tabsLabel": "Guardrails sections", + "guardrails.tab.overview": "Overview", + "guardrails.tab.rules": "Rules", + "guardrails.tab.tester": "Tester", + "guardrails.tab.activity": "Activity", + "guardrails.tab.settings": "Settings", + "guardrails.detectWarning": "Detect mode reports findings but does not mask sensitive data before it leaves the proxy.", + "guardrails.passthroughWarning": "Passthrough can send unmasked input upstream when Guardrails processing fails.", + "guardrails.metric.scanned": "Requests scanned", + "guardrails.metric.masked": "Values masked", + "guardrails.metric.detected": "Values detected", + "guardrails.metric.errors": "Warnings and blocks", + "guardrails.registryReady": "Registry ready", + "guardrails.registryFailed": "Registry failed", + "guardrails.registryDisabled": "Registry disabled", + "guardrails.effectiveRules": "{count} effective rule(s)", + "guardrails.registryGeneration": "Generation {generation}", + "guardrails.lastPassthrough": "Last passthrough incident: {date}", + "guardrails.topRules": "Top rules", + "guardrails.topCategories": "Top data categories", + "guardrails.noActivity": "No Guardrails activity yet", + "guardrails.noActivityHint": "Metadata appears here after protected requests are processed.", + "guardrails.documentation": "Read the Guardrails documentation", + "guardrails.searchRules": "Search rules", + "guardrails.source": "Source", + "guardrails.sourceOpenCodex": "OpenCodex rules", + "guardrails.sourceManual": "Built in", + "guardrails.sourceGitleaks": "Gitleaks rules", + "guardrails.allSources": "All sources", + "guardrails.status": "Status", + "guardrails.allStatuses": "All statuses", + "guardrails.allDataTypes": "All data types", + "guardrails.filteredRules": "{count} filtered rule(s)", + "guardrails.bulkEnable": "Enable {count}", + "guardrails.bulkDisable": "Disable {count}", + "guardrails.export": "Export", + "guardrails.exported": "Guardrails bundle exported.", + "guardrails.exportFailed": "Could not export the Guardrails bundle.", + "guardrails.import": "Import", + "guardrails.importMode": "Import mode", + "guardrails.importMerge": "Merge", + "guardrails.importReplace": "Replace", + "guardrails.importFailed": "Could not import Guardrails rules.", + "guardrails.importTooLarge": "Guardrails import files must be 4 MiB or smaller.", + "guardrails.imported": "Guardrails rules imported.", + "guardrails.importPreviewTitle": "Import preview", + "guardrails.importSecurityTitle": "Security settings", + "guardrails.importSecurityWeakening": "This import reduces protection. Review every highlighted change before continuing.", + "guardrails.importSecurityPotential": "This import changes security-relevant custom rule definitions and may reduce protection. Review them before continuing.", + "guardrails.importSecurityUnchanged": "No Guardrails security settings will change.", + "guardrails.importSecurityEnabled": "Traffic protection: {before} → {after}", + "guardrails.importSecurityMode": "Mode: {before} → {after}", + "guardrails.importSecurityFailure": "Failure handling: {before} → {after}", + "guardrails.importSecurityDataTypes": "Enabled data types: {before} → {after}", + "guardrails.importSecurityDisabledRules": "Disabled built-in rules: {before} → {after} (newly disabled: {disabled}, re-enabled: {reenabled})", + "guardrails.importSecurityCustomRules": "Custom rules: {before} → {after} (removed: {removed}, changed: {changed})", + "guardrails.importSecurityChangedRuleIds": "Changed rule definitions:", + "guardrails.importSecurityRemovedRuleIds": "Removed rule IDs:", + "guardrails.importSecurityPrefilter": "Keyword prefilter: {before} → {after}", + "guardrails.importSecurityReduced": "Reduced protection", + "guardrails.importSecurityReviewRequired": "Security review required", + "guardrails.importSecurityRemovedTypes": "No longer covered: {types}", + "guardrails.importCreateCount": "Create: {count}", + "guardrails.importReplaceCount": "Replace: {count}", + "guardrails.importUnchangedCount": "Unchanged: {count}", + "guardrails.importConflicts": "{count} conflicting rule IDs must be resolved before import.", + "guardrails.importApply": "Apply import", + "guardrails.noRules": "No rules match these filters.", + "guardrails.previous": "Previous", + "guardrails.next": "Next", + "guardrails.page": "Page {current} of {total}", + "guardrails.testerTitle": "Sensitive data tester", + "guardrails.testerHint": "Scan with the effective registry on this OpenCodex instance. The sample is sent to its Management API, never to an LLM provider, and is not stored server-side.", + "guardrails.testerSimulation": "Tester simulation only. Use synthetic values; never paste real secrets. In connected mode the sample may traverse your OpenCodex hub.", + "guardrails.testerTrafficDisabled": "Real traffic protection is disabled. Requests currently leave the proxy without Guardrails masking.", + "guardrails.testerTrafficDetect": "Real traffic is detect-only and is not masked before it reaches the provider.", + "guardrails.testerTrafficUnknown": "Real traffic protection status is still unknown. Wait for Guardrails status to load before relying on it.", + "guardrails.testerTrafficUnavailable": "Real traffic protection is unavailable. Resolve the Guardrails activation or registry failure before relying on masking.", + "guardrails.testerTrafficNoRules": "Real traffic has no active Guardrails rules. Enable at least one effective rule before relying on masking.", + "guardrails.testerTrafficNoProviderCoverage": "The simulation can find matches, but no active provider is currently selected for traffic protection.", + "guardrails.testerTrafficReduced": "Real traffic uses reduced protection. Review disabled rules, data types, provider coverage, and failure policy before relying on masking.", + "guardrails.testerDraftSettings": "Use draft settings", + "guardrails.testerDraftSettingsHint": "Test another data-type and keyword-prefilter selection without saving it.", + "guardrails.testerInput": "Text to scan", + "guardrails.testerPlaceholder": "Paste sample text here…", + "guardrails.byteCounter": "{current} / {max} bytes", + "guardrails.clear": "Clear", + "guardrails.test": "Scan", + "guardrails.testing": "Scanning…", + "guardrails.testerFailed": "Could not scan the text.", + "guardrails.testerResult": "Scan result", + "guardrails.effective": "Effective settings", + "guardrails.draft": "Draft settings", + "guardrails.findings": "Findings", + "guardrails.noFindings": "Enabled rules found no matches; this does not guarantee the text contains no sensitive data.", + "guardrails.placeholder": "Placeholder", + "guardrails.offsets": "Offsets", + "guardrails.activityTitle": "Guardrails activity", + "guardrails.activityPrivacy": "In-memory metadata only; this is not an audit log. Prompt text and placeholder mappings are never stored here.", + "guardrails.category": "Category", + "guardrails.allCategories": "All categories", + "guardrails.generation": "Generation", + "guardrails.activitySummary": "Filtered activity summary", + "guardrails.activityEvents": "Events: {count}", + "guardrails.activityFindings": "Findings: {count}", + "guardrails.activityAverageLatency": "Average latency: {value} ms", + "guardrails.activityTopRule": "Top rule: {rule}", + "guardrails.recentActivity": "Recent activity", + "guardrails.surface": "Surface", + "guardrails.surfaceCompact": "Compact", + "guardrails.allSurfaces": "All surfaces", + "guardrails.allModes": "All modes", + "guardrails.result": "Result", + "guardrails.allResults": "All results", + "guardrails.result.scanned": "Scanned", + "guardrails.result.masked": "Masked", + "guardrails.result.detected": "Detected", + "guardrails.result.blocked": "Blocked", + "guardrails.result.passthrough": "Passed through", + "guardrails.result.demaskWarning": "Demask warning", + "guardrails.result.toolSkipped": "Tool restore skipped", + "guardrails.refresh": "Refresh", + "guardrails.retentionEvents": "{current} of {max} events retained", + "guardrails.retentionEvicted": "{count} event(s) evicted", + "guardrails.showingEvents": "Showing {shown} of {total} matching event(s)", + "guardrails.oldestEvent": "Oldest retained event: {date}", + "guardrails.lastRefresh": "Last refresh: {date}", + "guardrails.tableScrollHint": "Scroll the table horizontally to see every column.", + "guardrails.time": "Time", + "guardrails.count": "Count", + "guardrails.rules": "Rules", + "guardrails.latency": "Latency", + "guardrails.dialog.disable.title": "Disable Guardrails?", + "guardrails.dialog.disable.changes": "New requests will no longer be scanned or masked.", + "guardrails.dialog.disable.breakage": "Sensitive values may leave this proxy unchanged.", + "guardrails.dialog.disable.undo": "Turn Guardrails back on in Overview or Settings.", + "guardrails.dialog.disable.confirm": "Disable", + "guardrails.dialog.detect.title": "Switch to detect-only mode?", + "guardrails.dialog.detect.changes": "Findings will be counted, but request text will not be masked.", + "guardrails.dialog.detect.breakage": "Detect mode does not protect data sent upstream.", + "guardrails.dialog.detect.undo": "Switch the mode back to Enforce.", + "guardrails.dialog.detect.confirm": "Use detect mode", + "guardrails.dialog.passthrough.title": "Allow passthrough on failure?", + "guardrails.dialog.passthrough.changes": "A Guardrails processing failure will forward the original request.", + "guardrails.dialog.passthrough.breakage": "The failed request can contain unmasked sensitive values.", + "guardrails.dialog.passthrough.undo": "Set failure handling back to Block request.", + "guardrails.dialog.passthrough.confirm": "Allow passthrough", + "guardrails.dialog.ruleDisable.title": "Disable rule {ruleId}?", + "guardrails.dialog.ruleDisable.changes": "This built-in rule will stop scanning new requests.", + "guardrails.dialog.ruleDisable.breakage": "Values detected only by this rule will no longer be protected.", + "guardrails.dialog.ruleDisable.undo": "Enable the rule again from the Rules tab.", + "guardrails.dialog.ruleDisable.confirm": "Disable rule", + "guardrails.dialog.categoryDisable.title": "Disable a data category?", + "guardrails.dialog.categoryDisable.changes": "Rules in this category will stop scanning new requests.", + "guardrails.dialog.categoryDisable.breakage": "Sensitive values covered only by this category may leave the proxy unchanged.", + "guardrails.dialog.categoryDisable.undo": "Enable the category again in Guardrails Settings.", + "guardrails.dialog.categoryDisable.confirm": "Disable category", + "guardrails.dialog.prefilter.title": "Enable the keyword prefilter?", + "guardrails.dialog.prefilter.changes": "Rules with keywords may be skipped when their keywords are absent from the candidate text.", + "guardrails.dialog.prefilter.breakage": "This performance trade-off can reduce detection recall.", + "guardrails.dialog.prefilter.undo": "Turn the keyword prefilter off in Guardrails Settings.", + "guardrails.dialog.prefilter.confirm": "Enable prefilter", + "guardrails.dialog.bulkDisable.title": "Disable {count} rules?", + "guardrails.dialog.bulkDisable.changes": "Every filtered built-in rule in this selection will be disabled.", + "guardrails.dialog.bulkDisable.breakage": "Sensitive values covered only by these rules may leave the proxy unchanged.", + "guardrails.dialog.bulkDisable.undo": "Enable the rules again from the Rules tab or import a prior export.", + "guardrails.dialog.bulkDisable.confirm": "Disable rules", + "guardrails.dialog.delete.title": "Delete custom rule {ruleId}?", + "guardrails.dialog.delete.changes": "The custom rule will be removed from the effective registry.", + "guardrails.dialog.delete.breakage": "Values detected only by this rule will no longer be protected.", + "guardrails.dialog.delete.undo": "Import or recreate the rule to restore it.", + "guardrails.dialog.delete.confirm": "Delete rule", + "guardrails.dialog.import.title": "Replace all Guardrails rules?", + "guardrails.dialog.import.changes": "The imported bundle will replace current Guardrails settings and custom rules.", + "guardrails.dialog.import.breakage": "Local rules absent from the bundle will be removed.", + "guardrails.dialog.import.undo": "Export the current bundle before replacing it.", + "guardrails.dialog.import.confirm": "Replace rules", + "guardrails.dialog.importWeakening.title": "Apply an import that reduces protection?", + "guardrails.dialog.importWeakening.changes": "The imported bundle will replace Guardrails settings and custom rules with the previewed values.", + "guardrails.dialog.importWeakening.breakage": "One or more highlighted settings will allow additional sensitive values to reach providers unchanged.", + "guardrails.dialog.importWeakening.undo": "Export the current bundle before applying this import.", + "guardrails.dialog.importWeakening.confirm": "Apply reduced protection", "models.displayNameSavedRefreshFailed": "The change was saved, but the model list could not be refreshed. Retry to refresh it.", "models.displayNameOutcomeUnknown": "The request did not finish. The change may have been saved. Retry to check the current name before making another change.", "models.displayNameCurrentUnavailable": "Current name unavailable until refresh", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 7afb44ab2c..43978a80c0 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -2557,6 +2557,278 @@ export const fr: Record = { "integrations.cursor.colReasoning": "Raisonnement", "integrations.cursor.colContext": "Contexte", "integrations.cursor.guide": "Ouvrir le guide de Cursor Private Inference", + "nav.guardrails": "Protection des données", + "guardrails.title": "Protection des données sensibles", + "guardrails.subtitle": "Remplace les valeurs sensibles détectées par des espaces réservés réversibles avant que les requêtes ne quittent ce proxy.", + "guardrails.loading": "Chargement des paramètres de protection des données…", + "guardrails.loadFailed": "Impossible de charger les paramètres de protection des données.", + "guardrails.saveFailed": "Impossible d’enregistrer les paramètres de protection des données.", + "guardrails.saved": "Paramètres de protection des données enregistrés.", + "guardrails.statusActive": "Activée", + "guardrails.statusDisabled": "Désactivée", + "guardrails.settingsTitle": "Protection des requêtes", + "guardrails.settingsHint": "Les modifications s’appliquent de façon atomique aux nouvelles requêtes. Les flux existants conservent leur correspondance actuelle.", + "guardrails.enabled": "Activer la protection des données", + "guardrails.enabledHint": "Masquer les champs autorisés des invites et des outils avant leur transmission au fournisseur en amont.", + "guardrails.mode": "Mode de fonctionnement", + "guardrails.modeEnforce": "Appliquer le masquage", + "guardrails.modeDetect": "Détection uniquement", + "guardrails.failurePolicy": "En cas d’échec du traitement", + "guardrails.failureBlock": "Bloquer la requête", + "guardrails.failurePassthrough": "Transmettre la requête", + "guardrails.keywordPrefilter": "Préfiltre par mot-clé", + "guardrails.keywordPrefilterHint": "Ignorer uniquement les règles intégrées dont l’expression prouve que chaque correspondance contient un mot-clé. Les règles personnalisées ou non prouvées sont toujours analysées.", + "guardrails.dataTypes": "Types de données activés", + "guardrails.dataType": "Type de données", + "guardrails.dataType.credentials": "Identifiants", + "guardrails.groupCredentialUrls": "URL avec identifiants", + "guardrails.dataType.apiKeys": "Clés API", + "guardrails.dataType.accessTokens": "Jetons d’accès", + "guardrails.dataType.ipAddresses": "Adresses IP", + "guardrails.dataType.personal": "Données personnelles", + "guardrails.dataType.custom": "Personnalisé", + "guardrails.rulesTitle": "Gestion des règles", + "guardrails.rulesHint": "Désactivez une règle intégrée ou gérez les règles personnalisées locales. Les expressions régulières intégrées ne sont jamais affichées ici.", + "guardrails.selectBuiltin": "Sélectionner une règle intégrée", + "guardrails.disableBuiltin": "Désactiver la règle", + "guardrails.enableBuiltin": "Activer la règle", + "guardrails.noDisabledBuiltins": "Aucune règle intégrée n’est désactivée.", + "guardrails.customRules": "Règles personnalisées", + "guardrails.noCustomRules": "Aucune règle personnalisée pour le moment.", + "guardrails.addRule": "Ajouter une règle", + "guardrails.editRule": "Modifier la règle", + "guardrails.deleteRule": "Supprimer la règle", + "guardrails.removeRuleConfirm": "Supprimer la règle personnalisée {ruleId} ?", + "guardrails.ruleSaveFailed": "Impossible d’enregistrer la règle personnalisée.", + "guardrails.ruleDeleteFailed": "Impossible de supprimer la règle personnalisée.", + "guardrails.ruleSaved": "Règle personnalisée enregistrée.", + "guardrails.ruleDeleted": "Règle personnalisée supprimée.", + "guardrails.ruleId": "Identifiant de la règle", + "guardrails.ruleName": "Nom de la règle", + "guardrails.group": "Groupe", + "guardrails.groupPriority": "Priorité du groupe", + "guardrails.displayName": "Nom affiché", + "guardrails.description": "Présentation de la règle", + "guardrails.regex": "Expression RE2", + "guardrails.placeholderType": "Type d’espace réservé", + "guardrails.captureGroups": "Groupes de capture", + "guardrails.captureGroupsInvalid": "Saisissez une liste de numéros de groupes entiers positifs, séparés par des virgules.", + "guardrails.minLength": "Longueur minimale", + "guardrails.entropy": "Seuil d’entropie", + "guardrails.keywords": "Mots-clés", + "guardrails.banlist": "Liste d’exclusion", + "guardrails.validators": "Validateurs", + "guardrails.saveRule": "Enregistrer la règle", + "guardrails.cancelEdit": "Annuler", + "guardrails.tabsLabel": "Sections de la protection des données", + "guardrails.tab.overview": "Vue d’ensemble", + "guardrails.tab.rules": "Règles", + "guardrails.tab.tester": "Test", + "guardrails.tab.activity": "Activité", + "guardrails.tab.settings": "Paramètres", + "guardrails.detectWarning": "Le mode Détection signale les valeurs sensibles, mais ne les masque pas avant qu’elles ne quittent le proxy.", + "guardrails.passthroughWarning": "En cas d’échec du traitement, la transmission peut envoyer en amont des données non masquées.", + "guardrails.metric.scanned": "Requêtes analysées", + "guardrails.metric.masked": "Valeurs masquées", + "guardrails.metric.detected": "Valeurs détectées", + "guardrails.metric.errors": "Avertissements et blocages", + "guardrails.registryReady": "Registre prêt", + "guardrails.registryFailed": "Échec du registre", + "guardrails.registryDisabled": "Registre désactivé", + "guardrails.effectiveRules": "Règles actives : {count}", + "guardrails.registryGeneration": "Génération {generation}", + "guardrails.lastPassthrough": "Dernier incident de transmission sans masquage : {date}", + "guardrails.topRules": "Règles principales", + "guardrails.topCategories": "Principales catégories de données", + "guardrails.noActivity": "Aucune activité de protection des données pour le moment", + "guardrails.noActivityHint": "Les métadonnées apparaîtront ici après le traitement de requêtes protégées.", + "guardrails.documentation": "Consulter la documentation sur la protection des données", + "guardrails.searchRules": "Rechercher des règles", + "guardrails.source": "Origine", + "guardrails.sourceOpenCodex": "Règles OpenCodex", + "guardrails.sourceManual": "Intégrée", + "guardrails.sourceGitleaks": "Règles Gitleaks", + "guardrails.allSources": "Toutes les sources", + "guardrails.status": "État", + "guardrails.allStatuses": "Tous les états", + "guardrails.allDataTypes": "Tous les types de données", + "guardrails.filteredRules": "Règles filtrées : {count}", + "guardrails.bulkEnable": "Activer ({count})", + "guardrails.bulkDisable": "Désactiver ({count})", + "guardrails.export": "Exporter", + "guardrails.exported": "Bundle Guardrails exporté.", + "guardrails.exportFailed": "Impossible d’exporter le bundle Guardrails.", + "guardrails.import": "Importer", + "guardrails.importMode": "Mode d’importation", + "guardrails.importMerge": "Fusionner", + "guardrails.importReplace": "Remplacer", + "guardrails.importFailed": "Impossible d’importer les règles de protection des données.", + "guardrails.importTooLarge": "Les fichiers d’importation de protection des données ne doivent pas dépasser 4 Mio.", + "guardrails.imported": "Règles de protection des données importées.", + "guardrails.importPreviewTitle": "Aperçu de l’importation", + "guardrails.importCreateCount": "À créer : {count}", + "guardrails.importReplaceCount": "À remplacer : {count}", + "guardrails.importUnchangedCount": "Inchangées : {count}", + "guardrails.importConflicts": "Des identifiants de règle sont en conflit : {count}. Résolvez-les avant l’importation.", + "guardrails.importApply": "Appliquer l’importation", + "guardrails.noRules": "Aucune règle ne correspond à ces filtres.", + "guardrails.previous": "Précédent", + "guardrails.next": "Suivant", + "guardrails.page": "Page {current} sur {total}", + "guardrails.testerTitle": "Testeur de données sensibles", + "guardrails.testerHint": "Analysez avec le registre actif de cette instance OpenCodex. L’échantillon est envoyé à son API de gestion, jamais à un fournisseur LLM, et n’est pas stocké côté serveur.", + "guardrails.testerDraftSettings": "Utiliser les paramètres provisoires", + "guardrails.testerDraftSettingsHint": "Testez un autre choix de types de données et de préfiltre par mot-clé sans l’enregistrer.", + "guardrails.testerInput": "Texte à analyser", + "guardrails.testerPlaceholder": "Collez ici un exemple de texte…", + "guardrails.byteCounter": "{current} / {max} octets", + "guardrails.clear": "Effacer", + "guardrails.test": "Analyser", + "guardrails.testing": "Analyse…", + "guardrails.testerFailed": "Impossible d’analyser le texte.", + "guardrails.testerResult": "Résultat de l’analyse", + "guardrails.effective": "Paramètres actifs", + "guardrails.draft": "Paramètres provisoires", + "guardrails.findings": "Détections", + "guardrails.noFindings": "Les règles activées n’ont trouvé aucune correspondance ; cela ne garantit pas que le texte ne contient aucune donnée sensible.", + "guardrails.placeholder": "Espace réservé", + "guardrails.offsets": "Positions", + "guardrails.activityTitle": "Activité de protection des données", + "guardrails.activityPrivacy": "Métadonnées en mémoire uniquement ; il ne s’agit pas d’un journal d’audit. Le texte des invites et les correspondances des espaces réservés ne sont jamais enregistrés ici.", + "guardrails.category": "Catégorie", + "guardrails.allCategories": "Toutes les catégories", + "guardrails.generation": "Génération", + "guardrails.activitySummary": "Résumé de l’activité filtrée", + "guardrails.activityEvents": "Événements : {count}", + "guardrails.activityFindings": "Détections : {count}", + "guardrails.activityAverageLatency": "Latence moyenne : {value} ms", + "guardrails.activityTopRule": "Règle principale : {rule}", + "guardrails.recentActivity": "Activité récente", + "guardrails.surface": "Interface", + "guardrails.surfaceCompact": "Compactage", + "guardrails.allSurfaces": "Toutes les interfaces", + "guardrails.allModes": "Tous les modes", + "guardrails.result": "Résultat", + "guardrails.allResults": "Tous les résultats", + "guardrails.result.scanned": "Analyse effectuée", + "guardrails.result.masked": "Masquage effectué", + "guardrails.result.detected": "Détection effectuée", + "guardrails.result.blocked": "Requête bloquée", + "guardrails.result.passthrough": "Transmise sans traitement", + "guardrails.result.demaskWarning": "Avertissement de restauration", + "guardrails.result.toolSkipped": "Restauration de l’outil ignorée", + "guardrails.refresh": "Actualiser", + "guardrails.retentionEvents": "Événements conservés : {current} sur {max}", + "guardrails.retentionEvicted": "Événements supprimés : {count}", + "guardrails.showingEvents": "Résultats affichés : {shown} sur {total}", + "guardrails.oldestEvent": "Événement conservé le plus ancien : {date}", + "guardrails.lastRefresh": "Dernière actualisation : {date}", + "guardrails.tableScrollHint": "Faites défiler le tableau horizontalement pour voir toutes les colonnes.", + "guardrails.time": "Heure", + "guardrails.count": "Nombre", + "guardrails.rules": "Règles", + "guardrails.latency": "Latence", + "guardrails.dialog.disable.title": "Désactiver la protection des données ?", + "guardrails.dialog.disable.changes": "Les nouvelles requêtes ne seront plus analysées ni masquées.", + "guardrails.dialog.disable.breakage": "Des valeurs sensibles pourront quitter ce proxy sans modification.", + "guardrails.dialog.disable.undo": "Réactivez la protection des données dans Vue d’ensemble ou Paramètres.", + "guardrails.dialog.disable.confirm": "Désactiver", + "guardrails.dialog.detect.title": "Passer au mode Détection uniquement ?", + "guardrails.dialog.detect.changes": "Les détections seront comptabilisées, mais le texte des requêtes ne sera pas masqué.", + "guardrails.dialog.detect.breakage": "Le mode Détection ne protège pas les données envoyées en amont.", + "guardrails.dialog.detect.undo": "Repassez au mode Appliquer le masquage.", + "guardrails.dialog.detect.confirm": "Utiliser le mode Détection", + "guardrails.dialog.passthrough.title": "Autoriser la transmission en cas d’échec ?", + "guardrails.dialog.passthrough.changes": "Un échec du traitement de protection des données transmettra la requête d’origine.", + "guardrails.dialog.passthrough.breakage": "La requête concernée peut contenir des valeurs sensibles non masquées.", + "guardrails.dialog.passthrough.undo": "Rétablissez l’option Bloquer la requête pour la gestion des échecs.", + "guardrails.dialog.passthrough.confirm": "Autoriser la transmission", + "guardrails.dialog.ruleDisable.title": "Désactiver la règle {ruleId} ?", + "guardrails.dialog.ruleDisable.changes": "Cette règle intégrée cessera d’analyser les nouvelles requêtes.", + "guardrails.dialog.ruleDisable.breakage": "Les valeurs détectées uniquement par cette règle ne seront plus protégées.", + "guardrails.dialog.ruleDisable.undo": "Réactivez la règle depuis l’onglet Règles.", + "guardrails.dialog.ruleDisable.confirm": "Désactiver la règle", + "guardrails.dialog.bulkDisable.title": "Désactiver {count} règles ?", + "guardrails.dialog.bulkDisable.changes": "Toutes les règles intégrées filtrées de cette sélection seront désactivées.", + "guardrails.dialog.bulkDisable.breakage": "Les valeurs sensibles couvertes uniquement par ces règles pourront quitter le proxy sans modification.", + "guardrails.dialog.bulkDisable.undo": "Réactivez les règles depuis l’onglet Règles ou importez une exportation antérieure.", + "guardrails.dialog.bulkDisable.confirm": "Désactiver les règles", + "guardrails.dialog.delete.title": "Supprimer la règle personnalisée {ruleId} ?", + "guardrails.dialog.delete.changes": "La règle personnalisée sera retirée du registre actif.", + "guardrails.dialog.delete.breakage": "Les valeurs détectées uniquement par cette règle ne seront plus protégées.", + "guardrails.dialog.delete.undo": "Importez ou recréez la règle pour la restaurer.", + "guardrails.dialog.delete.confirm": "Supprimer la règle", + "guardrails.dialog.import.title": "Remplacer toutes les règles de protection des données ?", + "guardrails.dialog.import.changes": "Le paquet importé remplacera les paramètres actuels de protection des données et les règles personnalisées.", + "guardrails.dialog.import.breakage": "Les règles locales absentes du paquet seront supprimées.", + "guardrails.dialog.import.undo": "Exportez le paquet actuel avant de le remplacer.", + "guardrails.dialog.import.confirm": "Remplacer les règles", + "guardrails.statusProtected": "Protégé", + "guardrails.statusDetectOnly": "Détection uniquement", + "guardrails.statusReducedCoverage": "Couverture réduite", + "guardrails.statusUnavailable": "Registre indisponible", + "guardrails.statusNoRules": "Aucune règle active", + "guardrails.statusNoProviderCoverage": "Aucun fournisseur protégé", + "guardrails.valueOn": "Activé", + "guardrails.valueOff": "Désactivé", + "guardrails.reducedCoverageWarning": "Couverture réduite : des types de données ou règles intégrées sont désactivés, la couverture des fournisseurs est limitée ou les erreurs peuvent être transmises.", + "guardrails.providerScope": "Fournisseurs protégés", + "guardrails.providerScopeAllValue": "Tous les fournisseurs", + "guardrails.providerScopeSelectedValue": "Sélectionnés ({count})", + "guardrails.importSecurityProviderScope": "Couverture des fournisseurs : {before} → {after}", + "guardrails.providerScopeAllHint": "Tous les fournisseurs actuels et futurs sont protégés.", + "guardrails.providerScopeSelectedHint": "Seuls les fournisseurs cochés sont protégés. Les nouveaux fournisseurs restent non protégés jusqu’à leur sélection.", + "guardrails.providerAnthropicNative": "Anthropic (natif)", + "guardrails.providerDisabled": "désactivé", + "guardrails.providerNotConfigured": "non configuré", + "guardrails.lastProviderRequired": "Conservez au moins un fournisseur ou désactivez complètement Guardrails.", + "guardrails.providerScopeWarning": "La couverture des fournisseurs est limitée. Les requêtes vers les fournisseurs non cochés ne sont ni analysées ni masquées.", + "guardrails.noProviderCoverageWarning": "Aucun fournisseur actif n’est sélectionné. Le trafic actuel n’est ni analysé ni masqué.", + "guardrails.dialog.providerScope.title": "Limiter la couverture des fournisseurs ?", + "guardrails.dialog.providerScope.changes": "Les nouvelles requêtes directes vers le fournisseur décoché ne seront plus analysées ni masquées.", + "guardrails.dialog.providerScope.breakage": "Des valeurs sensibles peuvent atteindre ce fournisseur sans modification. Les continuations protégées ne franchissent pas cette limite.", + "guardrails.dialog.providerScope.undo": "Resélectionnez le fournisseur dans les paramètres Guardrails pour rétablir la protection.", + "guardrails.dialog.providerScope.confirm": "Limiter la couverture", + "guardrails.lastDataTypeRequired": "Au moins un type de données doit rester activé.", + "guardrails.importSecurityTitle": "Paramètres de sécurité", + "guardrails.importSecurityWeakening": "Cette importation réduit la protection. Vérifiez chaque modification mise en évidence avant de continuer.", + "guardrails.importSecurityPotential": "Cette importation modifie des règles personnalisées sensibles pour la sécurité et peut réduire la protection. Vérifiez-les avant de continuer.", + "guardrails.importSecurityUnchanged": "Les paramètres de sécurité Guardrails ne changeront pas.", + "guardrails.importSecurityEnabled": "Protection du trafic : {before} → {after}", + "guardrails.importSecurityMode": "Mode : {before} → {after}", + "guardrails.importSecurityFailure": "Gestion des erreurs : {before} → {after}", + "guardrails.importSecurityDataTypes": "Types de données activés : {before} → {after}", + "guardrails.importSecurityDisabledRules": "Règles intégrées désactivées : {before} → {after} (nouvellement désactivées : {disabled}, réactivées : {reenabled})", + "guardrails.importSecurityCustomRules": "Règles personnalisées : {before} → {after} (supprimées : {removed}, modifiées : {changed})", + "guardrails.importSecurityChangedRuleIds": "Définitions de règles modifiées :", + "guardrails.importSecurityRemovedRuleIds": "Identifiants de règles supprimées :", + "guardrails.importSecurityPrefilter": "Préfiltre par mot-clé : {before} → {after}", + "guardrails.importSecurityReduced": "Protection réduite", + "guardrails.importSecurityReviewRequired": "Vérification de sécurité requise", + "guardrails.importSecurityRemovedTypes": "Plus couverts : {types}", + "guardrails.testerSimulation": "Simulation du Tester uniquement. Utilisez des valeurs synthétiques et ne collez jamais de vrais secrets. En mode connecté, l’échantillon peut transiter par votre hub OpenCodex.", + "guardrails.testerTrafficDisabled": "La protection du trafic réel est désactivée. Les requêtes quittent actuellement le proxy sans masquage Guardrails.", + "guardrails.testerTrafficDetect": "Le trafic réel est uniquement détecté et n’est pas masqué avant d’atteindre le fournisseur.", + "guardrails.testerTrafficUnknown": "L’état de protection du trafic réel est encore inconnu. Attendez le chargement de Guardrails.", + "guardrails.testerTrafficUnavailable": "La protection du trafic réel est indisponible. Corrigez l’activation ou le registre Guardrails.", + "guardrails.testerTrafficNoRules": "Aucune règle Guardrails n’est active pour le trafic réel. Activez au moins une règle effective.", + "guardrails.testerTrafficNoProviderCoverage": "La simulation peut trouver des correspondances, mais aucun fournisseur actif n’est sélectionné pour protéger le trafic.", + "guardrails.testerTrafficReduced": "Le trafic réel bénéficie d’une protection réduite. Vérifiez les règles désactivées, les types, la couverture des fournisseurs et la politique d’échec.", + "guardrails.dialog.categoryDisable.title": "Désactiver une catégorie de données ?", + "guardrails.dialog.categoryDisable.changes": "Les règles de cette catégorie ne contrôleront plus les nouvelles requêtes.", + "guardrails.dialog.categoryDisable.breakage": "Les valeurs sensibles couvertes uniquement par cette catégorie peuvent quitter le proxy sans modification.", + "guardrails.dialog.categoryDisable.undo": "Réactivez la catégorie dans les paramètres Guardrails.", + "guardrails.dialog.categoryDisable.confirm": "Désactiver la catégorie", + "guardrails.dialog.prefilter.title": "Activer le préfiltre par mot-clé ?", + "guardrails.dialog.prefilter.changes": "Les règles avec mots-clés peuvent être ignorées si ces mots-clés sont absents du texte analysé.", + "guardrails.dialog.prefilter.breakage": "Ce compromis de performance peut réduire la couverture de détection.", + "guardrails.dialog.prefilter.undo": "Désactivez le préfiltre par mot-clé dans les paramètres Guardrails.", + "guardrails.dialog.prefilter.confirm": "Activer le préfiltre", + "guardrails.dialog.importWeakening.title": "Appliquer une importation qui réduit la protection ?", + "guardrails.dialog.importWeakening.changes": "Le paquet importé remplacera les paramètres Guardrails et les règles personnalisées par les valeurs affichées.", + "guardrails.dialog.importWeakening.breakage": "Au moins un paramètre mis en évidence permettra à davantage de valeurs sensibles d’atteindre les fournisseurs sans modification.", + "guardrails.dialog.importWeakening.undo": "Exportez le paquet actuel avant d’appliquer cette importation.", + "guardrails.dialog.importWeakening.confirm": "Appliquer la protection réduite", "models.displayNameSavedRefreshFailed": "La modification a été enregistrée, mais la liste des modèles n’a pas pu être actualisée. Réessayez.", "models.displayNameOutcomeUnknown": "La requête n’a pas abouti. La modification a peut-être été enregistrée. Réessayez pour vérifier le nom actuel avant toute autre modification.", "models.displayNameCurrentUnavailable": "Nom actuel indisponible avant actualisation", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 75095da332..ee5d395c3f 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -2591,6 +2591,278 @@ export const ja: Record = { "integrations.cursor.colReasoning": "推論", "integrations.cursor.colContext": "コンテキスト", "integrations.cursor.guide": "Cursor Private Inference のガイドを開く", + "nav.guardrails": "Guardrails", + "guardrails.title": "機密データの保護", + "guardrails.subtitle": "リクエストがこのプロキシを離れる前に、検出された機密値を復元可能なプレースホルダーに置き換えます。", + "guardrails.loading": "Guardrails設定を読み込んでいます…", + "guardrails.loadFailed": "Guardrails設定を読み込めませんでした。", + "guardrails.saveFailed": "Guardrails設定を保存できませんでした。", + "guardrails.saved": "Guardrails設定を保存しました。", + "guardrails.statusActive": "有効", + "guardrails.statusDisabled": "無効", + "guardrails.settingsTitle": "リクエスト保護", + "guardrails.settingsHint": "変更は新しいリクエストにアトミックに適用されます。既存のストリームでは現在のマッピングが維持されます。", + "guardrails.enabled": "Guardrailsを有効にする", + "guardrails.enabledHint": "許可リストに登録されたプロンプトとツールのフィールドを、アップストリームプロバイダーに到達する前にマスクします。", + "guardrails.mode": "モード", + "guardrails.modeEnforce": "マスキングを強制", + "guardrails.modeDetect": "検出のみ", + "guardrails.failurePolicy": "処理失敗時", + "guardrails.failureBlock": "リクエストをブロック", + "guardrails.failurePassthrough": "リクエストをパススルー", + "guardrails.keywordPrefilter": "キーワード事前フィルター", + "guardrails.keywordPrefilterHint": "すべての一致にキーワードが含まれると正規表現から証明できる組み込みルールだけを省略します。カスタムルールと証明できないルールは常にスキャンします。", + "guardrails.dataTypes": "有効なデータ型", + "guardrails.dataType": "データ型", + "guardrails.dataType.credentials": "認証情報", + "guardrails.groupCredentialUrls": "認証情報を含む URL", + "guardrails.dataType.apiKeys": "APIキー", + "guardrails.dataType.accessTokens": "アクセストークン", + "guardrails.dataType.ipAddresses": "IPアドレス", + "guardrails.dataType.personal": "個人データ", + "guardrails.dataType.custom": "カスタム", + "guardrails.rulesTitle": "ルール管理", + "guardrails.rulesHint": "組み込みルールを無効にするか、ローカルのカスタムルールを管理します。組み込みの正規表現がここに表示されることはありません。", + "guardrails.selectBuiltin": "組み込みルールを選択", + "guardrails.disableBuiltin": "ルールを無効化", + "guardrails.enableBuiltin": "ルールを有効化", + "guardrails.noDisabledBuiltins": "無効になっている組み込みルールはありません。", + "guardrails.customRules": "カスタムルール", + "guardrails.noCustomRules": "カスタムルールはまだありません。", + "guardrails.addRule": "ルールを追加", + "guardrails.editRule": "ルールを編集", + "guardrails.deleteRule": "ルールを削除", + "guardrails.removeRuleConfirm": "カスタムルール {ruleId} を削除しますか?", + "guardrails.ruleSaveFailed": "カスタムルールを保存できませんでした。", + "guardrails.ruleDeleteFailed": "カスタムルールを削除できませんでした。", + "guardrails.ruleSaved": "カスタムルールを保存しました。", + "guardrails.ruleDeleted": "カスタムルールを削除しました。", + "guardrails.ruleId": "ルールID", + "guardrails.ruleName": "ルール名", + "guardrails.group": "グループ", + "guardrails.groupPriority": "グループの優先度", + "guardrails.displayName": "表示名", + "guardrails.description": "説明", + "guardrails.regex": "RE2パターン", + "guardrails.placeholderType": "プレースホルダーの種類", + "guardrails.captureGroups": "キャプチャグループ", + "guardrails.captureGroupsInvalid": "正の整数のグループ番号をカンマ区切りで入力してください。", + "guardrails.minLength": "最小長", + "guardrails.entropy": "エントロピーしきい値", + "guardrails.keywords": "キーワード", + "guardrails.banlist": "禁止リスト", + "guardrails.validators": "バリデーター", + "guardrails.saveRule": "ルールを保存", + "guardrails.cancelEdit": "キャンセル", + "guardrails.tabsLabel": "Guardrailsのセクション", + "guardrails.tab.overview": "概要", + "guardrails.tab.rules": "ルール", + "guardrails.tab.tester": "テスター", + "guardrails.tab.activity": "アクティビティ", + "guardrails.tab.settings": "設定", + "guardrails.detectWarning": "検出のみモードでは検出結果が報告されますが、機密データはプロキシを離れる前にマスクされません。", + "guardrails.passthroughWarning": "パススルーを許可すると、Guardrailsの処理に失敗した場合に、マスクされていない入力がアップストリームに送信される可能性があります。", + "guardrails.metric.scanned": "スキャン済みリクエスト", + "guardrails.metric.masked": "マスク済みの値", + "guardrails.metric.detected": "検出済みの値", + "guardrails.metric.errors": "警告とブロック", + "guardrails.lastPassthrough": "直近のパススルー事象: {date}", + "guardrails.topRules": "上位のルール", + "guardrails.topCategories": "上位のデータカテゴリ", + "guardrails.noActivity": "Guardrailsのアクティビティはまだありません", + "guardrails.noActivityHint": "保護対象のリクエストが処理されると、ここにメタデータが表示されます。", + "guardrails.documentation": "Guardrailsのドキュメントを読む", + "guardrails.searchRules": "ルールを検索", + "guardrails.source": "ソース", + "guardrails.sourceOpenCodex": "OpenCodexルール", + "guardrails.sourceManual": "組み込み", + "guardrails.allSources": "すべてのソース", + "guardrails.status": "ステータス", + "guardrails.allStatuses": "すべてのステータス", + "guardrails.allDataTypes": "すべてのデータ型", + "guardrails.filteredRules": "絞り込み結果: {count}件", + "guardrails.bulkEnable": "{count}件を有効化", + "guardrails.bulkDisable": "{count}件を無効化", + "guardrails.export": "エクスポート", + "guardrails.exported": "Guardrails バンドルをエクスポートしました。", + "guardrails.exportFailed": "Guardrails バンドルをエクスポートできませんでした。", + "guardrails.import": "インポート", + "guardrails.importMode": "インポートモード", + "guardrails.importMerge": "マージ", + "guardrails.importReplace": "置換", + "guardrails.importFailed": "Guardrailsルールをインポートできませんでした。", + "guardrails.importTooLarge": "Guardrails のインポートファイルは 4 MiB 以下にしてください。", + "guardrails.imported": "Guardrailsルールをインポートしました。", + "guardrails.noRules": "このフィルターに一致するルールはありません。", + "guardrails.previous": "前へ", + "guardrails.next": "次へ", + "guardrails.page": "ページ {current} / {total}", + "guardrails.testerTitle": "機密データテスター", + "guardrails.testerHint": "この OpenCodex インスタンスの有効なレジストリでスキャンします。サンプルは Management API に送信されますが、LLM プロバイダーには送信されず、サーバー側にも保存されません。", + "guardrails.testerInput": "スキャンするテキスト", + "guardrails.testerPlaceholder": "サンプルテキストをここに貼り付けてください…", + "guardrails.byteCounter": "{current} / {max} バイト", + "guardrails.clear": "クリア", + "guardrails.test": "スキャン", + "guardrails.testing": "スキャン中…", + "guardrails.testerFailed": "テキストをスキャンできませんでした。", + "guardrails.testerResult": "スキャン結果", + "guardrails.effective": "有効な設定", + "guardrails.draft": "下書き設定", + "guardrails.findings": "検出結果", + "guardrails.noFindings": "有効なルールでは一致が見つかりませんでした。テキストに機密データがないことを保証するものではありません。", + "guardrails.placeholder": "プレースホルダー", + "guardrails.offsets": "オフセット", + "guardrails.activityTitle": "Guardrailsのアクティビティ", + "guardrails.activityPrivacy": "メモリ内のメタデータのみです。これは監査ログではありません。プロンプトのテキストとプレースホルダーのマッピングがここに保存されることはありません。", + "guardrails.surface": "サーフェス", + "guardrails.allSurfaces": "すべてのサーフェス", + "guardrails.allModes": "すべてのモード", + "guardrails.result": "結果", + "guardrails.allResults": "すべての結果", + "guardrails.result.scanned": "スキャン済み", + "guardrails.result.masked": "マスク済み", + "guardrails.result.detected": "検出済み", + "guardrails.result.blocked": "ブロック済み", + "guardrails.result.passthrough": "パススルー済み", + "guardrails.result.demaskWarning": "マスク解除の警告", + "guardrails.result.toolSkipped": "ツールの復元をスキップ", + "guardrails.refresh": "更新", + "guardrails.retentionEvents": "保持中のイベント: {current} / {max}件", + "guardrails.retentionEvicted": "破棄されたイベント: {count}件", + "guardrails.lastRefresh": "最終更新: {date}", + "guardrails.time": "時刻", + "guardrails.count": "件数", + "guardrails.rules": "ルール", + "guardrails.latency": "レイテンシ", + "guardrails.dialog.disable.title": "Guardrailsを無効にしますか?", + "guardrails.dialog.disable.changes": "新しいリクエストはスキャンもマスクもされなくなります。", + "guardrails.dialog.disable.breakage": "機密値が変更されないままこのプロキシから送信される可能性があります。", + "guardrails.dialog.disable.undo": "「概要」または「設定」でGuardrailsを再度有効にできます。", + "guardrails.dialog.disable.confirm": "無効にする", + "guardrails.dialog.detect.title": "検出のみモードに切り替えますか?", + "guardrails.dialog.detect.changes": "検出結果はカウントされますが、リクエストのテキストはマスクされません。", + "guardrails.dialog.detect.breakage": "検出のみモードでは、アップストリームに送信されるデータは保護されません。", + "guardrails.dialog.detect.undo": "モードを「マスキングを強制」に戻してください。", + "guardrails.dialog.detect.confirm": "検出のみモードを使用", + "guardrails.dialog.passthrough.title": "失敗時のパススルーを許可しますか?", + "guardrails.dialog.passthrough.changes": "Guardrailsの処理に失敗すると、元のリクエストが転送されます。", + "guardrails.dialog.passthrough.breakage": "失敗したリクエストには、マスクされていない機密値が含まれる可能性があります。", + "guardrails.dialog.passthrough.undo": "失敗時の処理を「リクエストをブロック」に戻してください。", + "guardrails.dialog.passthrough.confirm": "パススルーを許可", + "guardrails.dialog.delete.title": "カスタムルール {ruleId} を削除しますか?", + "guardrails.dialog.delete.changes": "カスタムルールは有効なレジストリから削除されます。", + "guardrails.dialog.delete.breakage": "このルールでのみ検出される値は保護されなくなります。", + "guardrails.dialog.delete.undo": "ルールをインポートするか再作成すると復元できます。", + "guardrails.dialog.delete.confirm": "ルールを削除", + "guardrails.dialog.import.title": "すべてのGuardrailsルールを置き換えますか?", + "guardrails.dialog.import.changes": "インポートしたバンドルが、現在のGuardrails設定とカスタムルールを置き換えます。", + "guardrails.dialog.import.breakage": "バンドルに含まれないローカルルールは削除されます。", + "guardrails.dialog.import.undo": "置き換える前に現在のバンドルをエクスポートしてください。", + "guardrails.dialog.import.confirm": "ルールを置換", + "guardrails.sourceGitleaks": "Gitleaksルール", + "guardrails.surfaceCompact": "コンパクト", + "guardrails.registryReady": "レジストリは準備完了", + "guardrails.registryFailed": "レジストリでエラー", + "guardrails.registryDisabled": "レジストリは無効", + "guardrails.effectiveRules": "有効なルール: {count}件", + "guardrails.registryGeneration": "世代 {generation}", + "guardrails.testerDraftSettings": "下書き設定を使用", + "guardrails.testerDraftSettingsHint": "別のデータ型とキーワード事前フィルターの選択内容を、保存せずにテストします。", + "guardrails.showingEvents": "一致するイベントを{total}件中{shown}件表示", + "guardrails.oldestEvent": "保持中の最も古いイベント: {date}", + "guardrails.tableScrollHint": "すべての列を表示するには、表を横方向にスクロールしてください。", + "guardrails.importPreviewTitle": "インポートのプレビュー", + "guardrails.importCreateCount": "作成: {count}", + "guardrails.importReplaceCount": "置換: {count}", + "guardrails.importUnchangedCount": "変更なし: {count}", + "guardrails.importConflicts": "競合するルールIDを{count}件、インポート前に解決してください。", + "guardrails.importApply": "インポートを適用", + "guardrails.category": "カテゴリ", + "guardrails.allCategories": "すべてのカテゴリ", + "guardrails.generation": "世代", + "guardrails.activitySummary": "絞り込み後のアクティビティ概要", + "guardrails.activityEvents": "イベント: {count}", + "guardrails.activityFindings": "検出結果: {count}", + "guardrails.activityAverageLatency": "平均レイテンシ: {value} ms", + "guardrails.activityTopRule": "上位のルール: {rule}", + "guardrails.recentActivity": "最近のアクティビティ", + "guardrails.dialog.ruleDisable.title": "ルール {ruleId} を無効にしますか?", + "guardrails.dialog.ruleDisable.changes": "この組み込みルールは新しいリクエストのスキャンを停止します。", + "guardrails.dialog.ruleDisable.breakage": "このルールでのみ検出される値は保護されなくなります。", + "guardrails.dialog.ruleDisable.undo": "「ルール」タブからルールを再度有効にできます。", + "guardrails.dialog.ruleDisable.confirm": "ルールを無効化", + "guardrails.dialog.bulkDisable.title": "{count}件のルールを無効にしますか?", + "guardrails.dialog.bulkDisable.changes": "この選択に含まれる、絞り込み済みの組み込みルールがすべて無効になります。", + "guardrails.dialog.bulkDisable.breakage": "これらのルールでのみ検出される機密値が、変更されないままプロキシから送信される可能性があります。", + "guardrails.dialog.bulkDisable.undo": "「ルール」タブからルールを再度有効にするか、以前のエクスポートをインポートしてください。", + "guardrails.dialog.bulkDisable.confirm": "ルールを無効化", + "guardrails.statusProtected": "保護中", + "guardrails.statusDetectOnly": "検出のみ", + "guardrails.statusReducedCoverage": "保護範囲を縮小", + "guardrails.statusUnavailable": "レジストリを利用できません", + "guardrails.statusNoRules": "有効なルールなし", + "guardrails.statusNoProviderCoverage": "保護対象プロバイダーなし", + "guardrails.valueOn": "オン", + "guardrails.valueOff": "オフ", + "guardrails.reducedCoverageWarning": "保護範囲が縮小しています。データ型または組み込みルールが無効、プロバイダー範囲が限定、または処理失敗時にパススルーされる可能性があります。", + "guardrails.providerScope": "保護するプロバイダー", + "guardrails.providerScopeAllValue": "すべてのプロバイダー", + "guardrails.providerScopeSelectedValue": "選択済み({count})", + "guardrails.importSecurityProviderScope": "プロバイダーの保護範囲:{before} → {after}", + "guardrails.providerScopeAllHint": "現在および今後追加されるすべてのプロバイダーを保護します。", + "guardrails.providerScopeSelectedHint": "チェックしたプロバイダーだけを保護します。新しいプロバイダーは選択するまで保護されません。", + "guardrails.providerAnthropicNative": "Anthropic(ネイティブ)", + "guardrails.providerDisabled": "無効", + "guardrails.providerNotConfigured": "未設定", + "guardrails.lastProviderRequired": "少なくとも1つのプロバイダーを選択するか、Guardrails全体を無効にしてください。", + "guardrails.providerScopeWarning": "プロバイダーの保護範囲が限定されています。未選択のプロバイダーへのリクエストはスキャンもマスクもされません。", + "guardrails.noProviderCoverageWarning": "有効なプロバイダーが選択されていません。現在のプロバイダートラフィックはスキャンもマスクもされません。", + "guardrails.dialog.providerScope.title": "プロバイダーの保護範囲を限定しますか?", + "guardrails.dialog.providerScope.changes": "選択を外したプロバイダーへの新しい直接リクエストは、スキャンもマスクもされなくなります。", + "guardrails.dialog.providerScope.breakage": "機密値が変更されずにそのプロバイダーへ送信される可能性があります。保護された継続はこの境界を越えません。", + "guardrails.dialog.providerScope.undo": "Guardrails設定でプロバイダーを再選択すると保護を復元できます。", + "guardrails.dialog.providerScope.confirm": "保護範囲を限定", + "guardrails.lastDataTypeRequired": "少なくとも1つのデータ型を有効にしておく必要があります。", + "guardrails.importSecurityTitle": "セキュリティ設定", + "guardrails.importSecurityWeakening": "このインポートは保護を弱めます。続行前に強調表示された変更をすべて確認してください。", + "guardrails.importSecurityPotential": "このインポートはセキュリティに関係するカスタムルール定義を変更し、保護を弱める可能性があります。続行前に確認してください。", + "guardrails.importSecurityUnchanged": "Guardrails のセキュリティ設定は変更されません。", + "guardrails.importSecurityEnabled": "トラフィック保護: {before} → {after}", + "guardrails.importSecurityMode": "モード: {before} → {after}", + "guardrails.importSecurityFailure": "障害時の処理: {before} → {after}", + "guardrails.importSecurityDataTypes": "有効なデータ型: {before} → {after}", + "guardrails.importSecurityDisabledRules": "無効な組み込みルール: {before} → {after}(新規無効: {disabled}、再有効化: {reenabled})", + "guardrails.importSecurityCustomRules": "カスタムルール: {before} → {after}(削除: {removed}、変更: {changed})", + "guardrails.importSecurityChangedRuleIds": "定義が変更されたルール:", + "guardrails.importSecurityRemovedRuleIds": "削除されたルール ID:", + "guardrails.importSecurityPrefilter": "キーワード事前フィルター: {before} → {after}", + "guardrails.importSecurityReduced": "保護が縮小", + "guardrails.importSecurityReviewRequired": "セキュリティレビューが必要", + "guardrails.importSecurityRemovedTypes": "保護対象外になるデータ型: {types}", + "guardrails.testerSimulation": "Tester のシミュレーション専用です。合成値を使用し、実際のシークレットを貼り付けないでください。接続モードではサンプルが OpenCodex ハブを通過する場合があります。", + "guardrails.testerTrafficDisabled": "実トラフィックの保護は無効です。現在、リクエストは Guardrails でマスクされずにプロキシを離れます。", + "guardrails.testerTrafficDetect": "実トラフィックは検出のみで、プロバイダーに届く前にマスクされません。", + "guardrails.testerTrafficUnknown": "実トラフィックの保護状態はまだ不明です。Guardrails の状態が読み込まれるまで待ってください。", + "guardrails.testerTrafficUnavailable": "実トラフィックの保護を利用できません。Guardrails の有効化またはレジストリの問題を解決してください。", + "guardrails.testerTrafficNoRules": "実トラフィックに有効な Guardrails ルールがありません。少なくとも 1 つの有効なルールを有効化してください。", + "guardrails.testerTrafficNoProviderCoverage": "シミュレーションでは一致を検出できますが、トラフィック保護対象の有効なプロバイダーが選択されていません。", + "guardrails.testerTrafficReduced": "実トラフィックの保護範囲が縮小しています。無効なルール、データ型、プロバイダー範囲、失敗ポリシーを確認してください。", + "guardrails.dialog.categoryDisable.title": "データカテゴリを無効にしますか?", + "guardrails.dialog.categoryDisable.changes": "このカテゴリのルールは新しいリクエストをスキャンしなくなります。", + "guardrails.dialog.categoryDisable.breakage": "このカテゴリだけが対象とする機密値は、変更されずにプロキシを離れる可能性があります。", + "guardrails.dialog.categoryDisable.undo": "Guardrails 設定でカテゴリを再度有効にしてください。", + "guardrails.dialog.categoryDisable.confirm": "カテゴリを無効化", + "guardrails.dialog.prefilter.title": "キーワード事前フィルターを有効にしますか?", + "guardrails.dialog.prefilter.changes": "候補テキストにキーワードがない場合、キーワード付きルールがスキップされる可能性があります。", + "guardrails.dialog.prefilter.breakage": "この性能上のトレードオフにより検出範囲が狭まる可能性があります。", + "guardrails.dialog.prefilter.undo": "Guardrails 設定でキーワード事前フィルターを無効にしてください。", + "guardrails.dialog.prefilter.confirm": "事前フィルターを有効化", + "guardrails.dialog.importWeakening.title": "保護を弱めるインポートを適用しますか?", + "guardrails.dialog.importWeakening.changes": "インポートしたバンドルは Guardrails 設定とカスタムルールをプレビュー値で置き換えます。", + "guardrails.dialog.importWeakening.breakage": "強調表示された設定により、さらに多くの機密値が変更されずにプロバイダーへ届く可能性があります。", + "guardrails.dialog.importWeakening.undo": "このインポートを適用する前に現在のバンドルをエクスポートしてください。", + "guardrails.dialog.importWeakening.confirm": "縮小された保護を適用", "models.displayNameSavedRefreshFailed": "変更は保存されましたが、モデル一覧を更新できませんでした。再試行してください。", "models.displayNameOutcomeUnknown": "リクエストが完了しませんでした。変更が保存されている可能性があります。再度変更する前に再試行して現在の名前を確認してください。", "models.displayNameCurrentUnavailable": "更新するまで現在の名前を確認できません", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 5ae812e9e8..30d74e08ae 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -2592,6 +2592,278 @@ export const ko: Record = { "integrations.cursor.colReasoning": "추론", "integrations.cursor.colContext": "컨텍스트", "integrations.cursor.guide": "Cursor Private Inference 가이드 열기", + "nav.guardrails": "Guardrails", + "guardrails.title": "민감한 데이터 보호", + "guardrails.subtitle": "요청이 이 프록시를 벗어나기 전에 감지된 민감한 값을 복원 가능한 자리표시자로 바꿉니다.", + "guardrails.loading": "Guardrails 설정을 불러오는 중…", + "guardrails.loadFailed": "Guardrails 설정을 불러오지 못했습니다.", + "guardrails.saveFailed": "Guardrails 설정을 저장하지 못했습니다.", + "guardrails.saved": "Guardrails 설정을 저장했습니다.", + "guardrails.statusActive": "활성", + "guardrails.statusDisabled": "비활성", + "guardrails.settingsTitle": "요청 보호", + "guardrails.settingsHint": "변경 사항은 새 요청에 원자적으로 적용됩니다. 기존 스트림은 현재 매핑을 유지합니다.", + "guardrails.enabled": "Guardrails 활성화", + "guardrails.enabledHint": "허용 목록에 있는 프롬프트 및 도구 필드를 업스트림 제공자에게 도달하기 전에 마스킹합니다.", + "guardrails.mode": "모드", + "guardrails.modeEnforce": "마스킹 강제", + "guardrails.modeDetect": "감지만", + "guardrails.failurePolicy": "처리 실패 시", + "guardrails.failureBlock": "요청 차단", + "guardrails.failurePassthrough": "요청을 그대로 전달", + "guardrails.keywordPrefilter": "키워드 사전 필터", + "guardrails.keywordPrefilterHint": "모든 일치 항목에 키워드가 포함됨이 정규식으로 증명된 기본 규칙만 건너뜁니다. 사용자 지정 및 증명되지 않은 규칙은 항상 검사합니다.", + "guardrails.dataTypes": "활성화된 데이터 유형", + "guardrails.dataType": "데이터 유형", + "guardrails.dataType.credentials": "자격 증명", + "guardrails.groupCredentialUrls": "자격 증명 URL", + "guardrails.dataType.apiKeys": "API 키", + "guardrails.dataType.accessTokens": "액세스 토큰", + "guardrails.dataType.ipAddresses": "IP 주소", + "guardrails.dataType.personal": "개인 데이터", + "guardrails.dataType.custom": "사용자 지정", + "guardrails.rulesTitle": "규칙 관리", + "guardrails.rulesHint": "기본 제공 규칙을 비활성화하거나 로컬 사용자 지정 규칙을 관리합니다. 기본 제공 정규식은 여기에 표시되지 않습니다.", + "guardrails.selectBuiltin": "기본 제공 규칙 선택", + "guardrails.disableBuiltin": "규칙 비활성화", + "guardrails.enableBuiltin": "규칙 활성화", + "guardrails.noDisabledBuiltins": "비활성화된 기본 제공 규칙이 없습니다.", + "guardrails.customRules": "사용자 지정 규칙", + "guardrails.noCustomRules": "아직 사용자 지정 규칙이 없습니다.", + "guardrails.addRule": "규칙 추가", + "guardrails.editRule": "규칙 편집", + "guardrails.deleteRule": "규칙 삭제", + "guardrails.removeRuleConfirm": "사용자 지정 규칙 {ruleId}을(를) 삭제하시겠습니까?", + "guardrails.ruleSaveFailed": "사용자 지정 규칙을 저장하지 못했습니다.", + "guardrails.ruleDeleteFailed": "사용자 지정 규칙을 삭제하지 못했습니다.", + "guardrails.ruleSaved": "사용자 지정 규칙을 저장했습니다.", + "guardrails.ruleDeleted": "사용자 지정 규칙을 삭제했습니다.", + "guardrails.ruleId": "규칙 ID", + "guardrails.ruleName": "규칙 이름", + "guardrails.group": "그룹", + "guardrails.groupPriority": "그룹 우선순위", + "guardrails.displayName": "표시 이름", + "guardrails.description": "설명", + "guardrails.regex": "RE2 패턴", + "guardrails.placeholderType": "자리표시자 유형", + "guardrails.captureGroups": "캡처 그룹", + "guardrails.captureGroupsInvalid": "양의 정수 그룹 번호를 쉼표로 구분해 입력하세요.", + "guardrails.minLength": "최소 길이", + "guardrails.entropy": "엔트로피 임계값", + "guardrails.keywords": "키워드", + "guardrails.banlist": "금지 목록", + "guardrails.validators": "검증기", + "guardrails.saveRule": "규칙 저장", + "guardrails.cancelEdit": "취소", + "guardrails.tabsLabel": "Guardrails 섹션", + "guardrails.tab.overview": "개요", + "guardrails.tab.rules": "규칙", + "guardrails.tab.tester": "테스터", + "guardrails.tab.activity": "활동", + "guardrails.tab.settings": "설정", + "guardrails.detectWarning": "감지 전용 모드는 결과를 보고하지만, 민감한 데이터가 프록시를 벗어나기 전에 마스킹하지는 않습니다.", + "guardrails.passthroughWarning": "Guardrails 처리에 실패했을 때 원본 전달을 허용하면 마스킹되지 않은 입력이 업스트림으로 전송될 수 있습니다.", + "guardrails.metric.scanned": "스캔한 요청", + "guardrails.metric.masked": "마스킹한 값", + "guardrails.metric.detected": "감지한 값", + "guardrails.metric.errors": "경고 및 차단", + "guardrails.lastPassthrough": "최근 원본 전달 사고: {date}", + "guardrails.topRules": "상위 규칙", + "guardrails.topCategories": "상위 데이터 범주", + "guardrails.noActivity": "아직 Guardrails 활동이 없습니다", + "guardrails.noActivityHint": "보호된 요청이 처리되면 여기에 메타데이터가 표시됩니다.", + "guardrails.documentation": "Guardrails 문서 보기", + "guardrails.searchRules": "규칙 검색", + "guardrails.source": "출처", + "guardrails.sourceOpenCodex": "OpenCodex 규칙", + "guardrails.sourceManual": "기본 제공", + "guardrails.allSources": "모든 출처", + "guardrails.status": "상태", + "guardrails.allStatuses": "모든 상태", + "guardrails.allDataTypes": "모든 데이터 유형", + "guardrails.filteredRules": "필터링된 규칙 {count}개", + "guardrails.bulkEnable": "{count}개 활성화", + "guardrails.bulkDisable": "{count}개 비활성화", + "guardrails.export": "내보내기", + "guardrails.exported": "Guardrails 번들을 내보냈습니다.", + "guardrails.exportFailed": "Guardrails 번들을 내보내지 못했습니다.", + "guardrails.import": "가져오기", + "guardrails.importMode": "가져오기 모드", + "guardrails.importMerge": "병합", + "guardrails.importReplace": "바꾸기", + "guardrails.importFailed": "Guardrails 규칙을 가져오지 못했습니다.", + "guardrails.importTooLarge": "Guardrails 가져오기 파일은 4 MiB 이하여야 합니다.", + "guardrails.imported": "Guardrails 규칙을 가져왔습니다.", + "guardrails.noRules": "이 필터와 일치하는 규칙이 없습니다.", + "guardrails.previous": "이전", + "guardrails.next": "다음", + "guardrails.page": "{total}페이지 중 {current}페이지", + "guardrails.testerTitle": "민감한 데이터 테스터", + "guardrails.testerHint": "이 OpenCodex 인스턴스의 유효한 레지스트리로 스캔합니다. 샘플은 Management API로 전송되지만 LLM 공급자에게는 전송되지 않으며 서버에 저장되지 않습니다.", + "guardrails.testerInput": "스캔할 텍스트", + "guardrails.testerPlaceholder": "여기에 예시 텍스트를 붙여 넣으세요…", + "guardrails.byteCounter": "{current} / {max}바이트", + "guardrails.clear": "지우기", + "guardrails.test": "스캔", + "guardrails.testing": "스캔 중…", + "guardrails.testerFailed": "텍스트를 스캔하지 못했습니다.", + "guardrails.testerResult": "스캔 결과", + "guardrails.effective": "유효한 설정", + "guardrails.draft": "초안 설정", + "guardrails.findings": "감지 결과", + "guardrails.noFindings": "활성화된 규칙에서 일치 항목을 찾지 못했습니다. 텍스트에 민감한 데이터가 없음을 보장하지는 않습니다.", + "guardrails.placeholder": "자리표시자", + "guardrails.offsets": "오프셋", + "guardrails.activityTitle": "Guardrails 활동", + "guardrails.activityPrivacy": "메모리 내 메타데이터만 표시되며 감사 로그가 아닙니다. 프롬프트 텍스트와 자리표시자 매핑은 여기에 저장되지 않습니다.", + "guardrails.surface": "처리 영역", + "guardrails.allSurfaces": "모든 처리 영역", + "guardrails.allModes": "모든 모드", + "guardrails.result": "결과", + "guardrails.allResults": "모든 결과", + "guardrails.result.scanned": "스캔됨", + "guardrails.result.masked": "마스킹됨", + "guardrails.result.detected": "감지됨", + "guardrails.result.blocked": "차단됨", + "guardrails.result.passthrough": "그대로 전달됨", + "guardrails.result.demaskWarning": "마스킹 해제 경고", + "guardrails.result.toolSkipped": "도구 복원 건너뜀", + "guardrails.refresh": "새로 고침", + "guardrails.retentionEvents": "보관 중인 이벤트: {current} / {max}개", + "guardrails.retentionEvicted": "제거된 이벤트: {count}개", + "guardrails.lastRefresh": "마지막 새로 고침: {date}", + "guardrails.time": "시간", + "guardrails.count": "개수", + "guardrails.rules": "규칙", + "guardrails.latency": "지연 시간", + "guardrails.dialog.disable.title": "Guardrails를 비활성화하시겠습니까?", + "guardrails.dialog.disable.changes": "새 요청은 더 이상 스캔되거나 마스킹되지 않습니다.", + "guardrails.dialog.disable.breakage": "민감한 값이 변경되지 않은 채 이 프록시를 벗어날 수 있습니다.", + "guardrails.dialog.disable.undo": "개요 또는 설정에서 Guardrails를 다시 활성화할 수 있습니다.", + "guardrails.dialog.disable.confirm": "비활성화", + "guardrails.dialog.detect.title": "감지 전용 모드로 전환하시겠습니까?", + "guardrails.dialog.detect.changes": "감지 결과는 집계되지만 요청 텍스트는 마스킹되지 않습니다.", + "guardrails.dialog.detect.breakage": "감지 전용 모드는 업스트림으로 전송되는 데이터를 보호하지 않습니다.", + "guardrails.dialog.detect.undo": "모드를 다시 ‘마스킹 강제’로 전환하세요.", + "guardrails.dialog.detect.confirm": "감지 전용 모드 사용", + "guardrails.dialog.passthrough.title": "실패 시 원본 전달을 허용하시겠습니까?", + "guardrails.dialog.passthrough.changes": "Guardrails 처리에 실패하면 원본 요청이 전달됩니다.", + "guardrails.dialog.passthrough.breakage": "실패한 요청에 마스킹되지 않은 민감한 값이 포함될 수 있습니다.", + "guardrails.dialog.passthrough.undo": "실패 처리 방식을 다시 ‘요청 차단’으로 설정하세요.", + "guardrails.dialog.passthrough.confirm": "원본 전달 허용", + "guardrails.dialog.delete.title": "사용자 지정 규칙 {ruleId}을(를) 삭제하시겠습니까?", + "guardrails.dialog.delete.changes": "사용자 지정 규칙이 유효한 레지스트리에서 제거됩니다.", + "guardrails.dialog.delete.breakage": "이 규칙으로만 감지되는 값은 더 이상 보호되지 않습니다.", + "guardrails.dialog.delete.undo": "규칙을 가져오거나 다시 만들어 복원할 수 있습니다.", + "guardrails.dialog.delete.confirm": "규칙 삭제", + "guardrails.dialog.import.title": "모든 Guardrails 규칙을 바꾸시겠습니까?", + "guardrails.dialog.import.changes": "가져온 번들이 현재 Guardrails 설정과 사용자 지정 규칙을 바꿉니다.", + "guardrails.dialog.import.breakage": "번들에 없는 로컬 규칙은 제거됩니다.", + "guardrails.dialog.import.undo": "바꾸기 전에 현재 번들을 내보내세요.", + "guardrails.dialog.import.confirm": "규칙 바꾸기", + "guardrails.sourceGitleaks": "Gitleaks 규칙", + "guardrails.surfaceCompact": "요약 압축", + "guardrails.registryReady": "레지스트리 준비됨", + "guardrails.registryFailed": "레지스트리 실패", + "guardrails.registryDisabled": "레지스트리 비활성화됨", + "guardrails.effectiveRules": "유효한 규칙 {count}개", + "guardrails.registryGeneration": "세대 {generation}", + "guardrails.testerDraftSettings": "초안 설정 사용", + "guardrails.testerDraftSettingsHint": "다른 데이터 유형과 키워드 사전 필터 선택을 저장하지 않고 테스트합니다.", + "guardrails.showingEvents": "일치하는 이벤트 {total}개 중 {shown}개 표시", + "guardrails.oldestEvent": "보관 중인 가장 오래된 이벤트: {date}", + "guardrails.tableScrollHint": "모든 열을 보려면 표를 가로로 스크롤하세요.", + "guardrails.importPreviewTitle": "가져오기 미리 보기", + "guardrails.importCreateCount": "생성: {count}", + "guardrails.importReplaceCount": "바꾸기: {count}", + "guardrails.importUnchangedCount": "변경 없음: {count}", + "guardrails.importConflicts": "충돌하는 규칙 ID {count}개를 가져오기 전에 해결해야 합니다.", + "guardrails.importApply": "가져오기 적용", + "guardrails.category": "범주", + "guardrails.allCategories": "모든 범주", + "guardrails.generation": "세대", + "guardrails.activitySummary": "필터링된 활동 요약", + "guardrails.activityEvents": "이벤트: {count}", + "guardrails.activityFindings": "감지 결과: {count}", + "guardrails.activityAverageLatency": "평균 지연 시간: {value} ms", + "guardrails.activityTopRule": "상위 규칙: {rule}", + "guardrails.recentActivity": "최근 활동", + "guardrails.dialog.ruleDisable.title": "규칙 {ruleId}을(를) 비활성화하시겠습니까?", + "guardrails.dialog.ruleDisable.changes": "이 기본 제공 규칙은 새 요청 스캔을 중지합니다.", + "guardrails.dialog.ruleDisable.breakage": "이 규칙으로만 감지되는 값은 더 이상 보호되지 않습니다.", + "guardrails.dialog.ruleDisable.undo": "규칙 탭에서 이 규칙을 다시 활성화할 수 있습니다.", + "guardrails.dialog.ruleDisable.confirm": "규칙 비활성화", + "guardrails.dialog.bulkDisable.title": "규칙 {count}개를 비활성화하시겠습니까?", + "guardrails.dialog.bulkDisable.changes": "이 선택에 포함된 필터링된 기본 제공 규칙이 모두 비활성화됩니다.", + "guardrails.dialog.bulkDisable.breakage": "이 규칙들로만 감지되는 민감한 값이 변경되지 않은 채 프록시를 벗어날 수 있습니다.", + "guardrails.dialog.bulkDisable.undo": "규칙 탭에서 규칙을 다시 활성화하거나 이전에 내보낸 파일을 가져오세요.", + "guardrails.dialog.bulkDisable.confirm": "규칙 비활성화", + "guardrails.statusProtected": "보호 중", + "guardrails.statusDetectOnly": "감지만", + "guardrails.statusReducedCoverage": "보호 범위 감소", + "guardrails.statusUnavailable": "레지스트리 사용 불가", + "guardrails.statusNoRules": "활성 규칙 없음", + "guardrails.statusNoProviderCoverage": "보호되는 제공자 없음", + "guardrails.valueOn": "켜짐", + "guardrails.valueOff": "꺼짐", + "guardrails.reducedCoverageWarning": "보호 범위가 줄었습니다. 데이터 유형 또는 기본 규칙이 비활성화되었거나 공급자 범위가 제한되었거나 처리 실패가 통과될 수 있습니다.", + "guardrails.providerScope": "보호할 제공자", + "guardrails.providerScopeAllValue": "모든 제공자", + "guardrails.providerScopeSelectedValue": "선택됨({count})", + "guardrails.importSecurityProviderScope": "제공자 보호 범위: {before} → {after}", + "guardrails.providerScopeAllHint": "현재 및 향후의 모든 제공자를 보호합니다.", + "guardrails.providerScopeSelectedHint": "선택한 제공자만 보호합니다. 새 제공자는 선택할 때까지 보호되지 않습니다.", + "guardrails.providerAnthropicNative": "Anthropic(네이티브)", + "guardrails.providerDisabled": "비활성화됨", + "guardrails.providerNotConfigured": "구성되지 않음", + "guardrails.lastProviderRequired": "제공자를 하나 이상 선택하거나 Guardrails를 전체 비활성화하세요.", + "guardrails.providerScopeWarning": "제공자 보호 범위가 제한되었습니다. 선택하지 않은 제공자로 가는 요청은 검사되거나 마스킹되지 않습니다.", + "guardrails.noProviderCoverageWarning": "활성 제공자가 선택되지 않았습니다. 현재 제공자 트래픽은 검사되거나 마스킹되지 않습니다.", + "guardrails.dialog.providerScope.title": "제공자 보호 범위를 제한할까요?", + "guardrails.dialog.providerScope.changes": "선택 해제한 제공자로 직접 전송되는 새 요청은 더 이상 검사되거나 마스킹되지 않습니다.", + "guardrails.dialog.providerScope.breakage": "민감한 값이 변경 없이 해당 제공자에 전달될 수 있습니다. 보호된 연속 요청은 이 경계를 넘지 않습니다.", + "guardrails.dialog.providerScope.undo": "Guardrails 설정에서 제공자를 다시 선택하면 보호를 복원할 수 있습니다.", + "guardrails.dialog.providerScope.confirm": "범위 제한", + "guardrails.lastDataTypeRequired": "데이터 유형을 하나 이상 활성화해야 합니다.", + "guardrails.importSecurityTitle": "보안 설정", + "guardrails.importSecurityWeakening": "이 가져오기는 보호를 약화합니다. 계속하기 전에 강조된 변경 사항을 모두 검토하세요.", + "guardrails.importSecurityPotential": "이 가져오기는 보안 관련 사용자 규칙 정의를 변경하며 보호를 약화할 수 있습니다. 계속하기 전에 검토하세요.", + "guardrails.importSecurityUnchanged": "Guardrails 보안 설정은 변경되지 않습니다.", + "guardrails.importSecurityEnabled": "트래픽 보호: {before} → {after}", + "guardrails.importSecurityMode": "모드: {before} → {after}", + "guardrails.importSecurityFailure": "실패 처리: {before} → {after}", + "guardrails.importSecurityDataTypes": "활성 데이터 유형: {before} → {after}", + "guardrails.importSecurityDisabledRules": "비활성 기본 제공 규칙: {before} → {after} (새로 비활성: {disabled}, 다시 활성: {reenabled})", + "guardrails.importSecurityCustomRules": "사용자 규칙: {before} → {after} (삭제: {removed}, 변경: {changed})", + "guardrails.importSecurityChangedRuleIds": "정의가 변경된 규칙:", + "guardrails.importSecurityRemovedRuleIds": "삭제된 규칙 ID:", + "guardrails.importSecurityPrefilter": "키워드 사전 필터: {before} → {after}", + "guardrails.importSecurityReduced": "보호 감소", + "guardrails.importSecurityReviewRequired": "보안 검토 필요", + "guardrails.importSecurityRemovedTypes": "더 이상 보호하지 않음: {types}", + "guardrails.testerSimulation": "Tester 시뮬레이션 전용입니다. 합성 값을 사용하고 실제 비밀을 붙여 넣지 마세요. 연결 모드에서는 샘플이 OpenCodex 허브를 통과할 수 있습니다.", + "guardrails.testerTrafficDisabled": "실제 트래픽 보호가 꺼져 있습니다. 현재 요청은 Guardrails 마스킹 없이 프록시를 벗어납니다.", + "guardrails.testerTrafficDetect": "실제 트래픽은 감지만 수행하며 공급자에 도달하기 전에 마스킹되지 않습니다.", + "guardrails.testerTrafficUnknown": "실제 트래픽 보호 상태를 아직 알 수 없습니다. Guardrails 상태가 로드될 때까지 기다리세요.", + "guardrails.testerTrafficUnavailable": "실제 트래픽 보호를 사용할 수 없습니다. Guardrails 활성화 또는 레지스트리 오류를 해결하세요.", + "guardrails.testerTrafficNoRules": "실제 트래픽에 활성 Guardrails 규칙이 없습니다. 하나 이상의 유효한 규칙을 활성화하세요.", + "guardrails.testerTrafficNoProviderCoverage": "시뮬레이션은 일치 항목을 찾을 수 있지만 트래픽 보호 대상으로 선택된 활성 제공자가 없습니다.", + "guardrails.testerTrafficReduced": "실제 트래픽 보호 범위가 축소되었습니다. 비활성화된 규칙, 데이터 유형, 공급자 범위 및 실패 정책을 확인하세요.", + "guardrails.dialog.categoryDisable.title": "데이터 범주를 비활성화하시겠습니까?", + "guardrails.dialog.categoryDisable.changes": "이 범주의 규칙은 새 요청을 더 이상 검사하지 않습니다.", + "guardrails.dialog.categoryDisable.breakage": "이 범주만 감지하는 민감한 값은 변경되지 않은 채 프록시를 벗어날 수 있습니다.", + "guardrails.dialog.categoryDisable.undo": "Guardrails 설정에서 범주를 다시 활성화하세요.", + "guardrails.dialog.categoryDisable.confirm": "범주 비활성화", + "guardrails.dialog.prefilter.title": "키워드 사전 필터를 활성화하시겠습니까?", + "guardrails.dialog.prefilter.changes": "후보 텍스트에 키워드가 없으면 키워드가 있는 규칙을 건너뛸 수 있습니다.", + "guardrails.dialog.prefilter.breakage": "이 성능 절충은 감지 범위를 줄일 수 있습니다.", + "guardrails.dialog.prefilter.undo": "Guardrails 설정에서 키워드 사전 필터를 끄세요.", + "guardrails.dialog.prefilter.confirm": "사전 필터 활성화", + "guardrails.dialog.importWeakening.title": "보호를 약화하는 가져오기를 적용하시겠습니까?", + "guardrails.dialog.importWeakening.changes": "가져온 번들은 Guardrails 설정과 사용자 지정 규칙을 미리 본 값으로 바꿉니다.", + "guardrails.dialog.importWeakening.breakage": "강조된 설정으로 인해 더 많은 민감한 값이 변경되지 않은 채 공급자에 도달할 수 있습니다.", + "guardrails.dialog.importWeakening.undo": "이 가져오기를 적용하기 전에 현재 번들을 내보내세요.", + "guardrails.dialog.importWeakening.confirm": "감소된 보호 적용", "models.displayNameSavedRefreshFailed": "변경 사항은 저장되었지만 모델 목록을 새로 고치지 못했습니다. 다시 시도해 주세요.", "models.displayNameOutcomeUnknown": "요청이 완료되지 않았습니다. 변경 사항이 저장되었을 수 있습니다. 다시 변경하기 전에 재시도하여 현재 이름을 확인하세요.", "models.displayNameCurrentUnavailable": "새로 고침 전까지 현재 이름을 확인할 수 없음", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 2900abb6f3..3088270a3c 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -2593,6 +2593,278 @@ export const ru: Record = { "integrations.cursor.colReasoning": "Рассуждения", "integrations.cursor.colContext": "Контекст", "integrations.cursor.guide": "Открыть руководство по Cursor Private Inference", + "nav.guardrails": "Защита данных", + "guardrails.title": "Защита конфиденциальных данных", + "guardrails.subtitle": "Заменяет обнаруженные чувствительные значения обратимыми плейсхолдерами до отправки запроса из этого прокси.", + "guardrails.loading": "Загрузка настроек защиты…", + "guardrails.loadFailed": "Не удалось загрузить настройки защиты.", + "guardrails.saveFailed": "Не удалось сохранить настройки защиты.", + "guardrails.saved": "Настройки защиты сохранены.", + "guardrails.statusActive": "Включена", + "guardrails.statusProtected": "Защищает", + "guardrails.statusDetectOnly": "Только обнаружение", + "guardrails.statusReducedCoverage": "Сниженное покрытие", + "guardrails.statusUnavailable": "Реестр недоступен", + "guardrails.statusNoRules": "Нет активных правил", + "guardrails.statusNoProviderCoverage": "Нет защищённых провайдеров", + "guardrails.statusDisabled": "Выключена", + "guardrails.valueOn": "Вкл.", + "guardrails.valueOff": "Выкл.", + "guardrails.settingsTitle": "Защита запросов", + "guardrails.settingsHint": "Изменения атомарно применяются к новым запросам. Текущие потоки сохраняют своё сопоставление.", + "guardrails.enabled": "Включить защиту", + "guardrails.enabledHint": "Маскировать разрешённые поля запросов и инструментов до передачи провайдеру.", + "guardrails.mode": "Режим", + "guardrails.modeEnforce": "Маскировать", + "guardrails.modeDetect": "Только обнаружение", + "guardrails.failurePolicy": "Ошибка обработки", + "guardrails.failureBlock": "Заблокировать запрос", + "guardrails.failurePassthrough": "Пропустить запрос", + "guardrails.keywordPrefilter": "Предфильтр по ключевым словам", + "guardrails.keywordPrefilterHint": "Пропускать только встроенные правила, для которых regex доказывает наличие ключевого слова в каждом совпадении. Пользовательские и недоказанные правила сканируются всегда.", + "guardrails.reducedCoverageWarning": "Покрытие снижено: часть типов данных или встроенных правил выключена, охват провайдеров ограничен либо ошибки обработки могут пропускать данные без маскирования.", + "guardrails.providerScope": "Защищаемые провайдеры", + "guardrails.providerScopeAllValue": "Все провайдеры", + "guardrails.providerScopeSelectedValue": "Выбрано: {count}", + "guardrails.importSecurityProviderScope": "Покрытие провайдеров: {before} → {after}", + "guardrails.providerScopeAllHint": "Защищены все текущие и будущие провайдеры.", + "guardrails.providerScopeSelectedHint": "Защищены только отмеченные провайдеры. Новые провайдеры останутся без защиты, пока вы их не выберете.", + "guardrails.providerAnthropicNative": "Anthropic (нативный)", + "guardrails.providerDisabled": "отключён", + "guardrails.providerNotConfigured": "не настроен", + "guardrails.lastProviderRequired": "Оставьте хотя бы одного провайдера или выключите Guardrails целиком.", + "guardrails.providerScopeWarning": "Покрытие провайдеров ограничено. Запросы к неотмеченным провайдерам не сканируются и не маскируются.", + "guardrails.noProviderCoverageWarning": "Не выбран ни один активный провайдер. Текущий трафик провайдеров не сканируется и не маскируется.", + "guardrails.dialog.providerScope.title": "Ограничить покрытие провайдеров?", + "guardrails.dialog.providerScope.changes": "Новые запросы напрямую к снятому провайдеру больше не будут сканироваться и маскироваться.", + "guardrails.dialog.providerScope.breakage": "Чувствительные значения могут уйти этому провайдеру без изменений. Защищённые продолжения не пересекают эту границу.", + "guardrails.dialog.providerScope.undo": "Снова выберите провайдера в настройках Guardrails, чтобы вернуть защиту.", + "guardrails.dialog.providerScope.confirm": "Ограничить покрытие", + "guardrails.lastDataTypeRequired": "Хотя бы один тип данных должен оставаться включённым.", + "guardrails.dataTypes": "Включённые типы данных", + "guardrails.dataType": "Тип данных", + "guardrails.dataType.credentials": "Учётные данные", + "guardrails.groupCredentialUrls": "URL с учётными данными", + "guardrails.dataType.apiKeys": "API-ключи", + "guardrails.dataType.accessTokens": "Токены доступа", + "guardrails.dataType.ipAddresses": "IP-адреса", + "guardrails.dataType.personal": "Персональные данные", + "guardrails.dataType.custom": "Пользовательские", + "guardrails.rulesTitle": "Управление правилами", + "guardrails.rulesHint": "Отключайте встроенные правила и управляйте локальными правилами. Встроенные регулярные выражения здесь не раскрываются.", + "guardrails.selectBuiltin": "Выберите встроенное правило", + "guardrails.disableBuiltin": "Отключить правило", + "guardrails.enableBuiltin": "Включить правило", + "guardrails.noDisabledBuiltins": "Нет отключённых встроенных правил.", + "guardrails.customRules": "Пользовательские правила", + "guardrails.noCustomRules": "Пользовательских правил пока нет.", + "guardrails.addRule": "Добавить правило", + "guardrails.editRule": "Редактировать правило", + "guardrails.deleteRule": "Удалить правило", + "guardrails.removeRuleConfirm": "Удалить пользовательское правило {ruleId}?", + "guardrails.ruleSaveFailed": "Не удалось сохранить пользовательское правило.", + "guardrails.ruleDeleteFailed": "Не удалось удалить пользовательское правило.", + "guardrails.ruleSaved": "Пользовательское правило сохранено.", + "guardrails.ruleDeleted": "Пользовательское правило удалено.", + "guardrails.ruleId": "Идентификатор правила", + "guardrails.ruleName": "Название правила", + "guardrails.group": "Группа", + "guardrails.groupPriority": "Приоритет группы", + "guardrails.displayName": "Отображаемое имя", + "guardrails.description": "Описание", + "guardrails.regex": "Шаблон RE2", + "guardrails.placeholderType": "Тип плейсхолдера", + "guardrails.captureGroups": "Захватывающие группы", + "guardrails.captureGroupsInvalid": "Укажите номера групп положительными целыми числами через запятую.", + "guardrails.minLength": "Минимальная длина", + "guardrails.entropy": "Порог энтропии", + "guardrails.keywords": "Ключевые слова", + "guardrails.banlist": "Стоп-лист", + "guardrails.validators": "Валидаторы", + "guardrails.saveRule": "Сохранить правило", + "guardrails.cancelEdit": "Отмена", + "guardrails.tabsLabel": "Разделы защиты данных", + "guardrails.tab.overview": "Обзор", + "guardrails.tab.rules": "Правила", + "guardrails.tab.tester": "Проверка", + "guardrails.tab.activity": "Активность", + "guardrails.tab.settings": "Настройки", + "guardrails.detectWarning": "Режим обнаружения показывает находки, но не маскирует чувствительные данные перед отправкой из прокси.", + "guardrails.passthroughWarning": "При ошибке обработки passthrough может отправить провайдеру незамаскированный запрос.", + "guardrails.metric.scanned": "Проверено запросов", + "guardrails.metric.masked": "Замаскировано значений", + "guardrails.metric.detected": "Обнаружено значений", + "guardrails.metric.errors": "Предупреждения и блокировки", + "guardrails.registryReady": "Реестр готов", + "guardrails.registryFailed": "Ошибка реестра", + "guardrails.registryDisabled": "Реестр выключен", + "guardrails.effectiveRules": "Действующих правил: {count}", + "guardrails.registryGeneration": "Поколение {generation}", + "guardrails.lastPassthrough": "Последний случай passthrough: {date}", + "guardrails.topRules": "Частые правила", + "guardrails.topCategories": "Частые категории данных", + "guardrails.noActivity": "Активности защиты пока нет", + "guardrails.noActivityHint": "Метаданные появятся после обработки защищённых запросов.", + "guardrails.documentation": "Открыть документацию Guardrails", + "guardrails.searchRules": "Поиск правил", + "guardrails.source": "Источник", + "guardrails.sourceOpenCodex": "Правила OpenCodex", + "guardrails.sourceManual": "Встроенное", + "guardrails.sourceGitleaks": "Правила Gitleaks", + "guardrails.allSources": "Все источники", + "guardrails.status": "Статус", + "guardrails.allStatuses": "Все статусы", + "guardrails.allDataTypes": "Все типы данных", + "guardrails.filteredRules": "Правил по фильтру: {count}", + "guardrails.bulkEnable": "Включить {count}", + "guardrails.bulkDisable": "Отключить {count}", + "guardrails.export": "Экспорт", + "guardrails.exported": "Набор Guardrails экспортирован.", + "guardrails.exportFailed": "Не удалось экспортировать набор Guardrails.", + "guardrails.import": "Импорт", + "guardrails.importMode": "Режим импорта", + "guardrails.importMerge": "Объединить", + "guardrails.importReplace": "Заменить", + "guardrails.importFailed": "Не удалось импортировать правила Guardrails.", + "guardrails.importTooLarge": "Размер файла импорта Guardrails не должен превышать 4 МиБ.", + "guardrails.imported": "Правила Guardrails импортированы.", + "guardrails.importPreviewTitle": "Предпросмотр импорта", + "guardrails.importSecurityTitle": "Настройки защиты", + "guardrails.importSecurityWeakening": "Этот импорт снижает защиту. Проверьте каждое выделенное изменение перед продолжением.", + "guardrails.importSecurityPotential": "Этот импорт меняет значимые для безопасности определения пользовательских правил и может снизить защиту. Проверьте их перед продолжением.", + "guardrails.importSecurityUnchanged": "Настройки защиты Guardrails не изменятся.", + "guardrails.importSecurityEnabled": "Защита трафика: {before} → {after}", + "guardrails.importSecurityMode": "Режим: {before} → {after}", + "guardrails.importSecurityFailure": "Обработка ошибок: {before} → {after}", + "guardrails.importSecurityDataTypes": "Включённые типы данных: {before} → {after}", + "guardrails.importSecurityDisabledRules": "Выключенные встроенные правила: {before} → {after} (новых отключений: {disabled}, включено обратно: {reenabled})", + "guardrails.importSecurityCustomRules": "Пользовательские правила: {before} → {after} (удалено: {removed}, изменено: {changed})", + "guardrails.importSecurityChangedRuleIds": "Изменённые определения правил:", + "guardrails.importSecurityRemovedRuleIds": "Удалённые ID правил:", + "guardrails.importSecurityPrefilter": "Предфильтр по ключевым словам: {before} → {after}", + "guardrails.importSecurityReduced": "Защита снижена", + "guardrails.importSecurityReviewRequired": "Требуется проверка безопасности", + "guardrails.importSecurityRemovedTypes": "Больше не покрываются: {types}", + "guardrails.importCreateCount": "Создать: {count}", + "guardrails.importReplaceCount": "Заменить: {count}", + "guardrails.importUnchangedCount": "Без изменений: {count}", + "guardrails.importConflicts": "Перед импортом нужно устранить конфликты ID правил: {count}.", + "guardrails.importApply": "Применить импорт", + "guardrails.noRules": "Нет правил, соответствующих фильтрам.", + "guardrails.previous": "Назад", + "guardrails.next": "Далее", + "guardrails.page": "Страница {current} из {total}", + "guardrails.testerTitle": "Проверка чувствительных данных", + "guardrails.testerHint": "Проверка действующим реестром этого экземпляра OpenCodex. Образец отправляется в его Management API, но не LLM-провайдеру, и не сохраняется на сервере.", + "guardrails.testerSimulation": "Только симуляция Tester. Используйте синтетические значения; никогда не вставляйте реальные секреты. В подключённом режиме образец может пройти через ваш OpenCodex hub.", + "guardrails.testerTrafficDisabled": "Защита реального трафика выключена. Сейчас запросы покидают прокси без маскирования Guardrails.", + "guardrails.testerTrafficDetect": "Реальный трафик работает только в режиме обнаружения и не маскируется перед отправкой провайдеру.", + "guardrails.testerTrafficUnknown": "Состояние защиты реального трафика пока неизвестно. Дождитесь загрузки Guardrails, прежде чем полагаться на защиту.", + "guardrails.testerTrafficUnavailable": "Защита реального трафика недоступна. Устраните ошибку активации или реестра Guardrails, прежде чем полагаться на маскирование.", + "guardrails.testerTrafficNoRules": "Для реального трафика нет активных правил Guardrails. Включите хотя бы одно эффективное правило, прежде чем полагаться на маскирование.", + "guardrails.testerTrafficNoProviderCoverage": "Симуляция может найти совпадения, но для защиты трафика не выбран ни один активный провайдер.", + "guardrails.testerTrafficReduced": "Реальный трафик защищён частично. Проверьте отключённые правила, типы данных, охват провайдеров и политику ошибок.", + "guardrails.testerDraftSettings": "Использовать черновые настройки", + "guardrails.testerDraftSettingsHint": "Проверьте другой набор типов данных и keyword prefilter без сохранения.", + "guardrails.testerInput": "Текст для проверки", + "guardrails.testerPlaceholder": "Вставьте пример текста…", + "guardrails.byteCounter": "{current} из {max} байт", + "guardrails.clear": "Очистить", + "guardrails.test": "Проверить", + "guardrails.testing": "Проверка…", + "guardrails.testerFailed": "Не удалось проверить текст.", + "guardrails.testerResult": "Результат проверки", + "guardrails.effective": "Действующие настройки", + "guardrails.draft": "Черновые настройки", + "guardrails.findings": "Находки", + "guardrails.noFindings": "Включённые правила не нашли совпадений; это не гарантирует отсутствие чувствительных данных в тексте.", + "guardrails.placeholder": "Плейсхолдер", + "guardrails.offsets": "Позиции", + "guardrails.activityTitle": "Активность Guardrails", + "guardrails.activityPrivacy": "Только метаданные в памяти; это не журнал аудита. Тексты запросов и таблицы плейсхолдеров здесь не сохраняются.", + "guardrails.category": "Категория", + "guardrails.allCategories": "Все категории", + "guardrails.generation": "Поколение", + "guardrails.activitySummary": "Сводка по фильтру", + "guardrails.activityEvents": "События: {count}", + "guardrails.activityFindings": "Находки: {count}", + "guardrails.activityAverageLatency": "Средняя задержка: {value} мс", + "guardrails.activityTopRule": "Главное правило: {rule}", + "guardrails.recentActivity": "Последняя активность", + "guardrails.surface": "Протокол", + "guardrails.surfaceCompact": "Компактизация", + "guardrails.allSurfaces": "Все протоколы", + "guardrails.allModes": "Все режимы", + "guardrails.result": "Результат", + "guardrails.allResults": "Все результаты", + "guardrails.result.scanned": "Проверено", + "guardrails.result.masked": "Замаскировано", + "guardrails.result.detected": "Обнаружено", + "guardrails.result.blocked": "Заблокировано", + "guardrails.result.passthrough": "Пропущено без защиты", + "guardrails.result.demaskWarning": "Предупреждение восстановления", + "guardrails.result.toolSkipped": "Восстановление инструмента пропущено", + "guardrails.refresh": "Обновить", + "guardrails.retentionEvents": "Сохранено событий: {current} из {max}", + "guardrails.retentionEvicted": "Вытеснено событий: {count}", + "guardrails.showingEvents": "Показано {shown} из {total} подходящих событий", + "guardrails.oldestEvent": "Самое старое сохранённое событие: {date}", + "guardrails.lastRefresh": "Последнее обновление: {date}", + "guardrails.tableScrollHint": "Прокрутите таблицу по горизонтали, чтобы увидеть все столбцы.", + "guardrails.time": "Время", + "guardrails.count": "Количество", + "guardrails.rules": "Правила", + "guardrails.latency": "Задержка", + "guardrails.dialog.disable.title": "Выключить защиту данных?", + "guardrails.dialog.disable.changes": "Новые запросы больше не будут проверяться и маскироваться.", + "guardrails.dialog.disable.breakage": "Чувствительные значения могут выйти из прокси без изменений.", + "guardrails.dialog.disable.undo": "Снова включите защиту на вкладке «Обзор» или «Настройки».", + "guardrails.dialog.disable.confirm": "Выключить", + "guardrails.dialog.detect.title": "Перейти в режим только обнаружения?", + "guardrails.dialog.detect.changes": "Находки будут учитываться, но текст запроса не будет маскироваться.", + "guardrails.dialog.detect.breakage": "Режим обнаружения не защищает данные, отправляемые провайдеру.", + "guardrails.dialog.detect.undo": "Верните режим «Маскировать».", + "guardrails.dialog.detect.confirm": "Включить обнаружение", + "guardrails.dialog.passthrough.title": "Разрешить passthrough при ошибке?", + "guardrails.dialog.passthrough.changes": "При ошибке Guardrails исходный запрос будет отправлен провайдеру.", + "guardrails.dialog.passthrough.breakage": "Такой запрос может содержать незамаскированные чувствительные значения.", + "guardrails.dialog.passthrough.undo": "Верните обработку ошибок в режим «Заблокировать запрос».", + "guardrails.dialog.passthrough.confirm": "Разрешить passthrough", + "guardrails.dialog.ruleDisable.title": "Отключить правило {ruleId}?", + "guardrails.dialog.ruleDisable.changes": "Это встроенное правило перестанет проверять новые запросы.", + "guardrails.dialog.ruleDisable.breakage": "Значения, которые находило только это правило, больше не будут защищены.", + "guardrails.dialog.ruleDisable.undo": "Снова включите правило на вкладке «Правила».", + "guardrails.dialog.ruleDisable.confirm": "Отключить правило", + "guardrails.dialog.categoryDisable.title": "Выключить категорию данных?", + "guardrails.dialog.categoryDisable.changes": "Правила этой категории перестанут проверять новые запросы.", + "guardrails.dialog.categoryDisable.breakage": "Чувствительные значения, покрываемые только этой категорией, могут покинуть прокси без изменений.", + "guardrails.dialog.categoryDisable.undo": "Снова включите категорию в настройках Guardrails.", + "guardrails.dialog.categoryDisable.confirm": "Выключить категорию", + "guardrails.dialog.prefilter.title": "Включить предфильтр по ключевым словам?", + "guardrails.dialog.prefilter.changes": "Правила с ключевыми словами могут быть пропущены, если этих слов нет в проверяемом тексте.", + "guardrails.dialog.prefilter.breakage": "Этот компромисс ради производительности может снизить полноту обнаружения.", + "guardrails.dialog.prefilter.undo": "Выключите предфильтр в настройках Guardrails.", + "guardrails.dialog.prefilter.confirm": "Включить предфильтр", + "guardrails.dialog.bulkDisable.title": "Отключить правил: {count}?", + "guardrails.dialog.bulkDisable.changes": "Все отфильтрованные встроенные правила из этой выборки будут отключены.", + "guardrails.dialog.bulkDisable.breakage": "Чувствительные значения, покрываемые только этими правилами, могут выйти из прокси без изменений.", + "guardrails.dialog.bulkDisable.undo": "Снова включите правила на вкладке «Правила» или импортируйте предыдущий экспорт.", + "guardrails.dialog.bulkDisable.confirm": "Отключить правила", + "guardrails.dialog.delete.title": "Удалить правило {ruleId}?", + "guardrails.dialog.delete.changes": "Пользовательское правило будет удалено из действующего реестра.", + "guardrails.dialog.delete.breakage": "Значения, которые находило только это правило, больше не будут защищены.", + "guardrails.dialog.delete.undo": "Импортируйте или создайте правило заново.", + "guardrails.dialog.delete.confirm": "Удалить правило", + "guardrails.dialog.import.title": "Заменить все правила Guardrails?", + "guardrails.dialog.import.changes": "Импортированный набор заменит текущие настройки и пользовательские правила Guardrails.", + "guardrails.dialog.import.breakage": "Локальные правила, отсутствующие в наборе, будут удалены.", + "guardrails.dialog.import.undo": "Перед заменой экспортируйте текущий набор.", + "guardrails.dialog.import.confirm": "Заменить правила", + "guardrails.dialog.importWeakening.title": "Применить импорт, снижающий защиту?", + "guardrails.dialog.importWeakening.changes": "Импортированный набор заменит настройки и пользовательские правила Guardrails значениями из предпросмотра.", + "guardrails.dialog.importWeakening.breakage": "Одно или несколько выделенных изменений позволят дополнительным чувствительным данным уходить провайдерам без изменений.", + "guardrails.dialog.importWeakening.undo": "Экспортируйте текущий набор перед применением этого импорта.", + "guardrails.dialog.importWeakening.confirm": "Применить сниженную защиту", "models.displayNameSavedRefreshFailed": "Изменение сохранено, но список моделей не удалось обновить. Повторите попытку.", "models.displayNameOutcomeUnknown": "Запрос не завершён. Изменение могло сохраниться. Повторите попытку, чтобы проверить текущее имя перед следующим изменением.", "models.displayNameCurrentUnavailable": "Текущее имя недоступно до обновления", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index f551a97d43..1c4ada45e9 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -2593,6 +2593,278 @@ export const tr: Record = { "integrations.cursor.colReasoning": "Akıl yürütme", "integrations.cursor.colContext": "Bağlam", "integrations.cursor.guide": "Cursor Private Inference kılavuzunu aç", + "nav.guardrails": "Guardrails", + "guardrails.title": "Hassas veri koruması", + "guardrails.subtitle": "Algılanan hassas değerler, istekler bu proxy’den ayrılmadan önce geri döndürülebilir yer tutucularla değiştirilir.", + "guardrails.loading": "Guardrails ayarları yükleniyor…", + "guardrails.loadFailed": "Guardrails ayarları yüklenemedi.", + "guardrails.saveFailed": "Guardrails ayarları kaydedilemedi.", + "guardrails.saved": "Guardrails ayarları kaydedildi.", + "guardrails.statusActive": "Etkin", + "guardrails.statusDisabled": "Devre dışı", + "guardrails.settingsTitle": "İstek koruması", + "guardrails.settingsHint": "Değişiklikler yeni isteklere atomik olarak uygulanır. Mevcut akışlar geçerli eşlemelerini korur.", + "guardrails.enabled": "Guardrails’i etkinleştir", + "guardrails.enabledHint": "İzin listesindeki istem ve araç alanlarını yukarı akış sağlayıcısına ulaşmadan önce maskeleyin.", + "guardrails.mode": "Mod", + "guardrails.modeEnforce": "Maskelemeyi zorunlu kıl", + "guardrails.modeDetect": "Yalnızca algıla", + "guardrails.failurePolicy": "İşleme hatası", + "guardrails.failureBlock": "İsteği engelle", + "guardrails.failurePassthrough": "İsteği olduğu gibi ilet", + "guardrails.keywordPrefilter": "Anahtar sözcük ön filtresi", + "guardrails.keywordPrefilterHint": "Yalnızca her eşleşmenin bir anahtar sözcük içerdiği regex ile kanıtlanan yerleşik kuralları atlayın. Özel ve kanıtlanmamış kurallar her zaman taranır.", + "guardrails.dataTypes": "Etkin veri türleri", + "guardrails.dataType": "Veri türü", + "guardrails.dataType.credentials": "Kimlik bilgileri", + "guardrails.groupCredentialUrls": "Kimlik bilgisi içeren URL'ler", + "guardrails.dataType.apiKeys": "API anahtarları", + "guardrails.dataType.accessTokens": "Erişim belirteçleri", + "guardrails.dataType.ipAddresses": "IP adresleri", + "guardrails.dataType.personal": "Kişisel veriler", + "guardrails.dataType.custom": "Özel", + "guardrails.rulesTitle": "Kural yönetimi", + "guardrails.rulesHint": "Yerleşik bir kuralı devre dışı bırakın veya yerel özel kuralları yönetin. Yerleşik düzenli ifadeler burada hiçbir zaman gösterilmez.", + "guardrails.selectBuiltin": "Yerleşik bir kural seçin", + "guardrails.disableBuiltin": "Kuralı devre dışı bırak", + "guardrails.enableBuiltin": "Kuralı etkinleştir", + "guardrails.noDisabledBuiltins": "Devre dışı bırakılmış yerleşik kural yok.", + "guardrails.customRules": "Özel kurallar", + "guardrails.noCustomRules": "Henüz özel kural yok.", + "guardrails.addRule": "Kural ekle", + "guardrails.editRule": "Kuralı düzenle", + "guardrails.deleteRule": "Kuralı sil", + "guardrails.removeRuleConfirm": "{ruleId} özel kuralı silinsin mi?", + "guardrails.ruleSaveFailed": "Özel kural kaydedilemedi.", + "guardrails.ruleDeleteFailed": "Özel kural silinemedi.", + "guardrails.ruleSaved": "Özel kural kaydedildi.", + "guardrails.ruleDeleted": "Özel kural silindi.", + "guardrails.ruleId": "Kural ID’si", + "guardrails.ruleName": "Kural adı", + "guardrails.group": "Grup", + "guardrails.groupPriority": "Grup önceliği", + "guardrails.displayName": "Görünen ad", + "guardrails.description": "Açıklama", + "guardrails.regex": "RE2 deseni", + "guardrails.placeholderType": "Yer tutucu türü", + "guardrails.captureGroups": "Yakalama grupları", + "guardrails.captureGroupsInvalid": "Pozitif tam sayı grup numaralarını virgülle ayırarak girin.", + "guardrails.minLength": "En az uzunluk", + "guardrails.entropy": "Entropi eşiği", + "guardrails.keywords": "Anahtar sözcükler", + "guardrails.banlist": "Yasaklılar listesi", + "guardrails.validators": "Doğrulayıcılar", + "guardrails.saveRule": "Kuralı kaydet", + "guardrails.cancelEdit": "İptal", + "guardrails.tabsLabel": "Guardrails bölümleri", + "guardrails.tab.overview": "Genel bakış", + "guardrails.tab.rules": "Kurallar", + "guardrails.tab.tester": "Test aracı", + "guardrails.tab.activity": "Etkinlik", + "guardrails.tab.settings": "Ayarlar", + "guardrails.detectWarning": "Yalnızca algılama modu bulguları bildirir ancak hassas verileri proxy’den ayrılmadan önce maskelemez.", + "guardrails.passthroughWarning": "Olduğu gibi iletim, Guardrails işlemesi başarısız olduğunda maskelenmemiş girdiyi yukarı akışa gönderebilir.", + "guardrails.metric.scanned": "Taranan istekler", + "guardrails.metric.masked": "Maskelenen değerler", + "guardrails.metric.detected": "Algılanan değerler", + "guardrails.metric.errors": "Uyarılar ve engellemeler", + "guardrails.lastPassthrough": "Son olduğu gibi iletim olayı: {date}", + "guardrails.topRules": "En sık kurallar", + "guardrails.topCategories": "En sık veri kategorileri", + "guardrails.noActivity": "Henüz Guardrails etkinliği yok", + "guardrails.noActivityHint": "Korunan istekler işlendikten sonra meta veriler burada görünür.", + "guardrails.documentation": "Guardrails belgelerini okuyun", + "guardrails.searchRules": "Kurallarda ara", + "guardrails.source": "Kaynak", + "guardrails.sourceOpenCodex": "OpenCodex kuralları", + "guardrails.sourceManual": "Yerleşik", + "guardrails.allSources": "Tüm kaynaklar", + "guardrails.status": "Durum", + "guardrails.allStatuses": "Tüm durumlar", + "guardrails.allDataTypes": "Tüm veri türleri", + "guardrails.filteredRules": "Filtrelenen kural sayısı: {count}", + "guardrails.bulkEnable": "{count} kuralı etkinleştir", + "guardrails.bulkDisable": "{count} kuralı devre dışı bırak", + "guardrails.export": "Dışa aktar", + "guardrails.exported": "Guardrails paketi dışa aktarıldı.", + "guardrails.exportFailed": "Guardrails paketi dışa aktarılamadı.", + "guardrails.import": "İçe aktar", + "guardrails.importMode": "İçe aktarma modu", + "guardrails.importMerge": "Birleştir", + "guardrails.importReplace": "Değiştir", + "guardrails.importFailed": "Guardrails kuralları içe aktarılamadı.", + "guardrails.importTooLarge": "Guardrails içe aktarma dosyaları en fazla 4 MiB olabilir.", + "guardrails.imported": "Guardrails kuralları içe aktarıldı.", + "guardrails.noRules": "Bu filtrelerle eşleşen kural yok.", + "guardrails.previous": "Önceki", + "guardrails.next": "Sonraki", + "guardrails.page": "{total} sayfanın {current}. sayfası", + "guardrails.testerTitle": "Hassas veri test aracı", + "guardrails.testerHint": "Bu OpenCodex örneğinin etkin kural kayıt sistemiyle tarayın. Örnek Management API'ye gönderilir, hiçbir LLM sağlayıcısına gönderilmez ve sunucuda saklanmaz.", + "guardrails.testerInput": "Taranacak metin", + "guardrails.testerPlaceholder": "Örnek metni buraya yapıştırın…", + "guardrails.byteCounter": "{current} / {max} bayt", + "guardrails.clear": "Temizle", + "guardrails.test": "Tara", + "guardrails.testing": "Taranıyor…", + "guardrails.testerFailed": "Metin taranamadı.", + "guardrails.testerResult": "Tarama sonucu", + "guardrails.effective": "Etkin ayarlar", + "guardrails.draft": "Taslak ayarlar", + "guardrails.findings": "Bulgular", + "guardrails.noFindings": "Hassas değer algılanmadı.", + "guardrails.placeholder": "Yer tutucu", + "guardrails.offsets": "Konumlar", + "guardrails.activityTitle": "Guardrails etkinliği", + "guardrails.activityPrivacy": "Yalnızca bellekteki meta veriler gösterilir; bu bir denetim günlüğü değildir. İstem metni ve yer tutucu eşlemeleri burada hiçbir zaman saklanmaz.", + "guardrails.surface": "Arayüz", + "guardrails.allSurfaces": "Tüm arayüzler", + "guardrails.allModes": "Tüm modlar", + "guardrails.result": "Sonuç", + "guardrails.allResults": "Tüm sonuçlar", + "guardrails.result.scanned": "Tarandı", + "guardrails.result.masked": "Maskelendi", + "guardrails.result.detected": "Algılandı", + "guardrails.result.blocked": "Engellendi", + "guardrails.result.passthrough": "Olduğu gibi iletildi", + "guardrails.result.demaskWarning": "Maske kaldırma uyarısı", + "guardrails.result.toolSkipped": "Araç geri yükleme atlandı", + "guardrails.refresh": "Yenile", + "guardrails.retentionEvents": "Saklanan olaylar: {current} / {max}", + "guardrails.retentionEvicted": "Çıkarılan olay sayısı: {count}", + "guardrails.lastRefresh": "Son yenileme: {date}", + "guardrails.time": "Zaman", + "guardrails.count": "Sayı", + "guardrails.rules": "Kurallar", + "guardrails.latency": "Gecikme", + "guardrails.dialog.disable.title": "Guardrails devre dışı bırakılsın mı?", + "guardrails.dialog.disable.changes": "Yeni istekler artık taranmayacak veya maskelenmeyecek.", + "guardrails.dialog.disable.breakage": "Hassas değerler bu proxy’den değiştirilmeden çıkabilir.", + "guardrails.dialog.disable.undo": "Guardrails’i Genel Bakış veya Ayarlar bölümünden yeniden etkinleştirin.", + "guardrails.dialog.disable.confirm": "Devre dışı bırak", + "guardrails.dialog.detect.title": "Yalnızca algılama moduna geçilsin mi?", + "guardrails.dialog.detect.changes": "Bulgular sayılacak ancak istek metni maskelenmeyecek.", + "guardrails.dialog.detect.breakage": "Yalnızca algılama modu, yukarı akışa gönderilen verileri korumaz.", + "guardrails.dialog.detect.undo": "Modu yeniden “Maskelemeyi zorunlu kıl” olarak ayarlayın.", + "guardrails.dialog.detect.confirm": "Yalnızca algılama modunu kullan", + "guardrails.dialog.passthrough.title": "Hata durumunda olduğu gibi iletime izin verilsin mi?", + "guardrails.dialog.passthrough.changes": "Guardrails işlemesi başarısız olursa özgün istek iletilir.", + "guardrails.dialog.passthrough.breakage": "Başarısız istek maskelenmemiş hassas değerler içerebilir.", + "guardrails.dialog.passthrough.undo": "Hata işlemeyi yeniden “İsteği engelle” olarak ayarlayın.", + "guardrails.dialog.passthrough.confirm": "Olduğu gibi iletime izin ver", + "guardrails.dialog.delete.title": "{ruleId} özel kuralı silinsin mi?", + "guardrails.dialog.delete.changes": "Özel kural etkin kural kayıt sisteminden kaldırılacak.", + "guardrails.dialog.delete.breakage": "Yalnızca bu kuralın algıladığı değerler artık korunmayacak.", + "guardrails.dialog.delete.undo": "Kuralı geri yüklemek için içe aktarın veya yeniden oluşturun.", + "guardrails.dialog.delete.confirm": "Kuralı sil", + "guardrails.dialog.import.title": "Tüm Guardrails kuralları değiştirilsin mi?", + "guardrails.dialog.import.changes": "İçe aktarılan paket, geçerli Guardrails ayarlarının ve özel kuralların yerini alacak.", + "guardrails.dialog.import.breakage": "Pakette bulunmayan yerel kurallar kaldırılacak.", + "guardrails.dialog.import.undo": "Değiştirmeden önce geçerli paketi dışa aktarın.", + "guardrails.dialog.import.confirm": "Kuralları değiştir", + "guardrails.sourceGitleaks": "Gitleaks kuralları", + "guardrails.surfaceCompact": "Sıkıştırma", + "guardrails.registryReady": "Kural kayıt sistemi hazır", + "guardrails.registryFailed": "Kural kayıt sistemi başarısız", + "guardrails.registryDisabled": "Kural kayıt sistemi devre dışı", + "guardrails.effectiveRules": "Etkin kural sayısı: {count}", + "guardrails.registryGeneration": "Kayıt sürümü {generation}", + "guardrails.testerDraftSettings": "Taslak ayarları kullan", + "guardrails.testerDraftSettingsHint": "Farklı bir veri türü ve anahtar sözcük ön filtresi seçimini kaydetmeden test edin.", + "guardrails.showingEvents": "Eşleşen {total} olaydan {shown} tanesi gösteriliyor", + "guardrails.oldestEvent": "Saklanan en eski olay: {date}", + "guardrails.tableScrollHint": "Tüm sütunları görmek için tabloyu yatay kaydırın.", + "guardrails.importPreviewTitle": "İçe aktarma önizlemesi", + "guardrails.importCreateCount": "Oluştur: {count}", + "guardrails.importReplaceCount": "Değiştir: {count}", + "guardrails.importUnchangedCount": "Değişmedi: {count}", + "guardrails.importConflicts": "İçe aktarmadan önce çakışan {count} kural ID’si çözümlenmelidir.", + "guardrails.importApply": "İçe aktarmayı uygula", + "guardrails.category": "Kategori", + "guardrails.allCategories": "Tüm kategoriler", + "guardrails.generation": "Sürüm", + "guardrails.activitySummary": "Filtrelenmiş etkinlik özeti", + "guardrails.activityEvents": "Olaylar: {count}", + "guardrails.activityFindings": "Bulgular: {count}", + "guardrails.activityAverageLatency": "Ortalama gecikme: {value} ms", + "guardrails.activityTopRule": "En sık kural: {rule}", + "guardrails.recentActivity": "Son etkinlik", + "guardrails.dialog.ruleDisable.title": "{ruleId} kuralı devre dışı bırakılsın mı?", + "guardrails.dialog.ruleDisable.changes": "Bu yerleşik kural yeni istekleri taramayı durduracak.", + "guardrails.dialog.ruleDisable.breakage": "Yalnızca bu kuralın algıladığı değerler artık korunmayacak.", + "guardrails.dialog.ruleDisable.undo": "Kuralı Kurallar sekmesinden yeniden etkinleştirin.", + "guardrails.dialog.ruleDisable.confirm": "Kuralı devre dışı bırak", + "guardrails.dialog.bulkDisable.title": "{count} kural devre dışı bırakılsın mı?", + "guardrails.dialog.bulkDisable.changes": "Bu seçimdeki filtrelenmiş tüm yerleşik kurallar devre dışı bırakılacak.", + "guardrails.dialog.bulkDisable.breakage": "Yalnızca bu kuralların kapsadığı hassas değerler proxy’den değiştirilmeden çıkabilir.", + "guardrails.dialog.bulkDisable.undo": "Kuralları Kurallar sekmesinden yeniden etkinleştirin veya önceki bir dışa aktarımı içe aktarın.", + "guardrails.dialog.bulkDisable.confirm": "Kuralları devre dışı bırak", + "guardrails.statusProtected": "Korunuyor", + "guardrails.statusDetectOnly": "Yalnızca algılama", + "guardrails.statusReducedCoverage": "Azaltılmış kapsam", + "guardrails.statusUnavailable": "Kural kayıt sistemi kullanılamıyor", + "guardrails.statusNoRules": "Etkin kural yok", + "guardrails.statusNoProviderCoverage": "Korunan sağlayıcı yok", + "guardrails.valueOn": "Açık", + "guardrails.valueOff": "Kapalı", + "guardrails.reducedCoverageWarning": "Kapsam azaltıldı: Veri türleri veya yerleşik kurallar devre dışı, sağlayıcı kapsamı sınırlı ya da işleme hataları geçirilebilir.", + "guardrails.providerScope": "Korunan sağlayıcılar", + "guardrails.providerScopeAllValue": "Tüm sağlayıcılar", + "guardrails.providerScopeSelectedValue": "Seçili ({count})", + "guardrails.importSecurityProviderScope": "Sağlayıcı kapsamı: {before} → {after}", + "guardrails.providerScopeAllHint": "Mevcut ve gelecekteki tüm sağlayıcılar korunur.", + "guardrails.providerScopeSelectedHint": "Yalnızca işaretli sağlayıcılar korunur. Yeni sağlayıcılar seçilene kadar korunmaz.", + "guardrails.providerAnthropicNative": "Anthropic (yerel)", + "guardrails.providerDisabled": "devre dışı", + "guardrails.providerNotConfigured": "yapılandırılmamış", + "guardrails.lastProviderRequired": "En az bir sağlayıcı seçin veya Guardrails'i tamamen devre dışı bırakın.", + "guardrails.providerScopeWarning": "Sağlayıcı kapsamı sınırlı. İşaretlenmemiş sağlayıcılara yönlendirilen istekler taranmaz veya maskelenmez.", + "guardrails.noProviderCoverageWarning": "Etkin bir sağlayıcı seçilmedi. Geçerli sağlayıcı trafiği taranmaz veya maskelenmez.", + "guardrails.dialog.providerScope.title": "Sağlayıcı kapsamı sınırlansın mı?", + "guardrails.dialog.providerScope.changes": "İşareti kaldırılan sağlayıcıya doğrudan yönlendirilen yeni istekler artık taranmayacak veya maskelenmeyecek.", + "guardrails.dialog.providerScope.breakage": "Hassas değerler bu sağlayıcıya değiştirilmeden ulaşabilir. Korunan devam istekleri bu sınırı geçemez.", + "guardrails.dialog.providerScope.undo": "Korumayı geri yüklemek için Guardrails Ayarlarında sağlayıcıyı yeniden seçin.", + "guardrails.dialog.providerScope.confirm": "Kapsamı sınırla", + "guardrails.lastDataTypeRequired": "En az bir veri türü etkin kalmalıdır.", + "guardrails.importSecurityTitle": "Güvenlik ayarları", + "guardrails.importSecurityWeakening": "Bu içe aktarma korumayı azaltır. Devam etmeden önce vurgulanan her değişikliği inceleyin.", + "guardrails.importSecurityPotential": "Bu içe aktarma güvenlikle ilgili özel kural tanımlarını değiştirir ve korumayı azaltabilir. Devam etmeden önce inceleyin.", + "guardrails.importSecurityUnchanged": "Guardrails güvenlik ayarları değişmeyecek.", + "guardrails.importSecurityEnabled": "Trafik koruması: {before} → {after}", + "guardrails.importSecurityMode": "Mod: {before} → {after}", + "guardrails.importSecurityFailure": "Hata işleme: {before} → {after}", + "guardrails.importSecurityDataTypes": "Etkin veri türleri: {before} → {after}", + "guardrails.importSecurityDisabledRules": "Devre dışı yerleşik kurallar: {before} → {after} (yeni devre dışı: {disabled}, yeniden etkin: {reenabled})", + "guardrails.importSecurityCustomRules": "Özel kurallar: {before} → {after} (silinen: {removed}, değişen: {changed})", + "guardrails.importSecurityChangedRuleIds": "Tanımı değişen kurallar:", + "guardrails.importSecurityRemovedRuleIds": "Kaldırılan kural kimlikleri:", + "guardrails.importSecurityPrefilter": "Anahtar sözcük ön filtresi: {before} → {after}", + "guardrails.importSecurityReduced": "Azaltılmış koruma", + "guardrails.importSecurityReviewRequired": "Güvenlik incelemesi gerekli", + "guardrails.importSecurityRemovedTypes": "Artık korunmayanlar: {types}", + "guardrails.testerSimulation": "Yalnızca Tester simülasyonu. Sentetik değerler kullanın; gerçek sırları asla yapıştırmayın. Bağlı modda örnek OpenCodex hub'ınızdan geçebilir.", + "guardrails.testerTrafficDisabled": "Gerçek trafik koruması devre dışı. İstekler şu anda Guardrails maskelemesi olmadan proxy’den çıkıyor.", + "guardrails.testerTrafficDetect": "Gerçek trafik yalnızca algılanıyor ve sağlayıcıya ulaşmadan önce maskelenmiyor.", + "guardrails.testerTrafficUnknown": "Gerçek trafik koruma durumu henüz bilinmiyor. Guardrails durumunun yüklenmesini bekleyin.", + "guardrails.testerTrafficUnavailable": "Gerçek trafik koruması kullanılamıyor. Guardrails etkinleştirme veya kayıt hatasını giderin.", + "guardrails.testerTrafficNoRules": "Gerçek trafik için etkin Guardrails kuralı yok. En az bir etkili kuralı etkinleştirin.", + "guardrails.testerTrafficNoProviderCoverage": "Simülasyon eşleşmeleri bulabilir, ancak trafik koruması için etkin bir sağlayıcı seçilmemiştir.", + "guardrails.testerTrafficReduced": "Gerçek trafik azaltılmış koruma kullanıyor. Devre dışı kuralları, veri türlerini, sağlayıcı kapsamını ve hata politikasını inceleyin.", + "guardrails.dialog.categoryDisable.title": "Bir veri kategorisi devre dışı bırakılsın mı?", + "guardrails.dialog.categoryDisable.changes": "Bu kategorideki kurallar yeni istekleri taramayı durduracak.", + "guardrails.dialog.categoryDisable.breakage": "Yalnızca bu kategorinin kapsadığı hassas değerler proxy’den değiştirilmeden çıkabilir.", + "guardrails.dialog.categoryDisable.undo": "Kategoriyi Guardrails Ayarları’ndan yeniden etkinleştirin.", + "guardrails.dialog.categoryDisable.confirm": "Kategoriyi devre dışı bırak", + "guardrails.dialog.prefilter.title": "Anahtar sözcük ön filtresi etkinleştirilsin mi?", + "guardrails.dialog.prefilter.changes": "Anahtar sözcüklü kurallar, sözcükler aday metinde yoksa atlanabilir.", + "guardrails.dialog.prefilter.breakage": "Bu performans tercihi algılama kapsamını azaltabilir.", + "guardrails.dialog.prefilter.undo": "Guardrails Ayarları’nda anahtar sözcük ön filtresini kapatın.", + "guardrails.dialog.prefilter.confirm": "Ön filtreyi etkinleştir", + "guardrails.dialog.importWeakening.title": "Korumayı azaltan içe aktarma uygulansın mı?", + "guardrails.dialog.importWeakening.changes": "İçe aktarılan paket, Guardrails ayarlarını ve özel kuralları önizlenen değerlerle değiştirecek.", + "guardrails.dialog.importWeakening.breakage": "Vurgulanan bir veya daha fazla ayar, ek hassas değerlerin sağlayıcılara değiştirilmeden ulaşmasına izin verecek.", + "guardrails.dialog.importWeakening.undo": "Bu içe aktarmayı uygulamadan önce geçerli paketi dışa aktarın.", + "guardrails.dialog.importWeakening.confirm": "Azaltılmış korumayı uygula", "models.displayNameSavedRefreshFailed": "Değişiklik kaydedildi ancak model listesi yenilenemedi. Yenilemek için tekrar deneyin.", "models.displayNameOutcomeUnknown": "İstek tamamlanmadı. Değişiklik kaydedilmiş olabilir. Başka bir değişiklik yapmadan önce geçerli adı kontrol etmek için tekrar deneyin.", "models.displayNameCurrentUnavailable": "Geçerli ad yenilemeye kadar kullanılamıyor", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index ae0ee0517b..c878db0c12 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2555,6 +2555,278 @@ export const zhTW: Record = { "integrations.cursor.colReasoning": "推理", "integrations.cursor.colContext": "上下文", "integrations.cursor.guide": "開啟 Cursor Private Inference 指南", + "nav.guardrails": "資料防護", + "guardrails.title": "敏感資料防護", + "guardrails.subtitle": "在請求離開此 Proxy 前,將偵測到的敏感值替換為可還原的預留位置。", + "guardrails.loading": "正在載入 Guardrails 設定…", + "guardrails.loadFailed": "無法載入 Guardrails 設定。", + "guardrails.saveFailed": "無法儲存 Guardrails 設定。", + "guardrails.saved": "已儲存 Guardrails 設定。", + "guardrails.statusActive": "運作中", + "guardrails.statusDisabled": "已停用", + "guardrails.settingsTitle": "請求防護", + "guardrails.settingsHint": "變更會以不可分割的方式套用至新請求;現有串流會保留目前的對應關係。", + "guardrails.enabled": "啟用 Guardrails", + "guardrails.enabledHint": "在允許清單中的提示詞與工具欄位送達上游提供者前加上遮罩。", + "guardrails.mode": "模式", + "guardrails.modeEnforce": "強制遮罩", + "guardrails.modeDetect": "僅偵測", + "guardrails.failurePolicy": "處理失敗時", + "guardrails.failureBlock": "封鎖請求", + "guardrails.failurePassthrough": "直接傳送請求", + "guardrails.keywordPrefilter": "關鍵字預先篩選", + "guardrails.keywordPrefilterHint": "僅略過可由正規表示式證明每個相符項目都含有關鍵字的內建規則。自訂與無法證明的規則一律掃描。", + "guardrails.dataTypes": "已啟用的資料類型", + "guardrails.dataType": "資料類型", + "guardrails.dataType.credentials": "憑證", + "guardrails.groupCredentialUrls": "含憑證資訊的 URL", + "guardrails.dataType.apiKeys": "API 金鑰", + "guardrails.dataType.accessTokens": "存取權杖", + "guardrails.dataType.ipAddresses": "IP 位址", + "guardrails.dataType.personal": "個人資料", + "guardrails.dataType.custom": "自訂", + "guardrails.rulesTitle": "規則管理", + "guardrails.rulesHint": "停用內建規則或管理本機自訂規則。此處絕不會公開內建正規表示式。", + "guardrails.selectBuiltin": "選取內建規則", + "guardrails.disableBuiltin": "停用規則", + "guardrails.enableBuiltin": "啟用規則", + "guardrails.noDisabledBuiltins": "沒有停用的內建規則。", + "guardrails.customRules": "自訂規則", + "guardrails.noCustomRules": "目前沒有自訂規則。", + "guardrails.addRule": "新增規則", + "guardrails.editRule": "編輯規則", + "guardrails.deleteRule": "刪除規則", + "guardrails.removeRuleConfirm": "要刪除自訂規則 {ruleId} 嗎?", + "guardrails.ruleSaveFailed": "無法儲存自訂規則。", + "guardrails.ruleDeleteFailed": "無法刪除自訂規則。", + "guardrails.ruleSaved": "已儲存自訂規則。", + "guardrails.ruleDeleted": "已刪除自訂規則。", + "guardrails.ruleId": "規則 ID", + "guardrails.ruleName": "規則名稱", + "guardrails.group": "群組", + "guardrails.groupPriority": "群組優先順序", + "guardrails.displayName": "顯示名稱", + "guardrails.description": "說明", + "guardrails.regex": "RE2 模式", + "guardrails.placeholderType": "預留位置類型", + "guardrails.captureGroups": "擷取群組", + "guardrails.captureGroupsInvalid": "請輸入以逗號分隔的正整數群組編號。", + "guardrails.minLength": "最小長度", + "guardrails.entropy": "熵值門檻", + "guardrails.keywords": "關鍵字", + "guardrails.banlist": "排除清單", + "guardrails.validators": "驗證器", + "guardrails.saveRule": "儲存規則", + "guardrails.cancelEdit": "取消", + "guardrails.tabsLabel": "Guardrails 區段", + "guardrails.tab.overview": "總覽", + "guardrails.tab.rules": "規則", + "guardrails.tab.tester": "測試工具", + "guardrails.tab.activity": "活動", + "guardrails.tab.settings": "設定", + "guardrails.detectWarning": "僅偵測模式會回報結果,但不會在敏感資料離開 Proxy 前加上遮罩。", + "guardrails.passthroughWarning": "Guardrails 處理失敗時,直接傳送可能會將未遮罩的輸入送至上游。", + "guardrails.metric.scanned": "已掃描請求", + "guardrails.metric.masked": "已遮罩的值", + "guardrails.metric.detected": "已偵測的值", + "guardrails.metric.errors": "警告與封鎖", + "guardrails.registryReady": "規則庫已就緒", + "guardrails.registryFailed": "規則庫發生錯誤", + "guardrails.registryDisabled": "規則庫已停用", + "guardrails.effectiveRules": "{count} 項有效規則", + "guardrails.registryGeneration": "版本代次 {generation}", + "guardrails.lastPassthrough": "最近一次直接傳送事件:{date}", + "guardrails.topRules": "最常觸發的規則", + "guardrails.topCategories": "最常見的資料類別", + "guardrails.noActivity": "目前沒有 Guardrails 活動", + "guardrails.noActivityHint": "受保護的請求處理完成後,中繼資料會顯示於此。", + "guardrails.documentation": "閱讀 Guardrails 說明文件", + "guardrails.searchRules": "搜尋規則", + "guardrails.source": "來源", + "guardrails.sourceOpenCodex": "OpenCodex 規則", + "guardrails.sourceManual": "內建", + "guardrails.sourceGitleaks": "Gitleaks 規則", + "guardrails.allSources": "所有來源", + "guardrails.status": "狀態", + "guardrails.allStatuses": "所有狀態", + "guardrails.allDataTypes": "所有資料類型", + "guardrails.filteredRules": "篩選出 {count} 項規則", + "guardrails.bulkEnable": "啟用 {count} 項", + "guardrails.bulkDisable": "停用 {count} 項", + "guardrails.export": "匯出", + "guardrails.exported": "Guardrails 設定包已匯出。", + "guardrails.exportFailed": "無法匯出 Guardrails 設定包。", + "guardrails.import": "匯入", + "guardrails.importMode": "匯入模式", + "guardrails.importMerge": "合併", + "guardrails.importReplace": "取代", + "guardrails.importFailed": "無法匯入 Guardrails 規則。", + "guardrails.importTooLarge": "Guardrails 匯入檔案不得超過 4 MiB。", + "guardrails.imported": "已匯入 Guardrails 規則。", + "guardrails.importPreviewTitle": "匯入預覽", + "guardrails.importCreateCount": "建立:{count}", + "guardrails.importReplaceCount": "取代:{count}", + "guardrails.importUnchangedCount": "未變更:{count}", + "guardrails.importConflicts": "匯入前必須先解決 {count} 個規則 ID 衝突。", + "guardrails.importApply": "套用匯入", + "guardrails.noRules": "沒有符合這些篩選條件的規則。", + "guardrails.previous": "上一頁", + "guardrails.next": "下一頁", + "guardrails.page": "第 {current} 頁,共 {total} 頁", + "guardrails.testerTitle": "敏感資料測試工具", + "guardrails.testerHint": "使用此 OpenCodex 執行個體的有效規則庫掃描。範例會送至其 Management API,但絕不會送往 LLM 提供者,也不會儲存在伺服器端。", + "guardrails.testerDraftSettings": "使用草稿設定", + "guardrails.testerDraftSettingsHint": "測試另一組資料類型與關鍵字預先篩選設定,且不儲存變更。", + "guardrails.testerInput": "要掃描的文字", + "guardrails.testerPlaceholder": "在此貼上範例文字…", + "guardrails.byteCounter": "{current} / {max} 位元組", + "guardrails.clear": "清除", + "guardrails.test": "掃描", + "guardrails.testing": "正在掃描…", + "guardrails.testerFailed": "無法掃描文字。", + "guardrails.testerResult": "掃描結果", + "guardrails.effective": "生效中的設定", + "guardrails.draft": "草稿設定", + "guardrails.findings": "偵測結果", + "guardrails.noFindings": "未偵測到敏感值。", + "guardrails.placeholder": "預留位置", + "guardrails.offsets": "位置範圍", + "guardrails.activityTitle": "Guardrails 活動", + "guardrails.activityPrivacy": "僅保留記憶體中的中繼資料;這不是稽核記錄。此處絕不儲存提示詞文字或預留位置對應。", + "guardrails.category": "類別", + "guardrails.allCategories": "所有類別", + "guardrails.generation": "版本代次", + "guardrails.activitySummary": "篩選後的活動摘要", + "guardrails.activityEvents": "事件:{count}", + "guardrails.activityFindings": "偵測結果:{count}", + "guardrails.activityAverageLatency": "平均延遲:{value} 毫秒", + "guardrails.activityTopRule": "最常觸發規則:{rule}", + "guardrails.recentActivity": "最近活動", + "guardrails.surface": "介面", + "guardrails.surfaceCompact": "壓縮摘要", + "guardrails.allSurfaces": "所有介面", + "guardrails.allModes": "所有模式", + "guardrails.result": "結果", + "guardrails.allResults": "所有結果", + "guardrails.result.scanned": "已掃描", + "guardrails.result.masked": "已遮罩", + "guardrails.result.detected": "已偵測", + "guardrails.result.blocked": "已封鎖", + "guardrails.result.passthrough": "已直接傳送", + "guardrails.result.demaskWarning": "解除遮罩警告", + "guardrails.result.toolSkipped": "已略過工具還原", + "guardrails.refresh": "重新整理", + "guardrails.retentionEvents": "保留 {current} / {max} 個事件", + "guardrails.retentionEvicted": "已淘汰 {count} 個事件", + "guardrails.showingEvents": "顯示 {shown} / {total} 個符合事件", + "guardrails.oldestEvent": "最舊保留事件:{date}", + "guardrails.lastRefresh": "上次重新整理:{date}", + "guardrails.tableScrollHint": "水平捲動表格以查看所有欄位。", + "guardrails.time": "時間", + "guardrails.count": "數量", + "guardrails.rules": "規則", + "guardrails.latency": "延遲", + "guardrails.dialog.disable.title": "要停用 Guardrails 嗎?", + "guardrails.dialog.disable.changes": "新請求將不再被掃描或遮罩。", + "guardrails.dialog.disable.breakage": "敏感值可能未經變更便離開此 Proxy。", + "guardrails.dialog.disable.undo": "可在「總覽」或「設定」中重新啟用 Guardrails。", + "guardrails.dialog.disable.confirm": "停用", + "guardrails.dialog.detect.title": "要切換至僅偵測模式嗎?", + "guardrails.dialog.detect.changes": "系統會統計偵測結果,但不會遮罩請求文字。", + "guardrails.dialog.detect.breakage": "僅偵測模式無法保護送往上游的資料。", + "guardrails.dialog.detect.undo": "將模式切回「強制遮罩」。", + "guardrails.dialog.detect.confirm": "使用僅偵測模式", + "guardrails.dialog.passthrough.title": "要允許失敗時直接傳送嗎?", + "guardrails.dialog.passthrough.changes": "Guardrails 處理失敗時,系統會轉送原始請求。", + "guardrails.dialog.passthrough.breakage": "失敗的請求可能包含未遮罩的敏感值。", + "guardrails.dialog.passthrough.undo": "將失敗處理方式設回「封鎖請求」。", + "guardrails.dialog.passthrough.confirm": "允許直接傳送", + "guardrails.dialog.ruleDisable.title": "要停用規則 {ruleId} 嗎?", + "guardrails.dialog.ruleDisable.changes": "此內建規則將停止掃描新的請求。", + "guardrails.dialog.ruleDisable.breakage": "只有此規則能偵測的值將不再受到保護。", + "guardrails.dialog.ruleDisable.undo": "可在「規則」分頁重新啟用此規則。", + "guardrails.dialog.ruleDisable.confirm": "停用規則", + "guardrails.dialog.bulkDisable.title": "要停用 {count} 項規則嗎?", + "guardrails.dialog.bulkDisable.changes": "目前篩選結果中的所有內建規則都將被停用。", + "guardrails.dialog.bulkDisable.breakage": "只有這些規則能偵測的敏感值可能未經變更便離開 Proxy。", + "guardrails.dialog.bulkDisable.undo": "可在「規則」分頁重新啟用,或匯入先前匯出的設定。", + "guardrails.dialog.bulkDisable.confirm": "停用規則", + "guardrails.dialog.delete.title": "要刪除自訂規則 {ruleId} 嗎?", + "guardrails.dialog.delete.changes": "此自訂規則將從有效規則庫中移除。", + "guardrails.dialog.delete.breakage": "只有此規則能偵測的值將不再受到保護。", + "guardrails.dialog.delete.undo": "匯入或重新建立此規則即可還原。", + "guardrails.dialog.delete.confirm": "刪除規則", + "guardrails.dialog.import.title": "要取代所有 Guardrails 規則嗎?", + "guardrails.dialog.import.changes": "匯入的套件將取代目前的 Guardrails 設定與自訂規則。", + "guardrails.dialog.import.breakage": "套件中沒有的本機規則將被移除。", + "guardrails.dialog.import.undo": "取代前請先匯出目前的套件。", + "guardrails.dialog.import.confirm": "取代規則", + "guardrails.statusProtected": "已保護", + "guardrails.statusDetectOnly": "僅偵測", + "guardrails.statusReducedCoverage": "保護範圍已縮小", + "guardrails.statusUnavailable": "規則庫無法使用", + "guardrails.statusNoRules": "沒有有效規則", + "guardrails.statusNoProviderCoverage": "沒有受保護的供應商", + "guardrails.valueOn": "開啟", + "guardrails.valueOff": "關閉", + "guardrails.reducedCoverageWarning": "保護範圍已縮小:部分資料類型或內建規則已停用、供應商範圍受限,或處理失敗時可能直接放行。", + "guardrails.providerScope": "受保護的供應商", + "guardrails.providerScopeAllValue": "所有供應商", + "guardrails.providerScopeSelectedValue": "已選取({count})", + "guardrails.importSecurityProviderScope": "供應商保護範圍:{before} → {after}", + "guardrails.providerScopeAllHint": "保護目前及未來新增的所有供應商。", + "guardrails.providerScopeSelectedHint": "只保護已勾選的供應商。新供應商在被選取前不會受到保護。", + "guardrails.providerAnthropicNative": "Anthropic(原生)", + "guardrails.providerDisabled": "已停用", + "guardrails.providerNotConfigured": "未設定", + "guardrails.lastProviderRequired": "請至少保留一個供應商,或完全停用 Guardrails。", + "guardrails.providerScopeWarning": "供應商保護範圍受限。送往未勾選供應商的請求不會被掃描或遮罩。", + "guardrails.noProviderCoverageWarning": "未選取任何啟用中的供應商。目前的供應商流量不會被掃描或遮罩。", + "guardrails.dialog.providerScope.title": "限制供應商保護範圍?", + "guardrails.dialog.providerScope.changes": "直接送往取消勾選供應商的新請求將不再被掃描或遮罩。", + "guardrails.dialog.providerScope.breakage": "敏感值可能原樣送給該供應商。受保護的延續請求不能跨越此邊界。", + "guardrails.dialog.providerScope.undo": "在 Guardrails 設定中重新選取該供應商即可恢復保護。", + "guardrails.dialog.providerScope.confirm": "限制範圍", + "guardrails.lastDataTypeRequired": "必須至少保留一種已啟用的資料類型。", + "guardrails.importSecurityTitle": "安全性設定", + "guardrails.importSecurityWeakening": "此匯入會降低保護。繼續前請檢查每項醒目顯示的變更。", + "guardrails.importSecurityPotential": "此匯入會變更與安全相關的自訂規則定義,並可能降低保護。繼續前請先檢查。", + "guardrails.importSecurityUnchanged": "Guardrails 安全性設定不會變更。", + "guardrails.importSecurityEnabled": "流量保護:{before} → {after}", + "guardrails.importSecurityMode": "模式:{before} → {after}", + "guardrails.importSecurityFailure": "失敗處理:{before} → {after}", + "guardrails.importSecurityDataTypes": "已啟用的資料類型:{before} → {after}", + "guardrails.importSecurityDisabledRules": "已停用的內建規則:{before} → {after}(新增停用:{disabled},重新啟用:{reenabled})", + "guardrails.importSecurityCustomRules": "自訂規則:{before} → {after}(刪除:{removed},變更:{changed})", + "guardrails.importSecurityChangedRuleIds": "定義已變更的規則:", + "guardrails.importSecurityRemovedRuleIds": "已移除的規則 ID:", + "guardrails.importSecurityPrefilter": "關鍵字預先篩選:{before} → {after}", + "guardrails.importSecurityReduced": "保護已降低", + "guardrails.importSecurityReviewRequired": "需要安全審查", + "guardrails.importSecurityRemovedTypes": "不再保護:{types}", + "guardrails.testerSimulation": "僅限 Tester 模擬。請使用合成值,切勿貼上真實密鑰。連線模式下,範例可能會經過您的 OpenCodex hub。", + "guardrails.testerTrafficDisabled": "實際流量保護已停用。請求目前會在未經 Guardrails 遮罩的情況下離開 Proxy。", + "guardrails.testerTrafficDetect": "實際流量僅執行偵測,在到達供應商前不會被遮罩。", + "guardrails.testerTrafficUnknown": "實際流量保護狀態仍未知。請等待 Guardrails 狀態載入完成。", + "guardrails.testerTrafficUnavailable": "實際流量保護無法使用。請先解決 Guardrails 啟用或規則登錄故障。", + "guardrails.testerTrafficNoRules": "實際流量沒有啟用的 Guardrails 規則。請至少啟用一條有效規則。", + "guardrails.testerTrafficNoProviderCoverage": "模擬仍可找出符合項目,但目前未選取任何啟用中的供應商來保護流量。", + "guardrails.testerTrafficReduced": "實際流量使用較低層級的保護。請檢查已停用規則、資料類型、供應商範圍和失敗策略。", + "guardrails.dialog.categoryDisable.title": "要停用一個資料類別嗎?", + "guardrails.dialog.categoryDisable.changes": "此類別中的規則將停止掃描新請求。", + "guardrails.dialog.categoryDisable.breakage": "僅由此類別涵蓋的敏感值可能未經變更便離開 Proxy。", + "guardrails.dialog.categoryDisable.undo": "可在 Guardrails 設定中重新啟用此類別。", + "guardrails.dialog.categoryDisable.confirm": "停用類別", + "guardrails.dialog.prefilter.title": "要啟用關鍵字預先篩選嗎?", + "guardrails.dialog.prefilter.changes": "候選文字中沒有對應關鍵字時,帶有關鍵字的規則可能會被略過。", + "guardrails.dialog.prefilter.breakage": "此效能取捨可能降低偵測召回率。", + "guardrails.dialog.prefilter.undo": "可在 Guardrails 設定中關閉關鍵字預先篩選。", + "guardrails.dialog.prefilter.confirm": "啟用預先篩選", + "guardrails.dialog.importWeakening.title": "要套用會降低保護的匯入嗎?", + "guardrails.dialog.importWeakening.changes": "匯入的套件會以預覽值取代 Guardrails 設定與自訂規則。", + "guardrails.dialog.importWeakening.breakage": "一項或多項醒目顯示的設定會允許更多敏感值未經變更便到達供應商。", + "guardrails.dialog.importWeakening.undo": "套用此匯入前請先匯出目前的套件。", + "guardrails.dialog.importWeakening.confirm": "套用降低後的保護", "models.displayNameSavedRefreshFailed": "變更已儲存,但無法重新整理模型清單。請重試。", "models.displayNameOutcomeUnknown": "請求未完成。變更可能已儲存。再次變更之前,請重試以檢查目前名稱。", "models.displayNameCurrentUnavailable": "重新整理之前無法取得目前名稱", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index ca92aaeafc..fc8371a7cd 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -2591,6 +2591,278 @@ export const zh: Record = { "integrations.cursor.colReasoning": "推理", "integrations.cursor.colContext": "上下文", "integrations.cursor.guide": "打开 Cursor Private Inference 指南", + "nav.guardrails": "Guardrails", + "guardrails.title": "敏感数据防护", + "guardrails.subtitle": "在请求离开此代理之前,将检测到的敏感值替换为可还原的占位符。", + "guardrails.loading": "正在加载 Guardrails 设置…", + "guardrails.loadFailed": "无法加载 Guardrails 设置。", + "guardrails.saveFailed": "无法保存 Guardrails 设置。", + "guardrails.saved": "已保存 Guardrails 设置。", + "guardrails.statusActive": "已启用", + "guardrails.statusDisabled": "已禁用", + "guardrails.settingsTitle": "请求防护", + "guardrails.settingsHint": "更改会以原子方式应用于新请求;现有流会保留当前映射。", + "guardrails.enabled": "启用 Guardrails", + "guardrails.enabledHint": "在允许列表中的提示词和工具字段到达上游提供商之前对其进行脱敏。", + "guardrails.mode": "模式", + "guardrails.modeEnforce": "强制脱敏", + "guardrails.modeDetect": "仅检测", + "guardrails.failurePolicy": "处理失败时", + "guardrails.failureBlock": "阻止请求", + "guardrails.failurePassthrough": "直接转发请求", + "guardrails.keywordPrefilter": "关键词预筛选", + "guardrails.keywordPrefilterHint": "仅跳过可由正则表达式证明每个匹配都含有关键词的内置规则。自定义及无法证明的规则始终扫描。", + "guardrails.dataTypes": "已启用的数据类型", + "guardrails.dataType": "数据类型", + "guardrails.dataType.credentials": "凭据", + "guardrails.groupCredentialUrls": "含凭据的 URL", + "guardrails.dataType.apiKeys": "API 密钥", + "guardrails.dataType.accessTokens": "访问令牌", + "guardrails.dataType.ipAddresses": "IP 地址", + "guardrails.dataType.personal": "个人数据", + "guardrails.dataType.custom": "自定义", + "guardrails.rulesTitle": "规则管理", + "guardrails.rulesHint": "禁用内置规则或管理本地自定义规则。此处绝不会公开内置正则表达式。", + "guardrails.selectBuiltin": "选择内置规则", + "guardrails.disableBuiltin": "禁用规则", + "guardrails.enableBuiltin": "启用规则", + "guardrails.noDisabledBuiltins": "没有已禁用的内置规则。", + "guardrails.customRules": "自定义规则", + "guardrails.noCustomRules": "目前没有自定义规则。", + "guardrails.addRule": "添加规则", + "guardrails.editRule": "编辑规则", + "guardrails.deleteRule": "删除规则", + "guardrails.removeRuleConfirm": "要删除自定义规则 {ruleId} 吗?", + "guardrails.ruleSaveFailed": "无法保存自定义规则。", + "guardrails.ruleDeleteFailed": "无法删除自定义规则。", + "guardrails.ruleSaved": "已保存自定义规则。", + "guardrails.ruleDeleted": "已删除自定义规则。", + "guardrails.ruleId": "规则 ID", + "guardrails.ruleName": "规则名称", + "guardrails.group": "分组", + "guardrails.groupPriority": "分组优先级", + "guardrails.displayName": "显示名称", + "guardrails.description": "说明", + "guardrails.regex": "RE2 模式", + "guardrails.placeholderType": "占位符类型", + "guardrails.captureGroups": "捕获组", + "guardrails.captureGroupsInvalid": "请输入以逗号分隔的正整数捕获组编号。", + "guardrails.minLength": "最小长度", + "guardrails.entropy": "熵阈值", + "guardrails.keywords": "关键词", + "guardrails.banlist": "排除列表", + "guardrails.validators": "验证器", + "guardrails.saveRule": "保存规则", + "guardrails.cancelEdit": "取消", + "guardrails.tabsLabel": "Guardrails 分区", + "guardrails.tab.overview": "概览", + "guardrails.tab.rules": "规则", + "guardrails.tab.tester": "测试器", + "guardrails.tab.activity": "活动", + "guardrails.tab.settings": "设置", + "guardrails.detectWarning": "仅检测模式会报告检测结果,但不会在敏感数据离开代理前对其脱敏。", + "guardrails.passthroughWarning": "Guardrails 处理失败时,直接转发可能会将未脱敏的输入发送到上游。", + "guardrails.metric.scanned": "已扫描的请求", + "guardrails.metric.masked": "已脱敏的值", + "guardrails.metric.detected": "已检测的值", + "guardrails.metric.errors": "警告与阻止", + "guardrails.lastPassthrough": "最近一次直接转发事件:{date}", + "guardrails.topRules": "最常触发的规则", + "guardrails.topCategories": "最常见的数据类别", + "guardrails.noActivity": "目前没有 Guardrails 活动", + "guardrails.noActivityHint": "受保护的请求处理完成后,元数据会显示在这里。", + "guardrails.documentation": "阅读 Guardrails 文档", + "guardrails.searchRules": "搜索规则", + "guardrails.source": "来源", + "guardrails.sourceOpenCodex": "OpenCodex 规则", + "guardrails.sourceManual": "内置", + "guardrails.allSources": "所有来源", + "guardrails.status": "状态", + "guardrails.allStatuses": "所有状态", + "guardrails.allDataTypes": "所有数据类型", + "guardrails.filteredRules": "已筛选出 {count} 条规则", + "guardrails.bulkEnable": "启用 {count} 条", + "guardrails.bulkDisable": "禁用 {count} 条", + "guardrails.export": "导出", + "guardrails.exported": "Guardrails 配置包已导出。", + "guardrails.exportFailed": "无法导出 Guardrails 配置包。", + "guardrails.import": "导入", + "guardrails.importMode": "导入模式", + "guardrails.importMerge": "合并", + "guardrails.importReplace": "替换", + "guardrails.importFailed": "无法导入 Guardrails 规则。", + "guardrails.importTooLarge": "Guardrails 导入文件不得超过 4 MiB。", + "guardrails.imported": "已导入 Guardrails 规则。", + "guardrails.noRules": "没有符合这些筛选条件的规则。", + "guardrails.previous": "上一页", + "guardrails.next": "下一页", + "guardrails.page": "第 {current} 页,共 {total} 页", + "guardrails.testerTitle": "敏感数据测试器", + "guardrails.testerHint": "使用此 OpenCodex 实例的有效规则库扫描。样本会发送到其 Management API,但绝不会发送到 LLM 提供商,也不会在服务器端存储。", + "guardrails.testerInput": "要扫描的文本", + "guardrails.testerPlaceholder": "在此粘贴示例文本…", + "guardrails.byteCounter": "{current} / {max} 字节", + "guardrails.clear": "清除", + "guardrails.test": "扫描", + "guardrails.testing": "正在扫描…", + "guardrails.testerFailed": "无法扫描文本。", + "guardrails.testerResult": "扫描结果", + "guardrails.effective": "生效设置", + "guardrails.draft": "草稿设置", + "guardrails.findings": "检测结果", + "guardrails.noFindings": "未检测到敏感值。", + "guardrails.placeholder": "占位符", + "guardrails.offsets": "偏移量", + "guardrails.activityTitle": "Guardrails 活动", + "guardrails.activityPrivacy": "仅显示内存中的元数据;这不是审计日志。此处绝不会存储提示词文本或占位符映射。", + "guardrails.surface": "接口", + "guardrails.allSurfaces": "所有接口", + "guardrails.allModes": "所有模式", + "guardrails.result": "结果", + "guardrails.allResults": "所有结果", + "guardrails.result.scanned": "已扫描", + "guardrails.result.masked": "已脱敏", + "guardrails.result.detected": "已检测", + "guardrails.result.blocked": "已阻止", + "guardrails.result.passthrough": "已直接转发", + "guardrails.result.demaskWarning": "数据还原警告", + "guardrails.result.toolSkipped": "已跳过工具还原", + "guardrails.refresh": "刷新", + "guardrails.retentionEvents": "保留 {current} / {max} 个事件", + "guardrails.retentionEvicted": "已移除 {count} 个事件", + "guardrails.lastRefresh": "上次刷新:{date}", + "guardrails.time": "时间", + "guardrails.count": "数量", + "guardrails.rules": "规则", + "guardrails.latency": "延迟", + "guardrails.dialog.disable.title": "要禁用 Guardrails 吗?", + "guardrails.dialog.disable.changes": "新请求将不再被扫描或脱敏。", + "guardrails.dialog.disable.breakage": "敏感值可能未经处理便离开此代理。", + "guardrails.dialog.disable.undo": "可在“概览”或“设置”中重新启用 Guardrails。", + "guardrails.dialog.disable.confirm": "禁用", + "guardrails.dialog.detect.title": "要切换到仅检测模式吗?", + "guardrails.dialog.detect.changes": "系统会统计检测结果,但不会对请求文本进行脱敏。", + "guardrails.dialog.detect.breakage": "仅检测模式无法保护发送到上游的数据。", + "guardrails.dialog.detect.undo": "将模式切回“强制脱敏”。", + "guardrails.dialog.detect.confirm": "使用仅检测模式", + "guardrails.dialog.passthrough.title": "要允许失败时直接转发吗?", + "guardrails.dialog.passthrough.changes": "Guardrails 处理失败时,系统会转发原始请求。", + "guardrails.dialog.passthrough.breakage": "失败的请求可能包含未脱敏的敏感值。", + "guardrails.dialog.passthrough.undo": "将失败处理方式改回“阻止请求”。", + "guardrails.dialog.passthrough.confirm": "允许直接转发", + "guardrails.dialog.delete.title": "要删除自定义规则 {ruleId} 吗?", + "guardrails.dialog.delete.changes": "该自定义规则将从有效规则库中移除。", + "guardrails.dialog.delete.breakage": "仅由该规则检测到的值将不再受到保护。", + "guardrails.dialog.delete.undo": "导入或重新创建该规则即可恢复。", + "guardrails.dialog.delete.confirm": "删除规则", + "guardrails.dialog.import.title": "要替换所有 Guardrails 规则吗?", + "guardrails.dialog.import.changes": "导入的规则包将替换当前 Guardrails 设置和自定义规则。", + "guardrails.dialog.import.breakage": "规则包中没有的本地规则将被移除。", + "guardrails.dialog.import.undo": "替换前请先导出当前规则包。", + "guardrails.dialog.import.confirm": "替换规则", + "guardrails.sourceGitleaks": "Gitleaks 规则", + "guardrails.surfaceCompact": "压缩摘要", + "guardrails.registryReady": "规则库已就绪", + "guardrails.registryFailed": "规则库加载失败", + "guardrails.registryDisabled": "规则库已禁用", + "guardrails.effectiveRules": "{count} 条有效规则", + "guardrails.registryGeneration": "版本 {generation}", + "guardrails.testerDraftSettings": "使用草稿设置", + "guardrails.testerDraftSettingsHint": "测试另一组数据类型和关键词预筛选设置,但不保存更改。", + "guardrails.showingEvents": "显示 {total} 个匹配事件中的 {shown} 个", + "guardrails.oldestEvent": "最早保留的事件:{date}", + "guardrails.tableScrollHint": "横向滚动表格以查看所有列。", + "guardrails.importPreviewTitle": "导入预览", + "guardrails.importCreateCount": "创建:{count}", + "guardrails.importReplaceCount": "替换:{count}", + "guardrails.importUnchangedCount": "未更改:{count}", + "guardrails.importConflicts": "导入前必须先解决 {count} 个规则 ID 冲突。", + "guardrails.importApply": "应用导入", + "guardrails.category": "类别", + "guardrails.allCategories": "所有类别", + "guardrails.generation": "版本", + "guardrails.activitySummary": "筛选后的活动摘要", + "guardrails.activityEvents": "事件:{count}", + "guardrails.activityFindings": "检测结果:{count}", + "guardrails.activityAverageLatency": "平均延迟:{value} ms", + "guardrails.activityTopRule": "最常触发的规则:{rule}", + "guardrails.recentActivity": "最近活动", + "guardrails.dialog.ruleDisable.title": "要禁用规则 {ruleId} 吗?", + "guardrails.dialog.ruleDisable.changes": "该内置规则将停止扫描新请求。", + "guardrails.dialog.ruleDisable.breakage": "仅由该规则检测到的值将不再受到保护。", + "guardrails.dialog.ruleDisable.undo": "可在“规则”选项卡中重新启用该规则。", + "guardrails.dialog.ruleDisable.confirm": "禁用规则", + "guardrails.dialog.bulkDisable.title": "要禁用 {count} 条规则吗?", + "guardrails.dialog.bulkDisable.changes": "当前筛选结果中选定的所有内置规则都将被禁用。", + "guardrails.dialog.bulkDisable.breakage": "仅由这些规则检测到的敏感值可能未经处理便离开代理。", + "guardrails.dialog.bulkDisable.undo": "可在“规则”选项卡中重新启用这些规则,或导入之前导出的规则。", + "guardrails.dialog.bulkDisable.confirm": "禁用规则", + "guardrails.statusProtected": "已保护", + "guardrails.statusDetectOnly": "仅检测", + "guardrails.statusReducedCoverage": "保护范围已缩小", + "guardrails.statusUnavailable": "规则库不可用", + "guardrails.statusNoRules": "没有有效规则", + "guardrails.statusNoProviderCoverage": "没有受保护的提供商", + "guardrails.valueOn": "开启", + "guardrails.valueOff": "关闭", + "guardrails.reducedCoverageWarning": "保护范围已缩小:部分数据类型或内置规则已禁用、提供商范围受限,或处理失败时可能直接放行。", + "guardrails.providerScope": "受保护的提供商", + "guardrails.providerScopeAllValue": "所有提供商", + "guardrails.providerScopeSelectedValue": "已选择({count})", + "guardrails.importSecurityProviderScope": "提供商保护范围:{before} → {after}", + "guardrails.providerScopeAllHint": "保护当前及未来添加的所有提供商。", + "guardrails.providerScopeSelectedHint": "仅保护已勾选的提供商。新提供商在被选中前不会受到保护。", + "guardrails.providerAnthropicNative": "Anthropic(原生)", + "guardrails.providerDisabled": "已禁用", + "guardrails.providerNotConfigured": "未配置", + "guardrails.lastProviderRequired": "请至少保留一个提供商,或完全禁用 Guardrails。", + "guardrails.providerScopeWarning": "提供商保护范围受限。发送到未勾选提供商的请求不会被扫描或遮蔽。", + "guardrails.noProviderCoverageWarning": "未选择任何启用中的提供商。当前提供商流量不会被扫描或遮蔽。", + "guardrails.dialog.providerScope.title": "限制提供商保护范围?", + "guardrails.dialog.providerScope.changes": "直接发送到取消勾选提供商的新请求将不再被扫描或遮蔽。", + "guardrails.dialog.providerScope.breakage": "敏感值可能原样发送给该提供商。受保护的延续请求不能跨越此边界。", + "guardrails.dialog.providerScope.undo": "在 Guardrails 设置中重新选择该提供商即可恢复保护。", + "guardrails.dialog.providerScope.confirm": "限制范围", + "guardrails.lastDataTypeRequired": "必须至少保留一种已启用的数据类型。", + "guardrails.importSecurityTitle": "安全设置", + "guardrails.importSecurityWeakening": "此导入会降低保护。继续前请检查每项突出显示的更改。", + "guardrails.importSecurityPotential": "此导入会更改与安全相关的自定义规则定义,并可能降低保护。继续前请进行检查。", + "guardrails.importSecurityUnchanged": "Guardrails 安全设置不会更改。", + "guardrails.importSecurityEnabled": "流量保护:{before} → {after}", + "guardrails.importSecurityMode": "模式:{before} → {after}", + "guardrails.importSecurityFailure": "失败处理:{before} → {after}", + "guardrails.importSecurityDataTypes": "已启用的数据类型:{before} → {after}", + "guardrails.importSecurityDisabledRules": "已禁用的内置规则:{before} → {after}(新增禁用:{disabled},重新启用:{reenabled})", + "guardrails.importSecurityCustomRules": "自定义规则:{before} → {after}(删除:{removed},更改:{changed})", + "guardrails.importSecurityChangedRuleIds": "定义已更改的规则:", + "guardrails.importSecurityRemovedRuleIds": "已删除的规则 ID:", + "guardrails.importSecurityPrefilter": "关键词预筛选:{before} → {after}", + "guardrails.importSecurityReduced": "保护已降低", + "guardrails.importSecurityReviewRequired": "需要安全审查", + "guardrails.importSecurityRemovedTypes": "不再保护:{types}", + "guardrails.testerSimulation": "仅限 Tester 模拟。请使用合成值,切勿粘贴真实密钥。连接模式下,样本可能会经过您的 OpenCodex hub。", + "guardrails.testerTrafficDisabled": "实际流量保护已禁用。请求当前会在未经 Guardrails 脱敏的情况下离开代理。", + "guardrails.testerTrafficDetect": "实际流量仅执行检测,在到达提供商前不会被脱敏。", + "guardrails.testerTrafficUnknown": "实际流量保护状态尚未知。请等待 Guardrails 状态加载完成。", + "guardrails.testerTrafficUnavailable": "实际流量保护不可用。请先解决 Guardrails 激活或规则注册表故障。", + "guardrails.testerTrafficNoRules": "实际流量没有启用的 Guardrails 规则。请至少启用一条有效规则。", + "guardrails.testerTrafficNoProviderCoverage": "模拟仍可发现匹配项,但当前未选择任何启用中的提供商来保护流量。", + "guardrails.testerTrafficReduced": "实际流量使用较低级别的保护。请检查已禁用规则、数据类型、提供商范围和失败策略。", + "guardrails.dialog.categoryDisable.title": "要禁用一个数据类别吗?", + "guardrails.dialog.categoryDisable.changes": "此类别中的规则将停止扫描新请求。", + "guardrails.dialog.categoryDisable.breakage": "仅由此类别覆盖的敏感值可能未经处理便离开代理。", + "guardrails.dialog.categoryDisable.undo": "可在 Guardrails 设置中重新启用该类别。", + "guardrails.dialog.categoryDisable.confirm": "禁用类别", + "guardrails.dialog.prefilter.title": "要启用关键词预筛选吗?", + "guardrails.dialog.prefilter.changes": "当候选文本中没有相应关键词时,带关键词的规则可能会被跳过。", + "guardrails.dialog.prefilter.breakage": "此性能取舍可能降低检测召回率。", + "guardrails.dialog.prefilter.undo": "可在 Guardrails 设置中关闭关键词预筛选。", + "guardrails.dialog.prefilter.confirm": "启用预筛选", + "guardrails.dialog.importWeakening.title": "要应用会降低保护的导入吗?", + "guardrails.dialog.importWeakening.changes": "导入的规则包会用预览值替换 Guardrails 设置和自定义规则。", + "guardrails.dialog.importWeakening.breakage": "一项或多项突出显示的设置会允许更多敏感值未经处理便到达提供商。", + "guardrails.dialog.importWeakening.undo": "应用此导入前请先导出当前规则包。", + "guardrails.dialog.importWeakening.confirm": "应用降低后的保护", "models.displayNameSavedRefreshFailed": "更改已保存,但无法刷新模型列表。请重试以刷新。", "models.displayNameOutcomeUnknown": "请求未完成。更改可能已保存。再次更改之前,请重试以检查当前名称。", "models.displayNameCurrentUnavailable": "刷新之前无法获取当前名称", diff --git a/gui/src/pages/Guardrails.tsx b/gui/src/pages/Guardrails.tsx new file mode 100644 index 0000000000..dbe1d36252 --- /dev/null +++ b/gui/src/pages/Guardrails.tsx @@ -0,0 +1,560 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { IconLock } from "../icons"; +import { useDataSurface } from "../data-surface"; +import { Notice } from "../ui"; +import { useT, type TKey } from "../i18n/shared"; +import ConsequenceDialog from "./integrations/ConsequenceDialog"; +import { GuardrailsActivityPanel } from "./guardrails/activity-panel"; +import { + GUARDRAILS_CONSEQUENCE_COPY, + type GuardrailsConsequenceKind, +} from "./guardrails/consequence-copy"; +import { + deleteGuardrailsCustomRule, + fetchGuardrailsExport, + fetchGuardrailsActivity, + fetchGuardrailsOverview, + fetchGuardrailsRules, + GuardrailsApiError, + importGuardrailsBundle, + previewGuardrailsBundle, + saveGuardrailsCustomRule, + toggleGuardrailsRule, + updateGuardrailsSettings, +} from "./guardrails/guardrails-api"; +import { + guardrailsPanelDomId, + guardrailsTabDomId, + readGuardrailsTab, + selectGuardrailsTab, + type GuardrailsTab, +} from "./guardrails/guardrails-tab"; +import { GuardrailsTabStrip } from "./guardrails/guardrails-tab-strip"; +import { GuardrailsOverviewPanel } from "./guardrails/overview-panel"; +import { GuardrailsRulesPanel } from "./guardrails/rules-panel"; +import { GuardrailsSettingsPanel } from "./guardrails/settings-panel"; +import { guardrailsTrafficProtectionStatus } from "./guardrails/protection-status"; +import { GuardrailsStatusBadges } from "./guardrails/status-badges"; +import { SurfaceFrame } from "./guardrails/surface-frame"; +import { GuardrailsTesterPanel } from "./guardrails/tester-panel"; +import type { + GuardrailsActivity, + GuardrailsActivityFilters, + GuardrailsCustomRule, + GuardrailsImportPreview, + GuardrailsOverview, + GuardrailsRules, + GuardrailsSettings, + GuardrailsSettingsPatch, +} from "./guardrails/types"; + +type PendingConsequence = { + kind: GuardrailsConsequenceKind; + run: () => Promise; + vars?: Record; + returnFocus: HTMLElement | null; +}; +type PendingImport = { + bundle: unknown; + preview: GuardrailsImportPreview; + returnFocus: HTMLElement | null; + revision: string; +}; +type GuardrailsActivitySnapshot = { + activity: GuardrailsActivity; + refreshedAt: number; +}; + +function errorText(error: unknown, fallback: string): string { + return error instanceof Error && error.message ? error.message : fallback; +} + +function isGuardrailsRevisionConflict(error: unknown): boolean { + return error instanceof GuardrailsApiError + && error.status === 412 + && error.code === "guardrails_revision_conflict"; +} + +export default function Guardrails({ apiBase }: { apiBase: string }) { + const t = useT(); + const [tab, setTab] = useState(readGuardrailsTab); + const [mounted, setMounted] = useState>(() => new Set([readGuardrailsTab()])); + const [pending, setPending] = useState(false); + const pendingRef = useRef(false); + const [toast, setToast] = useState<{ tone: "err" | "ok"; text: string } | null>(null); + const [consequence, setConsequence] = useState(null); + const [importPreview, setImportPreview] = useState(null); + const [testerDraft, setTesterDraft] = useState<{ + rule: GuardrailsCustomRule; + version: number; + } | null>(null); + const [activityFilters, setActivityFilters] = useState({ + category: "", + mode: "", + result: "", + surface: "", + }); + const activityFilterKey = [ + activityFilters.category, + activityFilters.mode, + activityFilters.result, + activityFilters.surface, + ].join(":"); + + const overviewResource = useDataSurface( + `guardrails-overview:${apiBase}`, + [apiBase], + signal => fetchGuardrailsOverview(apiBase, signal, t("guardrails.loadFailed")), + { isEmpty: () => false, pollMs: 30_000 }, + ); + const rulesResource = useDataSurface( + `guardrails-rules:${apiBase}`, + [apiBase], + signal => fetchGuardrailsRules(apiBase, signal, t("guardrails.loadFailed")), + { isEmpty: data => data.rules.length === 0, pauseWhenHidden: true }, + ); + const activityResource = useDataSurface( + `guardrails-activity:${apiBase}:${activityFilterKey}`, + [apiBase, activityFilterKey], + async signal => { + const activity = await fetchGuardrailsActivity( + apiBase, + signal, + t("guardrails.loadFailed"), + activityFilters, + ); + return { activity, refreshedAt: Date.now() }; + }, + { isEmpty: data => data.activity.events.length === 0, pollMs: 15_000, pauseWhenHidden: true }, + ); + + const overview = overviewResource.state.data; + const rules = rulesResource.state.data; + const overviewRef = useRef(overview); + const rulesRef = useRef(rules); + useEffect(() => { + overviewRef.current = overview; + }, [overview]); + useEffect(() => { + rulesRef.current = rules; + }, [rules]); + const adoptSettings = useCallback((next: GuardrailsSettings) => { + if (overviewRef.current) overviewRef.current = { ...overviewRef.current, ...next }; + if (rulesRef.current) rulesRef.current = { ...rulesRef.current, revision: next.revision }; + }, []); + const adoptRules = useCallback((next: GuardrailsRules) => { + rulesRef.current = next; + if (overviewRef.current) { + overviewRef.current = { + ...overviewRef.current, + revision: next.revision, + customRuleCount: next.customRuleCount, + disabledBuiltinRuleIds: next.rules + .filter(rule => !rule.custom && !rule.enabled) + .map(rule => rule.ruleId), + }; + } + }, []); + + const activateTab = useCallback((next: GuardrailsTab) => { + setTab(next); + setMounted(current => current.has(next) ? current : new Set(current).add(next)); + }, []); + useEffect(() => { + const sync = () => activateTab(readGuardrailsTab()); + window.addEventListener("hashchange", sync); + return () => window.removeEventListener("hashchange", sync); + }, [activateTab]); + const selectTab = useCallback((next: GuardrailsTab) => { + activateTab(next); + selectGuardrailsTab(next); + }, [activateTab]); + + const refreshOverview = overviewResource.refresh; + const refreshRules = rulesResource.refresh; + const refreshActivity = activityResource.refresh; + const refreshAll = useCallback(() => { + refreshOverview(); + refreshRules(); + refreshActivity(); + }, [refreshActivity, refreshOverview, refreshRules]); + + const runMutation = useCallback(async (work: () => Promise, successKey: TKey) => { + if (pendingRef.current) return; + pendingRef.current = true; + setPending(true); + setToast(null); + try { + await work(); + setToast({ tone: "ok", text: t(successKey) }); + refreshAll(); + } catch (error) { + if (isGuardrailsRevisionConflict(error)) refreshAll(); + setToast({ tone: "err", text: errorText(error, t("guardrails.saveFailed")) }); + throw error; + } finally { + pendingRef.current = false; + setPending(false); + } + }, [refreshAll, t]); + + const performSettings = useCallback(async (patch: GuardrailsSettingsPatch) => { + const current = overviewRef.current; + if (!current) return; + await runMutation(async () => { + const next = await updateGuardrailsSettings(apiBase, patch, current.revision, t("guardrails.saveFailed")); + adoptSettings(next); + }, "guardrails.saved"); + }, [adoptSettings, apiBase, runMutation, t]); + + const requestSettings = useCallback(( + patch: GuardrailsSettingsPatch, + kind?: GuardrailsConsequenceKind, + ) => { + if (!kind) { + void performSettings(patch).catch(() => undefined); + return; + } + setConsequence({ + kind, + run: () => performSettings(patch), + returnFocus: document.activeElement instanceof HTMLElement ? document.activeElement : null, + }); + }, [performSettings]); + + const closeConsequence = useCallback(() => { + const focus = consequence?.returnFocus; + setConsequence(null); + window.requestAnimationFrame(() => focus?.focus({ preventScroll: true })); + }, [consequence]); + + const applyRuleToggle = useCallback((ruleId: string, enabled: boolean) => { + const current = rulesRef.current; + if (!current) return; + return runMutation(async () => { + const next = await toggleGuardrailsRule(apiBase, ruleId, enabled, current.revision, t("guardrails.ruleSaveFailed")); + adoptRules(next); + }, "guardrails.ruleSaved"); + }, [adoptRules, apiBase, runMutation, t]); + + const toggleRule = useCallback((ruleId: string, enabled: boolean) => { + if (enabled) { + void applyRuleToggle(ruleId, true)?.catch(() => undefined); + return; + } + setConsequence({ + kind: "disableRule", + vars: { ruleId }, + run: async () => { await applyRuleToggle(ruleId, false); }, + returnFocus: document.activeElement instanceof HTMLElement ? document.activeElement : null, + }); + }, [applyRuleToggle]); + + const applyBulkRules = useCallback((ids: string[], enabled: boolean) => { + const current = overviewRef.current; + if (!current) return; + return runMutation(async () => { + const selected = new Set(ids); + const disabled = new Set(current.disabledBuiltinRuleIds); + for (const ruleId of selected) { + if (enabled) disabled.delete(ruleId); + else disabled.add(ruleId); + } + const next = await updateGuardrailsSettings( + apiBase, + { disabledBuiltinRuleIds: [...disabled].sort() }, + current.revision, + t("guardrails.ruleSaveFailed"), + ); + adoptSettings(next); + }, "guardrails.ruleSaved"); + }, [adoptSettings, apiBase, runMutation, t]); + + const bulkRules = useCallback((ids: string[], enabled: boolean) => { + if (enabled) { + void applyBulkRules(ids, true)?.catch(() => undefined); + return; + } + setConsequence({ + kind: "bulkDisable", + vars: { count: String(ids.length) }, + run: async () => { await applyBulkRules(ids, false); }, + returnFocus: document.activeElement instanceof HTMLElement ? document.activeElement : null, + }); + }, [applyBulkRules]); + + const saveRule = useCallback(async (rule: GuardrailsCustomRule, editingId: string | null) => { + const current = rulesRef.current; + if (!current) return; + await runMutation(async () => { + const next = await saveGuardrailsCustomRule( + apiBase, + rule, + editingId, + current.revision, + t("guardrails.ruleSaveFailed"), + ); + adoptRules(next); + }, "guardrails.ruleSaved"); + }, [adoptRules, apiBase, runMutation, t]); + + const testDraftRule = useCallback((rule: GuardrailsCustomRule) => { + setTesterDraft(current => ({ + rule, + version: (current?.version ?? 0) + 1, + })); + selectTab("tester"); + }, [selectTab]); + + const requestDelete = useCallback((rule: GuardrailsCustomRule) => { + if (!rulesRef.current) return; + setConsequence({ + kind: "deleteRule", + vars: { ruleId: rule.ruleId }, + returnFocus: document.activeElement instanceof HTMLElement ? document.activeElement : null, + run: () => runMutation(async () => { + const current = rulesRef.current; + if (!current) return; + const next = await deleteGuardrailsCustomRule( + apiBase, + rule.ruleId, + current.revision, + t("guardrails.ruleDeleteFailed"), + ); + adoptRules(next); + }, "guardrails.ruleDeleted"), + }); + }, [adoptRules, apiBase, runMutation, t]); + + const exportRules = useCallback(() => { + if (pendingRef.current) return; + pendingRef.current = true; + setPending(true); + setToast(null); + void fetchGuardrailsExport(apiBase, t("guardrails.exportFailed")) + .then(blob => { + const objectUrl = URL.createObjectURL(blob); + try { + const anchor = document.createElement("a"); + anchor.href = objectUrl; + anchor.download = "opencodex-guardrails.json"; + anchor.hidden = true; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + } finally { + URL.revokeObjectURL(objectUrl); + } + setToast({ tone: "ok", text: t("guardrails.exported") }); + }) + .catch(error => { + setToast({ tone: "err", text: errorText(error, t("guardrails.exportFailed")) }); + }) + .finally(() => { + pendingRef.current = false; + setPending(false); + }); + }, [apiBase, t]); + + const requestImport = useCallback(( + bundle: unknown, + mode: "merge" | "replace", + returnFocus: HTMLElement | null, + ) => { + const current = rulesRef.current; + if (!current || pendingRef.current) return; + pendingRef.current = true; + setPending(true); + setToast(null); + void previewGuardrailsBundle(apiBase, bundle, mode, current.revision, t("guardrails.importFailed")) + .then(preview => setImportPreview({ + bundle, + preview, + returnFocus, + revision: current.revision, + })) + .catch(error => { + if (isGuardrailsRevisionConflict(error)) refreshAll(); + setToast({ tone: "err", text: errorText(error, t("guardrails.importFailed")) }); + }) + .finally(() => { + pendingRef.current = false; + setPending(false); + }); + }, [apiBase, refreshAll, t]); + + const closeImportPreview = useCallback(() => { + const focus = importPreview?.returnFocus; + setImportPreview(null); + window.requestAnimationFrame(() => focus?.focus({ preventScroll: true })); + }, [importPreview]); + + const applyImportPreview = useCallback(() => { + if (!importPreview || importPreview.preview.conflicts.length > 0) return; + const apply = async () => { + try { + await runMutation(async () => { + const next = await importGuardrailsBundle( + apiBase, + importPreview.bundle, + importPreview.preview.mode, + false, + importPreview.revision, + t("guardrails.importFailed"), + ); + adoptSettings(next); + }, "guardrails.imported"); + closeImportPreview(); + } catch (error) { + if (isGuardrailsRevisionConflict(error)) { + setConsequence(null); + closeImportPreview(); + } + throw error; + } + }; + if (importPreview.preview.mode === "replace") { + setConsequence({ + kind: importPreview.preview.securityDiff.requiresReview + ? "replaceImportWeakening" + : "replaceImport", + run: apply, + returnFocus: importPreview.returnFocus, + }); + return; + } + void apply().catch(() => undefined); + }, [adoptSettings, apiBase, closeImportPreview, importPreview, runMutation, t]); + + const meta = useMemo(() => ({ + rules: rules ? String(rules.rules.length) : undefined, + activity: activityResource.state.data + ? String(activityResource.state.data.activity.events.length) + : undefined, + }), [activityResource.state.data, rules]); + return ( +
+
+

{t("guardrails.title")}

+ {overview && } +
+

{t("guardrails.subtitle")}

+ {toast && + + {toast.text} + + + } + + + {mounted.has("overview") && ( + + )} + {mounted.has("rules") && ( + + )} + {mounted.has("tester") && ( + + )} + {mounted.has("activity") && ( + + )} + {mounted.has("settings") && ( + + )} + + {consequence && ( + { + await consequence.run(); + closeConsequence(); + }} + /> + )} +
+ ); +} diff --git a/gui/src/pages/guardrails/activity-panel.tsx b/gui/src/pages/guardrails/activity-panel.tsx new file mode 100644 index 0000000000..94ec3648f7 --- /dev/null +++ b/gui/src/pages/guardrails/activity-panel.tsx @@ -0,0 +1,158 @@ +import { EmptyState, Notice, Select } from "../../ui"; +import { useT } from "../../i18n/shared"; +import { GUARDRAILS_DATA_TYPES, GUARDRAILS_DATA_TYPE_KEYS } from "./constants"; +import type { + GuardrailsActivity, + GuardrailsActivityFilters, + GuardrailsMode, + GuardrailsResult, + GuardrailsSurface, +} from "./types"; + +export function GuardrailsActivityPanel({ + activity, + filters, + refreshing, + lastRefreshAt, + onFiltersChange, + onRefresh, +}: { + activity: GuardrailsActivity; + filters: GuardrailsActivityFilters; + refreshing: boolean; + lastRefreshAt: number | null; + onFiltersChange: (filters: GuardrailsActivityFilters) => void; + onRefresh: () => void; +}) { + const t = useT(); + const resultOptions: Array<{ value: GuardrailsResult | ""; label: string }> = [ + { value: "", label: t("guardrails.allResults") }, + { value: "scanned", label: t("guardrails.result.scanned") }, + { value: "masked", label: t("guardrails.result.masked") }, + { value: "detected", label: t("guardrails.result.detected") }, + { value: "blocked", label: t("guardrails.result.blocked") }, + { value: "passthrough", label: t("guardrails.result.passthrough") }, + { value: "demask_warning", label: t("guardrails.result.demaskWarning") }, + { value: "tool_argument_restore_skipped", label: t("guardrails.result.toolSkipped") }, + ]; + const surfaceOptions: Array<{ value: GuardrailsSurface | ""; label: string }> = [ + { value: "", label: t("guardrails.allSurfaces") }, + { value: "responses", label: t("api.protocolResponses") }, + { value: "chat", label: t("api.protocolChatCompletions") }, + { value: "messages", label: t("api.protocolMessages") }, + { value: "compact", label: t("guardrails.surfaceCompact") }, + ]; + const events = activity.events; + const updateFilter = ( + key: K, + value: GuardrailsActivityFilters[K], + ) => onFiltersChange({ ...filters, [key]: value }); + + return ( +
+ {t("guardrails.activityPrivacy")} +
+ updateFilter("mode", value as GuardrailsMode | "")} + options={[ + { value: "", label: t("guardrails.allModes") }, + { value: "enforce", label: t("guardrails.modeEnforce") }, + { value: "detect", label: t("guardrails.modeDetect") }, + ]} + /> + updateFilter( + "category", + value === "" ? "" : Number(value) as GuardrailsActivityFilters["category"], + )} + options={[ + { value: "", label: t("guardrails.allCategories") }, + ...GUARDRAILS_DATA_TYPES.map(value => ({ + value: String(value), + label: t(GUARDRAILS_DATA_TYPE_KEYS[value]), + })), + ]} + /> + +
+
+ {t("guardrails.activityEvents", { count: activity.filteredSummary.eventCount })} + {t("guardrails.activityFindings", { count: activity.filteredSummary.findingCount })} + {t("guardrails.activityAverageLatency", { + value: activity.filteredSummary.averageLatencyMs.toFixed(2), + })} + {t("guardrails.activityTopRule", { + rule: activity.filteredSummary.topRules[0]?.id ?? "—", + })} +
+
+ {t("guardrails.retentionEvents", { current: activity.retention.currentEvents, max: activity.retention.maxEvents })} + {t("guardrails.retentionEvicted", { count: activity.retention.evictedEvents })} + {t("guardrails.showingEvents", { shown: events.length, total: activity.totalMatching })} + {t("guardrails.oldestEvent", { + date: activity.retention.oldestAt === null ? "—" : new Date(activity.retention.oldestAt).toLocaleString(), + })} + {t("guardrails.lastRefresh", { + date: lastRefreshAt === null ? "—" : new Date(lastRefreshAt).toLocaleString(), + })} +
+ {events.length === 0 + ? {t("guardrails.noActivityHint")} + : ( + <> +

{t("guardrails.tableScrollHint")}

+
+ + + + + + + + + + + {events.map(event => ( + + + + + + + + + + ))} +
{t("guardrails.time")}{t("guardrails.surface")}{t("guardrails.result")}{t("guardrails.count")}{t("guardrails.rules")}{t("guardrails.generation")}{t("guardrails.latency")}
{new Date(event.timestamp).toLocaleString()}{surfaceOptions.find(option => option.value === event.surface)?.label ?? event.surface} + + {resultOptions.find(option => option.value === event.result)?.label ?? event.result} + + {event.count}{event.ruleIds.join(", ") || "—"}{event.registryGeneration}{event.latencyMs.toFixed(2)} ms
+
+ + )} +
+ ); +} diff --git a/gui/src/pages/guardrails/consequence-copy.ts b/gui/src/pages/guardrails/consequence-copy.ts new file mode 100644 index 0000000000..e87838ac32 --- /dev/null +++ b/gui/src/pages/guardrails/consequence-copy.ts @@ -0,0 +1,89 @@ +import type { ConsequenceCopy } from "../integrations/ConsequenceDialog"; + +export type GuardrailsConsequenceKind = + | "disable" + | "detect" + | "passthrough" + | "disableRule" + | "disableCategory" + | "limitProvider" + | "bulkDisable" + | "deleteRule" + | "replaceImport" + | "replaceImportWeakening"; + +export const GUARDRAILS_CONSEQUENCE_COPY: Record< + GuardrailsConsequenceKind, + ConsequenceCopy +> = { + disable: { + titleKey: "guardrails.dialog.disable.title", + changesKey: "guardrails.dialog.disable.changes", + breakageKey: "guardrails.dialog.disable.breakage", + undoKey: "guardrails.dialog.disable.undo", + confirmKey: "guardrails.dialog.disable.confirm", + }, + detect: { + titleKey: "guardrails.dialog.detect.title", + changesKey: "guardrails.dialog.detect.changes", + breakageKey: "guardrails.dialog.detect.breakage", + undoKey: "guardrails.dialog.detect.undo", + confirmKey: "guardrails.dialog.detect.confirm", + }, + passthrough: { + titleKey: "guardrails.dialog.passthrough.title", + changesKey: "guardrails.dialog.passthrough.changes", + breakageKey: "guardrails.dialog.passthrough.breakage", + undoKey: "guardrails.dialog.passthrough.undo", + confirmKey: "guardrails.dialog.passthrough.confirm", + }, + disableRule: { + titleKey: "guardrails.dialog.ruleDisable.title", + changesKey: "guardrails.dialog.ruleDisable.changes", + breakageKey: "guardrails.dialog.ruleDisable.breakage", + undoKey: "guardrails.dialog.ruleDisable.undo", + confirmKey: "guardrails.dialog.ruleDisable.confirm", + }, + disableCategory: { + titleKey: "guardrails.dialog.categoryDisable.title", + changesKey: "guardrails.dialog.categoryDisable.changes", + breakageKey: "guardrails.dialog.categoryDisable.breakage", + undoKey: "guardrails.dialog.categoryDisable.undo", + confirmKey: "guardrails.dialog.categoryDisable.confirm", + }, + limitProvider: { + titleKey: "guardrails.dialog.providerScope.title", + changesKey: "guardrails.dialog.providerScope.changes", + breakageKey: "guardrails.dialog.providerScope.breakage", + undoKey: "guardrails.dialog.providerScope.undo", + confirmKey: "guardrails.dialog.providerScope.confirm", + }, + bulkDisable: { + titleKey: "guardrails.dialog.bulkDisable.title", + changesKey: "guardrails.dialog.bulkDisable.changes", + breakageKey: "guardrails.dialog.bulkDisable.breakage", + undoKey: "guardrails.dialog.bulkDisable.undo", + confirmKey: "guardrails.dialog.bulkDisable.confirm", + }, + deleteRule: { + titleKey: "guardrails.dialog.delete.title", + changesKey: "guardrails.dialog.delete.changes", + breakageKey: "guardrails.dialog.delete.breakage", + undoKey: "guardrails.dialog.delete.undo", + confirmKey: "guardrails.dialog.delete.confirm", + }, + replaceImport: { + titleKey: "guardrails.dialog.import.title", + changesKey: "guardrails.dialog.import.changes", + breakageKey: "guardrails.dialog.import.breakage", + undoKey: "guardrails.dialog.import.undo", + confirmKey: "guardrails.dialog.import.confirm", + }, + replaceImportWeakening: { + titleKey: "guardrails.dialog.importWeakening.title", + changesKey: "guardrails.dialog.importWeakening.changes", + breakageKey: "guardrails.dialog.importWeakening.breakage", + undoKey: "guardrails.dialog.importWeakening.undo", + confirmKey: "guardrails.dialog.importWeakening.confirm", + }, +}; diff --git a/gui/src/pages/guardrails/constants.ts b/gui/src/pages/guardrails/constants.ts new file mode 100644 index 0000000000..995e433b8c --- /dev/null +++ b/gui/src/pages/guardrails/constants.ts @@ -0,0 +1,34 @@ +import type { TKey } from "../../i18n/shared"; +import type { GuardrailsDataType } from "./types"; + +export const GUARDRAILS_DATA_TYPES: readonly GuardrailsDataType[] = [1, 2, 3, 4, 5, 6]; +export const GUARDRAILS_IMPORT_MAX_BYTES = 4 * 1024 * 1024; +export const GUARDRAILS_MAX_CUSTOM_RULE_MIN_LENGTH = 128 * 1024; + +export const GUARDRAILS_VALIDATORS = [ + "luhn", + "snils", + "inn_person", + "inn_org", + "ogrn", + "ogrnip", + "iban_mod97", + "email_ascii", + "payment_card", + "payment_card_no_luhn", + "entropy", + "banlist", + "ip_v4", + "ip_v6", + "ip_public", + "ip_private", +] as const; + +export const GUARDRAILS_DATA_TYPE_KEYS: Record = { + 1: "guardrails.dataType.credentials", + 2: "guardrails.dataType.apiKeys", + 3: "guardrails.dataType.accessTokens", + 4: "guardrails.dataType.ipAddresses", + 5: "guardrails.dataType.personal", + 6: "guardrails.dataType.custom", +}; diff --git a/gui/src/pages/guardrails/guardrails-api.ts b/gui/src/pages/guardrails/guardrails-api.ts new file mode 100644 index 0000000000..8e900ffeaa --- /dev/null +++ b/gui/src/pages/guardrails/guardrails-api.ts @@ -0,0 +1,261 @@ +import { readJsonOrThrow } from "../../fetch-json"; +import type { + GuardrailsActivity, + GuardrailsActivityFilters, + GuardrailsCustomRule, + GuardrailsDataType, + GuardrailsOverview, + GuardrailsImportPreview, + GuardrailsRules, + GuardrailsSettingsPatch, + GuardrailsSettings, + GuardrailsTesterResult, +} from "./types"; + +function guardrailsSettingsMutationPayload( + patch: GuardrailsSettingsPatch, +): GuardrailsSettingsPatch { + return { + ...(patch.enabled !== undefined ? { enabled: patch.enabled } : {}), + ...(patch.mode !== undefined ? { mode: patch.mode } : {}), + ...(patch.failurePolicy !== undefined ? { failurePolicy: patch.failurePolicy } : {}), + ...(patch.providerScope !== undefined + ? { providerScope: patch.providerScope } + : {}), + ...(patch.enabledDataTypes !== undefined + ? { enabledDataTypes: patch.enabledDataTypes } + : {}), + ...(patch.disabledBuiltinRuleIds !== undefined + ? { disabledBuiltinRuleIds: patch.disabledBuiltinRuleIds } + : {}), + ...(patch.keywordPrefilterEnabled !== undefined + ? { keywordPrefilterEnabled: patch.keywordPrefilterEnabled } + : {}), + }; +} + +export class GuardrailsApiError extends Error { + readonly code?: string; + readonly status: number; + + constructor( + message: string, + status: number, + code?: string, + ) { + super(message); + this.name = "GuardrailsApiError"; + this.code = code; + this.status = status; + } +} + +const LOCALIZED_GUARDRAILS_ERROR_CODES = new Set([ + "config_mutation_busy", + "guardrails_assets_invalid", + "guardrails_capacity_exceeded", + "guardrails_revision_conflict", + "guardrails_revision_invalid", + "guardrails_revision_required", + "guardrails_rule_duplicate", + "guardrails_rule_invalid", + "guardrails_rule_not_found", +]); + +async function guardrailsMutationError(response: Response, fallback: string): Promise { + let serverMessage: string | undefined; + let code: string | undefined; + try { + const body = await response.json() as { code?: unknown; error?: unknown; message?: unknown }; + if (typeof body.error === "string" && body.error) serverMessage = body.error; + else if (typeof body.message === "string" && body.message) serverMessage = body.message; + if (typeof body.code === "string" && body.code) code = body.code; + } catch { + // Non-JSON errors keep the localized fallback. + } + const knownCode = code !== undefined + && (LOCALIZED_GUARDRAILS_ERROR_CODES.has(code) || code.startsWith("config_")); + const message = knownCode ? fallback : (serverMessage ?? fallback); + return new GuardrailsApiError(message, response.status, code); +} + +async function getJson(url: string, signal: AbortSignal, error: string): Promise { + const response = await fetch(url, { signal }); + const parsed = await readJsonOrThrow(response, error); + if (!parsed) throw new Error(error); + return parsed; +} + +export function fetchGuardrailsOverview(apiBase: string, signal: AbortSignal, error: string) { + return getJson(`${apiBase}/api/guardrails`, signal, error); +} + +export function fetchGuardrailsRules(apiBase: string, signal: AbortSignal, error: string) { + return getJson(`${apiBase}/api/guardrails/rules`, signal, error); +} + +export async function fetchGuardrailsExport(apiBase: string, error: string): Promise { + const response = await fetch(`${apiBase}/api/guardrails/export`); + if (!response.ok) throw await guardrailsMutationError(response, error); + return response.blob(); +} + +export function fetchGuardrailsActivity( + apiBase: string, + signal: AbortSignal, + error: string, + filters: GuardrailsActivityFilters, +) { + const query = new URLSearchParams({ limit: "100" }); + if (filters.category !== "") query.set("category", String(filters.category)); + if (filters.mode) query.set("mode", filters.mode); + if (filters.result) query.set("result", filters.result); + if (filters.surface) query.set("surface", filters.surface); + return getJson(`${apiBase}/api/guardrails/activity?${query}`, signal, error); +} + +async function mutate( + url: string, + method: "POST" | "PUT" | "DELETE", + body: unknown, + revision: string | undefined, + error: string, +): Promise { + const response = await fetch(url, { + method, + headers: { + ...(body === undefined ? {} : { "Content-Type": "application/json" }), + ...(revision ? { "If-Match": `"${revision}"` } : {}), + }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }); + if (!response.ok) throw await guardrailsMutationError(response, error); + const parsed = await readJsonOrThrow(response, error); + if (!parsed) throw new Error(error); + return parsed; +} + +export function updateGuardrailsSettings( + apiBase: string, + patch: GuardrailsSettingsPatch, + revision: string, + error: string, +) { + return mutate( + `${apiBase}/api/guardrails/settings`, + "PUT", + guardrailsSettingsMutationPayload(patch), + revision, + error, + ); +} + +export function toggleGuardrailsRule( + apiBase: string, + ruleId: string, + enabled: boolean, + revision: string, + error: string, +) { + return mutate( + `${apiBase}/api/guardrails/rules/${encodeURIComponent(ruleId)}/enabled`, + "PUT", + { enabled }, + revision, + error, + ); +} + +export function saveGuardrailsCustomRule( + apiBase: string, + rule: GuardrailsCustomRule, + editingRuleId: string | null, + revision: string, + error: string, +) { + return mutate( + editingRuleId + ? `${apiBase}/api/guardrails/rules/${encodeURIComponent(editingRuleId)}` + : `${apiBase}/api/guardrails/rules`, + editingRuleId ? "PUT" : "POST", + rule, + revision, + error, + ); +} + +export function deleteGuardrailsCustomRule( + apiBase: string, + ruleId: string, + revision: string, + error: string, +) { + return mutate( + `${apiBase}/api/guardrails/rules/${encodeURIComponent(ruleId)}`, + "DELETE", + undefined, + revision, + error, + ); +} + +export function testGuardrailsText( + apiBase: string, + text: string, + signal: AbortSignal, + error: string, + settings?: { + enabled: true; + enabledDataTypes: GuardrailsDataType[]; + keywordPrefilterEnabled: boolean; + }, + draftRule?: GuardrailsCustomRule, +) { + return fetch(`${apiBase}/api/guardrails/test`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + text, + ...(settings ? { settings } : {}), + ...(draftRule ? { draftRule } : {}), + }), + signal, + }).then(async response => { + const parsed = await readJsonOrThrow(response, error); + if (!parsed) throw new Error(error); + return parsed; + }); +} + +export async function importGuardrailsBundle( + apiBase: string, + bundle: unknown, + mode: "merge" | "replace", + dryRun: boolean, + revision: string, + error: string, +): Promise { + return mutate( + `${apiBase}/api/guardrails/import`, + "POST", + { mode, dryRun, bundle }, + revision, + error, + ); +} + +export async function previewGuardrailsBundle( + apiBase: string, + bundle: unknown, + mode: "merge" | "replace", + revision: string, + error: string, +): Promise { + return mutate( + `${apiBase}/api/guardrails/import`, + "POST", + { mode, dryRun: true, bundle }, + revision, + error, + ); +} diff --git a/gui/src/pages/guardrails/guardrails-tab-strip.tsx b/gui/src/pages/guardrails/guardrails-tab-strip.tsx new file mode 100644 index 0000000000..071e1b2af8 --- /dev/null +++ b/gui/src/pages/guardrails/guardrails-tab-strip.tsx @@ -0,0 +1,75 @@ +import { useRef, type KeyboardEvent } from "react"; +import { useT, type TKey } from "../../i18n/shared"; +import { + GUARDRAILS_TABS, + guardrailsPanelDomId, + guardrailsTabDomId, + type GuardrailsTab, +} from "./guardrails-tab"; + +const LABELS: Record = { + overview: "guardrails.tab.overview", + rules: "guardrails.tab.rules", + tester: "guardrails.tab.tester", + activity: "guardrails.tab.activity", + settings: "guardrails.tab.settings", +}; + +export function GuardrailsTabStrip({ + tab, + onSelect, + meta, +}: { + tab: GuardrailsTab; + onSelect: (tab: GuardrailsTab) => void; + meta?: Partial>; +}) { + const t = useT(); + const refs = useRef | null>(null); + if (refs.current === null) refs.current = new Map(); + + const move = (next: GuardrailsTab) => { + onSelect(next); + window.requestAnimationFrame(() => refs.current?.get(next)?.focus({ preventScroll: true })); + }; + const onKeyDown = (event: KeyboardEvent) => { + const current = GUARDRAILS_TABS.indexOf(tab); + let next: number | null = null; + if (event.key === "ArrowLeft") next = (current - 1 + GUARDRAILS_TABS.length) % GUARDRAILS_TABS.length; + else if (event.key === "ArrowRight") next = (current + 1) % GUARDRAILS_TABS.length; + else if (event.key === "Home") next = 0; + else if (event.key === "End") next = GUARDRAILS_TABS.length - 1; + if (next === null) return; + event.preventDefault(); + move(GUARDRAILS_TABS[next]!); + }; + + return ( +
+ {GUARDRAILS_TABS.map(candidate => { + const active = candidate === tab; + return ( + + ); + })} +
+ ); +} diff --git a/gui/src/pages/guardrails/guardrails-tab.ts b/gui/src/pages/guardrails/guardrails-tab.ts new file mode 100644 index 0000000000..6f73a02ce6 --- /dev/null +++ b/gui/src/pages/guardrails/guardrails-tab.ts @@ -0,0 +1,32 @@ +import { navigateHash, normalizeHashPath } from "../../hash-routing"; + +export type GuardrailsTab = "overview" | "rules" | "tester" | "activity" | "settings"; + +export const GUARDRAILS_TABS: readonly GuardrailsTab[] = [ + "overview", + "rules", + "tester", + "activity", + "settings", +]; + +export function guardrailsTabHash(tab: GuardrailsTab): string { + return tab === "overview" ? "guardrails" : `guardrails/${tab}`; +} +export function readGuardrailsTab(hash = window.location.hash): GuardrailsTab { + const path = normalizeHashPath(hash); + const candidate = path.startsWith("guardrails/") ? path.slice("guardrails/".length) : ""; + return GUARDRAILS_TABS.includes(candidate as GuardrailsTab) ? candidate as GuardrailsTab : "overview"; +} + +export function selectGuardrailsTab(tab: GuardrailsTab): void { + navigateHash(guardrailsTabHash(tab)); +} + +export function guardrailsTabDomId(tab: GuardrailsTab): string { + return `guardrails-tab-${tab}`; +} + +export function guardrailsPanelDomId(tab: GuardrailsTab): string { + return `guardrails-panel-${tab}`; +} diff --git a/gui/src/pages/guardrails/overview-panel.tsx b/gui/src/pages/guardrails/overview-panel.tsx new file mode 100644 index 0000000000..8a94e0d288 --- /dev/null +++ b/gui/src/pages/guardrails/overview-panel.tsx @@ -0,0 +1,127 @@ +import { IconLock } from "../../icons"; +import { EmptyState, Notice, Switch } from "../../ui"; +import { useT } from "../../i18n/shared"; +import { GUARDRAILS_DATA_TYPE_KEYS } from "./constants"; +import type { GuardrailsOverview, GuardrailsSettingsPatch } from "./types"; + +const RESULT_KEYS = { + scanned: "guardrails.result.scanned", + masked: "guardrails.result.masked", + detected: "guardrails.result.detected", + blocked: "guardrails.result.blocked", + passthrough: "guardrails.result.passthrough", + demask_warning: "guardrails.result.demaskWarning", + tool_argument_restore_skipped: "guardrails.result.toolSkipped", +} as const; + +export function GuardrailsOverviewPanel({ + data, + pending, + onSettings, +}: { + data: GuardrailsOverview; + pending: boolean; + onSettings: ( + patch: GuardrailsSettingsPatch, + consequence?: "disable" | "detect" | "passthrough", + ) => void; +}) { + const t = useT(); + const counters = data.overview.counters; + const lastPassthroughAt = data.overview.lastPassthroughAt; + const registryStatusKey = data.registry.status === "ready" + ? "guardrails.registryReady" + : data.registry.status === "failed" + ? "guardrails.registryFailed" + : "guardrails.registryDisabled"; + return ( +
+ {(data.mode === "detect" || data.failurePolicy === "passthrough") && ( + + {data.mode === "detect" ? t("guardrails.detectWarning") : t("guardrails.passthroughWarning")} + + )} +
+
+ {t("guardrails.enabled")} +

{t("guardrails.enabledHint")}

+
+ + {t(registryStatusKey)} + + {t(data.mode === "enforce" ? "guardrails.modeEnforce" : "guardrails.modeDetect")} + {t("guardrails.effectiveRules", { count: data.registry.effectiveRuleCount })} + {data.registry.generation !== null && ( + {t("guardrails.registryGeneration", { generation: data.registry.generation })} + )} +
+
+ onSettings( + { enabled: !data.enabled }, + data.enabled ? "disable" : undefined, + )} + /> +
+
+ {([ + ["guardrails.metric.scanned", counters.scanned], + ["guardrails.metric.masked", counters.masked], + ["guardrails.metric.detected", counters.detected], + ["guardrails.metric.errors", counters.blocked + counters.passthrough + counters.demaskWarning], + ] as const).map(([key, value]) => ( +
+ {t(key)}{value.toLocaleString()} +
+ ))} +
+ {lastPassthroughAt !== null && ( + + {t("guardrails.lastPassthrough", { date: new Date(lastPassthroughAt).toLocaleString() })} + + )} +
+
+
{t("guardrails.topRules")}
+ {data.overview.topRules.length === 0 + ? + :
    {data.overview.topRules.map(row => ( +
  • {row.id}{row.count}
  • + ))}
} +
+
+
{t("guardrails.topCategories")}
+ {data.overview.topCategories.length === 0 + ? + :
    {data.overview.topCategories.map(row => ( +
  • {t(GUARDRAILS_DATA_TYPE_KEYS[row.id])}{row.count}
  • + ))}
} +
+
+ {data.overview.recentEvents.length === 0 + ? ( + } title={t("guardrails.noActivity")}> + {t("guardrails.noActivityHint")} + + ) + : ( +
+
{t("guardrails.recentActivity")}
+
    {data.overview.recentEvents.map(event => ( +
  • + + {new Date(event.timestamp).toLocaleString()} · {t(RESULT_KEYS[event.result])} + {" · "} + {t("guardrails.registryGeneration", { generation: event.registryGeneration })} + + {event.count} +
  • + ))}
+
+ )} +
+ ); +} diff --git a/gui/src/pages/guardrails/protection-status.ts b/gui/src/pages/guardrails/protection-status.ts new file mode 100644 index 0000000000..d20c28e447 --- /dev/null +++ b/gui/src/pages/guardrails/protection-status.ts @@ -0,0 +1,40 @@ +import { GUARDRAILS_DATA_TYPES } from "./constants"; +import type { + GuardrailsOverview, + GuardrailsTrafficProtection, +} from "./types"; + +export function guardrailsTrafficProtectionStatus( + overview: GuardrailsOverview | undefined, +): GuardrailsTrafficProtection { + if (!overview) return "unknown"; + if (!overview.configuredEnabled && !overview.enabled) return "disabled"; + if ( + !overview.enabled + || overview.activation.status !== "active" + || overview.registry.status !== "ready" + ) { + return "unavailable"; + } + if (overview.registry.effectiveRuleCount <= 0) return "no-rules"; + if ( + overview.providerScope.mode === "selected" + && !overview.providerScope.providerIds.some(id => + overview.providerOptions.some(provider => + provider.id === id + && provider.configured + && !provider.disabled)) + ) { + return "no-provider-coverage"; + } + if (overview.mode === "detect") return "detect"; + if ( + overview.enabledDataTypes.length < GUARDRAILS_DATA_TYPES.length + || overview.disabledBuiltinRuleIds.length > 0 + || overview.failurePolicy === "passthrough" + || overview.providerScope.mode === "selected" + ) { + return "reduced"; + } + return "enforce"; +} diff --git a/gui/src/pages/guardrails/rule-form-utils.ts b/gui/src/pages/guardrails/rule-form-utils.ts new file mode 100644 index 0000000000..1876bc4869 --- /dev/null +++ b/gui/src/pages/guardrails/rule-form-utils.ts @@ -0,0 +1,8 @@ +export function parseGuardrailsCaptureGroups(value: string): number[] | null { + if (value.trim() === "") return []; + const tokens = value.split(",").map(token => token.trim()); + const groups = tokens.map(token => Number(token)); + return tokens.some((token, index) => token === "" || !Number.isInteger(groups[index]) || groups[index]! <= 0) + ? null + : groups; +} diff --git a/gui/src/pages/guardrails/rules-panel.tsx b/gui/src/pages/guardrails/rules-panel.tsx new file mode 100644 index 0000000000..7b051331a5 --- /dev/null +++ b/gui/src/pages/guardrails/rules-panel.tsx @@ -0,0 +1,520 @@ +import { Fragment, useCallback, useMemo, useRef, useState } from "react"; +import { EmptyState, Notice, Select, Switch } from "../../ui"; +import { IconPlus, IconTrash } from "../../icons"; +import { useT } from "../../i18n/shared"; +import { + GUARDRAILS_DATA_TYPES, + GUARDRAILS_DATA_TYPE_KEYS, + GUARDRAILS_IMPORT_MAX_BYTES, + GUARDRAILS_MAX_CUSTOM_RULE_MIN_LENGTH, + GUARDRAILS_VALIDATORS, +} from "./constants"; +import type { + GuardrailsCustomRule, + GuardrailsDataType, + GuardrailsImportPreview, + GuardrailsRules, +} from "./types"; +import { parseGuardrailsCaptureGroups } from "./rule-form-utils"; + +const PAGE_SIZE = 50; + +function blankRule(): GuardrailsCustomRule { + return { + ruleId: "", + name: "", + dataType: 6, + group: "CUSTOM", + groupPriority: 0, + displayName: "", + description: "", + regex: "", + keywords: [], + banlist: [], + validators: [], + masking: { captureGroups: [], placeholderType: "CUSTOM_SECRET" }, + }; +} + +export function GuardrailsRulesPanel({ + data, + pending, + onToggle, + onBulk, + onSave, + onTestDraft, + onDelete, + onExport, + onImport, + importPreview, + onApplyImport, + onCancelImport, + onImportError, +}: { + data: GuardrailsRules; + pending: boolean; + onToggle: (ruleId: string, enabled: boolean) => void; + onBulk: (ruleIds: string[], enabled: boolean) => void; + onSave: (rule: GuardrailsCustomRule, editingId: string | null) => Promise; + onTestDraft: (rule: GuardrailsCustomRule) => void; + onDelete: (rule: GuardrailsCustomRule) => void; + onExport: () => void; + onImport: (bundle: unknown, mode: "merge" | "replace", returnFocus: HTMLElement | null) => void; + importPreview: GuardrailsImportPreview | null; + onApplyImport: () => void; + onCancelImport: () => void; + onImportError: (error: unknown) => void; +}) { + const t = useT(); + const [query, setQuery] = useState(""); + const [source, setSource] = useState(""); + const [status, setStatus] = useState(""); + const [dataType, setDataType] = useState(""); + const [page, setPage] = useState(0); + const [editingId, setEditingId] = useState(null); + const [form, setForm] = useState(blankRule); + const [captureGroupsText, setCaptureGroupsText] = useState(""); + const [captureGroupsError, setCaptureGroupsError] = useState(false); + const [importMode, setImportMode] = useState<"merge" | "replace">("merge"); + const fileRef = useRef(null); + const formRef = useRef(null); + const importButtonRef = useRef(null); + const builtinLabel = useCallback((rule: GuardrailsRules["rules"][number]) => + t(rule.group === "CREDENTIAL_URLS" + ? "guardrails.groupCredentialUrls" + : GUARDRAILS_DATA_TYPE_KEYS[rule.dataType]), [t]); + const ruleLabel = useCallback((rule: GuardrailsRules["rules"][number]) => rule.custom + ? rule.displayName + : builtinLabel(rule), [builtinLabel]); + const groupLabel = (rule: GuardrailsRules["rules"][number]) => rule.custom + ? rule.group + : builtinLabel(rule); + + const filtered = useMemo(() => { + const needle = query.trim().toLowerCase(); + return data.rules + .filter(rule => !needle + || `${rule.ruleId} ${ruleLabel(rule)} ${rule.group}`.toLowerCase().includes(needle)) + .filter(rule => !source || rule.source === source) + .filter(rule => !status || (status === "enabled" ? rule.enabled : !rule.enabled)) + .filter(rule => !dataType || rule.dataType === Number(dataType)); + }, [data.rules, dataType, query, ruleLabel, source, status]); + const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); + const visiblePage = Math.min(page, pageCount - 1); + const visible = filtered.slice(visiblePage * PAGE_SIZE, (visiblePage + 1) * PAGE_SIZE); + const bulkIds = filtered.filter(rule => !rule.custom).map(rule => rule.ruleId); + const customById = new Map(data.customRules.map(rule => [rule.ruleId, rule])); + const securityDiff = importPreview?.securityDiff; + const onOff = (value: boolean) => t(value ? "guardrails.valueOn" : "guardrails.valueOff"); + const providerScopeLabel = (scope: NonNullable["providerScope"]["before"]) => + scope.mode === "all" + ? t("guardrails.providerScopeAllValue") + : `${t("guardrails.providerScopeSelectedValue", { + count: scope.providerIds.length, + })}: ${scope.providerIds.join(", ")}`; + const securityChanges = securityDiff + ? [ + { + key: "enabled", + changed: securityDiff.enabled.changed, + weakening: securityDiff.enabled.weakening, + reviewRequired: false, + label: t("guardrails.importSecurityEnabled", { + before: onOff(securityDiff.enabled.before), + after: onOff(securityDiff.enabled.after), + }), + }, + { + key: "mode", + changed: securityDiff.mode.changed, + weakening: securityDiff.mode.weakening, + reviewRequired: false, + label: t("guardrails.importSecurityMode", { + before: t(securityDiff.mode.before === "enforce" ? "guardrails.modeEnforce" : "guardrails.modeDetect"), + after: t(securityDiff.mode.after === "enforce" ? "guardrails.modeEnforce" : "guardrails.modeDetect"), + }), + }, + { + key: "failurePolicy", + changed: securityDiff.failurePolicy.changed, + weakening: securityDiff.failurePolicy.weakening, + reviewRequired: false, + label: t("guardrails.importSecurityFailure", { + before: t(securityDiff.failurePolicy.before === "block" ? "guardrails.failureBlock" : "guardrails.failurePassthrough"), + after: t(securityDiff.failurePolicy.after === "block" ? "guardrails.failureBlock" : "guardrails.failurePassthrough"), + }), + }, + { + key: "providerScope", + changed: securityDiff.providerScope.changed, + weakening: securityDiff.providerScope.weakening, + reviewRequired: false, + label: t("guardrails.importSecurityProviderScope", { + before: providerScopeLabel(securityDiff.providerScope.before), + after: providerScopeLabel(securityDiff.providerScope.after), + }), + }, + { + key: "enabledDataTypes", + changed: securityDiff.enabledDataTypes.changed, + weakening: securityDiff.enabledDataTypes.weakening, + reviewRequired: false, + label: t("guardrails.importSecurityDataTypes", { + before: securityDiff.enabledDataTypes.before.length, + after: securityDiff.enabledDataTypes.after.length, + }), + }, + { + key: "disabledBuiltinRules", + changed: securityDiff.disabledBuiltinRules.changed, + weakening: securityDiff.disabledBuiltinRules.weakening, + reviewRequired: false, + label: t("guardrails.importSecurityDisabledRules", { + before: securityDiff.disabledBuiltinRules.beforeCount, + after: securityDiff.disabledBuiltinRules.afterCount, + disabled: securityDiff.disabledBuiltinRules.newlyDisabledCount, + reenabled: securityDiff.disabledBuiltinRules.reenabledCount, + }), + }, + { + key: "customRules", + changed: securityDiff.customRules.changed, + weakening: securityDiff.customRules.weakening, + reviewRequired: securityDiff.customRules.requiresReview, + label: t("guardrails.importSecurityCustomRules", { + before: securityDiff.customRules.beforeCount, + after: securityDiff.customRules.afterCount, + removed: securityDiff.customRules.removedCount, + changed: securityDiff.customRules.changedDefinitionCount, + }), + }, + { + key: "keywordPrefilterEnabled", + changed: securityDiff.keywordPrefilterEnabled.changed, + weakening: securityDiff.keywordPrefilterEnabled.weakening, + reviewRequired: false, + label: t("guardrails.importSecurityPrefilter", { + before: onOff(securityDiff.keywordPrefilterEnabled.before), + after: onOff(securityDiff.keywordPrefilterEnabled.after), + }), + }, + ].filter(change => change.changed) + : []; + + const update = (key: K, value: GuardrailsCustomRule[K]) => { + setForm(current => ({ ...current, [key]: value })); + }; + const resetForm = () => { + setEditingId(null); + setForm(blankRule()); + setCaptureGroupsText(""); + setCaptureGroupsError(false); + }; + const toggleValidator = (validator: string) => { + setForm(current => { + const enabled = current.validators.includes(validator); + return { + ...current, + validators: enabled + ? current.validators.filter(value => value !== validator) + : [...current.validators, validator], + ...(validator === "entropy" && enabled ? { entropy: undefined } : {}), + ...(validator === "banlist" && enabled ? { banlist: [] } : {}), + }; + }); + }; + const resolvedDraftRule = (): GuardrailsCustomRule | null => { + const captureGroups = parseGuardrailsCaptureGroups(captureGroupsText); + if (captureGroups === null) { + setCaptureGroupsError(true); + return null; + } + setCaptureGroupsError(false); + return { + ...form, + masking: { + ...form.masking, + captureGroups, + }, + }; + }; + + return ( +
+
+
+ { setQuery(event.target.value); setPage(0); }} + /> + { setDataType(value); setPage(0); }} options={[ + { value: "", label: t("guardrails.allDataTypes") }, + ...GUARDRAILS_DATA_TYPES.map(value => ({ value: String(value), label: t(GUARDRAILS_DATA_TYPE_KEYS[value]) })), + ]} /> + setImportMode(value as "merge" | "replace")} options={[ + { value: "merge", label: t("guardrails.importMerge") }, + { value: "replace", label: t("guardrails.importReplace") }, + ]} /> + { + const file = event.target.files?.[0]; + if (!file) return; + if (file.size > GUARDRAILS_IMPORT_MAX_BYTES) { + onImportError(new Error(t("guardrails.importTooLarge"))); + event.target.value = ""; + return; + } + void file.text() + .then(text => onImport(JSON.parse(text) as unknown, importMode, importButtonRef.current)) + .catch(onImportError); + event.target.value = ""; + }} + /> + +
+
+ {importPreview && ( +
+
+ {t("guardrails.importPreviewTitle")} + + {t(importPreview.mode === "merge" ? "guardrails.importMerge" : "guardrails.importReplace")} + +
+
+ {t("guardrails.importCreateCount", { count: importPreview.createCount })} + {t("guardrails.importReplaceCount", { count: importPreview.replaceCount })} + {t("guardrails.importUnchangedCount", { count: importPreview.unchangedCount })} +
+ {securityDiff && ( +
+ {t("guardrails.importSecurityTitle")} + {securityDiff.weakensProtection && ( + {t("guardrails.importSecurityWeakening")} + )} + {!securityDiff.weakensProtection && securityDiff.requiresReview && ( + {t("guardrails.importSecurityPotential")} + )} + {securityChanges.length === 0 + ?

{t("guardrails.importSecurityUnchanged")}

+ : ( +
    {securityChanges.map(change => ( +
  • + {change.label} + {change.weakening && ( + + {t("guardrails.importSecurityReduced")} + + )} + {!change.weakening && change.reviewRequired && ( + + {t("guardrails.importSecurityReviewRequired")} + + )} +
  • + ))}
+ )} + {securityDiff.enabledDataTypes.removed.length > 0 && ( +

+ {t("guardrails.importSecurityRemovedTypes", { + types: securityDiff.enabledDataTypes.removed + .map(value => t(GUARDRAILS_DATA_TYPE_KEYS[value])) + .join(", "), + })} +

+ )} + {securityDiff.customRules.changedRuleIds.length > 0 && ( +

+ {t("guardrails.importSecurityChangedRuleIds")}{" "} + {securityDiff.customRules.changedRuleIds.join(", ")} +

+ )} + {securityDiff.customRules.removedRuleIds.length > 0 && ( +

+ {t("guardrails.importSecurityRemovedRuleIds")}{" "} + {securityDiff.customRules.removedRuleIds.join(", ")} +

+ )} +
+ )} + {importPreview.conflicts.length > 0 && ( + + {t("guardrails.importConflicts", { count: importPreview.conflicts.length })} + {importPreview.conflicts.join(", ")} + + )} +
+ + +
+
+ )} + {visible.length === 0 + ? + :
{visible.map((rule, index) => ( + + {(index === 0 || visible[index - 1]?.group !== rule.group) && ( +
{groupLabel(rule)}
+ )} +
+
+ {ruleLabel(rule)} + {rule.ruleId} + {rule.group} · {rule.source} +
+
+ {rule.custom ? ( + <> + + + + ) : ( + onToggle(rule.ruleId, !rule.enabled)} + /> + )} +
+
+
+ ))}
} +
+ + {t("guardrails.page", { current: visiblePage + 1, total: pageCount })} + +
+
+
+ {t(editingId ? "guardrails.editRule" : "guardrails.addRule")} + +
+
{ + event.preventDefault(); + const rule = resolvedDraftRule(); + if (!rule) return; + void onSave(rule, editingId).then(resetForm).catch(() => undefined); + }}> +
+ + + + + + + + + + {form.validators.includes("entropy") && ( + + )} + {form.validators.includes("banlist") && ( +