From a1b483f5a2028a1c2ea00267a0d21f506d546355 Mon Sep 17 00:00:00 2001 From: YEONGUK LEE Date: Tue, 13 Oct 2026 15:25:34 +0900 Subject: [PATCH] fix(claude): bound PID records and prepare v1.1.2 --- README.ja.md | 2 + README.ko.md | 2 + README.md | 2 + README.zh-CN.md | 2 + RELEASING.ja.md | 12 ++--- RELEASING.ko.md | 12 ++--- RELEASING.md | 12 ++--- RELEASING.zh-CN.md | 12 ++--- docs/README.md | 2 + docs/i18n-manifest.json | 7 +-- docs/ja/README.md | 2 + docs/ja/releases/v1.1.2.md | 21 ++++++++ docs/ko/README.md | 2 + docs/ko/releases/v1.1.2.md | 21 ++++++++ docs/releases/v1.1.2.md | 21 ++++++++ docs/zh-CN/README.md | 2 + docs/zh-CN/releases/v1.1.2.md | 21 ++++++++ session_peer.py | 53 +++++++++++++----- session_peer_core/claude.py | 36 ++++++++++--- session_peer_core/common.py | 2 +- session_peer_core/diagnostics.py | 7 ++- session_peer_core/handoff_remote.py | 8 +-- tests/core/test_claude_records.py | 83 +++++++++++++++++++++++++++++ tests/core/test_handoff_remote.py | 17 ++++++ tests/docs/test_docs_i18n.py | 2 +- tests/docs/test_release_commands.py | 4 +- tests/docs/test_stable_candidate.py | 16 +++++- 27 files changed, 323 insertions(+), 60 deletions(-) create mode 100644 docs/ja/releases/v1.1.2.md create mode 100644 docs/ko/releases/v1.1.2.md create mode 100644 docs/releases/v1.1.2.md create mode 100644 docs/zh-CN/releases/v1.1.2.md diff --git a/README.ja.md b/README.ja.md index 813711e6..74da6cb8 100644 --- a/README.ja.md +++ b/README.ja.md @@ -38,6 +38,8 @@ Python 3.9以上が必要です。基本のローカル・SSH CLIに外部Python 2026-10-12に準備したメンテナンス版の[リリースノート](docs/ja/releases/v1.1.1.md)を参照してください。このパッチはドキュメントと開発用テストを改善し、ランタイムの動作は変更しません。パッケージ公開と本番環境への配備は別です。 +開発候補1.1.2は未公開です。[リリースノート](docs/ja/releases/v1.1.2.md)は#298のClaude登録情報と対象選択の修正を説明します。公開を個別に確認するまで、上記の公開バージョンは1.1.1です。 + 実験的なhandoffは明示的に選択する機能です。ローカルPOSIX Claudeは受領ACKに対応し、CodexとSSHは相関IDによる送信のみ対応してACKの観察には対応しません。公開済みの範囲と残る検証条件は[handoffガイド](docs/ja/handoff-runtime.md)を参照してください。 ```bash diff --git a/README.ko.md b/README.ko.md index 775ee1e7..e2decd6a 100644 --- a/README.ko.md +++ b/README.ko.md @@ -38,6 +38,8 @@ Python 3.9 이상이 필요합니다. 기본 로컬·SSH CLI는 외부 Python 2026-10-12에 준비한 유지보수 버전의 [릴리스 노트](docs/ko/releases/v1.1.1.md)를 참고하십시오. 이번 패치는 문서와 개발용 테스트를 보완하며 런타임 동작은 바꾸지 않습니다. 패키지 공개와 운영 배포는 별개입니다. +개발 후보 1.1.2는 아직 공개되지 않았습니다. [릴리스 노트](docs/ko/releases/v1.1.2.md)는 #298의 Claude 등록 기록 및 대상 선택 수정을 설명합니다. 공개 여부를 별도로 확인하기 전까지 위 공개 버전은 1.1.1입니다. + 실험적 handoff는 명시적으로 선택해야 합니다. 로컬 POSIX Claude는 영수증 ACK를 지원하지만, Codex와 SSH는 상관 ID를 이용한 제출만 지원하며 ACK 관찰은 지원하지 않습니다. 공개된 범위와 남은 검증 조건은 [handoff 가이드](docs/ko/handoff-runtime.md)를 참고하십시오. ```bash diff --git a/README.md b/README.md index 07e617d3..94aed0f6 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ Version covered by this README: **1.1.1**. Maintenance release prepared on 2026-10-12; see the [release notes](docs/releases/v1.1.1.md). This patch updates documentation and development-only tests, not runtime behavior. Package publication and operational deployment are separate. +Development candidate 1.1.2 is unpublished; its [release notes](docs/releases/v1.1.2.md) describe the #298 Claude registry and target-resolution fix. The published version above remains 1.1.1 until publication is independently verified. + Experimental handoff is opt-in: local POSIX Claude supports receipt ACKs; Codex and SSH support correlation-only submission, not ACK observation. See the [handoff guide](docs/handoff-runtime.md) for the shipped scope and remaining acceptance gates. ```bash diff --git a/README.zh-CN.md b/README.zh-CN.md index 153f7f80..8c53b8d0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -38,6 +38,8 @@ session-peer使用各智能体自身的收件箱和队列;接收方智能体 请参阅2026-10-12准备的维护版本的[发布说明](docs/zh-CN/releases/v1.1.1.md)。此补丁改进文档和开发用测试,不改变运行时行为。软件包发布与生产环境部署是两项独立操作。 +开发候选版本1.1.2尚未发布。[发布说明](docs/zh-CN/releases/v1.1.2.md)介绍#298的Claude登记记录和目标选择修复。在单独确认发布之前,上述已发布版本仍为1.1.1。 + 实验性handoff需要明确选择启用:本地POSIX Claude支持收据ACK;Codex与SSH仅支持关联标识提交,不支持ACK观察。已发布范围与剩余验收条件请参阅[handoff指南](docs/zh-CN/handoff-runtime.md)。 ```bash diff --git a/RELEASING.ja.md b/RELEASING.ja.md index 79b22288..3d217f31 100644 --- a/RELEASING.ja.md +++ b/RELEASING.ja.md @@ -4,15 +4,15 @@ 以下のソース・設定の確認から公開検証・復旧までは、2026-10-09のリリース手順の過去の記録であり、再実行の依頼ではありません。v1.1.0を再作成、移動、再利用せず、公開例を再実行しないでください。将来の版では、新しくレビューしたバージョン、最新の証拠、明示的な承認で各条件を再確認します。準備、不変のGitHub公開、必須の人手によるPyPI環境レビュー、本番環境への配備は別です。保護されたmainでは、レビュー済みで最新のPRと、Pythonのプラットフォーム別テストマトリクス、ドキュメント、シェル、パッケージ/単独ファイル/MCP/Relay/Control、統合テスト、Python/Nodeの依存関係監査を含む統合リリースゲートへの合格が必要です。 -## 現在のv1.1.1メンテナンスリリース +## v1.1.2パッチ候補 — 未公開 -所有者は2026-10-12にこのメンテナンスリリースを承認しました。#297(開発用Paramiko 5.0.0テスト)と#299(4言語のドキュメント修正)を含み、ランタイムの動作は1.1.0と同じです。#298は未解決で、このパッチでは修正しません。最新の正確なコミットのCI、全ローカルテスト、単独ファイルの再生成確認、隔離したwheel/sdistのインストール、最新の依存関係監査を実行します。作成前にv1.1.1とPyPI 1.1.1が未使用であることを確認し、タグやバージョンを移動・再利用しないでください。現在のTrusted Publisherと必須の人手によるpypiレビューを確認します。準備PRのマージ後、正確なmainのCIが成功してからコマンドを実行します。公開前に不変ドラフトの7個のアセット、ハッシュ、証明を確認します。公開承認は人手によるPyPI環境承認の代わりにはなりません。GitHubとPyPIを個別に検証し、各端末やControl/Relayの配備は含めません。 +公開済み1.1.1を基に2026-10-13に準備しました。候補は#298を修正し、不正なClaude PID記録が正常なセッションの検出を妨げないようにし、非十進Unicodeのセッション名を名前として解決します。準備はマージ、タグ作成、公開、配備の承認ではありません。正確な最新コミットのCI、全ローカルテスト、単独ファイルの再生成、隔離したwheel/sdistのインストール、最新の依存関係監査が必要です。v1.1.2とPyPI 1.1.2が未使用であることを再確認し、タグやバージョンを移動・再利用しないでください。現在のTrusted Publisherと必須の人手によるpypiレビューを確認します。別途のマージ・公開承認、正確なmain CIの成功、不変ドラフト資産7個のレビューを完了した後にのみ下記コマンドを使います。GitHubとPyPIの公開、人手による環境承認、Control/Relayや各端末の配備は別です。 -次のコマンドはv1.1.1用です。以下のv1.1.0手順は過去の記録として残します。 +次のコマンドはv1.1.2用です。以下のv1.1.0手順は過去の記録として残します。 ```bash git fetch origin main --tags -release_tag=v1.1.1 +release_tag=v1.1.2 release_commit=$(git rev-parse origin/main) git tag "$release_tag" "$release_commit" git push origin "$release_tag" @@ -23,10 +23,10 @@ gh workflow run prepare-release.yml --repo abruption/session-peer \ --ref main -f tag="$release_tag" ``` -実行要求の後で停止してください。`gh workflow run`は準備の完了前に戻ります。正確なリリースコミットに対応するv1.1.1の実行を特定し、`gh run watch --repo abruption/session-peer --exit-status`で完了を待ちます。`attach-draft`を含む実行が成功するまで、下の公開コマンドを実行しないでください。リリースを下書きのまま保ち、添付資産が正確に7個あること、SHA256SUMS、release-provenance.jsonと認証済み証明がタグ・コミットに一致することを確認します。失敗、欠落または未検証の証拠があれば、公開せず停止します。この独立した検証の完了後にのみ、下のコマンドを実行してください。 +実行要求の後で停止してください。`gh workflow run`は準備の完了前に戻ります。正確なリリースコミットに対応するv1.1.2の実行を特定し、`gh run watch --repo abruption/session-peer --exit-status`で完了を待ちます。`attach-draft`を含む実行が成功するまで、下の公開コマンドを実行しないでください。リリースを下書きのまま保ち、添付資産が正確に7個あること、SHA256SUMS、release-provenance.jsonと認証済み証明がタグ・コミットに一致することを確認します。失敗、欠落または未検証の証拠があれば、公開せず停止します。この独立した検証の完了後にのみ、下のコマンドを実行してください。 ```bash -gh release edit v1.1.1 --repo abruption/session-peer \ +gh release edit v1.1.2 --repo abruption/session-peer \ --draft=false --prerelease=false --latest ``` diff --git a/RELEASING.ko.md b/RELEASING.ko.md index 89a5459d..206e80df 100644 --- a/RELEASING.ko.md +++ b/RELEASING.ko.md @@ -4,15 +4,15 @@ 아래 소스·설정 검사부터 공개 검증·복구까지는 2026-10-09 릴리스 절차의 역사 기록이며 재실행 요청이 아닙니다. v1.1.0을 다시 생성하거나 이동·재사용하지 말고 공개 예제를 다시 실행하지 마십시오. 후속 버전에서는 새로 검토한 버전, 새로운 검증 자료와 명시적 승인으로 각 조건을 다시 확인합니다. 준비, 불변 GitHub 공개, 사람이 수행하는 필수 PyPI 환경 검토와 운영 배포는 별개입니다. 보호된 main에 반영하려면 검토를 마치고 최신 main을 반영한 PR과, Python 플랫폼 매트릭스·문서·셸·패키지/독립 실행 파일/MCP/Relay/Control 테스트·통합 테스트·Python/Node 의존성 감사를 포함한 릴리스 게이트가 필요합니다. -## 현재 v1.1.1 유지보수 릴리스 +## v1.1.2 패치 후보 — 미공개 -소유자는 2026-10-12에 이번 유지보수 릴리스를 승인했습니다. #297(개발용 Paramiko 5.0.0 테스트)과 #299(네 언어 문서 수정)를 포함하며 런타임 동작은 1.1.0과 같습니다. #298은 열려 있으며 이번 패치에서 수정되지 않습니다. 정확한 최신 커밋의 CI, 전체 로컬 테스트, 독립 실행 파일 재생성 확인, 격리된 wheel/sdist 설치와 최신 의존성 감사를 수행합니다. 생성 전에 v1.1.1과 PyPI 1.1.1이 사용되지 않았는지 확인하며 태그·버전을 이동하거나 재사용하지 않습니다. 현재 Trusted Publisher와 필수 사람 pypi 검토를 확인합니다. 준비 PR을 병합하고 정확한 main의 CI가 통과한 후에만 명령을 실행합니다. 공개 전에 불변 초안 자산 7개, 해시와 증명을 검토합니다. 공개 승인은 사람의 PyPI 환경 승인 절차를 대신하지 않습니다. GitHub와 PyPI를 각각 검증하며 장비 또는 Control/Relay 배포는 포함하지 않습니다. +공개된 1.1.1을 기준으로 2026-10-13에 준비했습니다. 후보는 #298을 수정합니다. 잘못된 Claude PID 기록이 정상 세션 검색을 방해하지 않도록 하고, 비십진 유니코드 세션 이름을 이름으로 선택합니다. 준비 작업은 병합·태그 생성·공개·운영 배포 승인이 아닙니다. 정확한 최신 커밋의 CI, 전체 로컬 테스트, 독립 실행 파일 재생성, 격리된 wheel/sdist 설치와 최신 의존성 감사를 수행합니다. v1.1.2와 PyPI 1.1.2가 사용되지 않았는지 재확인하고 태그·버전을 이동하거나 재사용하지 않습니다. 현재 Trusted Publisher와 필수 사람 pypi 검토를 확인합니다. 별도의 병합·공개 승인, 정확한 main CI 성공 및 불변 초안 자산 7개 검토를 완료한 후에만 아래 명령을 사용합니다. GitHub·PyPI 공개, 사람의 환경 승인과 Control/Relay 또는 장비 배포는 별개입니다. -다음 명령은 v1.1.1용입니다. 아래 v1.1.0 절차는 과거 기록으로 유지합니다. +다음 명령은 v1.1.2용입니다. 아래 v1.1.0 절차는 과거 기록으로 유지합니다. ```bash git fetch origin main --tags -release_tag=v1.1.1 +release_tag=v1.1.2 release_commit=$(git rev-parse origin/main) git tag "$release_tag" "$release_commit" git push origin "$release_tag" @@ -23,10 +23,10 @@ gh workflow run prepare-release.yml --repo abruption/session-peer \ --ref main -f tag="$release_tag" ``` -실행 요청 후 중단: `gh workflow run`은 준비가 끝나기 전에 반환합니다. 정확한 릴리스 커밋의 v1.1.1 실행을 확인하고 `gh run watch --repo abruption/session-peer --exit-status`로 완료를 기다립니다. `attach-draft`를 포함한 해당 실행이 성공하기 전에는 아래 게시 명령을 실행하지 않습니다. 릴리스를 초안으로 유지한 채 첨부 자산이 정확히 7개인지 확인하고, SHA256SUMS, release-provenance.json 및 인증된 증명을 태그·커밋과 대조합니다. 실패, 누락 또는 미검증 자료가 있으면 게시하지 않고 중단합니다. 이 별도 검증을 완료한 후에만 아래 명령을 실행합니다. +실행 요청 후 중단: `gh workflow run`은 준비가 끝나기 전에 반환합니다. 정확한 릴리스 커밋의 v1.1.2 실행을 확인하고 `gh run watch --repo abruption/session-peer --exit-status`로 완료를 기다립니다. `attach-draft`를 포함한 해당 실행이 성공하기 전에는 아래 게시 명령을 실행하지 않습니다. 릴리스를 초안으로 유지한 채 첨부 자산이 정확히 7개인지 확인하고, SHA256SUMS, release-provenance.json 및 인증된 증명을 태그·커밋과 대조합니다. 실패, 누락 또는 미검증 자료가 있으면 게시하지 않고 중단합니다. 이 별도 검증을 완료한 후에만 아래 명령을 실행합니다. ```bash -gh release edit v1.1.1 --repo abruption/session-peer \ +gh release edit v1.1.2 --repo abruption/session-peer \ --draft=false --prerelease=false --latest ``` diff --git a/RELEASING.md b/RELEASING.md index 44dea33c..a1a8c14d 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -4,15 +4,15 @@ Version 1.1.0 is published. GitHub published v1.1.0 on 2026-10-09 at 06:46:41 UT The sections from source/configuration gates through publication recovery below preserve the historical 2026-10-09 release procedure, not a request to run it again. Do not recreate, move or reuse v1.1.0 or rerun its publication examples. For a future version, use a new reviewed version and repeat the gates with fresh evidence and explicit authorization. Preparation, immutable GitHub publication, required human PyPI environment review and operational deployment remain separate. Protected main requires reviewed, up-to-date PRs and the aggregate release gate, including Python platform matrices, documentation, shell, package/standalone/MCP/Relay/Control tests, integrations and Python/Node dependency audits. -## Current v1.1.1 maintenance release +## v1.1.2 patch candidate — unpublished -The owner authorized this maintenance release on 2026-10-12. It includes #297 (development-only Paramiko 5.0.0 fixture) and #299 (four-locale documentation corrections); runtime behavior remains unchanged from 1.1.0. #298 is open and is not fixed by this patch. Use fresh exact-head CI, full local tests, regenerated standalone verification, isolated wheel/sdist installs and current dependency audits. Verify that v1.1.1 and PyPI 1.1.1 are unused before creating them; never move or reuse a tag/version. Verify the current Trusted Publisher and required human pypi review. Run the commands only after the preparation PR is merged and exact main CI is green. Review all seven immutable draft assets, hashes and attestations before publication. Publication authorization does not replace the human PyPI environment gate. Verify GitHub and PyPI separately; no fleet or Control/Relay deployment is included. +Prepared on 2026-10-13 against published 1.1.1. The candidate addresses #298: invalid Claude PID records must not disrupt healthy discovery, and nondecimal Unicode session names must resolve as names. This is preparation only, not approval to merge, create a tag, publish or deploy. Require exact-head CI, full local tests, standalone regeneration, isolated wheel/sdist installations and current dependency audits. Recheck that v1.1.2 and PyPI 1.1.2 are unused; never move or reuse a tag/version. Verify the current Trusted Publisher and required human pypi review. Only after separate merge and publication authorization, green exact-main CI and review of all seven immutable draft assets may the commands below be used. GitHub and PyPI publication, the human environment gate and Control/Relay or fleet deployment remain separate. -The following commands are for v1.1.1. The v1.1.0 procedure below remains historical. +The following commands are for v1.1.2. The v1.1.0 procedure below remains historical. ```bash git fetch origin main --tags -release_tag=v1.1.1 +release_tag=v1.1.2 release_commit=$(git rev-parse origin/main) git tag "$release_tag" "$release_commit" git push origin "$release_tag" @@ -23,10 +23,10 @@ gh workflow run prepare-release.yml --repo abruption/session-peer \ --ref main -f tag="$release_tag" ``` -STOP after dispatch: `gh workflow run` returns before preparation finishes. Identify the run for v1.1.1 at the exact release commit and wait with `gh run watch --repo abruption/session-peer --exit-status`. Do not execute the publication block unless that run succeeds, including `attach-draft`. Keep the release a draft while reviewing exactly seven attached assets, SHA256SUMS, release-provenance.json and their authenticated attestations against the tag and commit. Failure, missing assets or unverified evidence means stop without publishing. Only after this separate verification may the following publication command be executed. +STOP after dispatch: `gh workflow run` returns before preparation finishes. Identify the run for v1.1.2 at the exact release commit and wait with `gh run watch --repo abruption/session-peer --exit-status`. Do not execute the publication block unless that run succeeds, including `attach-draft`. Keep the release a draft while reviewing exactly seven attached assets, SHA256SUMS, release-provenance.json and their authenticated attestations against the tag and commit. Failure, missing assets or unverified evidence means stop without publishing. Only after this separate verification may the following publication command be executed. ```bash -gh release edit v1.1.1 --repo abruption/session-peer \ +gh release edit v1.1.2 --repo abruption/session-peer \ --draft=false --prerelease=false --latest ``` diff --git a/RELEASING.zh-CN.md b/RELEASING.zh-CN.md index 09279e30..3bfe8b55 100644 --- a/RELEASING.zh-CN.md +++ b/RELEASING.zh-CN.md @@ -4,15 +4,15 @@ 以下从源码/配置门禁到发布验证/恢复的部分保留2026-10-09发布流程的历史记录,并非重新执行的请求。不要重新创建、移动或重用v1.1.0,也不要再次执行其发布示例。后续版本需采用新审核的版本、最新证据和明确授权,重新检查各项门禁。准备、不可变GitHub发布、必需的人工PyPI环境审核与生产环境部署相互独立。受保护的main要求PR经过审核并与main保持最新,同时通过汇总发布门禁;门禁涵盖Python平台测试矩阵、文档、shell、包/单文件/MCP/Relay/Control、集成测试以及Python/Node依赖审计。 -## 当前v1.1.1维护发布 +## v1.1.2补丁候选版本 — 尚未发布 -所有者于2026-10-12批准了此维护发布。它包含#297(开发用Paramiko 5.0.0测试)和#299(四种语言的文档修正),运行时行为与1.1.0相同。#298仍未解决,此补丁不修复该问题。需要重新执行准确最新提交的CI、全部本地测试、单文件重新生成检查、隔离wheel/sdist安装及最新依赖审计。创建前确认v1.1.1和PyPI 1.1.1尚未使用,不得移动或重用标签和版本。检查当前Trusted Publisher和必需的人工pypi审核。仅在准备PR合并且准确main的CI通过后执行命令。发布前检查不可变草稿的7个资产、哈希及证明。发布授权不能替代人工PyPI环境审批。分别验证GitHub和PyPI,不包含设备或Control/Relay部署。 +于2026-10-13基于已发布的1.1.1准备。候选版本修复#298:无效的Claude PID记录不应妨碍正常会话发现,非十进制Unicode会话名称应按名称匹配。准备不代表批准合并、创建标签、发布或部署。必须完成准确最新提交的CI、完整本地测试、独立文件再生成、隔离的wheel/sdist安装和最新依赖审计。重新确认v1.1.2及PyPI 1.1.2未被使用,不得移动或重用标签与版本。确认当前Trusted Publisher和必要的人工pypi审核。只有另行获得合并和发布批准、准确main CI通过且审查全部7项不可变草稿资产后,才能使用下列命令。GitHub与PyPI发布、人工环境审批以及Control/Relay或各设备部署是独立步骤。 -以下命令用于v1.1.1。下方v1.1.0流程仍保留为历史记录。 +以下命令用于v1.1.2。下方v1.1.0流程仍保留为历史记录。 ```bash git fetch origin main --tags -release_tag=v1.1.1 +release_tag=v1.1.2 release_commit=$(git rev-parse origin/main) git tag "$release_tag" "$release_commit" git push origin "$release_tag" @@ -23,10 +23,10 @@ gh workflow run prepare-release.yml --repo abruption/session-peer \ --ref main -f tag="$release_tag" ``` -提交执行请求后先停止:`gh workflow run`会在准备完成前返回。确认对应准确发布提交的v1.1.1运行,并使用`gh run watch --repo abruption/session-peer --exit-status`等待完成。在包括`attach-draft`的运行成功前,不要执行下方发布命令。保持发布为草稿,检查附件恰好为7个,并对照标签和提交验证SHA256SUMS、release-provenance.json及其经过认证的证明。运行失败、资产缺失或证据未验证时,应停止且不发布。只有完成这项独立验证后,才能执行下方命令。 +提交执行请求后先停止:`gh workflow run`会在准备完成前返回。确认对应准确发布提交的v1.1.2运行,并使用`gh run watch --repo abruption/session-peer --exit-status`等待完成。在包括`attach-draft`的运行成功前,不要执行下方发布命令。保持发布为草稿,检查附件恰好为7个,并对照标签和提交验证SHA256SUMS、release-provenance.json及其经过认证的证明。运行失败、资产缺失或证据未验证时,应停止且不发布。只有完成这项独立验证后,才能执行下方命令。 ```bash -gh release edit v1.1.1 --repo abruption/session-peer \ +gh release edit v1.1.2 --repo abruption/session-peer \ --draft=false --prerelease=false --latest ``` diff --git a/docs/README.md b/docs/README.md index 510b504e..91f33d28 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,6 +24,8 @@ installation paths. ## Development and history +- [v1.1.2 candidate notes](releases/v1.1.2.md) describe the unpublished #298 Claude PID-record and nondecimal Unicode-name fix prepared on 2026-10-13; CI and publication gates remain separate. + - [v1.1.1 maintenance notes](releases/v1.1.1.md) cover the documentation/test maintenance target prepared on 2026-10-12; runtime behavior is unchanged from 1.1.0. Verify package publication separately. - [Experimental handoff runtime](handoff-runtime.md) describes the limited private-receipt support shipped in 1.1.0, supported routes and remaining acceptance gates. diff --git a/docs/i18n-manifest.json b/docs/i18n-manifest.json index 1a1f0301..d497e8a4 100644 --- a/docs/i18n-manifest.json +++ b/docs/i18n-manifest.json @@ -7,10 +7,10 @@ "zh-CN" ], "sources": { - "README.md": "472a099a0378cc55d06c71ae87ee2d11796f274deee99fcba3c64f1a78163afa", - "RELEASING.md": "70677f3ff80e1098d07e14dfa488295fd11ba85f1fb7d8a0ceafb83d675d5e5e", + "README.md": "c8c0188cc763a96567aa1198fe17c3bd5a284fe6fae913ddca18d2e0fa643792", + "RELEASING.md": "36e8f1b9afd057b56767bfb6fb5c18e8acca3e7c16d39b0b8f8d05dc22522e3e", "SECURITY.md": "8f018f6f98002312e143609b9734aeb971ca5217e146597c06b57b5645031046", - "docs/README.md": "da3dbbd23eeeda76e2c05d8586e6d2dac31acee372ac2f901c756dfeda2ed463", + "docs/README.md": "67353922611866276190c06ba0a4dcf35725057d61c5ceaf3369db175f82fa96", "docs/agent-adapters.md": "bf185d3121c94829bb412071a25c4d943302e10a6b2e713111e653b8f8c3ea91", "docs/ai-assistant-guide.md": "9624ec96b92fb688fae3be3c94cb3564c77985091be451fbf1f056a1af9b9d9c", "docs/antigravity.md": "f2b5122a0ce6e6fb5bd4d8ed50ce2c8bd2dc9c98ec8ce328405cf7c2cd24229d", @@ -49,6 +49,7 @@ "docs/releases/v1.0.4.md": "5185d9ff93a98077621ec017e54f73138dcf05fe9de121b073f9a097240b40b4", "docs/releases/v1.1.0.md": "66e66c4ca2d18b4da03c169885443ce26af49885fae474e95476b9078e50252f", "docs/releases/v1.1.1.md": "e7d5b6060098be0d4db06a7c2845955ea46084cdecd3fe8e5dc6c44324c09cc0", + "docs/releases/v1.1.2.md": "3ffe3d6daa8e403e9f3c5e83ec81c3afa9f10750627b31362990ec5b92326f3a", "docs/validation/69-rc.md": "e490c6a9ad0fec9108b91c74922cedac7ddf214ed405f8f5523da22f48a4b222", "docs/validation/85-antigravity.md": "468ba1702a60c57079730ab5b7545b0b1968f11cc6832f0ac88460664ba87723", "docs/wake.md": "cde135da2ea7be21c815bb265e694f737dbe467d8e76bb82eb118e3b86e29d22", diff --git a/docs/ja/README.md b/docs/ja/README.md index 77d7a718..be6ab8a5 100644 --- a/docs/ja/README.md +++ b/docs/ja/README.md @@ -23,6 +23,8 @@ ## 開発と履歴 +- [v1.1.2候補ノート](releases/v1.1.2.md)は2026-10-13に準備した未公開の#298 Claude PID記録と非十進Unicode名の修正を説明します。CIと公開の承認手順は別です。 + - [v1.1.1メンテナンスノート](releases/v1.1.1.md)は2026-10-12に準備したドキュメント・テストのメンテナンス版を説明し、ランタイムの動作は1.1.0と同じです。パッケージ公開は別途確認してください。 - [実験的なHandoffランタイム](handoff-runtime.md)は、1.1.0に含まれる限定的な非公開受領確認機能、対応経路、残る受け入れ条件を説明します。 diff --git a/docs/ja/releases/v1.1.2.md b/docs/ja/releases/v1.1.2.md new file mode 100644 index 00000000..54fe6925 --- /dev/null +++ b/docs/ja/releases/v1.1.2.md @@ -0,0 +1,21 @@ +# v1.1.2パッチ候補 + +## 範囲 + +公開済み1.1.1を基に2026-10-13に準備しました。この候補は未公開で、#298のみを修正します。準備はマージ、タグ作成、公開、配備の承認ではありません。 + +## 変更点 + +- OSによるプロセス検査の前にClaude登録記録のPIDを範囲内で解析し、不正な記録を個別に扱って正常なセッションの検出を維持します。 +- ²のような非十進Unicodeのセッション名は整数変換ではなく名前で照合します。有効なPID指定と重複した名前の拒否は維持します。 +- 診断にも同じ範囲制限付きの記録処理を適用し、権限に関する診断を維持して単独実行CLIを再生成します。 + +## 検証と既知の制限 + +不正なファイル名、大きすぎるPID、正常なセッションの保持、Unicode名での対象選択、メッセージを送信しない実行前の失敗に対する回帰テストが必要です。公開前には、正確な最新コミットのプラットフォームCI、全ローカルテスト、単独ファイルの再生成、4言語の検査、隔離したwheel/sdistのインストール、最新の依存関係監査が必要です。準備ノートはこれらの条件を通過した証拠ではありません。 + +新たな実環境でのClaude、Codex、Antigravity、SSH、Relay、各端末の検証は主張しません。WindowsのPID再利用、プロセス開始時刻、所有者の確認は引き続き必要で、新たな通信経路やエージェント対応は追加しません。 + +実験的なHandoffは[Handoffガイド](../handoff-runtime.md)に記載した1.1.0の範囲を維持します。メッセージの送信は消費やACKではなく、結果が不明な場合は自動再送を許可しません。過去のRC4 ACK数は20/21のままで、#161は原因未確定の監視対象です。 + +パッケージ公開には運用環境への配備を含みません。Control/Relayサービスと各端末は別途の承認と検証が必要です。GitHubの不変資産と必須の人手によるPyPI環境レビューは別の条件です。 diff --git a/docs/ko/README.md b/docs/ko/README.md index 37af8422..1fe59dc0 100644 --- a/docs/ko/README.md +++ b/docs/ko/README.md @@ -23,6 +23,8 @@ ## 개발과 이력 +- [v1.1.2 후보 노트](releases/v1.1.2.md)는 2026-10-13에 준비한 미공개 #298 Claude PID 기록 및 비십진 유니코드 이름 수정을 설명합니다. CI와 공개 승인 절차는 별개입니다. + - [v1.1.1 유지보수 노트](releases/v1.1.1.md)는 2026-10-12에 준비한 문서·테스트 유지보수 버전을 설명하며 런타임 동작은 1.1.0과 같습니다. 패키지 공개는 별도로 확인하십시오. - [실험적 Handoff 런타임](handoff-runtime.md)은 1.1.0에 포함된 제한적 비공개 수신 확인 기능, 지원 경로와 남은 수락 조건을 설명합니다. diff --git a/docs/ko/releases/v1.1.2.md b/docs/ko/releases/v1.1.2.md new file mode 100644 index 00000000..09dc526d --- /dev/null +++ b/docs/ko/releases/v1.1.2.md @@ -0,0 +1,21 @@ +# v1.1.2 패치 후보 + +## 범위 + +공개된 1.1.1을 기준으로 2026-10-13에 준비했습니다. 이 후보는 미공개이며 #298만 수정합니다. 준비 작업은 병합·태그 생성·공개·운영 배포 승인이 아닙니다. + +## 변경 사항 + +- 운영체제의 프로세스 검사 전에 Claude 등록 기록의 PID를 범위 내에서 해석하고, 잘못된 기록을 개별 처리하여 정상 세션 검색을 유지합니다. +- ²와 같은 비십진 유니코드 세션 이름은 정수 변환 대신 이름으로 선택합니다. 유효한 PID 지정과 중복 이름 거부는 유지합니다. +- 진단에도 같은 범위 제한 기록 처리를 적용하고 권한 진단을 유지하며 독립 실행 CLI를 재생성합니다. + +## 검증과 알려진 한계 + +잘못된 파일 이름, 지나치게 큰 PID, 정상 세션 유지, 유니코드 이름 지정 및 메시지 제출 없이 발생하는 전달 전 실패에 대한 회귀 테스트가 필요합니다. 공개 전에 정확한 최신 커밋의 플랫폼 CI, 전체 로컬 테스트, 독립 실행 파일 재생성, 네 언어 검사, 격리된 wheel/sdist 설치와 최신 의존성 감사를 완료해야 합니다. 준비 노트는 이 조건을 통과했다는 증거가 아닙니다. + +새로운 실제 Claude·Codex·Antigravity·SSH·Relay 또는 장비 검증 결과를 주장하지 않습니다. Windows PID 재사용, 프로세스 시작 시각과 소유자 검사는 계속 필요하며 새 전송 경로나 에이전트 지원은 추가하지 않습니다. + +실험적 Handoff는 [Handoff 가이드](../handoff-runtime.md)에 설명된 1.1.0 범위를 유지합니다. 메시지 제출은 소비나 ACK가 아니며 결과가 불확실하면 자동 재전송을 허용하지 않습니다. 과거 RC4 ACK 수는 20/21로 유지하며 #161은 원인 미확정 상태로 관찰을 계속합니다. + +패키지 공개에는 운영 배포가 포함되지 않습니다. Control/Relay 서비스와 설치된 장비는 별도 승인과 검증이 필요합니다. GitHub 불변 자산과 필수 사람 PyPI 환경 검토는 별도의 조건입니다. diff --git a/docs/releases/v1.1.2.md b/docs/releases/v1.1.2.md new file mode 100644 index 00000000..bed04908 --- /dev/null +++ b/docs/releases/v1.1.2.md @@ -0,0 +1,21 @@ +# v1.1.2 patch candidate + +## Scope + +Prepared on 2026-10-13 against published 1.1.1. This candidate is unpublished and addresses #298 only; preparation does not authorize merge, tag creation, publication or deployment. + +## Changes + +- Bound Claude registry PID parsing before native process inspection and isolate invalid records so healthy sessions remain discoverable. +- Resolve nondecimal Unicode session names such as ² through name matching instead of integer conversion; preserve valid PID addressing and ambiguous-name refusal. +- Apply the same bounded record handling to diagnostics, retain permission diagnostics and regenerate the standalone CLI. + +## Validation and known limits + +Require regression tests for malformed filenames, oversized PIDs, healthy-session retention, Unicode-name targeting and pre-effect failures with zero message submissions. Require exact-head platform CI, full local tests, standalone regeneration, four-locale checks, isolated wheel/sdist installs and current dependency audits before publication. Preparation notes are not evidence that these gates passed. + +No new live Claude, Codex, Antigravity, SSH, Relay or fleet acceptance is established. Windows PID reuse, process-start and ownership checks remain required; no new transport or agent support is added. + +Experimental handoff retains the 1.1.0 scope described in the [handoff guide](../handoff-runtime.md). Submitted messages are not consumed messages or ACKs; unknown outcomes never authorize automatic resend. The historical RC4 ACK count remains 20/21 and #161 remains monitoring with its root cause unconfirmed. + +Package publication does not include operational deployment. Control/Relay services and installed devices require separate authorization and verification. GitHub immutable assets and required human PyPI environment review remain separate gates. diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index ffe68d64..f44e13c9 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -22,6 +22,8 @@ ## 开发与历史 +- [v1.1.2候选说明](releases/v1.1.2.md)介绍2026-10-13准备的未发布#298 Claude PID记录和非十进制Unicode名称修复。CI与发布审批是独立步骤。 + - [v1.1.1维护说明](releases/v1.1.1.md)介绍2026-10-12准备的文档与测试维护版本;运行时行为与1.1.0相同。软件包发布需单独确认。 - [实验性Handoff运行时](handoff-runtime.md)说明1.1.0中已发布的有限私有收据功能、支持路径及剩余验收条件。 diff --git a/docs/zh-CN/releases/v1.1.2.md b/docs/zh-CN/releases/v1.1.2.md new file mode 100644 index 00000000..91453aa0 --- /dev/null +++ b/docs/zh-CN/releases/v1.1.2.md @@ -0,0 +1,21 @@ +# v1.1.2补丁候选版本 + +## 范围 + +于2026-10-13基于已发布的1.1.1准备。此候选版本尚未发布,仅修复#298。准备不代表批准合并、创建标签、发布或部署。 + +## 变更 + +- 在操作系统进程检查之前,按范围限制解析Claude登记记录的PID,逐条处理无效记录,确保正常会话仍可被发现。 +- ²等非十进制Unicode会话名称按名称匹配,不再进行整数转换;保留有效PID指定和重名拒绝行为。 +- 诊断使用相同的有界记录处理,保留权限诊断,并重新生成独立CLI。 + +## 验证与已知限制 + +必须针对无效文件名、过大PID、正常会话保留、Unicode名称选择以及不提交消息的执行前失败添加回归测试。发布前必须完成准确最新提交的平台CI、完整本地测试、独立文件再生成、四语言检查、隔离的wheel/sdist安装及最新依赖审计。准备说明不能证明这些条件已通过。 + +不声称完成新的实际Claude、Codex、Antigravity、SSH、Relay或各设备验收。Windows PID复用、进程启动时间和所有者检查仍是必要条件,不增加新传输路径或代理支持。 + +实验性Handoff保持[Handoff指南](../handoff-runtime.md)所述的1.1.0范围。消息提交不代表消费或ACK;结果不确定时不得自动重发。历史RC4 ACK数保持20/21,#161继续作为原因未确认的监控项目。 + +软件包发布不包含运营部署。Control/Relay服务和已安装设备需要另行批准与验证。GitHub不可变资产和必要的人工PyPI环境审核是独立条件。 diff --git a/session_peer.py b/session_peer.py index e8629f37..0b327dab 100644 --- a/session_peer.py +++ b/session_peer.py @@ -45,7 +45,7 @@ except ImportError: # Native Windows uses its own read-only writer inspection. fcntl = None -__version__ = "1.1.1" +__version__ = "1.1.2" GITHUB_REPO = "abruption/session-peer" # Claude Code refuses a same-machine message once its serialized form passes @@ -1548,6 +1548,23 @@ def sessions_dir() -> Path: MAX_CLAUDE_RECORD_BYTES = 1024 * 1024 +def parse_claude_pid(value) -> int | None: + """Parse a bounded native PID without confusing digit-like names with PIDs. + + Decimal Unicode digits remain compatible with numeric addressing. Other + numeric characters (for example superscripts) are names, not decimal PIDs. + Reject invalid integers before signal inspection or Windows DWORD coercion. + """ + if isinstance(value, str): + if len(value) > 10 or not value.isdecimal(): + return None + value = int(value) + if type(value) is not int: + return None + maximum = (1 << 32) - 1 if IS_WINDOWS else (1 << 31) - 1 + return value if 1 < value <= maximum else None + + def read_claude_record(path: Path) -> dict: """Read bounded regular-file metadata; leave permission errors to callers. @@ -1587,7 +1604,8 @@ def read_claude_record(path: Path) -> dict: record = json.loads(b"".join(chunks).decode("utf-8", errors="strict")) except RecursionError as exc: raise ValueError("Claude session record is too deeply nested") from exc - if not isinstance(record, dict) or type(record.get("pid")) is not int: + if (not isinstance(record, dict) or type(record.get("pid")) is not int + or parse_claude_pid(record.get("pid")) is None): raise ValueError("Claude session record must be an object with an integer PID") for field in ("name", "cwd", "status", "messagingSocketPath"): value = record.get(field) @@ -1604,7 +1622,7 @@ def read_claude_record(path: Path) -> dict: def windows_process_start_ms(pid: int) -> int | None: """Read a live Windows process creation time without trusting a stale PID.""" - if pid <= 1: + if parse_claude_pid(pid) is None or type(pid) is not int: return None import ctypes from ctypes import wintypes @@ -1642,7 +1660,7 @@ class FILETIME(ctypes.Structure): def pid_alive(pid: int) -> bool: - if pid <= 1: + if parse_claude_pid(pid) is None or type(pid) is not int: return False if IS_WINDOWS: return windows_process_start_ms(pid) is not None @@ -1658,7 +1676,7 @@ def pid_alive(pid: int) -> bool: def claude_process_state(record: dict, record_pid: int) -> tuple[bool, str | None]: """Fail closed when a Windows session record outlives its process identity.""" pid = record.get("pid") - if type(pid) is not int or pid != record_pid: + if type(pid) is not int or parse_claude_pid(pid) is None or pid != record_pid: return False, "record_pid_mismatch" if not IS_WINDOWS: return pid_alive(pid), None @@ -1689,7 +1707,8 @@ def discover(include_unreachable: bool = False) -> list[dict]: except OSError as exc: raise CcPeerError(f"Cannot read Claude sessions at {directory}: {exc}") from exc for record_file in entries: - if not record_file.stem.isdigit(): + record_pid = parse_claude_pid(record_file.stem) + if record_pid is None: continue try: record = read_claude_record(record_file) @@ -1701,7 +1720,7 @@ def discover(include_unreachable: bool = False) -> list[dict]: continue sock = record.get("messagingSocketPath") or "" - alive, stale_reason = claude_process_state(record, int(record_file.stem)) + alive, stale_reason = claude_process_state(record, record_pid) if IS_WINDOWS: has_inbox = bool(sock) and sock.startswith("\\\\.\\pipe\\") else: @@ -1732,8 +1751,9 @@ def resolve_target(sessions: list[dict], target: str) -> dict: """Find one session by name, or by pid when the target is all digits.""" reachable = [s for s in sessions if s["reachable"]] - if target.isdigit(): - matches = [s for s in reachable if s["pid"] == int(target)] + if target.isdecimal(): + target_pid = parse_claude_pid(target) + matches = [s for s in reachable if s["pid"] == target_pid] else: wanted = target.casefold() matches = [s for s in reachable if (s["name"] or "").casefold() == wanted] @@ -3783,7 +3803,10 @@ def diagnose_claude() -> dict: permission_failures = 0 for record_file in entries: - if not record_file.stem.isdigit(): + record_pid = parse_claude_pid(record_file.stem) + if record_pid is None: + if record_file.stem.isdigit(): + result["invalidRecords"] += 1 continue try: record = read_claude_record(record_file) @@ -3798,7 +3821,7 @@ def diagnose_claude() -> dict: result["invalidRecords"] += 1 continue result["records"] += 1 - alive, stale_reason = claude_process_state(record, int(record_file.stem)) + alive, stale_reason = claude_process_state(record, record_pid) if stale_reason: result["staleRecords"] += 1 if alive: @@ -8249,7 +8272,7 @@ def handoff_remote_request_semantics(context): raise handoff_error("invalid_remote_native_request") if context["generation"] is not None: validate_target_generation(context["generation"]) - if context["phase"] == "effect" and (context["generation"] is None or not context["target"].isdigit() or int(context["target"]) <= 1): + if context["phase"] == "effect" and (context["generation"] is None or parse_claude_pid(context["target"]) is None): raise handoff_error("invalid_remote_native_request") elif context["generation"] is not None: raise handoff_error("invalid_remote_native_request") @@ -8374,9 +8397,11 @@ def handoff_remote_context_native(context, native, native_context, generation): if type(native.get("target")) is not dict or type(native["target"].get("pid")) is not int: raise handoff_error("invalid_remote_native_profile") target, home = str(native["target"]["pid"]), None + if parse_claude_pid(native["target"]["pid"]) is None: + raise handoff_error("invalid_remote_native_profile") requested = context["target"] - if (requested.isdigit() and int(requested) != int(target) or - not requested.isdigit() and (native["target"].get("name") or "").casefold() != requested.casefold()): + if (requested.isdecimal() and parse_claude_pid(requested) != int(target) or + not requested.isdecimal() and (native["target"].get("name") or "").casefold() != requested.casefold()): raise handoff_error("invalid_remote_native_profile") if context["generation"] is not None and generation != context["generation"]: raise handoff_error("invalid_remote_native_profile") diff --git a/session_peer_core/claude.py b/session_peer_core/claude.py index 6cb64e9d..115b4954 100644 --- a/session_peer_core/claude.py +++ b/session_peer_core/claude.py @@ -23,6 +23,23 @@ def sessions_dir() -> Path: MAX_CLAUDE_RECORD_BYTES = 1024 * 1024 +def parse_claude_pid(value) -> int | None: + """Parse a bounded native PID without confusing digit-like names with PIDs. + + Decimal Unicode digits remain compatible with numeric addressing. Other + numeric characters (for example superscripts) are names, not decimal PIDs. + Reject invalid integers before signal inspection or Windows DWORD coercion. + """ + if isinstance(value, str): + if len(value) > 10 or not value.isdecimal(): + return None + value = int(value) + if type(value) is not int: + return None + maximum = (1 << 32) - 1 if IS_WINDOWS else (1 << 31) - 1 + return value if 1 < value <= maximum else None + + def read_claude_record(path: Path) -> dict: """Read bounded regular-file metadata; leave permission errors to callers. @@ -62,7 +79,8 @@ def read_claude_record(path: Path) -> dict: record = json.loads(b"".join(chunks).decode("utf-8", errors="strict")) except RecursionError as exc: raise ValueError("Claude session record is too deeply nested") from exc - if not isinstance(record, dict) or type(record.get("pid")) is not int: + if (not isinstance(record, dict) or type(record.get("pid")) is not int + or parse_claude_pid(record.get("pid")) is None): raise ValueError("Claude session record must be an object with an integer PID") for field in ("name", "cwd", "status", "messagingSocketPath"): value = record.get(field) @@ -79,7 +97,7 @@ def read_claude_record(path: Path) -> dict: def windows_process_start_ms(pid: int) -> int | None: """Read a live Windows process creation time without trusting a stale PID.""" - if pid <= 1: + if parse_claude_pid(pid) is None or type(pid) is not int: return None import ctypes from ctypes import wintypes @@ -117,7 +135,7 @@ class FILETIME(ctypes.Structure): def pid_alive(pid: int) -> bool: - if pid <= 1: + if parse_claude_pid(pid) is None or type(pid) is not int: return False if IS_WINDOWS: return windows_process_start_ms(pid) is not None @@ -133,7 +151,7 @@ def pid_alive(pid: int) -> bool: def claude_process_state(record: dict, record_pid: int) -> tuple[bool, str | None]: """Fail closed when a Windows session record outlives its process identity.""" pid = record.get("pid") - if type(pid) is not int or pid != record_pid: + if type(pid) is not int or parse_claude_pid(pid) is None or pid != record_pid: return False, "record_pid_mismatch" if not IS_WINDOWS: return pid_alive(pid), None @@ -164,7 +182,8 @@ def discover(include_unreachable: bool = False) -> list[dict]: except OSError as exc: raise CcPeerError(f"Cannot read Claude sessions at {directory}: {exc}") from exc for record_file in entries: - if not record_file.stem.isdigit(): + record_pid = parse_claude_pid(record_file.stem) + if record_pid is None: continue try: record = read_claude_record(record_file) @@ -176,7 +195,7 @@ def discover(include_unreachable: bool = False) -> list[dict]: continue sock = record.get("messagingSocketPath") or "" - alive, stale_reason = claude_process_state(record, int(record_file.stem)) + alive, stale_reason = claude_process_state(record, record_pid) if IS_WINDOWS: has_inbox = bool(sock) and sock.startswith("\\\\.\\pipe\\") else: @@ -207,8 +226,9 @@ def resolve_target(sessions: list[dict], target: str) -> dict: """Find one session by name, or by pid when the target is all digits.""" reachable = [s for s in sessions if s["reachable"]] - if target.isdigit(): - matches = [s for s in reachable if s["pid"] == int(target)] + if target.isdecimal(): + target_pid = parse_claude_pid(target) + matches = [s for s in reachable if s["pid"] == target_pid] else: wanted = target.casefold() matches = [s for s in reachable if (s["name"] or "").casefold() == wanted] diff --git a/session_peer_core/common.py b/session_peer_core/common.py index b8d0191d..f99d0669 100644 --- a/session_peer_core/common.py +++ b/session_peer_core/common.py @@ -45,7 +45,7 @@ except ImportError: # Native Windows uses its own read-only writer inspection. fcntl = None -__version__ = "1.1.1" +__version__ = "1.1.2" GITHUB_REPO = "abruption/session-peer" # Claude Code refuses a same-machine message once its serialized form passes diff --git a/session_peer_core/diagnostics.py b/session_peer_core/diagnostics.py index 816bc7f1..2ebfe6ed 100644 --- a/session_peer_core/diagnostics.py +++ b/session_peer_core/diagnostics.py @@ -58,7 +58,10 @@ def diagnose_claude() -> dict: permission_failures = 0 for record_file in entries: - if not record_file.stem.isdigit(): + record_pid = parse_claude_pid(record_file.stem) + if record_pid is None: + if record_file.stem.isdigit(): + result["invalidRecords"] += 1 continue try: record = read_claude_record(record_file) @@ -73,7 +76,7 @@ def diagnose_claude() -> dict: result["invalidRecords"] += 1 continue result["records"] += 1 - alive, stale_reason = claude_process_state(record, int(record_file.stem)) + alive, stale_reason = claude_process_state(record, record_pid) if stale_reason: result["staleRecords"] += 1 if alive: diff --git a/session_peer_core/handoff_remote.py b/session_peer_core/handoff_remote.py index cd0ba93a..f14a3207 100644 --- a/session_peer_core/handoff_remote.py +++ b/session_peer_core/handoff_remote.py @@ -416,7 +416,7 @@ def handoff_remote_request_semantics(context): raise handoff_error("invalid_remote_native_request") if context["generation"] is not None: validate_target_generation(context["generation"]) - if context["phase"] == "effect" and (context["generation"] is None or not context["target"].isdigit() or int(context["target"]) <= 1): + if context["phase"] == "effect" and (context["generation"] is None or parse_claude_pid(context["target"]) is None): raise handoff_error("invalid_remote_native_request") elif context["generation"] is not None: raise handoff_error("invalid_remote_native_request") @@ -541,9 +541,11 @@ def handoff_remote_context_native(context, native, native_context, generation): if type(native.get("target")) is not dict or type(native["target"].get("pid")) is not int: raise handoff_error("invalid_remote_native_profile") target, home = str(native["target"]["pid"]), None + if parse_claude_pid(native["target"]["pid"]) is None: + raise handoff_error("invalid_remote_native_profile") requested = context["target"] - if (requested.isdigit() and int(requested) != int(target) or - not requested.isdigit() and (native["target"].get("name") or "").casefold() != requested.casefold()): + if (requested.isdecimal() and parse_claude_pid(requested) != int(target) or + not requested.isdecimal() and (native["target"].get("name") or "").casefold() != requested.casefold()): raise handoff_error("invalid_remote_native_profile") if context["generation"] is not None and generation != context["generation"]: raise handoff_error("invalid_remote_native_profile") diff --git a/tests/core/test_claude_records.py b/tests/core/test_claude_records.py index 95361109..3c791690 100644 --- a/tests/core/test_claude_records.py +++ b/tests/core/test_claude_records.py @@ -225,3 +225,86 @@ def test_windows_fallback_keeps_bounded_regular_file_reads(self): self.assertEqual(peer.read_claude_record(self.directory / '101.json'), self.healthy) flags = opened.call_args.args[1] self.assertEqual(flags, os.O_RDONLY | getattr(os, 'O_BINARY', 0)) + + def test_invalid_pid_filenames_preserve_healthy_cli_results(self): + for stem in ('²', '99999999999999999999', '0', '1', '-101'): + self.write(stem, json.dumps(self.healthy).encode()) + self.assertEqual([row['pid'] for row in peer.discover(include_unreachable=True)], [101]) + code, listing = self.invoke('list', '--agent', 'claude') + self.assertEqual(code, 0) + self.assertTrue(listing['ok']) + self.assertEqual([row['pid'] for row in listing['sessions']], [101]) + code, doctor = self.invoke('doctor') + self.assertEqual(code, 0) + self.assertEqual(doctor['claude']['status'], 'available') + self.assertEqual(doctor['claude']['records'], 1) + self.assertEqual(doctor['claude']['availableInboxes'], 1) + with mock.patch.object(peer, 'post_to_socket') as post: + code, dry_run = self.invoke('send', '--to', 'worker', '--dry-run', + '--no-from', '--no-reply-to', '--message', 'hello') + self.assertEqual(code, 0) + self.assertTrue(dry_run['ok']) + self.assertEqual(dry_run['target'], {'pid': 101, 'name': 'worker'}) + post.assert_not_called() + + def test_nondecimal_unicode_session_name_is_not_parsed_as_pid(self): + self.healthy['name'] = '²' + self.write(101, json.dumps(self.healthy).encode()) + with mock.patch.object(peer, 'post_to_socket') as post: + code, result = self.invoke('send', '--to', '²', '--dry-run', + '--no-from', '--no-reply-to', '--message', 'hello') + self.assertEqual(code, 0) + self.assertEqual(result['target'], {'pid': 101, 'name': '²'}) + post.assert_not_called() + + def test_decimal_pid_addressing_preserves_ascii_and_unicode_digits(self): + for target in ('101', '١٠١', '00101'): + with self.subTest(target=target), mock.patch.object(peer, 'post_to_socket') as post: + code, result = self.invoke('send', '--to', target, '--dry-run', + '--no-from', '--no-reply-to', '--message', 'hello') + self.assertEqual(code, 0) + self.assertEqual(result['target'], {'pid': 101, 'name': 'worker'}) + post.assert_not_called() + + def test_invalid_record_pids_are_rejected_before_process_inspection(self): + maximum = (2 ** 32 - 1) if peer.IS_WINDOWS else (2 ** 31 - 1) + for pid in (True, False, 0, 1, -1, maximum + 1, 10 ** 100): + with self.subTest(pid=pid): + path = self.write(200, json.dumps({**self.healthy, 'pid': pid}).encode()) + with self.assertRaises(ValueError): + peer.read_claude_record(path) + self.assertEqual([row['pid'] for row in peer.discover(include_unreachable=True)], [101]) + + +class ClaudePidBounds(unittest.TestCase): + def test_parser_bounds_native_pid_domain_and_decimal_lexemes(self): + for windows, maximum in ((False, 2 ** 31 - 1), (True, 2 ** 32 - 1)): + with self.subTest(windows=windows), mock.patch.object(peer, 'IS_WINDOWS', windows): + for value, expected in ((2, 2), (maximum, maximum), ('101', 101), + ('١٠١', 101), ('00101', 101), (str(maximum), maximum)): + with self.subTest(value=value): + self.assertEqual(peer.parse_claude_pid(value), expected) + for value in (None, True, False, 101.0, 0, 1, -1, maximum + 1, + '', '²', '+101', '-101', ' 101', '101 ', '1e2', + '99999999999999999999', '0' * 10000 + '101', str(maximum + 1)): + with self.subTest(value=repr(value)[:80]): + self.assertIsNone(peer.parse_claude_pid(value)) + + def test_invalid_pid_never_reaches_os_kill_or_windows_process_probe(self): + for windows, maximum in ((False, 2 ** 31 - 1), (True, 2 ** 32 - 1)): + with mock.patch.object(peer, 'IS_WINDOWS', windows), \ + mock.patch.object(peer.os, 'kill') as kill, \ + mock.patch.object(peer, 'windows_process_start_ms') as probe: + for value in (None, True, False, 101.0, '101', 0, 1, -1, maximum + 1, 10 ** 100): + with self.subTest(windows=windows, value=value): + self.assertFalse(peer.pid_alive(value)) + kill.assert_not_called() + probe.assert_not_called() + + def test_windows_process_helper_rejects_invalid_pid_before_ctypes(self): + with mock.patch.object(peer, 'IS_WINDOWS', True), \ + mock.patch('ctypes.WinDLL', create=True) as loader: + for value in (None, True, False, 101.0, '101', 0, 1, -1, 2 ** 32, 10 ** 100): + with self.subTest(value=value): + self.assertIsNone(peer.windows_process_start_ms(value)) + loader.assert_not_called() diff --git a/tests/core/test_handoff_remote.py b/tests/core/test_handoff_remote.py index 2f8fe0bc..57891182 100644 --- a/tests/core/test_handoff_remote.py +++ b/tests/core/test_handoff_remote.py @@ -88,6 +88,23 @@ def test_probe_claude_has_no_effect_and_validated_original_generation(self): for missing in ("status", "submitted", "consumptionConfirmed"): self.assertNotIn(missing, native) + def test_probe_nondecimal_claude_name_and_bounded_effect_pid(self): + self.session["name"] = "²" + context = self.context() + context["target"] = "²" + code, response, socket, _, _ = self.invoke(context) + self.assertEqual(code, 0, response) + socket.assert_not_called() + native = peer.handoff_validate_remote_response(response, context) + self.assertEqual(native["target"], {"pid": 4242, "name": "²"}) + for target in ("²", "99999999999999999999", "0", "1"): + invalid = self.effect() + invalid["target"] = target + with self.subTest(target=target): + code, response, socket, _, _ = self.invoke(invalid) + self.assertEqual(code, 1, response) + socket.assert_not_called() + def test_effect_claude_quotes_every_line_once_without_receipt_authority(self): context = self.effect() code, response, socket, _, generation = self.invoke(context) diff --git a/tests/docs/test_docs_i18n.py b/tests/docs/test_docs_i18n.py index 0cd25dd7..a297b6b5 100644 --- a/tests/docs/test_docs_i18n.py +++ b/tests/docs/test_docs_i18n.py @@ -64,7 +64,7 @@ class DocumentationTranslationsTest(unittest.TestCase): def test_every_canonical_document_has_each_translation(self): sources = canonical_documents() - self.assertEqual(46, len(sources), "Update the translation manifest for new canonical docs") + self.assertEqual(47, len(sources), "Update the translation manifest for new canonical docs") canonical_docs = { path.relative_to(ROOT / "docs") for path in (ROOT / "docs").rglob("*.md") diff --git a/tests/docs/test_release_commands.py b/tests/docs/test_release_commands.py index b3a02498..299ac211 100644 --- a/tests/docs/test_release_commands.py +++ b/tests/docs/test_release_commands.py @@ -20,7 +20,7 @@ def test_current_publication_requires_completed_preparation_and_asset_review(sel with self.subTest(document=name): text = (ROOT / name).read_text(encoding="utf-8") dispatch = text.index(' --ref main -f tag="$release_tag"') - publication = text.index("gh release edit v1.1.1") + publication = text.index("gh release edit v1.1.2") gate = text[dispatch:publication] for marker in ("gh run watch ", "--exit-status", "attach-draft", "SHA256SUMS", "release-provenance.json"): @@ -72,7 +72,7 @@ def test_release_preparation_commands_preserve_argument_boundaries(self): with self.subTest(document=name): blocks = re.findall(r"```bash\n(.*?)```", (ROOT / name).read_text(), re.DOTALL) self.assertEqual(len(blocks), 5, "current/historical preparation and publication plus installer remain distinct") - for offset, version in ((0, "v1.1.1"), (2, "v1.1.0")): + for offset, version in ((0, "v1.1.2"), (2, "v1.1.0")): calls, _ = self.shell_calls(blocks[offset]) self.assertEqual(calls, [ ["git", "fetch", "origin", "main", "--tags"], diff --git a/tests/docs/test_stable_candidate.py b/tests/docs/test_stable_candidate.py index 2e99cb43..d858fe8a 100644 --- a/tests/docs/test_stable_candidate.py +++ b/tests/docs/test_stable_candidate.py @@ -10,6 +10,18 @@ class StableCandidate(unittest.TestCase): + def test_pid_patch_candidate_remains_unpublished_and_narrow(self): + for locale in ("", "ko/", "ja/", "zh-CN/"): + note = (ROOT / ("docs/" + locale + "releases/v1.1.2.md")).read_text(encoding="utf-8") + for marker in ("#298", "1.1.1", "1.1.2", "2026-10-13", "²", + "Control/Relay", "20/21", "#161", "handoff-runtime.md"): + with self.subTest(locale=locale, marker=marker): + self.assertIn(marker, note) + english = (ROOT / "docs/releases/v1.1.2.md").read_text(encoding="utf-8") + self.assertIn("candidate is unpublished", english) + self.assertIn("preparation does not authorize merge", english) + self.assertIn("Package publication does not include operational deployment", english) + def test_maintenance_notes_do_not_claim_runtime_remediation(self): for locale in ("", "ko/", "ja/", "zh-CN/"): note = (ROOT / ("docs/" + locale + "releases/v1.1.1.md")).read_text(encoding="utf-8") @@ -106,8 +118,8 @@ def test_published_release_keeps_experimental_acceptance_gates(self): self.assertIn(marker, english) def test_package_and_generated_core_version_agree(self): - self.assertEqual(session_peer.__version__, "1.1.1") - self.assertIn('__version__ = "1.1.1"', + self.assertEqual(session_peer.__version__, "1.1.2") + self.assertIn('__version__ = "1.1.2"', (ROOT / "session_peer_core/common.py").read_text(encoding="utf-8")) def test_all_current_readmes_and_runbooks_use_stable_version(self):