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.
- {codex_core-0.3.0 → codex_core-0.5.0}/.gitignore +3 -0
- codex_core-0.5.0/AGENTS.md +11 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/CHANGELOG.md +23 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/PKG-INFO +17 -5
- {codex_core-0.3.0 → codex_core-0.5.0}/README.md +12 -0
- codex_core-0.5.0/docs/agent-skills/codex-core-maintenance/SKILL.md +14 -0
- codex_core-0.5.0/docs/en/tasks/agent_skills.md +26 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/tasks/getting_started.md +4 -0
- codex_core-0.5.0/docs/ru/tasks/agent_skills.md +26 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/tasks/getting_started.md +4 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/mkdocs.yml +2 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/pyproject.toml +20 -3
- codex_core-0.5.0/src/codex_core/agent_skills/__init__.py +1 -0
- codex_core-0.5.0/src/codex_core/agent_skills/__main__.py +4 -0
- codex_core-0.5.0/src/codex_core/agent_skills/cli.py +39 -0
- codex_core-0.5.0/src/codex_core/agent_skills/fs_transaction.py +174 -0
- codex_core-0.5.0/src/codex_core/agent_skills/manifest_rules.py +153 -0
- codex_core-0.5.0/src/codex_core/agent_skills/orchestrator.py +227 -0
- codex_core-0.5.0/src/codex_core/agent_skills/resources/SKILL.md +18 -0
- codex_core-0.5.0/src/codex_core/agent_skills/resources/__init__.py +1 -0
- codex_core-0.5.0/src/codex_core/agent_skills/resources/references/common-logging.md +17 -0
- codex_core-0.5.0/src/codex_core/agent_skills/resources/references/developer-tools.md +13 -0
- codex_core-0.5.0/src/codex_core/agent_skills/resources/references/dto-pii.md +17 -0
- codex_core-0.5.0/src/codex_core/agent_skills/resources/references/integration.md +14 -0
- codex_core-0.5.0/src/codex_core/agent_skills/resources/references/settings.md +16 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/common/__init__.py +4 -1
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/common/log_context.py +44 -12
- codex_core-0.5.0/src/codex_core/common/loguru_setup.py +307 -0
- codex_core-0.5.0/tests/unit/agent_skills/__init__.py +1 -0
- codex_core-0.5.0/tests/unit/agent_skills/test_installer.py +592 -0
- codex_core-0.5.0/tests/unit/common/test_log_context.py +98 -0
- codex_core-0.5.0/tests/unit/common/test_loguru_setup.py +221 -0
- codex_core-0.5.0/tools/dev/__init__.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/uv.lock +104 -71
- codex_core-0.3.0/project_structure.txt +0 -97
- codex_core-0.3.0/src/codex_core/common/loguru_setup.py +0 -343
- codex_core-0.3.0/tests/unit/common/test_log_context.py +0 -53
- codex_core-0.3.0/tests/unit/common/test_loguru_setup.py +0 -140
- codex_core-0.3.0/tests/unit/dev/__init__.py +0 -1
- {codex_core-0.3.0 → codex_core-0.5.0}/.github/workflows/ci.yml +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/.github/workflows/docs.yml +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/.github/workflows/publish.yml +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/.pre-commit-config.yaml +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/.python-version +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/changelog.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/README.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/common.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/core.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/dev/check_runner.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/dev/index.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/dev/project_tree.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/dev/static_compiler.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/index.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/api/settings.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/architecture/README.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/architecture/platform/common.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/architecture/platform/core.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/architecture/platform/dev.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/en/architecture/platform/settings.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/evolution/roadmap.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/index.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/planning/python_version_policy.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/README.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/architecture/README.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/architecture/platform/common.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/architecture/platform/core.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/architecture/platform/dev.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/ru/architecture/platform/settings.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/docs/stylesheets/extra.css +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/__init__.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/common/phone.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/common/text.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/core/__init__.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/core/base_dto.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/core/exceptions.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/core/pii.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/__init__.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/check_runner.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/project_tree.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/static_compiler/__init__.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/static_compiler/compiler.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/static_compiler/css.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/dev/static_compiler/js.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/py.typed +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/settings/__init__.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/src/codex_core/settings/base.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/conftest.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/integration/__init__.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/integration/conftest.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/integration/test_settings_integration.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/__init__.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/common/__init__.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/common/test_phone.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/common/test_text.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/conftest.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/core/__init__.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/core/test_exceptions.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/core/test_pii.py +0 -0
- {codex_core-0.3.0/tools → codex_core-0.5.0/tests/unit/dev}/__init__.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/dev/test_check_runner.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/dev/test_static_compiler.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/settings/__init__.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tests/unit/settings/test_settings.py +0 -0
- {codex_core-0.3.0/tools/dev → codex_core-0.5.0/tools}/__init__.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tools/dev/README.md +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tools/dev/check.py +0 -0
- {codex_core-0.3.0 → codex_core-0.5.0}/tools/dev/generate_project_tree.py +0 -0
|
@@ -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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: codex-core
|
|
3
|
-
Version: 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.
|
|
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>=
|
|
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.
|
|
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.
|
|
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>=
|
|
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.
|
|
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,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
|