codex-core 0.3.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {codex_core-0.3.0 → codex_core-0.5.0}/.gitignore +3 -0
  2. codex_core-0.5.0/AGENTS.md +11 -0
  3. {codex_core-0.3.0 → codex_core-0.5.0}/CHANGELOG.md +23 -0
  4. {codex_core-0.3.0 → codex_core-0.5.0}/PKG-INFO +17 -5
  5. {codex_core-0.3.0 → codex_core-0.5.0}/README.md +12 -0
  6. codex_core-0.5.0/docs/agent-skills/codex-core-maintenance/SKILL.md +14 -0
  7. codex_core-0.5.0/docs/en/tasks/agent_skills.md +26 -0
  8. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/tasks/getting_started.md +4 -0
  9. codex_core-0.5.0/docs/ru/tasks/agent_skills.md +26 -0
  10. {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/tasks/getting_started.md +4 -0
  11. {codex_core-0.3.0 → codex_core-0.5.0}/mkdocs.yml +2 -0
  12. {codex_core-0.3.0 → codex_core-0.5.0}/pyproject.toml +20 -3
  13. codex_core-0.5.0/src/codex_core/agent_skills/__init__.py +1 -0
  14. codex_core-0.5.0/src/codex_core/agent_skills/__main__.py +4 -0
  15. codex_core-0.5.0/src/codex_core/agent_skills/cli.py +39 -0
  16. codex_core-0.5.0/src/codex_core/agent_skills/fs_transaction.py +174 -0
  17. codex_core-0.5.0/src/codex_core/agent_skills/manifest_rules.py +153 -0
  18. codex_core-0.5.0/src/codex_core/agent_skills/orchestrator.py +227 -0
  19. codex_core-0.5.0/src/codex_core/agent_skills/resources/SKILL.md +18 -0
  20. codex_core-0.5.0/src/codex_core/agent_skills/resources/__init__.py +1 -0
  21. codex_core-0.5.0/src/codex_core/agent_skills/resources/references/common-logging.md +17 -0
  22. codex_core-0.5.0/src/codex_core/agent_skills/resources/references/developer-tools.md +13 -0
  23. codex_core-0.5.0/src/codex_core/agent_skills/resources/references/dto-pii.md +17 -0
  24. codex_core-0.5.0/src/codex_core/agent_skills/resources/references/integration.md +14 -0
  25. codex_core-0.5.0/src/codex_core/agent_skills/resources/references/settings.md +16 -0
  26. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/common/__init__.py +4 -1
  27. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/common/log_context.py +44 -12
  28. codex_core-0.5.0/src/codex_core/common/loguru_setup.py +307 -0
  29. codex_core-0.5.0/tests/unit/agent_skills/__init__.py +1 -0
  30. codex_core-0.5.0/tests/unit/agent_skills/test_installer.py +592 -0
  31. codex_core-0.5.0/tests/unit/common/test_log_context.py +98 -0
  32. codex_core-0.5.0/tests/unit/common/test_loguru_setup.py +221 -0
  33. codex_core-0.5.0/tools/dev/__init__.py +0 -0
  34. {codex_core-0.3.0 → codex_core-0.5.0}/uv.lock +104 -71
  35. codex_core-0.3.0/project_structure.txt +0 -97
  36. codex_core-0.3.0/src/codex_core/common/loguru_setup.py +0 -343
  37. codex_core-0.3.0/tests/unit/common/test_log_context.py +0 -53
  38. codex_core-0.3.0/tests/unit/common/test_loguru_setup.py +0 -140
  39. codex_core-0.3.0/tests/unit/dev/__init__.py +0 -1
  40. {codex_core-0.3.0 → codex_core-0.5.0}/.github/workflows/ci.yml +0 -0
  41. {codex_core-0.3.0 → codex_core-0.5.0}/.github/workflows/docs.yml +0 -0
  42. {codex_core-0.3.0 → codex_core-0.5.0}/.github/workflows/publish.yml +0 -0
  43. {codex_core-0.3.0 → codex_core-0.5.0}/.pre-commit-config.yaml +0 -0
  44. {codex_core-0.3.0 → codex_core-0.5.0}/.python-version +0 -0
  45. {codex_core-0.3.0 → codex_core-0.5.0}/docs/changelog.md +0 -0
  46. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/README.md +0 -0
  47. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/common.md +0 -0
  48. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/core.md +0 -0
  49. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/dev/check_runner.md +0 -0
  50. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/dev/index.md +0 -0
  51. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/dev/project_tree.md +0 -0
  52. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/dev/static_compiler.md +0 -0
  53. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/index.md +0 -0
  54. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/settings.md +0 -0
  55. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/architecture/README.md +0 -0
  56. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/architecture/platform/common.md +0 -0
  57. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/architecture/platform/core.md +0 -0
  58. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/architecture/platform/dev.md +0 -0
  59. {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/architecture/platform/settings.md +0 -0
  60. {codex_core-0.3.0 → codex_core-0.5.0}/docs/evolution/roadmap.md +0 -0
  61. {codex_core-0.3.0 → codex_core-0.5.0}/docs/index.md +0 -0
  62. {codex_core-0.3.0 → codex_core-0.5.0}/docs/planning/python_version_policy.md +0 -0
  63. {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/README.md +0 -0
  64. {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/architecture/README.md +0 -0
  65. {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/architecture/platform/common.md +0 -0
  66. {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/architecture/platform/core.md +0 -0
  67. {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/architecture/platform/dev.md +0 -0
  68. {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/architecture/platform/settings.md +0 -0
  69. {codex_core-0.3.0 → codex_core-0.5.0}/docs/stylesheets/extra.css +0 -0
  70. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/__init__.py +0 -0
  71. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/common/phone.py +0 -0
  72. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/common/text.py +0 -0
  73. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/core/__init__.py +0 -0
  74. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/core/base_dto.py +0 -0
  75. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/core/exceptions.py +0 -0
  76. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/core/pii.py +0 -0
  77. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/__init__.py +0 -0
  78. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/check_runner.py +0 -0
  79. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/project_tree.py +0 -0
  80. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/static_compiler/__init__.py +0 -0
  81. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/static_compiler/compiler.py +0 -0
  82. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/static_compiler/css.py +0 -0
  83. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/static_compiler/js.py +0 -0
  84. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/py.typed +0 -0
  85. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/settings/__init__.py +0 -0
  86. {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/settings/base.py +0 -0
  87. {codex_core-0.3.0 → codex_core-0.5.0}/tests/conftest.py +0 -0
  88. {codex_core-0.3.0 → codex_core-0.5.0}/tests/integration/__init__.py +0 -0
  89. {codex_core-0.3.0 → codex_core-0.5.0}/tests/integration/conftest.py +0 -0
  90. {codex_core-0.3.0 → codex_core-0.5.0}/tests/integration/test_settings_integration.py +0 -0
  91. {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/__init__.py +0 -0
  92. {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/common/__init__.py +0 -0
  93. {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/common/test_phone.py +0 -0
  94. {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/common/test_text.py +0 -0
  95. {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/conftest.py +0 -0
  96. {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/core/__init__.py +0 -0
  97. {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/core/test_exceptions.py +0 -0
  98. {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/core/test_pii.py +0 -0
  99. {codex_core-0.3.0/tools → codex_core-0.5.0/tests/unit/dev}/__init__.py +0 -0
  100. {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/dev/test_check_runner.py +0 -0
  101. {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/dev/test_static_compiler.py +0 -0
  102. {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/settings/__init__.py +0 -0
  103. {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/settings/test_settings.py +0 -0
  104. {codex_core-0.3.0/tools/dev → codex_core-0.5.0/tools}/__init__.py +0 -0
  105. {codex_core-0.3.0 → codex_core-0.5.0}/tools/dev/README.md +0 -0
  106. {codex_core-0.3.0 → codex_core-0.5.0}/tools/dev/check.py +0 -0
  107. {codex_core-0.3.0 → codex_core-0.5.0}/tools/dev/generate_project_tree.py +0 -0
@@ -202,3 +202,6 @@ gen/
202
202
  marimo/_static/
203
203
  marimo/_lsp/
204
204
  __marimo__/
205
+
206
+ # graphify
207
+ graphify-out/
@@ -0,0 +1,11 @@
1
+ ## graphify
2
+
3
+ This component is part of a larger project. It uses a global knowledge graph located at the workspace root (`../graphify-out/`).
4
+
5
+ Rules:
6
+ - Before answering architecture or codebase questions, read the global report at **`../graphify-out/GRAPH_REPORT.md`** for god nodes and community structure.
7
+ - If `../graphify-out/wiki/index.md` exists, navigate it instead of reading raw files.
8
+ - For cross-module questions, prefer `graphify query`, `graphify path`, or `graphify explain` (run from root or via MCP) over grep.
9
+ - After modifying code files in this session, run `graphify update .` from the **workspace root** to keep the global graph current.
10
+
11
+ For changes to this package, its consumer skill, installer, or release notes, read [codex-core maintenance](docs/agent-skills/codex-core-maintenance/SKILL.md).
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.5.0] - 2026-10-09
11
+
12
+ ### Added
13
+
14
+ - Packaged an optional `codex-core` agent usage skill with focused references and an offline install/update/delete/status tool.
15
+ - Added source-repository maintenance guidance for the package and skill delivery.
16
+
17
+ ### Fixed
18
+
19
+ - Refresh locked runtime, development, and documentation dependencies to patched releases identified by the release audit.
20
+ - Use `None` as the default logging context value to satisfy the B039 lint rule.
21
+ - Type the healthcheck filter with Loguru's record contract while keeping Loguru optional at runtime.
22
+ - Restrict source distributions to project sources and documentation, excluding local environments and build caches.
23
+
24
+ ## [0.4.0] - 2026-05-20
25
+
26
+ ### Added
27
+
28
+ - Added async-local logging context helpers and context injection for Loguru records.
29
+ - Added production JSON stdout logging and healthcheck filtering to `setup_logging`.
30
+
8
31
  ## [0.3.0] - 2026-04-04
9
32
 
10
33
  ### Added
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: codex-core
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Core utilities, schemas and settings for Codex WaaS toolkit
5
5
  Project-URL: Homepage, https://github.com/codexdlc/codex-core
6
6
  Project-URL: Documentation, https://codexdlc.github.io/codex-core/
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
19
19
  Requires-Python: >=3.12
20
- Requires-Dist: pydantic-settings>=2.0
20
+ Requires-Dist: pydantic-settings>=2.14.2
21
21
  Requires-Dist: pydantic<3.0,>=2.0
22
22
  Provides-Extra: dev
23
23
  Requires-Dist: bandit>=1.7; extra == 'dev'
@@ -27,12 +27,12 @@ Requires-Dist: pip-audit>=2.7; extra == 'dev'
27
27
  Requires-Dist: pre-commit>=3.0; extra == 'dev'
28
28
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
29
29
  Requires-Dist: pytest-cov; extra == 'dev'
30
- Requires-Dist: pytest>=8.0; extra == 'dev'
30
+ Requires-Dist: pytest>=9.0.3; extra == 'dev'
31
31
  Requires-Dist: ruff>=0.4; extra == 'dev'
32
32
  Provides-Extra: docs
33
33
  Requires-Dist: mike>=2.0; extra == 'docs'
34
34
  Requires-Dist: mkdocs-include-markdown-plugin; extra == 'docs'
35
- Requires-Dist: mkdocs-material>=9.0; extra == 'docs'
35
+ Requires-Dist: mkdocs-material>=9.7.7; extra == 'docs'
36
36
  Requires-Dist: mkdocs>=1.5; extra == 'docs'
37
37
  Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
38
38
  Provides-Extra: loguru
@@ -64,6 +64,18 @@ pip install "codex-core[loguru]"
64
64
 
65
65
  Requires Python 3.12 or newer.
66
66
 
67
+ ## Optional agent skill
68
+
69
+ Starting with version 0.5.0, the package includes an offline `codex-core` usage skill for coding agents. From the consumer project root, use the same Python environment that has the package installed:
70
+
71
+ ```bash
72
+ python -m pip install "codex-core>=0.5.0"
73
+ python -m codex_core.agent_skills install --project .
74
+ python -m codex_core.agent_skills status --project .
75
+ ```
76
+
77
+ `install` creates a managed copy under `.agents/skills/codex-core/` and a marked instruction in `AGENTS.md`. After upgrading the Python package, run `python -m codex_core.agent_skills update --project .` to refresh the skill. The installer refuses to discard local edits to managed files and preserves unrelated project content. See the [agent skill guide](docs/en/tasks/agent_skills.md) for the full lifecycle, removal, and recovery steps. The canonical editable source is `src/codex_core/agent_skills/resources/` in this repository.
78
+
67
79
  ## Development
68
80
 
69
81
  ```bash
@@ -23,6 +23,18 @@ pip install "codex-core[loguru]"
23
23
 
24
24
  Requires Python 3.12 or newer.
25
25
 
26
+ ## Optional agent skill
27
+
28
+ Starting with version 0.5.0, the package includes an offline `codex-core` usage skill for coding agents. From the consumer project root, use the same Python environment that has the package installed:
29
+
30
+ ```bash
31
+ python -m pip install "codex-core>=0.5.0"
32
+ python -m codex_core.agent_skills install --project .
33
+ python -m codex_core.agent_skills status --project .
34
+ ```
35
+
36
+ `install` creates a managed copy under `.agents/skills/codex-core/` and a marked instruction in `AGENTS.md`. After upgrading the Python package, run `python -m codex_core.agent_skills update --project .` to refresh the skill. The installer refuses to discard local edits to managed files and preserves unrelated project content. See the [agent skill guide](docs/en/tasks/agent_skills.md) for the full lifecycle, removal, and recovery steps. The canonical editable source is `src/codex_core/agent_skills/resources/` in this repository.
37
+
26
38
  ## Development
27
39
 
28
40
  ```bash
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: codex-core-maintenance
3
+ description: Use when changing codex-core public modules, packaging, release notes, or its consumer skill and offline installer in this source repository.
4
+ ---
5
+
6
+ # codex-core maintenance
7
+
8
+ This repository is the canonical source of the `codex-core` Python package. Package metadata and build rules are in `pyproject.toml`; public code is under `src/codex_core`. The packaged consumer skill source is `src/codex_core/agent_skills/resources/`. Consumer installations under `.agents/skills/codex-core/` are generated, manifest-owned copies. Edit source here, not a consumer copy or its `.manifest.json`.
9
+
10
+ For API changes, keep the skill references aligned with shipped public imports and behavior. `BaseDTO` freezes Pydantic fields and masks its string representation; it does not sanitize `model_dump()`. A `PIIRegistry` subclass globally replaces heuristic matching with exact names, so partial declarations affect all DTOs in the process. `BaseCommonSettings` intentionally omits an env-file path and defaults to `debug=True`. `codex_core.common` exposes normalization and logging helpers; Loguru configuration requires the optional extra and is application-owned.
11
+
12
+ The offline installer entrypoint is `python -m codex_core.agent_skills {install,update,delete,status} --project PATH`. It writes only its manifest-owned skill files and marked `AGENTS.md` block. Preserve unrelated project text and files byte-for-byte. Refuse malformed manifests, symlinks/reparse points, collisions, and changed managed files before mutations; retain rollback protection for ordinary I/O errors. Changes to installer behavior need lifecycle tests and an isolated install from a built artifact.
13
+
14
+ Before implementation, read the repository `AGENTS.md` and graph report/index as instructed there. From the repository root, relevant checks are `python -m pytest -q`, `python -m ruff check src tests`, `python -m mypy src/codex_core`, and `uv build --no-sources`; use the project's environment where those commands are installed. For package changes, inspect wheel contents and perform an isolated consumer install/update/status/delete from the built wheel. After code changes, run `graphify update .` from the workspace root as `AGENTS.md` directs. Update `CHANGELOG.md` without inventing a release date. Do not commit, tag, or publish as part of ordinary maintenance; those actions belong to the release workflow.
@@ -0,0 +1,26 @@
1
+ <!-- Type: GUIDE -->
2
+ [🏠 Home](../../index.md) | [🧭 Guide (EN)](../README.md) | [🚀 Getting Started](getting_started.md)
3
+
4
+ # Optional agent skill
5
+
6
+ Starting with `codex-core` 0.5.0, the Python package includes an offline usage skill for coding agents. Install the package in the Python environment you will use for the commands below, then run these commands from your consumer project root:
7
+
8
+ ```bash
9
+ python -m pip install "codex-core>=0.5.0"
10
+ python -m codex_core.agent_skills install --project .
11
+ python -m codex_core.agent_skills status --project .
12
+ ```
13
+
14
+ The installer creates `.agents/skills/codex-core/` with a version/hash manifest and adds a marked instruction to `AGENTS.md`. The copy and manifest are managed by the package. Keep application-specific rules in your own project skill or outside the marked block.
15
+
16
+ Updating the Python package and refreshing the installed skill are separate steps:
17
+
18
+ ```bash
19
+ python -m pip install --upgrade codex-core
20
+ python -m codex_core.agent_skills update --project .
21
+ python -m codex_core.agent_skills status --project .
22
+ ```
23
+
24
+ To remove only installer-owned content, run `python -m codex_core.agent_skills delete --project .`. `install` is safe to repeat when current, and `delete` succeeds when the skill is already absent. `status` exits with code 0 only when the skill is installed and current; it exits with code 1 when absent, outdated, or damaged. The other commands exit with code 1 on an error.
25
+
26
+ Update and delete refuse to discard changed managed files. Installation also refuses to take over an existing unowned destination or overwrite a colliding file. If a command reports local changes, save them in a project-owned skill, restore the managed files or marked block from a trusted backup of the installed version, and retry. Do not edit the manifest to make changes appear managed, or manually remove the destination to force an update. Unrelated `AGENTS.md` text and unowned files are preserved.
@@ -27,6 +27,10 @@ If you want to use the pre-configured Loguru setup, install the optional extra:
27
27
  pip install "codex-core[loguru]"
28
28
  ```
29
29
 
30
+ ### Optional agent skill
31
+
32
+ From version 0.5.0, install the offline coding-agent skill separately after installing the Python package. See [the agent skill guide](agent_skills.md) for installation, updates, removal, and recovery from local edits.
33
+
30
34
  ## First Steps
31
35
 
32
36
  ### 1. Create a BaseDTO
@@ -0,0 +1,26 @@
1
+ <!-- Type: GUIDE -->
2
+ [🏠 На главную](../../index.md) | [🧭 Руководство (RU)](../README.md) | [🚀 Начало работы](getting_started.md)
3
+
4
+ # Необязательный навык для агентов
5
+
6
+ Начиная с версии `codex-core` 0.5.0 пакет Python содержит автономный навык использования библиотеки для кодовых агентов. Установите пакет в среду Python, из которой будете запускать команды, затем выполните их из корня проекта-потребителя:
7
+
8
+ ```bash
9
+ python -m pip install "codex-core>=0.5.0"
10
+ python -m codex_core.agent_skills install --project .
11
+ python -m codex_core.agent_skills status --project .
12
+ ```
13
+
14
+ Установщик создаёт `.agents/skills/codex-core/` с манифестом версии и хешей и добавляет помеченную инструкцию в `AGENTS.md`. Копией навыка и манифестом управляет пакет. Правила вашего приложения храните в собственном навыке проекта или вне помеченного блока.
15
+
16
+ Обновление пакета Python и обновление установленного навыка — отдельные действия:
17
+
18
+ ```bash
19
+ python -m pip install --upgrade codex-core
20
+ python -m codex_core.agent_skills update --project .
21
+ python -m codex_core.agent_skills status --project .
22
+ ```
23
+
24
+ Чтобы удалить только файлы установщика, выполните `python -m codex_core.agent_skills delete --project .`. Повторный `install` допустим, если навык актуален; `delete` также успешно завершается, если навык уже отсутствует. `status` возвращает код 0 только для установленного и актуального навыка; если навык отсутствует, устарел или повреждён, возвращается код 1. Остальные команды возвращают код 1 при ошибке.
25
+
26
+ Команды `update` и `delete` откажутся удалять изменённые управляемые файлы. Установка также откажется занимать существующий неуправляемый каталог или перезаписывать конфликтующий файл. Если команда обнаружила локальные изменения, сохраните их в собственном навыке проекта, восстановите управляемые файлы или помеченный блок из надёжной резервной копии установленной версии и повторите команду. Не меняйте манифест ради принятия изменений и не удаляйте каталог вручную для принудительного обновления. Остальной текст `AGENTS.md` и неуправляемые файлы сохраняются.
@@ -27,6 +27,10 @@ pip install git+https://github.com/codexdlc/codex-core.git
27
27
  pip install "codex-core[loguru]"
28
28
  ```
29
29
 
30
+ ### Необязательный навык для агентов
31
+
32
+ Начиная с версии 0.5.0 после установки пакета Python можно отдельно установить автономный навык для кодовых агентов. Команды установки, обновления, удаления и восстановления после локальных изменений приведены в [руководстве по навыку](agent_skills.md).
33
+
30
34
  ## Первые шаги
31
35
 
32
36
  ### 1. Создание BaseDTO
@@ -62,6 +62,7 @@ nav:
62
62
  - Overview: en/README.md
63
63
  - Tasks:
64
64
  - Getting Started: en/tasks/getting_started.md
65
+ - Agent Skill: en/tasks/agent_skills.md
65
66
  - Architecture:
66
67
  - Overview: en/architecture/README.md
67
68
  - Core (PII & DTO): en/architecture/platform/core.md
@@ -72,6 +73,7 @@ nav:
72
73
  - Обзор: ru/README.md
73
74
  - Задачи (Tasks):
74
75
  - Начало работы: ru/tasks/getting_started.md
76
+ - Навык для агентов: ru/tasks/agent_skills.md
75
77
  - Архитектура:
76
78
  - Обзор: ru/architecture/README.md
77
79
  - Core (PII и DTO): ru/architecture/platform/core.md
@@ -21,7 +21,7 @@ classifiers = [
21
21
  ]
22
22
  dependencies = [
23
23
  "pydantic>=2.0,<3.0",
24
- "pydantic-settings>=2.0",
24
+ "pydantic-settings>=2.14.2",
25
25
  ]
26
26
 
27
27
  [project.urls]
@@ -34,7 +34,7 @@ Issues = "https://github.com/codexdlc/codex-core/issues"
34
34
  [project.optional-dependencies]
35
35
  loguru = ["loguru>=0.7.0"]
36
36
  dev = [
37
- "pytest>=8.0",
37
+ "pytest>=9.0.3",
38
38
  "pytest-asyncio>=0.23",
39
39
  "pytest-cov",
40
40
  "ruff>=0.4",
@@ -46,7 +46,7 @@ dev = [
46
46
  ]
47
47
  docs = [
48
48
  "mkdocs>=1.5",
49
- "mkdocs-material>=9.0",
49
+ "mkdocs-material>=9.7.7",
50
50
  "mkdocstrings[python]>=0.24",
51
51
  "mkdocs-include-markdown-plugin",
52
52
  "mike>=2.0",
@@ -58,6 +58,23 @@ source = "vcs"
58
58
  [tool.hatch.build.targets.wheel]
59
59
  packages = ["src/codex_core"]
60
60
 
61
+ [tool.hatch.build.targets.sdist]
62
+ include = [
63
+ "/src",
64
+ "/tests",
65
+ "/docs",
66
+ "/tools",
67
+ "/README.md",
68
+ "/CHANGELOG.md",
69
+ "/pyproject.toml",
70
+ "/uv.lock",
71
+ "/mkdocs.yml",
72
+ "/AGENTS.md",
73
+ "/.python-version",
74
+ "/.pre-commit-config.yaml",
75
+ "/.github",
76
+ ]
77
+
61
78
  [tool.ruff]
62
79
  target-version = "py312"
63
80
  line-length = 120
@@ -0,0 +1 @@
1
+ # empty module
@@ -0,0 +1,4 @@
1
+ from codex_core.agent_skills.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,39 @@
1
+ import argparse
2
+ import sys
3
+ from collections.abc import Sequence
4
+ from pathlib import Path
5
+
6
+ from codex_core.agent_skills.fs_transaction import FSError
7
+ from codex_core.agent_skills.orchestrator import OrchestratorError, check_status, perform_delete, perform_install
8
+
9
+
10
+ def main(args: Sequence[str] | None = None) -> None:
11
+ parser = argparse.ArgumentParser(prog="python -m codex_core.agent_skills")
12
+ parser.add_argument("action", choices=["install", "update", "delete", "status"])
13
+ parser.add_argument("--project", required=True, help="Path to project root")
14
+
15
+ parsed = parser.parse_args(args)
16
+
17
+ project_dir = Path(parsed.project).absolute()
18
+ if not project_dir.is_dir():
19
+ print(f"Error: Project directory does not exist or is not a directory: {project_dir}", file=sys.stderr)
20
+ sys.exit(1)
21
+
22
+ try:
23
+ if parsed.action == "install":
24
+ perform_install(project_dir, update=False)
25
+ print("Installed successfully.")
26
+ elif parsed.action == "update":
27
+ perform_install(project_dir, update=True)
28
+ print("Updated successfully.")
29
+ elif parsed.action == "delete":
30
+ perform_delete(project_dir)
31
+ print("Deleted successfully.")
32
+ elif parsed.action == "status":
33
+ is_valid, msg = check_status(project_dir)
34
+ print(msg)
35
+ if not is_valid:
36
+ sys.exit(1)
37
+ except (OrchestratorError, FSError) as e:
38
+ print(f"Error: {e}", file=sys.stderr)
39
+ sys.exit(1)
@@ -0,0 +1,174 @@
1
+ """Same-volume staged writes with best-effort rollback after ordinary I/O errors."""
2
+
3
+ import errno
4
+ import os
5
+ import shutil
6
+ import stat
7
+ import tempfile
8
+ from contextlib import suppress
9
+ from pathlib import Path
10
+
11
+
12
+ class FSError(Exception):
13
+ pass
14
+
15
+
16
+ def validate_no_symlinks(path: Path) -> Path:
17
+ """Inspect every existing component with lstat, including dangling links."""
18
+ path = path.absolute()
19
+ parts = list(reversed((path, *path.parents)))
20
+ for index, part in enumerate(parts):
21
+ try:
22
+ info = part.lstat()
23
+ except FileNotFoundError:
24
+ continue
25
+ except OSError as exc:
26
+ raise FSError(f"Cannot inspect {part}: {exc}") from exc
27
+ if stat.S_ISLNK(info.st_mode) or getattr(info, "st_file_attributes", 0) & getattr(
28
+ stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0
29
+ ):
30
+ raise FSError(f"Symlink or reparse point detected: {part}")
31
+ if index < len(parts) - 1 and not stat.S_ISDIR(info.st_mode):
32
+ raise FSError(f"Non-directory ancestor: {part}")
33
+ return path
34
+
35
+
36
+ def check_regular_file(path: Path) -> None:
37
+ validate_no_symlinks(path)
38
+ try:
39
+ info = path.lstat()
40
+ except FileNotFoundError:
41
+ return
42
+ except OSError as exc:
43
+ raise FSError(f"Cannot inspect {path}: {exc}") from exc
44
+ if not stat.S_ISREG(info.st_mode):
45
+ raise FSError(f"Not a regular file: {path}")
46
+
47
+
48
+ class FSTransaction:
49
+ def __init__(self, project_dir: Path):
50
+ self.project_dir = validate_no_symlinks(project_dir)
51
+ if not self.project_dir.is_dir():
52
+ raise FSError("Project is not a directory")
53
+ self.temp_dir = Path(tempfile.mkdtemp(prefix=".codex-core-tx-", dir=self.project_dir))
54
+ self.backup_dir = self.temp_dir / "backup"
55
+ self.stage_dir = self.temp_dir / "stage"
56
+ self.backup_dir.mkdir()
57
+ self.stage_dir.mkdir()
58
+ self.to_write: list[tuple[Path, bytes]] = []
59
+ self.to_delete: list[Path] = []
60
+ self.to_prune: list[Path] = []
61
+ self._created_dirs: list[Path] = []
62
+ self._removed_dirs: list[Path] = []
63
+ self._snapshots: dict[Path, Path | None] = {}
64
+ self._retain = False
65
+
66
+ def _target(self, target: Path) -> Path:
67
+ target = validate_no_symlinks(target)
68
+ if not target.is_relative_to(self.project_dir) or target.is_relative_to(self.temp_dir):
69
+ raise FSError("Target outside project or inside transaction directory")
70
+ check_regular_file(target)
71
+ return target
72
+
73
+ def stage_write(self, target: Path, content: bytes) -> None:
74
+ self.to_write.append((self._target(target), content))
75
+
76
+ def stage_delete(self, target: Path) -> None:
77
+ target = self._target(target)
78
+ if target.exists():
79
+ self.to_delete.append(target)
80
+
81
+ def stage_prune_empty(self, directory: Path) -> None:
82
+ directory = validate_no_symlinks(directory)
83
+ if not directory.is_relative_to(self.project_dir) or directory == self.project_dir:
84
+ raise FSError("Unsafe directory cleanup target")
85
+ self.to_prune.append(directory)
86
+
87
+ def _make_parent(self, parent: Path) -> None:
88
+ missing: list[Path] = []
89
+ current = parent
90
+ while not current.exists():
91
+ missing.append(current)
92
+ current = current.parent
93
+ for directory in reversed(missing):
94
+ directory.mkdir()
95
+ self._created_dirs.append(directory)
96
+
97
+ def apply(self) -> None:
98
+ targets = list(dict.fromkeys([*self.to_delete, *(p for p, _ in self.to_write)]))
99
+ if len(targets) != len(self.to_delete) + len(self.to_write):
100
+ raise FSError("A target appears in multiple operations")
101
+ try:
102
+ # Snapshot every existing destination before the first mutation.
103
+ for index, target in enumerate(targets):
104
+ self._target(target)
105
+ backup = None
106
+ if target.exists():
107
+ backup = self.backup_dir / f"backup_{index}"
108
+ shutil.copy2(target, backup)
109
+ self._snapshots[target] = backup
110
+ for index, (_, data) in enumerate(self.to_write):
111
+ (self.stage_dir / f"write_stg_{index}").write_bytes(data)
112
+ for target in self.to_delete:
113
+ target.unlink()
114
+ for index, (target, _) in enumerate(self.to_write):
115
+ self._make_parent(target.parent)
116
+ os.replace(self.stage_dir / f"write_stg_{index}", target)
117
+ for directory in sorted(set(self.to_prune), key=lambda p: len(p.parts), reverse=True):
118
+ validate_no_symlinks(directory)
119
+ try:
120
+ directory.rmdir()
121
+ self._removed_dirs.append(directory)
122
+ except FileNotFoundError:
123
+ pass
124
+ except OSError as exc:
125
+ if exc.errno not in {errno.ENOTEMPTY, errno.EEXIST} and getattr(exc, "winerror", None) != 145:
126
+ raise
127
+ except Exception as exc:
128
+ try:
129
+ self.rollback()
130
+ except FSError as recovery:
131
+ self._retain = True
132
+ raise FSError(
133
+ f"Transaction failed: {exc}. Recovery failed: {recovery}. Backups retained at {self.temp_dir}"
134
+ ) from exc
135
+ raise FSError(f"Transaction failed and was rolled back: {exc}") from exc
136
+
137
+ def rollback(self) -> None:
138
+ errors: list[str] = []
139
+ for directory in reversed(self._removed_dirs):
140
+ try:
141
+ directory.mkdir(exist_ok=True)
142
+ except Exception as exc:
143
+ errors.append(f"{directory}: {exc}")
144
+ for target, backup in reversed(list(self._snapshots.items())):
145
+ try:
146
+ if backup is None:
147
+ if target.exists():
148
+ target.unlink()
149
+ else:
150
+ self._make_parent(target.parent)
151
+ # Copy leaves the sole recovery copy intact if restoration fails.
152
+ shutil.copy2(backup, target)
153
+ except Exception as exc:
154
+ errors.append(f"{target}: {exc}")
155
+ for directory in reversed(self._created_dirs):
156
+ with suppress(OSError):
157
+ directory.rmdir()
158
+ if errors:
159
+ raise FSError("; ".join(errors))
160
+
161
+ def cleanup(self) -> None:
162
+ if self._retain:
163
+ return
164
+ root = self.temp_dir.absolute()
165
+ if root.parent != self.project_dir or not root.name.startswith(".codex-core-tx-"):
166
+ raise FSError("Unsafe transaction cleanup path")
167
+ validate_no_symlinks(root)
168
+ shutil.rmtree(root)
169
+
170
+ def __enter__(self) -> "FSTransaction":
171
+ return self
172
+
173
+ def __exit__(self, _type: object, _value: object, _traceback: object) -> None:
174
+ self.cleanup()
@@ -0,0 +1,153 @@
1
+ """Validation of the installer-owned manifest and AGENTS.md block."""
2
+
3
+ import hashlib
4
+ import json
5
+ import re
6
+ from typing import Any
7
+
8
+ MARKER_START = b"<!-- codex-core:skill:start -->"
9
+ MARKER_END = b"<!-- codex-core:skill:end -->"
10
+ MANIFEST_NAME = ".manifest.json"
11
+ SKILL_ID = "codex-core"
12
+ _INSTRUCTION = b"Use [codex-core](.agents/skills/codex-core/SKILL.md) for codex_core work."
13
+ _HASH = re.compile(r"[0-9a-f]{64}\Z")
14
+ _INVALID_WINDOWS = set('<>:"\\|?*')
15
+
16
+
17
+ class ManifestError(Exception):
18
+ pass
19
+
20
+
21
+ def hash_content(content: bytes) -> str:
22
+ return hashlib.sha256(content).hexdigest()
23
+
24
+
25
+ def validate_normalized_posix_path(path_str: str) -> None:
26
+ if not isinstance(path_str, str) or not path_str or path_str.startswith("/"):
27
+ raise ManifestError(f"Invalid relative path: {path_str!r}")
28
+ for segment in path_str.split("/"):
29
+ if (
30
+ not segment
31
+ or segment in {".", ".."}
32
+ or segment[-1] in ". "
33
+ or any(c in _INVALID_WINDOWS or ord(c) < 32 for c in segment)
34
+ ):
35
+ raise ManifestError(f"Invalid path segment: {path_str!r}")
36
+ device = segment.split(".", 1)[0].upper()
37
+ if device in {"CON", "PRN", "AUX", "NUL"} or re.fullmatch(r"(?:COM|LPT)[1-9]", device):
38
+ raise ManifestError(f"Windows device path: {path_str!r}")
39
+
40
+
41
+ def _validate_file_names(names: set[str]) -> None:
42
+ if "SKILL.md" not in names:
43
+ raise ManifestError("SKILL.md missing")
44
+ aliases: set[str] = set()
45
+ for name in names:
46
+ validate_normalized_posix_path(name)
47
+ if name.rsplit("/", 1)[-1].casefold() in {MANIFEST_NAME.casefold(), "agents.md"}:
48
+ raise ManifestError(f"Reserved owned path: {name}")
49
+ alias = name.casefold()
50
+ if alias in aliases:
51
+ raise ManifestError(f"Case alias: {name}")
52
+ aliases.add(alias)
53
+ for alias in aliases:
54
+ parts = alias.split("/")
55
+ if any("/".join(parts[:i]) in aliases for i in range(1, len(parts))):
56
+ raise ManifestError(f"File and parent conflict: {alias}")
57
+
58
+
59
+ def _unique_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
60
+ result: dict[str, Any] = {}
61
+ for key, value in pairs:
62
+ if key in result:
63
+ raise ManifestError(f"Duplicate JSON key: {key}")
64
+ result[key] = value
65
+ return result
66
+
67
+
68
+ def parse_manifest(content: bytes) -> dict[str, Any]:
69
+ try:
70
+ data = json.loads(content.decode("utf-8"), object_pairs_hook=_unique_pairs)
71
+ except (UnicodeError, ValueError, TypeError) as exc:
72
+ raise ManifestError("Malformed manifest JSON") from exc
73
+ if not isinstance(data, dict) or set(data) != {"version", "skill", "package_version", "files", "agents_created"}:
74
+ raise ManifestError("Invalid manifest schema")
75
+ if (
76
+ data["version"] != "1.0"
77
+ or data["skill"] != SKILL_ID
78
+ or not isinstance(data["package_version"], str)
79
+ or not data["package_version"]
80
+ ):
81
+ raise ManifestError("Invalid manifest identity")
82
+ if type(data["agents_created"]) is not bool or not isinstance(data["files"], dict):
83
+ raise ManifestError("Invalid manifest fields")
84
+ files = data["files"]
85
+ _validate_file_names(set(files))
86
+ for name, digest in files.items():
87
+ if not isinstance(digest, str) or not _HASH.fullmatch(digest):
88
+ raise ManifestError(f"Invalid SHA256 hash for {name}")
89
+ return data
90
+
91
+
92
+ def build_manifest(package_version: str, files: dict[str, bytes], agents_created: bool = False) -> bytes:
93
+ _validate_file_names(set(files))
94
+ if not package_version:
95
+ raise ManifestError("Missing package version")
96
+ data = {
97
+ "version": "1.0",
98
+ "skill": SKILL_ID,
99
+ "package_version": package_version,
100
+ "files": {name: hash_content(value) for name, value in sorted(files.items())},
101
+ "agents_created": agents_created,
102
+ }
103
+ return json.dumps(data, indent=2, ensure_ascii=False).encode("utf-8")
104
+
105
+
106
+ def _block_bounds(content: bytes) -> tuple[int, int, bool] | None:
107
+ body = content[3:] if content.startswith(b"\xef\xbb\xbf") else content
108
+ if body.count(MARKER_START) != body.count(MARKER_END) or body.count(MARKER_START) > 1:
109
+ raise ManifestError("Missing or duplicate AGENTS.md marker")
110
+ if MARKER_START not in body:
111
+ return None
112
+ start = body.index(MARKER_START)
113
+ end = body.index(MARKER_END, start) if MARKER_END in body[start:] else -1
114
+ if end < 0 or (start and body[start - 1 : start] != b"\n"):
115
+ raise ManifestError("Malformed AGENTS.md block")
116
+ newline = b"\r\n" if body[start + len(MARKER_START) :].startswith(b"\r\n") else b"\n"
117
+ for separator in (True, False):
118
+ metadata = b"<!-- codex-core:skill:separator=" + (b"1" if separator else b"0") + b" -->"
119
+ expected = newline.join((MARKER_START, metadata, _INSTRUCTION, MARKER_END))
120
+ if body[start : start + len(expected)] == expected:
121
+ finish = start + len(expected)
122
+ if body[finish : finish + len(newline)] == newline:
123
+ finish += len(newline)
124
+ elif finish != len(body):
125
+ raise ManifestError("Malformed AGENTS.md block ending")
126
+ if separator and (start == 0 or body[start - len(newline) : start] != newline):
127
+ raise ManifestError("Malformed AGENTS.md separator")
128
+ offset = 3 if content.startswith(b"\xef\xbb\xbf") else 0
129
+ return start + offset, finish + offset, separator
130
+ raise ManifestError("AGENTS.md instruction modified or corrupt")
131
+
132
+
133
+ def has_valid_agents_block(content: bytes) -> bool:
134
+ return _block_bounds(content) is not None
135
+
136
+
137
+ def update_agents_md_bytes(content: bytes, remove: bool = False) -> bytes:
138
+ bounds = _block_bounds(content)
139
+ if bounds is not None:
140
+ start, end, separator = bounds
141
+ if remove:
142
+ if separator:
143
+ start -= 2 if content[start - 2 : start] == b"\r\n" else 1
144
+ return content[:start] + content[end:]
145
+ return content
146
+ if remove:
147
+ return content
148
+ body = content[3:] if content.startswith(b"\xef\xbb\xbf") else content
149
+ newline = b"\r\n" if b"\r\n" in body else b"\n"
150
+ separator = bool(body and not body.endswith(b"\n"))
151
+ metadata = b"<!-- codex-core:skill:separator=" + (b"1" if separator else b"0") + b" -->"
152
+ block = newline.join((MARKER_START, metadata, _INSTRUCTION, MARKER_END)) + newline
153
+ return content + (newline if separator else b"") + block