diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..2480f65be --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to the ContextualWisdomLab central GitHub control plane are documented in this file. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versioned releases follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Security + +- Upgrade the central Strix dependency snapshots to `aiohttp==3.14.3`, `cryptography==50.0.0`, and the compatible `pyOpenSSL==26.4.0` closure so the hard dependency gates contain no known affected releases. +- Redact credentials from every sandbox evidence publication sink, including completed and timed-out process output, service log tails, commands, reviewer notes, nested JSON values, and JSON object keys. + +### Fixed + +- Replace quadratic sensitive-assignment rescanning with a bounded forward scan so one long ordinary diagnostic token cannot cause disproportionate log-processing work. + +### Documentation + +- Add an APA 7 doctoring record for the sandbox command/output redaction boundary, structured diagnostics, availability controls, verification evidence, limitations, and rollback requirements. diff --git a/docs/doctoring/sandboxed-command-log-redaction.md b/docs/doctoring/sandboxed-command-log-redaction.md new file mode 100644 index 000000000..517ae519c --- /dev/null +++ b/docs/doctoring/sandboxed-command-log-redaction.md @@ -0,0 +1,76 @@ +# Sandboxed command and output redaction + +## Decision + +The central verification wrappers treat subprocess output, service log tails, command arguments, shell-command strings, and reviewer evidence notes as potentially sensitive before writing them to GitHub Actions logs or machine-readable review evidence. + +One trusted redaction module owns this publication boundary: + +- captured standard output and standard error are redacted before printing; +- `TimeoutExpired` byte and text payloads use the same redaction path; +- service log tails are redacted before publication; +- command arguments following sensitive options such as `--token`, `--password`, or `--api-key` are replaced; +- sensitive `KEY=value` command arguments are replaced while preserving the key; +- standalone provider-token shapes are removed; +- valid JSON is traversed recursively so credential-shaped object keys and string values cannot bypass line-oriented patterns; +- shell command strings are parsed without execution and reconstructed from redacted arguments; and +- JSON result markers redact commands and evidence notes before serialization. + +The original argument vectors and output are used only inside the isolated execution boundary. Redaction changes neither the command that runs nor its exit status. It is applied at every publication sink instead of depending on each child process to avoid printing credentials. + +## Threat model + +Repository verification commands and web end-to-end services can emit credentials through exception messages, dependency-manager diagnostics, HTTP-client traces, command-line options, environment-derived configuration, startup logs, structured JSON diagnostics, and timeout payloads. An explicitly allowlisted environment variable can therefore remain correctly scoped to a child process and still be disclosed when that child echoes it. + +GitHub Actions logs and review envelopes are durable evidence with a potentially broader readership than the originating credential. MITRE classifies insertion of sensitive information into log files as CWE-532. OWASP's current logging guidance identifies access tokens, passwords, database connection strings, encryption keys, and other primary secrets as values that should normally be removed, masked, sanitized, hashed, or encrypted before logging. NIST SSDF requires protection of software and development artifacts from unauthorized access and disclosure. + +## Security and availability boundaries + +- No provider-shaped credential literal is committed as a test fixture. Tests construct credential-shaped values from fragments at runtime so Secret Scan remains authoritative. +- Redaction is fail-closed for recognized sensitive option names, assignments, bearer/basic values, JWTs, and known provider token formats, but it is not a general data-loss-prevention engine. +- Sensitive option detection uses explicit credential terms. Ambiguous short flags such as `-p` are not guessed because they can mean port, path, project, or password depending on the child tool. +- Shell strings are tokenized with `shlex.split`; no shell is invoked for redaction. Malformed strings fall back to line-oriented redaction. +- `subprocess.run` and `subprocess.Popen` receive structured argument arrays with `shell=False`. Preventing shell interpretation and preventing log disclosure are independent controls. +- The assignment scanner advances through each ordinary identifier once. A deterministic instrumentation test prevents a long non-sensitive token from reintroducing quadratic rescanning and log-processing denial of service. +- File paths, working directories, and sandbox paths remain visible operational evidence. Operators must not place credentials in path names. +- Redaction preserves line boundaries and ordinary non-sensitive diagnostics. It does not transform a failed command into a successful result or suppress a nonzero exit status. + +No formal OWASP, NIST, or CWE conformity is claimed. + +## Verification contract + +The focused regression suite constructs a credential-shaped token at runtime and proves that it does not appear in: + +1. completed verification stdout or stderr; +2. timeout output supplied as bytes or text; +3. human-readable command displays; +4. JSON result-marker command arrays; +5. backend, frontend, or E2E shell-command fields; +6. reviewer evidence notes; +7. service log tails; +8. nested JSON string values; or +9. JSON object keys. + +The tests also cover separate sensitive options, `--option=value`, `KEY=value` assignments, standalone provider-token shapes, malformed shell quoting, missing logs, bounded final-line selection, recursive JSON structures, bounded assignment scanning, and both wrappers' end-to-end publication paths. Ordinary text, line endings, result envelopes, cleanup, timeouts, and child-process exit codes remain observable. + +The exact pull-request head must additionally pass the complete central unit suite, 100% production statement and branch coverage for the changed surface, production docstring checks, Secret Scan, CodeQL, Semgrep, Python Security, Security Scan, OpenCode, Noema, CodeRabbit, independent current-head approval, and branch protection before merge. + +## Modular boundary + +`sandboxed_verify.py`, `sandboxed_web_e2e.py`, and `redact_sensitive_log.py` remain independently executable scripts and reusable Python modules. Product repositories consume the behavior through the organization control plane without copying repository-local redaction code. The wrappers preserve their existing CLI and machine-readable result contracts. + +## Rollback + +Rollback must restore every publication sink as one atomic change. Removing only command redaction, JSON traversal, service-tail redaction, or result-envelope redaction would recreate a bypass around the remaining controls. Before rollback, operators must prove that no allowlisted credential can reach child output or command metadata and must retain equivalent focused regression evidence. + +## APA 7 references + +MITRE Corporation. (2026). *CWE-117: Improper output neutralization for logs* (CWE Version 4.20). https://cwe.mitre.org/data/definitions/117.html + +MITRE Corporation. (2026). *CWE-532: Insertion of sensitive information into log file* (CWE Version 4.20). https://cwe.mitre.org/data/definitions/532.html + +National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218). https://doi.org/10.6028/NIST.SP.800-218 + +OWASP Foundation. (n.d.). *Logging cheat sheet*. OWASP Cheat Sheet Series. Retrieved August 5, 2026, from https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html + +Python Software Foundation. (2026). *subprocess—Subprocess management* (Python 3.14.6 documentation). https://docs.python.org/3/library/subprocess.html diff --git a/requirements-strix-ci-hashes.txt b/requirements-strix-ci-hashes.txt index e2c8f00eb..c305e9c84 100644 --- a/requirements-strix-ci-hashes.txt +++ b/requirements-strix-ci-hashes.txt @@ -4,127 +4,128 @@ aiohappyeyeballs==2.7.1 \ --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.14.1 \ - --hash=sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5 \ - --hash=sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983 \ - --hash=sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521 \ - --hash=sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340 \ - --hash=sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d \ - --hash=sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a \ - --hash=sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4 \ - --hash=sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a \ - --hash=sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f \ - --hash=sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee \ - --hash=sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8 \ - --hash=sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb \ - --hash=sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397 \ - --hash=sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05 \ - --hash=sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8 \ - --hash=sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09 \ - --hash=sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2 \ - --hash=sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba \ - --hash=sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf \ - --hash=sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271 \ - --hash=sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5 \ - --hash=sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847 \ - --hash=sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264 \ - --hash=sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf \ - --hash=sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6 \ - --hash=sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df \ - --hash=sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035 \ - --hash=sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126 \ - --hash=sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6 \ - --hash=sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35 \ - --hash=sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4 \ - --hash=sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333 \ - --hash=sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203 \ - --hash=sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c \ - --hash=sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1 \ - --hash=sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251 \ - --hash=sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365 \ - --hash=sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b \ - --hash=sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621 \ - --hash=sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94 \ - --hash=sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da \ - --hash=sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491 \ - --hash=sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe \ - --hash=sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d \ - --hash=sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080 \ - --hash=sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42 \ - --hash=sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c \ - --hash=sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397 \ - --hash=sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9 \ - --hash=sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8 \ - --hash=sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345 \ - --hash=sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3 \ - --hash=sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602 \ - --hash=sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2 \ - --hash=sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966 \ - --hash=sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192 \ - --hash=sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95 \ - --hash=sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3 \ - --hash=sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b \ - --hash=sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444 \ - --hash=sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6 \ - --hash=sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573 \ - --hash=sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af \ - --hash=sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15 \ - --hash=sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe \ - --hash=sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2 \ - --hash=sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496 \ - --hash=sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876 \ - --hash=sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817 \ - --hash=sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448 \ - --hash=sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e \ - --hash=sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6 \ - --hash=sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd \ - --hash=sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f \ - --hash=sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe \ - --hash=sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c \ - --hash=sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca \ - --hash=sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c \ - --hash=sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa \ - --hash=sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc \ - --hash=sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0 \ - --hash=sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0 \ - --hash=sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2 \ - --hash=sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844 \ - --hash=sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719 \ - --hash=sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1 \ - --hash=sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3 \ - --hash=sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178 \ - --hash=sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3 \ - --hash=sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95 \ - --hash=sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730 \ - --hash=sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842 \ - --hash=sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd \ - --hash=sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d \ - --hash=sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96 \ - --hash=sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85 \ - --hash=sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1 \ - --hash=sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199 \ - --hash=sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a \ - --hash=sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588 \ - --hash=sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec \ - --hash=sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004 \ - --hash=sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480 \ - --hash=sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04 \ - --hash=sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8 \ - --hash=sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce \ - --hash=sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087 \ - --hash=sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505 \ - --hash=sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780 \ - --hash=sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4 \ - --hash=sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d \ - --hash=sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca \ - --hash=sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665 \ - --hash=sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296 \ - --hash=sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c \ - --hash=sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a \ - --hash=sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7 \ - --hash=sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451 \ - --hash=sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3 +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 # via + # -r requirements-strix-ci.txt # gql # litellm aiosignal==1.4.0 \ @@ -401,53 +402,53 @@ click==8.4.1 \ # litellm # typer # uvicorn -cryptography==49.0.0 \ - --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ - --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ - --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ - --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ - --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ - --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ - --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ - --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ - --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ - --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ - --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ - --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ - --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ - --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ - --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ - --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ - --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ - --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ - --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ - --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ - --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ - --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ - --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ - --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ - --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ - --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ - --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ - --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ - --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ - --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ - --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ - --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ - --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ - --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ - --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ - --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ - --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ - --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ - --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ - --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ - --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ - --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ - --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ - --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ - --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ - --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b +cryptography==50.0.0 \ + --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ + --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ + --hash=sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437 \ + --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ + --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ + --hash=sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037 \ + --hash=sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269 \ + --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ + --hash=sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc \ + --hash=sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95 \ + --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ + --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ + --hash=sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c \ + --hash=sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41 \ + --hash=sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c \ + --hash=sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d \ + --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ + --hash=sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c \ + --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ + --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ + --hash=sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f \ + --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ + --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ + --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ + --hash=sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a \ + --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ + --hash=sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e \ + --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ + --hash=sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d \ + --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ + --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ + --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ + --hash=sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9 \ + --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ + --hash=sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3 \ + --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ + --hash=sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533 \ + --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ + --hash=sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ + --hash=sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f \ + --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ + --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 # via # -r requirements-strix-ci.txt # google-auth @@ -1680,9 +1681,9 @@ pyjwt==2.13.0 \ --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 # via mcp -pyopenssl==26.3.0 \ - --hash=sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3 \ - --hash=sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341 +pyopenssl==26.4.0 \ + --hash=sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7 \ + --hash=sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c # via google-auth python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ diff --git a/requirements-strix-ci.txt b/requirements-strix-ci.txt index e32bd39a9..98e5c33e2 100644 --- a/requirements-strix-ci.txt +++ b/requirements-strix-ci.txt @@ -1,6 +1,7 @@ strix-agent==1.0.4 +aiohttp==3.14.3 google-cloud-aiplatform==1.133.0 protobuf<7.0.0 -cryptography==49.0.0 +cryptography==50.0.0 python-multipart==0.0.32 pyasn1==0.6.4 diff --git a/scripts/ci/redact_sensitive_log.py b/scripts/ci/redact_sensitive_log.py index cb89fe67b..624273f34 100644 --- a/scripts/ci/redact_sensitive_log.py +++ b/scripts/ci/redact_sensitive_log.py @@ -5,7 +5,9 @@ import json import re +import shlex import sys +from collections.abc import Sequence from typing import Any REDACTED = "[REDACTED]" @@ -15,6 +17,16 @@ r"api[_-]?key|private[_-]?key|access[_-]?key|session[_-]?key)", re.IGNORECASE, ) +SENSITIVE_OPTION_PATTERN = ( + r"(?:token|secret|password|passwd|credential|authorization|jwt|" + r"api[-_]?key|private[-_]?key|access[-_]?key|session[-_]?key)" +) +SENSITIVE_OPTION_RE = re.compile(SENSITIVE_OPTION_PATTERN, re.IGNORECASE) +SEPARATE_SENSITIVE_OPTION_RE = re.compile( + rf"(?P(?(?!--)(?:\"[^\"]*\"|'[^']*'|[^\s,}]+))", + re.IGNORECASE, +) JWT_RE = re.compile( r"(? Any: - """Recursively replace values whose JSON keys identify credentials.""" + """Recursively redact sensitive keys and credential-shaped JSON strings.""" if isinstance(value, dict): return { - key: REDACTED if SENSITIVE_KEY_RE.search(str(key)) else _redact_json(item) + _redact_unstructured(str(key)): ( + REDACTED if SENSITIVE_KEY_RE.search(str(key)) else _redact_json(item) + ) for key, item in value.items() } if isinstance(value, list): return [_redact_json(item) for item in value] + if isinstance(value, str): + return _redact_unstructured(value) return value -def _consume_sensitive_assignment(text: str, start: int) -> tuple[str, int] | None: - """Return a redacted key/value assignment parsed in linear time.""" +def _consume_sensitive_assignment( + text: str, + start: int, +) -> tuple[str | None, int]: + """Parse one possible assignment and return replacement plus next cursor.""" cursor = start key_quote = "" if cursor < len(text) and text[cursor] in "\"'": @@ -53,27 +72,31 @@ def _consume_sensitive_assignment(text: str, start: int) -> tuple[str, int] | No cursor += 1 key_start = cursor if cursor >= len(text) or text[cursor] not in KEY_CHARS or text[cursor].isdigit(): - return None + return None, start + 1 while cursor < len(text) and text[cursor] in KEY_CHARS: cursor += 1 key = text[key_start:cursor] if key_quote: if cursor >= len(text) or text[cursor] != key_quote: - return None + return None, start + 1 cursor += 1 if not SENSITIVE_KEY_RE.search(key): - return None + return None, cursor while cursor < len(text) and text[cursor].isspace(): cursor += 1 if cursor >= len(text) or text[cursor] not in ":=": - return None + return None, cursor cursor += 1 while cursor < len(text) and text[cursor].isspace(): cursor += 1 if cursor >= len(text): - return None + return None, cursor value_start = cursor + if key.casefold() == "authorization": + while cursor < len(text) and text[cursor] not in ",}": + cursor += 1 + return text[start:value_start] + REDACTED, cursor if text[cursor] in "\"'": value_quote = text[cursor] cursor += 1 @@ -91,28 +114,31 @@ def _consume_sensitive_assignment(text: str, start: int) -> tuple[str, int] | No while cursor < len(text) and not text[cursor].isspace() and text[cursor] not in ",}": cursor += 1 if cursor == value_start: - return None + return None, cursor return text[start:value_start] + REDACTED, cursor def _redact_assignments(text: str) -> str: - """Redact sensitive key/value assignments without backtracking regexes.""" + """Redact sensitive key/value assignments in one bounded forward scan.""" output: list[str] = [] cursor = 0 while cursor < len(text): - match = _consume_sensitive_assignment(text, cursor) - if match is None: - output.append(text[cursor]) - cursor += 1 - continue - replacement, cursor = match - output.append(replacement) + replacement, next_cursor = _consume_sensitive_assignment(text, cursor) + if replacement is None: + output.append(text[cursor:next_cursor]) + else: + output.append(replacement) + cursor = next_cursor return "".join(output) def _redact_unstructured(text: str) -> str: """Redact credential-shaped values from non-JSON diagnostic text.""" cleaned = _redact_assignments(text) + cleaned = SEPARATE_SENSITIVE_OPTION_RE.sub( + lambda match: f"{match.group('prefix')}{REDACTED}", + cleaned, + ) cleaned = BEARER_RE.sub(lambda match: f"{match.group('prefix')}{REDACTED}", cleaned) cleaned = JWT_RE.sub(REDACTED, cleaned) for pattern in PROVIDER_TOKEN_RES: @@ -141,6 +167,40 @@ def redact_text(text: str) -> str: return "".join(output) +def redact_command_arguments(arguments: Sequence[str]) -> list[str]: + """Return command arguments with sensitive option values redacted.""" + redacted: list[str] = [] + redact_next = False + for raw_argument in arguments: + argument = str(raw_argument) + if redact_next: + redacted.append(REDACTED) + redact_next = False + continue + + option = argument.lstrip("-") + if "=" in option: + key, _value = option.split("=", 1) + if SENSITIVE_OPTION_RE.fullmatch(key): + separator_index = argument.find("=") + redacted.append(f"{argument[: separator_index + 1]}{REDACTED}") + continue + + redacted.append(redact_text(argument)) + if SENSITIVE_OPTION_RE.fullmatch(option): + redact_next = True + return redacted + + +def redact_shell_command(command: str) -> str: + """Return a shell command safe for logs without executing or expanding it.""" + try: + arguments = shlex.split(command) + except ValueError: + return redact_text(command) + return shlex.join(redact_command_arguments(arguments)) + + def main() -> int: """Redact standard input to standard output.""" sys.stdout.write(redact_text(sys.stdin.read())) diff --git a/scripts/ci/sandboxed_verify.py b/scripts/ci/sandboxed_verify.py index aace18d45..683c07256 100644 --- a/scripts/ci/sandboxed_verify.py +++ b/scripts/ci/sandboxed_verify.py @@ -6,6 +6,7 @@ import json import os import re +import shlex import shutil import subprocess import sys @@ -14,6 +15,14 @@ from collections.abc import Sequence from pathlib import Path +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +from scripts.ci.redact_sensitive_log import ( + redact_command_arguments, + redact_text, +) + DEFAULT_IGNORE = ( ".git", @@ -165,12 +174,12 @@ def run_command(command: Sequence[str], cwd: Path, env: dict[str, str], timeout: def timeout_output_text(value: str | bytes | None) -> str: - """Return timeout output as text, regardless of subprocess internals.""" + """Return redacted timeout output as text, regardless of subprocess internals.""" if value is None: return "" if isinstance(value, bytes): - return value.decode(errors="replace") - return value + return redact_text(value.decode(errors="replace")) + return redact_text(value) def emit_result( @@ -185,13 +194,13 @@ def emit_result( network: str, evidence_note: str, ) -> None: - """Print a machine-readable execution evidence summary.""" + """Print a machine-readable execution evidence summary without secrets.""" payload = { "allowed_env": sorted(set(allowed_env)), - "command": list(command), + "command": redact_command_arguments(command), "cwd": str(copied_repo), "elapsed_seconds": round(elapsed_seconds, 3), - "evidence_note": evidence_note, + "evidence_note": redact_text(evidence_note), "exit_code": exit_code, "network": network, "sandbox": str(sandbox_root) if kept else "(removed)", @@ -211,7 +220,8 @@ def main(argv: Sequence[str] | None = None) -> int: copied_repo = copy_workspace(Path(args.repo_root), sandbox, args.ignore) env = scrubbed_env(sandbox, args.allow_env) print(f"sandboxed-verify: cwd={copied_repo}") - print(f"sandboxed-verify: command={' '.join(args.command)}") + safe_command = shlex.join(redact_command_arguments(args.command)) + print(f"sandboxed-verify: command={safe_command}") if args.allow_env: print(f"sandboxed-verify: allowed env names={','.join(sorted(set(args.allow_env)))}") if args.network != "default": @@ -219,9 +229,9 @@ def main(argv: Sequence[str] | None = None) -> int: try: completed = run_command(args.command, copied_repo, env, args.timeout) if completed.stdout: - print(completed.stdout, end="") + print(redact_text(completed.stdout), end="") if completed.stderr: - print(completed.stderr, end="", file=sys.stderr) + print(redact_text(completed.stderr), end="", file=sys.stderr) exit_code = completed.returncode except subprocess.TimeoutExpired as exc: stdout = timeout_output_text(exc.stdout) diff --git a/scripts/ci/sandboxed_web_e2e.py b/scripts/ci/sandboxed_web_e2e.py index ae0c3105a..5dc0d61d6 100644 --- a/scripts/ci/sandboxed_web_e2e.py +++ b/scripts/ci/sandboxed_web_e2e.py @@ -22,6 +22,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from scripts.ci import sandboxed_verify +from scripts.ci.redact_sensitive_log import redact_shell_command, redact_text RESULT_MARKER = "SANDBOXED_WEB_E2E_RESULT" @@ -110,6 +111,7 @@ def start_service(label: str, command: str, cwd: Path, env: dict[str, str], logs stdout=log_file, stderr=subprocess.STDOUT, start_new_session=True, + shell=False, ) log_file.close() return Service(label=label, command=command, process=process, log_path=log_path) @@ -136,7 +138,7 @@ def wait_for_url(url: str, timeout: int, service: Service) -> bool: def run_shell(command: str, cwd: Path, env: dict[str, str], timeout: int) -> subprocess.CompletedProcess[str]: - """Run a shell command and capture its output.""" + """Run a shell-style command without invoking a shell and capture output.""" return subprocess.run( shlex.split(command), cwd=cwd, @@ -146,6 +148,7 @@ def run_shell(command: str, cwd: Path, env: dict[str, str], timeout: int) -> sub stderr=subprocess.PIPE, timeout=timeout, check=False, + shell=False, ) @@ -165,11 +168,11 @@ def stop_service(service: Service) -> None: def tail_text(path: Path, max_lines: int = 80) -> str: - """Return the final lines of a service log.""" + """Return redacted final lines of a service log.""" if not path.exists(): return "" lines = path.read_text(encoding="utf-8", errors="replace").splitlines() - return "\n".join(lines[-max_lines:]) + return redact_text("\n".join(lines[-max_lines:])) def emit_result( @@ -182,17 +185,17 @@ def emit_result( exit_code: int, elapsed_seconds: float, ) -> None: - """Print a machine-readable web E2E execution evidence summary.""" + """Print machine-readable web E2E evidence without credential values.""" payload = { - "backend_cmd": args.backend_cmd, + "backend_cmd": redact_shell_command(args.backend_cmd), "backend_ready": backend_ready, "allowed_env": sorted(set(args.allow_env)), "cwd": str(copied_repo), - "e2e_cmd": args.e2e_cmd, + "e2e_cmd": redact_shell_command(args.e2e_cmd), "elapsed_seconds": round(elapsed_seconds, 3), - "evidence_note": args.evidence_note, + "evidence_note": redact_text(args.evidence_note), "exit_code": exit_code, - "frontend_cmd": args.frontend_cmd, + "frontend_cmd": redact_shell_command(args.frontend_cmd), "frontend_ready": frontend_ready, "network": args.network, "sandbox": str(sandbox_root) if args.keep_sandbox else "(removed)", @@ -232,9 +235,9 @@ def main(argv: Sequence[str] | None = None) -> int: try: completed = run_shell(args.e2e_cmd, copied_repo, env, args.e2e_timeout) if completed.stdout: - print(completed.stdout, end="") + print(redact_text(completed.stdout), end="") if completed.stderr: - print(completed.stderr, end="", file=sys.stderr) + print(redact_text(completed.stderr), end="", file=sys.stderr) exit_code = completed.returncode return exit_code except subprocess.TimeoutExpired as exc: diff --git a/tests/test_redact_json_key_boundary.py b/tests/test_redact_json_key_boundary.py new file mode 100644 index 000000000..27798bcbe --- /dev/null +++ b/tests/test_redact_json_key_boundary.py @@ -0,0 +1,46 @@ +"""Regression evidence for credential-shaped JSON keys and bounded scanning.""" + +from __future__ import annotations + +import re + +from scripts.ci import redact_sensitive_log as redactor + + +def test_json_object_keys_use_the_unstructured_redaction_boundary(monkeypatch) -> None: + """A JSON key matching a credential format must never bypass redaction.""" + + monkeypatch.setattr( + redactor, + "PROVIDER_TOKEN_RES", + (re.compile(r"\bprovider_marker_value\b"),), + ) + + assert redactor.redact_text('{"provider_marker_value":"safe"}\n') == ( + '{"[REDACTED]":"safe"}\n' + ) + + +def test_assignment_scan_does_not_rescan_one_long_ordinary_identifier( + monkeypatch, +) -> None: + """A long non-sensitive token must be inspected once rather than quadratically.""" + + class CountingSensitivePattern: + """Count the total candidate characters inspected by key classification.""" + + def __init__(self) -> None: + self.inspected_characters = 0 + + def search(self, value: str): + """Record one candidate and report that it is not a sensitive key.""" + + self.inspected_characters += len(value) + return None + + counting_pattern = CountingSensitivePattern() + monkeypatch.setattr(redactor, "SENSITIVE_KEY_RE", counting_pattern) + ordinary_identifier = "ordinary_identifier_" * 512 + + assert redactor.redact_text(ordinary_identifier) == ordinary_identifier + assert counting_pattern.inspected_characters <= len(ordinary_identifier) diff --git a/tests/test_redact_sensitive_log_contract.py b/tests/test_redact_sensitive_log_contract.py new file mode 100644 index 000000000..c279987a3 --- /dev/null +++ b/tests/test_redact_sensitive_log_contract.py @@ -0,0 +1,112 @@ +"""Complete behavioral contracts for the central log-redaction primitive.""" + +from __future__ import annotations + +import io +import runpy +import sys +from pathlib import Path + +import pytest + +from scripts.ci import redact_sensitive_log as redactor + + +def _synthetic_provider_token() -> str: + """Construct a provider-shaped value without storing a scanner literal.""" + + return "gh" + "p_" + ("B" * 36) + + +def test_json_sensitive_keys_and_scalar_values_preserve_safe_evidence() -> None: + """Sensitive JSON values are replaced while ordinary scalars remain intact.""" + + assert redactor.redact_text( + '{"api_key":"value","count":3,"enabled":true,"empty":null}\n' + ) == ( + '{"api_key":"[REDACTED]","count":3,"enabled":true,"empty":null}\n' + ) + + +def test_assignment_parser_covers_quoted_keys_values_and_incomplete_forms() -> None: + """Quoted assignments redact values and malformed empty forms make progress.""" + + assert redactor.redact_text("'token' = 'quoted value'") == ( + "'token' = [REDACTED]" + ) + assert redactor.redact_text("token='escaped\\' value'") == ( + "token=[REDACTED]" + ) + assert redactor.redact_text("token='unterminated") == ( + "token=[REDACTED]" + ) + assert redactor.redact_text("'token=value") == ( + "'token=[REDACTED]" + ) + assert redactor.redact_text("token") == "token" + assert redactor.redact_text("token text") == "token text" + assert redactor.redact_text("token=") == "token=" + assert redactor.redact_text("token=,") == "token=," + assert redactor.redact_text("token=value ordinary") == ( + "token=[REDACTED] ordinary" + ) + assert redactor.redact_text("1token=value") == ( + "1token=[REDACTED]" + ) + + +def test_unstructured_patterns_cover_basic_jwt_and_provider_values() -> None: + """Independent credential formats share one non-JSON redaction boundary.""" + + provider_value = _synthetic_provider_token() + assert redactor.redact_text("Authorization: Basic opaque-value") == ( + "Authorization: [REDACTED]" + ) + assert redactor.redact_text("Basic opaque-value") == "Basic [REDACTED]" + assert redactor.redact_text("header.payload.signature") == redactor.REDACTED + assert redactor.redact_text(provider_value) == redactor.REDACTED + + +def test_unstructured_output_redacts_separate_sensitive_option_values() -> None: + """A child that echoes a separate secret option must not disclose its value.""" + + assert redactor.redact_text( + "running tool --api-key ordinary-value --mode safe" + ) == "running tool --api-key [REDACTED] --mode safe" + + +def test_command_argument_redaction_preserves_non_sensitive_options() -> None: + """Only explicit sensitive option values and credential shapes are replaced.""" + + assert redactor.redact_command_arguments( + ["tool", "--mode=safe", "--token"] + ) == ["tool", "--mode=safe", "--token"] + assert redactor.redact_shell_command("") == "" + + +def test_empty_text_and_cli_main_preserve_stream_contract(monkeypatch) -> None: + """Empty input is stable and the CLI writes only the redacted stream.""" + + assert redactor.redact_text("") == "" + input_stream = io.StringIO("password=value\nordinary\n") + output_stream = io.StringIO() + monkeypatch.setattr(sys, "stdin", input_stream) + monkeypatch.setattr(sys, "stdout", output_stream) + + assert redactor.main() == 0 + assert output_stream.getvalue() == "password=[REDACTED]\nordinary\n" + + +def test_module_entry_point_exits_after_redacting_standard_input(monkeypatch) -> None: + """Direct script execution preserves the same redacted stream contract.""" + + input_stream = io.StringIO("secret=value\n") + output_stream = io.StringIO() + monkeypatch.setattr(sys, "stdin", input_stream) + monkeypatch.setattr(sys, "stdout", output_stream) + + with pytest.raises(SystemExit) as raised: + runpy.run_path(str(Path(redactor.__file__).resolve()), run_name="__main__") + + assert raised.value.code == 0 + assert output_stream.getvalue() == "secret=[REDACTED]\n" diff --git a/tests/test_sandboxed_output_redaction.py b/tests/test_sandboxed_output_redaction.py new file mode 100644 index 000000000..bab3956e7 --- /dev/null +++ b/tests/test_sandboxed_output_redaction.py @@ -0,0 +1,199 @@ +"""Regression tests for secret-safe sandbox subprocess evidence.""" + +from __future__ import annotations + +import subprocess +from pathlib import Path +from typing import cast + +from scripts.ci import sandboxed_verify, sandboxed_web_e2e +from scripts.ci.redact_sensitive_log import ( + REDACTED, + redact_command_arguments, + redact_shell_command, + redact_text, +) + + +def _provider_token() -> str: + """Build a credential-shaped fixture without committing a scanner secret.""" + return "gh" + "p_" + ("A" * 36) + + +def test_json_string_values_are_redacted_recursively() -> None: + """Valid JSON cannot bypass token redaction through non-sensitive value keys.""" + token = _provider_token() + raw = ( + '{"message":"provider ' + + token + + '","nested":{"notes":["Bearer ' + + token + + '","safe"]}}\n' + ) + + redacted = redact_text(raw) + + assert token not in redacted + assert redacted == ( + '{"message":"provider [REDACTED]",' + '"nested":{"notes":["Bearer [REDACTED]","safe"]}}\n' + ) + + +def test_redact_command_arguments_covers_separate_equals_and_direct_tokens() -> None: + """Redact option values, assignments, and provider-shaped standalone values.""" + token = _provider_token() + + assert redact_command_arguments( + ["tool", "--api-key", token, f"TOKEN={token}", token, "plain"] + ) == [ + "tool", + "--api-key", + REDACTED, + f"TOKEN={REDACTED}", + REDACTED, + "plain", + ] + + +def test_redact_shell_command_handles_parsed_and_malformed_input() -> None: + """Redact parsed commands and fall back to line redaction for bad quoting.""" + token = _provider_token() + parsed = redact_shell_command(f"tool --password {token} --name safe") + malformed = redact_shell_command(f"api_key={token}'") + + assert token not in parsed + assert "--password '[REDACTED]'" in parsed + assert token not in malformed + assert REDACTED in malformed + + +def test_timeout_output_text_redacts_strings_bytes_and_none() -> None: + """Normalize every TimeoutExpired payload form without leaking credentials.""" + token = _provider_token() + + assert sandboxed_verify.timeout_output_text(None) == "" + assert sandboxed_verify.timeout_output_text(f"token={token}") == f"token={REDACTED}" + assert sandboxed_verify.timeout_output_text(f"token={token}".encode()) == f"token={REDACTED}" + + +def test_sandboxed_verify_redacts_completed_output_command_and_note( + monkeypatch, tmp_path: Path, capsys +) -> None: + """Keep ordinary command completion evidence secret-free end to end.""" + token = _provider_token() + repository = tmp_path / "repository" + repository.mkdir() + + def fake_run_command(command, cwd, env, timeout): + return subprocess.CompletedProcess( + command, + 0, + stdout=f"token={token}\n", + stderr=f"Authorization: Bearer {token}\n", + ) + + monkeypatch.setattr(sandboxed_verify, "run_command", fake_run_command) + + exit_code = sandboxed_verify.main( + [ + "--repo-root", + str(repository), + "--evidence-note", + f"api_key={token}", + "--", + "tool", + "--api-key", + token, + ] + ) + captured = capsys.readouterr() + + assert exit_code == 0 + assert token not in captured.out + assert token not in captured.err + assert REDACTED in captured.out + assert REDACTED in captured.err + + +class _DoneProcess: + """Minimal completed-process double accepted by the service cleanup path.""" + + pid = 12345 + + def poll(self) -> int: + """Report that the fake service has already exited.""" + return 0 + + def wait(self, timeout: int) -> int: + """Return immediately for interface compatibility.""" + del timeout + return 0 + + +def test_sandboxed_web_e2e_redacts_commands_output_and_service_logs( + monkeypatch, tmp_path: Path, capsys +) -> None: + """Keep web E2E output, JSON evidence, and service log tails secret-free.""" + token = _provider_token() + repository = tmp_path / "repository" + repository.mkdir() + + def fake_start_service(label, command, cwd, env, logs_dir): + del cwd, env + log_path = logs_dir / f"{label}.log" + log_path.write_text(f"secret={token}\n", encoding="utf-8") + return sandboxed_web_e2e.Service( + label=label, + command=command, + process=cast(subprocess.Popen[str], _DoneProcess()), + log_path=log_path, + ) + + def fake_run_shell(command, cwd, env, timeout): + del cwd, env, timeout + return subprocess.CompletedProcess( + command, + 0, + stdout=f"token={token}\n", + stderr=f"Bearer {token}\n", + ) + + monkeypatch.setattr(sandboxed_web_e2e, "start_service", fake_start_service) + monkeypatch.setattr(sandboxed_web_e2e, "wait_for_url", lambda url, timeout, service: True) + monkeypatch.setattr(sandboxed_web_e2e, "run_shell", fake_run_shell) + + exit_code = sandboxed_web_e2e.main( + [ + "--repo-root", + str(repository), + "--backend-cmd", + f"backend --token {token}", + "--frontend-cmd", + f"frontend TOKEN={token}", + "--e2e-cmd", + f"e2e --api-key {token}", + "--evidence-note", + f"secret={token}", + ] + ) + captured = capsys.readouterr() + + assert exit_code == 0 + assert token not in captured.out + assert token not in captured.err + assert captured.out.count(REDACTED) >= 7 + assert REDACTED in captured.err + + +def test_tail_text_handles_missing_and_bounded_existing_logs(tmp_path: Path) -> None: + """Return nothing for missing logs and redact only the requested final lines.""" + token = _provider_token() + missing = tmp_path / "missing.log" + log_path = tmp_path / "service.log" + log_path.write_text(f"first\nsecond token={token}\nthird\n", encoding="utf-8") + + assert sandboxed_web_e2e.tail_text(missing) == "" + tail = sandboxed_web_e2e.tail_text(log_path, max_lines=2) + assert tail == f"second token={REDACTED}\nthird" + assert token not in tail