From fba0e742fc9d04c1a1a600a7c6b59fe0cd7421bd Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Mon, 19 Oct 2026 02:02:29 -0600 Subject: [PATCH 1/3] docs(audit): archive security and dependency evidence --- .../SHA256SUMS.txt | 11 ++++ .../commands.txt | 6 ++ .../environment.txt | 9 +++ .../linux-py310-audit.json | 1 + .../linux-py310-audit.stderr | 3 + .../linux-py310-requirements.txt | 56 +++++++++++++++++++ .../local-venv-py312-audit.json | 1 + .../local-venv-py312-audit.stderr | 1 + .../secret-heuristics.txt | 3 + .../windows-py312-audit.json | 1 + .../windows-py312-audit.stderr | 3 + .../windows-py312-requirements.txt | 47 ++++++++++++++++ 12 files changed, 142 insertions(+) create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/SHA256SUMS.txt create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/commands.txt create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/environment.txt create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/linux-py310-audit.json create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/linux-py310-audit.stderr create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/linux-py310-requirements.txt create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/local-venv-py312-audit.json create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/local-venv-py312-audit.stderr create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/secret-heuristics.txt create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/windows-py312-audit.json create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/windows-py312-audit.stderr create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/windows-py312-requirements.txt diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/SHA256SUMS.txt b/docs/audits/2026-10-19-security-deps-87b58ea/SHA256SUMS.txt new file mode 100644 index 0000000..4afda21 --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/SHA256SUMS.txt @@ -0,0 +1,11 @@ +ecfc3c2e1801f5063d084d5bd67ac68572b13ab1212759885ea86cb4e4413c12 commands.txt +cf7ae852bb55ecdad6e990c61b42c3552743c0ba046011a5d8b4442ab306f8f5 environment.txt +2c7b55c7bfe875b233388e847e4f2c62c75005c7ff20653453c21c6688d3dc58 linux-py310-audit.json +79d30422388543b2563e1dde76bb7e7857d7ef0c6fdf05c8c71598633ca07e1e linux-py310-audit.stderr +72b6b78d2bda38834772ea0af2dbbdde3ca6d3aa1d3cc4b24e06d86d16537396 linux-py310-requirements.txt +8381dbda502f94962e1b89caa139f79d3f9506e10888fb7dcd57a3ec1f3c2758 local-venv-py312-audit.json +15950a68a7ed99c59779717acefdceb3f69cfd31bde67d2c954f2a3cea4d7955 local-venv-py312-audit.stderr +2da07ec89a3fc1dcace0d2fe0f8b2b23e452a79217039dac8db90d4b43a711c8 secret-heuristics.txt +7eea64cd1bb5099ea5ac9f80f8301930bc03e4a37523411e4870c06bd6b22a5b windows-py312-audit.json +79d30422388543b2563e1dde76bb7e7857d7ef0c6fdf05c8c71598633ca07e1e windows-py312-audit.stderr +9d9e7f2bcf6563658b3385e2caf67f5866fdc6119d2c23eb2a571df9dd9b2439 windows-py312-requirements.txt diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/commands.txt b/docs/audits/2026-10-19-security-deps-87b58ea/commands.txt new file mode 100644 index 0000000..dfafd4f --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/commands.txt @@ -0,0 +1,6 @@ +uv pip compile --all-extras --python-version 3.10 --python-platform linux --no-annotate --no-header --quiet --output-file linux-py310-requirements.txt pyproject.toml +uv pip compile --all-extras --python-version 3.12 --python-platform windows --no-annotate --no-header --quiet --output-file windows-py312-requirements.txt pyproject.toml +uv tool run --from pip-audit pip-audit --requirement linux-py310-requirements.txt --format json --vulnerability-service osv --strict --no-deps --disable-pip +uv tool run --from pip-audit pip-audit --requirement windows-py312-requirements.txt --format json --vulnerability-service osv --strict --no-deps --disable-pip +uv tool run --from pip-audit pip-audit --path .venv/lib/python3.12/site-packages --format json --vulnerability-service osv --strict +Secret heuristic: scan git-tracked text files (<12 MB) for PEM private-key headers and common AWS/GitHub/Google/Stripe/Slack/OpenAI/Anthropic token formats. Output contains file/rule names only, never matched values. diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/environment.txt b/docs/audits/2026-10-19-security-deps-87b58ea/environment.txt new file mode 100644 index 0000000..7758b88 --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/environment.txt @@ -0,0 +1,9 @@ +Audit timestamp UTC: 2026-10-19T07:55:25.553278+00:00 +Repository: ISyCode +Git revision: 87b58ea (local main) +Python environment: local .venv, Python 3.12.3 +uv: 0.12.17 +pip-audit: 2.10.1 +Advisory service: OSV API +Dependency resolution: uv pip compile from pyproject.toml with all extras; Linux/Python 3.10 and Windows/Python 3.12. +No lockfile exists in the audited checkout. diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/linux-py310-audit.json b/docs/audits/2026-10-19-security-deps-87b58ea/linux-py310-audit.json new file mode 100644 index 0000000..0a8bb30 --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/linux-py310-audit.json @@ -0,0 +1 @@ +{"dependencies": [{"name": "aiohappyeyeballs", "version": "2.7.1", "vulns": []}, {"name": "aiohttp", "version": "3.14.5", "vulns": []}, {"name": "aiosignal", "version": "1.4.0", "vulns": []}, {"name": "annotated-types", "version": "0.8.0", "vulns": []}, {"name": "anthropic", "version": "1.15.0", "vulns": []}, {"name": "anyio", "version": "4.15.1", "vulns": []}, {"name": "async-timeout", "version": "5.0.1", "vulns": []}, {"name": "attrs", "version": "26.1.0", "vulns": []}, {"name": "backports-asyncio-runner", "version": "1.2.0", "vulns": []}, {"name": "backports-tarfile", "version": "1.2.0", "vulns": []}, {"name": "cffi", "version": "2.1.1", "vulns": []}, {"name": "cryptography", "version": "50.0.2", "vulns": []}, {"name": "docstring-parser", "version": "0.18.0", "vulns": []}, {"name": "exceptiongroup", "version": "1.3.1", "vulns": []}, {"name": "frozenlist", "version": "1.8.0", "vulns": []}, {"name": "h11", "version": "0.16.0", "vulns": []}, {"name": "httpcore2", "version": "2.14.0", "vulns": []}, {"name": "httpx2", "version": "2.14.0", "vulns": []}, {"name": "idna", "version": "3.20", "vulns": []}, {"name": "importlib-metadata", "version": "9.0.1", "vulns": []}, {"name": "iniconfig", "version": "2.3.1", "vulns": []}, {"name": "jaraco-classes", "version": "3.4.0", "vulns": []}, {"name": "jaraco-context", "version": "6.1.2", "vulns": []}, {"name": "jaraco-functools", "version": "4.6.0", "vulns": []}, {"name": "jeepney", "version": "0.9.0", "vulns": []}, {"name": "jiter", "version": "0.17.0", "vulns": []}, {"name": "keyring", "version": "25.7.0", "vulns": []}, {"name": "linkify-it-py", "version": "2.2.0", "vulns": []}, {"name": "markdown-it-py", "version": "4.2.0", "vulns": []}, {"name": "mdit-py-plugins", "version": "0.6.1", "vulns": []}, {"name": "mdurl", "version": "0.1.2", "vulns": []}, {"name": "more-itertools", "version": "11.1.0", "vulns": []}, {"name": "multidict", "version": "7.1.0", "vulns": []}, {"name": "opentelemetry-api", "version": "1.45.1", "vulns": []}, {"name": "packaging", "version": "26.3", "vulns": []}, {"name": "pathspec", "version": "1.1.1", "vulns": []}, {"name": "pillow", "version": "12.3.0", "vulns": []}, {"name": "platformdirs", "version": "4.12.4", "vulns": []}, {"name": "pluggy", "version": "1.7.0", "vulns": []}, {"name": "propcache", "version": "0.5.4", "vulns": []}, {"name": "pycparser", "version": "3.11", "vulns": []}, {"name": "pydantic", "version": "2.14.1", "vulns": []}, {"name": "pydantic-core", "version": "2.50.1", "vulns": []}, {"name": "pygments", "version": "2.21.0", "vulns": []}, {"name": "pytest", "version": "9.1.1", "vulns": []}, {"name": "pytest-asyncio", "version": "1.4.0", "vulns": []}, {"name": "rich", "version": "14.3.4", "vulns": []}, {"name": "secretstorage", "version": "3.5.0", "vulns": []}, {"name": "sniffio", "version": "1.3.1", "vulns": []}, {"name": "textual", "version": "8.2.8", "vulns": []}, {"name": "tomli", "version": "2.5.0", "vulns": []}, {"name": "truststore", "version": "0.10.5", "vulns": []}, {"name": "typing-extensions", "version": "4.16.0", "vulns": []}, {"name": "typing-inspection", "version": "0.4.4", "vulns": []}, {"name": "yarl", "version": "1.25.1", "vulns": []}, {"name": "zipp", "version": "4.1.1", "vulns": []}], "fixes": []} diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/linux-py310-audit.stderr b/docs/audits/2026-10-19-security-deps-87b58ea/linux-py310-audit.stderr new file mode 100644 index 0000000..4e4ce70 --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/linux-py310-audit.stderr @@ -0,0 +1,3 @@ +WARNING:pip_audit._cli:--no-deps is supported, but users are encouraged to fully hash their pinned dependencies +WARNING:pip_audit._cli:Consider using a tool like `pip-compile`: https://pip-tools.readthedocs.io/en/latest/#using-hashes +No known vulnerabilities found diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/linux-py310-requirements.txt b/docs/audits/2026-10-19-security-deps-87b58ea/linux-py310-requirements.txt new file mode 100644 index 0000000..00c5db8 --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/linux-py310-requirements.txt @@ -0,0 +1,56 @@ +aiohappyeyeballs==2.7.1 +aiohttp==3.14.5 +aiosignal==1.4.0 +annotated-types==0.8.0 +anthropic==1.15.0 +anyio==4.15.1 +async-timeout==5.0.1 +attrs==26.1.0 +backports-asyncio-runner==1.2.0 +backports-tarfile==1.2.0 +cffi==2.1.1 +cryptography==50.0.2 +docstring-parser==0.18.0 +exceptiongroup==1.3.1 +frozenlist==1.8.0 +h11==0.16.0 +httpcore2==2.14.0 +httpx2==2.14.0 +idna==3.20 +importlib-metadata==9.0.1 +iniconfig==2.3.1 +jaraco-classes==3.4.0 +jaraco-context==6.1.2 +jaraco-functools==4.6.0 +jeepney==0.9.0 +jiter==0.17.0 +keyring==25.7.0 +linkify-it-py==2.2.0 +markdown-it-py==4.2.0 +mdit-py-plugins==0.6.1 +mdurl==0.1.2 +more-itertools==11.1.0 +multidict==7.1.0 +opentelemetry-api==1.45.1 +packaging==26.3 +pathspec==1.1.1 +pillow==12.3.0 +platformdirs==4.12.4 +pluggy==1.7.0 +propcache==0.5.4 +pycparser==3.11 +pydantic==2.14.1 +pydantic-core==2.50.1 +pygments==2.21.0 +pytest==9.1.1 +pytest-asyncio==1.4.0 +rich==14.3.4 +secretstorage==3.5.0 +sniffio==1.3.1 +textual==8.2.8 +tomli==2.5.0 +truststore==0.10.5 +typing-extensions==4.16.0 +typing-inspection==0.4.4 +yarl==1.25.1 +zipp==4.1.1 diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/local-venv-py312-audit.json b/docs/audits/2026-10-19-security-deps-87b58ea/local-venv-py312-audit.json new file mode 100644 index 0000000..57a4f93 --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/local-venv-py312-audit.json @@ -0,0 +1 @@ +{"dependencies": [{"name": "aiohappyeyeballs", "version": "2.7.1", "vulns": []}, {"name": "aiohttp", "version": "3.14.3", "vulns": []}, {"name": "aiosignal", "version": "1.4.0", "vulns": []}, {"name": "ast-serialize", "version": "0.13.0", "vulns": []}, {"name": "attrs", "version": "26.1.0", "vulns": []}, {"name": "cffi", "version": "2.1.1", "vulns": []}, {"name": "cryptography", "version": "50.0.2", "vulns": []}, {"name": "frozenlist", "version": "1.8.0", "vulns": []}, {"name": "idna", "version": "3.20", "vulns": []}, {"name": "iniconfig", "version": "2.3.0", "vulns": []}, {"name": "isycode", "version": "1.0.0rc1", "vulns": []}, {"name": "jaraco-classes", "version": "3.4.0", "vulns": []}, {"name": "jaraco-context", "version": "6.1.2", "vulns": []}, {"name": "jaraco-functools", "version": "4.6.0", "vulns": []}, {"name": "jeepney", "version": "0.9.0", "vulns": []}, {"name": "keyring", "version": "25.7.0", "vulns": []}, {"name": "librt", "version": "0.16.0", "vulns": []}, {"name": "linkify-it-py", "version": "2.2.0", "vulns": []}, {"name": "markdown-it-py", "version": "4.2.0", "vulns": []}, {"name": "mdit-py-plugins", "version": "0.6.1", "vulns": []}, {"name": "mdurl", "version": "0.1.2", "vulns": []}, {"name": "more-itertools", "version": "11.1.0", "vulns": []}, {"name": "multidict", "version": "6.9.1", "vulns": []}, {"name": "mypy", "version": "2.4.0", "vulns": []}, {"name": "mypy-extensions", "version": "1.1.0", "vulns": []}, {"name": "packaging", "version": "26.3", "vulns": []}, {"name": "pathspec", "version": "1.1.1", "vulns": []}, {"name": "pillow", "version": "12.3.0", "vulns": []}, {"name": "pip", "version": "26.2.1", "vulns": []}, {"name": "platformdirs", "version": "4.12.2", "vulns": []}, {"name": "pluggy", "version": "1.6.0", "vulns": []}, {"name": "propcache", "version": "0.5.4", "vulns": []}, {"name": "pycparser", "version": "3.0", "vulns": []}, {"name": "pygments", "version": "2.21.0", "vulns": []}, {"name": "pytest", "version": "9.1.1", "vulns": []}, {"name": "pytest-asyncio", "version": "1.4.0", "vulns": []}, {"name": "rich", "version": "14.3.4", "vulns": []}, {"name": "secretstorage", "version": "3.5.0", "vulns": []}, {"name": "textual", "version": "8.2.8", "vulns": []}, {"name": "textual-image", "version": "0.12.0", "vulns": []}, {"name": "typing-extensions", "version": "4.16.0", "vulns": []}, {"name": "yarl", "version": "1.25.1", "vulns": []}], "fixes": []} diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/local-venv-py312-audit.stderr b/docs/audits/2026-10-19-security-deps-87b58ea/local-venv-py312-audit.stderr new file mode 100644 index 0000000..5c418af --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/local-venv-py312-audit.stderr @@ -0,0 +1 @@ +No known vulnerabilities found diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/secret-heuristics.txt b/docs/audits/2026-10-19-security-deps-87b58ea/secret-heuristics.txt new file mode 100644 index 0000000..106e08b --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/secret-heuristics.txt @@ -0,0 +1,3 @@ +Method: custom high-confidence token/private-key regular-expression pass over files from git ls-files. +Result: 1,759 tracked text files scanned; 318 binary files or files over 12 MB skipped; zero candidate file/rule matches. +Scope: tracked files only. This is not a complete secret scan and does not cover ignored local files, untracked files, arbitrary credential formats, binaries, or files over 12 MB. No candidate values were captured. diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/windows-py312-audit.json b/docs/audits/2026-10-19-security-deps-87b58ea/windows-py312-audit.json new file mode 100644 index 0000000..16a431d --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/windows-py312-audit.json @@ -0,0 +1 @@ +{"dependencies": [{"name": "aiohappyeyeballs", "version": "2.7.1", "vulns": []}, {"name": "aiohttp", "version": "3.14.5", "vulns": []}, {"name": "aiosignal", "version": "1.4.0", "vulns": []}, {"name": "annotated-types", "version": "0.8.0", "vulns": []}, {"name": "anthropic", "version": "1.15.0", "vulns": []}, {"name": "anyio", "version": "4.15.1", "vulns": []}, {"name": "attrs", "version": "26.1.0", "vulns": []}, {"name": "colorama", "version": "0.4.6", "vulns": []}, {"name": "docstring-parser", "version": "0.18.0", "vulns": []}, {"name": "frozenlist", "version": "1.8.0", "vulns": []}, {"name": "h11", "version": "0.16.0", "vulns": []}, {"name": "httpcore2", "version": "2.14.0", "vulns": []}, {"name": "httpx2", "version": "2.14.0", "vulns": []}, {"name": "idna", "version": "3.20", "vulns": []}, {"name": "iniconfig", "version": "2.3.1", "vulns": []}, {"name": "jaraco-classes", "version": "3.4.0", "vulns": []}, {"name": "jaraco-context", "version": "6.1.2", "vulns": []}, {"name": "jaraco-functools", "version": "4.6.0", "vulns": []}, {"name": "jiter", "version": "0.17.0", "vulns": []}, {"name": "keyring", "version": "25.7.0", "vulns": []}, {"name": "linkify-it-py", "version": "2.2.0", "vulns": []}, {"name": "markdown-it-py", "version": "4.2.0", "vulns": []}, {"name": "mdit-py-plugins", "version": "0.6.1", "vulns": []}, {"name": "mdurl", "version": "0.1.2", "vulns": []}, {"name": "more-itertools", "version": "11.2.1", "vulns": []}, {"name": "multidict", "version": "7.1.0", "vulns": []}, {"name": "opentelemetry-api", "version": "1.45.1", "vulns": []}, {"name": "packaging", "version": "26.3", "vulns": []}, {"name": "pathspec", "version": "1.1.1", "vulns": []}, {"name": "pillow", "version": "12.3.0", "vulns": []}, {"name": "platformdirs", "version": "4.13.5", "vulns": []}, {"name": "pluggy", "version": "1.7.0", "vulns": []}, {"name": "propcache", "version": "0.5.4", "vulns": []}, {"name": "pydantic", "version": "2.14.1", "vulns": []}, {"name": "pydantic-core", "version": "2.50.1", "vulns": []}, {"name": "pygments", "version": "2.21.0", "vulns": []}, {"name": "pytest", "version": "9.1.1", "vulns": []}, {"name": "pytest-asyncio", "version": "1.4.0", "vulns": []}, {"name": "pywin32", "version": "312", "vulns": []}, {"name": "pywin32-ctypes", "version": "0.2.3", "vulns": []}, {"name": "rich", "version": "14.3.4", "vulns": []}, {"name": "sniffio", "version": "1.3.1", "vulns": []}, {"name": "textual", "version": "8.2.8", "vulns": []}, {"name": "truststore", "version": "0.10.5", "vulns": []}, {"name": "typing-extensions", "version": "4.16.0", "vulns": []}, {"name": "typing-inspection", "version": "0.4.4", "vulns": []}, {"name": "yarl", "version": "1.25.1", "vulns": []}], "fixes": []} diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/windows-py312-audit.stderr b/docs/audits/2026-10-19-security-deps-87b58ea/windows-py312-audit.stderr new file mode 100644 index 0000000..4e4ce70 --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/windows-py312-audit.stderr @@ -0,0 +1,3 @@ +WARNING:pip_audit._cli:--no-deps is supported, but users are encouraged to fully hash their pinned dependencies +WARNING:pip_audit._cli:Consider using a tool like `pip-compile`: https://pip-tools.readthedocs.io/en/latest/#using-hashes +No known vulnerabilities found diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/windows-py312-requirements.txt b/docs/audits/2026-10-19-security-deps-87b58ea/windows-py312-requirements.txt new file mode 100644 index 0000000..6963505 --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/windows-py312-requirements.txt @@ -0,0 +1,47 @@ +aiohappyeyeballs==2.7.1 +aiohttp==3.14.5 +aiosignal==1.4.0 +annotated-types==0.8.0 +anthropic==1.15.0 +anyio==4.15.1 +attrs==26.1.0 +colorama==0.4.6 +docstring-parser==0.18.0 +frozenlist==1.8.0 +h11==0.16.0 +httpcore2==2.14.0 +httpx2==2.14.0 +idna==3.20 +iniconfig==2.3.1 +jaraco-classes==3.4.0 +jaraco-context==6.1.2 +jaraco-functools==4.6.0 +jiter==0.17.0 +keyring==25.7.0 +linkify-it-py==2.2.0 +markdown-it-py==4.2.0 +mdit-py-plugins==0.6.1 +mdurl==0.1.2 +more-itertools==11.2.1 +multidict==7.1.0 +opentelemetry-api==1.45.1 +packaging==26.3 +pathspec==1.1.1 +pillow==12.3.0 +platformdirs==4.13.5 +pluggy==1.7.0 +propcache==0.5.4 +pydantic==2.14.1 +pydantic-core==2.50.1 +pygments==2.21.0 +pytest==9.1.1 +pytest-asyncio==1.4.0 +pywin32==312 +pywin32-ctypes==0.2.3 +rich==14.3.4 +sniffio==1.3.1 +textual==8.2.8 +truststore==0.10.5 +typing-extensions==4.16.0 +typing-inspection==0.4.4 +yarl==1.25.1 From 8debcf9754c07731f52deb043b30c37f5da61c2c Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Mon, 19 Oct 2026 02:25:42 -0600 Subject: [PATCH 2/3] fix(tui): preserve large workspace search results --- pyproject.toml | 17 +++- src/isycode/action_runtime.py | 160 +++++++++++++++++++++++-------- src/isycode/design.tcss | 2 +- src/isycode/git_owner.py | 23 ++++- src/isycode/hive/types.py | 6 +- src/isycode/isycode.tcss | 3 + src/isycode/tui.py | 28 +++--- src/isycode/tui_app_chat.py | 10 +- src/isycode/tui_app_providers.py | 10 +- src/isycode/tui_app_rail.py | 11 ++- src/isycode/tui_app_sessions.py | 6 +- src/isycode/tui_theme.py | 6 ++ src/isycode/workspace_pack.py | 8 +- tests/test_git_owner.py | 16 ++++ tests/test_model_button.py | 12 +++ tests/test_workspace_grep.py | 62 ++++++++++++ 16 files changed, 304 insertions(+), 76 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 34a698d..ecba684 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,10 +36,25 @@ where = ["src"] [tool.pytest.ini_options] testpaths = ["tests"] -pythonpath = [".", "src"] +pythonpath = ["src", "."] markers = [ "integration: requires an external ISyCo checkout or running Gateway service", ] [tool.setuptools.package-data] isycode = ["*.tcss", "assets/*.rgbz", "assets/*.png", "assets/*.gf", "assets/*.txt", "assets/*.json", "bundled_skills/manifest.json", "bundled_skills/LICENSE", "bundled_skills/**/*.md", "locales/*/LC_MESSAGES/*.po"] + + +[tool.ruff] +line-length = 100 +target-version = "py310" +src = ["src", "tests"] + +[tool.mypy] +python_version = "3.10" +mypy_path = ["src"] +ignore_missing_imports = true + +[tool.coverage.run] +source = ["isycode"] +branch = true diff --git a/src/isycode/action_runtime.py b/src/isycode/action_runtime.py index 384014d..c82fb8d 100644 --- a/src/isycode/action_runtime.py +++ b/src/isycode/action_runtime.py @@ -140,12 +140,25 @@ def _workspace_config_request(action_id: str, root: Path, target: str, "workspace_grep": "workspace.files.read", } GREP_SKIP_DIRECTORIES = frozenset({ - "node_modules", "__pycache__", ".venv", "venv", ".tox", ".mypy_cache", - ".pytest_cache", ".ruff_cache", "dist", "build", "target", ".next", + "node_modules", "__pycache__", ".venv", "venv", ".tox", ".nox", ".mypy_cache", + ".pytest_cache", ".ruff_cache", "dist", "build", "target", ".next", ".nuxt", + ".output", ".cache", ".eggs", "site-packages", ".worktrees", ".codex-worktrees", + ".zwork", "htmlcov", "coverage", "Pods", "DerivedData", }) -GREP_MAX_FILES = 3000 -GREP_MAX_BYTES = 24 * 1024 * 1024 -GREP_MAX_MATCHES = 200 +GREP_MAX_FILES = 25_000 +GREP_MAX_BYTES = 128 * 1024 * 1024 +GREP_MAX_MATCHES = 500 +SEARCH_MAX_DIRECTORIES = 25_000 +SEARCH_MAX_MATCHES = 500 +LIST_MAX_ENTRIES = 5_000 + + +def _is_skipped_scan_directory(name: str) -> bool: + folded = name.casefold() + return (name in GREP_SKIP_DIRECTORIES + or folded in GREP_SKIP_DIRECTORIES + or folded.startswith((".venv-", ".venv_", "venv-", "venv_")) + or folded.endswith(".egg-info")) MAX_FILE_BYTES = 128 * 1024 MAX_WRITE_BYTES = 128 * 1024 WRITE_PARAMETER_KEYS = frozenset({"path", "before_sha256", "after_sha256", "size", "diff_sha256", @@ -162,7 +175,7 @@ def _workspace_config_request(action_id: str, root: Path, target: str, WRITE_PROTECTED_NAMES = frozenset({".isyroot"}) MAX_OUTPUT_CHARS = 24_000 MAX_PACK_READ_OUTPUT_CHARS = 1_000_000 -MAX_SCAN_ENTRIES = 6_000 +MAX_SCAN_ENTRIES = 150_000 # Sandboxed workspace commands (isycode.command_runner). The request binds the # exact argv, resolved program, working folder, limits and the set of masked # sensitive paths; nothing is expanded by a shell. @@ -2438,6 +2451,12 @@ def _execute(self, action_id: str, arguments: dict[str, Any], *, if not isinstance(path_value, str) or len(path_value) > 4096: return ActionOutcome("Malformed action path.", "DENY", None, "path must be a bounded string") + if (self.owner_id == "workspace_read" + and path_value.strip() in {"", "/"} + and (action_id in {"workspace.files.list", "workspace.files.search"} + or (action_id == "workspace.files.read" and "query" in arguments))): + path_value = "." + arguments = {**arguments, "path": "."} try: target = str(self._lexical_target(path_value)) except (OSError, RuntimeError, ValueError) as exc: @@ -2483,10 +2502,14 @@ def _execute(self, action_id: str, arguments: dict[str, Any], *, except (OSError, ValueError) as exc: return ActionOutcome("Read action failed; no content was returned.", "ERROR", None, str(exc)[:300]) - if len(result) > output_limit and not ( + effective_limit = (max(output_limit, 96_000) + if (action_id in {"workspace.files.list", "workspace.files.search"} + or (action_id == "workspace.files.read" and "query" in arguments)) + else output_limit) + if len(result) > effective_limit and not ( self.owner_id == CONFIG_OWNER_ID and action_id == "workspace.config.read" and Path(target).relative_to(self.root).as_posix() == ".gitignore"): - result = result[:output_limit] + "\n[output truncated]" + result = self._fit_json_result(result, effective_limit) receipt = ActionReceipt( "rcpt_" + secrets.token_hex(8), action_id, request.digest, "ALLOW", "SUCCESS", hashlib.sha256(result.encode("utf-8")).hexdigest()) @@ -2518,6 +2541,54 @@ def _entries(self, directory: Path) -> list[Any]: finally: os.close(descriptor) + @staticmethod + def _fit_json_result(result: str, output_limit: int) -> str: + if len(result) <= output_limit: + return result + try: + payload = json.loads(result) + except Exception: + return result[:output_limit] + "\n[output truncated]" + if not isinstance(payload, dict): + return result[:output_limit] + "\n[output truncated]" + if isinstance(payload.get("text"), str): + text = payload["text"] + suffix = "\n[output truncated]" + lo, hi = 0, len(text) + best = json.dumps({**payload, "text": suffix.lstrip("\n"), "truncated": True}, + ensure_ascii=False) + while lo <= hi: + mid = (lo + hi) // 2 + candidate = json.dumps( + {**payload, "text": text[:mid] + suffix, "truncated": True}, + ensure_ascii=False, + ) + if len(candidate) <= output_limit: + best = candidate + lo = mid + 1 + else: + hi = mid - 1 + return best if len(best) <= output_limit else result[:output_limit] + for list_key in ("matches", "entries"): + if isinstance(payload.get(list_key), list): + items = payload[list_key] + lo, hi = 0, len(items) + best = json.dumps({**payload, list_key: [], "truncated": True}, + ensure_ascii=False) + while lo <= hi: + mid = (lo + hi) // 2 + candidate = json.dumps( + {**payload, list_key: items[:mid], "truncated": True}, + ensure_ascii=False, + ) + if len(candidate) <= output_limit: + best = candidate + lo = mid + 1 + else: + hi = mid - 1 + return best if len(best) <= output_limit else result[:output_limit] + return result[:output_limit] + "\n[output truncated]" + def _list(self, target: str) -> str: directory = Path(target) entries = [] @@ -2526,11 +2597,17 @@ def _list(self, target: str) -> str: continue kind = "directory" if item.is_dir(follow_symlinks=False) else "file" entries.append({"name": item.name, "kind": kind}) - if len(entries) >= 1000: - break entries.sort(key=lambda value: (value["kind"] != "directory", value["name"].casefold())) - return json.dumps({"path": str(directory.relative_to(self.root) or "."), - "entries": entries}, ensure_ascii=False) + truncated = len(entries) > LIST_MAX_ENTRIES + if truncated: + entries = entries[:LIST_MAX_ENTRIES] + payload: dict[str, Any] = { + "path": str(directory.relative_to(self.root) or "."), + "entries": entries, + } + if truncated: + payload["truncated"] = True + return json.dumps(payload, ensure_ascii=False) def _read(self, target: str, *, max_bytes: int = MAX_FILE_BYTES) -> str: path = Path(target) @@ -2552,42 +2629,47 @@ def _read(self, target: str, *, max_bytes: int = MAX_FILE_BYTES) -> str: ensure_ascii=False) def _search(self, query: str, start: Path) -> str: - pending = [start] + from collections import deque + pending = deque([start]) hits: list[dict[str, str]] = [] visited = 0 entries_seen = 0 truncated = False - folded = query.casefold() + folded = query.casefold().strip("/") + match_rel = "/" in folded while pending: - directory = pending.pop() + if visited >= SEARCH_MAX_DIRECTORIES: + truncated = True + break + directory = pending.popleft() try: - listed = self._entries(directory) + listed = sorted(self._entries(directory), key=lambda item: item.name.casefold()) except OSError: continue - children = [] + visited += 1 for child in listed: - children.append(child) + if WorkspaceReadSystembility.is_sensitive_name(child.name) or child.is_symlink(): + continue + is_dir = child.is_dir(follow_symlinks=False) + if is_dir and _is_skipped_scan_directory(child.name): + continue entries_seen += 1 - if entries_seen >= MAX_SCAN_ENTRIES: + if entries_seen > MAX_SCAN_ENTRIES: truncated = True break - visited += 1 - if visited > 300 or truncated: - truncated = True - break - for child in children: - if WorkspaceReadSystembility.is_sensitive_name(child.name) or child.is_symlink(): - continue path = directory / child.name - if folded in child.name.casefold(): + rel_posix = path.relative_to(self.root).as_posix() + matched = (folded in rel_posix.casefold() if match_rel + else folded in child.name.casefold()) + if matched: hits.append({ "path": str(path.relative_to(self.root)), - "kind": "directory" if child.is_dir(follow_symlinks=False) else "file", + "kind": "directory" if is_dir else "file", }) - if len(hits) >= 200: + if len(hits) >= SEARCH_MAX_MATCHES: truncated = True break - if child.is_dir(follow_symlinks=False): + if is_dir: pending.append(path) if truncated: break @@ -2597,18 +2679,20 @@ def _search(self, query: str, start: Path) -> str: def _grep(self, query: str, start: Path) -> str: """Literal, case-insensitive line search through descriptor-safe reads.""" + from collections import deque folded = query.casefold() matches: list[dict[str, Any]] = [] files = scanned_bytes = 0 truncated = False - pending = [start] if start != self.root and not start.is_dir(): - pending, candidates = [], [start] + pending: deque[Path] = deque() + candidates: deque[Path] = deque([start]) else: - candidates = [] + pending = deque([start]) + candidates = deque() while pending or candidates: if candidates: - path = candidates.pop() + path = candidates.popleft() if files >= GREP_MAX_FILES or scanned_bytes >= GREP_MAX_BYTES: truncated = True break @@ -2639,14 +2723,14 @@ def _grep(self, query: str, start: Path) -> str: if truncated: break continue - directory = pending.pop() + directory = pending.popleft() try: - children = sorted(self._entries(directory), key=lambda item: item.name) + children = sorted(self._entries(directory), key=lambda item: item.name.casefold()) except OSError: continue - for child in reversed(children): + for child in children: if (WorkspaceReadSystembility.is_sensitive_name(child.name) or child.is_symlink() - or child.name in GREP_SKIP_DIRECTORIES): + or _is_skipped_scan_directory(child.name)): continue if child.is_dir(follow_symlinks=False): pending.append(directory / child.name) diff --git a/src/isycode/design.tcss b/src/isycode/design.tcss index 3079431..37142f1 100644 --- a/src/isycode/design.tcss +++ b/src/isycode/design.tcss @@ -79,7 +79,7 @@ Tree > .tree--cursor, Tree:focus > .tree--cursor { background: $isy-selected-sur #command-bar Button { color: $isy-text; background: $isy-surface !important; } #command-bar Button:hover, #command-bar Button:focus { background: $isy-selected-surface !important; color: $isy-focus; } #settings-button { width: 5; dock: right; } -#model-button { max-width: 20; text-wrap: nowrap; text-overflow: ellipsis; } +#model-button { max-width: 28; text-wrap: nowrap; text-overflow: ellipsis; } #prompt-input, #idea-box, #shell-box, #queued-box, #agent-tasks, .user-turn, .external-review, .tool-activity, CommandOutputCard { background: $isy-surface !important; color: $isy-text; border: solid $isy-border; } diff --git a/src/isycode/git_owner.py b/src/isycode/git_owner.py index 2a32aaf..429c70b 100644 --- a/src/isycode/git_owner.py +++ b/src/isycode/git_owner.py @@ -140,6 +140,9 @@ def __init__(self, root: Path, authority: WorkspaceAuthority, approvals: ActionApprovalStore | None = None, repository: str | None = None): self.root = root.resolve(strict=True) + if isinstance(repository, str): + cleaned = repository.strip().removeprefix("./").rstrip("/") + repository = cleaned or "." self.repository = repository self.repo_root = self._resolve_repository(repository) self.authority = authority @@ -162,15 +165,20 @@ def _repository_problem(self) -> str | None: return "the selected direct child repository is unavailable" candidates = [candidate] else: + root_problem = self._check_repository_candidate(self.root, git) + if root_problem is None: + self.repo_root = self.root + return None candidates = [self.root] try: with os.scandir(self.root) as entries: inspected = 0 + child_candidates: list[Path] = [] for entry in entries: - if self.repository is not None: + if self.repository is not None and self.repository != ".": break inspected += 1 - if inspected > 4096: + if inspected > 16384: return ("the workspace has too many entries for safe repository discovery; " "retry with the repository field set to one direct child folder") if (entry.name.startswith(".") @@ -179,12 +187,19 @@ def _repository_problem(self) -> str | None: child = self.root / entry.name try: if stat.S_ISDIR((child / ".git").lstat().st_mode): - if len(candidates) >= 128: + if len(child_candidates) >= 512: return ("too many direct child repositories for automatic discovery; " "retry with the repository field set to one folder") - candidates.append(child) + child_candidates.append(child) except (FileNotFoundError, OSError): continue + child_candidates.sort(key=lambda item: item.name.casefold()) + if len(child_candidates) > 1: + self.repo_root = self.root + names = [item.relative_to(self.root).as_posix() for item in child_candidates] + return ("multiple repositories found: " + ", ".join(names[:12]) + + "; retry with the repository field set to one folder") + candidates.extend(child_candidates) except OSError: return "the workspace entries could not be inspected safely" diff --git a/src/isycode/hive/types.py b/src/isycode/hive/types.py index 1098a8c..834a35f 100644 --- a/src/isycode/hive/types.py +++ b/src/isycode/hive/types.py @@ -4,7 +4,7 @@ import json import re from dataclasses import dataclass, field, asdict -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from pathlib import Path from typing import Any, Literal, TypedDict @@ -40,7 +40,7 @@ class HiveMessage: hops: int = 0 requires_reply: bool = False needs_human: bool = False - created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat() + "Z") + created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")) def __post_init__(self) -> None: for value in (self.id, self.conversation, self.from_agent, self.to_agent): @@ -98,7 +98,7 @@ class HiveTask: status: Literal["todo", "doing", "blocked", "done"] = "todo" depends_on: list[str] = field(default_factory=list) priority: int = 0 - created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat() + "Z") + created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")) human_qa: list[HumanQA] = field(default_factory=list) result: str | None = None slack: dict[str, str] | None = None diff --git a/src/isycode/isycode.tcss b/src/isycode/isycode.tcss index d02ad33..0e29001 100644 --- a/src/isycode/isycode.tcss +++ b/src/isycode/isycode.tcss @@ -144,6 +144,9 @@ height: auto; margin: 1 2; padding: 0 1; border: round #c4a35a; background: #241c28; } + .assistant-turn { + height: auto; margin: 0 2; + } ThoughtBlock { height: auto; margin: 1 2; padding: 0 1; border: round #514d5a; diff --git a/src/isycode/tui.py b/src/isycode/tui.py index 5d315ea..b4bb32e 100644 --- a/src/isycode/tui.py +++ b/src/isycode/tui.py @@ -581,11 +581,11 @@ def _apply_locale_chrome(self) -> None: for widget_id, msgid in buttons.items(): try: self.query_one(widget_id, Button).label = tr(msgid) - except Exception: + except (NoMatches, NoScreen, ScreenStackError): continue try: self.query_one("#context-button", Button).label = self._context_button_label() - except Exception: + except (NoMatches, NoScreen, ScreenStackError): pass statics = { "#composer-hint": "Enter send · Ctrl+J newline · Esc back · Ctrl+P commands · F1 help", @@ -598,33 +598,33 @@ def _apply_locale_chrome(self) -> None: if widget_id == "#file-preview" and self._selected_file_path is not None: continue self.query_one(widget_id, Static).update(tr(msgid)) - except Exception: + except (NoMatches, NoScreen, ScreenStackError): continue try: self.query_one(PromptArea).placeholder = tr(PromptArea.DEFAULT_PLACEHOLDER) self.query_one("#idea-box").border_title = tr("Idea · Space expand") - except (NoMatches, NoScreen): + except (NoMatches, NoScreen, ScreenStackError): pass try: self.query_one("#action-search", Input).placeholder = tr("Filter this list…") - except Exception: + except (NoMatches, NoScreen, ScreenStackError): pass try: self.query_one("#provider-key-input", Input).placeholder = tr("Paste API key…") - except Exception: + except (NoMatches, NoScreen, ScreenStackError): pass try: self.query_one("#file-search", Input).placeholder = tr("Search workspace paths…") - except Exception: + except (NoMatches, NoScreen, ScreenStackError): pass try: self.query_one("#idea-box").update( tr("Idea box\nCapture an idea · Ctrl+Shift+Enter")) - except Exception: + except (NoMatches, NoScreen, ScreenStackError): pass try: self.query_one("#shell-box").update(tr("ShellBox · no processes")) - except Exception: + except (NoMatches, NoScreen, ScreenStackError): pass collapsibles = { "#rail-sentinel": "IsySentinel", @@ -641,7 +641,7 @@ def _apply_locale_chrome(self) -> None: for widget_id, msgid in collapsibles.items(): try: self.query_one(widget_id).title = tr(msgid) - except Exception: + except (NoMatches, NoScreen, ScreenStackError): continue self._refresh_model_button() self._paint_idea_box() @@ -779,7 +779,7 @@ def on_resize(self, event) -> None: self._rail_visible = True rail.display = self._rail_visible self.call_after_refresh(self.query_one(Banner).set_compact, True) - self.call_after_refresh(self._update_model_button_visibility) + self.call_after_refresh(self._refresh_model_button) self.call_after_refresh(self._paint_idle) if getattr(self, "_menu_mode", ""): card = self.query_one("#action-card", Vertical) @@ -1836,7 +1836,7 @@ def _append(self, text: str, color: str = TEXT, *, startup: bool = False) -> Non def _idle_mounted(self) -> bool: try: board = self.query_one("#idle-board", IdleBoard) - except Exception: + except (NoMatches, NoScreen, ScreenStackError): return False return bool(board.is_mounted and board.display and not board.has_class("startup-archived")) @@ -1855,7 +1855,7 @@ def _mount_idle_board(self) -> None: def _dismiss_idle(self) -> None: try: board = self.query_one("#idle-board", IdleBoard) - except Exception: + except (NoMatches, NoScreen, ScreenStackError): return if board.is_mounted and not board.has_class("startup-archived"): board.add_class("startup-archived") @@ -1866,7 +1866,7 @@ def _paint_idle(self) -> None: return try: board = self.query_one("#idle-board", IdleBoard) - except Exception: + except (NoMatches, NoScreen, ScreenStackError): return if not board.is_mounted or board.has_class("startup-archived"): return diff --git a/src/isycode/tui_app_chat.py b/src/isycode/tui_app_chat.py index 0cb1ce8..feabf06 100644 --- a/src/isycode/tui_app_chat.py +++ b/src/isycode/tui_app_chat.py @@ -577,7 +577,9 @@ def _content_line(*, force: bool = False) -> None: return lane = self._active_lane() content = "".join(step_content) - lane.partial = "".join(step_content) + if not content.strip(): + return + lane.partial = content if not self._lane_on_screen(): lane.stream_widget = None holder["widget"] = None @@ -608,7 +610,7 @@ def _content_line(*, force: bool = False) -> None: if clock: chat_now.mount(Static(Text(clock, style=MUTED), classes="message-clock")) w = SelectableText(RichMarkdown("", code_theme="monokai"), - selection_text="") + selection_text="", classes="assistant-turn") holder["widget"] = w lane.stream_widget = w chat_now.mount(w) @@ -873,7 +875,7 @@ async def send_provider_request(): steer_trial = None self._steering_try_active = False self._append("Steering accepted · continuing the turn.", CYAN) - if not step_content and isinstance(response.get("text"), str): + if not step_content and isinstance(response.get("text"), str) and response["text"].strip(): on_chunk("content", response["text"]) _content_line(force=True) finish_step() @@ -887,7 +889,7 @@ async def send_provider_request(): if step_reason: lane.lines.append(("reasoning", "".join(step_reason), _time.monotonic() - thought_started)) - if step_content: + if "".join(step_content).strip(): lane.lines.append(("assistant", "".join(step_content), assistant_sent_at)) lane.partial = "" lane.stream_widget = None diff --git a/src/isycode/tui_app_providers.py b/src/isycode/tui_app_providers.py index 47db302..a98c5b4 100644 --- a/src/isycode/tui_app_providers.py +++ b/src/isycode/tui_app_providers.py @@ -502,7 +502,15 @@ def _model_button_label(self) -> str: model = resolved_chat_model(name) from isycode.model_presentation import model_display_name from isycode.tui_theme import _fit_cells - return f"{_fit_cells(model_display_name(model), 8)} ▾" + term_width = getattr(getattr(self, "size", None), "width", 0) or 100 + rail_open = False + try: + rail_open = bool(self.query_one(SidePanel).display) + except (NoMatches, Exception): + pass + main_width = term_width - (40 if rail_open and term_width >= 100 else 0) + budget = 24 if main_width >= 110 else (16 if main_width >= 90 else 8) + return f"{_fit_cells(model_display_name(model), budget)} ▾" except (ConfigurationError, ProviderError): return "Model ▾" diff --git a/src/isycode/tui_app_rail.py b/src/isycode/tui_app_rail.py index d1fac87..f7e5636 100644 --- a/src/isycode/tui_app_rail.py +++ b/src/isycode/tui_app_rail.py @@ -236,7 +236,7 @@ def _origin_tag(origin: object) -> str: def _set_rail_title(self, section: str, title: str) -> None: try: self.query_one(f"#{section}", Collapsible).title = status_phrase(title) - except Exception: + except (NoMatches, NoScreen): pass def _refresh_lsp_status(self) -> None: @@ -460,9 +460,10 @@ async def _load_directory(self, logical_path: str) -> None: tree.root.add(label, allow_expand=entry.get("kind") == "directory", data={"path": child_path, "kind": entry.get("kind"), "bytes": entry.get("bytes", 0)}) self._search_mode = False + limit_note = " · listing capped" if payload.get("truncated") else "" status = verified_receipt_line(outcome.receipt.receipt_id) if not self._show_file_preview( - f"{len(tree.root.children)} entries · {browser_root}\n" + f"{len(tree.root.children)} entries{limit_note} · {browser_root}\n" f"{status}"): return tree.root.expand() @@ -563,7 +564,7 @@ async def on_tree_node_selected(self, event: Tree.NodeSelected) -> None: try: self.query_one("#file-copy-path", Button).disabled = False self.query_one("#file-open-preview", Button).disabled = False - except Exception: + except (NoMatches, NoScreen): pass if getattr(self, "_sidebar_file_preview", False): await self._preview_file(uri, modal=False) @@ -582,7 +583,7 @@ async def on_tree_node_highlighted(self, event: Tree.NodeHighlighted) -> None: try: self.query_one("#file-copy-path", Button).disabled = False self.query_one("#file-open-preview", Button).disabled = False - except Exception: + except (NoMatches, NoScreen): pass if getattr(self, "_sidebar_file_preview", False): self.run_worker(self._preview_file(uri, modal=False), group="file-preview") @@ -591,7 +592,7 @@ def _apply_sidebar_file_preview(self, enabled: bool) -> None: try: box = self.query_one("#file-preview-box", Vertical) box.display = enabled - except Exception: + except (NoMatches, NoScreen): pass async def _preview_file(self, uri: str, *, modal: bool = False) -> None: diff --git a/src/isycode/tui_app_sessions.py b/src/isycode/tui_app_sessions.py index d15ab7a..615215b 100644 --- a/src/isycode/tui_app_sessions.py +++ b/src/isycode/tui_app_sessions.py @@ -1327,7 +1327,8 @@ def _repaint_conversation(self) -> None: if clock_label(extra): chat.mount(Static(Text(clock_label(extra), style=MUTED), classes="message-clock")) chat.mount(SelectableText( - RichMarkdown(text or "", code_theme="monokai"), selection_text=text or "")) + RichMarkdown(text or "", code_theme="monokai"), selection_text=text or "", + classes="assistant-turn")) elif kind == "reasoning": block = ThoughtBlock() block.set_text(text) @@ -1359,7 +1360,8 @@ def _repaint_conversation(self) -> None: lane.stream_block = None if lane.partial: widget = SelectableText( - RichMarkdown(lane.partial, code_theme="monokai"), selection_text=lane.partial) + RichMarkdown(lane.partial, code_theme="monokai"), selection_text=lane.partial, + classes="assistant-turn") chat.mount(widget) lane.stream_widget = widget else: diff --git a/src/isycode/tui_theme.py b/src/isycode/tui_theme.py index 2bcb561..2cee1be 100644 --- a/src/isycode/tui_theme.py +++ b/src/isycode/tui_theme.py @@ -202,3 +202,9 @@ def static_content(widget: Static): def plain_text(widget: Static) -> str: content = static_content(widget) return content if isinstance(content, str) else getattr(content, "plain", str(content)) + + +authority_capability_label = _authority_capability_label +semantic_box_title = _semantic_box_title +fit_cells = _fit_cells +elapsed_label = _elapsed_label diff --git a/src/isycode/workspace_pack.py b/src/isycode/workspace_pack.py index b68bad6..32b6cc2 100644 --- a/src/isycode/workspace_pack.py +++ b/src/isycode/workspace_pack.py @@ -18,10 +18,12 @@ MAX_PACK_DIRECTORIES = 200 MAX_PACK_BYTES = 1024 * 1024 MAX_TOKEN_BUDGET = 100_000 -MAX_LIST_ENTRIES = 1000 +MAX_LIST_ENTRIES = 5000 SKIP_DIRECTORIES = frozenset({ - ".git", ".isycode", ".next", ".pytest_cache", ".tox", ".venv", "__pycache__", - "build", "dist", "node_modules", "target", "venv", + ".git", ".isycode", ".next", ".nuxt", ".output", ".cache", ".eggs", + ".mypy_cache", ".pytest_cache", ".ruff_cache", ".tox", ".nox", ".venv", + ".worktrees", ".codex-worktrees", ".zwork", "__pycache__", "build", + "coverage", "dist", "htmlcov", "node_modules", "site-packages", "target", "venv", }) diff --git a/tests/test_git_owner.py b/tests/test_git_owner.py index ab335a2..d539750 100644 --- a/tests/test_git_owner.py +++ b/tests/test_git_owner.py @@ -227,3 +227,19 @@ def test_worktree_config_is_allowed_but_its_file_is_still_checked(repo): (root / ".git" / "config.worktree").write_text("[core]\n\tfsmonitor = touch /tmp/x\n") outcome = owner.status() assert outcome.decision == "DENY" and "core.fsmonitor" in outcome.reason + + +def test_multi_repository_parent_workspace_lists_sorted_children_over_128(tmp_path, monkeypatch): + monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "state")) + monkeypatch.setenv("HOME", str(tmp_path)) + (tmp_path / ".gitconfig").write_text("[user]\n\tname = Test\n\temail = test@example.com\n") + workspace = tmp_path / "multi_ws" + workspace.mkdir() + for i in range(135): + child = workspace / f"repo_{i:03d}" + (child / ".git").mkdir(parents=True) + authority = WorkspaceAuthority(workspace, state_directory=tmp_path / "state" / "authority") + _grant(authority, "git.status") + outcome = GitOwner(workspace, authority).status() + assert outcome.decision == "DENY" + assert "multiple repositories found: repo_000, repo_001" in outcome.reason diff --git a/tests/test_model_button.py b/tests/test_model_button.py index 331bfd4..c23ae89 100644 --- a/tests/test_model_button.py +++ b/tests/test_model_button.py @@ -103,3 +103,15 @@ async def test_model_button_label_falls_back_without_provider(monkeypatch): app = TUIApp() async with app.run_test(size=(100, 30)) as pilot: assert app._model_button_label() == "Model ▾" + + +@pytest.mark.asyncio +async def test_model_button_label_uses_wider_budget_on_wide_terminal(monkeypatch): + monkeypatch.setenv("ISYCODE_PROVIDER", "nvidia") + monkeypatch.setenv("ISYCODE_MODEL", "stepfun-ai/step-5-preview") + app = TUIApp() + async with app.run_test(size=(140, 30)) as pilot: + await pilot.pause() + label = app._model_button_label() + assert "Step 5" in label and label.endswith("▾") + assert len(label) > 12 diff --git a/tests/test_workspace_grep.py b/tests/test_workspace_grep.py index da1073f..e50849b 100644 --- a/tests/test_workspace_grep.py +++ b/tests/test_workspace_grep.py @@ -67,3 +67,65 @@ def test_grep_cannot_leave_the_workspace(repo): owner, authority, _ = repo authority.set_mode("classic") assert _grep(owner, "parse", "../outside.txt").decision == "DENY" + + +def test_search_and_grep_handle_large_repos_bfs_and_skip_worktrees(repo): + owner, authority, root = repo + authority.set_mode("classic") + for skipped_dir in (".worktrees", ".codex-worktrees", ".venv-linux", "pkg.egg-info"): + heavy = root / skipped_dir / "deep" + heavy.mkdir(parents=True) + for i in range(50): + (heavy / f"ignored_{i}.yml").write_text("PARSE_CONFIG in worktree\n") + for idx in range(350): + sub = root / f"repo_{idx:03d}" / ".github" / "workflows" + sub.mkdir(parents=True) + if idx in (0, 175, 349): + (sub / "ci.yml").write_text("name: CI\non: push\n") + + search_outcome = owner.execute("workspace.files.search", {"query": ".github/workflows", "path": "/"}) + assert search_outcome.decision == "ALLOW" + search_payload = json.loads(search_outcome.text) + assert search_payload["truncated"] is False + assert search_payload["directories_scanned"] > 300 + assert len(search_payload["matches"]) == 353 + + file_search = json.loads( + owner.execute("workspace.files.search", {"query": "ci.yml", "path": "/"}).text + ) + assert file_search["truncated"] is False + assert {m["path"] for m in file_search["matches"]} == { + "repo_000/.github/workflows/ci.yml", + "repo_175/.github/workflows/ci.yml", + "repo_349/.github/workflows/ci.yml", + } + + grep_payload = json.loads(_grep(owner, "PARSE_CONFIG", "/").text) + assert grep_payload["truncated"] is False + assert all(not m["path"].startswith((".worktrees", ".codex-worktrees", ".venv-linux", "pkg.egg-info")) + for m in grep_payload["matches"]) + + +def test_list_sorts_directories_before_cap_and_output_limit_keeps_valid_json(repo): + owner, authority, root = repo + authority.set_mode("classic") + big_dir = root / "big" + big_dir.mkdir() + for i in range(1100): + (big_dir / f"file_{i:04d}.txt").write_text(f"line {i}\n") + (big_dir / "zzz_subdir").mkdir() + + list_outcome = owner.execute("workspace.files.list", {"path": "big"}) + assert list_outcome.decision == "ALLOW" + listed = json.loads(list_outcome.text) + assert listed["entries"][0] == {"name": "zzz_subdir", "kind": "directory"} + assert len(listed["entries"]) == 1101 + + large_file = root / "src" / "large.txt" + large_file.write_text("A" * 40_000) + read_outcome = owner.execute("workspace.files.read", {"path": "src/large.txt"}) + assert read_outcome.decision == "ALLOW" + parsed_read = json.loads(read_outcome.text) + assert parsed_read["path"] == "src/large.txt" + assert parsed_read["truncated"] is True + assert "[output truncated]" in parsed_read["text"] From f30f4e6158ce673c74236d19cf424dfcce596837 Mon Sep 17 00:00:00 2001 From: DannyBaanks Date: Mon, 19 Oct 2026 02:25:48 -0600 Subject: [PATCH 3/3] docs(audit): update status against merged main --- .../AUDITORIA_ISYCODE.md | 119 ++++++++++++++++++ .../auditoria_seguridad_dependencias.md | 90 +++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/AUDITORIA_ISYCODE.md create mode 100644 docs/audits/2026-10-19-security-deps-87b58ea/auditoria_seguridad_dependencias.md diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/AUDITORIA_ISYCODE.md b/docs/audits/2026-10-19-security-deps-87b58ea/AUDITORIA_ISYCODE.md new file mode 100644 index 0000000..a0dc264 --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/AUDITORIA_ISYCODE.md @@ -0,0 +1,119 @@ +# Informe consolidado de auditoría — ISyCode + +- **Fecha de consolidación:** 19 de octubre de 2026 +- **Proyecto:** `DannyBaanks/ISyCode` +- **Corte revisado:** `87b58ea` (`main`, merge local de `74218e3`) +- **Alcance:** integrar las auditorías disponibles, distinguir lo corregido de lo pendiente y registrar la evidencia de la reparación de navegación/búsqueda en repositorios grandes. + +> **Vigencia (19-10-2026):** este documento conserva el estado del corte `87b58ea`. [PR #62](https://github.com/DannyBaanks/ISyCode/pull/62) (`c033d82`) y [PR #63](https://github.com/DannyBaanks/ISyCode/pull/63) (`1095ad1`) se integraron a `main` después de ese corte. #62 incorporó validación de IDs/remitentes, rechazo de rutas enlazadas y límites de ejecución Hive, además de regenerar el snapshot M15; #63 atendió hallazgos de permisos de TUI y sincronizó líneas del snapshot. Por ello, los P1 abiertos aquí describen `87b58ea`, no el estado actual de `main`. Esta actualización no repitió los probes de explotación contra `1095ad1`. Los resultados de OSV y de la heurística de secretos también pertenecen al corte `87b58ea`. El arreglo TUI incluido en esta PR se portó desde `74218e3`: sus 52 pruebas focalizadas pasaron en el corte original, pero no se volvieron a ejecutar después del port; la CI de esta PR debe validarlo. + +## 1. Resumen ejecutivo y estado + +La auditoría de arquitectura, seguridad y dependencias ya está consolidada para `87b58ea`. El escaneo OSV no encontró avisos conocidos en tres conjuntos de dependencias; la heurística aplicada a archivos rastreados no encontró patrones comunes de secretos; y se registraron los controles presentes y las limitaciones del análisis. Esto **no cierra la evaluación de seguridad**: en `87b58ea` estaban abiertos tres hallazgos de severidad alta en Hive (rutas derivadas de mensajes no confiables, escape por symlink y lanzamiento de subprocess sin owner de ejecución). PR #62 incorporó después cambios para esos límites; esta actualización no repitió sus probes sobre `main`. + +### Trabajo de código completado + +La reparación de truncamiento de la TUI y búsqueda en repositorios grandes quedó en el commit `74218e3` y se integró localmente a `main` en `87b58ea`. El cambio amplía el recorrido ordenado por niveles, excluye directorios de dependencias/worktrees, eleva límites de exploración, conserva JSON válido al truncar y ajusta el descubrimiento de repositorios hijos. + +La verificación focalizada registrada para esa reparación fue: + +- `python3 -m pytest tests/test_git_owner.py tests/test_hive.py tests/test_model_button.py tests/test_workspace_grep.py` — 52 aprobadas. +- `git diff --check 594050e..87b58ea` — sin errores. +- `74218e3` es ancestro de `87b58ea`. + +En el corte de código `87b58ea`, la rama local estaba 2 commits adelante y 1 detrás de `origin/main`; no se hizo push. Este dato describe ese corte, antes de guardar localmente el informe y su evidencia. Las pruebas anteriores verifican la reparación focalizada; no son una validación de los hallazgos de Hive ni de toda la aplicación. + +### Alcance y límites + +La auditoría de seguridad/dependencias de este informe se ejecutó sobre el corte local `87b58ea`; guardó comandos, entorno, requirements resueltos, salidas JSON y hashes. Se verificaron los 11 archivos del manifiesto SHA-256 y todos coincidieron. No se ejecutaron pruebas nuevas, la suite completa, workflows CI, proveedores reales, OAuth, Gateway, credenciales reales ni infraestructura externa. Las tres fallas de Hive fueron reproducidas en una auditoría previa sobre `594050e`; como la reparación posterior no está en `87b58ea`, aquí se mantienen abiertas sin volver a afirmar una reproducción en este corte. + +## 2. Arquitectura observada + +- **Autoridad y decisiones:** Workspace Authority administra grants por workspace; IsySentinel aporta decisiones deny-by-default; `ProductActionGate` valida solicitudes y los execution owners realizan las acciones autorizadas. Las credenciales y los providers no son grants. +- **Efectos y auditoría:** los owners y adaptadores aíslan lectura, escritura, Git y comandos; los journals/ledgers registran resultados. La implementación depende de límites y cobertura correctos por owner. +- **Interfaz:** CLI/TUI construida sobre Textual. `TUIApp` compone varios mixins para sesiones, chat, providers, workspace, permisos, herramientas y rail. La separación ayuda a ubicar responsabilidades, aunque existe acoplamiento entre mixins y módulos extensos. +- **Proveedores y continuidad:** adaptadores para providers, transporte de streaming, sesiones y recuperación de turnos. +- **Hive:** coordinación local de agentes por inbox/outbox, router, hooks y lanzamiento de procesos. La superficie de lanzamiento y sus límites de escritura tienen hallazgos de seguridad aún abiertos; la existencia de Sentinel en otras rutas no los resuelve por sí sola. + +## 3. Seguridad y dependencias + +### Dependencias y secretos + +`pip-audit 2.10.1` consultó OSV sobre las dependencias instaladas en el `.venv` local (42), una resolución Linux/Python 3.10 con todos los extras (56) y una resolución Windows/Python 3.12 con todos los extras (47). El resultado fue 0 avisos conocidos en los 145 registros combinados. Las dos resoluciones se generaron desde los rangos vigentes en `pyproject.toml`; no existe lockfile ni hashes de artefactos, por lo que los resultados son una foto de las versiones compatibles, no una certificación de un release reproducible. + +La heurística de alta confianza revisó 1,759 archivos de texto rastreados y no halló patrones comunes de claves privadas ni tokens. No escaneó archivos ignorados/no rastreados, binarios, archivos de más de 12 MB ni formatos arbitrarios: no demuestra que no haya secretos locales. + +### Controles y configuración + +- `src/isycode/credentials.py` usa el keyring del sistema para valores de claves, separa metadatos en SQLite, rechaza backends inseguros y falla cerrado cuando el keyring no está disponible. +- `.gitignore` excluye `.env`, `.env.*`, `.isyroot` y estado local, y permite `.env.example`. `SECURITY.md` delimita las pruebas seguras y la divulgación. +- Los workflows declaran `contents: read` de forma general y `contents: write` solo en el job de release. No se encontró `pull_request_target` en los triggers revisados. +- Las acciones de GitHub usan etiquetas de versión mutables en vez de SHA completos. Es una mejora pendiente de cadena de suministro; GitHub documenta el SHA completo como referencia inmutable en [Secure use reference](https://docs.github.com/en/actions/reference/security/secure-use). +- `pyproject.toml` contiene configuración de Ruff, mypy y coverage; no se encontró evidencia de que los workflows los ejecuten ni de jobs de escaneo de dependencias o secretos. + +### Hallazgos de Hive + +La auditoría post-merge anterior reprodujo tres problemas P1 en fixtures sobre `594050e`. El commit local `74218e3` solo cambió `types.py` para generar timestamps UTC con zona horaria; no agregó validación de IDs o remitentes. `router.py`, `guard.py` y `spawn.py` no cambiaron. Los tres hallazgos estaban abiertos en `87b58ea`: + +1. El router forma rutas desde IDs de mensaje no confiables y conserva el remitente declarado por el payload (`src/isycode/hive/router.py`, `src/isycode/hive/types.py`). +2. Un symlink del outbox puede resolver fuera del espacio permitido de un agente (`src/isycode/hive/guard.py`). +3. El launcher inicia un subprocess sin un owner de ejecución acotado y autorizado (`src/isycode/hive/spawn.py`). + +El corte `87b58ea` no incluye los cambios que luego se integraron mediante PR #62 (`c033d82`). La guía de esa reparación está en `docs/audits/2026-10-18-hive-repair/GUIA.md`; este informe no vuelve a probar los casos contra la versión mergeada. + +## 4. Calidad, pruebas y mantenibilidad + +La revisión estructural señaló módulos planos y extensos, métodos grandes como `ChatMixin._run_chat`, el tamaño de `action_runtime.py` y acoplamiento entre mixins. El arreglo de truncamiento no descompone todavía esos módulos. + +El análisis estático del AST contó 134 handlers que capturan `Exception` o `BaseException`, de los que 15 empiezan con `pass`, `continue` o `return` sin diagnóstico. No se encontraron `except:` desnudos. Es una señal para revisar mantenibilidad y observabilidad, no 134 vulnerabilidades. El análisis tampoco encontró usos literales de `shell=True`, `os.system`, `ssl=False`, `CERT_NONE` ni `verify=False` en `src/isycode`. + +Las 52 pruebas focalizadas corresponden al arreglo local de TUI/búsqueda. No se ejecutaron durante esta auditoría de seguridad/dependencias, y no validan los hallazgos de Hive. La suite completa y la matriz CI quedan pendientes. + +El informe post-merge previo también registró fallos del snapshot M15 en su corte. `docs/security/m15-authority-coverage.json` cambió en el merge local, pero su generador y prueba dedicada no forman parte de la verificación focalizada aquí; en `87b58ea` quedaba **por verificar**; PR #62 lo regeneró y #63 sincronizó sus líneas. Este informe no vuelve a ejecutar el generador ni la prueba dedicada. + +## 5. Hallazgos priorizados + +| Severidad | Hallazgo | Archivos afectados | Estado al corte `87b58ea` | +|---|---|---|---| +| Alta (P1) | Router forma rutas desde IDs no confiables y confía en el remitente del mensaje. | `src/isycode/hive/router.py`, `src/isycode/hive/types.py` | Abierto; reproducción previa documentada sobre `594050e`; `types.py` solo tuvo un cambio de timestamp en `74218e3`, sin validación de identidad/ID. | +| Alta (P1) | Symlink del outbox puede escapar del espacio del agente. | `src/isycode/hive/guard.py` | Abierto; reproducción previa documentada; sin cambio en `74218e3`. | +| Alta (P1) | Hive lanza subprocess sin owner acotado que pase por Authority/IsySentinel y registre el efecto. | `src/isycode/hive/spawn.py` | Abierto; no cubierto por corregir solo la interfaz. | +| Media | No hay lockfile ni hashes para fijar las dependencias instaladas/publicadas. | `pyproject.toml`, proceso de distribución | Abierto como riesgo de reproducibilidad; OSV halló 0 avisos en las versiones resueltas y `.venv`. | +| Media-baja | Las GitHub Actions se referencian con etiquetas mutables, no SHA inmutable. | `.github/workflows/` | Pendiente de pinning verificado y mantenimiento. | +| Media | 134 handlers amplios; 15 no registran el error antes de continuar/retornar. | `src/isycode/` | Deuda de observabilidad; revisar caso por caso, preservando fail-closed. | +| Media / verificación | Estado del snapshot M15 después de regenerar el artefacto. | `docs/security/m15-authority-coverage.json`, `tests/test_action_coverage.py` | Por verificar en `87b58ea`; PR #62 lo regeneró y #63 sincronizó sus líneas. Esta PR no volvió a ejecutar el generador ni la prueba dedicada. | +| Informativa | Heurística de secretos sin candidatos de alta confianza en 1,759 archivos de texto rastreados. | Archivos rastreados incluidos por la pasada | Sin hallazgos dentro de ese alcance; no es certificación. | +| Informativa | `pip-audit` no encontró avisos OSV conocidos en 145 registros entre tres conjuntos. | `pyproject.toml`, `.venv`, evidencia bajo `docs/audits/` | Sin avisos conocidos en la corrida del 19-10-2026; repetir al preparar releases. | +| Corregido | TUI/listados y búsqueda en repositorios grandes truncaban resultados; la salida podía invalidar JSON. | `src/isycode/action_runtime.py`, `src/isycode/workspace_pack.py`, `src/isycode/tui_app_rail.py`, `src/isycode/git_owner.py` | Corregido en `74218e3`, integrado en `87b58ea`; 52 pruebas focalizadas aprobadas. | + +## 6. Plan recomendado + +1. Repetir los probes de los tres P1 de Hive sobre el `main` actual para confirmar los cambios mergeados por #62: validar IDs y remitentes, rechazar enlaces fuera del límite e impedir lanzamientos sin owner autorizado. +2. Confirmar M15 con el generador canónico y su prueba dedicada después de las actualizaciones de #62/#63; esta PR no vuelve a ejecutar esa verificación. +3. Añadir lockfile y hashes compatibles con el proceso de release; automatizar OSV sobre esa resolución y alertas periódicas. +4. Fijar acciones de GitHub a SHA verificados, con permisos mínimos y mecanismo explícito de actualización. +5. Incorporar escaneo de secretos al CI con revisión de falsos positivos que no exponga valores sensibles. +6. Revisar los 15 handlers sin diagnóstico, priorizando Hive; luego habilitar en CI los gates configurados de Ruff/mypy/coverage. +7. Ejecutar suite completa y CI tras los cambios de Hive y M15, registrando resultados por plataforma. + +## Fuentes + +- `auditoria_arquitectura_calidad.md` (18-10-2026): arquitectura, calidad, pruebas y plan técnico. +- `auditoria_seguridad_dependencias.md` (19-10-2026): auditoría OSV, secretos, configuración y errores; salidas y requisitos resueltos en `docs/audits/2026-10-19-security-deps-87b58ea/`. +- `ISyCode/AUDITORIA_CODIGO.md`, `ISyCode/AUDITORIA_HALLAZGOS.txt`, `ISyCode/AUDITORIA_RESUMEN.md` (18-10-2026): resumen de la revisión Kiro; afirmaciones de seguridad no tomadas como certificación independiente. +- `vicios-tecnicos/briefings/isicode/2026-10-18-post-merge-hive-audit.md`: hallazgos Hive y snapshot en el corte `594050e`. +- `vicios-tecnicos/briefings/isicode/2026-10-18-hive-repair.md` (nota histórica externa): alcance del cambio que después se integró mediante PR #62; el corte `87b58ea` no lo contiene. +- Diff local `594050e..87b58ea`, commit de reparación `74218e3`, y pruebas focalizadas indicadas arriba. + +### SHA-256 de las notas fuente + +```text +f76355305b5e8721ead224d9f958f12e5515c78e19ec15c4d55f637db9006ee2 auditoria_arquitectura_calidad.md +621d400cc3269ca50b83c4b790fbd1207d360ace24692409897e468be975ddc0 ISyCode/AUDITORIA_CODIGO.md +bb1ac69b37599154d8b35fa76e4e67888ce5009f1c7328970c5320a7e93c6407 ISyCode/AUDITORIA_HALLAZGOS.txt +f965259ad931b13996b476df46ea98d64971b93a13373d3300b40b6b06189fbd ISyCode/AUDITORIA_RESUMEN.md +e24cb156104667806babe3838116a893c27606d0958fcd6281a8c3bbf372ad98 vicios-tecnicos/briefings/isicode/2026-10-18-post-merge-hive-audit.md +ccd1f0897df164cb32dd66ec5f52533825f52b91a90562664d5f6dae201a4440 vicios-tecnicos/briefings/isicode/2026-10-18-hive-repair.md +eb30ff4f233fabc47cf71e7fbb6e87b159d3899c352506809cacf7fe6cc886de auditoria_seguridad_dependencias.md +228dd8c24df0c7fe6f4a76c7a76af0aae25ce1bc4e346802e1005843d73dc924 docs/audits/2026-10-19-security-deps-87b58ea/SHA256SUMS.txt +``` diff --git a/docs/audits/2026-10-19-security-deps-87b58ea/auditoria_seguridad_dependencias.md b/docs/audits/2026-10-19-security-deps-87b58ea/auditoria_seguridad_dependencias.md new file mode 100644 index 0000000..aa83087 --- /dev/null +++ b/docs/audits/2026-10-19-security-deps-87b58ea/auditoria_seguridad_dependencias.md @@ -0,0 +1,90 @@ +# Auditoría de seguridad, dependencias, configuración, secretos y errores — ISyCode + +- **Fecha:** 19 de octubre de 2026 +- **Corte:** `87b58ea` (`main` local) +- **Proyecto:** `DannyBaanks/ISyCode` +- **Tipo de revisión:** análisis estático del código y configuración, heurística de secretos sobre archivos rastreados y auditoría de versiones con OSV. No se hicieron pruebas contra proveedores, cuentas ni infraestructura externa. + +> **Vigencia (19-10-2026):** este informe conserva el estado del corte `87b58ea`. [PR #62](https://github.com/DannyBaanks/ISyCode/pull/62) (`c033d82`) y [PR #63](https://github.com/DannyBaanks/ISyCode/pull/63) (`1095ad1`) se integraron a `main` después de ese corte. #62 incorporó validación de IDs/remitentes, rechazo de rutas enlazadas y límites de ejecución Hive, además de regenerar el snapshot M15; #63 atendió hallazgos de permisos de TUI y sincronizó líneas del snapshot. Los P1 que aquí figuran abiertos corresponden a `87b58ea`; esta actualización no repitió los probes de explotación contra `1095ad1`. Los resultados de OSV y de secretos siguen limitados al corte original. La reparación TUI portada a esta PR pasó 52 pruebas focalizadas en `87b58ea`, sin repetirse tras el port; queda la CI de esta PR. + + +## Resumen + +`pip-audit` no encontró vulnerabilidades conocidas en tres conjuntos revisados: las dependencias instaladas en el `.venv` local (42 distribuciones), una resolución Linux/Python 3.10 con todos los extras (56) y una resolución Windows/Python 3.12 con todos los extras (47). La resolución parte de los rangos actuales de `pyproject.toml`; como no existe lockfile, no identifica una instalación reproducible de release ni sus hashes. + +La pasada heurística sobre **1,759 archivos de texto rastreados** no halló formatos comunes de claves privadas o tokens de alta confianza. No leyó valores de posibles secretos ni escaneó archivos no rastreados, ignorados, binarios o mayores de 12 MB. Por tanto, no certifica que no existan secretos locales ni cubre formatos arbitrarios. + +La revisión registró **tres hallazgos de severidad alta previamente reproducidos en Hive**: construcción de rutas desde mensajes no confiables, escape de límites con symlink y lanzamiento de proceso sin owner de ejecución. En el corte `87b58ea`, el commit `74218e3` solo modificó `types.py` para generar timestamps UTC con zona horaria; no añadió validación de IDs o remitentes. Los problemas estaban abiertos en ese corte. PR #62 incorporó después cambios en las rutas afectadas; no se repitieron aquí los probes sobre la versión mergeada. + +## Método y evidencia conservada + +- Corte de Git inspeccionado: `87b58ea`; la reparación de `74218e3` está incluida localmente. En el momento de ejecutar esta auditoría, el estado remoto no se había actualizado y no se había hecho push. +- Resolver: `uv 0.12.17`, `uv pip compile --all-extras` para Linux/Python 3.10 y Windows/Python 3.12. Las listas resultantes contienen versiones exactas, pero no hashes de artefactos. +- Escáner: `pip-audit 2.10.1`, fuente de vulnerabilidades OSV, en modo estricto. Las listas ya resueltas se analizaron con `--no-deps --disable-pip`; también se escaneó el `.venv` real. El primer intento que dejó a pip instalar el conjunto falló por ejecutarse bajo Python 3.14 y por el paquete exclusivo de Windows desde Linux; se repitió sin instalación, consultando las versiones exactas resueltas. La documentación de [pip-audit](https://github.com/pypa/pip-audit/blob/main/README.md) describe el análisis de requirements y el servicio OSV. +- Secretos: reglas de alta confianza para cabeceras PEM y patrones comunes de AWS, GitHub, Google, Stripe, Slack, OpenAI y Anthropic; el resultado guardado contiene solo conteos/rutas, nunca valores coincidentes. +- Evidencia cruda, listas resueltas, salidas JSON, stderr, comandos, entorno y hashes: [`./`](./). + +## Resultados por área + +### Dependencias + +| Conjunto | Distribuciones auditadas | Hallazgos OSV | +|---|---:|---:| +| `.venv` local, Python 3.12.3 | 42 | 0 | +| Resolución Linux, Python 3.10, todos los extras | 56 | 0 | +| Resolución Windows, Python 3.12, todos los extras | 47 | 0 | + +No hay `uv.lock`, `poetry.lock`, `Pipfile.lock` ni `requirements*.txt` en el checkout. Las dependencias directas están expresadas como rangos en `pyproject.toml`; el resultado de hoy puede variar cuando se instale mañana. Tampoco se encontró configuración de Dependabot o Renovate en `.github/`. No hay evidencia de un job de auditoría de dependencias en los workflows revisados. + +**Conclusión:** cero CVE/avisos conocidos en los tres conjuntos consultados al momento del escaneo; la actualización y trazabilidad de lo que se publica siguen sin quedar fijadas. El informe Kiro anterior señalaba OWASP A06 como pendiente; la auditoría actual ya consultó un advisory database para los conjuntos indicados, pero no convierte el proyecto en libre de vulnerabilidades. + +### Gestión de secretos y credenciales + +- `src/isycode/credentials.py` guarda valores de API key mediante el keyring del sistema operativo y metadatos separados en SQLite. Rechaza backends de keyring no seguros, usa permisos privados para el archivo, verifica enlaces duros/symlinks y falla cerrado si el keyring no está disponible. +- `.gitignore` excluye `.env`, `.env.*`, `.isyroot` y estado local, permitiendo explícitamente `.env.example`. +- `SECURITY.md` limita las pruebas a datos y credenciales ficticios, describe la divulgación privada y aclara que publicar código no es una auditoría independiente. +- Resultado heurístico: cero candidatos de alta confianza en los archivos de texto rastreados. La exploración de archivos ignorados/no rastreados y formatos genéricos de credencial queda **NOT_DEMONSTRATED**. + +### Configuración y CI + +- Los workflows declaran `permissions: contents: read` a nivel general; el job que crea releases pide `contents: write` localmente. Los triggers observados son `pull_request`, `push` y `workflow_dispatch`; no se encontró `pull_request_target`. +- Las acciones usan etiquetas principales, por ejemplo `actions/checkout@v7`, `actions/setup-python@v7`, `upload-artifact@v7` y `download-artifact@v8`, no SHA de commit inmutables. Es una exposición de cadena de suministro de severidad media-baja, mitigada en parte por acciones de organizaciones conocidas y permisos limitados. GitHub indica que fijar una acción a SHA completo es la forma inmutable de referenciar su release; consultar [Secure use reference](https://docs.github.com/en/actions/reference/security/secure-use). +- No se encontró un job que ejecute `pip-audit`, escáner de secretos ni análisis estático de seguridad. `pyproject.toml` sí contiene configuración de Ruff, mypy y coverage, pero los workflows inspeccionados no los ejecutan. + +### Manejo de errores y ejecución + +El análisis AST contó **134 handlers que capturan `Exception` o `BaseException`** bajo `src/isycode`; **15** empiezan con `pass`, `continue` o `return` sin registrar el error. No hay handlers `except:` desnudos. El conteo es una señal de revisión, no 134 vulnerabilidades: algunos límites deben denegar ante excepciones. Aun así, la supresión sin diagnóstico dificulta detectar fallos; los casos de `hive/router.py`, `hive/manager.py`, `hive/hook_server.py` y `hive/hook_shim.py` merecen prioridad por tratar mensajes y coordinación. + +No se encontró uso literal de `shell=True`, `os.system`, `ssl=False`, `CERT_NONE` ni `verify=False` en `src/isycode`. Los subprocesses se lanzan en varios módulos mediante `create_subprocess_exec`; eso evita el shell implícito, pero no sustituye la validación de argumentos ni un owner de efecto. `src/isycode/l1.py` ejecuta código Python candidato, pero el camino observado lo prepara con `CommandRunOwner`, un workspace temporal y `promote=False`; no se ejecutó un payload hostil en esta revisión. + +## Hallazgos clasificados + +No se demostró un hallazgo crítico en el alcance ejecutado. + +| Severidad | Hallazgo | Archivos afectados | Estado | +|---|---|---|---| +| **Alta** | El router forma rutas de entrega desde IDs del mensaje y confía en el remitente declarado. La revisión post-merge anterior lo reprodujo escribiendo fuera del Hive y conservando remitente falsificado. | `src/isycode/hive/router.py`, `src/isycode/hive/types.py` | **Abierto en `87b58ea`; PR #62 incorporó después cambios a esa ruta, no revalidados aquí.** `74218e3` solo cambió la generación del timestamp UTC en `types.py`; el `from_dict` aún acepta los campos serializados y no se añadieron validaciones de ID/remitente. Esta sesión no volvió a ejecutar el probe. Evidencia previa: `vicios-tecnicos/briefings/isicode/2026-10-18-post-merge-hive-audit.md`. | +| **Alta** | Un symlink dentro del outbox permite que el destino real caiga en el espacio de otro agente. | `src/isycode/hive/guard.py` | **Abierto en `87b58ea`; PR #62 incorporó después cambios a esa ruta, no revalidados aquí.** El archivo no fue modificado por `74218e3`; reproducción previa documentada en el mismo briefing. | +| **Alta** | El launcher Hive crea un subprocess sin una solicitud ligada a Authority/IsySentinel, owner acotado y journal de efecto. | `src/isycode/hive/spawn.py:78` y la ruta de lanzamiento Hive | **Abierto en `87b58ea`; PR #62 incorporó después cambios a esa ruta, no revalidados aquí.** La presencia de otros gates en la aplicación no cubre este efecto. La reparación de PR #62 no forma parte de `87b58ea`; #62 se integró después, sin que esta auditoría repitiera el probe. | +| **Media** | No hay lockfile ni hashes para fijar las dependencias que llegan a instalación/publicación; no se encontró automatización de alertas. | `pyproject.toml`, `.github/` | **Abierto como riesgo de reproducibilidad.** El escaneo actual encontró 0 avisos, pero es una foto de versiones resueltas. | +| **Media-baja** | Las acciones de GitHub se referencian por etiquetas de versión, no por SHA inmutable. | `.github/workflows/ci.yml`, `.github/workflows/platform-parity.yml` | **Observación de cadena de suministro.** Configurar pinning verificado y un proceso para actualizarlo. | +| **Media** | Existen 134 handlers amplios y 15 suprimen o abandonan errores sin diagnóstico. | `src/isycode/hive/`, `src/isycode/credentials.py`, `src/isycode/updater.py`, `src/isycode/tui_app_*.py` | **Deuda de observabilidad.** Revisar uno por uno; conservar el comportamiento fail-closed donde sea requisito. | +| **Informativa** | La heurística de secretos no detectó candidatos en texto rastreado. | 1,759 archivos de texto rastreados | **Sin hallazgo en el alcance de la heurística; no es certificación.** | +| **Informativa** | `pip-audit` no identificó vulnerabilidades conocidas en 145 registros de los tres conjuntos revisados. | `pyproject.toml`, `.venv`, evidencia de OSV | **Sin hallazgo de CVE conocido en esta corrida; repetir al fijar versión de release.** | + +## Plan de acción + +1. Repetir sobre `main` los probes de los hallazgos de Hive con datos ficticios para verificar los cambios mergeados por #62: validar IDs/remitentes, resolver y rechazar symlinks fuera del límite, y bloquear procesos sin owner autorizado. +2. Crear un lockfile y, si el proceso de distribución lo permite, fijar hashes; documentar cómo se actualiza para Linux, Windows y extras opcionales. +3. Añadir un job de auditoría de dependencias contra la resolución bloqueada y configurar alertas de actualizaciones; mantener la salida OSV/PyPI como evidencia del corte. +4. Fijar las GitHub Actions a SHA verificado o a un lock de acciones mantenido automáticamente, conservando los permisos mínimos. +5. Incorporar un escáner de secretos al CI sobre archivos rastreados y documentar la revisión de falsos positivos sin guardar valores sensibles. +6. Revisar los 15 handlers que ocultan errores, empezando por Hive; añadir diagnóstico estructurado y pruebas cuando la supresión no sea parte explícita del comportamiento fail-closed. + +## Límites y estado + +- No se ejecutó la suite de pruebas; esta tarea fue una auditoría de lectura y análisis de dependencias. +- No se hicieron pruebas con proveedores, OAuth, Gateway real, credenciales reales, APIs externas ni infraestructura de terceros. +- El escaneo OSV cubrió versiones resueltas y el `.venv` local, no una distribución construida ni todas las plataformas/arquitecturas. +- Las tres fallas de Hive proceden de una reproducción independiente previa sobre el corte `594050e`; este informe las registra abiertas porque `87b58ea` no incorporaba las correcciones que después entraron por PR #62 y esta sesión no repitió el probe. El cambio de timestamp en `types.py` no modifica esa conclusión al corte `87b58ea`; PR #62 se integró después y abordó las rutas descritas, sin revalidación de probes en esta sesión. +- Estado Git del proyecto al consolidar: `main` local `87b58ea`, adelante 2 y detrás 1 de `origin/main`; no se hizo push.