diff --git a/.github/workflows/docs-sync.yml b/.github/workflows/docs-sync.yml index 413075411..f26ab64b2 100644 --- a/.github/workflows/docs-sync.yml +++ b/.github/workflows/docs-sync.yml @@ -25,7 +25,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 138f5b0c3..f130d1136 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -53,7 +53,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true @@ -85,7 +85,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true @@ -171,7 +171,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true @@ -264,7 +264,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true @@ -338,7 +338,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true diff --git a/.github/workflows/provenance-refresh.yml b/.github/workflows/provenance-refresh.yml index 7167bd19f..bfe64a177 100644 --- a/.github/workflows/provenance-refresh.yml +++ b/.github/workflows/provenance-refresh.yml @@ -33,7 +33,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index abd90e2b5..054a79697 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -58,7 +58,7 @@ jobs: raise SystemExit("Runner Python does not satisfy protected-runtime ownership and permissions") PY - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true @@ -168,7 +168,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true diff --git a/.github/workflows/qc-autofix.yml b/.github/workflows/qc-autofix.yml index d86282b81..ac2582100 100644 --- a/.github/workflows/qc-autofix.yml +++ b/.github/workflows/qc-autofix.yml @@ -39,7 +39,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true diff --git a/.github/workflows/qc-ci.yml b/.github/workflows/qc-ci.yml index 33b807f35..4d387a217 100644 --- a/.github/workflows/qc-ci.yml +++ b/.github/workflows/qc-ci.yml @@ -24,7 +24,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true diff --git a/.github/workflows/qc-docs-drift.yml b/.github/workflows/qc-docs-drift.yml index 4a924c7c6..c0fcc169f 100644 --- a/.github/workflows/qc-docs-drift.yml +++ b/.github/workflows/qc-docs-drift.yml @@ -27,7 +27,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true diff --git a/.github/workflows/qc-patrol.yml b/.github/workflows/qc-patrol.yml index bee82fe21..f90f0f17d 100644 --- a/.github/workflows/qc-patrol.yml +++ b/.github/workflows/qc-patrol.yml @@ -37,7 +37,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true diff --git a/.github/workflows/qc-pr-review.yml b/.github/workflows/qc-pr-review.yml index b7561c66d..ea424dc74 100644 --- a/.github/workflows/qc-pr-review.yml +++ b/.github/workflows/qc-pr-review.yml @@ -42,7 +42,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true diff --git a/.github/workflows/qc-release.yml b/.github/workflows/qc-release.yml index b1d818f6b..f212e8d40 100644 --- a/.github/workflows/qc-release.yml +++ b/.github/workflows/qc-release.yml @@ -26,7 +26,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: "0.11.21" prune-cache: true diff --git a/README.md b/README.md index ff2a5a32e..10875084f 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ is silent and runs for about six minutes. | Fact | Value | |---|---| -| Current version | `4.89.10` | +| Current version | `4.90.2` | | Supported platforms | `codex, claude-code, cursor, kilo, opencode, openclaw, github-copilot-cli, github-copilot-vscode, cline-cli, cline-vscode, amp-cli, goose-cli, pi-cli, hermes-agent, qwen-code-cli, kimi-cli, factory-droid, antigravity-app, gemini-cli, omp-cli` | | Shipped skills | `115` | | Workflow packages | `21` | @@ -101,18 +101,22 @@ The guided installer explains the options, lets you select packages and supporte
Release included in this checkout -**Version:** 4.89.10
+**Version:** 4.90.2
-## What's new in 4.89.10 +## What's new in 4.90.2 -LocalSetup 4.89.10 updates the pinned CI actions and Python dependencies, keeps packaged dependency exports aligned, and makes existing boundary and allocation checks more reliable. +LocalSetup 4.90.2 carries forward the reviewed Herdr recovery, terminal startup, SDK helper and dependency maintenance since the last published release, and adds focused native test failure evidence. -- CI uses reviewed immutable pins for setup-python 7.1.0 and setup-uv 10.3.2. Regression checks enforce immutable, consistent pins without copying changing release hashes into tests; successful exact-commit validation remains reusable during main integration and publication. -- Python updates include OpenAI 3.37.0, setuptools 84.2.1, and Boto3/botocore 1.43.118, with regenerated SDK locks, integrity receipt and hashed storage requirements. -- More reliable checks: filesystem-boundary tests bind their simulated Git responses, and concurrent allocation tests separate readiness from a bounded completion watchdog. Production behavior, real locking, durable-artifact assertions and cleanup remain unchanged. +- Retire only an explicitly selected uninitialized Herdr creation: the shared owner requires the original request and creation token, exact pane and stable terminal identities, discard acknowledgement, and fresh evidence that no operation, writer or descendant work occupies the current shell. +- Preserve original owner and reservation bytes before closing, durably fence the close before dispatch, and reconcile a lost response without repeating it. Both original native identifiers must be conclusively absent before a later ordinary allocation for that same binding is permitted; recovery never automatically creates a replacement or runs a workload. +- Keep caller-owned storage in place. Optional retirement requires an exact bounded snapshot, durable writes and a nonblocking cross-process lock. Raw custom-store lock contention is reported as pane_busy before dispatch, while operation-body and cleanup failures are not reclassified as harmless lock refusal. +- Preserve the controlled Herdr startup loader across login-profile changes, recover a missing prior generation only from completed-operation and native-absence proof, and translate the child fraction to Herdr's parent-oriented split ratio. +- Carry forward the reviewed dependency graph, hash-pinned SDK and storage exports, reviewed routing-catalog evidence and bounded native qualification diagnostics from the unpublished maintenance line. Runtime, build and optional storage scopes remain separate; client model configuration is unchanged. +- Correct SDK wheel helper permissions: BuildSDK sets the packaged sudo_askpass helper to mode 0755 without changing its bytes and rejects symlinked helper destinations. Installed runtime protection still rejects a non-executable or group- or world-writable helper. +- Add failure-only native test metadata for popup identity rejection, primary viewport handoff and final family detach. The diagnostics do not change admission, assertions or deadlines and do not establish that the hosted failures are fixed. -See the [4.89.10 release guide](ls/docs/releases/4.89.10.md) for compatibility, updating, and verification. +See the [4.90.2 release guide](ls/docs/releases/4.90.2.md) for compatibility, updating, and verification.
diff --git a/VERSION b/VERSION index d1e19b24a..747629267 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.89.10 +4.90.2 diff --git a/ls/README.md b/ls/README.md index 6ba77340c..6ec49b79a 100644 --- a/ls/README.md +++ b/ls/README.md @@ -1,6 +1,6 @@ # LocalSetup Framework Engine -**Version:** 4.89.10
+**Version:** 4.90.2
`ls/` is the engine that makes the public LocalSetup promise real. It stores the framework code, shipped skills, workflow packages, platform templates, docs, tests, and install manifests that turn a repository into a portable agent workspace. @@ -26,7 +26,7 @@ For the public product overview, start with the [root README](../README.md). Thi LocalSetup-managed entries in consuming repositories are install output. Adapter directories may also contain project-owned skills, files, and symlinks; preserve that content in place. See [adapter ownership](docs/ADAPTER_OWNERSHIP.md). -Read the [current release guide](docs/releases/4.89.10.md) for LocalSetup 4.89.10, including compatibility, updating, and verification. Find downloads in the [latest published release](https://github.com/CruxExperts/localsetup/releases/latest). +Read the [current release guide](docs/releases/4.90.2.md) for LocalSetup 4.90.2, including compatibility, updating, and verification. Find downloads in the [latest published release](https://github.com/CruxExperts/localsetup/releases/latest). ## Install flow diff --git a/ls/config/sdk-dependency-receipt.json b/ls/config/sdk-dependency-receipt.json index c81c25f2a..3c6de6a19 100644 --- a/ls/config/sdk-dependency-receipt.json +++ b/ls/config/sdk-dependency-receipt.json @@ -1,8 +1,8 @@ { "files": { "sdk-build.lock": "6e7910c87ddfeaefcae794df890eeeb6be8ad963297bf693b09fba962a24ce6f", - "sdk-runtime.lock": "2765f24301657c2ef1ffd92026417c3fe258eb979b488258d13ff4d59ec34fe0" + "sdk-runtime.lock": "7e17d56032e7e0d875c3d23f6660e4bab484cdc9e5254d600461f90a27f63f38" }, "schema_version": 1, - "source_sha256": "7f498dbeff8b932ae9cfcc95df2f853445ae1d186694a932e3f8edfd60182fd5" + "source_sha256": "935fcf8472f150fb0f3ee4677caf112f5ee1afc794bad373bcb76faa61d1b321" } diff --git a/ls/config/sdk-runtime.lock b/ls/config/sdk-runtime.lock index e4a6be9fb..d6ab228ef 100644 --- a/ls/config/sdk-runtime.lock +++ b/ls/config/sdk-runtime.lock @@ -179,9 +179,9 @@ cryptography==50.0.2 \ --hash=sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480 \ --hash=sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb \ --hash=sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b -genai-prices==0.1.13 \ - --hash=sha256:6d2b40760a2f7f5e207bac9b98de0af80e60c58ead497872a5331a7cee610477 \ - --hash=sha256:76c9e2dd3917fdacdf3bc562297c4591384142ea2283c477e1512fafa4e3a384 +genai-prices==0.2.0 \ + --hash=sha256:21ede75742043b3489a6cec3ad5722782640b34676563c6c755b52061812e6a3 \ + --hash=sha256:a0e92059e96492f2ffe55fa708908fe2fc0afe11d111f8051106d52ae8fd9c12 griffelib==2.4.1 \ --hash=sha256:40c028dd2e623f464423f0e11e3c553158f1cf4f1368239a370534d73e1407ff \ --hash=sha256:76bc562f936ad1ce198d2cf6d0731bd5c92a7d934b69ef828ef22ee0ffdb62ce @@ -274,123 +274,123 @@ jsonschema==4.26.0 \ jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d -logfire-api==5.2.0 \ - --hash=sha256:06553f4a4b1b54d094dd01708f51cde69ccd5e8a06fc2664bece3491b593390d \ - --hash=sha256:f4bd56df590ba7e7a32adb0302c82af38dc4edb9eecd03b1bfa03ffa581eecf8 -openai==3.37.0 \ - --hash=sha256:670bd6f699000d43a2073bcf94374a55bb133bfe5de0d084d47eb9bb9f6a8e5d \ - --hash=sha256:d9f5b000f52397a7f9f946012744c5bc4c962eed3c100385252464c273a63e57 -opentelemetry-api==1.45.1 \ - --hash=sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75 \ - --hash=sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb +logfire-api==5.4.0 \ + --hash=sha256:00297901da03e350bcf4ebe3d829f3617ebbe62b000e5d53d3c485223c258204 \ + --hash=sha256:328b3fae6eaced55875339cf58521ebf13a9117bdb8be40f717f056aa62c43d3 +openai==3.41.0 \ + --hash=sha256:7060a74b239609b21f2919eb98807a8aad463b4f510b8b3b388b15559360b0a8 \ + --hash=sha256:d065fc59d3d7d8537a77b3ceb527732952c6ff610158e8a2a95b246be637e86f +opentelemetry-api==1.45.2 \ + --hash=sha256:2cbd737377e710ee740d261c94e93fa0f08e6d037c18f17513cc0a2d94479a34 \ + --hash=sha256:d7b0008e21e5fd61cbe7a7f34ca3b6905a78ab677d9bc395e299359c75cda6b4 pgpy==0.6.0 \ --hash=sha256:279c2e353f4c3a319f00bd9bd582456e420f8a3ac6de2b4e9731444746828383 -pyasn1==0.6.4 \ - --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ - --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b +pyasn1==0.6.5 \ + --hash=sha256:4017b0fce125096b8573b39c989fdb1a4b19cf761e683398184550fea0e66587 \ + --hash=sha256:6b11616a6bcf3a40e635f082bb232fa57b5e0dd7f517bd7ddd69ec3794508a47 pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \ --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 -pydantic==2.14.1 \ - --hash=sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454 \ - --hash=sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26 -pydantic-core==2.50.1 \ - --hash=sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c \ - --hash=sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b \ - --hash=sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019 \ - --hash=sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685 \ - --hash=sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b \ - --hash=sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482 \ - --hash=sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658 \ - --hash=sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498 \ - --hash=sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec \ - --hash=sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a \ - --hash=sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b \ - --hash=sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4 \ - --hash=sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a \ - --hash=sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8 \ - --hash=sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb \ - --hash=sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e \ - --hash=sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8 \ - --hash=sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7 \ - --hash=sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a \ - --hash=sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e \ - --hash=sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396 \ - --hash=sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255 \ - --hash=sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899 \ - --hash=sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43 \ - --hash=sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb \ - --hash=sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f \ - --hash=sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d \ - --hash=sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415 \ - --hash=sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c \ - --hash=sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea \ - --hash=sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2 \ - --hash=sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e \ - --hash=sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568 \ - --hash=sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6 \ - --hash=sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a \ - --hash=sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9 \ - --hash=sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1 \ - --hash=sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff \ - --hash=sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f \ - --hash=sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5 \ - --hash=sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb \ - --hash=sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4 \ - --hash=sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0 \ - --hash=sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597 \ - --hash=sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b \ - --hash=sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa \ - --hash=sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242 \ - --hash=sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f \ - --hash=sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc \ - --hash=sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f \ - --hash=sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c \ - --hash=sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61 \ - --hash=sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102 \ - --hash=sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4 \ - --hash=sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e \ - --hash=sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54 \ - --hash=sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070 \ - --hash=sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed \ - --hash=sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb \ - --hash=sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5 \ - --hash=sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19 \ - --hash=sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5 \ - --hash=sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112 \ - --hash=sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9 \ - --hash=sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5 \ - --hash=sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72 \ - --hash=sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2 \ - --hash=sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8 \ - --hash=sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295 \ - --hash=sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e \ - --hash=sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe \ - --hash=sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290 \ - --hash=sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea \ - --hash=sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662 \ - --hash=sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41 \ - --hash=sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44 \ - --hash=sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807 \ - --hash=sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78 \ - --hash=sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c \ - --hash=sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3 \ - --hash=sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831 \ - --hash=sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86 \ - --hash=sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2 \ - --hash=sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f \ - --hash=sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f \ - --hash=sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10 \ - --hash=sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5 \ - --hash=sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b \ - --hash=sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09 \ - --hash=sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a \ - --hash=sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e \ - --hash=sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459 \ - --hash=sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08 \ - --hash=sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709 \ - --hash=sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a \ - --hash=sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20 +pydantic==2.14.2 \ + --hash=sha256:7efbf7507b937931cd32a5755acb00ea1cf5ccb1d4e8067a5850d5069e382118 \ + --hash=sha256:fb7b2db6cefe8414dd612edeede49c4c433acb0432b0f92b95024e45f66c505d +pydantic-core==2.50.2 \ + --hash=sha256:04cf672960b8c4fc28bb159c8cdd101fbd311c520237a5c0360448b851adfb41 \ + --hash=sha256:0b94a515f44f1ebcb25395bcdba506c4cdaf6d3fcb0466b9658521b41e5ff057 \ + --hash=sha256:0d090ae76d5b09c003600c7779b3adcf3d1b49268f616c5c51d844e54c8e318d \ + --hash=sha256:126f158474346f457c0759c29c1a6334acd5f7f99dd272bb27d19314f3d81b4d \ + --hash=sha256:150f6e101f575c21a4ecfed1be1c05bb89a0ad49cb9c846ed377583bd71b32c2 \ + --hash=sha256:18f008ef58cd30782f81b6e0119e20b7e884edcefbc2d370162ebe033f6e5c99 \ + --hash=sha256:1ac5b99bc7b0a68ec64f5f75a6828a7bab5535c8df0cdc07901deb50e0815910 \ + --hash=sha256:1b0e8dc3932cf4e9ed5727570325cacc716b9409ffc794a221c0ccc72e8eb775 \ + --hash=sha256:1b9f652e17a13bbc09b45f74c3c326cca72059ce4dcdef687fcb04389db413a9 \ + --hash=sha256:1db9cab6197e797824f0479dd2ae5fd2e272aa61c61b7faa5a6e450ca7a0d3b7 \ + --hash=sha256:1fdf5b52ed22f1404c2433d0b46df2d86d9befb742d0b9b4042a2325c43cdb97 \ + --hash=sha256:21ea3b00f1e87c66f3f9e92f9f6fec20495ac0e60d7d2e68ba1fc49739d7cf40 \ + --hash=sha256:23385c4626f8b5a7792aa381ea573d9432cfbf4c21375a0a5eed2a4fb64e0e58 \ + --hash=sha256:240fec32a74af0979f7253532859b3fd209ad5eea822416bb010929dba445c3d \ + --hash=sha256:24120c644322a35b5eb765416ac8442214c45689e255f5fc6e761e7e7c743c8b \ + --hash=sha256:2d4a043c820363359e40ac96270e1915e5376e116be1f59cee0701775de3e376 \ + --hash=sha256:31a1fb3ffbd1195b24490f671d12623dd4c9bfa56d6af0a299fa43b516134f5b \ + --hash=sha256:33c6800ab9563adaa0d7893e9cd8f5b972ef1e6a23df70498e05c24f3fcec5e1 \ + --hash=sha256:36917e29c0b815d6b940bc91560fcf453b5b85d55c579796373f0ea11d1c111a \ + --hash=sha256:36d8b9725684ca3bf658b90a101d335fc8cae405bb22196b7b5ef41344913756 \ + --hash=sha256:3fbcd402e4b607d4b8de5d4591215f31f042af03f9d09e58f693a6221e32833f \ + --hash=sha256:41ed8abeffda6a880207fdc0cd1343a448cec3cc6b38b53f730c0745f745292d \ + --hash=sha256:43a8fd56f3c681f3cda2d5c842ad28e88241df742dfcbe8fd1dd437a3056e1f6 \ + --hash=sha256:48e944091b7bff233920bed8a60b0ed6c96c73d3c5836bf51c4b9a4bfb58e4ca \ + --hash=sha256:4ac996e42a93d80122a23edce44e02c6b9c4913f5c6198a077da31475341a39a \ + --hash=sha256:4e9392ddf67fe97ab9889f796d468c9908a8754b8b6c7520eb03a65012b9a1e7 \ + --hash=sha256:4ea01f30e119f1ec7a2034e85c40f491d469896aef02cd6436fd9292fe1be3fb \ + --hash=sha256:4eb54c25e3463d73e1e4ed439ffc45d1d7e590df40bc7bb06f11a906afd996c3 \ + --hash=sha256:523fe6574aea50f456cbc301c1cdddcbf237939f7657f9af99b4f0f71942b59c \ + --hash=sha256:5299296ff5cafcb69851fd652de8f9268a0bb036bcbedbfcaa94cf743baf3986 \ + --hash=sha256:53bd25e8ab54c02bb4c55d4848c1d36f75f3b9a7356e83179fe27794d9e9e6c4 \ + --hash=sha256:57bccd3e1128736c792094da1feac6da46ec3feb1957ef649a9b3e4f8de6fa04 \ + --hash=sha256:596921489390a002d6a5e303384a0f1f98384f8bb550fbf38a1aafa6093da0f9 \ + --hash=sha256:5d3cd8be71c4da3c9b42cc92f17e5e3036bdca98bf18555a0e072c3fe87abbb4 \ + --hash=sha256:61e13100b30d8d25fc0d1f0297014b32941bf0975a3584bd08f3adebb68d1ead \ + --hash=sha256:6a438cb8a3f87f2c49e82e43fb19511d4d5043f53e0787dc4df937ccb904373a \ + --hash=sha256:6c25b8826be85714b27dafe908d3461e1b09e3e31c0279a7029040c832c781ef \ + --hash=sha256:6d47b2d0e00e030e0ebdb08115a37da7294e7a6d196d134a77ed7ee6c142284b \ + --hash=sha256:6e731bf5a5e8e25398544d7709e39068ec4230b0177e4db7fa955692fd415ba3 \ + --hash=sha256:6eb6ff07964f39e33c6f57fefe939514e76b8c0d4a694e5663e2e04e80cc1798 \ + --hash=sha256:6f5a432c6c1ef18a1a02a04b3616b8d3f2f88b13faa46caae1ac6559ab202234 \ + --hash=sha256:6ffb4dce3ecb2cd563aa30f9eb5088a1cf1f4f688b90cbb374fa96e49fc50b02 \ + --hash=sha256:713d7c33c0e231e3786eb0ec95885814dd65d5f3a09754399b6acbf5e9365f11 \ + --hash=sha256:783abdfac21247d33bec40aa683d26bba30d61655c40de38b2decc4797aabf92 \ + --hash=sha256:7c539d452cba22f250103c7d1c00cbe9038eb705e657476e701c43374efd9f2e \ + --hash=sha256:823b222c4fc861a82c650998a6117ecbd150cc71bf8b78f9a31bf3f48c456108 \ + --hash=sha256:83d6f42ae6eba44548a1a3d4f900a5b1500fdd14f09f04754a8800ab0d544432 \ + --hash=sha256:8431ecb131489cbdaf6f51f7ade47475f73aff4f1912552f75ba19a6cfe233a8 \ + --hash=sha256:848984e231190ae49bc8b5229e64d2a6fba4231dc6046e1b6b28ddc060bc7601 \ + --hash=sha256:8772c2c0091e6fc2d0499151793d31bf811826986b0727052935a95104845544 \ + --hash=sha256:8835566b3e94026da3ce2249f9ee35c9d2ebe4cd131f6a39bfc82708d3010a46 \ + --hash=sha256:91238117cd296352f54f24117ff5a227bdea428865dba1244aa0cdd1424c0bc2 \ + --hash=sha256:91315aea7adb51821d4a3378ede953bfdd76e3bf71551de4a82d7638cef7dc02 \ + --hash=sha256:91c92856b846aa690ea6239ffd6c5003c5e3e296a69095570bf4efee5bfa0bdc \ + --hash=sha256:92acf02c770b9d5c4e9a3e8d64e9d95ae26456a86ebb7b7cf64a97f899e5871b \ + --hash=sha256:93bedc68c4138088ea3c042189c01d2945f530f0dcc36e1ea02df8418bd4f1cd \ + --hash=sha256:93eb41d626d9a2749a678823c70bba8c25033cef474ae8001fd7621adbbd8437 \ + --hash=sha256:95838be125133fa314753f8cc5168f7c41e3a9dd5eef5383dbbbeed511bd4853 \ + --hash=sha256:9667637665acba166a7dbbc786f14c50512d3a01f7199e0e1a13b929e92d7c7a \ + --hash=sha256:98722314e12a8e4fa346e0afd3fff2098b5732875ff45592b450a7b589b43c4f \ + --hash=sha256:99e1d482138a71902415ba375bb4627679a5fefd3bd07803c3a8f37632f4981d \ + --hash=sha256:9a8a62281fd9c6a373b3fd3d473685e6838d43341eecde5f47729748e2c11ee3 \ + --hash=sha256:9b01a092955c1c7530196ad526755cff4f2d11f37ca9a6fc9ebe37ca49a48977 \ + --hash=sha256:9b2383933b1dd7a8c93e02f99ebfd305f1286057172483b8e6898bed3fef69d3 \ + --hash=sha256:9e7460abce637f90e37f8c443559fbd687c38884d05cb69ef265e634945f4324 \ + --hash=sha256:9eff570402175c7d5fff8497d5a9c05580664a79bb66c07b8f205d17f69e580b \ + --hash=sha256:9fcde11462e0faab4c2a2ae8cd4516f7dd8239eec583bd88c5524d8064ad2d0c \ + --hash=sha256:a1b4d60bad57d6e0e7cd30cc4d1dc646d258dd6dfb7935ce76c7dd72dd1e98b0 \ + --hash=sha256:a3317630e2791f56071b82f89bb71624146125e4311e6643f708c6ba744b5ac5 \ + --hash=sha256:a53d5f11cd6d15569ba3e53f3498077f0d5e633f6ec604d2ea613e895017ebee \ + --hash=sha256:a55c1163e829a08707b4a92c8ee02a5a3a3847ced74aa4da94e3e975ffe1ec63 \ + --hash=sha256:a59db03b80a156a6fbeae41096866d1cb89cdf31031ba44c768a804538880636 \ + --hash=sha256:b0b25d647036972c2951059046cb498f72b7af5a2def27bf8e5f44c3067ab7dd \ + --hash=sha256:b1adf2f37521b578c438f6ec61d1c1d0c20b076863ddd5d8b6f3ab658857db3f \ + --hash=sha256:b29b7c5adf4b2be7c941f7f76432aa3269cd5052ff8610376b8ece24d7b02c51 \ + --hash=sha256:b381574614245f18fdb2d6d1aa8f149e225869c47b5ee7f81812de8977817314 \ + --hash=sha256:b3e681208e2d39fde13fbea449ef2736a85ec1ea2afa41e9193a8c536e71dafe \ + --hash=sha256:b62c89545d21d930c333208fd548e7b4f262228ba3a322fd75c987a75ab11f31 \ + --hash=sha256:b7abcfaf66c1dc14c5ed928c349614f223c972a8b021b4be9548dbea6c817371 \ + --hash=sha256:bb6d49f00e2babd40af4889d6b9f4bf50879f522dd4e2e22cb705cac5e0c5e8e \ + --hash=sha256:bd1fe69056a19c377a6c6de2c1240fb8676ba0512a969965efb486510c5e0f1f \ + --hash=sha256:bd6ecae08c0d871cf93b6f0d4ee76b06c65be44ea22311981a022b2f1948cf9d \ + --hash=sha256:cc488daac19e6f5292180fac5e0ef7efffabeef4e096af9a17a75a7d3d6781f6 \ + --hash=sha256:d18d56c1ec63b304f4bb3c7db7e0d311e61cca1b1c944590a4b13ac39b2ca92b \ + --hash=sha256:d3d049b557875f9d15230eec8a731749a7a873d8ed6a051903b05bdf46e0e65f \ + --hash=sha256:d684ff87d00de2f6abcbbfa9b0fdce8b513dd799a900809dcfa7eb62ad69832a \ + --hash=sha256:d79aaa468193c1bed6b0560d3c5c9f9e369e3efb0b128b38a2537e2d51d42773 \ + --hash=sha256:e2e6240a0a5ad847017b3d65187877fdb7a11d5a58d43c30736f9f523478913b \ + --hash=sha256:e3d824b6152e1c0fd58dc6266f2de470eb7b2286135294f5f7cd26a0f0693738 \ + --hash=sha256:e5c801bdf3cc92274b8fc2c7023c8751e5ae81260c009ac558ff55ce712a2d45 \ + --hash=sha256:efad0bdc67d3b119c3bab6b94d341a27333c2bb89ed75dfa182a4a06cd0ea329 \ + --hash=sha256:f07c965a8aa6d62145f122641f292129984f39a3e3697585b19298a1488287db \ + --hash=sha256:f142cd663d1148ed32993ed9a58e5c67ea312f446a01a6c13a956945aafc9f23 \ + --hash=sha256:f73930dacad5e611fe019e18df6f53c30629b1843d038d303b8dec86a32493bd \ + --hash=sha256:f81df206b937d0dca5efb38c25fc99037d82dbcd3740e34c3f81997d2322ac1c \ + --hash=sha256:fd9aa71a800c87c0f7ab53fa38009169a23a82c685989626ca74a4bd483e1b67 python-frontmatter==1.3.0 \ --hash=sha256:9f7dd9260bec99044219159a329f64f039087f9d1a2124c9442556f2fe6f82ec \ --hash=sha256:acc73e477a568dc2a25c9e130c6c68ae8daa8c204c8f7e813db47d6a7280dcf2 diff --git a/ls/core/herdr_ops/layout.py b/ls/core/herdr_ops/layout.py index 660a36e2a..36cb18560 100644 --- a/ls/core/herdr_ops/layout.py +++ b/ls/core/herdr_ops/layout.py @@ -5,6 +5,7 @@ from datetime import datetime, timezone import hashlib import os +import stat from pathlib import Path import time from typing import Any, Mapping @@ -147,6 +148,7 @@ def create_owned_pane( owner_path = _owner_path(root, identity) if owner_path.is_symlink(): raise HerdrOpsError("owner_conflict", "The requested owner ID has unsafe existing state.") + reservation_record: dict[str, Any] | None = None if owner_path.exists(): existing = _owner_record(root, identity) reserved = ( @@ -161,10 +163,13 @@ def create_owned_pane( ) if not reserved: raise HerdrOpsError("owner_conflict", "The requested owner ID already exists; refusing to overwrite it.") + reservation_record = existing caller = get_pane(endpoint, caller_pane_id, workspace_id=workspace_id, state_dir=root) before = request(endpoint, "pane.layout", {"pane_id": caller_pane_id}).get("layout") if not isinstance(before, dict) or before.get("tab_id") != caller.get("tab_id"): raise HerdrOpsError("invalid_response", "Herdr did not return the caller pane's current layout.") + # Herdr's split ratio sizes the first (parent) pane, whereas the + # managed owner record's ratio is the desired new child fraction. result = request( endpoint, "pane.split", @@ -172,7 +177,7 @@ def create_owned_pane( "workspace_id": workspace_id, "target_pane_id": caller_pane_id, "direction": "down", - "ratio": float(ratio), + "ratio": 1.0 - float(ratio) if purpose == "managed_ops" else float(ratio), "cwd": str(working_directory), "focus": False, **({"env": dict(environment)} if environment is not None else {}), @@ -186,7 +191,10 @@ def create_owned_pane( terminal_id = pane.get("terminal_id") if not isinstance(new_pane_id, str) or not new_pane_id or not isinstance(terminal_id, str) or not terminal_id: raise HerdrOpsError("split_unconfirmed", "Herdr returned incomplete pane identity; reconcile before retrying.", "unknown") - _check_workspace(pane, workspace_id, field="created pane") + try: + _check_workspace(pane, workspace_id, field="created pane") + except HerdrOpsError: + raise HerdrOpsError("split_scope_unknown", "Herdr created a pane but its returned workspace scope is unverified.", "unknown") from None owner = { "schema_version": 1, "owner_id": identity, @@ -205,6 +213,13 @@ def create_owned_pane( } if owner_metadata is not None: owner["owner_metadata"] = dict(owner_metadata) + replacement = reservation_record.get("replacement") if reservation_record is not None else None + generation_id = reservation_record.get("generation_id") if reservation_record is not None else None + if isinstance(replacement, dict) and isinstance(generation_id, str): + owner["generation_id"] = generation_id + owner["replacement"] = dict(replacement) | {"state": "owned"} + if reservation_record is not None and isinstance(reservation_record.get("retired_creation"), dict): + owner["retired_creation"] = dict(reservation_record["retired_creation"]) try: _write_owner(root, owner) except HerdrOpsError: @@ -253,6 +268,431 @@ def _new_pane_idle_signature(endpoint: str, pane: Mapping[str, Any]) -> str: time.sleep(0.2) +def _complete_endpoint_inventory(endpoint: str) -> list[dict[str, Any]]: + response = request(endpoint, "session.snapshot", {}) + body: Any = response + for _ in range(3): + if not isinstance(body, dict): + break + if isinstance(body.get("snapshot"), dict): + body = body["snapshot"] + elif isinstance(body.get("result"), dict): + body = body["result"] + else: + break + panes = body.get("panes") if isinstance(body, dict) else None + if (not isinstance(panes, list) + or any(not isinstance(pane, dict) + or not isinstance(pane.get("pane_id"), str) or not pane["pane_id"] + or not isinstance(pane.get("terminal_id"), str) or not pane["terminal_id"] + for pane in panes)): + raise HerdrOpsError("inventory_unknown", "Herdr did not return a complete endpoint pane inventory.") + pane_ids = [pane["pane_id"] for pane in panes] + terminal_ids = [pane["terminal_id"] for pane in panes] + if len(set(pane_ids)) != len(pane_ids) or len(set(terminal_ids)) != len(terminal_ids): + raise HerdrOpsError("inventory_ambiguous", "Herdr's endpoint pane inventory contains ambiguous identities.") + return panes + + +def _replacement_source( + endpoint: str, + root: Path, + identity: str, + workspace_id: str, + caller_pane_id: str, + cwd: str, + ratio: float, + owner_metadata: Mapping[str, Any], + previous_binding: Mapping[str, Any], +) -> tuple[dict[str, Any], bytes, dict[str, Any] | None]: + binding_key = previous_binding.get("binding_key") + repository = owner_metadata.get("repository") + host_alias = owner_metadata.get("host_alias") + if (not isinstance(repository, str) or not repository + or (host_alias is not None and (not isinstance(host_alias, str) or not host_alias))): + raise HerdrOpsError("replacement_scope_invalid", "The managed owner metadata cannot identify its exact pane name.") + expected_name = f"{repository}{'-' + host_alias if host_alias is not None else ''}-sudo" + pane_id = previous_binding.get("pane_id") + endpoint_value = previous_binding.get("endpoint") + if (previous_binding.get("name") != expected_name + or previous_binding.get("state") not in ("ready", "busy") + or previous_binding.get("backend") != "herdr" + or binding_key != owner_metadata.get("binding_key") + or not isinstance(binding_key, str) or binding_key[:32] != identity + or not isinstance(pane_id, str) or not pane_id or pane_id == caller_pane_id + or not isinstance(previous_binding.get("terminal_id"), str) + or not previous_binding.get("terminal_id") + or not isinstance(endpoint_value, str) or not endpoint_value + or _endpoint_identity(endpoint_value) != _endpoint_identity(endpoint) + or previous_binding.get("parent_id") != caller_pane_id + or previous_binding.get("workspace_id") != workspace_id + or previous_binding.get("repository") != repository + or previous_binding.get("controller_id") != owner_metadata.get("controller_id") + or previous_binding.get("host_alias") != host_alias + or not isinstance(previous_binding.get("shell_path"), str) + or Path(previous_binding["shell_path"]).name != "bash" + or isinstance(previous_binding.get("shell_pid"), bool) + or not isinstance(previous_binding.get("shell_pid"), int) + or previous_binding.get("shell_pid") <= 0 + or not isinstance(previous_binding.get("pane_tty"), str) + or not previous_binding.get("pane_tty") + or not isinstance(previous_binding.get("machine_generation"), str) + or not previous_binding.get("machine_generation")): + raise HerdrOpsError("replacement_binding_mismatch", "The previous managed pane binding is not an exact Herdr generation.") + path = _owner_path(root, identity) + try: + owner_info = path.lstat() + except OSError: + raise HerdrOpsError("replacement_owner_missing", "The exact managed pane owner record is unavailable.") from None + if (not stat.S_ISREG(owner_info.st_mode) or owner_info.st_uid != os.getuid() + or owner_info.st_mode & 0o077): + raise HerdrOpsError("replacement_owner_unsafe", "The exact managed pane owner record is not owner-private.") + current = _owner_record(root, identity) + marker = current.get("replacement") + if current.get("state") == "owned": + source = current + try: + source_bytes = path.read_bytes() + except OSError: + raise HerdrOpsError("replacement_owner_missing", "The exact managed pane owner record cannot be preserved.") from None + prior_marker = None + elif (current.get("state") == "failed" and isinstance(marker, dict) + and marker.get("state") == "failed"): + digest = marker.get("previous_owner_sha256") + archive = marker.get("owner_archive") + generation = marker.get("generation_id") + expected_archive = f"panes/archive/{identity}/{digest}.json" + if (not isinstance(digest, str) or len(digest) != 64 + or any(char not in "0123456789abcdef" for char in digest) + or archive != expected_archive + or not isinstance(generation, str) or len(generation) != 32 + or any(char not in "0123456789abcdef" for char in generation)): + raise HerdrOpsError("replacement_archive_invalid", "The previous owner archive receipt is invalid.") + archive_path = root / archive + source_bytes = _read_archive_file(archive_path) + if hashlib.sha256(source_bytes).hexdigest() != digest: + raise HerdrOpsError("replacement_archive_invalid", "The immutable previous owner archive has changed.") + source = read_json_state(archive_path) + prior_marker = dict(marker) + if (current.get("owner_id") != identity + or current.get("pane_id") != previous_binding.get("pane_id") + or current.get("terminal_id") != previous_binding.get("terminal_id") + or current.get("endpoint") != _endpoint_identity(endpoint) + or current.get("workspace_id") != workspace_id + or current.get("caller_pane_id") != caller_pane_id + or current.get("cwd") != cwd + or current.get("ratio") != float(ratio) + or current.get("owner_metadata") != dict(owner_metadata) + or marker.get("previous_pane_id") != previous_binding.get("pane_id") + or marker.get("previous_terminal_id") != previous_binding.get("terminal_id")): + raise HerdrOpsError("replacement_owner_mismatch", "The failed replacement reservation does not match its original generation.") + else: + raise HerdrOpsError("replacement_owner_conflict", "The managed pane owner is active, unknown, or already reserved.") + if (source.get("owner_id") != identity + or source.get("purpose") != "managed_ops" + or source.get("state") != "owned" + or source.get("endpoint") != _endpoint_identity(endpoint) + or source.get("workspace_id") != workspace_id + or source.get("caller_pane_id") != caller_pane_id + or source.get("pane_id") != previous_binding.get("pane_id") + or source.get("terminal_id") != previous_binding.get("terminal_id") + or source.get("cwd") != cwd + or source.get("ratio") != float(ratio) + or source.get("owner_metadata") != dict(owner_metadata)): + raise HerdrOpsError("replacement_owner_mismatch", "The original managed pane owner metadata does not match this exact scope.") + panes = _complete_endpoint_inventory(endpoint) + parents = [pane for pane in panes if pane["pane_id"] == caller_pane_id] + if (len(parents) != 1 or parents[0].get("workspace_id") != workspace_id + or not isinstance(parents[0].get("tab_id"), str) + or source.get("tab_id") != parents[0].get("tab_id")): + raise HerdrOpsError("replacement_parent_mismatch", "The exact original Herdr parent is not present in its recorded scope.") + if any(pane["pane_id"] == previous_binding["pane_id"] + or pane["terminal_id"] == previous_binding["terminal_id"] for pane in panes): + raise HerdrOpsError("replacement_generation_present", "The original pane or stable terminal identity is still present on this endpoint.") + from ..terminal_ops.shell import machine_generation + try: + current_generation = machine_generation() + except Exception: + raise HerdrOpsError("shell_generation_unknown", "The original controlled Bash generation cannot be qualified.") from None + if previous_binding["machine_generation"] == current_generation: + if not Path("/proc").is_dir(): + raise HerdrOpsError("shell_generation_unknown", "The original controlled Bash process cannot be verified as gone.") + try: + os.stat(f"/proc/{previous_binding['shell_pid']}") + except FileNotFoundError: + pass + except OSError: + raise HerdrOpsError("shell_generation_unknown", "The original controlled Bash process cannot be verified as gone.") from None + else: + raise HerdrOpsError("shell_generation_live", "The original controlled Bash process is still live; replacement is blocked.") + return source, source_bytes, prior_marker + + +def verify_missing_managed_pane( + endpoint: str, + workspace_id: str, + caller_pane_id: str, + cwd: str | os.PathLike[str], + owner_id: str, + previous_binding: Mapping[str, Any], + *, + state_dir: str | os.PathLike[str] | None = None, + ratio: float = 1 / 3, + owner_metadata: Mapping[str, Any], +) -> None: + """Prove one exact managed generation absent without changing its owner record.""" + identity = validate_owner_id(owner_id) + working_directory = Path(cwd).expanduser() + root = state_directory(state_dir, create=False) + with writer_lock(root): + _replacement_source( + endpoint, root, identity, workspace_id, caller_pane_id, + str(working_directory), ratio, owner_metadata, previous_binding, + ) + + +def _fsync_directory(path: Path) -> None: + descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) + + +def _archive_directory(parent: Path, name: str) -> Path: + path = parent / name + try: + path.mkdir(mode=0o700) + except FileExistsError: + pass + except OSError: + raise HerdrOpsError("owner_archive_failed", "Could not prepare the immutable managed pane owner archive.") from None + try: + info = path.lstat() + except OSError: + raise HerdrOpsError("unsafe_owner_archive", "The immutable managed pane owner archive is unavailable.") from None + if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o077): + raise HerdrOpsError("unsafe_owner_archive", "The immutable managed pane owner archive is not owner-private.") + try: + _fsync_directory(parent) + except OSError: + raise HerdrOpsError("owner_archive_failed", "Could not persist the immutable managed pane owner archive directory.") from None + return path + + +def _read_archive_file(path: Path) -> bytes: + descriptor: int | None = None + try: + descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW) + info = os.fstat(descriptor) + if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o077): + raise HerdrOpsError("unsafe_owner_archive", "The immutable managed pane owner archive file is unsafe.") + with os.fdopen(descriptor, "rb") as stream: + descriptor = None + return stream.read() + except HerdrOpsError: + raise + except OSError: + raise HerdrOpsError("owner_archive_invalid", "The immutable managed pane owner archive cannot be read safely.") from None + finally: + if descriptor is not None: + try: + os.close(descriptor) + except OSError: + pass + + +def _archive_owner_bytes(root: Path, identity: str, raw: bytes) -> tuple[str, str]: + digest = hashlib.sha256(raw).hexdigest() + owners = root / "panes" + try: + info = owners.lstat() + except OSError: + raise HerdrOpsError("unsafe_owner_state", "Managed pane ownership state is unavailable.") from None + if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o077): + raise HerdrOpsError("unsafe_owner_state", "Managed pane ownership state is not a safe directory.") + archive_root = _archive_directory(owners, "archive") + archive_dir = _archive_directory(archive_root, identity) + path = archive_dir / f"{digest}.json" + descriptor: int | None = None + created = False + try: + descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + created = True + os.fchmod(descriptor, 0o600) + with os.fdopen(descriptor, "wb") as stream: + descriptor = None + stream.write(raw) + stream.flush() + os.fsync(stream.fileno()) + _fsync_directory(archive_dir) + except FileExistsError: + if _read_archive_file(path) != raw: + raise HerdrOpsError("owner_archive_collision", "The immutable managed pane owner archive conflicts with different bytes.") + try: + _fsync_directory(archive_dir) + except OSError: + raise HerdrOpsError("owner_archive_failed", "Could not persist the immutable managed pane owner archive.") from None + except OSError: + cleanup_failed = False + if descriptor is not None: + try: + os.close(descriptor) + except OSError: + cleanup_failed = True + if created: + try: + path.unlink(missing_ok=True) + _fsync_directory(archive_dir) + except OSError: + cleanup_failed = True + if cleanup_failed: + raise HerdrOpsError("owner_archive_cleanup_failed", "Could not safely remove the incomplete owner archive; no pane was created.") from None + raise HerdrOpsError("owner_archive_failed", "Could not preserve the original managed pane owner bytes; no pane was created.") from None + return f"panes/archive/{identity}/{digest}.json", digest + + +def _retirement_owner(root: Path, endpoint: str, identity: str, workspace: str, + parent: str, cwd: str, metadata: Mapping[str, Any], + pane_id: str, terminal_id: str) -> dict[str, Any]: + path = _owner_path(root, identity) + _read_archive_file(path) + owner = read_json_state(path) + before = owner.get("before_layout") + if (owner.get("schema_version") != 1 or owner.get("owner_id") != identity + or owner.get("purpose") != "managed_ops" + or owner.get("endpoint") != _endpoint_identity(endpoint) + or owner.get("workspace_id") != workspace or owner.get("caller_pane_id") != parent + or owner.get("cwd") != cwd or owner.get("ratio") != 1 / 3 + or owner.get("owner_metadata") != dict(metadata) + or not isinstance(owner.get("created_at"), str) or not owner["created_at"] + or not isinstance(before, dict) or before.get("tab_id") != owner.get("tab_id") + or owner.get("pane_id") != pane_id or owner.get("terminal_id") != terminal_id + or pane_id == parent): + raise HerdrOpsError("retirement_owner_mismatch", "The managed owner does not match the exact retirement scope.") + # Any duplicate claim is ambiguous, including malformed or unsafe records. + for other in (root / "panes").glob("*.json"): + if other == path: + continue + _read_archive_file(other) + candidate = read_json_state(other) + other_metadata = candidate.get("owner_metadata") + if ((candidate.get("endpoint") == _endpoint_identity(endpoint) + and (candidate.get("pane_id") == pane_id or candidate.get("terminal_id") == terminal_id)) + or candidate.get("owner_id") == identity + or (isinstance(other_metadata, dict) + and other_metadata.get("binding_key") == metadata.get("binding_key"))): + raise HerdrOpsError("retirement_owner_ambiguous", "Another owner record claims the pane or terminal.") + return owner + + +def _retirement_absent(endpoint: str, pane_id: str, terminal_id: str) -> bool: + return not any(row["pane_id"] == pane_id or row["terminal_id"] == terminal_id + for row in _complete_endpoint_inventory(endpoint)) + + +def retire_managed_creation(endpoint: str, workspace: str, parent: str, cwd: str, + identity: str, metadata: Mapping[str, Any], pane_id: str, + terminal_id: str, retirement_id: str, reservation: bytes, + qualify: Any, fence: Any, *, + state_dir: str | os.PathLike[str] | None = None) -> dict[str, Any]: + """Archive and fence one exact uninitialized managed owner before one close.""" + root = state_directory(state_dir, create=False) + identity = validate_owner_id(identity) + with writer_lock(root): + owner = _retirement_owner(root, endpoint, identity, workspace, parent, cwd, + metadata, pane_id, terminal_id) + if owner.get("state") != "owned" or owner.get("retirement") is not None: + raise HerdrOpsError("retirement_owner_conflict", "The owner is not an unretired owned generation.") + panes = _complete_endpoint_inventory(endpoint) + matches = [row for row in panes if row["pane_id"] == pane_id] + parents = [row for row in panes if row["pane_id"] == parent] + if (len(matches) != 1 or len(parents) != 1 + or matches[0]["terminal_id"] != terminal_id + or matches[0].get("workspace_id") != workspace + or parents[0].get("workspace_id") != workspace + or not isinstance(owner.get("tab_id"), str) + or matches[0].get("tab_id") != owner["tab_id"] + or parents[0].get("tab_id") != owner["tab_id"]): + raise HerdrOpsError("retirement_native_mismatch", "The native pane, stable terminal, parent or tab changed.") + observation = qualify(matches[0]) + owner_raw = _read_archive_file(_owner_path(root, identity)) + owner_archive, owner_digest = _archive_owner_bytes(root, identity, owner_raw) + reservation_archive, reservation_digest = _archive_owner_bytes(root, identity, reservation) + marker = {"retirement_id": retirement_id, "pane_id": pane_id, "terminal_id": terminal_id, + "state": "unknown", "owner_archive": owner_archive, + "owner_sha256": owner_digest, "reservation_archive": reservation_archive, + "reservation_sha256": reservation_digest, "observation": observation} + # Both journals are durable before close. Either incomplete fence prevents replay. + fence(marker) + owner["retirement"] = marker + owner["state"] = "retirement_unknown" + _write_owner(root, owner) + try: + current = _complete_endpoint_inventory(endpoint) + rows = [row for row in current if row["pane_id"] == pane_id] + if (len(rows) != 1 or rows[0] != matches[0] + or qualify(rows[0]) != observation): + return marker + request(endpoint, "pane.close", {"pane_id": pane_id}, mutation=True) + except Exception: + return marker + if _retirement_absent(endpoint, pane_id, terminal_id): + marker["state"] = "retired" + owner["state"] = "retired" + owner["retirement"] = marker + _write_owner(root, owner) + return marker + + +def reconcile_managed_creation_retirement(endpoint: str, workspace: str, parent: str, + cwd: str, identity: str, metadata: Mapping[str, Any], + marker: Mapping[str, Any], *, + state_dir: str | os.PathLike[str] | None = None, + reservation: Mapping[str, Any] | None = None) -> dict[str, Any]: + root = state_directory(state_dir, create=False) + identity = validate_owner_id(identity) + with writer_lock(root): + owner = _retirement_owner(root, endpoint, identity, workspace, parent, cwd, metadata, + marker["pane_id"], marker["terminal_id"]) + saved = owner.get("retirement") + if (not isinstance(saved, dict) or any(saved.get(key) != marker.get(key) + for key in ("retirement_id", "pane_id", "terminal_id", "owner_sha256", + "reservation_sha256", "observation"))): + raise HerdrOpsError("retirement_fence_mismatch", "The retirement journals do not agree.") + for prefix in ("owner", "reservation"): + digest = saved.get(f"{prefix}_sha256") + archive = saved.get(f"{prefix}_archive") + if (not isinstance(digest, str) or len(digest) != 64 + or any(ch not in "0123456789abcdef" for ch in digest) + or not isinstance(archive, str) + or archive != f"panes/archive/{identity}/{digest}.json" + or hashlib.sha256(_read_archive_file(root / archive)).hexdigest() != digest): + raise HerdrOpsError("retirement_archive_invalid", "The original retirement archive is invalid.") + archived_owner = read_json_state(root / saved["owner_archive"]) + original_owner = dict(owner) + original_owner.pop("retirement", None) + original_owner["state"] = "owned" + if archived_owner != original_owner: + raise HerdrOpsError("retirement_archive_invalid", "The archived owner differs from the fenced original owner.") + archived_reservation = read_json_state(root / saved["reservation_archive"]) + if reservation is not None and archived_reservation != dict(reservation): + raise HerdrOpsError("retirement_archive_invalid", "The original creation reservation changed.") + if owner.get("state") not in {"retired", "retirement_unknown"}: + raise HerdrOpsError("retirement_fence_mismatch", "The owner is not fenced for this retirement.") + if _retirement_absent(endpoint, marker["pane_id"], marker["terminal_id"]): + saved = dict(saved) | {"state": "retired"} + if owner.get("state") != "retired": + owner["state"] = "retired" + owner["retirement"] = saved + _write_owner(root, owner) + else: + saved = dict(saved) | {"state": "unknown"} + return saved + def ensure_owned_pane( endpoint: str, workspace_id: str, @@ -265,6 +705,8 @@ def ensure_owned_pane( purpose: str = "pane", owner_metadata: Mapping[str, Any] | None = None, environment: Mapping[str, str] | None = None, + previous_binding: Mapping[str, Any] | None = None, + retirement_proof: Mapping[str, Any] | None = None, ) -> dict[str, Any]: """Create a stable owned pane once, or return it only if its idle baseline is unchanged.""" identity = validate_owner_id(owner_id) @@ -277,6 +719,8 @@ def ensure_owned_pane( if path.is_symlink(): raise HerdrOpsError("owner_conflict", "The stable owner ID has unsafe existing state.") if not path.exists(): + if previous_binding is not None: + raise HerdrOpsError("replacement_owner_missing", "The original managed pane owner record is unavailable.") reservation = { "schema_version": 1, "owner_id": identity, @@ -290,7 +734,10 @@ def ensure_owned_pane( "state": "creating", "created_at": datetime.now(timezone.utc).isoformat(), } - _write_owner(root, reservation) + try: + _write_owner(root, reservation) + except HerdrOpsError as exc: + raise HerdrOpsError(exc.code, f"{exc.message} Stable owner ID: {identity}.", "unknown") from None try: created = create_owned_pane( endpoint, @@ -307,22 +754,124 @@ def ensure_owned_pane( ) except HerdrOpsError as exc: reservation.update({"state": "unknown" if exc.effect == "unknown" else "failed", "error": exc.as_dict()}) + try: + _write_owner(root, reservation) + except HerdrOpsError: + raise HerdrOpsError(exc.code, f"{exc.message} Stable owner ID: {identity}.", "unknown") from None + raise HerdrOpsError(exc.code, f"{exc.message} Stable owner ID: {identity}.", exc.effect) from None + try: + owner, pane = _load_owned(endpoint, root, identity, allow_managed_ops=purpose == "managed_ops")[:2] + if pane is None: + raise HerdrOpsError("owned_pane_missing", "Herdr created the pane but it is no longer available.") + if purpose == "managed_ops": + if owner.get("owner_metadata") != dict(owner_metadata or {}): + raise HerdrOpsError("owner_scope_mismatch", "The managed pane owner metadata does not match its exact controller scope.") + else: + owner["idle_baseline"] = _new_pane_idle_signature(endpoint, pane) + _write_owner(root, owner) + except HerdrOpsError as exc: + raise HerdrOpsError(exc.code, f"{exc.message} Stable owner ID: {identity}.", "unknown") from None + return created + existing = _owner_record(root, identity) + if retirement_proof is not None: + if purpose != "managed_ops" or owner_metadata is None or previous_binding is not None: + raise HerdrOpsError("retirement_scope_invalid", "Only a managed owner can allocate after retirement.") + marker = reconcile_managed_creation_retirement( + endpoint, workspace_id, caller_pane_id, str(working_directory), identity, + owner_metadata, retirement_proof, state_dir=root, + ) + if marker.get("state") != "retired" or existing.get("state") != "retired": + raise HerdrOpsError("retirement_unresolved", "The original creation has not been conclusively retired.") + reservation = dict(existing) + reservation.update({"state": "creating", "retired_creation": dict(marker)}) + _write_owner(root, reservation) + try: + return create_owned_pane( + endpoint, workspace_id, caller_pane_id, working_directory, + state_dir=root, owner_id=identity, purpose=purpose, ratio=ratio, + owner_metadata=owner_metadata, environment=environment, _reservation=True, + ) + except HerdrOpsError as exc: + # No automatic retry even if the split failed before dispatch. + reservation["state"] = "unknown" if exc.effect == "unknown" else "failed" + _write_owner(root, reservation) + raise + if previous_binding is not None: + if purpose != "managed_ops" or owner_metadata is None: + raise HerdrOpsError("replacement_scope_invalid", "Only an exact managed-operations owner can replace a missing generation.") + _, source_bytes, prior_marker = _replacement_source( + endpoint, root, identity, workspace_id, caller_pane_id, + str(working_directory), ratio, owner_metadata, previous_binding, + ) + if prior_marker is None: + archive, digest = _archive_owner_bytes(root, identity, source_bytes) + generation_id = uuid.uuid4().hex + marker = { + "generation_id": generation_id, + "owner_archive": archive, + "previous_owner_sha256": digest, + "previous_pane_id": previous_binding["pane_id"], + "previous_terminal_id": previous_binding["terminal_id"], + "state": "creating", + } + else: + marker = prior_marker + generation_id = str(marker["generation_id"]) + marker["state"] = "creating" + reservation = dict(existing) + reservation.update({ + "state": "creating", + "generation_id": generation_id, + "replacement": marker, + "replacement_started_at": datetime.now(timezone.utc).isoformat(), + }) + try: _write_owner(root, reservation) + except HerdrOpsError as exc: + raise HerdrOpsError(exc.code, f"{exc.message} Stable owner ID: {identity}.", "unknown") from None + try: + created = create_owned_pane( + endpoint, workspace_id, caller_pane_id, working_directory, + state_dir=root, owner_id=identity, purpose=purpose, + ratio=ratio, owner_metadata=owner_metadata, + environment=environment, _reservation=True, + ) + except HerdrOpsError as exc: + try: + failed = _owner_record(root, identity) + failed["state"] = "failed" if exc.effect == "none" else "unknown" + failed["replacement"] = dict(marker) | { + "state": "failed" if exc.effect == "none" else "unknown", + } + failed["error"] = exc.as_dict() + _write_owner(root, failed) + except HerdrOpsError: + raise HerdrOpsError(exc.code, f"{exc.message} Stable owner ID: {identity}.", "unknown") from None raise HerdrOpsError(exc.code, f"{exc.message} Stable owner ID: {identity}.", exc.effect) from None - owner, pane = _load_owned(endpoint, root, identity, allow_managed_ops=purpose == "managed_ops")[:2] - if pane is None: - raise HerdrOpsError("owned_pane_missing", f"Herdr created the pane but it is no longer available. Stable owner ID: {identity}.", "unknown") - if purpose == "managed_ops": - if owner.get("owner_metadata") != dict(owner_metadata or {}): + try: + owner, pane, _ = _load_owned(endpoint, root, identity, allow_managed_ops=True) + if pane is None: + raise HerdrOpsError("owned_pane_missing", "Herdr created the pane but it is no longer available.") + if owner.get("owner_metadata") != dict(owner_metadata): raise HerdrOpsError("owner_scope_mismatch", "The managed pane owner metadata does not match its exact controller scope.") - else: + owner["generation_id"] = generation_id + owner["replacement"] = dict(marker) | {"state": "owned"} + _write_owner(root, owner) + except HerdrOpsError as exc: try: - owner["idle_baseline"] = _new_pane_idle_signature(endpoint, pane) - except HerdrOpsError as exc: - raise HerdrOpsError(exc.code, f"{exc.message} Stable owner ID: {identity}.", exc.effect) from None - _write_owner(root, owner) + current = _owner_record(root, identity) + if (current.get("state") == "owned" + and current.get("generation_id") == generation_id + and current.get("replacement") == (dict(marker) | {"state": "owned"}) + and current.get("owner_metadata") == dict(owner_metadata)): + current["state"] = "unknown" + current["replacement"] = dict(marker) | {"state": "unknown"} + current["error"] = {"code": exc.code, "message": exc.message, "effect": "unknown"} + _write_owner(root, current) + except HerdrOpsError: + pass + raise HerdrOpsError(exc.code, f"{exc.message} Stable owner ID: {identity}.", "unknown") from None return created - existing = _owner_record(root, identity) if existing.get("endpoint") != _endpoint_identity(endpoint): raise HerdrOpsError("endpoint_mismatch", "The stable owner ID belongs to a different Herdr endpoint.") if existing.get("purpose") != purpose or existing.get("state") != "owned": diff --git a/ls/core/s3_sdk/exports.py b/ls/core/s3_sdk/exports.py index 9b526af3b..20b6661c8 100644 --- a/ls/core/s3_sdk/exports.py +++ b/ls/core/s3_sdk/exports.py @@ -21,7 +21,7 @@ def export(root: Path) -> bytes: data = result.stdout entries = [line for line in data.decode("utf-8").splitlines() if "==" in line] names = {line.split("==", 1)[0] for line in entries} - if names != PACKAGES or not any(line.startswith("boto3==1.43.118 ") for line in entries): + if names != PACKAGES or not any(line.startswith("boto3==1.43.122 ") for line in entries): raise ValueError("S3 SDK export does not match the reviewed dependency graph") blocks = data.decode("utf-8").replace("\\\n", "").splitlines() if len(blocks) != len(PACKAGES) or any( diff --git a/ls/core/sdk_payload/build.py b/ls/core/sdk_payload/build.py index acf6fab22..15709cdcf 100644 --- a/ls/core/sdk_payload/build.py +++ b/ls/core/sdk_payload/build.py @@ -2,8 +2,10 @@ from __future__ import annotations from importlib.util import module_from_spec, spec_from_file_location +import os from pathlib import Path import shutil +import stat from setuptools.command.build_py import build_py @@ -58,7 +60,26 @@ def run(self) -> None: target = config / name if target.is_symlink() or (target.exists() and _dependencies.regular_bytes(target) != data): raise ValueError("Stale dependency build output: use a fresh build directory") + tools = build_root / "ls" / "tools" + askpass = tools / "sudo_askpass" + if tools.is_symlink() or (tools.exists() and not tools.is_dir()) or askpass.is_symlink(): + raise ValueError("Askpass build destination must not contain symlinks") super().run() + if tools.is_symlink() or (tools.exists() and not tools.is_dir()) or askpass.is_symlink(): + raise ValueError("Askpass build destination must not contain symlinks") + if askpass.exists(): + tools_fd = os.open(tools, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + fd = os.open("sudo_askpass", os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=tools_fd) + try: + info = os.fstat(fd) + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1: + raise ValueError("Askpass build output must be a regular, unlinked file") + os.fchmod(fd, 0o755) + finally: + os.close(fd) + finally: + os.close(tools_fd) config.mkdir(parents=True, exist_ok=True) for name, data in dependency_data.items(): (config / name).write_bytes(data) diff --git a/ls/core/terminal_ops/backends.py b/ls/core/terminal_ops/backends.py index dc3cab8fa..41b2b2d77 100644 --- a/ls/core/terminal_ops/backends.py +++ b/ls/core/terminal_ops/backends.py @@ -5,20 +5,19 @@ import os from pathlib import Path import re -import shlex import shutil import socket import stat import struct import subprocess import time -from typing import Any, Protocol +from typing import Any, Mapping, Protocol from ..herdr_ops import layout as herdr_layout from ..herdr_ops import transport as herdr_transport from .credentials import child_environment from .models import PaneBinding, PaneError, PaneRequest, PaneStatus -from .shell import DISPATCH_KEY, machine_generation, read_prompt_state, write_bash_bootstrap +from .shell import DISPATCH_KEY, machine_generation, read_prompt_state, write_bash_bootstrap, write_bash_startup_home _ENV_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\Z") @@ -49,8 +48,8 @@ def _credential_names_in_process(pid: int) -> set[str] | None: class PaneAdapter(Protocol): """Native endpoint operations needed by the common lifecycle service.""" - def inspect(self, request: PaneRequest) -> PaneStatus: ... - def create(self, request: PaneRequest) -> PaneBinding: ... + def inspect(self, request: PaneRequest, *, previous_binding: PaneBinding | None = None) -> PaneStatus: ... + def create(self, request: PaneRequest, *, previous_binding: PaneBinding | None = None) -> PaneBinding: ... def send_trigger(self, request: PaneRequest, binding: PaneBinding) -> None: ... @@ -99,6 +98,60 @@ def _shell_owns_foreground(pid: int, tty: str) -> bool: except (OSError, ValueError, IndexError): return False +def _retirement_process(pid: int, tty: str, cwd: str, server_pid: int) -> dict[str, Any]: + """Observe the current Bash generation and refuse any other live session work.""" + def fields(process: int) -> list[str]: + raw = Path(f"/proc/{process}/stat").read_text(encoding="ascii") + return raw[raw.rfind(")") + 1:].split() + + try: + proc = Path(f"/proc/{pid}") + info = fields(pid) + if (proc.stat().st_uid != os.getuid() or Path(os.readlink(proc / "exe")).name != "bash" + or not _same_path(_process_cwd(pid) or "", cwd) + or not _shell_owns_foreground(pid, tty)): + raise ValueError("shell identity") + session = int(info[3]) + start = int(info[19]) + if session <= 0 or start <= 0: + raise ValueError("process generation") + ancestor = int(info[1]) + server_start = int(fields(server_pid)[19]) + if server_start <= 0 or Path(f"/proc/{server_pid}").stat().st_uid != os.getuid(): + raise ValueError("server generation") + seen = {pid} + while ancestor != server_pid: + if ancestor <= 1 or ancestor in seen: + raise ValueError("server ancestry") + seen.add(ancestor) + ancestor = int(fields(ancestor)[1]) + processes: dict[int, tuple[int, int, str]] = {} + for entry in Path("/proc").iterdir(): + if not entry.name.isdigit() or int(entry.name) == pid: + continue + try: + other = fields(int(entry.name)) + except FileNotFoundError: + continue + processes[int(entry.name)] = (int(other[1]), int(other[3]), other[0]) + descendants = {pid} + while True: + expanded = descendants | {child for child, (parent, _, _) in processes.items() + if parent in descendants} + if expanded == descendants: + break + descendants = expanded + if any(state not in {"Z", "X"} and (child in descendants or other_session == session) + for child, (_, other_session, state) in processes.items()): + raise ValueError("live descendant or session work") + if fields(pid)[19] != str(start) or fields(server_pid)[19] != str(server_start): + raise ValueError("process generation changed") + except (OSError, ValueError, IndexError) as exc: + raise PaneError("retirement_process_unverified", "The current Bash generation, server ancestry, or idle session cannot be proven.") from exc + return {"shell_pid": pid, "pane_tty": tty, "shell_starttime": start, + "machine_generation": machine_generation(), "server_pid": server_pid, + "server_starttime": server_start} + def _prompt_status(state_dir: str | os.PathLike[str], request: PaneRequest, pane_id: str, *, terminal_id: str | None, tab_id: str | None, native_pid: int | None, @@ -165,6 +218,34 @@ def _prompt_status(state_dir: str | os.PathLike[str], request: PaneRequest, pane changed=created) +def _managed_owner_metadata(request: PaneRequest) -> dict[str, Any]: + return { + "api_version": 1, + "binding_key": request.binding_key, + "repository": request.repository, + "controller_id": request.controller_id, + "host_alias": request.host_alias, + "parent_id": request.parent_id, + "workspace_id": request.workspace_id, + "machine_id": request.machine_id, + } + + +def _previous_binding_matches(request: PaneRequest, binding: PaneBinding) -> bool: + return ( + binding.name == request.name + and binding.binding_key == request.binding_key + and binding.backend == request.backend + and binding.endpoint == request.endpoint + and binding.parent_id == request.parent_id + and binding.repository == request.repository + and binding.controller_id == request.controller_id + and binding.host_alias == request.host_alias + and binding.workspace_id == request.workspace_id + and binding.session_id == request.session_id + ) + + class HerdrAdapter: """Herdr adapter using its existing owned-pane allocator and exact native IDs.""" @@ -197,7 +278,7 @@ def _endpoint(self, request: PaneRequest) -> str: herdr_transport.verify_local_herdr_endpoint(endpoint) return endpoint - def _server_credential_names(self, request: PaneRequest) -> set[str]: + def _server_startup_environment(self, request: PaneRequest) -> tuple[set[str], str]: if self.client is not None: identity = self.client.endpoint_identity() pid = identity.get("pid") if isinstance(identity, dict) else None @@ -210,10 +291,41 @@ def _server_credential_names(self, request: PaneRequest) -> set[str]: raise PaneError("endpoint_unverified", "The Herdr endpoint owner changed before pane creation.") if not isinstance(pid, int) or pid <= 0: raise PaneError("server_environment_unknown", "The Herdr server environment cannot be qualified.") - names = _credential_names_in_process(pid) - if names is None: - raise PaneError("server_environment_unknown", "The Herdr server environment cannot be qualified.") - return names + try: + if Path(f"/proc/{pid}").stat().st_uid != os.getuid(): + raise PaneError("server_environment_unknown", "The Herdr server environment cannot be qualified.") + raw = Path(f"/proc/{pid}/environ").read_bytes() + except OSError: + raise PaneError("server_environment_unknown", "The Herdr server environment cannot be qualified.") from None + names: set[str] = set() + home_raw: bytes | None = None + for item in raw.split(b"\0"): + name, separator, value = item.partition(b"=") + if not separator: + continue + if name == b"HOME": + if home_raw is not None: + raise PaneError("server_environment_unknown", "The Herdr startup HOME is ambiguous.") + home_raw = value + if not name.endswith(b"_SUDO_KEY") or not value: + continue + try: + decoded = name.decode("ascii") + except UnicodeDecodeError: + raise PaneError("server_environment_unknown", "The Herdr server environment cannot be qualified.") from None + if not _ENV_NAME.fullmatch(decoded): + raise PaneError("server_environment_unknown", "The Herdr server environment cannot be qualified.") + names.add(decoded) + if not home_raw: + raise PaneError("server_environment_unknown", "The Herdr startup HOME cannot be qualified.") + try: + home = home_raw.decode("utf-8") + except UnicodeDecodeError: + raise PaneError("server_environment_unknown", "The Herdr startup HOME cannot be qualified.") from None + if (not Path(home).is_absolute() or "\x00" in home + or any(ord(char) < 32 or ord(char) == 127 for char in home)): + raise PaneError("server_environment_unknown", "The Herdr startup HOME is not a valid absolute path.") + return names, home def _inventory(self, request: PaneRequest) -> list[dict[str, Any]]: if self.client is not None and callable(getattr(self.client, "_pane_inventory", None)): @@ -229,29 +341,120 @@ def _owned(self, request: PaneRequest) -> tuple[dict[str, Any], dict[str, Any]]: owner, pane = herdr_layout.owned_pane( request.endpoint, owner_id, state_dir=self._state(), allow_managed_ops=True ) - expected = { - "api_version": 1, - "binding_key": request.binding_key, - "repository": request.repository, - "controller_id": request.controller_id, - "host_alias": request.host_alias, - "parent_id": request.parent_id, - "workspace_id": request.workspace_id, - "machine_id": request.machine_id, - } + expected = _managed_owner_metadata(request) if owner.get("purpose") != "managed_ops" or owner.get("owner_metadata") != expected: raise PaneError("owner_scope_mismatch", "The stored Herdr pane belongs to a different managed scope.") if not isinstance(pane, dict): raise PaneError("pane_missing", "The owned Herdr pane is no longer present.", effect="unknown") return owner, pane - def inspect(self, request: PaneRequest) -> PaneStatus: + def _retirement_observation(self, request: PaneRequest, pane: dict[str, Any]) -> dict[str, Any]: + self._endpoint(request) + pane_id = pane["pane_id"] + native = self._request(request, "pane.get", {"pane_id": pane_id}).get("pane") + if (not isinstance(native, dict) or native.get("pane_id") != pane_id + or native.get("terminal_id") != pane.get("terminal_id") + or native.get("workspace_id") != request.workspace_id + or native.get("tab_id") != pane.get("tab_id") + or native.get("label") != request.name + or native.get("agent") or native.get("agent_session") + or native.get("agent_status") not in {None, "idle", "unknown"}): + raise PaneError("retirement_native_mismatch", "The current native pane identity or agent occupancy is not safe.") + if native.get("agent_status") == "unknown": + body = self._request(request, "session.snapshot", {}) + for _ in range(3): + if isinstance(body.get("snapshot"), dict): + body = body["snapshot"] + elif isinstance(body.get("result"), dict): + body = body["result"] + else: + break + agents = body.get("agents") + if (not isinstance(agents, list) + or any(not isinstance(agent, dict) or agent.get("pane_id") == pane_id for agent in agents)): + raise PaneError("retirement_pane_busy", "The current pane's agent occupancy cannot be proven idle.") + process = _result_payload(self._request(request, "pane.process_info", {"pane_id": pane_id}), "process_info") + pid = process.get("shell_pid") + foreground = process.get("foreground_processes") + if (type(pid) is not int or pid <= 0 or not isinstance(foreground, list) + or not foreground or any(not isinstance(item, dict) or item.get("pid") != pid for item in foreground)): + raise PaneError("retirement_pane_busy", "The native foreground process is not the exact Bash shell.") + try: + tty = os.readlink(f"/proc/{pid}/fd/0") + if self.client is not None: + server = self.client.endpoint_identity().get("pid") + else: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection: + connection.settimeout(3) + connection.connect(request.endpoint) + server, uid, _ = struct.unpack("3i", connection.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)) + if uid != os.getuid(): + raise ValueError("server owner") + if type(server) is not int or server <= 0 or not tty.startswith("/dev/"): + raise ValueError("server or tty") + except (OSError, ValueError, AttributeError) as exc: + raise PaneError("retirement_process_unverified", "The endpoint server and shell TTY cannot be qualified.") from exc + return _retirement_process(pid, tty, request.cwd, server) + + def retire_creation(self, request: PaneRequest, pane_id: str, terminal_id: str, + retirement_id: str, reservation: bytes, fence: Any, recheck: Any) -> dict[str, Any]: + def qualify(pane: dict[str, Any]) -> dict[str, Any]: + recheck() + return self._retirement_observation(request, pane) + + try: + endpoint = self._endpoint(request) + return herdr_layout.retire_managed_creation( + endpoint, request.workspace_id or "", request.parent_id, request.cwd, + request.binding_key[:32], _managed_owner_metadata(request), pane_id, + terminal_id, retirement_id, reservation, + qualify, fence, + state_dir=self._state(), + ) + except herdr_layout.HerdrOpsError as exc: + raise PaneError(exc.code, exc.message, effect=exc.effect) from exc + + def reconcile_creation_retirement(self, request: PaneRequest, marker: Mapping[str, Any], + reservation: Mapping[str, Any]) -> dict[str, Any]: + try: + endpoint = self._endpoint(request) + return herdr_layout.reconcile_managed_creation_retirement( + endpoint, request.workspace_id or "", request.parent_id, request.cwd, + request.binding_key[:32], _managed_owner_metadata(request), marker, + state_dir=self._state(), reservation=reservation, + ) + except herdr_layout.HerdrOpsError as exc: + raise PaneError(exc.code, exc.message, effect=exc.effect) from exc + + def _missing_replacement(self, request: PaneRequest, endpoint: str, + previous_binding: PaneBinding) -> PaneStatus: + try: + herdr_layout.verify_missing_managed_pane( + endpoint, request.workspace_id or "", request.parent_id, request.cwd, + request.binding_key[:32], previous_binding.to_dict(), state_dir=self._state(), + ratio=1 / 3, owner_metadata=_managed_owner_metadata(request), + ) + except herdr_layout.HerdrOpsError as exc: + return PaneStatus("unknown", request.backend, request.binding_key, request.name, + detail=exc.message) + return PaneStatus("missing", request.backend, request.binding_key, request.name) + + def inspect(self, request: PaneRequest, *, previous_binding: PaneBinding | None = None) -> PaneStatus: if request.backend != "herdr": raise PaneError("backend_mismatch", "The Herdr adapter received a different backend request.") - self._endpoint(request) + endpoint = self._endpoint(request) + if previous_binding is not None and not _previous_binding_matches(request, previous_binding): + return PaneStatus("unknown", request.backend, request.binding_key, request.name, + detail="The previous managed binding does not match this request scope.") try: owner, pane = self._owned(request) except herdr_layout.HerdrOpsError as exc: + if previous_binding is not None: + if exc.code in {"managed_pane_missing", "pane_not_found", "not_found", + "state_dir_unavailable", "ownership_mismatch"}: + return self._missing_replacement(request, endpoint, previous_binding) + return PaneStatus("unknown", request.backend, request.binding_key, request.name, + detail=exc.message) if exc.code in {"not_found", "state_dir_unavailable"}: # Look for matching labels before permitting a new allocation. inventory = self._inventory(request) @@ -264,8 +467,8 @@ def inspect(self, request: PaneRequest) -> PaneStatus: other = herdr_layout._managed_ops_owner(request.endpoint, str(row.get("pane_id")), self._state()) metadata = other.get("owner_metadata") if isinstance(other, dict) else None if (isinstance(metadata, dict) and other.get("state") == "owned" - and metadata.get("parent_id") != request.parent_id - and metadata.get("controller_id") != request.controller_id + and (metadata.get("parent_id") != request.parent_id + or metadata.get("controller_id") != request.controller_id) and any(parent.get("pane_id") == metadata.get("parent_id") and parent.get("tab_id") == parent_tab for parent in inventory)): continue @@ -278,9 +481,19 @@ def inspect(self, request: PaneRequest) -> PaneStatus: return PaneStatus("unknown", request.backend, request.binding_key, request.name, detail=exc.message) raise PaneError(exc.code, exc.message, effect=exc.effect) from exc except PaneError as exc: + if previous_binding is not None: + if exc.code == "not_found": + return self._missing_replacement(request, endpoint, previous_binding) + return PaneStatus("unknown", request.backend, request.binding_key, request.name, + detail=exc.message) if exc.code == "not_found": return PaneStatus("missing", request.backend, request.binding_key, request.name) raise + replacement = owner.get("replacement") + if isinstance(replacement, dict) and replacement.get("state") in {"creating", "unknown"}: + return PaneStatus("unknown", request.backend, request.binding_key, request.name, + detail="The Herdr replacement is reserved or its outcome is uncertain; reconciliation is required.") + pane_id = pane.get("pane_id") if not isinstance(pane_id, str) or pane.get("workspace_id") != request.workspace_id: return PaneStatus("unknown", request.backend, request.binding_key, request.name, @@ -314,9 +527,6 @@ def inspect(self, request: PaneRequest) -> PaneStatus: if not isinstance(shell_pid, int) or shell_pid <= 0 or not isinstance(foreground, list): return PaneStatus("unknown", request.backend, request.binding_key, request.name, detail="Herdr cannot verify the managed pane's shell occupant.") - if any(not isinstance(item, dict) or item.get("pid") != shell_pid for item in foreground): - return PaneStatus("busy", request.backend, request.binding_key, request.name, - detail="A foreground process occupies the managed Herdr pane.") try: shell_tty = os.readlink(f"/proc/{shell_pid}/fd/0") except OSError: @@ -330,11 +540,18 @@ def inspect(self, request: PaneRequest) -> PaneStatus: state = _prompt_status(self._state(), request, pane_id, terminal_id=pane.get("terminal_id"), tab_id=tab_id, native_pid=shell_pid, native_tty=shell_tty, label=request.name) + if (any(not isinstance(item, dict) or item.get("pid") != shell_pid for item in foreground) + and state.state == "ready"): + return PaneStatus("unknown", request.backend, request.binding_key, request.name, + detail="The native foreground and controlled Bash prompt state disagree.") return state - def create(self, request: PaneRequest) -> PaneBinding: + def create(self, request: PaneRequest, *, previous_binding: PaneBinding | None = None, + retirement_proof: Mapping[str, Any] | None = None) -> PaneBinding: if request.backend != "herdr": raise PaneError("backend_mismatch", "The Herdr adapter received a different backend request.") + if previous_binding is not None and not _previous_binding_matches(request, previous_binding): + raise PaneError("replacement_binding_mismatch", "The previous Herdr binding does not match this request scope.") endpoint = self._endpoint(request) parent = self._request(request, "pane.get", {"pane_id": request.parent_id}).get("pane") if not isinstance(parent, dict) or parent.get("workspace_id") != request.workspace_id: @@ -345,23 +562,24 @@ def create(self, request: PaneRequest) -> PaneBinding: raise PaneError("parent_mismatch", "The exact Herdr parent is not in the requested workspace.") root = self._state() bootstrap = write_bash_bootstrap(root, request.binding_key, pane_id_env="HERDR_PANE_ID") - metadata = { - "api_version": 1, "binding_key": request.binding_key, - "repository": request.repository, "controller_id": request.controller_id, - "host_alias": request.host_alias, "parent_id": request.parent_id, - "workspace_id": request.workspace_id, "machine_id": request.machine_id, - } - environment = { - "INPUTRC": "/dev/null", - "PROMPT_COMMAND": "source " + shlex.quote(str(bootstrap)), - } - for name in self._server_credential_names(request): + credential_names, original_home = self._server_startup_environment(request) + startup_home = write_bash_startup_home( + root, request.binding_key, original_home=original_home, bootstrap=bootstrap, + ) + metadata = _managed_owner_metadata(request) + # Herdr's supported pane.split env map carries process environment, but + # login profiles may replace scalar PROMPT_COMMAND before the first prompt. + # A private HOME loader runs the gate after the original profile instead. + environment = {"HOME": str(startup_home), "INPUTRC": "/dev/null"} + for name in credential_names: environment[name] = "" try: created = herdr_layout.ensure_owned_pane( endpoint, request.workspace_id or "", request.parent_id, request.cwd, request.binding_key[:32], state_dir=root, ratio=1 / 3, purpose="managed_ops", owner_metadata=metadata, environment=environment, + previous_binding=previous_binding.to_dict() if previous_binding is not None else None, + retirement_proof=retirement_proof, ) except herdr_layout.HerdrOpsError as exc: raise PaneError(exc.code, exc.message, effect=exc.effect) from exc @@ -474,7 +692,10 @@ def _in_session(pane: dict[str, str], request: PaneRequest) -> bool: session = request.session_id or "" return pane.get("session") == session if session.startswith("$") else pane.get("session_name") == session - def inspect(self, request: PaneRequest) -> PaneStatus: + def inspect(self, request: PaneRequest, *, previous_binding: PaneBinding | None = None) -> PaneStatus: + if previous_binding is not None: + return PaneStatus("unknown", request.backend, request.binding_key, request.name, + detail="Missing-generation recovery is not supported by tmux.") if request.backend != "tmux": raise PaneError("backend_mismatch", "The tmux adapter received a different backend request.") parent = self._pane(request, request.parent_id) @@ -551,7 +772,9 @@ def inspect(self, request: PaneRequest) -> PaneStatus: state.operation_id, state.detail, state.changed) return state - def create(self, request: PaneRequest) -> PaneBinding: + def create(self, request: PaneRequest, *, previous_binding: PaneBinding | None = None) -> PaneBinding: + if previous_binding is not None: + raise PaneError("replacement_backend_unsupported", "Missing-generation recovery is supported only for managed Herdr panes.") if request.backend != "tmux": raise PaneError("backend_mismatch", "The tmux adapter received a different backend request.") if not Path(request.cwd).is_dir(): diff --git a/ls/core/terminal_ops/pane.py b/ls/core/terminal_ops/pane.py index e3728193a..1351eae1b 100644 --- a/ls/core/terminal_ops/pane.py +++ b/ls/core/terminal_ops/pane.py @@ -2,7 +2,7 @@ from __future__ import annotations -from contextlib import contextmanager +from contextlib import ExitStack, contextmanager from dataclasses import replace import fcntl from pathlib import Path @@ -10,7 +10,8 @@ import os import tempfile import threading -from typing import Any, Mapping +import re +from typing import Any, Mapping, cast import uuid from .backends import PaneAdapter @@ -54,6 +55,21 @@ def get(self, key: str) -> dict[str, Any] | None: raise PaneError("invalid_store_record", "The pane binding record is invalid.") return value + def snapshot(self, key: str) -> bytes: + """Read exact bounded private bytes for an immutable owner archive.""" + try: + descriptor = os.open(self._path(key), os.O_RDONLY | os.O_NOFOLLOW) + with os.fdopen(descriptor, "rb") as stream: + info = os.fstat(stream.fileno()) + if info.st_uid != os.getuid() or info.st_mode & 0o077 or info.st_size > 1024 * 1024: + raise ValueError("unsafe or oversized reservation") + raw = stream.read(1024 * 1024 + 1) + if len(raw) > 1024 * 1024: + raise ValueError("oversized reservation") + return raw + except (OSError, ValueError) as exc: + raise PaneError("retirement_reservation_unavailable", "The original private reservation cannot be archived safely.") from exc + def records(self): for path in self.root.glob("*.json"): value = self.get(path.stem) @@ -153,10 +169,16 @@ def _put(self, key: str, value: Mapping[str, Any]) -> None: def _lock(self, key: str): locker = getattr(self.store, "lock", None) if callable(locker): - try: - return locker(key) - except Exception as exc: - raise PaneError("pane_busy", "The existing pane store is already locked by another operation.") from exc + @contextmanager + def _store_lock(): + with ExitStack() as stack: + try: + stack.enter_context(cast(Any, locker(key))) + except BlockingIOError as exc: + raise PaneError("pane_busy", "The existing pane store is already locked by another operation.") from exc + yield + + return _store_lock() with _LOCAL_LOCK_GUARD: lock = _LOCAL_LOCKS.setdefault(key, threading.Lock()) if not lock.acquire(blocking=False): @@ -171,6 +193,152 @@ def __exit__(self, *_: Any) -> None: return _Release() + def _retirement_store(self, request: PaneRequest) -> tuple[dict[str, Any], bytes]: + if request.backend != "herdr": + raise PaneError("retirement_backend_unsupported", "Creation retirement is supported only for Herdr.") + if (not callable(getattr(self.store, "snapshot", None)) + or not callable(getattr(self.store, "lock", None)) + or not callable(getattr(self.store, "put", None))): + raise PaneError("retirement_store_unsupported", "Retirement requires a durable locked store with exact-byte snapshots.") + stored = self._get(request.binding_key) + if (not isinstance(stored, dict) or stored.get("binding_key") != request.binding_key + or stored.get("request") != request.to_dict()): + raise PaneError("retirement_scope_mismatch", "The original reservation does not match the exact request.") + from ..herdr_ops.transport import _strict_object + + try: + raw = self.store.snapshot(request.binding_key) + if not isinstance(raw, bytes) or len(raw) > 1024 * 1024: + raise ValueError("invalid reservation snapshot") + if json.loads(raw, object_pairs_hook=_strict_object) != stored: + raise ValueError("reservation changed") + except Exception as exc: + raise PaneError("retirement_reservation_unavailable", "The original private reservation cannot be archived safely.") from exc + return stored, raw + + def _retirement_idle(self, request: PaneRequest, stored: Mapping[str, Any]) -> None: + if stored.get("operation") is not None or stored.get("last_binding") is not None: + raise PaneError("retirement_operation_present", "A prior managed operation or binding prevents creation retirement.") + status = stored.get("status") + if not isinstance(status, dict) or status.get("binding") is not None or status.get("operation_id") is not None: + raise PaneError("retirement_operation_present", "The reserved status is not an uninitialized creation.") + directory = self.state_dir / "terminal_ops" / request.binding_key + try: + info = directory.lstat() + if directory.is_symlink() or not directory.is_dir() or info.st_uid != os.getuid() or info.st_mode & 0o077: + raise ValueError("unsafe runtime directory") + for item in directory.iterdir(): + if item.name == "writer-lease" or item.name.endswith(".json"): + raise ValueError("operation, prompt, or lease exists") + except (OSError, ValueError) as exc: + raise PaneError("retirement_operation_present", "Runtime state cannot prove an uninitialized, operation-free creation.") from exc + + @staticmethod + def _retirement_result(request: PaneRequest, marker: Mapping[str, Any], *, changed: bool) -> dict[str, Any]: + return {"api_version": 1, "retirement_id": marker["retirement_id"], + "binding_key": request.binding_key, "pane_id": marker["pane_id"], + "terminal_id": marker["terminal_id"], "state": marker["state"], + "changed": changed, "evidence": {"owner_archived": True, "reservation_archived": True, + "native_absence_verified": marker["state"] == "retired"}} + + def _reconcile_creation_retirement_locked(self, request: PaneRequest, retirement_id: str) -> dict[str, Any]: + stored, _ = self._retirement_store(request) + marker = stored.get("retirement") + if not isinstance(marker, dict) or marker.get("retirement_id") != retirement_id: + raise PaneError("retirement_id_mismatch", "No retirement fence matches this ID and request.") + if marker.get("state") not in {"unknown", "retired"}: + raise PaneError("retirement_record_invalid", "The retirement fence has an invalid state.") + if stored.get("creation") != {"state": "reserved", "token": marker.get("creation_token")}: + raise PaneError("retirement_reservation_mismatch", "The fenced original reservation changed.") + self._retirement_idle(request, stored) + original = dict(stored) + original.pop("retirement", None) + try: + current = getattr(self._adapter(request), "reconcile_creation_retirement")(request, marker, original) + except Exception: + return self._retirement_result(request, dict(marker) | {"state": "unknown"}, changed=False) + changed = marker.get("state") != current.get("state") + if changed: + stored["retirement"] = current | {"creation_token": marker["creation_token"]} + self._put(request.binding_key, stored) + return self._retirement_result(request, current, changed=changed) + + def retire_creation(self, request: PaneRequest, *, retirement_id: str, + expected_creation_token: str, expected_pane_id: str, + expected_terminal_id: str, discard_uncontrolled_shell: bool) -> dict[str, Any]: + if (not isinstance(retirement_id, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}", retirement_id) + or not isinstance(expected_creation_token, str) or not re.fullmatch(r"[a-f0-9]{32}", expected_creation_token) + or any(not isinstance(value, str) or not value or len(value) > 256 + or any(ord(char) < 32 or ord(char) == 127 for char in value) + for value in (expected_pane_id, expected_terminal_id)) + or discard_uncontrolled_shell is not True): + raise PaneError("invalid_retirement_request", "An exact retirement ID, creation token, native identities and discard acknowledgement are required.") + with self._lock(request.binding_key): + stored, raw = self._retirement_store(request) + existing = stored.get("retirement") + if existing is not None: + if (not isinstance(existing, dict) or existing.get("retirement_id") != retirement_id + or existing.get("creation_token") != expected_creation_token + or existing.get("pane_id") != expected_pane_id + or existing.get("terminal_id") != expected_terminal_id): + raise PaneError("retirement_id_mismatch", "A different retirement is already fenced for this reservation.") + return self._reconcile_creation_retirement_locked(request, retirement_id) + creation = stored.get("creation") + if not isinstance(creation, dict) or creation != {"state": "reserved", "token": expected_creation_token}: + raise PaneError("retirement_reservation_mismatch", "The exact original creation reservation changed.") + self._retirement_idle(request, stored) + fenced: dict[str, Any] | None = None + + def fence(marker: dict[str, Any]) -> None: + nonlocal fenced + current, current_raw = self._retirement_store(request) + if current_raw != raw or current != stored: + raise PaneError("retirement_reservation_mismatch", "The reservation changed before close.") + self._retirement_idle(request, current) + fenced = dict(marker) | {"creation_token": expected_creation_token} + current["retirement"] = fenced + self._put(request.binding_key, current) + + def recheck() -> None: + current, _ = self._retirement_store(request) + if (current.get("creation") != creation + or (fenced is None and current != stored) + or (fenced is not None and current.get("retirement") != fenced)): + raise PaneError("retirement_reservation_mismatch", "The reservation or retirement fence changed.") + self._retirement_idle(request, current) + + retire = getattr(self._adapter(request), "retire_creation", None) + if not callable(retire): + raise PaneError("retirement_backend_unsupported", "The Herdr adapter does not support creation retirement.") + try: + marker = retire(request, expected_pane_id, expected_terminal_id, + retirement_id, raw, fence, recheck) + except Exception as exc: + if fenced is None: + if isinstance(exc, PaneError): + raise + raise PaneError("retirement_unavailable", "The native retirement could not be qualified.") from exc + return self._retirement_result(request, fenced, changed=True) + if (fenced is None or not isinstance(marker, dict) + or marker.get("state") not in {"unknown", "retired"} + or any(marker.get(key) != fenced.get(key) for key in + ("retirement_id", "pane_id", "terminal_id", "owner_sha256", + "reservation_sha256", "observation"))): + if fenced is None: + raise PaneError("retirement_fence_mismatch", "The native retirement was not durably fenced.") + return self._retirement_result(request, fenced, changed=True) + marker = dict(marker) | {"creation_token": expected_creation_token} + current = self._get(request.binding_key) + if not isinstance(current, dict) or current.get("retirement") != fenced: + raise PaneError("retirement_fence_mismatch", "The durable retirement fence changed.", effect="unknown") + current["retirement"] = marker + self._put(request.binding_key, current) + return self._retirement_result(request, marker, changed=True) + + def reconcile_creation_retirement(self, request: PaneRequest, retirement_id: str) -> dict[str, Any]: + with self._lock(request.binding_key): + return self._reconcile_creation_retirement_locked(request, retirement_id) + def _record(self, request: PaneRequest, status: PaneStatus, *, operation: dict[str, Any] | None = None, creation: dict[str, Any] | None = None) -> None: previous = self._get(request.binding_key) @@ -188,10 +356,102 @@ def _record(self, request: PaneRequest, status: PaneStatus, *, operation: dict[s payload["last_binding"] = last_binding if operation is not None: payload["operation"] = dict(operation) + if isinstance(previous, dict) and isinstance(previous.get("retirement"), dict): + payload["retirement"] = dict(previous["retirement"]) if creation is not None: payload["creation"] = dict(creation) self._put(request.binding_key, payload) + def _replacement_proof( + self, request: PaneRequest, stored: dict[str, Any] | None = None, + ) -> tuple[PaneBinding, dict[str, Any]] | None: + if stored is None: + stored = self._get(request.binding_key) + if stored is None: + return None + previous_data = stored.get("last_binding") + if not isinstance(previous_data, dict): + return None + if (stored.get("binding_key") != request.binding_key + or stored.get("request") != request.to_dict() + or stored.get("creation") is not None): + raise PaneError("replacement_scope_mismatch", "The stored pane request or creation state does not match this exact replacement.") + if request.backend != "herdr": + raise PaneError("replacement_backend_unsupported", "Missing-generation recovery is supported only for managed Herdr panes.") + try: + previous = PaneBinding.from_dict(previous_data) + except PaneError as exc: + raise PaneError("replacement_binding_invalid", "The previous managed pane binding is incomplete.") from exc + if (previous.state not in {"ready", "busy"} + or previous.backend != request.backend + or previous.binding_key != request.binding_key + or previous.endpoint != request.endpoint + or previous.parent_id != request.parent_id + or previous.repository != request.repository + or previous.controller_id != request.controller_id + or previous.host_alias != request.host_alias + or previous.workspace_id != request.workspace_id + or previous.session_id != request.session_id): + raise PaneError("replacement_binding_mismatch", "The previous pane binding does not match the original request scope.") + stored_operation = stored.get("operation") + status = stored.get("status") + if not isinstance(stored_operation, dict) or not isinstance(status, dict): + raise PaneError("replacement_receipt_missing", "A completed managed operation receipt is required before replacing the pane.") + if (status.get("backend") != request.backend + or status.get("binding_key") != request.binding_key + or status.get("name") != request.name): + raise PaneError("replacement_status_mismatch", "The stored pane status does not match its exact request scope.") + stored_binding_data = status.get("binding") + if stored_binding_data is None: + if status.get("state") != "missing": + raise PaneError("replacement_status_mismatch", "The stored pane status no longer confirms the exact previous binding.") + else: + stored_binding = PaneBinding.from_dict(stored_binding_data) + if stored_binding.to_dict() != previous.to_dict(): + raise PaneError("replacement_status_mismatch", "The stored pane status and prior binding identify different generations.") + operation_id = stored_operation.get("operation_id") + if (not isinstance(operation_id, str) or not operation_id + or status.get("operation_id") != operation_id): + raise PaneError("replacement_receipt_mismatch", "The stored operation and pane status do not identify the same completed operation.") + try: + _, receipt = _read_operation(str(self.state_dir), request.binding_key, operation_id) + receipt_fields = ( + "binding_key", "operation_id", "pane_id", "shell_pid", "pane_tty", + "shell_path", "machine_generation", "endpoint", "controller_id", + "lease_token", + ) + if (any(stored_operation.get(field) != receipt.get(field) for field in receipt_fields) + or receipt.get("binding_key") != request.binding_key + or receipt.get("operation_id") != operation_id + or receipt.get("pane_id") != previous.pane_id + or receipt.get("shell_pid") != previous.shell_pid + or receipt.get("pane_tty") != previous.pane_tty + or receipt.get("shell_path") != previous.shell_path + or receipt.get("machine_generation") != previous.machine_generation + or receipt.get("endpoint") != request.endpoint + or receipt.get("controller_id") != request.controller_id + or receipt.get("target") != (request.host_alias or request.repository) + or not isinstance(receipt.get("lease_token"), str) + or not receipt.get("lease_token")): + raise ValueError("operation receipt does not match the stored pane generation") + if receipt.get("state") in {"staged", "completed"}: + receipt = reconcile_transition( + self.state_dir, binding_key=request.binding_key, operation_id=operation_id, + ) + exit_code = receipt.get("exit_code") + if (receipt.get("state") != "completed" + or isinstance(exit_code, bool) or not isinstance(exit_code, int) + or not 0 <= exit_code <= 255 + or not isinstance(receipt.get("completed_at"), str) + or not receipt.get("completed_at")): + raise ValueError("the prior operation is not durably completed") + lease = self.state_dir / "terminal_ops" / request.binding_key / "writer-lease" + if lease.exists() or lease.is_symlink(): + raise ValueError("a writer lease remains after completed-operation reconciliation") + except (OSError, ValueError, KeyError, TypeError, PaneError) as exc: + raise PaneError("replacement_receipt_unknown", "The previous operation receipt or writer lease cannot prove a completed, idle generation.") from exc + return replace(previous, metadata={}), receipt + def _check_stored(self, request: PaneRequest, status: PaneStatus) -> PaneStatus: stored = self._get(request.binding_key) if stored is None: @@ -267,8 +527,13 @@ def _check_stored(self, request: PaneRequest, status: PaneStatus) -> PaneStatus: or lease.exists() or lease.is_symlink()): return PaneStatus("unknown", request.backend, request.binding_key, request.name, detail="A prior operation or writer lease prevents replacement of the closed pane.") - # The adapter's exact native inventory proved the old pane absent. - # Its binding is invalidated and one replacement may be allocated. + if request.backend == "herdr": + try: + if self._replacement_proof(request, stored) is None: + raise PaneError("replacement_binding_missing", "No exact previous binding is recorded.") + except PaneError: + return PaneStatus("unknown", request.backend, request.binding_key, request.name, + detail="A prior completed receipt and exact absent-generation proof are required before replacement.") return status if status.binding is not None and isinstance(previous_binding, dict): if (previous_binding.get("pane_id") != status.binding.pane_id @@ -285,39 +550,112 @@ def _check_stored(self, request: PaneRequest, status: PaneStatus) -> PaneStatus: "A prior managed operation still owns the pane writer lease.") return status + def _inspect_locked( + self, request: PaneRequest, + ) -> tuple[PaneStatus, PaneBinding | None, dict[str, Any] | None]: + adapter = self._adapter(request) + native = adapter.inspect(request) + status = self._check_stored(request, native) + if request.backend != "herdr" or status.state not in {"unknown", "missing"}: + return status, None, None + stored = self._get(request.binding_key) + if (not isinstance(stored, dict) or stored.get("creation") is not None + or not isinstance(stored.get("last_binding"), dict)): + return status, None, None + try: + proof = self._replacement_proof(request, stored) + except PaneError: + return status, None, None + if proof is None: + return status, None, None + previous_binding, receipt = proof + native = adapter.inspect(request, previous_binding=previous_binding) + status = self._check_stored(request, native) + if status.state == "missing": + return status, previous_binding, receipt + return status, None, None + def inspect(self, request: PaneRequest) -> PaneStatus: - status = self._adapter(request).inspect(request) - return self._check_stored(request, status) + native = self._adapter(request).inspect(request) + status = self._check_stored(request, native) + if request.backend != "herdr" or status.state not in {"unknown", "missing"}: + return status + stored = self._get(request.binding_key) + if (not isinstance(stored, dict) or stored.get("creation") is not None + or not isinstance(stored.get("last_binding"), dict)): + return status + with self._lock(request.binding_key): + status, _, _ = self._inspect_locked(request) + return status def ensure(self, request: PaneRequest) -> PaneStatus: with self._lock(request.binding_key): - native = self._adapter(request).inspect(request) stored = self._get(request.binding_key) - if isinstance(stored, dict) and stored.get("creation") is not None: + retirement_proof = None + if isinstance(stored, dict) and isinstance(stored.get("retirement"), dict): + marker = stored["retirement"] + creation = stored.get("creation") + if isinstance(creation, dict) and creation.get("token") == marker.get("creation_token"): + if marker.get("state") != "retired": + return PaneStatus("unknown", request.backend, request.binding_key, request.name, + detail="The creation retirement remains fenced; reconcile without replay.") + result = self._reconcile_creation_retirement_locked(request, marker["retirement_id"]) + if result["state"] != "retired": + return PaneStatus("unknown", request.backend, request.binding_key, request.name, + detail="The retired native identities cannot be verified absent.") + retirement_proof = marker + if retirement_proof is not None: + status = PaneStatus("missing", request.backend, request.binding_key, request.name) + previous_binding = prior_operation = None + elif isinstance(stored, dict) and stored.get("creation") is not None: + native = self._adapter(request).inspect(request) if (native.state == "ready" and native.binding is not None and stored.get("binding_key") == request.binding_key - and stored.get("request", {}).get("binding_key") == request.binding_key): - # Reconcile a prior uncertain creation only when the native - # adapter independently proves its exact owned pane ready. + and stored.get("request") == request.to_dict()): self._record(request, native) return native return self._check_stored(request, native) - status = self._check_stored(request, native) + else: + status, previous_binding, prior_operation = self._inspect_locked(request) if status.state == "ready": - self._record(request, status) + operation = stored.get("operation") if request.backend == "herdr" and isinstance(stored, dict) else None + if isinstance(operation, dict) and isinstance(operation.get("operation_id"), str): + status = replace(status, operation_id=operation["operation_id"]) + self._record(request, status, operation=operation) + else: + self._record(request, status) return status if status.state != "missing": return status + if previous_binding is not None: + try: + current_proof = self._replacement_proof(request) + except PaneError: + current_proof = None + if (current_proof is None + or current_proof[0].to_dict() != previous_binding.to_dict()): + return PaneStatus("unknown", request.backend, request.binding_key, request.name, + detail="The stored previous binding changed before replacement.") + previous_binding, prior_operation = current_proof self._record(request, PaneStatus("unknown", request.backend, request.binding_key, request.name, detail="Native pane creation is reserved."), + operation=prior_operation if previous_binding is not None else None, creation={"state": "reserved", "token": uuid.uuid4().hex}) try: - binding = self._adapter(request).create(request) + if retirement_proof is not None: + binding = getattr(self._adapter(request), "create")(request, retirement_proof=retirement_proof) + else: + binding = self._adapter(request).create(request, previous_binding=previous_binding) except PaneError as exc: if exc.effect == "none": - # The adapter proved no native mutation was attempted. + reserved = self._get(request.binding_key) + preserved_operation = reserved.get("operation") if isinstance(reserved, dict) else None + operation_id = (preserved_operation.get("operation_id") + if isinstance(preserved_operation, dict) else None) self._record(request, PaneStatus("missing", request.backend, request.binding_key, - request.name, detail=exc.message)) + request.name, operation_id=operation_id, + detail=exc.message), + operation=preserved_operation if previous_binding is not None else None) raise verified = self._adapter(request).inspect(request) if verified.state != "ready" or verified.binding is None or verified.binding.pane_id != binding.pane_id: @@ -338,7 +676,7 @@ def ensure(self, request: PaneRequest) -> PaneStatus: def transition(self, request: PaneRequest, transition: PaneTransition) -> PaneStatus: with self._lock(request.binding_key): - status = self._check_stored(request, self._adapter(request).inspect(request)) + status, _, _ = self._inspect_locked(request) if status.state != "ready" or status.binding is None: return status prompt_nonce = status.binding.metadata.get("prompt_nonce") @@ -377,14 +715,14 @@ def reconcile(self, request: PaneRequest, operation_id: str) -> PaneStatus: except PaneError as exc: return PaneStatus("unknown", request.backend, request.binding_key, request.name, operation_id=operation_id, detail=exc.message) - current = self._check_stored(request, self._adapter(request).inspect(request)) + current, _, _ = self._inspect_locked(request) state = operation.get("state") if state == "completed": if current.state == "ready": current = PaneStatus(current.state, current.backend, current.binding_key, current.name, current.binding, operation_id, "The operation receipt records completion.") - elif current.state != "unknown": + elif current.state == "busy": current = PaneStatus("busy", current.backend, current.binding_key, current.name, current.binding, operation_id, "The operation completed; the shell has not announced its next primary prompt.") diff --git a/ls/core/terminal_ops/shell.py b/ls/core/terminal_ops/shell.py index 72659fe8a..b2d771f20 100644 --- a/ls/core/terminal_ops/shell.py +++ b/ls/core/terminal_ops/shell.py @@ -639,6 +639,86 @@ def write_bash_bootstrap( return target +def _write_private_script(target: Path, content: str) -> None: + """Atomically install one private Bash startup script.""" + if target.is_symlink(): + raise PaneError("unsafe_state_file", "A managed Bash startup script cannot be a symbolic link.") + descriptor, temporary_name = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent) + temporary = Path(temporary_name) + try: + os.fchmod(descriptor, 0o600) + with os.fdopen(descriptor, "w", encoding="utf-8") as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + if target.is_symlink(): + raise PaneError("unsafe_state_file", "A managed Bash startup script cannot be a symbolic link.") + os.replace(temporary, target) + finally: + try: + temporary.unlink() + except OSError: + pass + + +def write_bash_startup_home( + state_dir: str | os.PathLike[str], + binding_key: str, + *, + original_home: str | os.PathLike[str], + bootstrap: str | os.PathLike[str], +) -> Path: + """Create a private HOME overlay that runs managed setup after user startup files.""" + original = Path(original_home).expanduser() + source = Path(bootstrap).expanduser() + if (not original.is_absolute() or "\x00" in str(original) + or any(ord(char) < 32 or ord(char) == 127 for char in str(original))): + raise PaneError("invalid_home", "The managed Bash startup requires an explicit absolute original HOME.") + if not source.is_absolute() or "\x00" in str(source): + raise PaneError("invalid_bootstrap", "The managed Bash startup requires an absolute bootstrap file.") + directory = _state_directory(state_dir, binding_key, create=True) + managed_home = directory / "home" + try: + managed_home.mkdir(mode=0o700, exist_ok=True) + info = managed_home.lstat() + except OSError as exc: + raise PaneError("state_unavailable", "The private managed Bash startup HOME is unavailable.") from exc + if (stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode) + or (hasattr(os, "getuid") and info.st_uid != os.getuid())): + raise PaneError("unsafe_state_dir", "The managed Bash startup HOME must be an owned real directory.") + try: + managed_home.chmod(0o700) + except OSError as exc: + raise PaneError("state_unavailable", "The managed Bash startup HOME permissions could not be verified.") from exc + if original.resolve(strict=False) == managed_home.resolve(strict=False): + raise PaneError("invalid_home", "The managed Bash startup HOME cannot replace the original HOME.") + original_home = shlex.quote(str(original)) + bootstrap_path = shlex.quote(str(source)) + login_profile = f'''# LocalSetup managed login-shell startup loader. +HOME={original_home} +export HOME +for __ls_profile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do + if [[ -r $__ls_profile ]]; then + . "$__ls_profile" + break + fi +done +unset __ls_profile +. {bootstrap_path} || {{ printf '%s\\n' 'managed Bash bootstrap failed' >&2; return 1 2>/dev/null || exit 1; }} +''' + interactive_rc = f'''# LocalSetup managed interactive-shell startup loader. +HOME={original_home} +export HOME +if [[ -r "$HOME/.bashrc" ]]; then + . "$HOME/.bashrc" +fi +. {bootstrap_path} || {{ printf '%s\\n' 'managed Bash bootstrap failed' >&2; return 1 2>/dev/null || exit 1; }} +''' + _write_private_script(managed_home / ".bash_profile", login_profile) + _write_private_script(managed_home / ".bashrc", interactive_rc) + return managed_home + + def _read_operation(state_dir: str, binding_key: str, operation_id: str) -> tuple[Path, dict[str, Any]]: directory = _state_directory(state_dir, binding_key, create=False) path = _operation_path(directory, operation_id) diff --git a/ls/docs/ADAPTER_OWNERSHIP.md b/ls/docs/ADAPTER_OWNERSHIP.md index cc99667de..492ea1e1a 100644 --- a/ls/docs/ADAPTER_OWNERSHIP.md +++ b/ls/docs/ADAPTER_OWNERSHIP.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/AGENTIC_AGENT_Q_BIDIRECTIONAL_BUILD_SPEC.md b/ls/docs/AGENTIC_AGENT_Q_BIDIRECTIONAL_BUILD_SPEC.md index 495f8beff..8e2673492 100644 --- a/ls/docs/AGENTIC_AGENT_Q_BIDIRECTIONAL_BUILD_SPEC.md +++ b/ls/docs/AGENTIC_AGENT_Q_BIDIRECTIONAL_BUILD_SPEC.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-agentq-ship-ingest implemented: "shared signed encrypted binary Agent Q envelope; private registry v2 with persistent authority; opaque file-drop and bounded one-attachment mail carrier; exact receipt before queue promotion; versioned lifecycle, reader and workflow contracts" deferred: "ls/tools/agentq_transport_client/docs/DEFERRED.md" diff --git a/ls/docs/AGENTIC_AGENT_Q_PATTERN.md b/ls/docs/AGENTIC_AGENT_Q_PATTERN.md index dbe53d313..ceff8437f 100644 --- a/ls/docs/AGENTIC_AGENT_Q_PATTERN.md +++ b/ls/docs/AGENTIC_AGENT_Q_PATTERN.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-agentq-ship-ingest --- diff --git a/ls/docs/AGENTIC_AGENT_Q_SCENARIOS.md b/ls/docs/AGENTIC_AGENT_Q_SCENARIOS.md index 8bcb0a7df..8987f3c8a 100644 --- a/ls/docs/AGENTIC_AGENT_Q_SCENARIOS.md +++ b/ls/docs/AGENTIC_AGENT_Q_SCENARIOS.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-agentq-ship-ingest audience: humans, agents --- diff --git a/ls/docs/AGENTIC_AGENT_TO_AGENT_PROTOCOL.md b/ls/docs/AGENTIC_AGENT_TO_AGENT_PROTOCOL.md index 7a25ffe26..122043315 100644 --- a/ls/docs/AGENTIC_AGENT_TO_AGENT_PROTOCOL.md +++ b/ls/docs/AGENTIC_AGENT_TO_AGENT_PROTOCOL.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-agentq-ship-ingest --- diff --git a/ls/docs/AGENTIC_DESIGN_INDEX.md b/ls/docs/AGENTIC_DESIGN_INDEX.md index 9b6a1e5ac..0f0b0d86e 100644 --- a/ls/docs/AGENTIC_DESIGN_INDEX.md +++ b/ls/docs/AGENTIC_DESIGN_INDEX.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-docs-create-move --- diff --git a/ls/docs/AGENTIC_UMBRELLA_WORKFLOWS.md b/ls/docs/AGENTIC_UMBRELLA_WORKFLOWS.md index 4bef88354..badb90931 100644 --- a/ls/docs/AGENTIC_UMBRELLA_WORKFLOWS.md +++ b/ls/docs/AGENTIC_UMBRELLA_WORKFLOWS.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_package: ls-wf-multi-phase-task-execution --- diff --git a/ls/docs/AGENT_CONTEXT_AND_MCP_CONTRACT.md b/ls/docs/AGENT_CONTEXT_AND_MCP_CONTRACT.md index aab8b6cd1..0ba29c977 100644 --- a/ls/docs/AGENT_CONTEXT_AND_MCP_CONTRACT.md +++ b/ls/docs/AGENT_CONTEXT_AND_MCP_CONTRACT.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-context-index-search --- diff --git a/ls/docs/AGENT_SKILLS_COMPLIANCE.md b/ls/docs/AGENT_SKILLS_COMPLIANCE.md index d7a1199f5..44a8d4bc6 100644 --- a/ls/docs/AGENT_SKILLS_COMPLIANCE.md +++ b/ls/docs/AGENT_SKILLS_COMPLIANCE.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-create-agent-skills --- diff --git a/ls/docs/BRANDING.md b/ls/docs/BRANDING.md index 519cfa4d4..4492feca7 100644 --- a/ls/docs/BRANDING.md +++ b/ls/docs/BRANDING.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-script-docs-authoring --- diff --git a/ls/docs/CLIENT_INTEGRATION_METADATA.md b/ls/docs/CLIENT_INTEGRATION_METADATA.md index 757e7178c..6b9cfbdc8 100644 --- a/ls/docs/CLIENT_INTEGRATION_METADATA.md +++ b/ls/docs/CLIENT_INTEGRATION_METADATA.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/CLIENT_STATE.md b/ls/docs/CLIENT_STATE.md index db6a455fe..dcf53aeed 100644 --- a/ls/docs/CLIENT_STATE.md +++ b/ls/docs/CLIENT_STATE.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/CLI_SKILLS_ENV.md b/ls/docs/CLI_SKILLS_ENV.md index 4d37da9af..5b9ba8094 100644 --- a/ls/docs/CLI_SKILLS_ENV.md +++ b/ls/docs/CLI_SKILLS_ENV.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/CODEX_GITHUB_ISSUE_GOAL_LOOP.md b/ls/docs/CODEX_GITHUB_ISSUE_GOAL_LOOP.md index 10f9fdced..f7cba87af 100644 --- a/ls/docs/CODEX_GITHUB_ISSUE_GOAL_LOOP.md +++ b/ls/docs/CODEX_GITHUB_ISSUE_GOAL_LOOP.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_package: ls-wf-codex-github-issue-goal-loop --- diff --git a/ls/docs/COMMAND_REFERENCE.md b/ls/docs/COMMAND_REFERENCE.md index b88df69f4..fd3bc8980 100644 --- a/ls/docs/COMMAND_REFERENCE.md +++ b/ls/docs/COMMAND_REFERENCE.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/DECISION_TREE_WORKFLOW.md b/ls/docs/DECISION_TREE_WORKFLOW.md index cb380a61a..d8b105c80 100644 --- a/ls/docs/DECISION_TREE_WORKFLOW.md +++ b/ls/docs/DECISION_TREE_WORKFLOW.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_package: ls-wf-spec-clarify-reverse --- diff --git a/ls/docs/DOCUMENT_LIFECYCLE_MANAGEMENT.md b/ls/docs/DOCUMENT_LIFECYCLE_MANAGEMENT.md index 99078f4e7..178c40bc6 100644 --- a/ls/docs/DOCUMENT_LIFECYCLE_MANAGEMENT.md +++ b/ls/docs/DOCUMENT_LIFECYCLE_MANAGEMENT.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-docs-source-drift --- diff --git a/ls/docs/ENVMAN_INTEGRATION_CONTRACT.md b/ls/docs/ENVMAN_INTEGRATION_CONTRACT.md index 186b84f09..7e0f14cde 100644 --- a/ls/docs/ENVMAN_INTEGRATION_CONTRACT.md +++ b/ls/docs/ENVMAN_INTEGRATION_CONTRACT.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-architecture-decision-review --- diff --git a/ls/docs/FEATURES.md b/ls/docs/FEATURES.md index 63f4ba0ae..8ffa78c8d 100644 --- a/ls/docs/FEATURES.md +++ b/ls/docs/FEATURES.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-docs-create-move --- @@ -29,7 +29,7 @@ implemented controls and prerequisites. ## Generated Facts -- Current version: `4.89.10` +- Current version: `4.90.2` - Supported platforms: `codex, claude-code, cursor, kilo, opencode, openclaw, github-copilot-cli, github-copilot-vscode, cline-cli, cline-vscode, amp-cli, goose-cli, pi-cli, hermes-agent, qwen-code-cli, kimi-cli, factory-droid, antigravity-app, gemini-cli, omp-cli` - Shipped skills: `115` - Workflow packages: `21` diff --git a/ls/docs/FRAMEWORK_LIBRARY_ARCHITECTURE.md b/ls/docs/FRAMEWORK_LIBRARY_ARCHITECTURE.md index bfe3b4953..b36ea2f36 100644 --- a/ls/docs/FRAMEWORK_LIBRARY_ARCHITECTURE.md +++ b/ls/docs/FRAMEWORK_LIBRARY_ARCHITECTURE.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-architecture-decision-review --- diff --git a/ls/docs/FRONTEND_WEB_APP_SKILL_ROUTING.md b/ls/docs/FRONTEND_WEB_APP_SKILL_ROUTING.md index 2710e920a..9f17547f9 100644 --- a/ls/docs/FRONTEND_WEB_APP_SKILL_ROUTING.md +++ b/ls/docs/FRONTEND_WEB_APP_SKILL_ROUTING.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-task-skill-matcher --- diff --git a/ls/docs/GIT_TRACEABILITY.md b/ls/docs/GIT_TRACEABILITY.md index 2b9b3728f..f193208b7 100644 --- a/ls/docs/GIT_TRACEABILITY.md +++ b/ls/docs/GIT_TRACEABILITY.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-git-merge-rebase --- diff --git a/ls/docs/GLOBAL_HANDOFF_LEDGER.md b/ls/docs/GLOBAL_HANDOFF_LEDGER.md index 862af02a7..b1aff4dfb 100644 --- a/ls/docs/GLOBAL_HANDOFF_LEDGER.md +++ b/ls/docs/GLOBAL_HANDOFF_LEDGER.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/HARNESS_AUTOMATION.md b/ls/docs/HARNESS_AUTOMATION.md index f8fce7aec..228391e82 100644 --- a/ls/docs/HARNESS_AUTOMATION.md +++ b/ls/docs/HARNESS_AUTOMATION.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-codex-heartbeat-checks --- diff --git a/ls/docs/INPUT_HARDENING_STANDARD.md b/ls/docs/INPUT_HARDENING_STANDARD.md index e4947b675..7ce94686d 100644 --- a/ls/docs/INPUT_HARDENING_STANDARD.md +++ b/ls/docs/INPUT_HARDENING_STANDARD.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-script-docs-authoring --- diff --git a/ls/docs/LICENSING.md b/ls/docs/LICENSING.md index cbdabc086..b3606bce2 100644 --- a/ls/docs/LICENSING.md +++ b/ls/docs/LICENSING.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-docs-create-move --- diff --git a/ls/docs/LSCLI.md b/ls/docs/LSCLI.md index f0de0d31b..67b075d5e 100644 --- a/ls/docs/LSCLI.md +++ b/ls/docs/LSCLI.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-architecture-decision-review --- diff --git a/ls/docs/LSCLI_QUALIFICATION.md b/ls/docs/LSCLI_QUALIFICATION.md index b4f311c28..873c92913 100644 --- a/ls/docs/LSCLI_QUALIFICATION.md +++ b/ls/docs/LSCLI_QUALIFICATION.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-architecture-decision-review --- diff --git a/ls/docs/LSCLI_RUNTIME.md b/ls/docs/LSCLI_RUNTIME.md index 6c66f535d..f24051835 100644 --- a/ls/docs/LSCLI_RUNTIME.md +++ b/ls/docs/LSCLI_RUNTIME.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-architecture-decision-review --- diff --git a/ls/docs/MULTI_PLATFORM_INSTALL.md b/ls/docs/MULTI_PLATFORM_INSTALL.md index 79a1241a6..3681be115 100644 --- a/ls/docs/MULTI_PLATFORM_INSTALL.md +++ b/ls/docs/MULTI_PLATFORM_INSTALL.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/NODE_DASHBOARD_CONTROL_BOUNDARY.md b/ls/docs/NODE_DASHBOARD_CONTROL_BOUNDARY.md index 7ce6052bd..055f360b9 100644 --- a/ls/docs/NODE_DASHBOARD_CONTROL_BOUNDARY.md +++ b/ls/docs/NODE_DASHBOARD_CONTROL_BOUNDARY.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-system-design-tradeoffs --- diff --git a/ls/docs/OPENPGP_RUNTIME.md b/ls/docs/OPENPGP_RUNTIME.md index 0542b31b3..da0dc4801 100644 --- a/ls/docs/OPENPGP_RUNTIME.md +++ b/ls/docs/OPENPGP_RUNTIME.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-agentq-ship-ingest --- diff --git a/ls/docs/OUTPUT_AND_DOC_GENERATION.md b/ls/docs/OUTPUT_AND_DOC_GENERATION.md index d3598e66e..0fdbc529b 100644 --- a/ls/docs/OUTPUT_AND_DOC_GENERATION.md +++ b/ls/docs/OUTPUT_AND_DOC_GENERATION.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-script-docs-authoring last_updated: "2026-02-20" --- diff --git a/ls/docs/PLATFORM_REGISTRY.md b/ls/docs/PLATFORM_REGISTRY.md index 1f99d8738..a381fda31 100644 --- a/ls/docs/PLATFORM_REGISTRY.md +++ b/ls/docs/PLATFORM_REGISTRY.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/PLUGIN_PACKS.md b/ls/docs/PLUGIN_PACKS.md index 843e350b9..c1a85bb6f 100644 --- a/ls/docs/PLUGIN_PACKS.md +++ b/ls/docs/PLUGIN_PACKS.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-docs-create-move --- diff --git a/ls/docs/PRD_SCHEMA_EXTERNAL_AGENT_GUIDE.md b/ls/docs/PRD_SCHEMA_EXTERNAL_AGENT_GUIDE.md index b947acb32..7c5b8d8cd 100644 --- a/ls/docs/PRD_SCHEMA_EXTERNAL_AGENT_GUIDE.md +++ b/ls/docs/PRD_SCHEMA_EXTERNAL_AGENT_GUIDE.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-agentq-ship-ingest --- diff --git a/ls/docs/PYTHON_ARCHITECTURE_STANDARD.md b/ls/docs/PYTHON_ARCHITECTURE_STANDARD.md index 7d38f5e79..ab8df3508 100644 --- a/ls/docs/PYTHON_ARCHITECTURE_STANDARD.md +++ b/ls/docs/PYTHON_ARCHITECTURE_STANDARD.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-script-docs-authoring --- diff --git a/ls/docs/QUICKSTART.md b/ls/docs/QUICKSTART.md index 28af95a96..7ba35c43a 100644 --- a/ls/docs/QUICKSTART.md +++ b/ls/docs/QUICKSTART.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/README.md b/ls/docs/README.md index 9adb9fe23..e35e6c77f 100644 --- a/ls/docs/README.md +++ b/ls/docs/README.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-docs-create-move --- @@ -19,7 +19,7 @@ This is the public documentation map for LocalSetup. Start here when you want th ## Generated Facts -- Current version: `4.89.10` +- Current version: `4.90.2` - Supported platforms: `codex, claude-code, cursor, kilo, opencode, openclaw, github-copilot-cli, github-copilot-vscode, cline-cli, cline-vscode, amp-cli, goose-cli, pi-cli, hermes-agent, qwen-code-cli, kimi-cli, factory-droid, antigravity-app, gemini-cli, omp-cli` - Shipped skills: `115` - Workflow packages: `21` @@ -55,7 +55,7 @@ This is the public documentation map for LocalSetup. Start here when you want th | [Harness automation](HARNESS_AUTOMATION.md) | Opt-in heartbeat activation, typed LSCli profiles, reserved actions/controller accounting, runtime artifacts, cron gating and command-policy boundaries. | -Read the [current release guide](releases/4.89.10.md) for LocalSetup 4.89.10, including compatibility, updating, and verification. Find downloads in the [latest published release](https://github.com/CruxExperts/localsetup/releases/latest). +Read the [current release guide](releases/4.90.2.md) for LocalSetup 4.90.2, including compatibility, updating, and verification. Find downloads in the [latest published release](https://github.com/CruxExperts/localsetup/releases/latest). The [4.4.0 guide](releases/4.4.0.md) covers the earlier context and package consolidation. diff --git a/ls/docs/REPO_AND_DATA_SEPARATION.md b/ls/docs/REPO_AND_DATA_SEPARATION.md index 8d4551d12..d1be0be86 100644 --- a/ls/docs/REPO_AND_DATA_SEPARATION.md +++ b/ls/docs/REPO_AND_DATA_SEPARATION.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/REPO_CONVERSION.md b/ls/docs/REPO_CONVERSION.md index 187f67fcf..e34e39e24 100644 --- a/ls/docs/REPO_CONVERSION.md +++ b/ls/docs/REPO_CONVERSION.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_package: ls-wf-repository-localsetup-conversion --- diff --git a/ls/docs/REPO_MAINTENANCE.md b/ls/docs/REPO_MAINTENANCE.md index 611868be4..b1701a6ce 100644 --- a/ls/docs/REPO_MAINTENANCE.md +++ b/ls/docs/REPO_MAINTENANCE.md @@ -1,11 +1,32 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- # Repository Maintenance +## Dependency update integration + +The `astral-sh/setup-uv` action revision and its `with.version` input select +different components. Keep the action pinned to a reviewed full commit SHA; +update the installed `uv` version only when that tool update is also in scope. +The action's v10.3.3 release uses Astral's version-manifest mirror first, with +GitHub fallback, while retaining archive checksum verification. See the +[upstream release](https://github.com/astral-sh/setup-uv/releases/tag/v10.3.3). + +When updating `uv.lock`, refresh every affected exported dependency graph through +its existing generator. The S3 skill snapshots and reviewed Boto3 guard must agree +with the optional `s3-sdk` group; SDK runtime locks and their integrity receipt +must also agree with the canonical SDK export. Do not hand-edit distribution +hashes or assume a passing root lockfile check validates these exports. + +Treat passing dependency and bootstrap checks separately from framework test +coverage. Failed version or documentation prerequisites can skip every framework +shard; skipped shards are neither passing tests nor evidence of a dependency +runtime regression. Prepare version and generated-document synchronization +through the canonical publish preflight before publishing dependency updates. + ## Test resource safety Source pytest runs automatically enter a verified Linux cgroup v2 boundary @@ -45,6 +66,35 @@ must retain disabled distribution: re-enabling it recursively creates worker pools. The regression check covers that boundary. Resource exhaustion fails validation and must not be reported as a passing suite. +For native terminal smoke failures, retain a bounded tail of viewer output and +the process, endpoint and retirement state in assertion diagnostics. A nonzero +viewer exit remains a failure; do not relax clean-detach or cleanup assertions +merely because a failure did not reproduce on another host. +Keep one PTY reader and isolate the final-detach transition in the bounded tail. +Emit failure fields as separate lines rather than an assertion tuple that pytest +can shorten before displaying the terminal evidence. +For theme-preview failures, include the exact visible stage and whether menu and +preview receipts exist. Keep viewer tails bounded and redact fixture-private +paths; receipt contents are not needed to diagnose the handoff. +For private authentication dialogs, also retain stable popup error codes and +bounded process-identity observations, never form values, renderer arguments or +channel paths. Unknown delivery or unverified overlay cleanup remains a failure; +do not infer a successful authentication or replay it from a missing prompt. +Keep identity-predicate observations Boolean. Label process-state samples taken +after the command-line read explicitly; they do not establish the earlier +admission decision or authorize accepting an unqualified helper. +Concurrent native fixtures may briefly receive `busy` while an autonomous +finalizer owns the lifecycle lock. Use bounded contention handling only for +`busy` with `effect=none`; retain the original state and geometry assertions, +and never replay uncertain outcomes. + +Routing's reviewed static catalog expires at the end of its `fresh_until` date. +Refresh it only after checking every official source in the owning capability +matrix manifest. Update the review window, evidence access dates and canonical +snapshot/manifest integrity anchors together; do not bypass stale rejection or +renew dates without a source review. A catalog review does not change the active +client model configuration or automatically add newly documented candidates. + ## Adaptive workers and engineering provenance Local pytest uses one queue slot per active worker. The controller owns unstarted @@ -504,6 +554,16 @@ UV_CACHE_DIR=/tmp/localsetup-uv-cache uv run --locked python ls/tools/localsetup the session lock, keeping this final read out of runner exit; reconciliation may still publish telemetry during other lifecycle changes. The native family test keeps its passive eight-second cleanup assertion. +- The native popup fixture records bounded stderr classifications and a + non-reaping child exit observation only after an identity rejection; the + anonymous stderr capture is never rendered as raw text. A failed 180-column + primary handoff reports layout generations, client geometry, and pin/process + identities after the existing convergence deadline. The isolated family + fixture appends timestamped attach-wait and detach/cleanup events to its + private hook trace and includes a bounded tail on final viewer-exit failure. + These observations do not change admission, timeouts, or exit assertions and + are evidence for investigation, not proof of a fix. Treat fixture traces and + process identities as private; do not publish raw terminal output or inputs. - `publish` is explicitly dispatched on `main` after source checks and a verified signed tag. It prepares a validated release draft; complete its artifact inventory and notes before publication, following [VERSIONING.md](VERSIONING.md#github-release-workflow). It should not be a maintainer's first signal that version sync is missing. - `triage` labels issues and PRs from metadata only. It must not check out or run untrusted pull request code. Its handoff summary uses fixed text, event kind, and item number to link maintainers to the [Codex GitHub Issue Goal Loop](CODEX_GITHUB_ISSUE_GOAL_LOOP.md); it does not include issue or PR title/body text. - `triage` also bootstraps the maintainer label set used by issue forms and Dependabot. Run it manually once with `workflow_dispatch` before enabling Dependabot on a fresh repository. diff --git a/ls/docs/SDK_FORK.md b/ls/docs/SDK_FORK.md index a5893bee0..6ca29b4fb 100644 --- a/ls/docs/SDK_FORK.md +++ b/ls/docs/SDK_FORK.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-architecture-decision-review --- @@ -99,6 +99,14 @@ data. It validates before copying, includes every listed runtime resource, license, patch, and manifest, and verifies the resulting tree. Extra files, symlinks, or a different retained manifest in a reused build directory stop the build; use a fresh build output directory after changing the SDK source. +The build sets the packaged fixed `ls/tools/sudo_askpass` helper to mode 0755, +regardless of staging permissions, without changing its bytes. It rejects +symlinked helper destinations. Installed permissions also depend on the +installer: the managed runtime uses a private uv cache, copy mode and a +restrictive umask. The runtime still rejects a helper that is not executable or +is group- or world-writable; wheel permissions alone cannot guarantee an +arbitrary installation meets that requirement. + The build command loads its sibling verifier directly because setuptools loads custom commands before the source package is importable. The core package also @@ -120,11 +128,15 @@ which builds the public LocalSetup framework archive through its own packager. The SDK build regression inspects a real candidate sdist, checks the nested document bytes and archive exclusions, then exercises a portable `--preset all` install from that extracted candidate without installing dependencies. -The wheel regression also builds and installs a candidate wheel into a temporary -target without dependencies, then runs its installed `localsetup` entry point -without `--source-root` using the normal personal global preset. It verifies the -copied bootstrap reference documents and excludes private `docs/local-context` -content from the installed wheel and materialized references. +The wheel regression also builds from a permissive-mode helper input and checks +the helper's wheel mode, bytes, installed permissions, and runtime acceptance. +It installs the candidate into a temporary target without dependencies, using +the managed runtime's filtered uv environment, private cache, copy mode and +restrictive umask. It then runs its installed `localsetup` entry point without +`--source-root` using the normal personal global preset. It verifies the copied +bootstrap reference documents and +excludes private `docs/local-context` content from the installed wheel and +materialized references. Build and inspect candidates using the repository's pinned backend: diff --git a/ls/docs/SKILLS.md b/ls/docs/SKILLS.md index 841736ef2..ddd00d48c 100644 --- a/ls/docs/SKILLS.md +++ b/ls/docs/SKILLS.md @@ -1,14 +1,14 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_package: generate-docs localsetup_provenance: schema_version: 1 - source_provenance_hash: da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b + source_provenance_hash: fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f emitter: generate-docs -framework_version: 4.89.10 -source_commit: 0a1e7bed168b86d4a74801e95c413c12930ee7ee -artifact_sha256: 0d059193de13fb71f7f7235c6efd7a1b4fdfcb630f7110950d37ccabb58dfc76 +framework_version: 4.90.2 +source_commit: 79c243ebda390673a13cada9b0b3e8e5054b4b99 +artifact_sha256: 62013412dd5d1b84f25e790c1a30535d86fb12f4230c238b513332fdd358c040 --- # Shipped skills catalog diff --git a/ls/docs/SKILLS_AND_RULES.md b/ls/docs/SKILLS_AND_RULES.md index 7a27b66a7..b99142a2b 100644 --- a/ls/docs/SKILLS_AND_RULES.md +++ b/ls/docs/SKILLS_AND_RULES.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-task-skill-matcher --- diff --git a/ls/docs/SKILL_DISCOVERY.md b/ls/docs/SKILL_DISCOVERY.md index b5ec37cff..32b8ad7d8 100644 --- a/ls/docs/SKILL_DISCOVERY.md +++ b/ls/docs/SKILL_DISCOVERY.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-find-public-skills --- diff --git a/ls/docs/SKILL_IMPORTING.md b/ls/docs/SKILL_IMPORTING.md index c1ea840a5..673e77471 100644 --- a/ls/docs/SKILL_IMPORTING.md +++ b/ls/docs/SKILL_IMPORTING.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-import-external-skills --- diff --git a/ls/docs/SKILL_INTEROPERABILITY.md b/ls/docs/SKILL_INTEROPERABILITY.md index 1628ccd32..e5ba5d94b 100644 --- a/ls/docs/SKILL_INTEROPERABILITY.md +++ b/ls/docs/SKILL_INTEROPERABILITY.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-create-agent-skills --- diff --git a/ls/docs/SKILL_NORMALIZATION.md b/ls/docs/SKILL_NORMALIZATION.md index 64c92f8f7..f03d26dba 100644 --- a/ls/docs/SKILL_NORMALIZATION.md +++ b/ls/docs/SKILL_NORMALIZATION.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-normalize-skill-format --- diff --git a/ls/docs/SKILL_VALIDATION_PATTERNS.md b/ls/docs/SKILL_VALIDATION_PATTERNS.md index 0cbddd19b..b2b4a2d23 100644 --- a/ls/docs/SKILL_VALIDATION_PATTERNS.md +++ b/ls/docs/SKILL_VALIDATION_PATTERNS.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-skill-security-vetting --- diff --git a/ls/docs/STORAGE_SKILLS.md b/ls/docs/STORAGE_SKILLS.md index 66444c1da..e3432349c 100644 --- a/ls/docs/STORAGE_SKILLS.md +++ b/ls/docs/STORAGE_SKILLS.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-backblaze-b2-storage --- diff --git a/ls/docs/TASK_SKILL_MATCHING.md b/ls/docs/TASK_SKILL_MATCHING.md index 85c6c0c4f..63720f548 100644 --- a/ls/docs/TASK_SKILL_MATCHING.md +++ b/ls/docs/TASK_SKILL_MATCHING.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-task-skill-matcher --- diff --git a/ls/docs/TMUX_TERMINAL_MODE.md b/ls/docs/TMUX_TERMINAL_MODE.md index 988e99105..89821eb54 100644 --- a/ls/docs/TMUX_TERMINAL_MODE.md +++ b/ls/docs/TMUX_TERMINAL_MODE.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_package: ls-wf-tmux-terminal-mode --- diff --git a/ls/docs/TOOLING_POLICY.md b/ls/docs/TOOLING_POLICY.md index 9e3d44e1d..89ffe2af9 100644 --- a/ls/docs/TOOLING_POLICY.md +++ b/ls/docs/TOOLING_POLICY.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-script-docs-authoring --- diff --git a/ls/docs/TRUSTED_WORK_QUEUE.md b/ls/docs/TRUSTED_WORK_QUEUE.md index fc172d572..0d896ce7b 100644 --- a/ls/docs/TRUSTED_WORK_QUEUE.md +++ b/ls/docs/TRUSTED_WORK_QUEUE.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-architecture-decision-review --- diff --git a/ls/docs/VERSIONING.md b/ls/docs/VERSIONING.md index 30a91c79d..647db4270 100644 --- a/ls/docs/VERSIONING.md +++ b/ls/docs/VERSIONING.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-version-bump-commits --- @@ -11,7 +11,7 @@ LocalSetup uses the root `VERSION` file as the source of truth for the framework ## Current Version - Source of truth: [`../../VERSION`](../../VERSION) -- Current value: `4.89.10` +- Current value: `4.90.2` - Generated facts: [`_generated/facts.json`](_generated/facts.json) ## 4.x major-line lock and one-time numbering reconciliation diff --git a/ls/docs/WHY_LOCALSETUP.md b/ls/docs/WHY_LOCALSETUP.md index 33b1ca3d8..a84d1d103 100644 --- a/ls/docs/WHY_LOCALSETUP.md +++ b/ls/docs/WHY_LOCALSETUP.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-docs-create-move --- diff --git a/ls/docs/WORKFLOW_PACKAGES.md b/ls/docs/WORKFLOW_PACKAGES.md index 126686301..16536a906 100644 --- a/ls/docs/WORKFLOW_PACKAGES.md +++ b/ls/docs/WORKFLOW_PACKAGES.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-docs-create-move --- diff --git a/ls/docs/WORKFLOW_QUICK_REF.md b/ls/docs/WORKFLOW_QUICK_REF.md index fb79d19d6..7a097075f 100644 --- a/ls/docs/WORKFLOW_QUICK_REF.md +++ b/ls/docs/WORKFLOW_QUICK_REF.md @@ -1,14 +1,14 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_package: generate-docs localsetup_provenance: schema_version: 1 - source_provenance_hash: da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b + source_provenance_hash: fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f emitter: generate-docs -framework_version: 4.89.10 -source_commit: 0a1e7bed168b86d4a74801e95c413c12930ee7ee -artifact_sha256: 45ec2266b0d1fb7cda42e95eed2dac08569969331dac20774676e0380ad37caf +framework_version: 4.90.2 +source_commit: 79c243ebda390673a13cada9b0b3e8e5054b4b99 +artifact_sha256: a4b461150d256a3f5b30a3f72e96b617dd4760a74716d6a2638acb41e75deea8 --- # Workflow quick reference diff --git a/ls/docs/WORKFLOW_REGISTRY.md b/ls/docs/WORKFLOW_REGISTRY.md index 7238894a7..8d364a8ca 100644 --- a/ls/docs/WORKFLOW_REGISTRY.md +++ b/ls/docs/WORKFLOW_REGISTRY.md @@ -1,14 +1,14 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_package: generate-docs localsetup_provenance: schema_version: 1 - source_provenance_hash: da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b + source_provenance_hash: fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f emitter: generate-docs -framework_version: 4.89.10 -source_commit: 0a1e7bed168b86d4a74801e95c413c12930ee7ee -artifact_sha256: 7e4295af685adcaa9cea0ab89db9593e3daad12e29041c8914f6e35f1834c1db +framework_version: 4.90.2 +source_commit: 79c243ebda390673a13cada9b0b3e8e5054b4b99 +artifact_sha256: 9eee9b13396f27436dab26cebe9380b85f3f0eb54adec69b934e02f4013c1306 --- # Workflow and module registry (LocalSetup) diff --git a/ls/docs/WORKFLOW_SKILLS_REVIEW_BUILD_SPEC.md b/ls/docs/WORKFLOW_SKILLS_REVIEW_BUILD_SPEC.md index 2b0c7cc66..1d4ea35fb 100644 --- a/ls/docs/WORKFLOW_SKILLS_REVIEW_BUILD_SPEC.md +++ b/ls/docs/WORKFLOW_SKILLS_REVIEW_BUILD_SPEC.md @@ -1,6 +1,6 @@ --- status: DEPRECATED -version: 4.89 +version: 4.90 --- # Workflow Skills Review Build Spec diff --git a/ls/docs/WORKFLOW_STANDARD.md b/ls/docs/WORKFLOW_STANDARD.md index 3cc6a45b7..5c157f50f 100644 --- a/ls/docs/WORKFLOW_STANDARD.md +++ b/ls/docs/WORKFLOW_STANDARD.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/_generated/artifact-registry.json b/ls/docs/_generated/artifact-registry.json index e55ca12b5..c5bc505a4 100644 --- a/ls/docs/_generated/artifact-registry.json +++ b/ls/docs/_generated/artifact-registry.json @@ -3,10 +3,10 @@ { "artifact_sha256": "bed10d1fb964d318474266896970cea6e9b9e284e8c012e81f01fd584439af2c", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "assets/README.md", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -23,12 +23,12 @@ "type": "markdown" }, { - "artifact_sha256": "3ef05585ecc30f318204c882f7f0a32b28c589e8eb113245d8b0c2cad9dc985b", + "artifact_sha256": "876c289e08f9ba1710871e72571b6f0b69646417e012acd5126b1bb420b44d7c", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/SKILLS.md", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -45,12 +45,12 @@ "type": "markdown" }, { - "artifact_sha256": "d9769465145f94dc322444fbe44626a5e580cef9793c9d93d5ea45812810944c", + "artifact_sha256": "5e01a51f861f90551883cde2d2f88fea0c34309d125296e22c9d798e15804c01", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/WORKFLOW_QUICK_REF.md", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -67,12 +67,12 @@ "type": "markdown" }, { - "artifact_sha256": "907fc42f293193b08ab160acfc1dfd11da405dc72d4ca1ea25bf9dc12eb8c4d2", + "artifact_sha256": "bfd4467922e666c2ba9f1d083c299daad0be97d691e4c111063edeef07cc1d8c", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/WORKFLOW_REGISTRY.md", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -89,12 +89,12 @@ "type": "markdown" }, { - "artifact_sha256": "b99e57f2668f64c9f951aaf68e6795fd913de52923ab625ce6acde8a979a010b", + "artifact_sha256": "68503a4245792880b91a6b9661c83a64b1294815df0b1d0a4a4afa8ecec4c446", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/docs-alignment-summary.md", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -111,12 +111,12 @@ "type": "markdown" }, { - "artifact_sha256": "3e6e9739a36283fff58c05e7886e2d246241cd67fb03802eccd2fa6de8862546", + "artifact_sha256": "dc3d68fee7de1297705e6ff574bbbf95d3773cd80aed8750428ce4aaee74b4fa", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/docs-asset-manifest.json", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -133,12 +133,12 @@ "type": "json" }, { - "artifact_sha256": "9a0a6b6881d2d436691bf25930208b1875b186055b85dc18ea033408b2637440", + "artifact_sha256": "073e548547f623656285ce09e647dd52b5fb6dc6da19265925323469be101040", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/docs-audit-result.json", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -155,12 +155,12 @@ "type": "json" }, { - "artifact_sha256": "1d921be540e811cff6345a05ca121fe9c8c2a56e8acca69977bbb7ee25d42b0d", + "artifact_sha256": "a9251b715de66f34ac6bab514ff2b6228ef4c1a04ef9a5a805653c456a747ba9", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/docs-inventory.json", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -177,12 +177,12 @@ "type": "json" }, { - "artifact_sha256": "d1b7d59545473d7d6aa2c495675730b3931f5faa14062abb9781a35367e4c6a4", + "artifact_sha256": "1fc8f29d940a5378f404d0878f31fea2600d246095f4bbfb4e5e34398b6bdc14", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/docs-truth-map.json", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -199,12 +199,12 @@ "type": "json" }, { - "artifact_sha256": "31c018495243c8fc4cf76460b5b9babf5cefc5676b9772bb430c44cf2079c29e", + "artifact_sha256": "0146354ddc442e56cd248a82e637b4b57887ec4b4c1bda9f4a340bf7ebab619f", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/facts.json", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -221,12 +221,12 @@ "type": "json" }, { - "artifact_sha256": "58ca28231fa82803f05f8caf68016f4eb76a0a3e2937f2070045ed5f3d7bfd1e", + "artifact_sha256": "d1e9ba5f45b5bfab3a744de89f8192ea870f437422d35d8209071f55c274820c", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/implementation-file-map.md", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -243,12 +243,12 @@ "type": "markdown" }, { - "artifact_sha256": "1104c2e17cf8dad9da92ef7c77617dc7a963853440cac3f9f057176c419ce779", + "artifact_sha256": "ce56d8b1c3687c16371118db4564fe02ae3215b95b8e435829f368b69903f523", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/platform-adapters.md", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -265,12 +265,12 @@ "type": "markdown" }, { - "artifact_sha256": "c906fad067b73e7782c7e771748ecb03462db0e8b06efaa79088659c869c1aac", + "artifact_sha256": "54371adf651cefbb18a778b38d0d31ee183add5f9403d78e8e997eae538eeacb", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/plugin-packs.json", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -287,12 +287,12 @@ "type": "json" }, { - "artifact_sha256": "872f768dc4d035a7042e4875d3a495ec79f11f248bb2f6ead00da7c9a0b6df75", + "artifact_sha256": "c3bd6b5847501b44e832e9f5052daa3c4821d3d399afebc3df6c90a76274deb8", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/plugin-packs.md", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -309,12 +309,12 @@ "type": "markdown" }, { - "artifact_sha256": "e29b6e5364d700e33f8aca80eeee7fe59e3623e09e9ff5508850e4da0c269975", + "artifact_sha256": "cea36ebbc2eb29a5621063f761531220d0fe36e813c018b2d0b38827a36cdbac", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/skill-packs.md", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -331,12 +331,12 @@ "type": "markdown" }, { - "artifact_sha256": "58e1a44c65491876343c8243c7275d3154be7c6783d2a332c267653770f9b4ac", + "artifact_sha256": "089f1d6fba99a2f04262c9be19f69863f3d794752bb1672a83f5605a3ed3e938", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/skill-taxonomy.json", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -353,12 +353,12 @@ "type": "json" }, { - "artifact_sha256": "c592a047b958d083e2827f501b8b26cb877b2fbd631c86bb7108af6f9fd5adb8", + "artifact_sha256": "3b9994e44f179634a55c19920e6bffae70a109ff25de823e7743ecf2c4c98aee", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/skill_aliases.json", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -375,12 +375,12 @@ "type": "json" }, { - "artifact_sha256": "aa80f78f578be7eee1ce8a8c769a099d657c62dbe98f2bacf74847dbb70b0666", + "artifact_sha256": "00d5413d23cf754a9a7430b21727f5c823ca6e1ce05b83c13280093c9b51fd02", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/_generated/workflow-catalog.json", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -397,12 +397,12 @@ "type": "json" }, { - "artifact_sha256": "acadd3c2e1c5cd6961c94fd0109cacaca75d6fcbc4663a563307af4b374aa07c", + "artifact_sha256": "43a4019f4152456c1698d298eb417504a87e12be4f9dec286000955bd649e82c", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "path": "ls/docs/migration/skill-alias-map.md", - "provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", + "provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", "source_inputs": [ "VERSION", "ls/skills", @@ -421,16 +421,16 @@ ], "provenance": { "artifact_path": "ls/docs/_generated/artifact-registry.json", - "artifact_sha256": "1fabe327b08a01e586bdf6f97f12e6cc6c6125c91f022d310c299c0bebbd3602", + "artifact_sha256": "9112cfab8e73400c05960d6ab9e543e6d12cc0672ab10392b0c4e1c2b0bdc03f", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "schema_version": 1, - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", - "source_dirty": false, - "source_provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_root_id": "4e46d3aab17f450f4229b417c716410a50584c4f96918aca19c4bd12b0e7c494", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", + "source_dirty": true, + "source_provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_root_id": "67b326143cdb933493f0017283b66af024262e6add9fc200a3021593ccb173a2", "source_tag": null, - "source_tree_sha": "8b7f6d1646f44125531a941231e03d631e1c5c80" + "source_tree_sha": "ae1d3b28f3503d50ac20227f6923fe7c4b6695d7" }, "schema_version": 1 } diff --git a/ls/docs/_generated/docs-alignment-summary.md b/ls/docs/_generated/docs-alignment-summary.md index 8bd830682..f91379c2f 100644 --- a/ls/docs/_generated/docs-alignment-summary.md +++ b/ls/docs/_generated/docs-alignment-summary.md @@ -1,14 +1,14 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_package: docs-align localsetup_provenance: schema_version: 1 - source_provenance_hash: da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b + source_provenance_hash: fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f emitter: docs-align -framework_version: 4.89.10 -source_commit: 0a1e7bed168b86d4a74801e95c413c12930ee7ee -artifact_sha256: 598f25bd27398d5c53d2fa86bedf255eddcbdc9c6b36641068e58eb8cf7622c0 +framework_version: 4.90.2 +source_commit: 79c243ebda390673a13cada9b0b3e8e5054b4b99 +artifact_sha256: 0fe136881baa91d15d64c8aeb2931ecb10d3ea126ad70313fcb4a1639a246789 --- # Documentation Alignment Summary @@ -16,8 +16,8 @@ This page is generated from repository inventory, source-truth manifests, asset | Signal | Value | |---|---:| -| Version | `4.89.10` | -| Documentation files inventoried | 569 | +| Version | `4.90.2` | +| Documentation files inventoried | 574 | | Immutable upstream documents | 64 | | Shipped skills | 115 | | Workflow packages | 21 | diff --git a/ls/docs/_generated/docs-asset-manifest.json b/ls/docs/_generated/docs-asset-manifest.json index a45ffb188..c4ae2541f 100644 --- a/ls/docs/_generated/docs-asset-manifest.json +++ b/ls/docs/_generated/docs-asset-manifest.json @@ -252,14 +252,14 @@ "artifact_path": "ls/docs/_generated/docs-asset-manifest.json", "artifact_sha256": "f757bc8e39f708af157ee6f777eaf0e044021005a62ce49546b214cd2e66e95a", "emitter": "docs-align", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "schema_version": 1, - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", - "source_dirty": false, - "source_provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_root_id": "4e46d3aab17f450f4229b417c716410a50584c4f96918aca19c4bd12b0e7c494", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", + "source_dirty": true, + "source_provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_root_id": "67b326143cdb933493f0017283b66af024262e6add9fc200a3021593ccb173a2", "source_tag": null, - "source_tree_sha": "8b7f6d1646f44125531a941231e03d631e1c5c80" + "source_tree_sha": "ae1d3b28f3503d50ac20227f6923fe7c4b6695d7" }, "schema_version": "1.0" } diff --git a/ls/docs/_generated/docs-audit-result.json b/ls/docs/_generated/docs-audit-result.json index 818e84ac4..355c6f651 100644 --- a/ls/docs/_generated/docs-audit-result.json +++ b/ls/docs/_generated/docs-audit-result.json @@ -9,14 +9,14 @@ "artifact_path": "ls/docs/_generated/docs-audit-result.json", "artifact_sha256": "e82619e42f31266261fa11fa954df7778b2d19b7f12d224a71a85ec1a8cf31a8", "emitter": "docs-align", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "schema_version": 1, - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", - "source_dirty": false, - "source_provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_root_id": "4e46d3aab17f450f4229b417c716410a50584c4f96918aca19c4bd12b0e7c494", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", + "source_dirty": true, + "source_provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_root_id": "67b326143cdb933493f0017283b66af024262e6add9fc200a3021593ccb173a2", "source_tag": null, - "source_tree_sha": "8b7f6d1646f44125531a941231e03d631e1c5c80" + "source_tree_sha": "ae1d3b28f3503d50ac20227f6923fe7c4b6695d7" }, "schema_version": "1.0" } diff --git a/ls/docs/_generated/docs-inventory.json b/ls/docs/_generated/docs-inventory.json index 6e5c70370..e62c54fec 100644 --- a/ls/docs/_generated/docs-inventory.json +++ b/ls/docs/_generated/docs-inventory.json @@ -326,8 +326,8 @@ "workflow" ], "counts": { - "docs": 569, - "framework_docs": 122, + "docs": 574, + "framework_docs": 127, "generated_docs": 5, "platforms": 20, "public_docs": 6, @@ -550,7 +550,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/ADAPTER_OWNERSHIP.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -560,7 +560,7 @@ "owner_skill": "ls-agentq-ship-ingest", "path": "ls/docs/AGENTIC_AGENT_Q_BIDIRECTIONAL_BUILD_SPEC.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -570,7 +570,7 @@ "owner_skill": "ls-agentq-ship-ingest", "path": "ls/docs/AGENTIC_AGENT_Q_PATTERN.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -580,7 +580,7 @@ "owner_skill": "ls-agentq-ship-ingest", "path": "ls/docs/AGENTIC_AGENT_Q_SCENARIOS.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -590,7 +590,7 @@ "owner_skill": "ls-agentq-ship-ingest", "path": "ls/docs/AGENTIC_AGENT_TO_AGENT_PROTOCOL.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -600,7 +600,7 @@ "owner_skill": "ls-docs-create-move", "path": "ls/docs/AGENTIC_DESIGN_INDEX.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -610,7 +610,7 @@ "owner_skill": "", "path": "ls/docs/AGENTIC_UMBRELLA_WORKFLOWS.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -620,7 +620,7 @@ "owner_skill": "ls-context-index-search", "path": "ls/docs/AGENT_CONTEXT_AND_MCP_CONTRACT.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -630,7 +630,7 @@ "owner_skill": "ls-create-agent-skills", "path": "ls/docs/AGENT_SKILLS_COMPLIANCE.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -640,7 +640,7 @@ "owner_skill": "ls-script-docs-authoring", "path": "ls/docs/BRANDING.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -650,7 +650,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/CLIENT_INTEGRATION_METADATA.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -660,7 +660,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/CLIENT_STATE.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -670,7 +670,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/CLI_SKILLS_ENV.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -680,7 +680,7 @@ "owner_skill": "", "path": "ls/docs/CODEX_GITHUB_ISSUE_GOAL_LOOP.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -690,7 +690,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/COMMAND_REFERENCE.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -700,7 +700,7 @@ "owner_skill": "", "path": "ls/docs/DECISION_TREE_WORKFLOW.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -710,7 +710,7 @@ "owner_skill": "ls-docs-source-drift", "path": "ls/docs/DOCUMENT_LIFECYCLE_MANAGEMENT.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -720,7 +720,7 @@ "owner_skill": "ls-architecture-decision-review", "path": "ls/docs/ENVMAN_INTEGRATION_CONTRACT.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -732,7 +732,7 @@ "owner_skill": "ls-docs-create-move", "path": "ls/docs/FEATURES.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -742,7 +742,7 @@ "owner_skill": "ls-architecture-decision-review", "path": "ls/docs/FRAMEWORK_LIBRARY_ARCHITECTURE.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -752,7 +752,7 @@ "owner_skill": "ls-task-skill-matcher", "path": "ls/docs/FRONTEND_WEB_APP_SKILL_ROUTING.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -762,7 +762,7 @@ "owner_skill": "ls-git-merge-rebase", "path": "ls/docs/GIT_TRACEABILITY.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -772,7 +772,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/GLOBAL_HANDOFF_LEDGER.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -782,7 +782,7 @@ "owner_skill": "ls-codex-heartbeat-checks", "path": "ls/docs/HARNESS_AUTOMATION.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -792,7 +792,7 @@ "owner_skill": "ls-script-docs-authoring", "path": "ls/docs/INPUT_HARDENING_STANDARD.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -802,7 +802,7 @@ "owner_skill": "ls-docs-create-move", "path": "ls/docs/LICENSING.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -812,7 +812,7 @@ "owner_skill": "ls-architecture-decision-review", "path": "ls/docs/LSCLI.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -822,7 +822,7 @@ "owner_skill": "ls-architecture-decision-review", "path": "ls/docs/LSCLI_QUALIFICATION.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -832,7 +832,7 @@ "owner_skill": "ls-architecture-decision-review", "path": "ls/docs/LSCLI_RUNTIME.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -842,7 +842,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/MULTI_PLATFORM_INSTALL.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -852,7 +852,7 @@ "owner_skill": "ls-system-design-tradeoffs", "path": "ls/docs/NODE_DASHBOARD_CONTROL_BOUNDARY.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -862,7 +862,7 @@ "owner_skill": "ls-agentq-ship-ingest", "path": "ls/docs/OPENPGP_RUNTIME.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -872,7 +872,7 @@ "owner_skill": "ls-script-docs-authoring", "path": "ls/docs/OUTPUT_AND_DOC_GENERATION.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -882,7 +882,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/PLATFORM_REGISTRY.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -892,7 +892,7 @@ "owner_skill": "ls-docs-create-move", "path": "ls/docs/PLUGIN_PACKS.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -902,7 +902,7 @@ "owner_skill": "ls-agentq-ship-ingest", "path": "ls/docs/PRD_SCHEMA_EXTERNAL_AGENT_GUIDE.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -912,7 +912,7 @@ "owner_skill": "ls-script-docs-authoring", "path": "ls/docs/PYTHON_ARCHITECTURE_STANDARD.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -922,7 +922,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/QUICKSTART.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -935,7 +935,7 @@ "owner_skill": "ls-docs-create-move", "path": "ls/docs/README.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -945,7 +945,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/REPO_AND_DATA_SEPARATION.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -955,7 +955,7 @@ "owner_skill": "", "path": "ls/docs/REPO_CONVERSION.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -965,7 +965,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/REPO_MAINTENANCE.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -975,7 +975,7 @@ "owner_skill": "ls-architecture-decision-review", "path": "ls/docs/SDK_FORK.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -985,7 +985,7 @@ "owner_skill": "", "path": "ls/docs/SKILLS.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -995,7 +995,7 @@ "owner_skill": "ls-task-skill-matcher", "path": "ls/docs/SKILLS_AND_RULES.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1005,7 +1005,7 @@ "owner_skill": "ls-find-public-skills", "path": "ls/docs/SKILL_DISCOVERY.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1015,7 +1015,7 @@ "owner_skill": "ls-import-external-skills", "path": "ls/docs/SKILL_IMPORTING.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1025,7 +1025,7 @@ "owner_skill": "ls-create-agent-skills", "path": "ls/docs/SKILL_INTEROPERABILITY.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1035,7 +1035,7 @@ "owner_skill": "ls-normalize-skill-format", "path": "ls/docs/SKILL_NORMALIZATION.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1045,7 +1045,7 @@ "owner_skill": "ls-skill-security-vetting", "path": "ls/docs/SKILL_VALIDATION_PATTERNS.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1055,7 +1055,7 @@ "owner_skill": "ls-backblaze-b2-storage", "path": "ls/docs/STORAGE_SKILLS.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1065,7 +1065,7 @@ "owner_skill": "ls-task-skill-matcher", "path": "ls/docs/TASK_SKILL_MATCHING.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1075,7 +1075,7 @@ "owner_skill": "", "path": "ls/docs/TMUX_TERMINAL_MODE.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1085,7 +1085,7 @@ "owner_skill": "ls-script-docs-authoring", "path": "ls/docs/TOOLING_POLICY.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1095,7 +1095,7 @@ "owner_skill": "ls-architecture-decision-review", "path": "ls/docs/TRUSTED_WORK_QUEUE.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1105,7 +1105,7 @@ "owner_skill": "ls-version-bump-commits", "path": "ls/docs/VERSIONING.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1115,7 +1115,7 @@ "owner_skill": "ls-docs-create-move", "path": "ls/docs/WHY_LOCALSETUP.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1125,7 +1125,7 @@ "owner_skill": "ls-docs-create-move", "path": "ls/docs/WORKFLOW_PACKAGES.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1135,7 +1135,7 @@ "owner_skill": "", "path": "ls/docs/WORKFLOW_QUICK_REF.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1145,7 +1145,7 @@ "owner_skill": "", "path": "ls/docs/WORKFLOW_REGISTRY.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1155,7 +1155,7 @@ "owner_skill": "", "path": "ls/docs/WORKFLOW_SKILLS_REVIEW_BUILD_SPEC.md", "status": "DEPRECATED", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1165,7 +1165,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/WORKFLOW_STANDARD.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "generated", @@ -1175,7 +1175,7 @@ "owner_skill": "", "path": "ls/docs/_generated/docs-alignment-summary.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "generated", @@ -1305,7 +1305,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/bootstrap-packs/INDEX.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1315,7 +1315,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/bootstrap-packs/codex-agent-team/AUDIT_PROMPT.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1325,7 +1325,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/bootstrap-packs/codex-agent-team/README.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1335,7 +1335,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/bootstrap-packs/opencode-agent-team/AUDIT_PROMPT.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1345,7 +1345,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/bootstrap-packs/opencode-agent-team/MODEL_MAP.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1355,7 +1355,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/bootstrap-packs/opencode-agent-team/README.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1375,7 +1375,7 @@ "owner_skill": "ls-localsetup-framework-changes", "path": "ls/docs/migration/overview.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1385,7 +1385,7 @@ "owner_skill": "", "path": "ls/docs/migration/skill-alias-map.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1395,7 +1395,7 @@ "owner_skill": "ls-herdr-task-routing", "path": "ls/docs/ops/herdr-ops.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1405,7 +1405,7 @@ "owner_skill": "", "path": "ls/docs/ops/tmux-ops-managed.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1415,7 +1415,7 @@ "owner_skill": "", "path": "ls/docs/ops/tmux-ops-remote.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "framework", @@ -1787,6 +1787,26 @@ "status": "ACTIVE", "version": "4.89" }, + { + "class": "framework", + "has_frontmatter": true, + "managed_blocks": [], + "owner_package": "", + "owner_skill": "ls-github-release-publishing", + "path": "ls/docs/releases/4.89.11.md", + "status": "ACTIVE", + "version": "4.89" + }, + { + "class": "framework", + "has_frontmatter": true, + "managed_blocks": [], + "owner_package": "", + "owner_skill": "ls-github-release-publishing", + "path": "ls/docs/releases/4.89.12.md", + "status": "ACTIVE", + "version": "4.89" + }, { "class": "framework", "has_frontmatter": true, @@ -1807,6 +1827,36 @@ "status": "ACTIVE", "version": "4.89" }, + { + "class": "framework", + "has_frontmatter": true, + "managed_blocks": [], + "owner_package": "", + "owner_skill": "ls-github-release-publishing", + "path": "ls/docs/releases/4.90.0.md", + "status": "ACTIVE", + "version": "4.9" + }, + { + "class": "framework", + "has_frontmatter": true, + "managed_blocks": [], + "owner_package": "", + "owner_skill": "ls-github-release-publishing", + "path": "ls/docs/releases/4.90.1.md", + "status": "ACTIVE", + "version": "4.9" + }, + { + "class": "framework", + "has_frontmatter": true, + "managed_blocks": [], + "owner_package": "", + "owner_skill": "ls-github-release-publishing", + "path": "ls/docs/releases/4.90.2.md", + "status": "ACTIVE", + "version": "4.9" + }, { "class": "framework", "has_frontmatter": true, @@ -1815,7 +1865,7 @@ "owner_skill": "ls-scrapling-url-extraction", "path": "ls/docs/scrapling-cheat-sheet.md", "status": "ACTIVE", - "version": "4.89" + "version": "4.9" }, { "class": "skill", @@ -6482,16 +6532,16 @@ ], "provenance": { "artifact_path": "ls/docs/_generated/docs-inventory.json", - "artifact_sha256": "299cd28d3f81839ee4111017038c8fad77563e2ebb88ecf3d60d3a081ba9c4a2", + "artifact_sha256": "2d4a84ea19218819ca12cc319fcf67031c12e9bfaf0a0d7b2e246d98e921210b", "emitter": "docs-align", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "schema_version": 1, - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", - "source_dirty": false, - "source_provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_root_id": "4e46d3aab17f450f4229b417c716410a50584c4f96918aca19c4bd12b0e7c494", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", + "source_dirty": true, + "source_provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_root_id": "67b326143cdb933493f0017283b66af024262e6add9fc200a3021593ccb173a2", "source_tag": null, - "source_tree_sha": "8b7f6d1646f44125531a941231e03d631e1c5c80" + "source_tree_sha": "ae1d3b28f3503d50ac20227f6923fe7c4b6695d7" }, "repo": ".", "schema_version": "1.0", diff --git a/ls/docs/_generated/docs-truth-map.json b/ls/docs/_generated/docs-truth-map.json index 105fd5fa8..4b1383c38 100644 --- a/ls/docs/_generated/docs-truth-map.json +++ b/ls/docs/_generated/docs-truth-map.json @@ -1,6 +1,6 @@ { "generated_facts": { - "major_minor": "4.89", + "major_minor": "4.90", "platform_count": 20, "platforms": [ { @@ -126,16 +126,16 @@ ], "provenance": { "artifact_path": "ls/docs/_generated/facts.json", - "artifact_sha256": "a00e10e159d1d94a5b5dfc2fd5282d0f46043655763a94438036c240f799ff1d", + "artifact_sha256": "0da459fd86c0c59864f95bd765a49d8d2530256dffb0375caeea816dd87c6275", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "schema_version": 1, - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", - "source_dirty": false, - "source_provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_root_id": "4e46d3aab17f450f4229b417c716410a50584c4f96918aca19c4bd12b0e7c494", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", + "source_dirty": true, + "source_provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_root_id": "67b326143cdb933493f0017283b66af024262e6add9fc200a3021593ccb173a2", "source_tag": null, - "source_tree_sha": "8b7f6d1646f44125531a941231e03d631e1c5c80" + "source_tree_sha": "ae1d3b28f3503d50ac20227f6923fe7c4b6695d7" }, "skill_count": 115, "skills": [ @@ -2070,7 +2070,7 @@ "version": "1.0" } ], - "version": "4.89.10", + "version": "4.90.2", "workflow_count": 21, "workflows": [ { @@ -2216,14 +2216,14 @@ "artifact_path": "ls/docs/_generated/skill-taxonomy.json", "artifact_sha256": "3748c9f3ed45a4bb66d591327dbc435014bd7bc4c66dc1e04faff1f650a75a79", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "schema_version": 1, - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", - "source_dirty": false, - "source_provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_root_id": "4e46d3aab17f450f4229b417c716410a50584c4f96918aca19c4bd12b0e7c494", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", + "source_dirty": true, + "source_provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_root_id": "67b326143cdb933493f0017283b66af024262e6add9fc200a3021593ccb173a2", "source_tag": null, - "source_tree_sha": "8b7f6d1646f44125531a941231e03d631e1c5c80" + "source_tree_sha": "ae1d3b28f3503d50ac20227f6923fe7c4b6695d7" }, "schema_version": 1, "skills": [ @@ -3931,16 +3931,16 @@ }, "provenance": { "artifact_path": "ls/docs/_generated/docs-truth-map.json", - "artifact_sha256": "3333e9876a4cc3e16dafd26648459f2e6160b03bc0a47d0c88e0cc33fa90eb2d", + "artifact_sha256": "9c6460a1a109547f0b40f4f75832b5fedbd80ac20b83b8efe381d01239640775", "emitter": "docs-align", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "schema_version": 1, - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", - "source_dirty": false, - "source_provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_root_id": "4e46d3aab17f450f4229b417c716410a50584c4f96918aca19c4bd12b0e7c494", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", + "source_dirty": true, + "source_provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_root_id": "67b326143cdb933493f0017283b66af024262e6add9fc200a3021593ccb173a2", "source_tag": null, - "source_tree_sha": "8b7f6d1646f44125531a941231e03d631e1c5c80" + "source_tree_sha": "ae1d3b28f3503d50ac20227f6923fe7c4b6695d7" }, "schema_version": "1.0", "truths": { @@ -4534,6 +4534,16 @@ "owner_skill": "ls-github-release-publishing", "path": "ls/docs/releases/4.89.10.md" }, + { + "owner_package": "", + "owner_skill": "ls-github-release-publishing", + "path": "ls/docs/releases/4.89.11.md" + }, + { + "owner_package": "", + "owner_skill": "ls-github-release-publishing", + "path": "ls/docs/releases/4.89.12.md" + }, { "owner_package": "", "owner_skill": "ls-github-release-publishing", @@ -4544,6 +4554,21 @@ "owner_skill": "ls-github-release-publishing", "path": "ls/docs/releases/4.89.8.md" }, + { + "owner_package": "", + "owner_skill": "ls-github-release-publishing", + "path": "ls/docs/releases/4.90.0.md" + }, + { + "owner_package": "", + "owner_skill": "ls-github-release-publishing", + "path": "ls/docs/releases/4.90.1.md" + }, + { + "owner_package": "", + "owner_skill": "ls-github-release-publishing", + "path": "ls/docs/releases/4.90.2.md" + }, { "owner_package": "", "owner_skill": "ls-scrapling-url-extraction", @@ -4555,7 +4580,7 @@ "sources": [ "VERSION" ], - "value": "4.89" + "value": "4.90" }, "platform_count": { "sources": [ @@ -6292,14 +6317,14 @@ } }, "version": { - "facts_version": "4.89.10", - "pyproject_version": "4.89.10", + "facts_version": "4.90.2", + "pyproject_version": "4.90.2", "sources": [ "VERSION", "pyproject.toml", "ls/docs/_generated/facts.json" ], - "value": "4.89.10" + "value": "4.90.2" }, "workflow_catalog": { "sources": [ diff --git a/ls/docs/_generated/facts.json b/ls/docs/_generated/facts.json index c92114991..fe7753d61 100644 --- a/ls/docs/_generated/facts.json +++ b/ls/docs/_generated/facts.json @@ -1,5 +1,5 @@ { - "major_minor": "4.89", + "major_minor": "4.90", "platform_count": 20, "platforms": [ { @@ -125,16 +125,16 @@ ], "provenance": { "artifact_path": "ls/docs/_generated/facts.json", - "artifact_sha256": "a00e10e159d1d94a5b5dfc2fd5282d0f46043655763a94438036c240f799ff1d", + "artifact_sha256": "0da459fd86c0c59864f95bd765a49d8d2530256dffb0375caeea816dd87c6275", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "schema_version": 1, - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", - "source_dirty": false, - "source_provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_root_id": "4e46d3aab17f450f4229b417c716410a50584c4f96918aca19c4bd12b0e7c494", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", + "source_dirty": true, + "source_provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_root_id": "67b326143cdb933493f0017283b66af024262e6add9fc200a3021593ccb173a2", "source_tag": null, - "source_tree_sha": "8b7f6d1646f44125531a941231e03d631e1c5c80" + "source_tree_sha": "ae1d3b28f3503d50ac20227f6923fe7c4b6695d7" }, "skill_count": 115, "skills": [ @@ -2069,7 +2069,7 @@ "version": "1.0" } ], - "version": "4.89.10", + "version": "4.90.2", "workflow_count": 21, "workflows": [ { diff --git a/ls/docs/_generated/implementation-file-map.md b/ls/docs/_generated/implementation-file-map.md index 0c488338b..97eda4c60 100644 --- a/ls/docs/_generated/implementation-file-map.md +++ b/ls/docs/_generated/implementation-file-map.md @@ -1,11 +1,11 @@ --- localsetup_provenance: schema_version: 1 - source_provenance_hash: da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b + source_provenance_hash: fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f emitter: generate-docs -framework_version: 4.89.10 -source_commit: 0a1e7bed168b86d4a74801e95c413c12930ee7ee -artifact_sha256: e3ab7f2aa31bdd57e54d6ade8d55a32d260e6c03b5cae66a2b4a0efe9ae09ff4 +framework_version: 4.90.2 +source_commit: 79c243ebda390673a13cada9b0b3e8e5054b4b99 +artifact_sha256: d0460c03a0359e1a7d05c09e8f66dcf1d39fce5c8c2197b09e24a15d687beeb8 --- # Implementation File Map @@ -895,10 +895,20 @@ artifact_sha256: e3ab7f2aa31bdd57e54d6ade8d55a32d260e6c03b5cae66a2b4a0efe9ae09ff | `keep` | `ls/docs/releases/4.84.6.md` | | `keep` | `ls/docs/releases/4.89.10.json` | | `keep` | `ls/docs/releases/4.89.10.md` | +| `keep` | `ls/docs/releases/4.89.11.json` | +| `keep` | `ls/docs/releases/4.89.11.md` | +| `keep` | `ls/docs/releases/4.89.12.json` | +| `keep` | `ls/docs/releases/4.89.12.md` | | `keep` | `ls/docs/releases/4.89.4.json` | | `keep` | `ls/docs/releases/4.89.4.md` | | `keep` | `ls/docs/releases/4.89.8.json` | | `keep` | `ls/docs/releases/4.89.8.md` | +| `keep` | `ls/docs/releases/4.90.0.json` | +| `keep` | `ls/docs/releases/4.90.0.md` | +| `keep` | `ls/docs/releases/4.90.1.json` | +| `keep` | `ls/docs/releases/4.90.1.md` | +| `keep` | `ls/docs/releases/4.90.2.json` | +| `keep` | `ls/docs/releases/4.90.2.md` | | `keep` | `ls/docs/scrapling-cheat-sheet.md` | | `keep` | `ls/lib/data_paths.sh` | | `keep` | `ls/lib/deps.py` | @@ -1653,6 +1663,7 @@ artifact_sha256: e3ab7f2aa31bdd57e54d6ade8d55a32d260e6c03b5cae66a2b4a0efe9ae09ff | `keep` | `ls/tests/test_heartbeat_review.py` | | `keep` | `ls/tests/test_heartbeat_schedule.py` | | `keep` | `ls/tests/test_herdr_installation.py` | +| `keep` | `ls/tests/test_herdr_managed_pane.py` | | `keep` | `ls/tests/test_herdr_ops_cli.py` | | `keep` | `ls/tests/test_herdr_ops_config.py` | | `keep` | `ls/tests/test_herdr_ops_layout.py` | diff --git a/ls/docs/_generated/platform-adapters.md b/ls/docs/_generated/platform-adapters.md index 4cfa418a0..f3fe12e6d 100644 --- a/ls/docs/_generated/platform-adapters.md +++ b/ls/docs/_generated/platform-adapters.md @@ -1,10 +1,10 @@ --- localsetup_provenance: schema_version: 1 - source_provenance_hash: da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b + source_provenance_hash: fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f emitter: generate-docs -framework_version: 4.89.10 -source_commit: 0a1e7bed168b86d4a74801e95c413c12930ee7ee +framework_version: 4.90.2 +source_commit: 79c243ebda390673a13cada9b0b3e8e5054b4b99 artifact_sha256: 5ce4949227d75f75f72c4ce822e3c0e7958e57a16acf1fdc16a050a35da5d212 --- # Platform Adapters diff --git a/ls/docs/_generated/plugin-packs.json b/ls/docs/_generated/plugin-packs.json index 93979effe..2364f429e 100644 --- a/ls/docs/_generated/plugin-packs.json +++ b/ls/docs/_generated/plugin-packs.json @@ -398,14 +398,14 @@ "artifact_path": "ls/docs/_generated/plugin-packs.json", "artifact_sha256": "dcc147156b64d08ffc43885ad115adaebc834cb516fad268eefaef31649bd9e5", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "schema_version": 1, - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", - "source_dirty": false, - "source_provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_root_id": "4e46d3aab17f450f4229b417c716410a50584c4f96918aca19c4bd12b0e7c494", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", + "source_dirty": true, + "source_provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_root_id": "67b326143cdb933493f0017283b66af024262e6add9fc200a3021593ccb173a2", "source_tag": null, - "source_tree_sha": "8b7f6d1646f44125531a941231e03d631e1c5c80" + "source_tree_sha": "ae1d3b28f3503d50ac20227f6923fe7c4b6695d7" }, "schema_version": 1 } diff --git a/ls/docs/_generated/plugin-packs.md b/ls/docs/_generated/plugin-packs.md index 510fbed93..0f029601e 100644 --- a/ls/docs/_generated/plugin-packs.md +++ b/ls/docs/_generated/plugin-packs.md @@ -1,10 +1,10 @@ --- localsetup_provenance: schema_version: 1 - source_provenance_hash: da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b + source_provenance_hash: fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f emitter: generate-docs -framework_version: 4.89.10 -source_commit: 0a1e7bed168b86d4a74801e95c413c12930ee7ee +framework_version: 4.90.2 +source_commit: 79c243ebda390673a13cada9b0b3e8e5054b4b99 artifact_sha256: 0431f0f4b34af5061d3e66a48d1792b075f3e351d0f649963037c61f66252057 --- # Plugin Packs diff --git a/ls/docs/_generated/skill-packs.md b/ls/docs/_generated/skill-packs.md index e08ca320a..3409979c3 100644 --- a/ls/docs/_generated/skill-packs.md +++ b/ls/docs/_generated/skill-packs.md @@ -1,10 +1,10 @@ --- localsetup_provenance: schema_version: 1 - source_provenance_hash: da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b + source_provenance_hash: fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f emitter: generate-docs -framework_version: 4.89.10 -source_commit: 0a1e7bed168b86d4a74801e95c413c12930ee7ee +framework_version: 4.90.2 +source_commit: 79c243ebda390673a13cada9b0b3e8e5054b4b99 artifact_sha256: 07eff9baab9c71beb76cca0eb5b8db4deeab2bbf133c10cc4e6e4d821fc1fbbf --- # Skill And Workflow Packs diff --git a/ls/docs/_generated/skill-taxonomy.json b/ls/docs/_generated/skill-taxonomy.json index c29eee45e..2c85234cd 100644 --- a/ls/docs/_generated/skill-taxonomy.json +++ b/ls/docs/_generated/skill-taxonomy.json @@ -13,14 +13,14 @@ "artifact_path": "ls/docs/_generated/skill-taxonomy.json", "artifact_sha256": "3748c9f3ed45a4bb66d591327dbc435014bd7bc4c66dc1e04faff1f650a75a79", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "schema_version": 1, - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", - "source_dirty": false, - "source_provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_root_id": "4e46d3aab17f450f4229b417c716410a50584c4f96918aca19c4bd12b0e7c494", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", + "source_dirty": true, + "source_provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_root_id": "67b326143cdb933493f0017283b66af024262e6add9fc200a3021593ccb173a2", "source_tag": null, - "source_tree_sha": "8b7f6d1646f44125531a941231e03d631e1c5c80" + "source_tree_sha": "ae1d3b28f3503d50ac20227f6923fe7c4b6695d7" }, "schema_version": 1, "skills": [ diff --git a/ls/docs/_generated/skill_aliases.json b/ls/docs/_generated/skill_aliases.json index 3efe4f211..25e4ef238 100644 --- a/ls/docs/_generated/skill_aliases.json +++ b/ls/docs/_generated/skill_aliases.json @@ -286,13 +286,13 @@ "artifact_path": "ls/docs/_generated/skill_aliases.json", "artifact_sha256": "2bf995ed1b1425296a6b5576e4dcfe07508f1cb7db7672efb0a871cf32495291", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "schema_version": 1, - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", - "source_dirty": false, - "source_provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_root_id": "4e46d3aab17f450f4229b417c716410a50584c4f96918aca19c4bd12b0e7c494", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", + "source_dirty": true, + "source_provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_root_id": "67b326143cdb933493f0017283b66af024262e6add9fc200a3021593ccb173a2", "source_tag": null, - "source_tree_sha": "8b7f6d1646f44125531a941231e03d631e1c5c80" + "source_tree_sha": "ae1d3b28f3503d50ac20227f6923fe7c4b6695d7" } } diff --git a/ls/docs/_generated/workflow-catalog.json b/ls/docs/_generated/workflow-catalog.json index 35b22dd25..ee0907a5d 100644 --- a/ls/docs/_generated/workflow-catalog.json +++ b/ls/docs/_generated/workflow-catalog.json @@ -4,14 +4,14 @@ "artifact_path": "ls/docs/_generated/workflow-catalog.json", "artifact_sha256": "7acb2334f8508e596193cf01679eb5076c5d42213b0a8969f34366f028d4b165", "emitter": "generate-docs", - "framework_version": "4.89.10", + "framework_version": "4.90.2", "schema_version": 1, - "source_commit": "0a1e7bed168b86d4a74801e95c413c12930ee7ee", - "source_dirty": false, - "source_provenance_hash": "da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b", - "source_root_id": "4e46d3aab17f450f4229b417c716410a50584c4f96918aca19c4bd12b0e7c494", + "source_commit": "79c243ebda390673a13cada9b0b3e8e5054b4b99", + "source_dirty": true, + "source_provenance_hash": "fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f", + "source_root_id": "67b326143cdb933493f0017283b66af024262e6add9fc200a3021593ccb173a2", "source_tag": null, - "source_tree_sha": "8b7f6d1646f44125531a941231e03d631e1c5c80" + "source_tree_sha": "ae1d3b28f3503d50ac20227f6923fe7c4b6695d7" }, "workflows": [ { diff --git a/ls/docs/bootstrap-packs/INDEX.md b/ls/docs/bootstrap-packs/INDEX.md index d32491cac..14b2a14ec 100644 --- a/ls/docs/bootstrap-packs/INDEX.md +++ b/ls/docs/bootstrap-packs/INDEX.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/bootstrap-packs/codex-agent-team/AUDIT_PROMPT.md b/ls/docs/bootstrap-packs/codex-agent-team/AUDIT_PROMPT.md index c0a899255..9f1567218 100644 --- a/ls/docs/bootstrap-packs/codex-agent-team/AUDIT_PROMPT.md +++ b/ls/docs/bootstrap-packs/codex-agent-team/AUDIT_PROMPT.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/bootstrap-packs/codex-agent-team/README.md b/ls/docs/bootstrap-packs/codex-agent-team/README.md index 976d17384..2f51ad47a 100644 --- a/ls/docs/bootstrap-packs/codex-agent-team/README.md +++ b/ls/docs/bootstrap-packs/codex-agent-team/README.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/bootstrap-packs/opencode-agent-team/AUDIT_PROMPT.md b/ls/docs/bootstrap-packs/opencode-agent-team/AUDIT_PROMPT.md index 742d9a2b2..4e5885707 100644 --- a/ls/docs/bootstrap-packs/opencode-agent-team/AUDIT_PROMPT.md +++ b/ls/docs/bootstrap-packs/opencode-agent-team/AUDIT_PROMPT.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/bootstrap-packs/opencode-agent-team/MODEL_MAP.md b/ls/docs/bootstrap-packs/opencode-agent-team/MODEL_MAP.md index d9560290e..aec53c712 100644 --- a/ls/docs/bootstrap-packs/opencode-agent-team/MODEL_MAP.md +++ b/ls/docs/bootstrap-packs/opencode-agent-team/MODEL_MAP.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/bootstrap-packs/opencode-agent-team/README.md b/ls/docs/bootstrap-packs/opencode-agent-team/README.md index d72849e82..7a0971c64 100644 --- a/ls/docs/bootstrap-packs/opencode-agent-team/README.md +++ b/ls/docs/bootstrap-packs/opencode-agent-team/README.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/migration/overview.md b/ls/docs/migration/overview.md index c50f0b4dc..0b94063ab 100644 --- a/ls/docs/migration/overview.md +++ b/ls/docs/migration/overview.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-localsetup-framework-changes --- diff --git a/ls/docs/migration/skill-alias-map.md b/ls/docs/migration/skill-alias-map.md index 2914c201a..d55afbf70 100644 --- a/ls/docs/migration/skill-alias-map.md +++ b/ls/docs/migration/skill-alias-map.md @@ -1,14 +1,14 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_package: generate-docs localsetup_provenance: schema_version: 1 - source_provenance_hash: da4f94cc5311f24042a68efa023b1a7b89d4b43668864ee8ce7e600cd3ddde9b + source_provenance_hash: fda7c779937904c4b96636d318137b2c6b79aec42da63a5dc8f90c0b893e637f emitter: generate-docs -framework_version: 4.89.10 -source_commit: 0a1e7bed168b86d4a74801e95c413c12930ee7ee -artifact_sha256: f137d98acbf226ab31d8deefed1083ec684522db75b578cc2b4039acb3cad61b +framework_version: 4.90.2 +source_commit: 79c243ebda390673a13cada9b0b3e8e5054b4b99 +artifact_sha256: c27fa1f769a3729cac1718a35ef5870679055c1b3b0a65df0403b7905dee65e0 --- # Skill Alias Map diff --git a/ls/docs/ops/herdr-ops.md b/ls/docs/ops/herdr-ops.md index 22e69d05f..ecafa674e 100644 --- a/ls/docs/ops/herdr-ops.md +++ b/ls/docs/ops/herdr-ops.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-herdr-task-routing --- @@ -50,7 +50,8 @@ tmux translate native primitives while the shared owner makes creation, reuse, layout and transition decisions. Caller-supplied storage retains existing private state ownership. Operations panes split directly below the actual caller without focus and occupy one third of the caller's original height; -normal operations never create a separate operations tab. +the managed owner records the child fraction while Herdr's native split ratio +sizes the first (parent) pane. Normal operations never create a separate tab. An observed shell process or prompt text does not prove empty input. The managed Bash handler verifies the primary prompt, empty Readline buffer, @@ -65,6 +66,37 @@ read-only reconciliation of the original operation. Only a proven idle owned attachment can detach or log out automatically. Verify the original local shell before changing directory or target. +## Reserved creation retirement + +`ManagedPaneService.retire_creation(...)` is a Herdr-only recovery for an exact +reserved creation that never established a controlled shell. It requires the +original request and creation token, expected native pane and stable terminal +identifiers, a caller-selected retirement ID, and explicit acceptance of +unsubmitted input and shell-local state loss. It is not ordinary cleanup or a +way to adopt an unqualified shell. + +The shared owner verifies the current owner, controller and native identities, +checks current Bash PID/start generation, TTY, directory and process occupancy, +and refuses active operations, writers or descendant work. Missing historical +controlled-shell evidence is not replaced with a claim that a handshake once +completed. Missing current process or endpoint evidence refuses recovery. + +Before native close, the owner archives exact original owner and reservation +bytes and durably records a dispatch fence. Caller-owned stores retain their +existing locations: retirement additionally requires bounded owner-private +`snapshot(key)` bytes, durable `put(key, mapping)` and a nonblocking +cross-process `lock(key)`. It does not migrate records into another store. +Existing API-v1 callers that do not request retirement retain their normal +lifecycle; clients must detect the optional methods before using this recovery. + +`reconcile_creation_retirement(request, retirement_id)` performs native +observation only. A dispatched close is never repeated, including after a lost +response. Completion requires conclusive absence of both original native +identifiers, not merely a missing label or one incomplete inventory result. +Historical records remain available, and only completed retirement proof +permits a later ordinary allocation. Neither method automatically creates a +replacement or runs a workload. Unsupported backends refuse before mutation. + ## Layout and command lifecycle Temporary work areas are owned panes below the caller, using a 30% split by diff --git a/ls/docs/ops/tmux-ops-managed.md b/ls/docs/ops/tmux-ops-managed.md index e2bb6ce20..8b57a669d 100644 --- a/ls/docs/ops/tmux-ops-managed.md +++ b/ls/docs/ops/tmux-ops-managed.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_package: ls-wf-sudo-tmux-session --- @@ -29,13 +29,41 @@ context when needed. The visible name is `-sudo` at the root and one third of its parent's original height without changing focus. The parent, endpoint, target and live controller distinguish concurrent identical names. -The shared `ls.core.terminal_ops` owner serves Herdr and tmux. Its controlled Bash -handler checks primary-prompt and empty-input state, machine/TTY/shell generation -and writer ownership before executing staged argv. The native adapter sends only -a reserved trigger. It never appends command text, presses Enter or clears input. -Busy/unknown panes stop dispatch; a lost response never causes replay. Automatic -dispatch currently requires Linux process/TTY evidence and controlled Bash; an -unsupported environment returns an unavailable result rather than typing into it. +The shared `ls.core.terminal_ops` owner serves Herdr and tmux. For Herdr, pane +creation uses a private per-binding startup-HOME loader. It restores the +qualified server's original `HOME` before loading the original user login +profile and the controlled Bash bootstrap. The system login profile runs first +with the private startup HOME, so its HOME-relative completion or integration +settings may differ from an ordinary login shell. No user profile is modified. +The server's nonempty `_SUDO_KEY` variables are set empty in the pane environment; +their source values are never copied into the managed shell. Helper subprocesses +use an environment copy with all `_SUDO_KEY` variables removed. + +The controlled Bash handler checks primary-prompt and empty-input state, +machine/TTY/shell generation and writer ownership before executing staged argv. +The native adapter sends only a reserved trigger. It never appends command text, +presses Enter or clears input. A busy shell may retain its exact original +binding for read-only identity observation, but that is not a ready result and +cannot pass the transition dispatch gate. Busy/unknown panes stop dispatch; a +lost response never causes replay. Automatic dispatch currently requires Linux +process/TTY evidence and controlled Bash; an unsupported environment returns an +unavailable result rather than typing into it. + +### Verified missing-generation recovery + +A managed Herdr pane may be replaced after a restart only when the shared +lifecycle service verifies the exact stored request, a durably completed prior +operation receipt with no writer lease, and Herdr's complete endpoint inventory +proves that both the old pane ID and stable terminal ID are absent. It also +rechecks the exact managed owner, parent, workspace and original working +directory, and proves the prior controlled Bash generation is gone. Before +reserving the replacement, it archives the exact previous owner-record bytes; +the new pane uses the same controlled Bash startup path. + +Only a confirmed no-effect creation failure can retry that archived generation. +An uncertain native result remains fenced for read-only reconciliation, never +for replay or duplicate creation. A missing tmux pane does not use this Herdr-only +recovery path, and a matching label alone is never authority to adopt a pane. A managed-pane run returns a durable `status_handle`. Keep it to observe the receipt after changing directory or restarting the controller: diff --git a/ls/docs/ops/tmux-ops-remote.md b/ls/docs/ops/tmux-ops-remote.md index 5349bcff3..aab392e36 100644 --- a/ls/docs/ops/tmux-ops-remote.md +++ b/ls/docs/ops/tmux-ops-remote.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_package: ls-wf-sudo-tmux-session --- diff --git a/ls/docs/releases/4.89.11.json b/ls/docs/releases/4.89.11.json new file mode 100644 index 000000000..eff0a4ca8 --- /dev/null +++ b/ls/docs/releases/4.89.11.json @@ -0,0 +1,55 @@ +{ + "schema_version": 1, + "version": "4.89.11", + "source_commit": "de1f22e402ec4d2994fd9f366d299c967315d44d", + "baseline_tag": "v4.89.10", + "summary": "LocalSetup 4.89.11 updates the reviewed CI bootstrap action and Python dependency graph, with matching hash-pinned SDK and storage exports.", + "highlights": [ + { + "text": "CI pins setup-uv 10.3.3 to its reviewed immutable commit. The selected uv tool version remains 0.11.21; updating the action does not implicitly update the installed tool.", + "evidence": [ + ".github/workflows/pr-validation.yml", + ".github/workflows/docs-sync.yml", + ".github/workflows/publish.yml", + "ls/docs/REPO_MAINTENANCE.md" + ] + }, + { + "text": "The locked Python graph updates pyasn1 to 0.6.5, genai-prices to 0.2.0, logfire-api to 5.4.0, OpenAI to 3.41.0, opentelemetry-api to 1.45.2, Pydantic to 2.14.2, pytest to 9.2.0 and optional Boto3 to 1.43.122. The corresponding transitives include botocore 1.43.123 and pydantic-core 2.50.2.", + "evidence": [ + "pyproject.toml", + "uv.lock" + ] + }, + { + "text": "Canonical generators align the SDK runtime lock and integrity receipt, both storage-skill requirement snapshots and the reviewed Boto3 export guard with the accepted graph. Runtime, build and optional storage dependency scopes remain separate.", + "evidence": [ + "ls/config/sdk-runtime.lock", + "ls/config/sdk-dependency-receipt.json", + "ls/config/sdk-build.lock", + "ls/core/s3_sdk/exports.py", + "ls/skills/ls-backblaze-b2-storage/requirements-s3-sdk.txt", + "ls/skills/ls-garage-s3-admin/requirements-s3-sdk.txt" + ] + }, + { + "text": "Native viewer retirement and private authentication-dialog checks retain bounded failure diagnostics while preserving clean-exit, delivery, cleanup and privacy requirements. Runtime behavior and test deadlines are unchanged; dialog diagnostics exclude form values, renderer arguments and channel paths.", + "evidence": [ + "ls/tests/test_shell_family.py", + "ls/tests/test_sudo_dialog_native.py", + "ls/docs/REPO_MAINTENANCE.md" + ] + } + ], + "compatibility": [ + "This maintenance update does not change framework command or file formats. Synchronize source environments from the lockfile rather than mixing independently selected package versions.", + "The CI Python and uv tool selections are unchanged. Dependency-advisory lookups are point-in-time evidence, not a guarantee that a package has no vulnerabilities." + ], + "update": [ + "Use the normal verified installation or source-refresh procedure. Source checkouts should run uv sync --locked --all-groups before using the updated framework environment." + ], + "verification": [ + "The dependency implementation passed guarded local validation with 5,993 tests passed and three skipped. Focused dependency-export checks, offline Pydantic/OpenAI/S3 consumer smoke checks, the native viewer qualification case and all three isolated native authentication-dialog outcomes also passed locally. These local results do not replace successful checks for the exact final candidate commit.", + "Verify any framework release archive using its .sha256 checksum and .cdx.json SBOM sidecars." + ] +} diff --git a/ls/docs/releases/4.89.11.md b/ls/docs/releases/4.89.11.md new file mode 100644 index 000000000..9768ca398 --- /dev/null +++ b/ls/docs/releases/4.89.11.md @@ -0,0 +1,42 @@ +--- +status: ACTIVE +version: 4.89 +owner_skill: ls-github-release-publishing +--- + +# LocalSetup 4.89.11 + +LocalSetup 4.89.11 updates the reviewed CI bootstrap action and Python dependency graph, with matching hash-pinned SDK and storage exports. + +## Highlights + +- CI pins setup-uv 10.3.3 to its reviewed immutable commit. The selected uv tool version remains 0.11.21; updating the action does not implicitly update the installed tool. +- The locked Python graph updates pyasn1 to 0.6.5, genai-prices to 0.2.0, logfire-api to 5.4.0, OpenAI to 3.41.0, opentelemetry-api to 1.45.2, Pydantic to 2.14.2, pytest to 9.2.0 and optional Boto3 to 1.43.122. The corresponding transitives include botocore 1.43.123 and pydantic-core 2.50.2. +- Canonical generators align the SDK runtime lock and integrity receipt, both storage-skill requirement snapshots and the reviewed Boto3 export guard with the accepted graph. Runtime, build and optional storage dependency scopes remain separate. +- Native viewer retirement and private authentication-dialog checks retain bounded failure diagnostics while preserving clean-exit, delivery, cleanup and privacy requirements. Runtime behavior and test deadlines are unchanged; dialog diagnostics exclude form values, renderer arguments and channel paths. + +After publication, see the [release and downloads](https://github.com/CruxExperts/localsetup/releases/tag/v4.89.11) for release assets. + +## Compatibility + +This maintenance update does not change framework command or file formats. Synchronize source environments from the lockfile rather than mixing independently selected package versions. + +The CI Python and uv tool selections are unchanged. Dependency-advisory lookups are point-in-time evidence, not a guarantee that a package has no vulnerabilities. + +## Update + +Use the normal verified installation or source-refresh procedure. Source checkouts should run uv sync --locked --all-groups before using the updated framework environment. + +For installation and source refresh, read the [quickstart update instructions](../QUICKSTART.md#update). Adapter changes follow the [adapter ownership guide](../ADAPTER_OWNERSHIP.md). + +## Verify the download + +The dependency implementation passed guarded local validation with 5,993 tests passed and three skipped. Focused dependency-export checks, offline Pydantic/OpenAI/S3 consumer smoke checks, the native viewer qualification case and all three isolated native authentication-dialog outcomes also passed locally. These local results do not replace successful checks for the exact final candidate commit. + +Verify any framework release archive using its .sha256 checksum and .cdx.json SBOM sidecars. + +Download the framework archive with both its `.sha256` checksum and `.cdx.json` SBOM sidecars. Keep all three files in the same directory before running `verify-release`. + +```bash +uv run --locked python ls/tools/localsetup.py --source-root . verify-release /path/to/localsetup-v4.89.11.tar.gz +``` diff --git a/ls/docs/releases/4.89.12.json b/ls/docs/releases/4.89.12.json new file mode 100644 index 000000000..4e9d5cc7d --- /dev/null +++ b/ls/docs/releases/4.89.12.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "version": "4.89.12", + "source_commit": "1033ca0993f2ab8e6e5909e763de180e33d6959c", + "baseline_tag": "v4.89.10", + "summary": "LocalSetup 4.89.12 updates the reviewed CI bootstrap action and Python dependency graph, aligns hash-pinned SDK and storage exports, and refreshes the reviewed routing catalog without changing client model configuration.", + "highlights": [ + { + "text": "CI pins setup-uv 10.3.3 to its reviewed immutable commit. The selected uv tool version remains 0.11.21; updating the action does not implicitly update the installed tool.", + "evidence": [".github/workflows/pr-validation.yml", ".github/workflows/docs-sync.yml", ".github/workflows/publish.yml", "ls/docs/REPO_MAINTENANCE.md"] + }, + { + "text": "The locked Python graph updates pyasn1 to 0.6.5, genai-prices to 0.2.0, logfire-api to 5.4.0, OpenAI to 3.41.0, opentelemetry-api to 1.45.2, Pydantic to 2.14.2, pytest to 9.2.0 and optional Boto3 to 1.43.122. The corresponding transitives include botocore 1.43.123 and pydantic-core 2.50.2.", + "evidence": ["pyproject.toml", "uv.lock"] + }, + { + "text": "Canonical generators align the SDK runtime lock and integrity receipt, both storage-skill requirement snapshots and the reviewed Boto3 export guard with the accepted graph. Runtime, build and optional storage dependency scopes remain separate.", + "evidence": ["ls/config/sdk-runtime.lock", "ls/config/sdk-dependency-receipt.json", "ls/config/sdk-build.lock", "ls/core/s3_sdk/exports.py", "ls/skills/ls-backblaze-b2-storage/requirements-s3-sdk.txt", "ls/skills/ls-garage-s3-admin/requirements-s3-sdk.txt"] + }, + { + "text": "The existing routing catalog has been re-reviewed against all seven official sources, with synchronized evidence dates and byte-derived integrity anchors. Candidate policy and client model configuration are unchanged; authentic resources remain accepted through their expiry date and rejected afterward.", + "evidence": ["ls/skills/ls-agent-capability-routing/resources/model-capability-matrix/snapshot.json", "ls/skills/ls-agent-capability-routing/resources/model-capability-matrix/manifest.json", "ls/skills/ls-agent-capability-routing/scripts/agent_routing.py", "ls/tests/test_agent_routing.py"] + }, + { + "text": "Native viewer retirement, nested theme-preview and private authentication-dialog checks retain bounded failure diagnostics while preserving clean-exit, delivery, cleanup and privacy requirements. A single terminal reader preserves final-detach evidence, and separate failure fields avoid shortened assertion output. Private popup identity observations are Boolean and exclude form values, renderer arguments and channel paths; theme-preview diagnostics redact fixture paths and record only receipt presence. Concurrent retained-status checks handle only bounded, pre-dispatch busy responses with no effect while preserving retention and geometry assertions. Runtime behavior and existing UI deadlines are unchanged; diagnostics do not establish a fix for an unproven presentation failure.", + "evidence": ["ls/tests/test_shell_family.py", "ls/tests/test_shell_ui_refresh.py", "ls/tests/conftest.py", "ls/tests/test_terminal_dialog_native.py", "ls/tests/test_sudo_dialog_native.py", "ls/tests/test_openpgp_native_dialog.py", "ls/docs/REPO_MAINTENANCE.md"] + } + ], + "compatibility": [ + "This maintenance update does not change framework command or file formats. Synchronize source environments from the lockfile rather than mixing independently selected package versions.", + "The CI Python and uv tool selections are unchanged. Dependency-advisory lookups and catalog reviews are point-in-time evidence, not a guarantee of package safety or per-account model access.", + "Routing catalog expiry remains enforced. Renewing its review requires checking the official source evidence; it does not authorize a model migration or weaken integrity checks." + ], + "update": [ + "Use the normal verified installation or source-refresh procedure. Source checkouts should run uv sync --locked --all-groups before using the updated framework environment." + ], + "verification": [ + "The dependency implementation passed guarded local validation with 5,993 tests passed and three skipped. Focused dependency-export and offline Pydantic/OpenAI/S3 consumer smoke checks passed. After the catalog review and diagnostic updates, all 76 focused routing and native-dialog tests passed, and the real selector command returned a reviewed lane. The isolated native refresh and concurrent-family scenarios also passed with their visible-control, retention and cleanup assertions intact. Updated capture and identity observations passed the native family scenario and all six focused terminal, OpenPGP and fake-sudo dialog cases. A deliberately stopped isolated fixture server remained a failure and exposed its native transition text. These local results do not replace successful checks for the exact final candidate commit.", + "Verify any framework release archive using its .sha256 checksum and .cdx.json SBOM sidecars." + ] +} diff --git a/ls/docs/releases/4.89.12.md b/ls/docs/releases/4.89.12.md new file mode 100644 index 000000000..9366c680a --- /dev/null +++ b/ls/docs/releases/4.89.12.md @@ -0,0 +1,45 @@ +--- +status: ACTIVE +version: 4.89 +owner_skill: ls-github-release-publishing +--- + +# LocalSetup 4.89.12 + +LocalSetup 4.89.12 updates the reviewed CI bootstrap action and Python dependency graph, aligns hash-pinned SDK and storage exports, and refreshes the reviewed routing catalog without changing client model configuration. + +## Highlights + +- CI pins setup-uv 10.3.3 to its reviewed immutable commit. The selected uv tool version remains 0.11.21; updating the action does not implicitly update the installed tool. +- The locked Python graph updates pyasn1 to 0.6.5, genai-prices to 0.2.0, logfire-api to 5.4.0, OpenAI to 3.41.0, opentelemetry-api to 1.45.2, Pydantic to 2.14.2, pytest to 9.2.0 and optional Boto3 to 1.43.122. The corresponding transitives include botocore 1.43.123 and pydantic-core 2.50.2. +- Canonical generators align the SDK runtime lock and integrity receipt, both storage-skill requirement snapshots and the reviewed Boto3 export guard with the accepted graph. Runtime, build and optional storage dependency scopes remain separate. +- The existing routing catalog has been re-reviewed against all seven official sources, with synchronized evidence dates and byte-derived integrity anchors. Candidate policy and client model configuration are unchanged; authentic resources remain accepted through their expiry date and rejected afterward. +- Native viewer retirement, nested theme-preview and private authentication-dialog checks retain bounded failure diagnostics while preserving clean-exit, delivery, cleanup and privacy requirements. A single terminal reader preserves final-detach evidence, and separate failure fields avoid shortened assertion output. Private popup identity observations are Boolean and exclude form values, renderer arguments and channel paths; theme-preview diagnostics redact fixture paths and record only receipt presence. Concurrent retained-status checks handle only bounded, pre-dispatch busy responses with no effect while preserving retention and geometry assertions. Runtime behavior and existing UI deadlines are unchanged; diagnostics do not establish a fix for an unproven presentation failure. + +After publication, see the [release and downloads](https://github.com/CruxExperts/localsetup/releases/tag/v4.89.12) for release assets. + +## Compatibility + +This maintenance update does not change framework command or file formats. Synchronize source environments from the lockfile rather than mixing independently selected package versions. + +The CI Python and uv tool selections are unchanged. Dependency-advisory lookups and catalog reviews are point-in-time evidence, not a guarantee of package safety or per-account model access. + +Routing catalog expiry remains enforced. Renewing its review requires checking the official source evidence; it does not authorize a model migration or weaken integrity checks. + +## Update + +Use the normal verified installation or source-refresh procedure. Source checkouts should run uv sync --locked --all-groups before using the updated framework environment. + +For installation and source refresh, read the [quickstart update instructions](../QUICKSTART.md#update). Adapter changes follow the [adapter ownership guide](../ADAPTER_OWNERSHIP.md). + +## Verify the download + +The dependency implementation passed guarded local validation with 5,993 tests passed and three skipped. Focused dependency-export and offline Pydantic/OpenAI/S3 consumer smoke checks passed. After the catalog review and diagnostic updates, all 76 focused routing and native-dialog tests passed, and the real selector command returned a reviewed lane. The isolated native refresh and concurrent-family scenarios also passed with their visible-control, retention and cleanup assertions intact. Updated capture and identity observations passed the native family scenario and all six focused terminal, OpenPGP and fake-sudo dialog cases. A deliberately stopped isolated fixture server remained a failure and exposed its native transition text. These local results do not replace successful checks for the exact final candidate commit. + +Verify any framework release archive using its .sha256 checksum and .cdx.json SBOM sidecars. + +Download the framework archive with both its `.sha256` checksum and `.cdx.json` SBOM sidecars. Keep all three files in the same directory before running `verify-release`. + +```bash +uv run --locked python ls/tools/localsetup.py --source-root . verify-release /path/to/localsetup-v4.89.12.tar.gz +``` diff --git a/ls/docs/releases/4.90.0.json b/ls/docs/releases/4.90.0.json new file mode 100644 index 000000000..c3efcce43 --- /dev/null +++ b/ls/docs/releases/4.90.0.json @@ -0,0 +1,45 @@ +{ + "schema_version": 1, + "version": "4.90.0", + "source_commit": "ed0648442b0b0137e5b16af6d57ec86059191f73", + "baseline_tag": "v4.89.10", + "summary": "LocalSetup 4.90.0 adds explicit, journal-preserving SDK recovery for an owned Herdr creation that never established its controlled shell. It also includes the reviewed startup, generation, pane-sizing and maintenance changes accumulated since the last published release.", + "highlights": [ + { + "text": "Retire only an explicitly selected uninitialized Herdr creation: the shared owner requires the original request and creation token, exact pane and stable terminal identities, discard acknowledgement, and fresh evidence that no operation, writer or descendant work occupies the current shell.", + "evidence": ["ls/core/terminal_ops/pane.py", "ls/core/terminal_ops/backends.py", "ls/docs/ops/herdr-ops.md"] + }, + { + "text": "Preserve original owner and reservation bytes before closing, durably fence the close before dispatch, and reconcile a lost response without repeating it. Both original native identifiers must be conclusively absent before a later ordinary allocation is permitted; recovery never automatically creates a replacement or runs a workload.", + "evidence": ["ls/core/herdr_ops/layout.py", "ls/core/terminal_ops/pane.py", "ls/tests/test_herdr_managed_pane.py"] + }, + { + "text": "Keep caller-owned storage in place. Optional retirement requires an exact bounded snapshot, durable writes and a nonblocking cross-process lock. Raw custom-store lock contention is reported as pane_busy before dispatch, while operation-body and cleanup failures are not reclassified as harmless lock refusal.", + "evidence": ["ls/core/terminal_ops/pane.py", "ls/tests/test_herdr_managed_pane.py"] + }, + { + "text": "Preserve the controlled Herdr startup loader across login-profile changes, recover a missing prior generation only from completed-operation and native-absence proof, and translate the child fraction to Herdr's parent-oriented split ratio.", + "evidence": ["17e12628e88d17717a685b96c99a6edb5f65941b", "169ac81aa0f2ba10a0bf208bc00a9d228742b665", "3cb87c07bf1a40bc430f9ad721dd31438aedbc12"] + }, + { + "text": "Carry forward the reviewed dependency graph, hash-pinned SDK and storage exports, reviewed routing-catalog evidence and bounded native qualification diagnostics from the unpublished maintenance line. Runtime, build and optional storage scopes remain separate; client model configuration is unchanged.", + "evidence": ["ls/docs/releases/4.89.11.json", "ls/docs/releases/4.89.12.json", "ls/config/sdk-dependency-receipt.json"] + } + ], + "compatibility": [ + "The optional recovery methods extend managed-terminal API version 1. Existing callers that do not request retirement retain their normal lifecycle, and older runtimes must refuse the missing optional capability rather than substitute generic cleanup.", + "The new retirement process proof requires Linux /proc visibility and provable native server ancestry. Missing platform, identity or occupancy evidence refuses recovery; normal Herdr and tmux operation support is unchanged.", + "Keep the original retirement ID and durable evidence until reconciliation and any subsequent allocation complete. Do not delete journals, replay a close or downgrade the recovery owner to bypass an unknown outcome.", + "Dependency and routing-catalog reviews are point-in-time evidence, not a package-safety or per-account model-access guarantee. This release does not authorize changes to configured models, schedules or host services." + ], + "update": [ + "Use the existing verified installation or source-refresh procedure, and verify the released archive and its checksum/SBOM sidecars. Managed consumers retain their last qualified pin until the new release passes their compatibility checks.", + "Retirement is an explicitly authorized discard of one exact owned local shell, including its unsubmitted input and shell-local state. Ordinary cleanup and command dispatch do not grant that authority." + ], + "verification": [ + "The new shared recovery and acquisition-boundary changes passed independent source review and 34 focused lifecycle and real-PTY tests. The PTY fixtures reported three fork-related deprecation warnings under the threaded test runner.", + "Twenty-five FleetOps adapter regressions passed against the corrected source. A separate registered-CLI smoke used the real FleetOps store and shared service to verify exact original archives, one close, idempotency, lost-response reconciliation and structured held-lock refusal. Native I/O and process identity in that integration smoke were fixture simulations, not live endpoint qualification.", + "Release activation still requires the full suite and required checks for the exact tagged candidate. These local checks do not replace that gate or the consumer's installed-runtime and live-operation qualification.", + "Verify the exact framework archive with its .sha256 checksum and .cdx.json SBOM sidecars before adopting it." + ] +} diff --git a/ls/docs/releases/4.90.0.md b/ls/docs/releases/4.90.0.md new file mode 100644 index 000000000..a58511700 --- /dev/null +++ b/ls/docs/releases/4.90.0.md @@ -0,0 +1,53 @@ +--- +status: ACTIVE +version: 4.90 +owner_skill: ls-github-release-publishing +--- + +# LocalSetup 4.90.0 + +LocalSetup 4.90.0 adds explicit, journal-preserving SDK recovery for an owned Herdr creation that never established its controlled shell. It also includes the reviewed startup, generation, pane-sizing and maintenance changes accumulated since the last published release. + +## Highlights + +- Retire only an explicitly selected uninitialized Herdr creation: the shared owner requires the original request and creation token, exact pane and stable terminal identities, discard acknowledgement, and fresh evidence that no operation, writer or descendant work occupies the current shell. +- Preserve original owner and reservation bytes before closing, durably fence the close before dispatch, and reconcile a lost response without repeating it. Both original native identifiers must be conclusively absent before a later ordinary allocation is permitted; recovery never automatically creates a replacement or runs a workload. +- Keep caller-owned storage in place. Optional retirement requires an exact bounded snapshot, durable writes and a nonblocking cross-process lock. Raw custom-store lock contention is reported as pane_busy before dispatch, while operation-body and cleanup failures are not reclassified as harmless lock refusal. +- Preserve the controlled Herdr startup loader across login-profile changes, recover a missing prior generation only from completed-operation and native-absence proof, and translate the child fraction to Herdr's parent-oriented split ratio. +- Carry forward the reviewed dependency graph, hash-pinned SDK and storage exports, reviewed routing-catalog evidence and bounded native qualification diagnostics from the unpublished maintenance line. Runtime, build and optional storage scopes remain separate; client model configuration is unchanged. + +After publication, see the [release and downloads](https://github.com/CruxExperts/localsetup/releases/tag/v4.90.0) for release assets. + +## Compatibility + +The optional recovery methods extend managed-terminal API version 1. Existing callers that do not request retirement retain their normal lifecycle, and older runtimes must refuse the missing optional capability rather than substitute generic cleanup. + +The new retirement process proof requires Linux /proc visibility and provable native server ancestry. Missing platform, identity or occupancy evidence refuses recovery; normal Herdr and tmux operation support is unchanged. + +Keep the original retirement ID and durable evidence until reconciliation and any subsequent allocation complete. Do not delete journals, replay a close or downgrade the recovery owner to bypass an unknown outcome. + +Dependency and routing-catalog reviews are point-in-time evidence, not a package-safety or per-account model-access guarantee. This release does not authorize changes to configured models, schedules or host services. + +## Update + +Use the existing verified installation or source-refresh procedure, and verify the released archive and its checksum/SBOM sidecars. Managed consumers retain their last qualified pin until the new release passes their compatibility checks. + +Retirement is an explicitly authorized discard of one exact owned local shell, including its unsubmitted input and shell-local state. Ordinary cleanup and command dispatch do not grant that authority. + +For installation and source refresh, read the [quickstart update instructions](../QUICKSTART.md#update). Adapter changes follow the [adapter ownership guide](../ADAPTER_OWNERSHIP.md). + +## Verify the download + +The new shared recovery and acquisition-boundary changes passed independent source review and 34 focused lifecycle and real-PTY tests. The PTY fixtures reported three fork-related deprecation warnings under the threaded test runner. + +Twenty-five FleetOps adapter regressions passed against the corrected source. A separate registered-CLI smoke used the real FleetOps store and shared service to verify exact original archives, one close, idempotency, lost-response reconciliation and structured held-lock refusal. Native I/O and process identity in that integration smoke were fixture simulations, not live endpoint qualification. + +Release activation still requires the full suite and required checks for the exact tagged candidate. These local checks do not replace that gate or the consumer's installed-runtime and live-operation qualification. + +Verify the exact framework archive with its .sha256 checksum and .cdx.json SBOM sidecars before adopting it. + +Download the framework archive with both its `.sha256` checksum and `.cdx.json` SBOM sidecars. Keep all three files in the same directory before running `verify-release`. + +```bash +uv run --locked python ls/tools/localsetup.py --source-root . verify-release /path/to/localsetup-v4.90.0.tar.gz +``` diff --git a/ls/docs/releases/4.90.1.json b/ls/docs/releases/4.90.1.json new file mode 100644 index 000000000..dc6c9ea5f --- /dev/null +++ b/ls/docs/releases/4.90.1.json @@ -0,0 +1,51 @@ +{ + "schema_version": 1, + "version": "4.90.1", + "source_commit": "d3e95de4392a27c49556280d07379ac8831dd61c", + "baseline_tag": "v4.89.10", + "summary": "LocalSetup 4.90.1 includes the reviewed Herdr retirement and terminal startup changes since the last published release, together with a correction to SDK wheel helper permissions and the reviewed dependency maintenance.", + "highlights": [ + { + "text": "Retire only an explicitly selected uninitialized Herdr creation: the shared owner requires the original request and creation token, exact pane and stable terminal identities, discard acknowledgement, and fresh evidence that no operation, writer or descendant work occupies the current shell.", + "evidence": ["ls/core/terminal_ops/pane.py", "ls/core/terminal_ops/backends.py", "ls/docs/ops/herdr-ops.md"] + }, + { + "text": "Preserve original owner and reservation bytes before closing, durably fence the close before dispatch, and reconcile a lost response without repeating it. Both original native identifiers must be conclusively absent before a later ordinary allocation for that same binding is permitted; recovery never automatically creates a replacement or runs a workload.", + "evidence": ["ls/core/herdr_ops/layout.py", "ls/core/terminal_ops/pane.py", "ls/tests/test_herdr_managed_pane.py"] + }, + { + "text": "Keep caller-owned storage in place. Optional retirement requires an exact bounded snapshot, durable writes and a nonblocking cross-process lock. Raw custom-store lock contention is reported as pane_busy before dispatch, while operation-body and cleanup failures are not reclassified as harmless lock refusal.", + "evidence": ["ls/core/terminal_ops/pane.py", "ls/tests/test_herdr_managed_pane.py"] + }, + { + "text": "Preserve the controlled Herdr startup loader across login-profile changes, recover a missing prior generation only from completed-operation and native-absence proof, and translate the child fraction to Herdr's parent-oriented split ratio.", + "evidence": ["17e12628e88d17717a685b96c99a6edb5f65941b", "169ac81aa0f2ba10a0bf208bc00a9d228742b665", "3cb87c07bf1a40bc430f9ad721dd31438aedbc12"] + }, + { + "text": "Carry forward the reviewed dependency graph, hash-pinned SDK and storage exports, reviewed routing-catalog evidence and bounded native qualification diagnostics from the unpublished maintenance line. Runtime, build and optional storage scopes remain separate; client model configuration is unchanged.", + "evidence": ["ls/docs/releases/4.89.11.json", "ls/docs/releases/4.89.12.json", "ls/config/sdk-dependency-receipt.json"] + }, + { + "text": "Correct SDK wheel helper permissions: BuildSDK sets the packaged sudo_askpass helper to mode 0755 without changing its bytes and rejects symlinked helper destinations. Installed runtime protection still rejects a non-executable or group- or world-writable helper.", + "evidence": ["ls/core/sdk_payload/build.py", "ls/tests/test_sdk_build.py", "ls/docs/SDK_FORK.md"] + } + ], + "compatibility": [ + "The optional recovery methods extend managed-terminal API version 1. Existing callers that do not request retirement retain their normal lifecycle, and older runtimes must refuse the missing optional capability rather than substitute generic cleanup.", + "The new retirement process proof requires Linux /proc visibility and provable native server ancestry. Missing platform, identity or occupancy evidence refuses recovery; normal Herdr and tmux operation support is unchanged.", + "Keep the original retirement ID and durable evidence until reconciliation and any subsequent allocation complete. Do not delete journals, replay a close or downgrade the recovery owner to bypass an unknown outcome.", + "Safe wheel permission metadata does not make an arbitrary installer secure: installation permissions also depend on the installer and its environment. The installed qualification used the existing protected contract: a private cache, filtered environment, --no-config, --offline, --link-mode copy and umask 077.", + "Dependency and routing-catalog reviews are point-in-time evidence, not a package-safety or per-account model-access guarantee. This release does not authorize changes to configured models, schedules or host services." + ], + "update": [ + "Use the existing verified installation or source-refresh procedure, and verify the released archive and its checksum/SBOM sidecars. Managed consumers retain their last qualified pin until the new release passes their compatibility checks.", + "Retirement is an explicitly authorized discard of one exact owned local shell, including its unsubmitted input and shell-local state. Ordinary cleanup and command dispatch do not grant that authority." + ], + "verification": [ + "The corrected installed-wheel regression passed, as did 25 SDK build and payload tests and 25 FleetOps adapter tests against the corrected installed wheel, including the previously failing tmux subcases.", + "The earlier shared recovery and acquisition-boundary changes passed independent source review and 34 focused lifecycle and real-PTY tests. The PTY fixtures reported three fork-related deprecation warnings under the threaded test runner.", + "An earlier registered-CLI retirement smoke used the real FleetOps store and shared service to verify exact original archives, one close, idempotency, lost-response reconciliation and structured held-lock refusal. Native I/O and process identity were simulated; this is not live-host qualification.", + "Release activation requires full validation and all required release gates for the exact final candidate. The earlier 4.90.0 candidate archive is not the corrected deliverable. Focused results do not establish provider completion or health, live-host readiness, or acceptance of a final published artifact.", + "Verify the exact framework archive with its .sha256 checksum and .cdx.json SBOM sidecars before adopting it." + ] +} diff --git a/ls/docs/releases/4.90.1.md b/ls/docs/releases/4.90.1.md new file mode 100644 index 000000000..38e02775a --- /dev/null +++ b/ls/docs/releases/4.90.1.md @@ -0,0 +1,58 @@ +--- +status: ACTIVE +version: 4.90 +owner_skill: ls-github-release-publishing +--- + +# LocalSetup 4.90.1 + +LocalSetup 4.90.1 includes the reviewed Herdr retirement and terminal startup changes since the last published release, together with a correction to SDK wheel helper permissions and the reviewed dependency maintenance. + +## Highlights + +- Retire only an explicitly selected uninitialized Herdr creation: the shared owner requires the original request and creation token, exact pane and stable terminal identities, discard acknowledgement, and fresh evidence that no operation, writer or descendant work occupies the current shell. +- Preserve original owner and reservation bytes before closing, durably fence the close before dispatch, and reconcile a lost response without repeating it. Both original native identifiers must be conclusively absent before a later ordinary allocation for that same binding is permitted; recovery never automatically creates a replacement or runs a workload. +- Keep caller-owned storage in place. Optional retirement requires an exact bounded snapshot, durable writes and a nonblocking cross-process lock. Raw custom-store lock contention is reported as pane_busy before dispatch, while operation-body and cleanup failures are not reclassified as harmless lock refusal. +- Preserve the controlled Herdr startup loader across login-profile changes, recover a missing prior generation only from completed-operation and native-absence proof, and translate the child fraction to Herdr's parent-oriented split ratio. +- Carry forward the reviewed dependency graph, hash-pinned SDK and storage exports, reviewed routing-catalog evidence and bounded native qualification diagnostics from the unpublished maintenance line. Runtime, build and optional storage scopes remain separate; client model configuration is unchanged. +- Correct SDK wheel helper permissions: BuildSDK sets the packaged sudo_askpass helper to mode 0755 without changing its bytes and rejects symlinked helper destinations. Installed runtime protection still rejects a non-executable or group- or world-writable helper. + +After publication, see the [release and downloads](https://github.com/CruxExperts/localsetup/releases/tag/v4.90.1) for release assets. + +## Compatibility + +The optional recovery methods extend managed-terminal API version 1. Existing callers that do not request retirement retain their normal lifecycle, and older runtimes must refuse the missing optional capability rather than substitute generic cleanup. + +The new retirement process proof requires Linux /proc visibility and provable native server ancestry. Missing platform, identity or occupancy evidence refuses recovery; normal Herdr and tmux operation support is unchanged. + +Keep the original retirement ID and durable evidence until reconciliation and any subsequent allocation complete. Do not delete journals, replay a close or downgrade the recovery owner to bypass an unknown outcome. + +Safe wheel permission metadata does not make an arbitrary installer secure: installation permissions also depend on the installer and its environment. The installed qualification used the existing protected contract: a private cache, filtered environment, --no-config, --offline, --link-mode copy and umask 077. + +Dependency and routing-catalog reviews are point-in-time evidence, not a package-safety or per-account model-access guarantee. This release does not authorize changes to configured models, schedules or host services. + +## Update + +Use the existing verified installation or source-refresh procedure, and verify the released archive and its checksum/SBOM sidecars. Managed consumers retain their last qualified pin until the new release passes their compatibility checks. + +Retirement is an explicitly authorized discard of one exact owned local shell, including its unsubmitted input and shell-local state. Ordinary cleanup and command dispatch do not grant that authority. + +For installation and source refresh, read the [quickstart update instructions](../QUICKSTART.md#update). Adapter changes follow the [adapter ownership guide](../ADAPTER_OWNERSHIP.md). + +## Verify the download + +The corrected installed-wheel regression passed, as did 25 SDK build and payload tests and 25 FleetOps adapter tests against the corrected installed wheel, including the previously failing tmux subcases. + +The earlier shared recovery and acquisition-boundary changes passed independent source review and 34 focused lifecycle and real-PTY tests. The PTY fixtures reported three fork-related deprecation warnings under the threaded test runner. + +An earlier registered-CLI retirement smoke used the real FleetOps store and shared service to verify exact original archives, one close, idempotency, lost-response reconciliation and structured held-lock refusal. Native I/O and process identity were simulated; this is not live-host qualification. + +Release activation requires full validation and all required release gates for the exact final candidate. The earlier 4.90.0 candidate archive is not the corrected deliverable. Focused results do not establish provider completion or health, live-host readiness, or acceptance of a final published artifact. + +Verify the exact framework archive with its .sha256 checksum and .cdx.json SBOM sidecars before adopting it. + +Download the framework archive with both its `.sha256` checksum and `.cdx.json` SBOM sidecars. Keep all three files in the same directory before running `verify-release`. + +```bash +uv run --locked python ls/tools/localsetup.py --source-root . verify-release /path/to/localsetup-v4.90.1.tar.gz +``` diff --git a/ls/docs/releases/4.90.2.json b/ls/docs/releases/4.90.2.json new file mode 100644 index 000000000..1f603a9f6 --- /dev/null +++ b/ls/docs/releases/4.90.2.json @@ -0,0 +1,56 @@ +{ + "schema_version": 1, + "version": "4.90.2", + "source_commit": "2b4df2e691d339085ea0e31c53c5ede3612641fb", + "baseline_tag": "v4.89.10", + "summary": "LocalSetup 4.90.2 carries forward the reviewed Herdr recovery, terminal startup, SDK helper and dependency maintenance since the last published release, and adds focused native test failure evidence.", + "highlights": [ + { + "text": "Retire only an explicitly selected uninitialized Herdr creation: the shared owner requires the original request and creation token, exact pane and stable terminal identities, discard acknowledgement, and fresh evidence that no operation, writer or descendant work occupies the current shell.", + "evidence": ["ls/core/terminal_ops/pane.py", "ls/core/terminal_ops/backends.py", "ls/docs/ops/herdr-ops.md"] + }, + { + "text": "Preserve original owner and reservation bytes before closing, durably fence the close before dispatch, and reconcile a lost response without repeating it. Both original native identifiers must be conclusively absent before a later ordinary allocation for that same binding is permitted; recovery never automatically creates a replacement or runs a workload.", + "evidence": ["ls/core/herdr_ops/layout.py", "ls/core/terminal_ops/pane.py", "ls/tests/test_herdr_managed_pane.py"] + }, + { + "text": "Keep caller-owned storage in place. Optional retirement requires an exact bounded snapshot, durable writes and a nonblocking cross-process lock. Raw custom-store lock contention is reported as pane_busy before dispatch, while operation-body and cleanup failures are not reclassified as harmless lock refusal.", + "evidence": ["ls/core/terminal_ops/pane.py", "ls/tests/test_herdr_managed_pane.py"] + }, + { + "text": "Preserve the controlled Herdr startup loader across login-profile changes, recover a missing prior generation only from completed-operation and native-absence proof, and translate the child fraction to Herdr's parent-oriented split ratio.", + "evidence": ["17e12628e88d17717a685b96c99a6edb5f65941b", "169ac81aa0f2ba10a0bf208bc00a9d228742b665", "3cb87c07bf1a40bc430f9ad721dd31438aedbc12"] + }, + { + "text": "Carry forward the reviewed dependency graph, hash-pinned SDK and storage exports, reviewed routing-catalog evidence and bounded native qualification diagnostics from the unpublished maintenance line. Runtime, build and optional storage scopes remain separate; client model configuration is unchanged.", + "evidence": ["ls/docs/releases/4.89.11.json", "ls/docs/releases/4.89.12.json", "ls/config/sdk-dependency-receipt.json"] + }, + { + "text": "Correct SDK wheel helper permissions: BuildSDK sets the packaged sudo_askpass helper to mode 0755 without changing its bytes and rejects symlinked helper destinations. Installed runtime protection still rejects a non-executable or group- or world-writable helper.", + "evidence": ["ls/core/sdk_payload/build.py", "ls/tests/test_sdk_build.py", "ls/docs/SDK_FORK.md"] + }, + { + "text": "Add failure-only native test metadata for popup identity rejection, primary viewport handoff and final family detach. The diagnostics do not change admission, assertions or deadlines and do not establish that the hosted failures are fixed.", + "evidence": ["ls/tests/conftest.py", "ls/tests/test_shell_ui_refresh.py", "ls/tests/test_shell_family.py", "ls/docs/REPO_MAINTENANCE.md"] + } + ], + "compatibility": [ + "The optional recovery methods extend managed-terminal API version 1. Existing callers that do not request retirement retain their normal lifecycle, and older runtimes must refuse the missing optional capability rather than substitute generic cleanup.", + "The new retirement process proof requires Linux /proc visibility and provable native server ancestry. Missing platform, identity or occupancy evidence refuses recovery; normal Herdr and tmux operation support is unchanged.", + "Keep the original retirement ID and durable evidence until reconciliation and any subsequent allocation complete. Do not delete journals, replay a close or downgrade the recovery owner to bypass an unknown outcome.", + "Safe wheel permission metadata does not make an arbitrary installer secure: installation permissions also depend on the installer and its environment. The installed qualification used the existing protected contract: a private cache, filtered environment, --no-config, --offline, --link-mode copy and umask 077.", + "Dependency and routing-catalog reviews are point-in-time evidence, not a package-safety or per-account model-access guarantee. This release does not authorize changes to configured models, schedules or host services." + ], + "update": [ + "Use the existing verified installation or source-refresh procedure, and verify the released archive and its checksum/SBOM sidecars. Managed consumers retain their last qualified pin until the new release passes their compatibility checks.", + "Retirement is an explicitly authorized discard of one exact owned local shell, including its unsubmitted input and shell-local state. Ordinary cleanup and command dispatch do not grant that authority." + ], + "verification": [ + "Earlier exact source and installed-wheel snapshots retained evidence for 34 focused shared lifecycle and real-PTY tests, 25 SDK build and payload tests, and 25 FleetOps adapter tests against the corrected installed wheel. The PTY fixtures reported three fork-related deprecation warnings under the threaded test runner.", + "An earlier registered-CLI retirement smoke used the real FleetOps store and shared service to verify exact original archives, one close, idempotency, lost-response reconciliation and structured held-lock refusal. Native I/O and process identity were simulated; this is not live-host qualification.", + "The diagnostic slice passed eight normal native scenarios. Three deliberately injected private native faults retained the original test failures while producing the corresponding failure evidence; these exercises do not demonstrate a production fix.", + "Hosted validation of earlier candidate a43862d3 failed three framework shards. Its recorded exact-seed local partitions passed 1510 tests with one skip, but those local results are not hosted clearance.", + "The new candidate still requires full hosted validation and all release gates for the exact final candidate. Focused results do not establish live-host readiness, provider health or acceptance of a final published artifact.", + "Verify the exact framework archive with its .sha256 checksum and .cdx.json SBOM sidecars before adopting it." + ] +} diff --git a/ls/docs/releases/4.90.2.md b/ls/docs/releases/4.90.2.md new file mode 100644 index 000000000..0d62757fd --- /dev/null +++ b/ls/docs/releases/4.90.2.md @@ -0,0 +1,61 @@ +--- +status: ACTIVE +version: 4.90 +owner_skill: ls-github-release-publishing +--- + +# LocalSetup 4.90.2 + +LocalSetup 4.90.2 carries forward the reviewed Herdr recovery, terminal startup, SDK helper and dependency maintenance since the last published release, and adds focused native test failure evidence. + +## Highlights + +- Retire only an explicitly selected uninitialized Herdr creation: the shared owner requires the original request and creation token, exact pane and stable terminal identities, discard acknowledgement, and fresh evidence that no operation, writer or descendant work occupies the current shell. +- Preserve original owner and reservation bytes before closing, durably fence the close before dispatch, and reconcile a lost response without repeating it. Both original native identifiers must be conclusively absent before a later ordinary allocation for that same binding is permitted; recovery never automatically creates a replacement or runs a workload. +- Keep caller-owned storage in place. Optional retirement requires an exact bounded snapshot, durable writes and a nonblocking cross-process lock. Raw custom-store lock contention is reported as pane_busy before dispatch, while operation-body and cleanup failures are not reclassified as harmless lock refusal. +- Preserve the controlled Herdr startup loader across login-profile changes, recover a missing prior generation only from completed-operation and native-absence proof, and translate the child fraction to Herdr's parent-oriented split ratio. +- Carry forward the reviewed dependency graph, hash-pinned SDK and storage exports, reviewed routing-catalog evidence and bounded native qualification diagnostics from the unpublished maintenance line. Runtime, build and optional storage scopes remain separate; client model configuration is unchanged. +- Correct SDK wheel helper permissions: BuildSDK sets the packaged sudo_askpass helper to mode 0755 without changing its bytes and rejects symlinked helper destinations. Installed runtime protection still rejects a non-executable or group- or world-writable helper. +- Add failure-only native test metadata for popup identity rejection, primary viewport handoff and final family detach. The diagnostics do not change admission, assertions or deadlines and do not establish that the hosted failures are fixed. + +After publication, see the [release and downloads](https://github.com/CruxExperts/localsetup/releases/tag/v4.90.2) for release assets. + +## Compatibility + +The optional recovery methods extend managed-terminal API version 1. Existing callers that do not request retirement retain their normal lifecycle, and older runtimes must refuse the missing optional capability rather than substitute generic cleanup. + +The new retirement process proof requires Linux /proc visibility and provable native server ancestry. Missing platform, identity or occupancy evidence refuses recovery; normal Herdr and tmux operation support is unchanged. + +Keep the original retirement ID and durable evidence until reconciliation and any subsequent allocation complete. Do not delete journals, replay a close or downgrade the recovery owner to bypass an unknown outcome. + +Safe wheel permission metadata does not make an arbitrary installer secure: installation permissions also depend on the installer and its environment. The installed qualification used the existing protected contract: a private cache, filtered environment, --no-config, --offline, --link-mode copy and umask 077. + +Dependency and routing-catalog reviews are point-in-time evidence, not a package-safety or per-account model-access guarantee. This release does not authorize changes to configured models, schedules or host services. + +## Update + +Use the existing verified installation or source-refresh procedure, and verify the released archive and its checksum/SBOM sidecars. Managed consumers retain their last qualified pin until the new release passes their compatibility checks. + +Retirement is an explicitly authorized discard of one exact owned local shell, including its unsubmitted input and shell-local state. Ordinary cleanup and command dispatch do not grant that authority. + +For installation and source refresh, read the [quickstart update instructions](../QUICKSTART.md#update). Adapter changes follow the [adapter ownership guide](../ADAPTER_OWNERSHIP.md). + +## Verify the download + +Earlier exact source and installed-wheel snapshots retained evidence for 34 focused shared lifecycle and real-PTY tests, 25 SDK build and payload tests, and 25 FleetOps adapter tests against the corrected installed wheel. The PTY fixtures reported three fork-related deprecation warnings under the threaded test runner. + +An earlier registered-CLI retirement smoke used the real FleetOps store and shared service to verify exact original archives, one close, idempotency, lost-response reconciliation and structured held-lock refusal. Native I/O and process identity were simulated; this is not live-host qualification. + +The diagnostic slice passed eight normal native scenarios. Three deliberately injected private native faults retained the original test failures while producing the corresponding failure evidence; these exercises do not demonstrate a production fix. + +Hosted validation of earlier candidate a43862d3 failed three framework shards. Its recorded exact-seed local partitions passed 1510 tests with one skip, but those local results are not hosted clearance. + +The new candidate still requires full hosted validation and all release gates for the exact final candidate. Focused results do not establish live-host readiness, provider health or acceptance of a final published artifact. + +Verify the exact framework archive with its .sha256 checksum and .cdx.json SBOM sidecars before adopting it. + +Download the framework archive with both its `.sha256` checksum and `.cdx.json` SBOM sidecars. Keep all three files in the same directory before running `verify-release`. + +```bash +uv run --locked python ls/tools/localsetup.py --source-root . verify-release /path/to/localsetup-v4.90.2.tar.gz +``` diff --git a/ls/docs/scrapling-cheat-sheet.md b/ls/docs/scrapling-cheat-sheet.md index 4b0bc71c0..00c4617eb 100644 --- a/ls/docs/scrapling-cheat-sheet.md +++ b/ls/docs/scrapling-cheat-sheet.md @@ -1,6 +1,6 @@ --- status: ACTIVE -version: 4.89 +version: 4.90 owner_skill: ls-scrapling-url-extraction --- diff --git a/ls/skills/ls-agent-capability-routing/resources/model-capability-matrix/manifest.json b/ls/skills/ls-agent-capability-routing/resources/model-capability-matrix/manifest.json index 94f230eb2..14ac3227c 100644 --- a/ls/skills/ls-agent-capability-routing/resources/model-capability-matrix/manifest.json +++ b/ls/skills/ls-agent-capability-routing/resources/model-capability-matrix/manifest.json @@ -5,14 +5,14 @@ "schema_path": "schema.json", "snapshot_path": "snapshot.json", "matrix_schema_sha256": "1b4e6c04d72f7683e3808182e99c7e29530bf002e9246eff785870d6f3a3ff42", - "snapshot_sha256": "4392f1fba2927c22775a19dcbb504d4efa49a76257b5db2b4f1a0bf0a07adf88", + "snapshot_sha256": "e4449f25590c836da7c3b084987a9af6c9979cc05dca5c04e7fd69bad5ad42c4", "evidence": [ - {"evidence_id": "api-family-gpt-6", "url": "https://developers.openai.com/api/docs/guides/latest-model", "access_date": "2026-10-03", "claim_scope": "api_family"}, - {"evidence_id": "model-gpt-6-astra", "url": "https://developers.openai.com/api/docs/models/gpt-6-astra", "access_date": "2026-10-03", "claim_scope": "model_version"}, - {"evidence_id": "model-gpt-6.1-sol", "url": "https://developers.openai.com/api/docs/models/gpt-6.1-sol", "access_date": "2026-10-03", "claim_scope": "model_version"}, - {"evidence_id": "model-gpt-6-luna", "url": "https://developers.openai.com/api/docs/models/gpt-6-luna", "access_date": "2026-10-03", "claim_scope": "model_version"}, - {"evidence_id": "client-codex-subagents", "url": "https://learn.chatgpt.com/docs/agent-configuration/subagents", "access_date": "2026-10-03", "claim_scope": "client_product"}, - {"evidence_id": "accounting-api-pricing-standard-short-context", "url": "https://developers.openai.com/api/docs/pricing", "access_date": "2026-10-03", "claim_scope": "accounting"}, - {"evidence_id": "accounting-codex-token-rate-card", "url": "https://learn.chatgpt.com/docs/pricing", "access_date": "2026-10-03", "claim_scope": "accounting"} + {"evidence_id": "api-family-gpt-6", "url": "https://developers.openai.com/api/docs/guides/latest-model", "access_date": "2026-11-02", "claim_scope": "api_family"}, + {"evidence_id": "model-gpt-6-astra", "url": "https://developers.openai.com/api/docs/models/gpt-6-astra", "access_date": "2026-11-02", "claim_scope": "model_version"}, + {"evidence_id": "model-gpt-6.1-sol", "url": "https://developers.openai.com/api/docs/models/gpt-6.1-sol", "access_date": "2026-11-02", "claim_scope": "model_version"}, + {"evidence_id": "model-gpt-6-luna", "url": "https://developers.openai.com/api/docs/models/gpt-6-luna", "access_date": "2026-11-02", "claim_scope": "model_version"}, + {"evidence_id": "client-codex-subagents", "url": "https://learn.chatgpt.com/docs/agent-configuration/subagents", "access_date": "2026-11-02", "claim_scope": "client_product"}, + {"evidence_id": "accounting-api-pricing-standard-short-context", "url": "https://developers.openai.com/api/docs/pricing", "access_date": "2026-11-02", "claim_scope": "accounting"}, + {"evidence_id": "accounting-codex-token-rate-card", "url": "https://learn.chatgpt.com/docs/pricing", "access_date": "2026-11-02", "claim_scope": "accounting"} ] } diff --git a/ls/skills/ls-agent-capability-routing/resources/model-capability-matrix/snapshot.json b/ls/skills/ls-agent-capability-routing/resources/model-capability-matrix/snapshot.json index 44cd32c10..e21f5e18f 100644 --- a/ls/skills/ls-agent-capability-routing/resources/model-capability-matrix/snapshot.json +++ b/ls/skills/ls-agent-capability-routing/resources/model-capability-matrix/snapshot.json @@ -3,8 +3,8 @@ "schema_version": 1, "resource_id": "model-capability-matrix", "owner_skill": "ls-agent-capability-routing", - "reviewed_at": "2026-10-03", - "fresh_until": "2026-11-02", + "reviewed_at": "2026-11-02", + "fresh_until": "2026-12-02", "candidates": [ {"lane": "Agent-Compact", "model_id": "gpt-6-luna", "eligibility": "reviewed", "policy_priority": 10, "task_classes": ["discovery", "routine", "research"], "maximum_risk": "medium", "capabilities": {"vision": {"value": "unknown"}}}, {"lane": "Agent-Realtime", "model_id": "gpt-6-luna", "eligibility": "reviewed", "policy_priority": 5, "task_classes": ["routine"], "maximum_risk": "low", "capabilities": {"vision": {"value": "unknown"}}}, diff --git a/ls/skills/ls-agent-capability-routing/scripts/agent_routing.py b/ls/skills/ls-agent-capability-routing/scripts/agent_routing.py index 5340f890e..ce4875573 100644 --- a/ls/skills/ls-agent-capability-routing/scripts/agent_routing.py +++ b/ls/skills/ls-agent-capability-routing/scripts/agent_routing.py @@ -35,20 +35,20 @@ # These anchors are intentionally in the isolated standard-library selector, not # in a mutable manifest. A valid manifest digest cannot substitute for one. -CANONICAL_MANIFEST_SHA256 = "e252bca8d5e7c06a1298dfecb8d68e4a33ec2052ab07ae729c9ca0b991b934d7" +CANONICAL_MANIFEST_SHA256 = "db84e810c9bf8477ee33a650c6f211e8b462c506cbda187d74929848e4c1d37e" CANONICAL_MATRIX_SCHEMA_SHA256 = "1b4e6c04d72f7683e3808182e99c7e29530bf002e9246eff785870d6f3a3ff42" -CANONICAL_SNAPSHOT_SHA256 = "4392f1fba2927c22775a19dcbb504d4efa49a76257b5db2b4f1a0bf0a07adf88" +CANONICAL_SNAPSHOT_SHA256 = "e4449f25590c836da7c3b084987a9af6c9979cc05dca5c04e7fd69bad5ad42c4" CANONICAL_REQUEST_SCHEMA_SHA256 = "f05f89d84b8a4617855cf51a23663e08709051b9020f6cfdf4d22ee46d0ed562" CANONICAL_RECEIPT_SCHEMA_SHA256 = "b9c8076f8af65af04e214d9729d21f2200e9b5f9397dab8a0943b95aa693f1f5" CANONICAL_EVIDENCE = ( - ("api-family-gpt-6", "https://developers.openai.com/api/docs/guides/latest-model", "2026-10-03", "api_family"), - ("model-gpt-6-astra", "https://developers.openai.com/api/docs/models/gpt-6-astra", "2026-10-03", "model_version"), - ("model-gpt-6.1-sol", "https://developers.openai.com/api/docs/models/gpt-6.1-sol", "2026-10-03", "model_version"), - ("model-gpt-6-luna", "https://developers.openai.com/api/docs/models/gpt-6-luna", "2026-10-03", "model_version"), - ("client-codex-subagents", "https://learn.chatgpt.com/docs/agent-configuration/subagents", "2026-10-03", "client_product"), - ("accounting-api-pricing-standard-short-context", "https://developers.openai.com/api/docs/pricing", "2026-10-03", "accounting"), - ("accounting-codex-token-rate-card", "https://learn.chatgpt.com/docs/pricing", "2026-10-03", "accounting"), + ("api-family-gpt-6", "https://developers.openai.com/api/docs/guides/latest-model", "2026-11-02", "api_family"), + ("model-gpt-6-astra", "https://developers.openai.com/api/docs/models/gpt-6-astra", "2026-11-02", "model_version"), + ("model-gpt-6.1-sol", "https://developers.openai.com/api/docs/models/gpt-6.1-sol", "2026-11-02", "model_version"), + ("model-gpt-6-luna", "https://developers.openai.com/api/docs/models/gpt-6-luna", "2026-11-02", "model_version"), + ("client-codex-subagents", "https://learn.chatgpt.com/docs/agent-configuration/subagents", "2026-11-02", "client_product"), + ("accounting-api-pricing-standard-short-context", "https://developers.openai.com/api/docs/pricing", "2026-11-02", "accounting"), + ("accounting-codex-token-rate-card", "https://learn.chatgpt.com/docs/pricing", "2026-11-02", "accounting"), ) CANONICAL_FACT_PAYLOADS: dict[str, dict[str, Any]] = { diff --git a/ls/skills/ls-backblaze-b2-storage/requirements-s3-sdk.txt b/ls/skills/ls-backblaze-b2-storage/requirements-s3-sdk.txt index 6d82a815b..c5f4cf5a9 100644 --- a/ls/skills/ls-backblaze-b2-storage/requirements-s3-sdk.txt +++ b/ls/skills/ls-backblaze-b2-storage/requirements-s3-sdk.txt @@ -1,9 +1,9 @@ -boto3==1.43.118 \ - --hash=sha256:ab8f3d2c622ef039338b4f228d97ea55f1abd573d84bb9b9a5ee8cb6780c0ccd \ - --hash=sha256:c0620f799e0f0c222ba42c337b65cdbcd3da38a3121187f634d8926cbbb28a9c -botocore==1.43.118 \ - --hash=sha256:7f3f9d88cc9e847587690f82a42e3c3c292c3026c92c1e60d68a9310018e003f \ - --hash=sha256:a45d7ae570674161e4a3542d8c68633bf7503f92d1de781ef7361b79488eda6f +boto3==1.43.122 \ + --hash=sha256:c923650d09e4c445564ca14d5ce89466c3083884b6f6c189f0f445fc17f02eb5 \ + --hash=sha256:cca51b3650080594a1df344b8a2e76007eab6d5fa50504a0b622dfdc14cdeaa1 +botocore==1.43.123 \ + --hash=sha256:80948cb9b1cdf1a1a25809089a42e3a1bc4af0258d198222db6e97dcc660dd0c \ + --hash=sha256:e30580f9c1282c321529238ff8ff75066096a2adbd0a293c8ae592c21d1733a3 jmespath==1.1.0 \ --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 diff --git a/ls/skills/ls-garage-s3-admin/requirements-s3-sdk.txt b/ls/skills/ls-garage-s3-admin/requirements-s3-sdk.txt index 6d82a815b..c5f4cf5a9 100644 --- a/ls/skills/ls-garage-s3-admin/requirements-s3-sdk.txt +++ b/ls/skills/ls-garage-s3-admin/requirements-s3-sdk.txt @@ -1,9 +1,9 @@ -boto3==1.43.118 \ - --hash=sha256:ab8f3d2c622ef039338b4f228d97ea55f1abd573d84bb9b9a5ee8cb6780c0ccd \ - --hash=sha256:c0620f799e0f0c222ba42c337b65cdbcd3da38a3121187f634d8926cbbb28a9c -botocore==1.43.118 \ - --hash=sha256:7f3f9d88cc9e847587690f82a42e3c3c292c3026c92c1e60d68a9310018e003f \ - --hash=sha256:a45d7ae570674161e4a3542d8c68633bf7503f92d1de781ef7361b79488eda6f +boto3==1.43.122 \ + --hash=sha256:c923650d09e4c445564ca14d5ce89466c3083884b6f6c189f0f445fc17f02eb5 \ + --hash=sha256:cca51b3650080594a1df344b8a2e76007eab6d5fa50504a0b622dfdc14cdeaa1 +botocore==1.43.123 \ + --hash=sha256:80948cb9b1cdf1a1a25809089a42e3a1bc4af0258d198222db6e97dcc660dd0c \ + --hash=sha256:e30580f9c1282c321529238ff8ff75066096a2adbd0a293c8ae592c21d1733a3 jmespath==1.1.0 \ --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 diff --git a/ls/tests/conftest.py b/ls/tests/conftest.py index a19c6976f..8ad8061e5 100644 --- a/ls/tests/conftest.py +++ b/ls/tests/conftest.py @@ -30,3 +30,140 @@ def default_opencode_environment(monkeypatch): for name in ('OPENCODE_TEST_HOME', 'OPENCODE_CONFIG_DIR', 'OPENCODE_DISABLE_EXTERNAL_SKILLS', 'XDG_CONFIG_HOME'): monkeypatch.delenv(name, raising=False) + + +@pytest.fixture +def native_popup_diagnostics(monkeypatch): + """Observe private popup failures without retaining arguments or input.""" + import shutil + import subprocess + import threading + + from ls.core.terminal_ops.command_native_backend import NativeCanvasV3 + from ls.core.terminal_ops.command_state import ShellCommandError, process_info, process_state + + diagnostics: dict[str, object] = {} + samples: list[dict[str, object]] = [] + tmux_path = Path(shutil.which("tmux") or "tmux").resolve() + original_show = NativeCanvasV3.show_dialog + original_command = NativeCanvasV3._process_command + original_launch = NativeCanvasV3._run_overlay_client + expected: dict[str, bytes] = {} + local = threading.local() + original_popen = subprocess.Popen + + def observed_popen(*args, **kwargs): + scope = getattr(local, "scope", None) + argv = args[0] if args else kwargs.get("args") + if (scope is None or scope.get("attempted") or argv is None or argv is not scope["argv"] + or len(argv) < 8 or argv[1] != "-f" or argv[3] != "-S" + or os.fsencode(argv[4]) != scope["endpoint"] or argv[5] != "display-popup" + or argv[6] != "-c" or os.fsencode(argv[7]) != scope["client"] + or kwargs.get("stderr") != subprocess.DEVNULL): + return original_popen(*args, **kwargs) + scope["attempted"] = True + try: + scope["fd"] = os.memfd_create("popup-stderr", getattr(os, "MFD_CLOEXEC", 0)) + except (AttributeError, OSError) as exc: + scope["capture_error"] = type(exc).__name__ + else: + kwargs = {**kwargs, "stderr": scope["fd"]} + process = original_popen(*args, **kwargs) + scope["process"] = process + return process + + def failure_evidence(scope): + result = {"capture_error": scope.get("capture_error")} + process = scope.get("process") + if process is not None: + pid = process.pid + info = process_info(pid) + result.update(pid=pid, process=( + {key: info.get(key) for key in ("pid", "ppid", "starttime")} if info else None), + state=process_state(pid)) + if all(hasattr(os, name) for name in ("waitid", "P_PID", "WEXITED", "WNOHANG", "WNOWAIT")): + try: + status = os.waitid(os.P_PID, pid, os.WEXITED | os.WNOHANG | os.WNOWAIT) + result["exit"] = ({"exited": False} if status is None else + {"exited": True, "si_code": status.si_code, "si_status": status.si_status}) + except (OSError, ChildProcessError) as exc: + result["exit"] = {"error": type(exc).__name__} + else: + result["exit"] = {"error": "unsupported"} + fd = scope.get("fd") + if fd is not None: + length = os.fstat(fd).st_size + data = os.pread(fd, min(length, 1048576), 0).lower() + phrases = { + "client_missing": (b"no such client", b"can't find client", b"client not found"), + "terminal_unsupported": (b"terminal does not support", b"terminal lacks", b"not a terminal", b"unknown terminal"), + "server_missing": (b"no server running", b"failed to connect to server", b"server not found", b"no such socket"), + "invalid_argument": (b"invalid argument", b"unknown option", b"usage:"), + "invalid_size": (b"invalid size", b"size too small", b"width too small", b"height too small", b"popup too large"), + "permission_denied": (b"permission denied", b"operation not permitted"), + "session_missing": (b"no such session", b"can't find session"), + "pane_missing": (b"no such pane", b"can't find pane"), + "nested_session": (b"sessions should be nested",), + "server_terminated": (b"server exited unexpectedly", b"lost server"), + "popup_unsupported": (b"unknown command", b"unknown flag", b"popup already exists"), + } + result["stderr"] = {"bytes": length, "classified_bytes": len(data), "flags": { + name: (True if any(phrase in data for phrase in choices) else False if len(data) == length else None) + for name, choices in phrases.items() + }} + return result + + + def observed_launch(self, record, client, argv, *, on_spawn, deadline=None): + previous = getattr(local, "scope", None) + scope = {"argv": argv, "endpoint": os.fsencode(record["endpoint"]), + "client": os.fsencode(client["name"])} + local.scope = scope + expected.update(endpoint=os.fsencode(record["endpoint"]), client=os.fsencode(client["name"])) + try: + return original_launch(self, record, client, argv, on_spawn=on_spawn, deadline=deadline) + except ShellCommandError as exc: + if exc.code == "overlay_identity_unknown": + try: + diagnostics["failure"] = failure_evidence(scope) + except Exception as error: + diagnostics["failure"] = {"diagnostic_error": type(error).__name__} + raise + finally: + local.scope = previous + expected.clear() + if scope.get("fd") is not None: + os.close(scope["fd"]) + + def observed_show(self, record, client_pid, channel, *, on_spawn): + samples.clear() + diagnostics.clear() + diagnostics.update(show_entered=True, process_samples=samples) + try: + return original_show(self, record, client_pid, channel, on_spawn=on_spawn) + except ShellCommandError as exc: + diagnostics.update(error_code=exc.code, effect=exc.effect) + raise + + def observed_command(pid: int) -> list[bytes] | None: + command = original_command(pid) + if diagnostics.get("show_entered") and expected: + if len(samples) == 4: + samples.pop(0) + samples.append({ + "pid": pid, "command_available": command is not None, + "command_nonempty": bool(command), + "process_present_after_read": process_info(pid) is not None, + "process_zombie_after_read": process_state(pid) == "Z", + "argv0_is_tmux": bool(command and Path(os.fsdecode(command[0])).resolve() == tmux_path), + "popup_argument_present": bool(command and b"display-popup" in command), + "endpoint_matches": bool(command and expected.get("endpoint") in command), + "client_matches": bool(command and expected.get("client") in command), + }) + return command + + monkeypatch.setattr(NativeCanvasV3, "show_dialog", observed_show) + monkeypatch.setattr(subprocess, "Popen", observed_popen) + monkeypatch.setattr(NativeCanvasV3, "_run_overlay_client", observed_launch) + monkeypatch.setattr(NativeCanvasV3, "_process_command", staticmethod(observed_command)) + return diagnostics diff --git a/ls/tests/test_agent_routing.py b/ls/tests/test_agent_routing.py index ebb578a93..e568b5ba0 100644 --- a/ls/tests/test_agent_routing.py +++ b/ls/tests/test_agent_routing.py @@ -209,6 +209,37 @@ def test_every_retained_lane_is_reachable_through_cli( assert receipt["selected"] == {"lane": expected_lane} +@pytest.mark.parametrize( + ("days_after_expiry", "status", "reason"), + [(0, "selected", "selected_static_reviewed"), (1, "rejected", "resource_stale")], +) +def test_authenticated_catalog_expiry_is_inclusive( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + days_after_expiry: int, + status: str, + reason: str, +) -> None: + module = load_selector_module(SCRIPT) + expiry = module.dt.date.fromisoformat( + load_json(SKILL / "resources/model-capability-matrix/snapshot.json")["fresh_until"] + ) + today = expiry + module.dt.timedelta(days=days_after_expiry) + + class ReviewDate(module.dt.date): + @classmethod + def today(cls): + return today + + # Change only this test's clock; load the real bytes and integrity anchors. + monkeypatch.setattr(module.dt, "date", ReviewDate) + receipt = run_loaded_select(module, monkeypatch, capsys) + + assert (receipt["status"], receipt["reason"]) == (status, reason) + if status == "rejected": + assert "selected" not in receipt + + @pytest.mark.parametrize("resource_error", ("resource_invalid", "resource_stale")) def test_resource_error_precedes_request_read( monkeypatch: pytest.MonkeyPatch, diff --git a/ls/tests/test_herdr_managed_pane.py b/ls/tests/test_herdr_managed_pane.py new file mode 100644 index 000000000..7fa6bd6cf --- /dev/null +++ b/ls/tests/test_herdr_managed_pane.py @@ -0,0 +1,781 @@ +"""Focused regressions for managed Herdr pane ownership and observation.""" + +from __future__ import annotations +from contextlib import contextmanager +import fcntl + +import json +import os +from pathlib import Path +import subprocess +import sys + +import pytest + +from ls.core.herdr_ops.errors import HerdrOpsError +from ls.core.terminal_ops import backends +from ls.core.terminal_ops.models import PaneBinding, PaneError, PaneRequest, PaneStatus, PaneTransition +from ls.core.terminal_ops.pane import FilePaneStore, ManagedPaneService +from ls.core.terminal_ops.shell import finish_transition + + +@pytest.fixture +def herdr_request(tmp_path: Path) -> PaneRequest: + return PaneRequest( + "herdr", str(tmp_path / "herdr.sock"), "project", "controller-current", + "parent-pane", str(tmp_path), workspace_id="workspace", machine_id="machine", + ) + + +def _completed_prior_state( + tmp_path: Path, request: PaneRequest, binding: PaneBinding, +) -> tuple[Path, dict[str, dict], PaneStatus, dict]: + state_dir = tmp_path / "state" + state_dir.mkdir(mode=0o700) + operations_dir = state_dir / "terminal_ops" + operations_dir.mkdir(mode=0o700) + directory = operations_dir / request.binding_key + directory.mkdir(mode=0o700) + operation_id = "completed-before-reboot" + lease_token = "completed-lease-token" + lease = directory / "writer-lease" + lease.mkdir(mode=0o700) + (lease / "lease.json").write_text(json.dumps({ + "binding_key": request.binding_key, + "operation_id": operation_id, + "token": lease_token, + }), encoding="utf-8") + operation = { + "schema_version": 1, + "binding_key": request.binding_key, + "operation_id": operation_id, + "state": "running", + "pane_id": binding.pane_id, + "shell_pid": binding.shell_pid, + "pane_tty": binding.pane_tty, + "shell_path": binding.shell_path, + "machine_generation": binding.machine_generation, + "target": request.host_alias or request.repository, + "endpoint": request.endpoint, + "controller_id": request.controller_id, + "working_directory": request.cwd, + "lease_token": lease_token, + "transition": PaneTransition(operation_id, ("/usr/bin/true",)).to_dict(), + } + receipt_path = directory / f"{operation_id}.json" + receipt_path.write_text(json.dumps(operation), encoding="utf-8") + receipt_path.chmod(0o600) + (lease / "lease.json").chmod(0o600) + completed = finish_transition( + state_dir, binding_key=request.binding_key, operation_id=operation_id, exit_code=0, + ) + ready = PaneStatus("ready", request.backend, request.binding_key, request.name, + binding=binding, operation_id=operation_id) + store = {request.binding_key: { + "binding_key": request.binding_key, + "request": request.to_dict(), + "status": ready.to_dict(), + "last_binding": binding.to_dict(), + "operation": completed, + }} + return state_dir, store, ready, completed + + +def _owner_record(state_dir: Path, endpoint: str, pane_id: str, *, controller: str, + parent: str, workspace: str = "workspace") -> None: + record = { + "schema_version": 1, + "owner_id": pane_id, + "purpose": "managed_ops", + "endpoint": str(Path(endpoint).absolute()), + "workspace_id": workspace, + "caller_pane_id": parent, + "pane_id": pane_id, + "terminal_id": f"terminal-{pane_id}", + "tab_id": "tab-1", + "cwd": "/tmp", + "ratio": 1 / 3, + "state": "owned", + "owner_metadata": { + "controller_id": controller, + "parent_id": parent, + }, + } + owners = state_dir / "panes" + owners.mkdir(parents=True, exist_ok=True, mode=0o700) + (owners / f"{pane_id}.json").write_text(json.dumps(record), encoding="utf-8") + + +@pytest.mark.parametrize( + ("owned", "other_controller", "expected"), + [ + (True, True, "missing"), # same label/parent, but an exact other controller owns it + (False, False, "unknown"), # an unowned label collision cannot be adopted + (True, False, "unknown"), # a same-controller collision is not an exact binding + ], +) +def test_same_label_inventory_isolated_only_for_foreign_owned_controller( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, herdr_request: PaneRequest, + owned: bool, other_controller: bool, expected: str, +) -> None: + request = herdr_request + adapter = backends.HerdrAdapter(state_dir=tmp_path / "state") + monkeypatch.setattr(adapter, "_endpoint", lambda _request: request.endpoint) + pane = { + "pane_id": "other-managed-pane", "workspace_id": request.workspace_id, + "tab_id": "tab-1", "label": request.name, + } + inventory = [ + {"pane_id": request.parent_id, "workspace_id": request.workspace_id, "tab_id": "tab-1"}, + pane, + ] + monkeypatch.setattr(adapter, "_inventory", lambda _request: inventory) + if owned: + _owner_record( + tmp_path / "state", request.endpoint, pane["pane_id"], + controller="controller-other" if other_controller else request.controller_id, + parent=request.parent_id, + ) + + status = adapter.inspect(request) + + assert status.state == expected + assert status.binding is None + assert status.binding_key == request.binding_key + + +@pytest.mark.skipif(not Path("/proc/self/environ").exists(), reason="Linux process environment required") +def test_startup_home_and_nonempty_sudo_key_names_come_from_real_server_process( + tmp_path: Path, herdr_request: PaneRequest, +) -> None: + home = tmp_path / "server-home" + home.mkdir() + environment = { + "HOME": str(home), + "PATH": os.defpath, + "GENERIC_SUDO_KEY": "dummy-generic-secret", + "ALIEN_SUDO_KEY": "dummy-host-secret", + } + server = subprocess.Popen( + [sys.executable, "-c", "import time; time.sleep(30)"], + env=environment, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + try: + adapter = backends.HerdrAdapter(state_dir=tmp_path / "state") + + class EndpointClient: + @staticmethod + def endpoint_identity() -> dict[str, int]: + return {"pid": server.pid} + + adapter.client = EndpointClient() + names, startup_home = adapter._server_startup_environment(herdr_request) + + assert startup_home == str(home) + assert names == {"GENERIC_SUDO_KEY", "ALIEN_SUDO_KEY"} + finally: + server.terminate() + server.wait(timeout=5) + + +def test_busy_original_shell_binding_is_observable_but_never_dispatchable( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + request = PaneRequest("herdr", str(tmp_path / "herdr.sock"), "project", "controller", + "parent", str(tmp_path), workspace_id="workspace") + observed_binding = PaneBinding( + name=request.name, binding_key=request.binding_key, backend="herdr", + endpoint=request.endpoint, parent_id=request.parent_id, pane_id="child", + repository=request.repository, controller_id=request.controller_id, + workspace_id=request.workspace_id, tab_id="tab", terminal_id="terminal", + pane_tty="/dev/pts/7", shell_pid=421, + shell_path="/bin/bash", machine_generation="generation", cwd=str(tmp_path), + state="busy", metadata={"prompt_nonce": "busy-nonce", "phase": "busy"}, + ) + monkeypatch.setattr(backends, "read_prompt_state", lambda *_args: { + "pane_id": observed_binding.pane_id, + "shell_pid": observed_binding.shell_pid, + "pane_tty": observed_binding.pane_tty, + "shell_path": observed_binding.shell_path, + "cwd": observed_binding.cwd, + "machine_generation": observed_binding.machine_generation, + "phase": "busy", + "keymap_ready": True, + "prompt_nonce": "busy-nonce", + }) + monkeypatch.setattr(backends, "machine_generation", lambda: "generation") + monkeypatch.setattr(backends, "_process_cwd", lambda _pid: str(tmp_path)) + monkeypatch.setattr(backends, "_process_tty_matches", lambda _pid, _tty: True) + monkeypatch.setattr(backends, "_same_path", lambda left, right: left == right) + monkeypatch.setattr(backends, "_credential_names_in_process", lambda _pid: set()) + monkeypatch.setattr(backends, "_shell_owns_foreground", lambda _pid, _tty: False) + native = backends.HerdrAdapter(state_dir=tmp_path) + native.client = type("NativeClient", (), { + "process_info": lambda self, pane: { + "process_info": {"shell_pid": 421, "foreground_processes": [{"pid": 422}]}, + }, + })() + monkeypatch.setattr(native, "_endpoint", lambda _: request.endpoint) + monkeypatch.setattr(native, "_owned", lambda _: ( + {"caller_pane_id": request.parent_id, "workspace_id": request.workspace_id, + "tab_id": "tab", "cwd": str(tmp_path)}, + {"pane_id": "child", "workspace_id": request.workspace_id, "tab_id": "tab", + "terminal_id": "terminal", "label": request.name}, + )) + monkeypatch.setattr(native, "_inventory", lambda _: [ + {"pane_id": request.parent_id, "tab_id": "tab"}, + ]) + readlink = os.readlink + monkeypatch.setattr(os, "readlink", lambda path: ( + "/dev/pts/7" if str(path) == "/proc/421/fd/0" else readlink(path) + )) + status = native.inspect(request) + assert status.state == "busy" + assert status.binding == observed_binding + + class BusyAdapter: + triggered = False + + def inspect(self, _request: PaneRequest) -> PaneStatus: + return status + + def send_trigger(self, _request: PaneRequest, _binding: PaneBinding) -> None: + self.triggered = True + + adapter = BusyAdapter() + service = ManagedPaneService({}, {"herdr": adapter}, state_dir=tmp_path) + result = service.transition(request, PaneTransition("must-not-dispatch", ("true",))) + + assert result.state == "busy" + assert result.binding == observed_binding + assert result.operation_id is None + assert not adapter.triggered + assert not (tmp_path / "terminal_ops" / request.binding_key / "must-not-dispatch.json").exists() + + +@pytest.mark.parametrize("first_effect", ["unknown", "none"]) +def test_completed_operation_and_absence_proof_are_required_before_replacement( + tmp_path: Path, herdr_request: PaneRequest, first_effect: str, +) -> None: + request = herdr_request + previous = PaneBinding( + name=request.name, binding_key=request.binding_key, backend="herdr", + endpoint=request.endpoint, parent_id=request.parent_id, pane_id="old-pane", + repository=request.repository, controller_id=request.controller_id, + workspace_id=request.workspace_id, tab_id="old-tab", terminal_id="old-terminal", + pane_tty="/dev/pts/200", shell_pid=900001, shell_path="/bin/bash", + machine_generation="old-generation", cwd="/work/previous", state="ready", + ) + state_dir, store, _ready, _receipt = _completed_prior_state(tmp_path, request, previous) + + class Native: + def __init__(self) -> None: + self.created: list[PaneBinding | None] = [] + + def inspect( + self, _request: PaneRequest, *, previous_binding: PaneBinding | None = None, + ) -> PaneStatus: + if previous_binding is not None: + return PaneStatus("missing", request.backend, request.binding_key, request.name) + return PaneStatus("unknown", request.backend, request.binding_key, request.name, + detail="ordinary owner inspection cannot prove the missing generation") + + def create( + self, _request: PaneRequest, *, previous_binding: PaneBinding | None = None, + ) -> PaneBinding: + self.created.append(previous_binding) + effect = first_effect if len(self.created) == 1 else "unknown" + raise PaneError("split_result", "simulated native result", effect=effect) + + native = Native() + service = ManagedPaneService(store, {"herdr": native}, state_dir=state_dir) + + observed = service.inspect(request) + assert observed.state == "missing" + + with pytest.raises(PaneError) as caught: + service.ensure(request) + assert caught.value.effect == first_effect + assert len(native.created) == 1 + + if first_effect == "unknown": + assert store[request.binding_key]["creation"]["state"] == "reserved" + fenced = service.ensure(request) + assert fenced.state == "unknown" + assert len(native.created) == 1 + else: + assert "creation" not in store[request.binding_key] + assert store[request.binding_key]["status"]["state"] == "missing" + assert store[request.binding_key]["status"]["operation_id"] == "completed-before-reboot" + with pytest.raises(PaneError) as retry: + service.ensure(request) + assert retry.value.effect == "unknown" + assert len(native.created) == 2 + assert store[request.binding_key]["creation"]["state"] == "reserved" + + +def test_ready_reuse_preserves_completion_proof_for_one_replacement( + tmp_path: Path, herdr_request: PaneRequest, +) -> None: + request = herdr_request + previous = PaneBinding( + name=request.name, binding_key=request.binding_key, backend="herdr", + endpoint=request.endpoint, parent_id=request.parent_id, pane_id="old-pane", + repository=request.repository, controller_id=request.controller_id, + workspace_id=request.workspace_id, tab_id="old-tab", terminal_id="old-terminal", + pane_tty="/dev/pts/200", shell_pid=900001, shell_path="/bin/bash", + machine_generation="old-generation", cwd="/work/previous", state="ready", + ) + replacement = PaneBinding( + name=request.name, binding_key=request.binding_key, backend="herdr", + endpoint=request.endpoint, parent_id=request.parent_id, pane_id="new-pane", + repository=request.repository, controller_id=request.controller_id, + workspace_id=request.workspace_id, tab_id="new-tab", terminal_id="new-terminal", + pane_tty="/dev/pts/201", shell_pid=900002, shell_path="/bin/bash", + machine_generation="new-generation", cwd=request.cwd, state="ready", + ) + state_dir, store, _ready, _receipt = _completed_prior_state(tmp_path, request, previous) + + class Native: + def __init__(self) -> None: + self.current: PaneBinding | None = previous + self.created: list[PaneBinding | None] = [] + + def inspect( + self, _request: PaneRequest, *, previous_binding: PaneBinding | None = None, + ) -> PaneStatus: + if self.current is None: + if previous_binding is not None: + return PaneStatus("missing", request.backend, request.binding_key, request.name) + return PaneStatus("unknown", request.backend, request.binding_key, request.name, + detail="ordinary inspection cannot prove the missing generation") + return PaneStatus("ready", request.backend, request.binding_key, request.name, + binding=self.current) + + def create( + self, _request: PaneRequest, *, previous_binding: PaneBinding | None = None, + ) -> PaneBinding: + self.created.append(previous_binding) + self.current = replacement + return replacement + + native = Native() + service = ManagedPaneService(store, {"herdr": native}, state_dir=state_dir) + + reused = service.ensure(request) + assert reused.state == "ready" + assert reused.binding == previous + assert store[request.binding_key]["operation"]["state"] == "completed" + assert store[request.binding_key]["status"]["operation_id"] == "completed-before-reboot" + + native.current = None + missing = service.inspect(request) + assert missing.state == "missing" + assert not native.created + assert store[request.binding_key]["operation"]["state"] == "completed" + + created = service.ensure(request) + assert created.state == "ready" + assert created.binding == replacement + assert len(native.created) == 1 + assert native.created[0] is not None + assert native.created[0].pane_id == previous.pane_id + + reused_replacement = service.ensure(request) + assert reused_replacement.state == "ready" + assert reused_replacement.binding == replacement + assert len(native.created) == 1 + + +def test_replacement_never_uses_prior_binding_for_a_changed_stored_request( + tmp_path: Path, herdr_request: PaneRequest, +) -> None: + request = herdr_request + previous = PaneBinding( + name=request.name, binding_key=request.binding_key, backend="herdr", + endpoint=request.endpoint, parent_id=request.parent_id, pane_id="old-pane", + repository=request.repository, controller_id=request.controller_id, + workspace_id=request.workspace_id, tab_id="old-tab", terminal_id="old-terminal", + pane_tty="/dev/pts/200", shell_pid=900001, shell_path="/bin/bash", + machine_generation="old-generation", cwd="/work/previous", state="ready", + ) + state_dir, store, _ready, _receipt = _completed_prior_state(tmp_path, request, previous) + store[request.binding_key]["request"]["cwd"] = "/different" + + class Native: + def __init__(self) -> None: + self.inspected: list[PaneBinding | None] = [] + + def inspect( + self, _request: PaneRequest, *, previous_binding: PaneBinding | None = None, + ) -> PaneStatus: + self.inspected.append(previous_binding) + return PaneStatus("unknown", request.backend, request.binding_key, request.name) + + native = Native() + service = ManagedPaneService(store, {"herdr": native}, state_dir=state_dir) + + assert service.inspect(request).state == "unknown" + assert native.inspected == [None, None] + + +@pytest.mark.parametrize(("proof_succeeds", "expected"), [(True, "missing"), (False, "unknown")]) +def test_herdr_adapter_requires_layout_proof_for_previous_binding( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, herdr_request: PaneRequest, + proof_succeeds: bool, expected: str, +) -> None: + request = herdr_request + previous = PaneBinding( + name=request.name, binding_key=request.binding_key, backend="herdr", + endpoint=request.endpoint, parent_id=request.parent_id, pane_id="old-pane", + repository=request.repository, controller_id=request.controller_id, + workspace_id=request.workspace_id, tab_id="old-tab", terminal_id="old-terminal", + pane_tty="/dev/pts/200", shell_pid=900001, shell_path="/bin/bash", + machine_generation="old-generation", cwd="/work/previous", state="ready", + ) + adapter = backends.HerdrAdapter(state_dir=tmp_path / "state") + monkeypatch.setattr(adapter, "_endpoint", lambda _request: request.endpoint) + + def missing_owner(_request: PaneRequest): + raise HerdrOpsError("managed_pane_missing", "old pane missing", effect="unknown") + + proof_calls = [] + + def verify(*args, **kwargs): + proof_calls.append((args, kwargs)) + if not proof_succeeds: + raise HerdrOpsError("inventory_unknown", "inventory is not complete") + + monkeypatch.setattr(adapter, "_owned", missing_owner) + monkeypatch.setattr(backends.herdr_layout, "verify_missing_managed_pane", verify) + + status = adapter.inspect(request, previous_binding=previous) + + assert status.state == expected + assert len(proof_calls) == 1 + + +@pytest.mark.parametrize("marker_state", ["creating", "unknown"]) +def test_herdr_adapter_does_not_adopt_an_unresolved_replacement_owner( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, herdr_request: PaneRequest, + marker_state: str, +) -> None: + request = herdr_request + adapter = backends.HerdrAdapter(state_dir=tmp_path / "state") + monkeypatch.setattr(adapter, "_endpoint", lambda _request: request.endpoint) + monkeypatch.setattr(adapter, "_owned", lambda _request: ( + {"replacement": {"state": marker_state}}, + {"pane_id": "new-pane", "workspace_id": request.workspace_id}, + )) + + status = adapter.inspect(request) + + assert status.state == "unknown" + assert "replacement is reserved" in (status.detail or "") + + +@pytest.fixture +def reserved_creation(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, herdr_request: PaneRequest): + request = herdr_request + state = tmp_path / "state" + state.mkdir(mode=0o700) + store = FilePaneStore(state) + runtime = state / "terminal_ops" / request.binding_key + runtime.mkdir(mode=0o700) + (runtime / "bashrc").write_text("# bootstrap\n", encoding="utf-8") + token = "a" * 32 + status = PaneStatus("unknown", "herdr", request.binding_key, request.name) + store.put(request.binding_key, { + "schema_version": 1, "api_version": 1, "binding_key": request.binding_key, + "request": request.to_dict(), "status": status.to_dict(), + "creation": {"state": "reserved", "token": token}, + }) + original = store._path(request.binding_key).read_bytes() + owner_id = request.binding_key[:32] + owners = state / "panes" + owners.mkdir(mode=0o700) + owner = { + "schema_version": 1, "owner_id": owner_id, "purpose": "managed_ops", + "endpoint": request.endpoint, "workspace_id": request.workspace_id, + "caller_pane_id": request.parent_id, "pane_id": "child", "terminal_id": "terminal", + "created_at": "2026-01-01T00:00:00Z", "before_layout": {"tab_id": "tab"}, + "tab_id": "tab", "cwd": request.cwd, "ratio": 1 / 3, "state": "owned", + "owner_metadata": backends._managed_owner_metadata(request), + } + owner_path = owners / f"{owner_id}.json" + owner_path.write_text(json.dumps(owner), encoding="utf-8") + owner_path.chmod(0o600) + original_owner = owner_path.read_bytes() + parent = {"pane_id": request.parent_id, "terminal_id": "parent-terminal", + "workspace_id": request.workspace_id, "tab_id": "tab"} + child = {"pane_id": "child", "terminal_id": "terminal", + "workspace_id": request.workspace_id, "tab_id": "tab", "label": request.name} + native = {"panes": [parent, child], "closes": 0, "lost": False, "remove": True} + + def native_request(_endpoint, method, params=None, **_kwargs): + if method == "session.snapshot": + return {"snapshot": {"panes": list(native["panes"])}} + if method == "pane.close": + assert params == {"pane_id": "child"} + native["closes"] += 1 + if native["remove"]: + native["panes"] = [parent] + if native["lost"]: + raise HerdrOpsError("response_lost", "lost", "unknown") + return {"type": "pane_closed"} + if method == "pane.get": + return {"pane": next(pane for pane in native["panes"] if pane["pane_id"] == params["pane_id"])} + if method == "pane.layout": + return {"layout": {"tab_id": "tab", "workspace_id": request.workspace_id}} + if method == "pane.split": + pane = {"pane_id": "new-child", "terminal_id": "new-terminal", + "workspace_id": request.workspace_id, "tab_id": "tab"} + native["panes"].append(pane) + return {"pane": pane} + raise AssertionError(method) + + monkeypatch.setattr(backends.herdr_layout, "request", native_request) + adapter = backends.HerdrAdapter(state_dir=state) + monkeypatch.setattr(adapter, "_endpoint", lambda _request: request.endpoint) + monkeypatch.setattr(adapter, "_retirement_observation", lambda _request, _pane: { + "shell_pid": 123, "pane_tty": "/dev/pts/1", "shell_starttime": 42, + "server_pid": 7, "machine_generation": "boot", + }) + service = ManagedPaneService(store, {"herdr": adapter}, state_dir=state) + args = {"retirement_id": "retire-1", "expected_creation_token": token, + "expected_pane_id": "child", "expected_terminal_id": "terminal", + "discard_uncontrolled_shell": True} + return service, store, owner_path, request, args, native, original, original_owner + + +class RawFlockPaneStore(FilePaneStore): + """Store whose lock exposes raw nonblocking flock errors.""" + + @contextmanager + def lock(self, key): + with self._path(key).with_suffix(".lock").open("a+") as stream: + fcntl.flock(stream.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + try: + yield + finally: + fcntl.flock(stream.fileno(), fcntl.LOCK_UN) + + +def test_reserved_creation_retirement_archives_and_is_idempotent(reserved_creation): + service, store, owner_path, request, args, native, original, original_owner = reserved_creation + result = service.retire_creation(request, **args) + assert result["state"] == "retired" + assert result["changed"] is True + assert result["pane_id"] == "child" and result["terminal_id"] == "terminal" + assert native["closes"] == 1 + owner = json.loads(owner_path.read_text(encoding="utf-8")) + marker = owner["retirement"] + assert (owner_path.parent.parent / marker["owner_archive"]).read_bytes() == original_owner + assert (owner_path.parent.parent / marker["reservation_archive"]).read_bytes() == original + assert store.get(request.binding_key)["creation"]["token"] == args["expected_creation_token"] + again = service.retire_creation(request, **args) + assert again["state"] == "retired" and again["changed"] is False + assert native["closes"] == 1 + + +@pytest.mark.parametrize("remove", [False, True]) +def test_retirement_lost_close_response_reconciles_without_replay(reserved_creation, remove): + service, _store, _owner_path, request, args, native, _original, _original_owner = reserved_creation + native["lost"] = True + native["remove"] = remove + result = service.retire_creation(request, **args) + assert result["state"] == "unknown" + result = service.reconcile_creation_retirement(request, args["retirement_id"]) + assert result["state"] == ("retired" if remove else "unknown") + service.retire_creation(request, **args) + assert native["closes"] == 1 + + +@pytest.mark.parametrize("conflict", ["token", "terminal", "operation", "lease", "duplicate"]) +def test_retirement_rejects_unqualified_reservation(reserved_creation, conflict): + service, store, owner_path, request, args, native, _original, _original_owner = reserved_creation + if conflict == "token": + args["expected_creation_token"] = "b" * 32 + elif conflict == "terminal": + args["expected_terminal_id"] = "other-terminal" + elif conflict == "operation": + record = store.get(request.binding_key) + record["operation"] = {"state": "unknown"} + store.put(request.binding_key, record) + elif conflict == "lease": + (service.state_dir / "terminal_ops" / request.binding_key / "writer-lease").mkdir() + else: + other = owner_path.parent / "another.json" + other.write_bytes(owner_path.read_bytes()) + other.chmod(0o600) + with pytest.raises(PaneError): + service.retire_creation(request, **args) + assert native["closes"] == 0 + + +def test_retirement_respects_binding_writer_lock(reserved_creation): + service, store, _owner_path, request, args, native, _original, _original_owner = reserved_creation + with store.lock(request.binding_key), pytest.raises(PaneError) as error: + service.retire_creation(request, **args) + assert error.value.code == "pane_busy" + assert native["closes"] == 0 + + +def test_retirement_normalizes_raw_store_lock_contention_before_close(reserved_creation): + service, store, owner_path, request, args, native, original, original_owner = reserved_creation + service.store = RawFlockPaneStore(service.state_dir) + lock_path = store._path(request.binding_key).with_suffix(".lock") + with lock_path.open("a+") as held: + fcntl.flock(held.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + with pytest.raises(PaneError) as error: + service.retire_creation(request, **args) + assert error.value.code == "pane_busy" + assert error.value.effect == "none" + assert isinstance(error.value.__cause__, BlockingIOError) + assert native["closes"] == 0 + assert store._path(request.binding_key).read_bytes() == original + assert owner_path.read_bytes() == original_owner + + +def test_retirement_normalizes_store_lock_factory_contention(reserved_creation, monkeypatch): + service, store, owner_path, request, args, native, original, original_owner = reserved_creation + failure = BlockingIOError("lock factory") + + def fail_lock(_key): + raise failure + + monkeypatch.setattr(store, "lock", fail_lock) + with pytest.raises(PaneError) as error: + service.retire_creation(request, **args) + assert error.value.code == "pane_busy" + assert error.value.effect == "none" + assert error.value.__cause__ is failure + assert native["closes"] == 0 + assert store._path(request.binding_key).read_bytes() == original + assert owner_path.read_bytes() == original_owner + + +def test_raw_store_lock_does_not_normalize_operation_body_error(reserved_creation, monkeypatch): + service, _store, _owner_path, request, args, native, _original, _original_owner = reserved_creation + store = RawFlockPaneStore(service.state_dir) + service.store = store + failure = BlockingIOError("operation body") + + def fail_get(_key): + raise failure + + monkeypatch.setattr(store, "get", fail_get) + with pytest.raises(BlockingIOError) as error: + service.retire_creation(request, **args) + assert error.value is failure + assert native["closes"] == 0 + with store.lock(request.binding_key): + pass + + +def test_raw_store_lock_does_not_normalize_cleanup_error_after_close(reserved_creation): + service, _store, _owner_path, request, args, native, _original, _original_owner = reserved_creation + failure = BlockingIOError("lock cleanup") + + class CleanupFailureStore(RawFlockPaneStore): + @contextmanager + def lock(self, key): + with super().lock(key): + yield + raise failure + + service.store = CleanupFailureStore(service.state_dir) + with pytest.raises(BlockingIOError) as error: + service.retire_creation(request, **args) + assert error.value is failure + assert native["closes"] == 1 + + +def test_completed_retirement_allows_new_generation(reserved_creation, monkeypatch): + service, store, owner_path, request, args, native, _original, _original_owner = reserved_creation + assert service.retire_creation(request, **args)["state"] == "retired" + adapter = service.adapters["herdr"] + binding = PaneBinding( + name=request.name, binding_key=request.binding_key, backend="herdr", + endpoint=request.endpoint, parent_id=request.parent_id, pane_id="new-child", + repository=request.repository, controller_id=request.controller_id, + workspace_id=request.workspace_id, terminal_id="new-terminal", tab_id="tab", + pane_tty="/dev/pts/2", shell_pid=456, shell_path="/bin/bash", + machine_generation="boot", cwd=request.cwd, state="ready", + ) + + def create(_request, *, retirement_proof): + result = backends.herdr_layout.ensure_owned_pane( + request.endpoint, request.workspace_id, request.parent_id, request.cwd, + request.binding_key[:32], state_dir=service.state_dir, ratio=1 / 3, + purpose="managed_ops", owner_metadata=backends._managed_owner_metadata(request), + retirement_proof=retirement_proof, + ) + assert result["pane"]["pane_id"] == binding.pane_id + return binding + + monkeypatch.setattr(adapter, "create", create) + monkeypatch.setattr(adapter, "inspect", lambda _request: PaneStatus( + "ready", "herdr", request.binding_key, request.name, binding=binding, + )) + result = service.ensure(request) + assert result.state == "ready" and result.binding.pane_id == "new-child" + assert native["closes"] == 1 + assert json.loads(owner_path.read_text(encoding="utf-8"))["retired_creation"]["state"] == "retired" + assert store.get(request.binding_key)["retirement"]["state"] == "retired" + + +def test_changed_shell_generation_fences_without_close(reserved_creation, monkeypatch): + service, _store, _owner_path, request, args, native, _original, _original_owner = reserved_creation + adapter = service.adapters["herdr"] + observations = iter([{"shell_pid": 123, "shell_starttime": 1}, + {"shell_pid": 123, "shell_starttime": 2}]) + monkeypatch.setattr(adapter, "_retirement_observation", lambda *_: next(observations)) + assert service.retire_creation(request, **args)["state"] == "unknown" + assert service.retire_creation(request, **args)["state"] == "unknown" + assert native["closes"] == 0 + assert service.ensure(request).state == "unknown" + + +def test_reservation_change_during_qualification_refuses_before_fence(reserved_creation, monkeypatch): + service, store, _owner_path, request, args, native, _original, _original_owner = reserved_creation + adapter = service.adapters["herdr"] + + def change_reservation(*_): + record = store.get(request.binding_key) + record["operation"] = {"state": "unknown"} + store.put(request.binding_key, record) + return {"shell_pid": 123, "shell_starttime": 1} + + monkeypatch.setattr(adapter, "_retirement_observation", change_reservation) + with pytest.raises(PaneError) as error: + service.retire_creation(request, **args) + assert error.value.code == "retirement_reservation_mismatch" + assert native["closes"] == 0 + assert "retirement" not in store.get(request.binding_key) + + +def test_retirement_refuses_other_backends_before_native_work(reserved_creation): + service, _store, _owner_path, request, args, native, _original, _original_owner = reserved_creation + tmux = PaneRequest("tmux", request.endpoint, request.repository, request.controller_id, + request.parent_id, request.cwd, session_id="session") + with pytest.raises(PaneError) as error: + service.retire_creation(tmux, **args) + assert error.value.code == "retirement_backend_unsupported" + assert native["closes"] == 0 + + +def test_retirement_refuses_changed_archive_without_replay(reserved_creation): + service, store, owner_path, request, args, native, _original, _original_owner = reserved_creation + native["lost"] = True + assert service.retire_creation(request, **args)["state"] == "unknown" + owner = json.loads(owner_path.read_text(encoding="utf-8")) + archive = owner_path.parent.parent / owner["retirement"]["owner_archive"] + archive.write_text("{}", encoding="utf-8") + result = service.reconcile_creation_retirement(request, args["retirement_id"]) + assert result["state"] == "unknown" + assert store.get(request.binding_key)["retirement"]["state"] == "unknown" + assert native["closes"] == 1 diff --git a/ls/tests/test_herdr_ops_layout.py b/ls/tests/test_herdr_ops_layout.py index 60b09f305..d166bb6e0 100644 --- a/ls/tests/test_herdr_ops_layout.py +++ b/ls/tests/test_herdr_ops_layout.py @@ -1,5 +1,6 @@ from __future__ import annotations +import hashlib import json import os from pathlib import Path @@ -8,6 +9,7 @@ from ls.core.herdr_ops import layout from ls.core.herdr_ops.errors import HerdrOpsError +from ls.core.terminal_ops import shell as terminal_shell ENDPOINT = "/tmp/herdr-fixture.sock" @@ -21,6 +23,267 @@ def layout_snapshot(workspace: str = "w1", tab: str = "w1:t1"): return {"workspace_id": workspace, "tab_id": tab, "zoomed": False, "area": {"x": 0, "y": 0, "width": 100, "height": 40}, "focused_pane_id": "w1:p1", "panes": [], "splits": []} +def _replacement_fixture(state: Path): + owner_id = "a" * 32 + binding_key = owner_id + "b" * 32 + owner_metadata = { + "api_version": 1, + "binding_key": binding_key, + "repository": "project", + "controller_id": "controller", + "host_alias": None, + "parent_id": "w1:p1", + "workspace_id": "w1", + "machine_id": "machine", + } + owner = { + "schema_version": 1, + "owner_id": owner_id, + "purpose": "managed_ops", + "endpoint": str(Path(ENDPOINT).absolute()), + "workspace_id": "w1", + "caller_pane_id": "w1:p1", + "pane_id": "w1:p8", + "terminal_id": "old-terminal", + "tab_id": "w1:t1", + "cwd": "/work/project", + "ratio": 1 / 3, + "state": "owned", + "owner_metadata": owner_metadata, + } + layout._write_owner(state, owner) + owner_path = state / "panes" / f"{owner_id}.json" + previous_binding = { + "name": "project-sudo", + "binding_key": binding_key, + "backend": "herdr", + "endpoint": ENDPOINT, + "parent_id": "w1:p1", + "pane_id": "w1:p8", + "terminal_id": "old-terminal", + "repository": "project", + "controller_id": "controller", + "host_alias": None, + "workspace_id": "w1", + "tab_id": "w1:t1", + "pane_tty": "/dev/pts/200", + "shell_pid": 900001, + "shell_path": "/bin/bash", + "machine_generation": "old-generation", + "cwd": "/work/after-command", + "state": "ready", + "metadata": {}, + } + return owner_id, owner_metadata, previous_binding, owner_path + + +def _missing_generation_snapshot(*extra_panes): + return { + "type": "session_snapshot", + "snapshot": { + "panes": [pane("w1:p1", terminal="parent-terminal"), *extra_panes], + }, + } + + +def test_verify_missing_managed_generation_requires_unique_complete_absence_and_is_read_only( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + state = tmp_path / "state" + owner_id, metadata, previous, owner_path = _replacement_fixture(state) + original = owner_path.read_bytes() + monkeypatch.setattr(terminal_shell, "machine_generation", lambda: "new-generation") + + def snapshot_request(_endpoint, method, *_args, **_kwargs): + if method != "session.snapshot": + raise AssertionError(method) + return _missing_generation_snapshot() + monkeypatch.setattr(layout, "request", snapshot_request) + + layout.verify_missing_managed_pane( + ENDPOINT, "w1", "w1:p1", "/work/project", owner_id, previous, + state_dir=state, ratio=1 / 3, owner_metadata=metadata, + ) + + assert owner_path.read_bytes() == original + + +def test_verify_missing_managed_generation_rejects_reappearing_terminal_identity( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + state = tmp_path / "state" + owner_id, metadata, previous, _owner_path = _replacement_fixture(state) + monkeypatch.setattr(terminal_shell, "machine_generation", lambda: "new-generation") + + def snapshot_request(_endpoint, method, *_args, **_kwargs): + if method != "session.snapshot": + raise AssertionError(method) + return _missing_generation_snapshot(pane("w1:p9", terminal="old-terminal")) + monkeypatch.setattr(layout, "request", snapshot_request) + + with pytest.raises(HerdrOpsError) as caught: + layout.verify_missing_managed_pane( + ENDPOINT, "w1", "w1:p1", "/work/project", owner_id, previous, + state_dir=state, ratio=1 / 3, owner_metadata=metadata, + ) + + assert caught.value.code == "replacement_generation_present" + + +@pytest.mark.parametrize(("pid_exists", "expected"), [(False, None), (True, "shell_generation_live")]) +def test_verify_missing_requires_old_pid_absence_when_machine_generation_matches( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, pid_exists: bool, expected: str | None, +) -> None: + state = tmp_path / "state" + owner_id, metadata, previous, _owner_path = _replacement_fixture(state) + monkeypatch.setattr(terminal_shell, "machine_generation", lambda: "old-generation") + + def stat_process(path, *args, **kwargs): + if os.fspath(path) == f"/proc/{previous['shell_pid']}": + if pid_exists: + return os.stat_result((0,) * 10) + raise FileNotFoundError(os.fspath(path)) + return original_stat(path, *args, **kwargs) + + original_stat = os.stat + monkeypatch.setattr(os, "stat", stat_process) + + def snapshot_request(_endpoint, method, *_args, **_kwargs): + if method != "session.snapshot": + raise AssertionError(method) + return _missing_generation_snapshot() + monkeypatch.setattr(layout, "request", snapshot_request) + + if expected is None: + layout.verify_missing_managed_pane( + ENDPOINT, "w1", "w1:p1", "/work/project", owner_id, previous, + state_dir=state, ratio=1 / 3, owner_metadata=metadata, + ) + else: + with pytest.raises(HerdrOpsError) as caught: + layout.verify_missing_managed_pane( + ENDPOINT, "w1", "w1:p1", "/work/project", owner_id, previous, + state_dir=state, ratio=1 / 3, owner_metadata=metadata, + ) + assert caught.value.code == expected + + +@pytest.mark.parametrize( + ("first_failure", "expected_effect", "retry_succeeds"), + [("none", "none", True), ("unknown", "unknown", False), ("scope", "unknown", False)], +) +def test_replacement_archives_before_split_and_retries_only_known_no_effect( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, first_failure: str, + expected_effect: str, retry_succeeds: bool, +) -> None: + state = tmp_path / "state" + owner_id, metadata, previous, owner_path = _replacement_fixture(state) + original = owner_path.read_bytes() + calls: list[str] = [] + split_attempts = 0 + + def fake_request(_endpoint, method, params=None, **_kwargs): + nonlocal split_attempts + calls.append(method) + if method == "session.snapshot": + return _missing_generation_snapshot() + if method == "pane.get": + pane_id = params["pane_id"] + if pane_id == "w1:p1": + return {"pane": pane(pane_id, terminal="parent-terminal", label="parent")} + if pane_id == "w1:p9": + return {"pane": pane(pane_id, terminal="new-terminal", label="project-sudo")} + raise HerdrOpsError("not_found", "pane not found") + if method == "pane.layout": + return {"layout": layout_snapshot()} + if method == "pane.split": + split_attempts += 1 + if split_attempts == 1: + if first_failure == "scope": + return {"pane": pane("w1:p9", workspace="w2", terminal="new-terminal")} + raise HerdrOpsError("split_result", "first split attempt", first_failure) + return {"pane": pane("w1:p9", terminal="new-terminal", label="project-sudo")} + raise AssertionError(method) + + monkeypatch.setattr(layout, "request", fake_request) + monkeypatch.setattr(terminal_shell, "machine_generation", lambda: "new-generation") + + with pytest.raises(HerdrOpsError) as caught: + layout.ensure_owned_pane( + ENDPOINT, "w1", "w1:p1", "/work/project", owner_id, + state_dir=state, ratio=1 / 3, purpose="managed_ops", + owner_metadata=metadata, previous_binding=previous, + ) + assert caught.value.effect == expected_effect + failed = json.loads(owner_path.read_text(encoding="utf-8")) + assert failed["state"] == ("failed" if expected_effect == "none" else "unknown") + assert failed["replacement"]["state"] == ("failed" if expected_effect == "none" else "unknown") + generation_id = failed["generation_id"] + archive_path = state / failed["replacement"]["owner_archive"] + assert archive_path.read_bytes() == original + assert failed["replacement"]["previous_owner_sha256"] == hashlib.sha256(original).hexdigest() + + if retry_succeeds: + result = layout.ensure_owned_pane( + ENDPOINT, "w1", "w1:p1", "/work/project", owner_id, + state_dir=state, ratio=1 / 3, purpose="managed_ops", + owner_metadata=metadata, previous_binding=previous, + ) + assert result["pane"]["pane_id"] == "w1:p9" + owned = json.loads(owner_path.read_text(encoding="utf-8")) + assert owned["generation_id"] == generation_id + assert owned["replacement"]["state"] == "owned" + assert owned["replacement"]["owner_archive"] == failed["replacement"]["owner_archive"] + assert split_attempts == 2 + else: + with pytest.raises(HerdrOpsError) as repeated: + layout.ensure_owned_pane( + ENDPOINT, "w1", "w1:p1", "/work/project", owner_id, + state_dir=state, ratio=1 / 3, purpose="managed_ops", + owner_metadata=metadata, previous_binding=previous, + ) + assert repeated.value.code == "replacement_owner_conflict" + assert split_attempts == 1 + + +def test_replacement_reservation_write_failure_is_unknown_and_never_splits( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + state = tmp_path / "state" + owner_id, metadata, previous, owner_path = _replacement_fixture(state) + original = owner_path.read_bytes() + calls: list[str] = [] + + def fake_request(_endpoint, method, params=None, **_kwargs): + calls.append(method) + if method == "session.snapshot": + return _missing_generation_snapshot() + if method == "pane.get": + if params["pane_id"] == "w1:p1": + return {"pane": pane("w1:p1", terminal="parent-terminal", label="parent")} + raise HerdrOpsError("not_found", "pane not found") + if method == "pane.layout": + return {"layout": layout_snapshot()} + raise AssertionError(method) + + def fail_owner_write(_root, _record): + raise HerdrOpsError("owner_write_failed", "simulated owner reservation failure") + + monkeypatch.setattr(layout, "request", fake_request) + monkeypatch.setattr(terminal_shell, "machine_generation", lambda: "new-generation") + monkeypatch.setattr(layout, "_write_owner", fail_owner_write) + + with pytest.raises(HerdrOpsError) as caught: + layout.ensure_owned_pane( + ENDPOINT, "w1", "w1:p1", "/work/project", owner_id, + state_dir=state, ratio=1 / 3, purpose="managed_ops", + owner_metadata=metadata, previous_binding=previous, + ) + assert caught.value.effect == "unknown" + assert "pane.split" not in calls + assert owner_path.read_bytes() == original + + def test_create_uses_explicit_scope_down_ratio_cwd_and_no_focus(tmp_path: Path, monkeypatch): calls = [] @@ -54,6 +317,30 @@ def fake_request(endpoint, method, params=None, **kwargs): assert os.stat(record_path).st_mode & 0o777 == 0o600 +def test_managed_split_ratio_sizes_child_and_preserves_owner_contract(tmp_path: Path, monkeypatch): + calls = [] + + def fake_request(endpoint, method, params=None, **kwargs): + calls.append((method, params)) + if method == "pane.get": + return {"pane": pane(params["pane_id"], terminal="caller")} + if method == "pane.layout": + return {"layout": layout_snapshot()} + if method == "pane.split": + return {"pane": pane("w1:p2", terminal="new-terminal", tab="w1:t1")} + raise AssertionError(method) + + monkeypatch.setattr(layout, "request", fake_request) + result = layout.create_owned_pane( + ENDPOINT, "w1", "w1:p1", "/work/project", state_dir=tmp_path, + purpose="managed_ops", owner_metadata={"parent_id": "w1:p1"}, ratio=1 / 3, + ) + split = next(params for method, params in calls if method == "pane.split") + assert split["ratio"] == pytest.approx(2 / 3) + record = json.loads((tmp_path / "panes" / f"{result['owner_id']}.json").read_text()) + assert record["ratio"] == 1 / 3 + + def test_create_refuses_protected_sudo_pane_before_any_mutation(tmp_path: Path, monkeypatch): calls = [] diff --git a/ls/tests/test_openpgp_native_dialog.py b/ls/tests/test_openpgp_native_dialog.py index 6c156a230..7552ec808 100644 --- a/ls/tests/test_openpgp_native_dialog.py +++ b/ls/tests/test_openpgp_native_dialog.py @@ -27,7 +27,7 @@ expected_expiry_date, ) from ls.core.openpgp.native_input import ( - NativeInputContext, describe_enrollment_operation, describe_open_operation, + NativeInputContext, NativeInputError, describe_enrollment_operation, describe_open_operation, describe_seal_operation, describe_generation_operation, describe_seal_file_operation, describe_open_file_operation, ) @@ -46,7 +46,7 @@ def _private_fixture(): @pytest.mark.skipif(not shutil.which("tmux") or not shutil.which("gpg") or not Path("/proc/self/stat").exists(), reason="native Linux tmux and GnuPG are required") -def test_native_passphrase_round_trip_and_certificate_enrollment() -> None: +def test_native_passphrase_round_trip_and_certificate_enrollment(monkeypatch, native_popup_diagnostics) -> None: root = Path(__file__).resolve().parents[2] with _private_fixture() as work: backend = TmuxCommandBackend(config_file="/dev/null", socket_root=work / "sockets") @@ -61,6 +61,33 @@ def test_native_passphrase_round_trip_and_certificate_enrollment() -> None: outer, home = work / "viewer.sock", work / "key-home" canary = "NativeOpenPGPCanary9" resolver = SecretResolver(env_values={}) + input_diagnostics: dict[str, object] = {} + original_request = NativeInputContext.request_passphrase + original_dialog = service.dialog + + def observed_request(context, operation): + input_diagnostics["request_entered"] = True + try: + return original_request(context, operation) + except NativeInputError as exc: + input_diagnostics["request_error"] = exc.code + raise + + def observed_dialog(*args, **kwargs): + input_diagnostics["dialog_entered"] = True + try: + result = original_dialog(*args, **kwargs) + except ShellCommandError as exc: + input_diagnostics.update(dialog_error=exc.code, effect=exc.effect) + raise + input_diagnostics["dialog_result"] = { + key: result.get(key) + for key in ("state", "effect_certainty", "overlay_state", "cleanup_pending") + } + return result + + monkeypatch.setattr(NativeInputContext, "request_passphrase", observed_request) + monkeypatch.setattr(service, "dialog", observed_dialog) handle = None runner = None viewer_server = None @@ -125,6 +152,8 @@ def primary_ready(): def perform(title, descriptor, value, consume, *, confirmation=False, more_values=(), absent=None): nonlocal runner result = [] + input_diagnostics.clear() + native_popup_diagnostics.clear() context = NativeInputContext(service=service, handle=handle, owner=owner, operation_id=descriptor.purpose, expected=descriptor, deadline=time.monotonic() + 25, validate_current=lambda: True) @@ -141,7 +170,9 @@ def invoke(): eventually(lambda: title in tmux("capture-pane", "-p", "-t", pane)) except AssertionError: raise AssertionError({"title": title, "result_types": [type(item).__name__ for item in result], - "codes": [str(getattr(item, "code", "")) for item in result]}) from None + "codes": [str(getattr(item, "code", "")) for item in result], + "native_input": input_diagnostics, + "popup_boundary": native_popup_diagnostics}) from None tmux("send-keys", "-t", pane, "-l", value) if absent is not None: assert not absent.exists() and not list(absent.parent.glob(".ls-openpgp-*")) diff --git a/ls/tests/test_sdk_build.py b/ls/tests/test_sdk_build.py index 106eb3f4f..fb8371f48 100644 --- a/ls/tests/test_sdk_build.py +++ b/ls/tests/test_sdk_build.py @@ -8,6 +8,7 @@ import sys import tarfile import tomllib +import zipfile import pytest from setuptools import Distribution @@ -86,6 +87,20 @@ def test_reused_build_output_rejects_substitution(tmp_path, mutation): build.run() +def test_build_rejects_symlinked_askpass_output_without_changing_target(tmp_path): + build = command(tmp_path) + tools = Path(build.build_lib) / "ls" / "tools" + tools.mkdir(parents=True) + victim = tmp_path / "victim" + victim.write_bytes(b"unchanged") + victim.chmod(0o600) + (tools / "sudo_askpass").symlink_to(victim) + with pytest.raises(ValueError, match="Askpass build destination"): + build.run() + assert victim.read_bytes() == b"unchanged" + assert victim.stat().st_mode & 0o777 == 0o600 + + def test_editable_framework_build_does_not_supply_sdk(tmp_path): build = command(tmp_path) build.editable_mode = True @@ -272,6 +287,12 @@ def test_installed_wheel_normal_global_preset_materializes_bootstrap_references( fixture = tmp_path / "source" fixture.mkdir() _copy_sdist_fixture(ROOT, fixture) + # The fixture normally hardlinks source files; detach before changing its mode. + askpass_source = fixture / "ls" / "tools" / "sudo_askpass" + askpass_bytes = askpass_source.read_bytes() + askpass_source.unlink() + askpass_source.write_bytes(askpass_bytes) + askpass_source.chmod(0o777) wheel_dir = tmp_path / "wheel" wheel_dir.mkdir() @@ -300,6 +321,10 @@ def test_installed_wheel_normal_global_preset_materializes_bootstrap_references( ) wheel_path = wheel_dir / built.stdout.strip().splitlines()[-1] assert wheel_path.is_file(), built.stdout + built.stderr + with zipfile.ZipFile(wheel_path) as wheel: + helper = wheel.getinfo("ls/tools/sudo_askpass") + assert (helper.external_attr >> 16) & 0o777 == 0o755 + assert wheel.read(helper) == askpass_bytes home = tmp_path / "home" target = tmp_path / "target" @@ -326,11 +351,22 @@ def test_installed_wheel_normal_global_preset_materializes_bootstrap_references( installed_root = tmp_path / "installed" uv = shutil.which("uv") assert uv is not None, "the repository test environment requires uv to install the wheel" + cache = tmp_path / "uv-cache" + cache.mkdir(mode=0o700) + uv_env = { + key: value for key, value in os.environ.items() + if key in {"PATH", "LANG", "LC_ALL", "SYSTEMROOT"} + } + uv_env.update(HOME=str(home), UV_CACHE_DIR=str(cache), UV_PYTHON_DOWNLOADS="never") installed = subprocess.run( [ uv, + "--no-config", "pip", "install", + "--link-mode", + "copy", + "--offline", "--python", sys.executable, "--no-index", @@ -340,12 +376,32 @@ def test_installed_wheel_normal_global_preset_materializes_bootstrap_references( str(wheel_path), ], cwd=tmp_path, - env=install_env, + env=uv_env, capture_output=True, text=True, + umask=0o077, check=False, ) assert installed.returncode == 0, installed.stdout + installed.stderr + installed_askpass = installed_root / "ls" / "tools" / "sudo_askpass" + assert installed_askpass.read_bytes() == askpass_bytes + assert os.access(installed_askpass, os.X_OK) + assert installed_askpass.stat().st_mode & 0o022 == 0 + probe_env = install_env.copy() + probe_env["PYTHONPATH"] = str(installed_root) + subprocess.run( + [ + sys.executable, "-c", + "from pathlib import Path; from ls.core.terminal_ops.sudo import _askpass_executable; " + "import sys; assert Path(_askpass_executable()) == Path(sys.argv[1])", + str(installed_askpass), + ], + cwd=tmp_path, + env=probe_env, + capture_output=True, + text=True, + check=True, + ) profile_resources = ( "application-profiles/registry.json", diff --git a/ls/tests/test_shell_family.py b/ls/tests/test_shell_family.py index 5f299487a..32705418a 100644 --- a/ls/tests/test_shell_family.py +++ b/ls/tests/test_shell_family.py @@ -1472,7 +1472,8 @@ def _install_family_hook_trace(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) args = sys.argv[1:] def emit(kind, **fields): - row = json.dumps({{"kind": kind, **fields}}, sort_keys=True) + "\\n" + row = json.dumps({{"kind": kind, "monotonic_ns": time.monotonic_ns(), + "pid": os.getpid(), **fields}}, sort_keys=True) + "\\n" fd = os.open(TRACE, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) try: os.write(fd, row.encode()) @@ -1893,7 +1894,18 @@ def test_isolated_tmux_family_stacks_concurrent_operations( assert retained_pane["metadata"]["label"] == retained_result["pane_label"] before_geometry = [(pane["pane_id"], pane["height"]) for pane in final["windows"][0]["panes"]] for _ in range(2): - assert service.status(str(retained_result["operation_handle"]), owner=owner)["cleanup_state"] == "retained" + # Autonomous finalizers can briefly own the lifecycle lock. Only a + # pre-dispatch busy response is safe to retry; retention must hold + # for both successfully completed status observations. + for attempt in range(40): + try: + retained_status = service.status(str(retained_result["operation_handle"]), owner=owner) + break + except ShellCommandError as exc: + if exc.code != "busy" or exc.effect != "none" or attempt == 39: + raise + time.sleep(0.025) + assert retained_status["cleanup_state"] == "retained" after_geometry = [(pane["pane_id"], pane["height"]) for pane in backend.inspect_session(service._load(handle)[1])["windows"][0]["panes"]] assert after_geometry == before_geometry @@ -1922,7 +1934,85 @@ def test_isolated_tmux_family_stacks_concurrent_operations( viewer_drain_thread.start() fcntl.ioctl(viewer_slave, termios.TIOCSWINSZ, struct.pack("HHHH", 40, 100, 0, 0)) viewer_done = tmp_path / "human-viewer.exit" + # This child-local observer records only the exact attach child's wait + # result and cleanup verbs in the existing private hook trace. + viewer_observer = textwrap.dedent(f"""\ + import json, os, subprocess, time + from ls.core.terminal_ops.command_backend import TmuxCommandBackend + from ls.core.terminal_ops.command_service import ShellCommandService + def emit(kind, **fields): + try: + row = json.dumps(dict(kind=kind, monotonic_ns=time.monotonic_ns(), + pid=os.getpid(), **fields)) + "\\n" + fd = os.open({str(trace)!r}, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) + try: + os.write(fd, row.encode("ascii")) + finally: + os.close(fd) + except OSError: + pass + original_attach = TmuxCommandBackend.attach + original_popen = subprocess.Popen + original_checked = TmuxCommandBackend._checked + original_retirement = ShellCommandService._finish_retirement_locked + def observed_attach(self, record, *args, **kwargs): + def popen(*popen_args, **popen_kwargs): + child = original_popen(*popen_args, **popen_kwargs) + command = popen_args[0] if popen_args else popen_kwargs.get("args") + if (isinstance(command, (list, tuple)) and len(command) > 3 + and command[0] == (self.tmux_binary or "tmux") and command[1] == "-S" + and command[2] == str(record["endpoint"]) and command[3] == "attach-session"): + wait = child.wait + reported = False + def observed_wait(*wait_args, **wait_kwargs): + nonlocal reported + result = wait(*wait_args, **wait_kwargs) + if not reported: + reported = True + emit("attach_wait_return", child_pid=child.pid, raw_status=result) + return result + child.wait = observed_wait + emit("attach_spawn", child_pid=child.pid) + return child + subprocess.Popen = popen + emit("attach_start") + try: + result = original_attach(self, record, *args, **kwargs) + except BaseException as error: + emit("attach_exception", error=type(error).__name__) + raise + else: + emit("attach_return", raw_status=result) + return result + finally: + subprocess.Popen = original_popen + def observed_checked(self, endpoint, args, *extra, **kwargs): + verb = args[0] if args else None + if verb not in ("detach-client", "kill-server"): + return original_checked(self, endpoint, args, *extra, **kwargs) + emit("cleanup_dispatch", verb=verb) + try: + result = original_checked(self, endpoint, args, *extra, **kwargs) + except BaseException as error: + emit("cleanup_exception", verb=verb, error=type(error).__name__) + raise + emit("cleanup_return", verb=verb) + return result + def observed_retirement(self, *args, **kwargs): + emit("retirement_start") + try: + result = original_retirement(self, *args, **kwargs) + except BaseException as error: + emit("retirement_exception", error=type(error).__name__) + raise + emit("retirement_return") + return result + TmuxCommandBackend.attach = observed_attach + TmuxCommandBackend._checked = observed_checked + ShellCommandService._finish_retirement_locked = observed_retirement + """) attach_code = ( + f"exec({viewer_observer!r}); " "from pathlib import Path; from ls.core.terminal_ops.command_cli import main; " f"result=main(['--state-root',{str(state_root)!r},'attach','--session'," f"{str(created['native_session_name'])!r}],source_root=Path({str(SOURCE_ROOT)!r})," @@ -2108,21 +2198,54 @@ def test_isolated_tmux_family_stacks_concurrent_operations( assert closed_for_dispatch.value.code == "session_not_ready" assert process_info(viewer_pid) is not None client_name = backend._clients(family_record)[0]["name"] - backend._checked(family_record["endpoint"], ["detach-client", "-t", client_name], mutation=True) - import select + viewer_output.clear() + def final_detach_event(kind): + try: + row = json.dumps({"kind": kind, "monotonic_ns": time.monotonic_ns(), + "pid": os.getpid(), "verb": "detach-client"}) + "\n" + fd = os.open(trace, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) + try: + os.write(fd, row.encode("ascii")) + finally: + os.close(fd) + except OSError: + pass + final_detach_event("final_detach_dispatch") + try: + backend._checked(family_record["endpoint"], ["detach-client", "-t", client_name], mutation=True) + except BaseException: + final_detach_event("final_detach_exception") + raise + else: + final_detach_event("final_detach_return") + # The background drain is the sole PTY reader so transition evidence + # cannot be consumed and discarded by this endpoint wait. deadline = time.monotonic() + 5 while time.monotonic() < deadline: - if select.select([viewer_master], [], [], .1)[0]: - try: - os.read(viewer_master, 65536) - except OSError: - pass + time.sleep(.05) if backend._endpoint_state(family_record["endpoint"]) != "live": break deadline = time.monotonic() + 2 while (not viewer_done.exists() or viewer_done.read_text() != "0") and time.monotonic() < deadline: time.sleep(.05) - assert viewer_done.exists() and viewer_done.read_text() == "0" + def trace_tail(): + try: + with trace.open("rb") as stream: + stream.seek(0, os.SEEK_END) + stream.seek(max(0, stream.tell() - 32768)) + return stream.read(32768).decode("ascii", errors="replace") + except OSError: + return None + if not (viewer_done.exists() and viewer_done.read_text() == "0"): + pytest.fail("\n".join(( + f"viewer exit after final detach: exitfile={viewer_done.read_text() if viewer_done.exists() else None!r}", + f"process={process_info(viewer_pid)!r}", + f"endpoint={backend._endpoint_state(family_record['endpoint'])!r}", + "session={!r}".format({key: value for key, value in service._load(handle)[1].items() + if key in ("state", "cleanup_state", "retirement_armed_at", "retirement_detaches")}), + f"hook_trace_tail={trace_tail()!r}", + f"pty_tail={bytes(viewer_output[-8192:]).replace(os.fsencode(tmp_path), b'')!r}", + )), pytrace=False) viewer_drain_stop.set() viewer_drain_thread.join(timeout=1) os.close(viewer_master) diff --git a/ls/tests/test_shell_ui_refresh.py b/ls/tests/test_shell_ui_refresh.py index 571e7ba33..4805e44c1 100644 --- a/ls/tests/test_shell_ui_refresh.py +++ b/ls/tests/test_shell_ui_refresh.py @@ -6,6 +6,7 @@ import json from pathlib import Path import shlex +import re import subprocess import sys import tempfile @@ -209,7 +210,24 @@ def cli_refresh(): tmux("send-keys", "-t", viewer, "Enter") eventually(lambda: "Preview Midnight" in tmux("capture-pane", "-p", "-t", viewer)) tmux("send-keys", "-t", viewer, "Enter") - eventually(lambda: "Candidate palette" in tmux("capture-pane", "-p", "-t", viewer)) + try: + eventually(lambda: "Candidate palette" in tmux("capture-pane", "-p", "-t", viewer)) + except AssertionError: + current = service._load(handle)[1] + overlay = current["native_layout"].get("overlay_lock") + raise AssertionError({ + "stage": "candidate_palette", + "viewer_tail": tmux("capture-pane", "-p", "-t", viewer)[-8192:].replace( + str(work), "" + ), + "overlay": ({key: overlay.get(key) for key in ( + "kind", "pid", "starttime", "client_pid", "client_starttime", + "cleanup_state", "cleanup_error", + )} if isinstance(overlay, dict) else None), + "menu_receipt_present": any(directory.glob(".menu-choice-*")), + "preview_receipt_present": any(directory.glob(".theme-preview-*")), + "ui_generation": current.get("ui_generation"), + }) from None tmux("send-keys", "-t", viewer, "Escape") eventually(lambda: service._load(handle)[1]["native_layout"]["overlay_lock"] is None) wider = tmux("new-session", "-d", "-P", "-F", "#{pane_id}", "-x", "180", "-y", "45", @@ -217,7 +235,55 @@ def cli_refresh(): eventually(lambda: len(backend._clients(service._load(handle)[1])) == 2) assert service._load(handle)[1]["native_layout"]["columns"] == 120 tmux("send-keys", "-t", viewer, "C-q") - eventually(lambda: service._load(handle)[1]["native_layout"]["columns"] == 180) + try: + eventually(lambda: service._load(handle)[1]["native_layout"]["columns"] == 180) + except AssertionError: + from ls.core.terminal_ops.command_state import process_info, process_state + + def safe(value): + if value is None or type(value) in (bool, int): + return value + if isinstance(value, str) and re.fullmatch(r"[A-Za-z0-9_.-]{0,128}", value): + return value + return None + + def process(pid): + if type(pid) is not int: + return None + info = process_info(pid) + return ({**{key: info.get(key) for key in ("pid", "ppid", "starttime")}, + "state": process_state(pid)} if info else None) + + evidence = {} + try: + current_directory, current, _ = service._load(handle) + layout = current["native_layout"] + evidence["layout"] = {key: safe(layout.get(key)) for key in ( + "columns", "rows", "desired_generation", "applied_generation", + "layout_state", "layout_error", "pending")} + clients = backend._clients(current) + evidence["client_count"] = len(clients) + evidence["clients"] = [] + for client in clients[:32]: + item = {"pid": safe(client.get("pid")), "flags": sorted( + flag for flag in client.get("flags", ()) if isinstance(flag, str) and safe(flag) is not None)[:32]} + try: + item["geometry"] = backend.observer_geometry(current, client) + except Exception as error: + item["geometry_error"] = type(error).__name__ + evidence["clients"].append(item) + pins, unknown = service._pins(current_directory, remove_dead=False) + evidence["pins_unknown"] = unknown + evidence["pin_count"] = len(pins) + evidence["pins"] = [{ + **{key: safe(pin.get(key)) for key in ( + "kind", "pid", "starttime", "viewer_pid", "primary_viewport")}, + "attach_process": process(pin.get("pid")), + "viewer_process": process(pin.get("viewer_pid")), + } for pin in pins[:32]] + except Exception as error: + evidence["diagnostic_error"] = type(error).__name__ + raise AssertionError({"stage": "primary_handoff_180", **evidence}) from None eventually(lambda: (s := service._load(handle)[1])["native_layout"]["applied_generation"] == s["native_layout"]["desired_generation"]) tmux("resize-window", "-t", wider, "-x", "200", "-y", "50") diff --git a/ls/tests/test_sudo_dialog_native.py b/ls/tests/test_sudo_dialog_native.py index 023c908d6..4dc3b7578 100644 --- a/ls/tests/test_sudo_dialog_native.py +++ b/ls/tests/test_sudo_dialog_native.py @@ -28,7 +28,7 @@ @pytest.mark.skipif(shutil.which("tmux") is None or not Path("/proc/self/stat").exists(), reason="native Linux tmux is required") @pytest.mark.parametrize("action", ["submit", "cancel", "delayed_submit"]) -def test_managed_sudo_uses_private_native_dialog_once(monkeypatch, action: str) -> None: +def test_managed_sudo_uses_private_native_dialog_once(monkeypatch, native_popup_diagnostics, action: str) -> None: tmp_path = Path(tempfile.mkdtemp(prefix="ls-sd-")) root = Path(__file__).resolve().parents[2] backend = TmuxCommandBackend(config_file="/dev/null", socket_root=tmp_path / "family-sockets") @@ -72,6 +72,7 @@ def test_managed_sudo_uses_private_native_dialog_once(monkeypatch, action: str) FAKE_SUDO_DIGEST=os.environ["FAKE_SUDO_DIGEST"]) monkeypatch.setattr(managed, "_caller_identity", lambda pid, tty: f"fixture-controller:{pid}") + def tmux(endpoint: Path, *args: str) -> str: completed = subprocess.run(["tmux", "-f", "/dev/null", "-S", str(endpoint), *args], env=clean_env, capture_output=True, text=True, timeout=5, check=True) @@ -166,6 +167,7 @@ def invoke() -> None: for item in result if isinstance(item, dict)], "sudo_invoked": count.exists(), "overlay": observed["native_layout"].get("overlay_lock"), + "popup_boundary": native_popup_diagnostics, }) from None assert "Sudo authentication" not in tmux(outer, "capture-pane", "-p", "-t", second) if action == "delayed_submit": diff --git a/ls/tests/test_terminal_dialog_native.py b/ls/tests/test_terminal_dialog_native.py index c80700bd9..25c70046f 100644 --- a/ls/tests/test_terminal_dialog_native.py +++ b/ls/tests/test_terminal_dialog_native.py @@ -22,7 +22,7 @@ @pytest.mark.parametrize("mode", ["submit", "disconnect"]) -def test_native_dialog_is_private_to_primary_and_preserves_input_on_resize(mode: str) -> None: +def test_native_dialog_is_private_to_primary_and_preserves_input_on_resize(mode: str, native_popup_diagnostics) -> None: root = Path(__file__).resolve().parents[2] with tempfile.TemporaryDirectory(prefix="ls-dialog-native-") as temporary: work = Path(temporary) @@ -117,12 +117,17 @@ def request(): except AssertionError: directory, observed, _ = service._load(handle) pins, unknown = service._pins(directory, remove_dead=False) - raise AssertionError({ - "result": repr(result), "first": tmux("capture-pane", "-p", "-t", first), - "second": tmux("capture-pane", "-p", "-t", second), "pins_unknown": unknown, - "pins": [{key: pin.get(key) for key in ("pid", "starttime", "viewer_pid", "primary_viewport")} for pin in pins], - "overlay": observed["native_layout"].get("overlay_lock"), - }) from None + pytest.fail("\n".join(( + f"popup={native_popup_diagnostics!r}", + f"results={[{'type': type(item).__name__, 'code': getattr(item, 'code', None), 'effect': getattr(item, 'effect', None)} for item in result]!r}", + f"first={tmux('capture-pane', '-p', '-t', first)[-8192:].replace(str(work), '')!r}", + f"second={tmux('capture-pane', '-p', '-t', second)[-8192:].replace(str(work), '')!r}", + f"pins_unknown={unknown!r}", + f"pins={[{key: pin.get(key) for key in ('pid', 'starttime', 'viewer_pid', 'primary_viewport')} for pin in pins]!r}", + "overlay={!r}".format({key: (observed["native_layout"].get("overlay_lock") or {}).get(key) + for key in ("kind", "pid", "starttime", "client_pid", "client_starttime", + "cleanup_state", "cleanup_error")}), + )), pytrace=False) assert "Private native form probe" not in tmux("capture-pane", "-p", "-t", second) directory, session, _ = service._load(handle) original_lock = session["native_layout"]["overlay_lock"].copy() diff --git a/ls/tests/test_terminal_pane_pty.py b/ls/tests/test_terminal_pane_pty.py index 74e5a912a..baf5235ad 100644 --- a/ls/tests/test_terminal_pane_pty.py +++ b/ls/tests/test_terminal_pane_pty.py @@ -12,11 +12,13 @@ import pytest +from ls.core.terminal_ops.backends import _retirement_process from ls.core.terminal_ops.models import PaneError, PaneTransition from ls.core.terminal_ops.shell import ( read_prompt_state, stage_transition, write_bash_bootstrap, + write_bash_startup_home, ) @@ -30,6 +32,84 @@ def _until(predicate, *, timeout: float = 8.0): pytest.fail("managed Bash did not reach the expected observable state") +@pytest.mark.skipif(not Path("/proc/self/stat").exists(), reason="Linux process identity required") +def test_login_bash_gate_loads_after_scalar_vte_profile_replacement(tmp_path: Path) -> None: + binding_key = "b" * 64 + original_home = tmp_path / "original-home" + original_home.mkdir() + before = tmp_path / "profile-prompt-before" + after = tmp_path / "profile-prompt-after" + (original_home / ".bash_profile").write_text( + '''__vte_prompt_command() { :; } +VTE_VERSION=3405 +export VTE_VERSION +if [[ "${VTE_VERSION:-0}" -ge 3405 ]]; then + case "$TERM" in + xterm*|vte*|gnome*) + printf '%s\\n' "${PROMPT_COMMAND:-}" > "$PROFILE_PROMPT_BEFORE" + PROMPT_COMMAND="__vte_prompt_command" + printf '%s\\n' "$PROMPT_COMMAND" > "$PROFILE_PROMPT_AFTER" + ;; + esac +fi +''', + encoding="utf-8", + ) + bootstrap = write_bash_bootstrap(tmp_path, binding_key, pane_id="test-herdr-pane", + pane_id_env="HERDR_PANE_ID") + startup_home = write_bash_startup_home( + tmp_path, binding_key, original_home=original_home, bootstrap=bootstrap, + ) + child_environment = os.environ.copy() + child_environment.update({ + "HOME": str(startup_home), + "INPUTRC": "/dev/null", + "PROMPT_COMMAND": "__ls_inherited_scalar_hook", + "PROFILE_PROMPT_BEFORE": str(before), + "PROFILE_PROMPT_AFTER": str(after), + "HERDR_PANE_ID": "test-herdr-pane", + "VTE_VERSION": "0", + "TERM": "xterm-256color", + "WEZTERM_SHELL_SKIP_ALL": "1", + }) + master, slave = pty.openpty() + child = os.fork() + if child == 0: + os.close(master) + os.setsid() + import fcntl + import termios + + fcntl.ioctl(slave, termios.TIOCSCTTY, 0) + for stream in (0, 1, 2): + os.dup2(slave, stream) + if slave > 2: + os.close(slave) + os.execve("/bin/bash", ["-bash", "--login", "-i"], child_environment) + os.close(slave) + try: + def primary(): + try: + state = read_prompt_state(tmp_path, binding_key) + except PaneError as error: + if error.code != "state_unavailable": + raise + return None + return state if state.get("phase") == "primary" else None + + prompt = _until(primary) + assert prompt["phase"] == "primary" + assert prompt["keymap_ready"] is True + assert prompt["pane_id"] == "test-herdr-pane" + assert prompt["shell_pid"] == child + assert before.read_text(encoding="utf-8").strip() == "__ls_inherited_scalar_hook" + assert after.read_text(encoding="utf-8").strip() == "__vte_prompt_command" + finally: + os.killpg(child, signal.SIGKILL) + os.waitpid(child, 0) + os.close(master) + + @pytest.mark.skipif(not Path("/proc/self/stat").exists(), reason="Linux process identity required") def test_real_bash_dispatch_and_continuation_guard(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: binding_key = "a" * 64 @@ -165,3 +245,53 @@ def _completed(root: Path, key: str, operation_id: str): except (OSError, ValueError): return None return value if value.get("state") == "completed" else None + + +@pytest.mark.skipif(not Path("/proc/self/stat").exists(), reason="Linux process identity required") +def test_retirement_process_requires_idle_current_bash_generation(tmp_path: Path) -> None: + master, slave = pty.openpty() + tty = os.ttyname(slave) + child = os.fork() + if child == 0: + os.close(master) + os.setsid() + import fcntl + import termios + + fcntl.ioctl(slave, termios.TIOCSCTTY, 0) + for stream in (0, 1, 2): + os.dup2(slave, stream) + if slave > 2: + os.close(slave) + os.chdir(tmp_path) + os.execv("/bin/bash", ["bash", "--noprofile", "--norc", "-i"]) + os.close(slave) + background = None + try: + def idle(): + try: + return _retirement_process(child, tty, str(tmp_path), os.getpid()) + except PaneError: + return None + + observation = _until(idle) + assert observation["shell_pid"] == child + assert observation["shell_starttime"] > 0 + os.write(master, b"sleep 30 &\n") + children_path = Path(f"/proc/{child}/task/{child}/children") + background = _until(lambda: next(iter(children_path.read_text().split()), None)) + with pytest.raises(PaneError) as error: + _retirement_process(child, tty, str(tmp_path), os.getpid()) + assert error.value.code == "retirement_process_unverified" + finally: + if background is not None: + try: + os.kill(int(background), signal.SIGKILL) + except ProcessLookupError: + pass + try: + os.killpg(child, signal.SIGKILL) + except ProcessLookupError: + pass + os.waitpid(child, 0) + os.close(master) diff --git a/pyproject.toml b/pyproject.toml index e0d0bd7f1..f3164baab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "localsetup" -version = "4.89.10" +version = "4.90.2" description = "Visible operations, shared context, and controlled workflows for AI agent tools" license = "PolyForm-Noncommercial-1.0.0" license-files = ["LICENSE", "ls/licenses/MIT-legacy.txt"] @@ -45,7 +45,7 @@ dev = [ "setuptools==84.2.1", ] s3-sdk = [ - "boto3==1.43.118", + "boto3==1.43.122", ] [project.scripts] diff --git a/uv.lock b/uv.lock index 8ba161d19..f2b722f2c 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 5 requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] [manifest] build-constraints = [ @@ -42,30 +46,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.118" +version = "1.43.122" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/8b/5694c8433b5202f34a82def4edee274f7df1eba2919c66d9d389d2202ec8/boto3-1.43.118.tar.gz", hash = "sha256:c0620f799e0f0c222ba42c337b65cdbcd3da38a3121187f634d8926cbbb28a9c", size = 112649, upload-time = "2026-10-22T19:22:09.629Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/0b/6fb3820f86bf7d65313e85f7fb360147eb35170b5c306753eca69539b24f/boto3-1.43.122.tar.gz", hash = "sha256:c923650d09e4c445564ca14d5ce89466c3083884b6f6c189f0f445fc17f02eb5", size = 112646, upload-time = "2026-10-29T19:19:55.597Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/a3/65bb3d64928f3c2da17640b92044084150317f02adf22442cb0c52af2a8b/boto3-1.43.118-py3-none-any.whl", hash = "sha256:ab8f3d2c622ef039338b4f228d97ea55f1abd573d84bb9b9a5ee8cb6780c0ccd", size = 140045, upload-time = "2026-10-22T19:22:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/ee/fd/fef3d36be7875f413d8ef3a17c1743b64f9686693d1aed8d5126e7fa1dcd/boto3-1.43.122-py3-none-any.whl", hash = "sha256:cca51b3650080594a1df344b8a2e76007eab6d5fa50504a0b622dfdc14cdeaa1", size = 140042, upload-time = "2026-10-29T19:19:54.241Z" }, ] [[package]] name = "botocore" -version = "1.43.118" +version = "1.43.123" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d4/b8/3e55154ddaf1a6d1d4ddda85c0e5983205a3b7657edacd8ebba8c6fd774c/botocore-1.43.118.tar.gz", hash = "sha256:7f3f9d88cc9e847587690f82a42e3c3c292c3026c92c1e60d68a9310018e003f", size = 16420126, upload-time = "2026-10-22T19:22:04.81Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/de/011fc2a47fb066c47064b122498d2fd21f55bc90ea514acbddc37ca0f8c1/botocore-1.43.123.tar.gz", hash = "sha256:80948cb9b1cdf1a1a25809089a42e3a1bc4af0258d198222db6e97dcc660dd0c", size = 16486269, upload-time = "2026-10-30T19:18:44.609Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/ce/61b39a8e7a69af5ea43c1c3e1b0065a4866b42ccddf107e16686a3143614/botocore-1.43.118-py3-none-any.whl", hash = "sha256:a45d7ae570674161e4a3542d8c68633bf7503f92d1de781ef7361b79488eda6f", size = 16122016, upload-time = "2026-10-22T19:22:00.414Z" }, + { url = "https://files.pythonhosted.org/packages/db/96/4f2dffd304425c07160ab2c12cb13322d852fd956fe9d4386228c93f02ea/botocore-1.43.123-py3-none-any.whl", hash = "sha256:e30580f9c1282c321529238ff8ff75066096a2adbd0a293c8ae592c21d1733a3", size = 16185987, upload-time = "2026-10-30T19:18:39.794Z" }, ] [[package]] @@ -340,15 +344,15 @@ wheels = [ [[package]] name = "genai-prices" -version = "0.1.13" +version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx2" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/de/119c7ba00b9ddbf629de34cf390ab472da6d7bc5df0c51cdbec900280a74/genai_prices-0.1.13.tar.gz", hash = "sha256:76c9e2dd3917fdacdf3bc562297c4591384142ea2283c477e1512fafa4e3a384", size = 134622, upload-time = "2026-10-15T09:47:04.739Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/93/95297c8b37f0deec1780d7579e85915fd7fe4dbe0da38cb79bc0fcc16a2a/genai_prices-0.2.0.tar.gz", hash = "sha256:a0e92059e96492f2ffe55fa708908fe2fc0afe11d111f8051106d52ae8fd9c12", size = 159032, upload-time = "2026-10-29T14:41:09.524Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/5a/b6b7d55148d367d07bcd3efdf24e3086ad199ea5109d0c335a872dfaac95/genai_prices-0.1.13-py3-none-any.whl", hash = "sha256:6d2b40760a2f7f5e207bac9b98de0af80e60c58ead497872a5331a7cee610477", size = 142541, upload-time = "2026-10-15T09:47:03.424Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2a/c2369565aa7dde5ea33de724acae7cc753f9d19239daea0000779102f4b5/genai_prices-0.2.0-py3-none-any.whl", hash = "sha256:21ede75742043b3489a6cec3ad5722782640b34676563c6c755b52061812e6a3", size = 168199, upload-time = "2026-10-29T14:41:08.104Z" }, ] [[package]] @@ -560,7 +564,7 @@ wheels = [ [[package]] name = "localsetup" -version = "4.89.10" +version = "4.90.2" source = { editable = "." } dependencies = [ { name = "anyio" }, @@ -632,7 +636,7 @@ dev = [ { name = "pytest-xdist", specifier = ">=3.6" }, { name = "setuptools", specifier = "==84.2.1" }, ] -s3-sdk = [{ name = "boto3", specifier = "==1.43.118" }] +s3-sdk = [{ name = "boto3", specifier = "==1.43.122" }] sdk-build = [ { name = "packaging", specifier = "==26.3" }, { name = "setuptools", specifier = "==84.2.1" }, @@ -641,16 +645,16 @@ sdk-build = [ [[package]] name = "logfire-api" -version = "5.2.0" +version = "5.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/ef/302ecf3ea120d1ce127fae8a15ee5cfa6d178d956d253d332f49b7bc8f0c/logfire_api-5.2.0.tar.gz", hash = "sha256:06553f4a4b1b54d094dd01708f51cde69ccd5e8a06fc2664bece3491b593390d", size = 99170, upload-time = "2026-10-13T17:36:32.044Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/3e/065cbc78fbfdbf96194833522c6a9a3677c67c6e66987b226eccbf0e0d0b/logfire_api-5.4.0.tar.gz", hash = "sha256:328b3fae6eaced55875339cf58521ebf13a9117bdb8be40f717f056aa62c43d3", size = 101625, upload-time = "2026-10-26T15:17:54.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/5a/cd2d6e41b04c932c33dd027e119e3b6923c740e7b68da6f78cfb901040b4/logfire_api-5.2.0-py3-none-any.whl", hash = "sha256:f4bd56df590ba7e7a32adb0302c82af38dc4edb9eecd03b1bfa03ffa581eecf8", size = 151057, upload-time = "2026-10-13T17:36:29.201Z" }, + { url = "https://files.pythonhosted.org/packages/4c/03/1e412a0bbae5d7cab75cf2645aa6e330835db95d76c20b9912af495dcd6a/logfire_api-5.4.0-py3-none-any.whl", hash = "sha256:00297901da03e350bcf4ebe3d829f3617ebbe62b000e5d53d3c485223c258204", size = 154066, upload-time = "2026-10-26T15:17:49.827Z" }, ] [[package]] name = "openai" -version = "3.37.0" +version = "3.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -660,21 +664,21 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/66/d4c48dc690e36b297c178636d8434050ac92df9db8a4c89d9bd451cccc95/openai-3.37.0.tar.gz", hash = "sha256:670bd6f699000d43a2073bcf94374a55bb133bfe5de0d084d47eb9bb9f6a8e5d", size = 2325534, upload-time = "2026-10-22T20:04:39.602Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/f1/3aaadae62d59a9b464f9d2e1deab74f9eb697244beb7dc37fff64df9c92c/openai-3.41.0.tar.gz", hash = "sha256:d065fc59d3d7d8537a77b3ceb527732952c6ff610158e8a2a95b246be637e86f", size = 2313147, upload-time = "2026-10-28T22:27:52.317Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/73/c605f0cee1882e453a4b7fc2f17f4a42a7d4c1ed2304aff4b163ffade819/openai-3.37.0-py3-none-any.whl", hash = "sha256:d9f5b000f52397a7f9f946012744c5bc4c962eed3c100385252464c273a63e57", size = 2303939, upload-time = "2026-10-22T20:04:37.707Z" }, + { url = "https://files.pythonhosted.org/packages/83/23/089333ff950ee07c30762160f4eff5ac5d376934c2d6797abf6da9cd06a5/openai-3.41.0-py3-none-any.whl", hash = "sha256:7060a74b239609b21f2919eb98807a8aad463b4f510b8b3b388b15559360b0a8", size = 2380171, upload-time = "2026-10-28T22:27:50.372Z" }, ] [[package]] name = "opentelemetry-api" -version = "1.45.1" +version = "1.45.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/14/cf3a6b6448849656e1e5f4e2101f3988dc6789724050096aba1bc5ccf592/opentelemetry_api-1.45.2.tar.gz", hash = "sha256:d7b0008e21e5fd61cbe7a7f34ca3b6905a78ab677d9bc395e299359c75cda6b4", size = 73277, upload-time = "2026-10-29T17:29:37.888Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/610db864d7513158cebb8f36cc88e67e73bd6c7aa1905ef18d89e1281c81/opentelemetry_api-1.45.2-py3-none-any.whl", hash = "sha256:2cbd737377e710ee740d261c94e93fa0f08e6d037c18f17513cc0a2d94479a34", size = 60335, upload-time = "2026-10-29T17:29:06.955Z" }, ] [[package]] @@ -783,11 +787,11 @@ wheels = [ [[package]] name = "pyasn1" -version = "0.6.4" +version = "0.6.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/7e/3c0da41061a2021cac7cd524fd57092adbe5b515e8bf9005702e366802e2/pyasn1-0.6.5.tar.gz", hash = "sha256:6b11616a6bcf3a40e635f082bb232fa57b5e0dd7f517bd7ddd69ec3794508a47", size = 155152, upload-time = "2026-10-27T06:32:49.125Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, + { url = "https://files.pythonhosted.org/packages/19/1f/3eefd5c84c35cae44549ce1c7f7e58f7945b34f5a6bce07a135438522073/pyasn1-0.6.5-py3-none-any.whl", hash = "sha256:4017b0fce125096b8573b39c989fdb1a4b19cf761e683398184550fea0e66587", size = 84807, upload-time = "2026-10-27T06:32:48.045Z" }, ] [[package]] @@ -801,7 +805,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.14.1" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -809,115 +813,115 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26", size = 900362, upload-time = "2026-10-11T18:37:55.396Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/d5/b4867fa7c7f04c34997d7d8aad744f64d210bb99d67cf18bf3fb1eea1aed/pydantic-2.14.2.tar.gz", hash = "sha256:fb7b2db6cefe8414dd612edeede49c4c433acb0432b0f92b95024e45f66c505d", size = 911596, upload-time = "2026-10-29T16:37:12.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454", size = 488106, upload-time = "2026-10-11T18:37:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/97/01/c71a71258b58659c72504e5fd65b451f6e36e34dc931d0b237ded213c814/pydantic-2.14.2-py3-none-any.whl", hash = "sha256:7efbf7507b937931cd32a5755acb00ea1cf5ccb1d4e8067a5850d5069e382118", size = 490403, upload-time = "2026-10-29T16:37:10.181Z" }, ] [[package]] name = "pydantic-core" -version = "2.50.1" +version = "2.50.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz", hash = "sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09", size = 513515, upload-time = "2026-10-11T18:35:44.82Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/8e/23ec1021834e35041fbb2f10946141e6acdfd39379c45cc4d65a4e8a1d98/pydantic_core-2.50.2.tar.gz", hash = "sha256:1b0e8dc3932cf4e9ed5727570325cacc716b9409ffc794a221c0ccc72e8eb775", size = 518711, upload-time = "2026-10-29T16:34:57.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/94/101b49a6c63f99586755690696eae14532772587d9e39e93f7b8d28a49da/pydantic_core-2.50.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0", size = 2222428, upload-time = "2026-10-11T18:32:20.228Z" }, - { url = "https://files.pythonhosted.org/packages/db/a3/f8b1b39349c33037c73d7a1b794c9c29d8f07d5bd2b74399c1375a529cc6/pydantic_core-2.50.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e", size = 2025779, upload-time = "2026-10-11T18:32:21.723Z" }, - { url = "https://files.pythonhosted.org/packages/d2/2d/862a7d115305cc621358e51ba13f296e0ffdf4fbd984be9f63bc7862730b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41", size = 2048429, upload-time = "2026-10-11T18:32:23.3Z" }, - { url = "https://files.pythonhosted.org/packages/c0/7d/fed95d18bf2b5abe2d5ab2bbec4184bfc22b6805994a33ccdae8edffd25a/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3", size = 2173274, upload-time = "2026-10-11T18:32:24.697Z" }, - { url = "https://files.pythonhosted.org/packages/93/1e/c6f22ceac65dc3d291ff79bea0eb74cbd1de8b128f01fee48af03bcea1e1/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5", size = 2360403, upload-time = "2026-10-11T18:32:26.281Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/190b5b6bfe4c72f265feb657913eb30749193927a5ad7b83cfa7866a25a2/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6", size = 2439799, upload-time = "2026-10-11T18:32:27.844Z" }, - { url = "https://files.pythonhosted.org/packages/90/24/48cd98388e4ac2c08d0800af36d15413db15e20387f1038123a2c86cb2f6/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e", size = 2160157, upload-time = "2026-10-11T18:32:29.553Z" }, - { url = "https://files.pythonhosted.org/packages/00/86/d909133d0b9979ed14c20f110f21124a0e346caf071bd66ac5af3009062b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a", size = 2229523, upload-time = "2026-10-11T18:32:31.027Z" }, - { url = "https://files.pythonhosted.org/packages/90/31/4b43f08131b2098e73a852fd932063385f3103b33839fd9d1abdb89e673d/pydantic_core-2.50.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9", size = 2242700, upload-time = "2026-10-11T18:32:32.622Z" }, - { url = "https://files.pythonhosted.org/packages/88/77/de39658569dc7a4c293d110a89629f61145e7538f00d1e4280c28debefd3/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff", size = 2278452, upload-time = "2026-10-11T18:32:34.165Z" }, - { url = "https://files.pythonhosted.org/packages/46/bd/1eeb9e4e791e81210ef9096f58080b32ba15b5b13c108d50aaa3f8ce9f73/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4", size = 2450098, upload-time = "2026-10-11T18:32:35.547Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e6/b2accfdff17e5a88d6358536702146de00f9339adaa4d5bbe1ee93cb4832/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2", size = 2476839, upload-time = "2026-10-11T18:32:37.119Z" }, - { url = "https://files.pythonhosted.org/packages/3b/7e/c8a83eb5d2cd42c2e13507150fb777b1edea5a018adc9cbf2858a1d08285/pydantic_core-2.50.1-cp312-cp312-win32.whl", hash = "sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e", size = 2101425, upload-time = "2026-10-11T18:32:38.593Z" }, - { url = "https://files.pythonhosted.org/packages/c5/17/8bbd530b8659e9d963f5b16f6cb8e159f4cd174b4d6304e48969ab12dba8/pydantic_core-2.50.1-cp312-cp312-win_amd64.whl", hash = "sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498", size = 2172522, upload-time = "2026-10-11T18:32:40.112Z" }, - { url = "https://files.pythonhosted.org/packages/af/9f/a79d690b127e4e3cff143d031538ab9aa715f6b2fbeecbd07cd6f2bf9fb7/pydantic_core-2.50.1-cp312-cp312-win_arm64.whl", hash = "sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1", size = 2161366, upload-time = "2026-10-11T18:32:41.606Z" }, - { url = "https://files.pythonhosted.org/packages/ae/57/0e237d7091d2cd44a35d243b7344227f90440166860469cd036afc23a04d/pydantic_core-2.50.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b", size = 2222774, upload-time = "2026-10-11T18:32:43.103Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b9/c720e56858d4e1539503297ed37063e0c08e0f3541c41b777f6a800f75fa/pydantic_core-2.50.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482", size = 2024738, upload-time = "2026-10-11T18:32:44.587Z" }, - { url = "https://files.pythonhosted.org/packages/4d/b8/fbfc25875219cc060e613170ff10e850c6d8924beb4910da57c2ee3ba1d2/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d", size = 2047049, upload-time = "2026-10-11T18:32:46.164Z" }, - { url = "https://files.pythonhosted.org/packages/d7/43/34210124d504c553688f2f04b48500b131237528ac545b445e0d6d30e0d5/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658", size = 2173415, upload-time = "2026-10-11T18:32:47.722Z" }, - { url = "https://files.pythonhosted.org/packages/65/cf/6e178e8fdc11da5965bef980983bf46326a42f436871dd51ba0a57f39df1/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4", size = 2361719, upload-time = "2026-10-11T18:32:49.217Z" }, - { url = "https://files.pythonhosted.org/packages/11/14/bd5169d356aa91bf777e28ba0b281c192d286d03c2812c9c9db023a2f00e/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255", size = 2439803, upload-time = "2026-10-11T18:32:51.025Z" }, - { url = "https://files.pythonhosted.org/packages/1c/bc/d79d000e5203ebef39af839f2ce77a777fcad6e08ad26af9a2fcd114ffc8/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec", size = 2158402, upload-time = "2026-10-11T18:32:52.714Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a5/4902cb5fd599422c130bd3124ab31ee8b771199bd56662702eed07d3fec7/pydantic_core-2.50.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72", size = 2229755, upload-time = "2026-10-11T18:32:54.126Z" }, - { url = "https://files.pythonhosted.org/packages/1a/f5/c1481f8669f6060d89110c9b1374173fc8767ca276b84f4870bd8acd5e3f/pydantic_core-2.50.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c", size = 2242303, upload-time = "2026-10-11T18:32:55.641Z" }, - { url = "https://files.pythonhosted.org/packages/04/f9/77fc3c7653ba9b6e42049e17e96b25388187d4a7c274ab1cb07f58ac8419/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568", size = 2278345, upload-time = "2026-10-11T18:32:57.38Z" }, - { url = "https://files.pythonhosted.org/packages/95/9b/0579c5d12e7f2b16b27e6782427987341fcce07b0725e02ddb0b74add0c4/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8", size = 2449947, upload-time = "2026-10-11T18:32:58.896Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ac/1b677db91eba54cc5922f4de6edc46ba45c2bd712dfdc0130382d158e214/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4", size = 2476889, upload-time = "2026-10-11T18:33:00.665Z" }, - { url = "https://files.pythonhosted.org/packages/4d/2a/3a9f6624ee3ea9ccba5249dde11418bb4c35780a7a92608f8768bd3fea39/pydantic_core-2.50.1-cp313-cp313-win32.whl", hash = "sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685", size = 2101118, upload-time = "2026-10-11T18:33:02.329Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1f/323f78ddd9d9938aac420c1abb4e8ba799fc8bdab0acc67c0593b837c979/pydantic_core-2.50.1-cp313-cp313-win_amd64.whl", hash = "sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f", size = 2170840, upload-time = "2026-10-11T18:33:03.919Z" }, - { url = "https://files.pythonhosted.org/packages/cb/09/8497c52a739ae425c3ac2f7f56414cbc711c67d374346174f40fe2062644/pydantic_core-2.50.1-cp313-cp313-win_arm64.whl", hash = "sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662", size = 2161633, upload-time = "2026-10-11T18:33:05.518Z" }, - { url = "https://files.pythonhosted.org/packages/49/33/28b96e81677153715e3eafb9f26663a80841e859fde282a380359b0d3fa1/pydantic_core-2.50.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a", size = 2224428, upload-time = "2026-10-11T18:33:07.153Z" }, - { url = "https://files.pythonhosted.org/packages/94/40/15c06410c9b7b8da5805d27b64e09bd3f900e986728106db2689dbc51513/pydantic_core-2.50.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9", size = 2023740, upload-time = "2026-10-11T18:33:08.763Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4e/5eb629f6efc2a27e421d789dd4bfacbb5f6d09c80c28b13d1e87d73163d5/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c", size = 2049435, upload-time = "2026-10-11T18:33:10.366Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8e/f195aebec49ad12318876ac2368c197a7939f5d52f8e156d2238ef8a4588/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb", size = 2172975, upload-time = "2026-10-11T18:33:12.368Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f5/7ad9fb83010cd5ea0948409db105676ed779c4e709e2d3d89b9fe2558794/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597", size = 2364197, upload-time = "2026-10-11T18:33:14.244Z" }, - { url = "https://files.pythonhosted.org/packages/ca/fb/bf0aab3e78301d202b82a0322ec968cd703b11fc0623fde9a28708936f62/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899", size = 2444259, upload-time = "2026-10-11T18:33:15.79Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/38f6d6564fae57d9b12e5676dfa947e0e7d5c46dbeaa7eb3352261d6d299/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea", size = 2159857, upload-time = "2026-10-11T18:33:17.51Z" }, - { url = "https://files.pythonhosted.org/packages/65/a0/fb0a3ca10f139dcf765b2d312d10cf0f65c57c59993229d00ad12b14ceff/pydantic_core-2.50.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e", size = 2233954, upload-time = "2026-10-11T18:33:19.591Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6a/e63842252702aa4ec6e6b7178ca85e541a77459076592c23ebe2d9840b33/pydantic_core-2.50.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20", size = 2243876, upload-time = "2026-10-11T18:33:21.393Z" }, - { url = "https://files.pythonhosted.org/packages/39/25/5991cf8318b37e0dfab47b87541619a1df8501a793cba0d978846cba37a7/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc", size = 2277908, upload-time = "2026-10-11T18:33:23.324Z" }, - { url = "https://files.pythonhosted.org/packages/a7/3e/3ee8baaa6cc25a6961c69168cf9ff0f002d56f4e0d541ad6724c18fb61f3/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396", size = 2449285, upload-time = "2026-10-11T18:33:24.942Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/acbec6deac13fe697a80c275a9b6661db62c4d323bac8a47347b1f39c7cd/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb", size = 2479013, upload-time = "2026-10-11T18:33:26.925Z" }, - { url = "https://files.pythonhosted.org/packages/10/08/21a3f237b264389f6219d053ee78fc1b5c2fd402c1b1cb2b6cb8b85f9834/pydantic_core-2.50.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5", size = 1484400, upload-time = "2026-10-11T18:33:28.693Z" }, - { url = "https://files.pythonhosted.org/packages/09/ce/077a6d262d12ef09108377ac0717f029f420d773ace63cafd6143af75568/pydantic_core-2.50.1-cp314-cp314-win32.whl", hash = "sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2", size = 2103222, upload-time = "2026-10-11T18:33:30.326Z" }, - { url = "https://files.pythonhosted.org/packages/14/4c/350a2415209c43d670eb71d3c040f332c04a30583d39e311b05c7ac15762/pydantic_core-2.50.1-cp314-cp314-win_amd64.whl", hash = "sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f", size = 2171586, upload-time = "2026-10-11T18:33:32.139Z" }, - { url = "https://files.pythonhosted.org/packages/bf/92/9bea6ca96580a0902fed366f064f0829e404f41889b34543279a1162b888/pydantic_core-2.50.1-cp314-cp314-win_arm64.whl", hash = "sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea", size = 2155773, upload-time = "2026-10-11T18:33:33.896Z" }, - { url = "https://files.pythonhosted.org/packages/86/8c/f121f073cf32bdd5cba7e6230b1ce0ac845b0cf43e44dd597d95af272db2/pydantic_core-2.50.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070", size = 2234017, upload-time = "2026-10-11T18:33:35.588Z" }, - { url = "https://files.pythonhosted.org/packages/a4/69/2bb2bcd6146cffe98d760a46c42ae71efd5151d9b2f9c9bf6619a3b32083/pydantic_core-2.50.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e", size = 2001016, upload-time = "2026-10-11T18:33:37.57Z" }, - { url = "https://files.pythonhosted.org/packages/20/b3/fbf854c7d07ec114260c26e9e2071a4381740f9ae09641dbfcbdf2a18c45/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5", size = 2023015, upload-time = "2026-10-11T18:33:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/65/20/6de55b2f92cdb614b745c6e9ced639fc4fd7e1e77604825a88bdece6fcbd/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb", size = 2192543, upload-time = "2026-10-11T18:33:41.381Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d9/19e91c94bd5c405945ce2f15526808aea37e162c253160de8ed7bf70b406/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e", size = 2388704, upload-time = "2026-10-11T18:33:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/b6/bc/2e24c8415eae1a25ee5a5484946a917123bad2e9d01689a759236928175a/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b", size = 2464118, upload-time = "2026-10-11T18:33:44.856Z" }, - { url = "https://files.pythonhosted.org/packages/bd/1f/945b8053cb061c64e102bcaf7bfb9ed740c0bd4349f9bb978a7d4ddff4ab/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019", size = 2150142, upload-time = "2026-10-11T18:33:46.661Z" }, - { url = "https://files.pythonhosted.org/packages/2b/78/96a3e50bf0d64aaae781107eb9335b7529d05a85793ed6e7241c4cd1d931/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112", size = 2245577, upload-time = "2026-10-11T18:33:48.574Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5d/a6038a0322232758a6ebfa709f4ca14a60bf5b93940cd0b345056a557da4/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a", size = 2263728, upload-time = "2026-10-11T18:33:50.527Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4b/76ded3333a457a9344c4f2d63f2d82d0101654b05178fa4ddfe7d3627674/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb", size = 2293630, upload-time = "2026-10-11T18:33:52.362Z" }, - { url = "https://files.pythonhosted.org/packages/a9/00/9eca378335c9c1b72bc779bf6e4c2a82ce784f14ec48a0e87102c9870e05/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b", size = 2468048, upload-time = "2026-10-11T18:33:54.118Z" }, - { url = "https://files.pythonhosted.org/packages/35/ca/e3832e9cf93651251de43c8c5c029ed680a3c1a0a1b17ee26c81bed08cbd/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807", size = 2512159, upload-time = "2026-10-11T18:33:55.99Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f2/773469b5a10a39116a2cb17edaf6d722f017dd8da07161b371465311c542/pydantic_core-2.50.1-cp314-cp314t-win32.whl", hash = "sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19", size = 2098406, upload-time = "2026-10-11T18:33:58.043Z" }, - { url = "https://files.pythonhosted.org/packages/ee/42/0bb74f8f25204b259b11ab7c12dc7f180893b6bd706118b385147fb6efd5/pydantic_core-2.50.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c", size = 2148481, upload-time = "2026-10-11T18:33:59.913Z" }, - { url = "https://files.pythonhosted.org/packages/47/0d/d801646c9679a4e630e15cf521d4108b93854b42a6e6e02391bd4c6b1095/pydantic_core-2.50.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86", size = 2174972, upload-time = "2026-10-11T18:34:01.875Z" }, - { url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102", size = 2224599, upload-time = "2026-10-11T18:34:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c", size = 2024191, upload-time = "2026-10-11T18:34:05.901Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7", size = 2048252, upload-time = "2026-10-11T18:34:07.757Z" }, - { url = "https://files.pythonhosted.org/packages/b2/54/13f419bf1eb59852003818e25935aaf175687d978f4c4bca70e08fe40a3b/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43", size = 2173376, upload-time = "2026-10-11T18:34:09.592Z" }, - { url = "https://files.pythonhosted.org/packages/6d/6c/b5a34d24cd0c81669d8f8339d74e6815abcf2f8fb48ab4b49b84c09be1d5/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78", size = 2364559, upload-time = "2026-10-11T18:34:11.534Z" }, - { url = "https://files.pythonhosted.org/packages/eb/8d/d64d6216a8df365082665927ff923f183056f9049fee08e9777c9ac05296/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831", size = 2444697, upload-time = "2026-10-11T18:34:13.535Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa", size = 2159961, upload-time = "2026-10-11T18:34:15.56Z" }, - { url = "https://files.pythonhosted.org/packages/1e/35/f236549299dcc78e71e945495d7ec67e78d20844d0999c60601685003031/pydantic_core-2.50.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5", size = 2234581, upload-time = "2026-10-11T18:34:17.502Z" }, - { url = "https://files.pythonhosted.org/packages/6a/85/26901a490522b7f75ef9bb9a7afb73e5bb550f0e1cb5b99a0069183e2eb9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415", size = 2244090, upload-time = "2026-10-11T18:34:19.402Z" }, - { url = "https://files.pythonhosted.org/packages/ca/2c/481bcfc70ceeb77a778ca6e5b705fe592cb64735c108157f81b7dd9c280e/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10", size = 2277948, upload-time = "2026-10-11T18:34:21.317Z" }, - { url = "https://files.pythonhosted.org/packages/24/eb/f1e09333faa7ba447cde967310f758430cc7c5e987bdab5228816c70030d/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f", size = 2449688, upload-time = "2026-10-11T18:34:23.321Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b3/036bde636db8f76d92996e81aefc75678ab5cec4a07eea1ad0c72a893fc3/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459", size = 2479286, upload-time = "2026-10-11T18:34:25.214Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a3/07f018294ee18d144afeb6df1a47d9e92960f014be45c5ed13519fa5af95/pydantic_core-2.50.1-cp315-cp315-win32.whl", hash = "sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290", size = 2103683, upload-time = "2026-10-11T18:34:27.42Z" }, - { url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl", hash = "sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed", size = 2171681, upload-time = "2026-10-11T18:34:29.508Z" }, - { url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl", hash = "sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54", size = 2156251, upload-time = "2026-10-11T18:34:31.455Z" }, - { url = "https://files.pythonhosted.org/packages/47/47/83643225b08f2aef6c8cc4bbe6e3f79c5e139c4364a6e450d0f399d87774/pydantic_core-2.50.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a", size = 2234363, upload-time = "2026-10-11T18:34:33.65Z" }, - { url = "https://files.pythonhosted.org/packages/5e/66/127ca649ba2f2462039dc1e694c6c01e00a3689f023a617364d3507e6d97/pydantic_core-2.50.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08", size = 2000972, upload-time = "2026-10-11T18:34:36.037Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4f/e421e0a5d653b1203b090b2e48745976988d7338a647940704b5b9c2b399/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe", size = 2022925, upload-time = "2026-10-11T18:34:37.972Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/ea5568e2491e1a71100f15ae8c2d01ef52db42a184a2d4e716bc79e5eb8f/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f", size = 2192867, upload-time = "2026-10-11T18:34:39.921Z" }, - { url = "https://files.pythonhosted.org/packages/ae/5e/3b8c3a35acbe219909ada5defad5d7d9fed845fb2b37bd5ec518f453c119/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8", size = 2388687, upload-time = "2026-10-11T18:34:42.184Z" }, - { url = "https://files.pythonhosted.org/packages/b6/aa/7889b4e515f91a2e8c0ae6b5081fec0feb30c4398d1434e14793c60f173a/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a", size = 2464415, upload-time = "2026-10-11T18:34:44.384Z" }, - { url = "https://files.pythonhosted.org/packages/08/78/93449e628eb8a6fdcce3eff9043081179d1bc7ce6f1bff32dc5006f41e00/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8", size = 2150823, upload-time = "2026-10-11T18:34:46.392Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f1/72c5bc129fceb0d00f05dc1e67f518c1728de1928c55f81fc13d7690de39/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709", size = 2245806, upload-time = "2026-10-11T18:34:48.805Z" }, - { url = "https://files.pythonhosted.org/packages/28/2a/922a0e78f3aa6ab837b59f88190233fb8dad546c17995fde9bb3ed3b9b49/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2", size = 2264160, upload-time = "2026-10-11T18:34:50.876Z" }, - { url = "https://files.pythonhosted.org/packages/52/a8/0f1449e3e1b20941c9372faa18e7b6a092e30cdfa841902473f7989532ac/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a", size = 2294031, upload-time = "2026-10-11T18:34:52.876Z" }, - { url = "https://files.pythonhosted.org/packages/d1/d0/1031f492857de70355fb16524bbb03efce5fd34c93ed4a1ec60be07e0d4b/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f", size = 2468423, upload-time = "2026-10-11T18:34:54.995Z" }, - { url = "https://files.pythonhosted.org/packages/46/52/269ffffa645b8e47906395a39cf9db1151ae9fa4bcd7f47b960bc31baf37/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b", size = 2512598, upload-time = "2026-10-11T18:34:57.149Z" }, - { url = "https://files.pythonhosted.org/packages/31/5c/e47e28281f20326ff6f3c31d626f0a83e615d2d94ba41bb0ad6ec237184a/pydantic_core-2.50.1-cp315-cp315t-win32.whl", hash = "sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f", size = 2098535, upload-time = "2026-10-11T18:34:59.313Z" }, - { url = "https://files.pythonhosted.org/packages/03/ad/759e181e69c1b472c60b2049e5f61d5da1deefdd5ce1df4bb2ebcf771f25/pydantic_core-2.50.1-cp315-cp315t-win_amd64.whl", hash = "sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b", size = 2148645, upload-time = "2026-10-11T18:35:01.571Z" }, - { url = "https://files.pythonhosted.org/packages/6d/56/8a702c27e5be9f47e5f19d8669227424290e4c024e7c370279cbaf244b4e/pydantic_core-2.50.1-cp315-cp315t-win_arm64.whl", hash = "sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44", size = 2175297, upload-time = "2026-10-11T18:35:04.079Z" }, - { url = "https://files.pythonhosted.org/packages/a5/09/1bcf160f3cd6333e2a76982fae5963e5032de0bcaaba38464223dc266bfe/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242", size = 2217070, upload-time = "2026-10-11T18:35:15.688Z" }, - { url = "https://files.pythonhosted.org/packages/3d/81/f02de95eec7794dfc7a5bc69a834f0e834b31b3009014ce6de903855f775/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5", size = 2039027, upload-time = "2026-10-11T18:35:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/4d/16/227746ed3771d9bf2304568b55134849ece34e13b38b1d76de763015c77f/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295", size = 2109009, upload-time = "2026-10-11T18:35:20.218Z" }, - { url = "https://files.pythonhosted.org/packages/e5/b9/7664d1592a0e5a74903f357cd5b28ceb7336f3473256c5a4e8c8432f1881/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61", size = 2269883, upload-time = "2026-10-11T18:35:22.48Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/6609a66cda909d414c58438f9bd70b1e947fe12c25f6bd40c93121afd7ef/pydantic_core-2.50.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e3d824b6152e1c0fd58dc6266f2de470eb7b2286135294f5f7cd26a0f0693738", size = 2220341, upload-time = "2026-10-29T16:32:10.193Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/92463ae7be78d73885487f72c4599902c2cc190378bccaad5718b7495250/pydantic_core-2.50.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:596921489390a002d6a5e303384a0f1f98384f8bb550fbf38a1aafa6093da0f9", size = 2093701, upload-time = "2026-10-29T16:32:11.502Z" }, + { url = "https://files.pythonhosted.org/packages/62/d3/24147a1aa22f7eade546172750e1110fd7b2793998057809800d3ebeae4e/pydantic_core-2.50.2-cp312-cp312-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:36d8b9725684ca3bf658b90a101d335fc8cae405bb22196b7b5ef41344913756", size = 2253622, upload-time = "2026-10-29T16:32:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/f6/50/2628ca3c83a0d9a6fda3305fe59b1b28e40c0221e8f234cd1ef86fb64745/pydantic_core-2.50.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:43a8fd56f3c681f3cda2d5c842ad28e88241df742dfcbe8fd1dd437a3056e1f6", size = 2114101, upload-time = "2026-10-29T16:32:14.702Z" }, + { url = "https://files.pythonhosted.org/packages/21/c8/cc6d7304a93f91f5e06260ccad71cc7d882489663a37dec009532379fc46/pydantic_core-2.50.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:6f5a432c6c1ef18a1a02a04b3616b8d3f2f88b13faa46caae1ac6559ab202234", size = 2199541, upload-time = "2026-10-29T16:32:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/18f44ac5696b3e2b2384cb89c9fbe6ed69860f528e232b9fb46cc5b45a17/pydantic_core-2.50.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5d3cd8be71c4da3c9b42cc92f17e5e3036bdca98bf18555a0e072c3fe87abbb4", size = 2368597, upload-time = "2026-10-29T16:32:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/1d/22/5f2a48ef819a7e1b74fb003a85d27576ad2f9ddf3901b88998ac63ce1e00/pydantic_core-2.50.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a438cb8a3f87f2c49e82e43fb19511d4d5043f53e0787dc4df937ccb904373a", size = 2451701, upload-time = "2026-10-29T16:32:18.956Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/2a6556bbdb8e918175af9d849a5d0b367b5644e41939d040f3a7215b1339/pydantic_core-2.50.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9667637665acba166a7dbbc786f14c50512d3a01f7199e0e1a13b929e92d7c7a", size = 2242782, upload-time = "2026-10-29T16:32:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/90/ed/dd9f29a92257f32eb5b99d5120826186ee0632ecc3aaf6c23e2fab89ed6d/pydantic_core-2.50.2-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:b3e681208e2d39fde13fbea449ef2736a85ec1ea2afa41e9193a8c536e71dafe", size = 2253706, upload-time = "2026-10-29T16:32:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/11/19/1ccf3bf38eb4ed97d8aeefc558458834ae0ad53fdbb1edf61af7228ea500/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6eb6ff07964f39e33c6f57fefe939514e76b8c0d4a694e5663e2e04e80cc1798", size = 2288603, upload-time = "2026-10-29T16:32:23.465Z" }, + { url = "https://files.pythonhosted.org/packages/ea/cd/bae2320fc89a65bcd87bf66ec83aac51b3b43b970a2edece15feb8af64dd/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:4eb54c25e3463d73e1e4ed439ffc45d1d7e590df40bc7bb06f11a906afd996c3", size = 2476597, upload-time = "2026-10-29T16:32:24.824Z" }, + { url = "https://files.pythonhosted.org/packages/db/35/1bdb8d52c8e562e95c5e6ebb5784792af568ac6a81470dd546d95f392b60/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:91238117cd296352f54f24117ff5a227bdea428865dba1244aa0cdd1424c0bc2", size = 2478664, upload-time = "2026-10-29T16:32:26.197Z" }, + { url = "https://files.pythonhosted.org/packages/12/72/0179b40ac6a6b0899c61826f2e3e80f88063f6d821e0dd9641927de46700/pydantic_core-2.50.2-cp312-cp312-win32.whl", hash = "sha256:98722314e12a8e4fa346e0afd3fff2098b5732875ff45592b450a7b589b43c4f", size = 2108820, upload-time = "2026-10-29T16:32:27.736Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fb/679144362d7e5162519d9f6631ec982521417de3196351c6c56485f5b29f/pydantic_core-2.50.2-cp312-cp312-win_amd64.whl", hash = "sha256:b381574614245f18fdb2d6d1aa8f149e225869c47b5ee7f81812de8977817314", size = 2252238, upload-time = "2026-10-29T16:32:29.572Z" }, + { url = "https://files.pythonhosted.org/packages/0f/68/e3ccc885c3e2086ff84d7ca496f45b974a9f0a49ce2da8973aaa3a369a4b/pydantic_core-2.50.2-cp312-cp312-win_arm64.whl", hash = "sha256:f07c965a8aa6d62145f122641f292129984f39a3e3697585b19298a1488287db", size = 2162565, upload-time = "2026-10-29T16:32:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fc/ac3ae9d934932ce3c380e7fe53e01372b49ef5dcb49db56f1a1f005b4d8c/pydantic_core-2.50.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:93eb41d626d9a2749a678823c70bba8c25033cef474ae8001fd7621adbbd8437", size = 2222463, upload-time = "2026-10-29T16:32:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/7605223654ed4e777d05668e914d32b476b1fa2bb756bff9820fe270456b/pydantic_core-2.50.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c539d452cba22f250103c7d1c00cbe9038eb705e657476e701c43374efd9f2e", size = 2093995, upload-time = "2026-10-29T16:32:33.496Z" }, + { url = "https://files.pythonhosted.org/packages/b4/29/4a1a653cfedd3f5e1ccd3b1e919fe324fb45607d12c30fdd7ca5af16ba8d/pydantic_core-2.50.2-cp313-cp313-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:53bd25e8ab54c02bb4c55d4848c1d36f75f3b9a7356e83179fe27794d9e9e6c4", size = 2253178, upload-time = "2026-10-29T16:32:34.77Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/03ce5ed01c98b60272cdee8ac1abb819b64ef347717c11d96215e32d9952/pydantic_core-2.50.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f142cd663d1148ed32993ed9a58e5c67ea312f446a01a6c13a956945aafc9f23", size = 2113894, upload-time = "2026-10-29T16:32:39.241Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bc/f7644474ac0faad9a7fdf5179239893b21339169b8b4f89e3075c4be16b2/pydantic_core-2.50.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:126f158474346f457c0759c29c1a6334acd5f7f99dd272bb27d19314f3d81b4d", size = 2198248, upload-time = "2026-10-29T16:32:42.357Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8f/3d3a1c1d313e02774f6c034eb1910eb6e795922e66e81f0f053f98afe866/pydantic_core-2.50.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0b94a515f44f1ebcb25395bcdba506c4cdaf6d3fcb0466b9658521b41e5ff057", size = 2368505, upload-time = "2026-10-29T16:32:43.755Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/38fb4545c00ce2bfebc2a0ffd99f7f580f1e1efb8db3b2faf88def640435/pydantic_core-2.50.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a55c1163e829a08707b4a92c8ee02a5a3a3847ced74aa4da94e3e975ffe1ec63", size = 2451704, upload-time = "2026-10-29T16:32:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/79/be/b559a1ceaff8b1d4581733d47d2ffa785635ac05716eae4c964466aac6e8/pydantic_core-2.50.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31a1fb3ffbd1195b24490f671d12623dd4c9bfa56d6af0a299fa43b516134f5b", size = 2241760, upload-time = "2026-10-29T16:32:46.382Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7c/3c8ab7cb45e4dc7687a4f16a953fee14d2eca18dd90d6be8bd470ef06962/pydantic_core-2.50.2-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:83d6f42ae6eba44548a1a3d4f900a5b1500fdd14f09f04754a8800ab0d544432", size = 2254115, upload-time = "2026-10-29T16:32:47.818Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ad/3f3471206224cdac2af3492a3f7c724c47a82f5f8850ff364c2e36023497/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:9fcde11462e0faab4c2a2ae8cd4516f7dd8239eec583bd88c5524d8064ad2d0c", size = 2288598, upload-time = "2026-10-29T16:32:49.404Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1e/2070b388b1d8633cfb72e3964ac3c95aaeb195c240209894ff386847b494/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:848984e231190ae49bc8b5229e64d2a6fba4231dc6046e1b6b28ddc060bc7601", size = 2475116, upload-time = "2026-10-29T16:32:51.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/96/e96ff19527c73b9aaddce2a4424006dc15439fd89ef2d39641048c5ee85a/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8772c2c0091e6fc2d0499151793d31bf811826986b0727052935a95104845544", size = 2478926, upload-time = "2026-10-29T16:32:52.856Z" }, + { url = "https://files.pythonhosted.org/packages/35/10/2fefc082eb46f959e80735e859161305c0c2fbb3d5d20ac5547c49a84a3b/pydantic_core-2.50.2-cp313-cp313-win32.whl", hash = "sha256:bd6ecae08c0d871cf93b6f0d4ee76b06c65be44ea22311981a022b2f1948cf9d", size = 2108354, upload-time = "2026-10-29T16:32:54.276Z" }, + { url = "https://files.pythonhosted.org/packages/6f/dd/14eeec394ec1422f9c4e9743cd746f4502232a7c65eb235d1a03729d94ab/pydantic_core-2.50.2-cp313-cp313-win_amd64.whl", hash = "sha256:e5c801bdf3cc92274b8fc2c7023c8751e5ae81260c009ac558ff55ce712a2d45", size = 2250556, upload-time = "2026-10-29T16:32:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/75/b7/a014acf1ae4b4aa7d32ff957e2250698637daa3a29448444180360b92284/pydantic_core-2.50.2-cp313-cp313-win_arm64.whl", hash = "sha256:fd9aa71a800c87c0f7ab53fa38009169a23a82c685989626ca74a4bd483e1b67", size = 2162633, upload-time = "2026-10-29T16:32:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/64/fc525bb981a9f287cfa34346e7ed1d5ef7c043c2b9b517f54c6136032d10/pydantic_core-2.50.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:efad0bdc67d3b119c3bab6b94d341a27333c2bb89ed75dfa182a4a06cd0ea329", size = 2226414, upload-time = "2026-10-29T16:32:58.559Z" }, + { url = "https://files.pythonhosted.org/packages/db/68/315701095f25687938ab79d263a6d939337456c2840fe69038fd38000338/pydantic_core-2.50.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ffb4dce3ecb2cd563aa30f9eb5088a1cf1f4f688b90cbb374fa96e49fc50b02", size = 2095041, upload-time = "2026-10-29T16:32:59.873Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/efd3abf31aa24bc97b311735f418886f947b4f193df02a1977643963b03e/pydantic_core-2.50.2-cp314-cp314-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:150f6e101f575c21a4ecfed1be1c05bb89a0ad49cb9c846ed377583bd71b32c2", size = 2257635, upload-time = "2026-10-29T16:33:01.096Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6c/146b36eed4de8115a0fda76840703e75f5f8b36aeb3af86976bf094d1a17/pydantic_core-2.50.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1db9cab6197e797824f0479dd2ae5fd2e272aa61c61b7faa5a6e450ca7a0d3b7", size = 2116807, upload-time = "2026-10-29T16:33:02.692Z" }, + { url = "https://files.pythonhosted.org/packages/fb/94/db006ea79bbe405cc38b25899319c7d461ff1813cbdfe6aa90a65ed1dfff/pydantic_core-2.50.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1b9f652e17a13bbc09b45f74c3c326cca72059ce4dcdef687fcb04389db413a9", size = 2201037, upload-time = "2026-10-29T16:33:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/41/f9/ea2bc1ec13a67af4a63c9a887fa0aa6ef4ade9018e2bb412d4fc80110e19/pydantic_core-2.50.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:823b222c4fc861a82c650998a6117ecbd150cc71bf8b78f9a31bf3f48c456108", size = 2374904, upload-time = "2026-10-29T16:33:05.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/8e/26a6a075301f0e563646037ea69bef8c63911d38fbd257f94cec69cdf985/pydantic_core-2.50.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:783abdfac21247d33bec40aa683d26bba30d61655c40de38b2decc4797aabf92", size = 2457626, upload-time = "2026-10-29T16:33:07.271Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a2/8eeabaf1d394f0d3b9a27129ac21556eabee6f2ddcb418069ef4037e00b0/pydantic_core-2.50.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b29b7c5adf4b2be7c941f7f76432aa3269cd5052ff8610376b8ece24d7b02c51", size = 2245505, upload-time = "2026-10-29T16:33:08.824Z" }, + { url = "https://files.pythonhosted.org/packages/bd/54/449778cd397e2682b30d1bcfab0872f6ac36702b59cc326296b7f4f14928/pydantic_core-2.50.2-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4ac996e42a93d80122a23edce44e02c6b9c4913f5c6198a077da31475341a39a", size = 2259563, upload-time = "2026-10-29T16:33:10.107Z" }, + { url = "https://files.pythonhosted.org/packages/26/c2/2f65691d25488ee0f3c17861d079e8ccb7817955522d07cd3c3970d30af5/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:99e1d482138a71902415ba375bb4627679a5fefd3bd07803c3a8f37632f4981d", size = 2290470, upload-time = "2026-10-29T16:33:11.273Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/aa7c40234cd24536afa0895ff1d26f9f15638004e25242c5193ce053b96b/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:a53d5f11cd6d15569ba3e53f3498077f0d5e633f6ec604d2ea613e895017ebee", size = 2477272, upload-time = "2026-10-29T16:33:12.771Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/c347c68e040cf93d8487a8f4fc28f2768312834c7d21c301538f416f9955/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23385c4626f8b5a7792aa381ea573d9432cfbf4c21375a0a5eed2a4fb64e0e58", size = 2484352, upload-time = "2026-10-29T16:33:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8e/72556126c8b6154fe7acaf757b96b2daa37aeffc3a517deb76e272f5ad09/pydantic_core-2.50.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9a8a62281fd9c6a373b3fd3d473685e6838d43341eecde5f47729748e2c11ee3", size = 1491226, upload-time = "2026-10-29T16:33:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/5b/66/004c85511812964a36b3535af022c5c0dfcb760a31e4905b96310e8dd6c8/pydantic_core-2.50.2-cp314-cp314-win32.whl", hash = "sha256:18f008ef58cd30782f81b6e0119e20b7e884edcefbc2d370162ebe033f6e5c99", size = 2113134, upload-time = "2026-10-29T16:33:16.87Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5f/dcd3df20ab2a7279d42a8415fc184e8e77bcf9aa3c9ac3e938c1300a242c/pydantic_core-2.50.2-cp314-cp314-win_amd64.whl", hash = "sha256:b62c89545d21d930c333208fd548e7b4f262228ba3a322fd75c987a75ab11f31", size = 2253407, upload-time = "2026-10-29T16:33:18.47Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fb/0685895f5d199b215011dc4eb8ccea032a64ec23dfa88eeb5acca939c2d2/pydantic_core-2.50.2-cp314-cp314-win_arm64.whl", hash = "sha256:6e731bf5a5e8e25398544d7709e39068ec4230b0177e4db7fa955692fd415ba3", size = 2160456, upload-time = "2026-10-29T16:33:19.782Z" }, + { url = "https://files.pythonhosted.org/packages/84/2f/4c8f67efe62837a508b74cf910904f73029cb211371351c5ceb36d56e85e/pydantic_core-2.50.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:33c6800ab9563adaa0d7893e9cd8f5b972ef1e6a23df70498e05c24f3fcec5e1", size = 2217558, upload-time = "2026-10-29T16:33:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/ce/73/e0ef88cfbcca5409f57751289bc20be63a58af596a36e94b8cd795e59747/pydantic_core-2.50.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f81df206b937d0dca5efb38c25fc99037d82dbcd3740e34c3f81997d2322ac1c", size = 2074335, upload-time = "2026-10-29T16:33:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/7b/cc/47844a3179ebce27064e13619594b3fc1a9b4509d8dec1e3960c5f87f5bb/pydantic_core-2.50.2-cp314-cp314t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:21ea3b00f1e87c66f3f9e92f9f6fec20495ac0e60d7d2e68ba1fc49739d7cf40", size = 2264201, upload-time = "2026-10-29T16:33:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/30e9d3c86d81884ee3199560ddfa2c1a66a8dc0736441748eae43064abbc/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9eff570402175c7d5fff8497d5a9c05580664a79bb66c07b8f205d17f69e580b", size = 2094353, upload-time = "2026-10-29T16:33:26.739Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8f/392a5999480d0bcab4539db0dafa93265152396fd63e87be4d24e13d6d82/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:240fec32a74af0979f7253532859b3fd209ad5eea822416bb010929dba445c3d", size = 2203119, upload-time = "2026-10-29T16:33:28.154Z" }, + { url = "https://files.pythonhosted.org/packages/cb/80/6e4e276a3fde5500224a39150e06a4f134f136165908be69402720718a36/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92acf02c770b9d5c4e9a3e8d64e9d95ae26456a86ebb7b7cf64a97f899e5871b", size = 2392881, upload-time = "2026-10-29T16:33:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/d5/5eb56c6317ab4abf33ef9519a50bfac0c96f7f408bad1bc36da6f27d43e4/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1ac5b99bc7b0a68ec64f5f75a6828a7bab5535c8df0cdc07901deb50e0815910", size = 2457290, upload-time = "2026-10-29T16:33:30.587Z" }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f1fe9384bb37e4e728c51c79151808deacf7b2502e245f50b57c85eeb199/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:713d7c33c0e231e3786eb0ec95885814dd65d5f3a09754399b6acbf5e9365f11", size = 2241568, upload-time = "2026-10-29T16:33:32.04Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f1/cffc4f8456c35690bcb65a8f2304afbf2f519766345a28aba553d5a55cc8/pydantic_core-2.50.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:61e13100b30d8d25fc0d1f0297014b32941bf0975a3584bd08f3adebb68d1ead", size = 2257185, upload-time = "2026-10-29T16:33:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/ccd4df1cc39cb1e42fb7ec86a0dc0a23e8479de04d3012ca6b0bf2279a03/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:523fe6574aea50f456cbc301c1cdddcbf237939f7657f9af99b4f0f71942b59c", size = 2299384, upload-time = "2026-10-29T16:33:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/83/60/404b4b0c416a61867ee642cdc1450bca4d8a9a10e5f5f21e1ecaad05fdc8/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:6c25b8826be85714b27dafe908d3461e1b09e3e31c0279a7029040c832c781ef", size = 2479384, upload-time = "2026-10-29T16:33:36.106Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0d/57c81418a2fdfde0578c71396df773965adebd4421d9985a0f8dcbe9ec4d/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a59db03b80a156a6fbeae41096866d1cb89cdf31031ba44c768a804538880636", size = 2504616, upload-time = "2026-10-29T16:33:37.423Z" }, + { url = "https://files.pythonhosted.org/packages/08/66/dec78e28a604f3f4985f5a795d4da3e5fef78b0921aeb7f614ed7c780a32/pydantic_core-2.50.2-cp314-cp314t-win32.whl", hash = "sha256:0d090ae76d5b09c003600c7779b3adcf3d1b49268f616c5c51d844e54c8e318d", size = 2093179, upload-time = "2026-10-29T16:33:38.705Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ea/79d9ee60648463804145d44b08f1e7e2474427d9704e3331d814483908b3/pydantic_core-2.50.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9b01a092955c1c7530196ad526755cff4f2d11f37ca9a6fc9ebe37ca49a48977", size = 2220575, upload-time = "2026-10-29T16:33:39.929Z" }, + { url = "https://files.pythonhosted.org/packages/eb/01/aa6a7bd017c3228a41942da4fd788b86fb4aad9677212a4737a0c157fe74/pydantic_core-2.50.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4ea01f30e119f1ec7a2034e85c40f491d469896aef02cd6436fd9292fe1be3fb", size = 2160866, upload-time = "2026-10-29T16:33:41.237Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7f/ec2974015b4be5e50266ce732b8942d3d0b3bff751ace8900999c6577bce/pydantic_core-2.50.2-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:a3317630e2791f56071b82f89bb71624146125e4311e6643f708c6ba744b5ac5", size = 2230936, upload-time = "2026-10-29T16:33:43.656Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/af99a09eddc348c1d0744c0df28dfcd4839ca497067f0fa2168208b41b1b/pydantic_core-2.50.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f73930dacad5e611fe019e18df6f53c30629b1843d038d303b8dec86a32493bd", size = 2100409, upload-time = "2026-10-29T16:33:44.946Z" }, + { url = "https://files.pythonhosted.org/packages/84/75/8db3c46f02d6005277e5c2ff5933c85e7e1eb8798aa334c676c828650c3f/pydantic_core-2.50.2-cp315-cp315-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:e2e6240a0a5ad847017b3d65187877fdb7a11d5a58d43c30736f9f523478913b", size = 2259728, upload-time = "2026-10-29T16:33:46.461Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9d/98c8d524921ca24e5c8a81a4b8f12209ac6ab05d0dd58b374e357da0ffc5/pydantic_core-2.50.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fdf5b52ed22f1404c2433d0b46df2d86d9befb742d0b9b4042a2325c43cdb97", size = 2121771, upload-time = "2026-10-29T16:33:47.952Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ea/68c3b1fa919946012c940dea88e94e33e7dde46abe3e3d57053793c9bab8/pydantic_core-2.50.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4e9392ddf67fe97ab9889f796d468c9908a8754b8b6c7520eb03a65012b9a1e7", size = 2196328, upload-time = "2026-10-29T16:33:49.131Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9b/cfb6312a5b65b02eab6bc00fb0d258d0e2e49468dab29b430e7cb49b2ceb/pydantic_core-2.50.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:48e944091b7bff233920bed8a60b0ed6c96c73d3c5836bf51c4b9a4bfb58e4ca", size = 2380152, upload-time = "2026-10-29T16:33:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/7fcced1d84ea294200abfa93e488f36d00732101d17bf5e1f1389e66de26/pydantic_core-2.50.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:91c92856b846aa690ea6239ffd6c5003c5e3e296a69095570bf4efee5bfa0bdc", size = 2462299, upload-time = "2026-10-29T16:33:51.884Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d0/dd0e355694094f0045922f4da60ac9bc7a314babc494e46089fb00640fd3/pydantic_core-2.50.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5299296ff5cafcb69851fd652de8f9268a0bb036bcbedbfcaa94cf743baf3986", size = 2249999, upload-time = "2026-10-29T16:33:53.333Z" }, + { url = "https://files.pythonhosted.org/packages/3a/79/f5f4b36434604d22aacb7152667f87a470e68847c89b136c68a278cf409d/pydantic_core-2.50.2-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d3d049b557875f9d15230eec8a731749a7a873d8ed6a051903b05bdf46e0e65f", size = 2262745, upload-time = "2026-10-29T16:33:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/14/21/a4c47a286745857758a4bb8d4d5e260c736a7bd6fab5da618f5711d1a3f1/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b0b25d647036972c2951059046cb498f72b7af5a2def27bf8e5f44c3067ab7dd", size = 2293663, upload-time = "2026-10-29T16:33:56.132Z" }, + { url = "https://files.pythonhosted.org/packages/0d/db/25330e609fd219ce16d0a089af1b4b62095f01595dbe6972f7a7dfc313b9/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:8835566b3e94026da3ce2249f9ee35c9d2ebe4cd131f6a39bfc82708d3010a46", size = 2473247, upload-time = "2026-10-29T16:33:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/53/16/704ca88b82156dc485da2dfb11d5d52a10b9ef15a294639c0aa5ebd1643e/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:3fbcd402e4b607d4b8de5d4591215f31f042af03f9d09e58f693a6221e32833f", size = 2490536, upload-time = "2026-10-29T16:33:59.297Z" }, + { url = "https://files.pythonhosted.org/packages/50/34/1fdd0eebee9f8b6c4ec3c2490f49dc59a652c8c5a7fa0cbcaada0d91cda9/pydantic_core-2.50.2-cp315-cp315-win32.whl", hash = "sha256:8431ecb131489cbdaf6f51f7ade47475f73aff4f1912552f75ba19a6cfe233a8", size = 2117204, upload-time = "2026-10-29T16:34:00.514Z" }, + { url = "https://files.pythonhosted.org/packages/24/6b/702adc91486fb96ba55675d58ee15ddc38fb48553fb35e26fd005e303206/pydantic_core-2.50.2-cp315-cp315-win_amd64.whl", hash = "sha256:04cf672960b8c4fc28bb159c8cdd101fbd311c520237a5c0360448b851adfb41", size = 2261064, upload-time = "2026-10-29T16:34:01.773Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/7f117432b8614526aa0d96fadd2b7fb7393156ddf2caf26a6fe606e6cfb6/pydantic_core-2.50.2-cp315-cp315-win_arm64.whl", hash = "sha256:36917e29c0b815d6b940bc91560fcf453b5b85d55c579796373f0ea11d1c111a", size = 2161783, upload-time = "2026-10-29T16:34:02.966Z" }, + { url = "https://files.pythonhosted.org/packages/45/1c/2ba7a34fe8da4c83ec200fb9f85ab4d54b961be08367922743623f987849/pydantic_core-2.50.2-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:bd1fe69056a19c377a6c6de2c1240fb8676ba0512a969965efb486510c5e0f1f", size = 2223184, upload-time = "2026-10-29T16:34:04.214Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/f71a8da1f53a5be1787d2a97ba6fd9adba115305c7f15ea0bf733ed8336c/pydantic_core-2.50.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9b2383933b1dd7a8c93e02f99ebfd305f1286057172483b8e6898bed3fef69d3", size = 2078174, upload-time = "2026-10-29T16:34:05.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0a/847e1f5427372692f6eb5c38261fed6b8cedf379e37d9f6134d48b9c8ee9/pydantic_core-2.50.2-cp315-cp315t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:a1b4d60bad57d6e0e7cd30cc4d1dc646d258dd6dfb7935ce76c7dd72dd1e98b0", size = 2268813, upload-time = "2026-10-29T16:34:06.733Z" }, + { url = "https://files.pythonhosted.org/packages/f5/bb/12966dc82b5e9c347981658b52e24349b6242902b644725c00ee7bdf4267/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:93bedc68c4138088ea3c042189c01d2945f530f0dcc36e1ea02df8418bd4f1cd", size = 2098331, upload-time = "2026-10-29T16:34:08.69Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fe/340416b7ebe664524e7ada16ad2e3e069888b168010d75bda6a502bf2bf8/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:d684ff87d00de2f6abcbbfa9b0fdce8b513dd799a900809dcfa7eb62ad69832a", size = 2197432, upload-time = "2026-10-29T16:34:10.091Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4d/425162c3d88af0d190c4dc4f940402cc6dd44d934359a37f50da6b7e4672/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bb6d49f00e2babd40af4889d6b9f4bf50879f522dd4e2e22cb705cac5e0c5e8e", size = 2399300, upload-time = "2026-10-29T16:34:11.462Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fe/2882bddf2e2038c8f41fa69f16ddeb6eeb1c002cc0cc0eb2157f73bb34e9/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6d47b2d0e00e030e0ebdb08115a37da7294e7a6d196d134a77ed7ee6c142284b", size = 2462672, upload-time = "2026-10-29T16:34:12.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ac/d7e6bfc6a95573647d1a699e19228dd99914ae6aab79a2bec293a038e980/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95838be125133fa314753f8cc5168f7c41e3a9dd5eef5383dbbbeed511bd4853", size = 2246293, upload-time = "2026-10-29T16:34:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/74/64/97473ccaeb4ae5eb338162bd8479af121484e67f7078499730e3ee0a5592/pydantic_core-2.50.2-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:2d4a043c820363359e40ac96270e1915e5376e116be1f59cee0701775de3e376", size = 2259507, upload-time = "2026-10-29T16:34:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/78a9b7a6a0d0679ce1e46cdd38ff95afd18dd2d6a01ed87c80a01bc003f7/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:57bccd3e1128736c792094da1feac6da46ec3feb1957ef649a9b3e4f8de6fa04", size = 2303550, upload-time = "2026-10-29T16:34:17.162Z" }, + { url = "https://files.pythonhosted.org/packages/39/ba/b697173f82e99cdcf5ec6654b2edb66a00f2f92ef9d9a168861fca3fcded/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:24120c644322a35b5eb765416ac8442214c45689e255f5fc6e761e7e7c743c8b", size = 2474919, upload-time = "2026-10-29T16:34:18.841Z" }, + { url = "https://files.pythonhosted.org/packages/71/10/986be76723784ae87f9664658fae39df340eaa21681cea95b6339d2e1b3e/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:b1adf2f37521b578c438f6ec61d1c1d0c20b076863ddd5d8b6f3ab658857db3f", size = 2509095, upload-time = "2026-10-29T16:34:20.16Z" }, + { url = "https://files.pythonhosted.org/packages/5f/0b/38dbd38c67de7642f87348da70122b89ca4c5fe8227cfdd5cc00c1cdb454/pydantic_core-2.50.2-cp315-cp315t-win32.whl", hash = "sha256:d79aaa468193c1bed6b0560d3c5c9f9e369e3efb0b128b38a2537e2d51d42773", size = 2096972, upload-time = "2026-10-29T16:34:21.715Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/154baf8413b08d6daf2f5dec211c303ec6bed50c032717f354f8dd85c994/pydantic_core-2.50.2-cp315-cp315t-win_amd64.whl", hash = "sha256:cc488daac19e6f5292180fac5e0ef7efffabeef4e096af9a17a75a7d3d6781f6", size = 2226659, upload-time = "2026-10-29T16:34:23.177Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/eea7a1e7d98a79ef11b6d31748b70c58f8723f87d7ee42edbb6a101f6245/pydantic_core-2.50.2-cp315-cp315t-win_arm64.whl", hash = "sha256:91315aea7adb51821d4a3378ede953bfdd76e3bf71551de4a82d7638cef7dc02", size = 2166387, upload-time = "2026-10-29T16:34:24.433Z" }, + { url = "https://files.pythonhosted.org/packages/64/4d/23560b11366b547e3e68a3b8f9c02557cee707effa54fd39dbb3c4fff1fa/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:d18d56c1ec63b304f4bb3c7db7e0d311e61cca1b1c944590a4b13ac39b2ca92b", size = 2204868, upload-time = "2026-10-29T16:34:34.745Z" }, + { url = "https://files.pythonhosted.org/packages/06/58/b9ddfaad189143d6568627166a730b6624ca90bd9a9a4af4854df88d444b/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:41ed8abeffda6a880207fdc0cd1343a448cec3cc6b38b53f730c0745f745292d", size = 2049162, upload-time = "2026-10-29T16:34:36.186Z" }, + { url = "https://files.pythonhosted.org/packages/8b/20/99d259bf7835b2f13c6d0b3eb7bcf669fb6349986736fa7a8308a9e620b0/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b7abcfaf66c1dc14c5ed928c349614f223c972a8b021b4be9548dbea6c817371", size = 2112135, upload-time = "2026-10-29T16:34:37.777Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/652c8fe599abacafd852479059d209e608596ad3de82f1bff22e9f0f02fe/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7460abce637f90e37f8c443559fbd687c38884d05cb69ef265e634945f4324", size = 2270339, upload-time = "2026-10-29T16:34:39.039Z" }, ] [[package]] @@ -931,7 +935,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.1.1" +version = "9.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -940,9 +944,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" }, ] [[package]]