dotagents-cli 0.7.0__tar.gz → 0.7.2__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.
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/CHANGELOG.md +26 -2
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/PKG-INFO +3 -2
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/README.md +2 -1
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/guide/commands.md +5 -1
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/guide/install.md +11 -1
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/guide/overlays.md +11 -2
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/AGENTS.md +18 -3
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/__init__.py +1 -1
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_context.py +47 -3
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/templates/AGENTS.md +6 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_sources.py +60 -6
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/_common.py +4 -1
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_cmds_discovery.py +2 -2
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_context.py +1 -1
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_env_regressions.py +6 -6
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_env_safety.py +1 -2
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_findings_cmd.py +4 -4
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_adapters.py +1 -1
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_cli.py +1 -1
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_env.py +1 -2
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_hooks.py +1 -1
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_init.py +1 -1
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_overlays.py +2 -2
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_scope_context.py +1 -2
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_skills_adapters.py +2 -2
- dotagents_cli-0.7.2/tests/test_git_ssl_revoke.py +102 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_last_mile.py +3 -3
- dotagents_cli-0.7.2/tests/test_local_additions.py +86 -0
- dotagents_cli-0.7.2/tests/test_no_internal_labels.py +231 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_overlays_cli.py +5 -5
- dotagents_cli-0.7.0/tests/test_review_security.py → dotagents_cli-0.7.2/tests/test_quoting_injection_safety.py +3 -3
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/.gitignore +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/LICENSE +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/agents.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/cli.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/commands.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/context.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/env.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/fs.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/hooks.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/merge.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/overlays.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/reference.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/resources.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/scope.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/skills.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/sources.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/api/wrappers.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/changelog.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/guide/authoring.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/guide/private-sync.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/docs/index.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/install.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/mkdocs.yml +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/pyproject.toml +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/__main__.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_agents.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_env.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_fs.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_hooks.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_ignore.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_merge.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/cmds/findings.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/cmds/launch.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/hooks/preinvocation_antigravity_context.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/hooks/pretooluse_codex_env.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/hooks/sessionstart_codex_context.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/templates/PROJECT.md +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlays.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_resources.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_scope.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_skills.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_wrappers.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/__init__.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/about.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/build_pyz.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/context.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/env.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/init.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/overlays.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/cli/path.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/py.typed +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/_helpers.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/_shell.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/conftest.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_about.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_agents.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_api_header.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_audit_leak.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_build_pyz.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_cli_main.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_cloud_setup.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_env.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_env_cli_scope.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_env_format.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_env_into.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_fix_packaging.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_hooks.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_ignore.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_init_base.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_launch_cmd.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_mcp.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_overlay_libs.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_overlays_command.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_path_timeout_frontmatter.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_powershell_env_hook.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_scope.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_sources.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_sources_uri.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_wire_hooks.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tests/test_wrappers.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tools/audit.py +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tools/cloud-setup.sh +0 -0
- {dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/tools/pyz_smoke.sh +0 -0
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.2] - 2026-10-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `git_ssl_revoke` in `<store>/dotagents/config.toml` sets how the git clones and
|
|
15
|
+
fetches of overlay repos check certificate revocation: `best-effort` (the default)
|
|
16
|
+
skips a revocation list that is missing or unreachable, `false` skips the check, as
|
|
17
|
+
git on Linux does, `true` enforces it. It is passed to every git call, so a stricter
|
|
18
|
+
git configuration elsewhere no longer fails an https clone on Windows, and a clone
|
|
19
|
+
that still fails on revocation says which setting to change. A git too old to know
|
|
20
|
+
`best-effort` (it rejects the value) runs the call again without the setting.
|
|
21
|
+
|
|
22
|
+
## [0.7.1] - 2026-10-01
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Your own additions to an installed overlay's file live at the same path under the
|
|
27
|
+
store root (`~/.agents/kb/RUST.md` for the rust overlay's `kb/RUST.md`, or
|
|
28
|
+
`<project>/.agents/kb/RUST.md`), where `overlays sync` never touches them. The
|
|
29
|
+
base `AGENTS.md` tells agents to read them alongside the overlay's file, and
|
|
30
|
+
`dotagents context` lists the ones it finds (JSON: `local_additions`).
|
|
31
|
+
|
|
10
32
|
## [0.7.0] - 2026-10-01
|
|
11
33
|
|
|
12
34
|
### Changed
|
|
@@ -1347,7 +1369,7 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
|
|
|
1347
1369
|
|
|
1348
1370
|
### Changed
|
|
1349
1371
|
|
|
1350
|
-
- Migrate the CLI to `duho>=0.3.3` (was `>=0.1.1`). duho's
|
|
1372
|
+
- Migrate the CLI to `duho>=0.3.3` (was `>=0.1.1`). duho's `Args`/`Cmd`
|
|
1351
1373
|
split means commands are now `class X(LoggingArgs, Cmd)` with a `__call__` entrypoint
|
|
1352
1374
|
(was a bare `LoggingArgs` with `__run__`) and the umbrella root is
|
|
1353
1375
|
`class Dotagents(LoggingArgs, Cli)`. Field declarations (annotation + help string +
|
|
@@ -1362,7 +1384,9 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
|
|
|
1362
1384
|
affected module sources (`dotagents.cli`, `duho.presets`) to extracted temp files
|
|
1363
1385
|
before dispatch; a no-op for a plain install.
|
|
1364
1386
|
|
|
1365
|
-
[Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.7.
|
|
1387
|
+
[Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.7.2...HEAD
|
|
1388
|
+
[0.7.2]: https://github.com/jose-pr/dotagents/compare/v0.7.1...v0.7.2
|
|
1389
|
+
[0.7.1]: https://github.com/jose-pr/dotagents/compare/v0.7.0...v0.7.1
|
|
1366
1390
|
[0.7.0]: https://github.com/jose-pr/dotagents/compare/v0.6.3...v0.7.0
|
|
1367
1391
|
[0.6.3]: https://github.com/jose-pr/dotagents/compare/v0.6.2...v0.6.3
|
|
1368
1392
|
[0.6.2]: https://github.com/jose-pr/dotagents/compare/v0.6.1...v0.6.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: dotagents-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: The dotagents CLI: install and manage a portable ~/.agents config for AI coding agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/jose-pr/dotagents/
|
|
6
6
|
Project-URL: Repository, https://github.com/jose-pr/dotagents
|
|
@@ -270,7 +270,8 @@ config miss is a **finding** — `dotagents findings add -g` for the user store
|
|
|
270
270
|
triage closes each with its resolution, which is the record. How you keep any
|
|
271
271
|
decisions beyond that is up to you. A store's `dotagents/` directory exists only for
|
|
272
272
|
your own command modules (`dotagents/cmds/`), which you create when you add your first
|
|
273
|
-
one, and for `dotagents/config.toml`, which records an `init --from` base
|
|
273
|
+
one, and for `dotagents/config.toml`, which records an `init --from` base and,
|
|
274
|
+
if you set it, `git_ssl_revoke` (how git checks certificate revocation on Windows). This
|
|
274
275
|
repo follows the same rule: its working material lives in an **untracked** `.agents/`,
|
|
275
276
|
never committed — so what's public here is only the CLI, the base overlay, and the
|
|
276
277
|
opt-in overlays. If you fork, keep the tracked surface free of personal paths and
|
|
@@ -220,7 +220,8 @@ config miss is a **finding** — `dotagents findings add -g` for the user store
|
|
|
220
220
|
triage closes each with its resolution, which is the record. How you keep any
|
|
221
221
|
decisions beyond that is up to you. A store's `dotagents/` directory exists only for
|
|
222
222
|
your own command modules (`dotagents/cmds/`), which you create when you add your first
|
|
223
|
-
one, and for `dotagents/config.toml`, which records an `init --from` base
|
|
223
|
+
one, and for `dotagents/config.toml`, which records an `init --from` base and,
|
|
224
|
+
if you set it, `git_ssl_revoke` (how git checks certificate revocation on Windows). This
|
|
224
225
|
repo follows the same rule: its working material lives in an **untracked** `.agents/`,
|
|
225
226
|
never committed — so what's public here is only the CLI, the base overlay, and the
|
|
226
227
|
opt-in overlays. If you fork, keep the tracked surface free of personal paths and
|
|
@@ -216,7 +216,11 @@ Assembles the effective context an agent should load — the overlay `CONTEXT.md
|
|
|
216
216
|
and the project root's own `AGENTS.md` / `AGENTS.local.md` — minus whatever the harness already loads by itself (for Claude Code, whatever its
|
|
217
217
|
`CLAUDE.md` files really `@`-include) — and prints it to **stdout** by default
|
|
218
218
|
(POSIX convention); pass a path to write a file, or `--write-agent` to merge it into
|
|
219
|
-
each agent's own instruction file.
|
|
219
|
+
each agent's own instruction file. It ends with the skills on offer and, when there are
|
|
220
|
+
any, your **local additions to overlay files** — a store file at the same path as an
|
|
221
|
+
overlay's (`~/.agents/kb/RUST.md` for the rust overlay's `kb/RUST.md`; see
|
|
222
|
+
[Overlays](overlays.md)); `--format json` carries both as their own keys (`skills`,
|
|
223
|
+
`local_additions`).
|
|
220
224
|
|
|
221
225
|
```bash
|
|
222
226
|
dotagents context # print the active agent's context to stdout
|
|
@@ -101,7 +101,17 @@ python -m dotagents build-pyz --out dist/dotagents.pyz # or build it (needs a
|
|
|
101
101
|
[Authoring → Custom commands](authoring.md#custom-commands)).
|
|
102
102
|
- `dotagents/config.toml` — only after `init --from`: records that base, so a later
|
|
103
103
|
`init` and every `overlays add` / `remove` / `sync` compose over it. Delete its
|
|
104
|
-
`base` line to return to the bundled base.
|
|
104
|
+
`base` line to return to the bundled base. You may add one setting by hand:
|
|
105
|
+
`git_ssl_revoke` — how strictly the git clones of overlay repos check that the
|
|
106
|
+
server's certificate is not revoked. `best-effort` (the default) skips a
|
|
107
|
+
revocation list that is missing or unreachable; `false` skips the check, as git
|
|
108
|
+
on Linux always does; `true` enforces it. It matters on Windows, where git
|
|
109
|
+
checks revocation and an https clone otherwise fails when the list cannot be
|
|
110
|
+
fetched; a project store's setting wins over the user store's.
|
|
111
|
+
|
|
112
|
+
```toml
|
|
113
|
+
git_ssl_revoke = "false"
|
|
114
|
+
```
|
|
105
115
|
|
|
106
116
|
## What runs, and from where
|
|
107
117
|
|
|
@@ -79,8 +79,17 @@ up under `<store>/install_backup/` first. What the `.gitignore` / `.ignore` at t
|
|
|
79
79
|
overlay's root match (an ignore file in a subdirectory is just a file it ships), and
|
|
80
80
|
tool caches, belong to the install (a setup script's output, local
|
|
81
81
|
state) and are left alone; `--prune` clears them too. A `requires` added upstream is
|
|
82
|
-
installed. An installed overlay is upstream's copy, not a place for your edits
|
|
83
|
-
|
|
82
|
+
installed. An installed overlay is upstream's copy, not a place for your edits.
|
|
83
|
+
|
|
84
|
+
### Your additions to an overlay's files
|
|
85
|
+
|
|
86
|
+
Put your own text for an overlay file at the **same path under the store root**: an
|
|
87
|
+
addition to the rust overlay's `kb/RUST.md` goes in `~/.agents/kb/RUST.md` (or
|
|
88
|
+
`<project>/.agents/kb/RUST.md` for one project). `sync` never touches it, and agents
|
|
89
|
+
read it alongside the overlay's file, winning on conflict: the base `AGENTS.md` tells
|
|
90
|
+
them to, and `dotagents context` lists every addition it finds (for each `.md` an
|
|
91
|
+
overlay ships in a subdirectory other than `skills/`). Private names, local tool paths
|
|
92
|
+
and links into your own notes belong there, never in the overlay.
|
|
84
93
|
|
|
85
94
|
Removing an overlay deletes only its directory and unpublishes only the skills **it**
|
|
86
95
|
published (a copy the user edited is left alone). Its lines in `AGENTS.md`'s managed
|
|
@@ -306,7 +306,15 @@ a directory of overlays or a registry file (default: the root); for a **source**
|
|
|
306
306
|
- `redact(text) -> str` — URL userinfo removed, query values masked.
|
|
307
307
|
- `uri_path_class()` — `pathlib_next.uri.UriPath`, or `None` without the `uri` extra.
|
|
308
308
|
- `url_scheme(location) -> str` — the lower-cased scheme.
|
|
309
|
-
- `
|
|
309
|
+
- `GIT_SSL_REVOKE_KEY = "git_ssl_revoke"`, `GIT_SSL_REVOKE_VALUES = ("best-effort",
|
|
310
|
+
"true", "false")`, `GIT_SSL_REVOKE_DEFAULT = "best-effort"`;
|
|
311
|
+
`git_ssl_revoke(*stores) -> str` — the first store config (`config.toml`) that sets
|
|
312
|
+
the key, project store first; another value raises `SourceError` naming the file.
|
|
313
|
+
- `class SourceCache(root, logger=None, ssl_revoke="best-effort")` — every git call
|
|
314
|
+
runs as `git -c http.schannelCheckRevoke=<ssl_revoke> ...` (Git for Windows'
|
|
315
|
+
Schannel checks revocation; git with OpenSSL ignores it), and a failure that looks
|
|
316
|
+
like a revocation check names the key. A git that rejects the value as a bad
|
|
317
|
+
boolean (older than `best-effort`) is asked again without it. `repo_dir(location, ref=None) -> Path`
|
|
310
318
|
(`<root>/<slug>-<hash>`); `checkout(spec) -> Path` (clone or fetch, once per
|
|
311
319
|
process; a failed fetch of an existing checkout is a warning);
|
|
312
320
|
`materialize(spec) -> Path` (a `url` spec: `file://` in place, anything else
|
|
@@ -433,7 +441,8 @@ API:
|
|
|
433
441
|
`Overlay.sort_key`, then contract-A `AGENTS.md` (every store and the project root)
|
|
434
442
|
and `AGENTS.local.md` (project store and project root), each prefixed
|
|
435
443
|
`<!-- Source: <path> -->`. Then a skills listing (`- **name** (`<SKILL.md>`):
|
|
436
|
-
description`, from each skill's leading frontmatter; a project skill wins)
|
|
444
|
+
description`, from each skill's leading frontmatter; a project skill wins), then
|
|
445
|
+
a "Local additions to overlay files" section when `local_additions` finds any.
|
|
437
446
|
`<PROJECT_ROOT>` and `<NAME_OVERLAY_ROOT>` placeholders expand; with
|
|
438
447
|
`expand_vars` so do `$NAME_OVERLAY_ROOT` / `${NAME_OVERLAY_ROOT}`. `inline`
|
|
439
448
|
appends the on-demand `.md` files the sources reference, each resolved against
|
|
@@ -441,7 +450,13 @@ API:
|
|
|
441
450
|
system source never into the project).
|
|
442
451
|
- `assemble_context_data(agent, scope, *, inline=False, expand_vars=True) -> dict`
|
|
443
452
|
— `{"agent", "harness", "sources", "context", "skills": [{"name",
|
|
444
|
-
"description", "path"}]
|
|
453
|
+
"description", "path"}], "local_additions": [{"overlay_file", "local": [...]}]}`;
|
|
454
|
+
`context` without the skills and local-additions listings.
|
|
455
|
+
- `local_additions(scope) -> list[tuple[Path, list[Path]]]` — the user's own
|
|
456
|
+
additions to installed overlay files: for each `.md` an overlay in `scope.overlays`
|
|
457
|
+
ships in a subdirectory other than `skills/`, every store's file at the same
|
|
458
|
+
relative path (`<store>/kb/RUST.md` for the overlay's `kb/RUST.md`), in
|
|
459
|
+
`scope.stores` order. A store's root files never match.
|
|
445
460
|
|
|
446
461
|
## `dotagents._agents`
|
|
447
462
|
|
|
@@ -309,6 +309,44 @@ def _get_skills_listing(scope: _scope.Scope) -> str:
|
|
|
309
309
|
return "\n\n## Available Skills (Opt-in)\n" + "\n".join(lines) + "\n"
|
|
310
310
|
|
|
311
311
|
|
|
312
|
+
def local_additions(scope: _scope.Scope) -> "list[tuple[Path, list[Path]]]":
|
|
313
|
+
"""The user's own additions to installed overlay files: for each ``.md``
|
|
314
|
+
an overlay ships in a subdirectory (``kb/``, ``flows/``, ``rules/`` ... --
|
|
315
|
+
not ``skills/``, whose published copies sit at the same path in a store),
|
|
316
|
+
every store's file at the same relative path (``~/.agents/kb/RUST.md`` for
|
|
317
|
+
an overlay's ``kb/RUST.md``), in store order. An overlay file is upstream's
|
|
318
|
+
copy, replaced by ``overlays sync``; the addition is where the user's own
|
|
319
|
+
text lives. A store's root files (``AGENTS.md``, ``env.py``) never match:
|
|
320
|
+
only a file in a subdirectory counts."""
|
|
321
|
+
stores = list(scope.stores)
|
|
322
|
+
out: "list[tuple[Path, list[Path]]]" = []
|
|
323
|
+
for overlay in scope.overlays:
|
|
324
|
+
for path in overlay.files():
|
|
325
|
+
rel = path.relative_to(overlay.path)
|
|
326
|
+
if path.suffix.lower() != ".md" or len(rel.parts) < 2 or rel.parts[0] == "skills":
|
|
327
|
+
continue
|
|
328
|
+
found = [store / rel for store in stores if (store / rel).is_file()]
|
|
329
|
+
if found:
|
|
330
|
+
out.append((path, found))
|
|
331
|
+
return out
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _get_local_additions_listing(scope: _scope.Scope) -> str:
|
|
335
|
+
"""The local additions (:func:`local_additions`) as a markdown section, or ''."""
|
|
336
|
+
additions = local_additions(scope)
|
|
337
|
+
if not additions:
|
|
338
|
+
return ""
|
|
339
|
+
lines = [
|
|
340
|
+
"- `%s` -- also read %s" % (path, ", ".join("`%s`" % p for p in found))
|
|
341
|
+
for path, found in additions
|
|
342
|
+
]
|
|
343
|
+
return (
|
|
344
|
+
"\n\n## Local additions to overlay files\n"
|
|
345
|
+
"The user's own additions: read each with the overlay file it extends; it wins on conflict.\n"
|
|
346
|
+
+ "\n".join(lines) + "\n"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
312
350
|
def _resolve_and_filter_sources(
|
|
313
351
|
agent: _agents.Agent, scope: _scope.Scope
|
|
314
352
|
) -> "tuple[list[tuple[str, Path, Path | None]], list[Path]]":
|
|
@@ -424,6 +462,7 @@ def assemble_context(
|
|
|
424
462
|
if not source_paths:
|
|
425
463
|
return ""
|
|
426
464
|
text += _get_skills_listing(scope)
|
|
465
|
+
text += _get_local_additions_listing(scope)
|
|
427
466
|
return text
|
|
428
467
|
|
|
429
468
|
|
|
@@ -440,11 +479,12 @@ def assemble_context_data(
|
|
|
440
479
|
"sources": [<absolute source path>, ...], # after harness subtraction
|
|
441
480
|
"context": <assembled markdown text, minus the skills listing>,
|
|
442
481
|
"skills": [{"name": ..., "description": ..., "path": <SKILL.md>}, ...],
|
|
482
|
+
"local_additions": [{"overlay_file": ..., "local": [<path>, ...]}, ...],
|
|
443
483
|
}
|
|
444
484
|
``context`` is the same assembled(+inlined, with ``inline=True``) text the
|
|
445
|
-
markdown format emits, but WITHOUT the skills
|
|
446
|
-
their own structured
|
|
447
|
-
|
|
485
|
+
markdown format emits, but WITHOUT the skills and local-additions listings
|
|
486
|
+
appended -- both are their own structured fields so a consumer can render
|
|
487
|
+
them separately."""
|
|
448
488
|
text, source_paths = _assemble(agent, scope, inline, expand_vars)
|
|
449
489
|
|
|
450
490
|
skills = [
|
|
@@ -457,4 +497,8 @@ def assemble_context_data(
|
|
|
457
497
|
"sources": source_paths,
|
|
458
498
|
"context": text,
|
|
459
499
|
"skills": skills,
|
|
500
|
+
"local_additions": [
|
|
501
|
+
{"overlay_file": str(path), "local": [str(p) for p in found]}
|
|
502
|
+
for path, found in local_additions(scope)
|
|
503
|
+
],
|
|
460
504
|
}
|
{dotagents_cli-0.7.0 → dotagents_cli-0.7.2}/src/dotagents/_overlay/dotagents/templates/AGENTS.md
RENAMED
|
@@ -16,6 +16,12 @@ Startup: annotate that you read `{{AGENTS_MD}}`.
|
|
|
16
16
|
config). These files say what lives there and how to work in it, so reading them
|
|
17
17
|
first is cheaper than rediscovering it from the source. How `.agents/` is
|
|
18
18
|
populated — real directory, symlink, committed or not — is yours to decide.
|
|
19
|
+
- **Local additions to overlay files**: an overlay file (`$<NAME>_OVERLAY_ROOT/<path>`)
|
|
20
|
+
is upstream's copy, and `dotagents overlays sync` replaces it. The user's own
|
|
21
|
+
additions live at the same `<path>` under this config directory and the project's
|
|
22
|
+
`.agents/`: when you read an overlay file, read those too — they extend it and win on
|
|
23
|
+
conflict (`dotagents context` lists the ones it finds). Never write user-specific text
|
|
24
|
+
into an overlay file; put it there.
|
|
19
25
|
- **Global-config misses**: if these instructions caused a mistake or rework, or you
|
|
20
26
|
have an improvement idea, record it and move on — don't edit the config:
|
|
21
27
|
`dotagents findings add -g "<one line>" -b "<what happened, evidence>"`. Read the
|
|
@@ -214,6 +214,48 @@ def url_scheme(location: str) -> str:
|
|
|
214
214
|
return location.split("://", 1)[0].lower()
|
|
215
215
|
|
|
216
216
|
|
|
217
|
+
#: The store-config key (``<store>/dotagents/config.toml``) for git's
|
|
218
|
+
#: certificate-revocation check on https clones and fetches, passed to every
|
|
219
|
+
#: git call as ``http.schannelCheckRevoke`` -- Git for Windows' Schannel
|
|
220
|
+
#: backend checks revocation; git elsewhere (OpenSSL) checks no CRLs and
|
|
221
|
+
#: ignores the setting. ``best-effort`` (the default, stated explicitly so a
|
|
222
|
+
#: stricter git config cannot fail dotagents' clones) skips a CRL that is
|
|
223
|
+
#: missing or unreachable; ``false`` skips the check, as on Linux; ``true``
|
|
224
|
+
#: enforces it.
|
|
225
|
+
GIT_SSL_REVOKE_KEY = "git_ssl_revoke"
|
|
226
|
+
GIT_SSL_REVOKE_VALUES = ("best-effort", "true", "false")
|
|
227
|
+
GIT_SSL_REVOKE_DEFAULT = "best-effort"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def git_ssl_revoke(*stores: "Optional[Path]") -> str:
|
|
231
|
+
"""The first ``git_ssl_revoke`` set in ``stores``' config (the project
|
|
232
|
+
store before the user store), else the default. Any other value raises
|
|
233
|
+
:class:`SourceError` naming the file."""
|
|
234
|
+
from dotagents.cli._common import STORE_CONFIG, read_store_config
|
|
235
|
+
|
|
236
|
+
for store in stores:
|
|
237
|
+
if store is None:
|
|
238
|
+
continue
|
|
239
|
+
value = read_store_config(store).get(GIT_SSL_REVOKE_KEY)
|
|
240
|
+
if value is None:
|
|
241
|
+
continue
|
|
242
|
+
normalized = value.strip().lower()
|
|
243
|
+
if normalized not in GIT_SSL_REVOKE_VALUES:
|
|
244
|
+
raise SourceError(
|
|
245
|
+
"error: %s = %r in %s: expected one of %s"
|
|
246
|
+
% (GIT_SSL_REVOKE_KEY, value, Path(store) / STORE_CONFIG, ", ".join(GIT_SSL_REVOKE_VALUES))
|
|
247
|
+
)
|
|
248
|
+
return normalized
|
|
249
|
+
return GIT_SSL_REVOKE_DEFAULT
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
#: What a revocation-check failure looks like in git's stderr (Schannel).
|
|
253
|
+
_REVOCATION_FAILURE = re.compile(r"revocation|CRYPT_E_|SEC_E_UNTRUSTED", re.IGNORECASE)
|
|
254
|
+
#: A git too old to know ``best-effort`` reads it as a broken boolean and
|
|
255
|
+
#: fails every https call: the call is made again without the setting.
|
|
256
|
+
_UNKNOWN_REVOKE_VALUE = re.compile(r"bad boolean config value .* for 'http\.schannelcheckrevoke'", re.IGNORECASE)
|
|
257
|
+
|
|
258
|
+
|
|
217
259
|
class SourceCache(object):
|
|
218
260
|
"""What the commands fetch, under ``root``: git clones, one per repository
|
|
219
261
|
location AND ref (two registry entries naming the same repository at
|
|
@@ -223,9 +265,10 @@ class SourceCache(object):
|
|
|
223
265
|
(a remote directory synced down, a remote file copied), each refreshed
|
|
224
266
|
once per process."""
|
|
225
267
|
|
|
226
|
-
def __init__(self, root: Path, logger: Any = None):
|
|
268
|
+
def __init__(self, root: Path, logger: Any = None, ssl_revoke: str = GIT_SSL_REVOKE_DEFAULT):
|
|
227
269
|
self.root = Path(root)
|
|
228
270
|
self.logger = logger
|
|
271
|
+
self.ssl_revoke = ssl_revoke # git's http.schannelCheckRevoke (GIT_SSL_REVOKE_KEY)
|
|
229
272
|
self._fresh: "set[str]" = set() # locations fetched during this process
|
|
230
273
|
|
|
231
274
|
# --- pathlib_next paths ----------------------------------------------
|
|
@@ -314,17 +357,28 @@ class SourceCache(object):
|
|
|
314
357
|
def _git(self, args: "list[str]", cwd: "Optional[Path]", *, check: bool = True) -> "subprocess.CompletedProcess":
|
|
315
358
|
env = dict(os.environ)
|
|
316
359
|
env["GIT_TERMINAL_PROMPT"] = "0" # never hang on a credential prompt
|
|
360
|
+
setting = ["-c", "http.schannelCheckRevoke=%s" % self.ssl_revoke]
|
|
317
361
|
try:
|
|
318
362
|
proc = subprocess.run(
|
|
319
|
-
["git", *args], cwd=str(cwd) if cwd else None,
|
|
320
|
-
capture_output=True, text=True, env=env,
|
|
363
|
+
["git", *setting, *args], cwd=str(cwd) if cwd else None, capture_output=True, text=True, env=env,
|
|
321
364
|
)
|
|
365
|
+
if proc.returncode != 0 and _UNKNOWN_REVOKE_VALUE.search(proc.stderr or ""):
|
|
366
|
+
# An older git (no best-effort): its own default applies.
|
|
367
|
+
proc = subprocess.run(
|
|
368
|
+
["git", *args], cwd=str(cwd) if cwd else None, capture_output=True, text=True, env=env,
|
|
369
|
+
)
|
|
322
370
|
except OSError as exc:
|
|
323
371
|
raise SourceError("error: git is needed for a git overlay source: %s" % exc)
|
|
324
372
|
if check and proc.returncode != 0:
|
|
373
|
+
output = proc.stderr.strip() or proc.stdout.strip()
|
|
374
|
+
hint = ""
|
|
375
|
+
if _REVOCATION_FAILURE.search(output):
|
|
376
|
+
hint = (
|
|
377
|
+
"\n(the certificate's revocation check failed: set %s = \"false\" in"
|
|
378
|
+
" <store>/dotagents/config.toml to skip it, as git does on Linux)" % GIT_SSL_REVOKE_KEY
|
|
379
|
+
)
|
|
325
380
|
raise SourceError(
|
|
326
|
-
"error: git %s failed (exit %d): %s"
|
|
327
|
-
% (args[0], proc.returncode, redact(proc.stderr.strip() or proc.stdout.strip()))
|
|
381
|
+
"error: git %s failed (exit %d): %s%s" % (args[0], proc.returncode, redact(output), hint)
|
|
328
382
|
)
|
|
329
383
|
return proc
|
|
330
384
|
|
|
@@ -934,4 +988,4 @@ def resolve(
|
|
|
934
988
|
ordered += [Spec("dir", str(p)) for p in registry_files(*stores)]
|
|
935
989
|
if not ordered and not allow_empty:
|
|
936
990
|
raise SourceError(NO_SOURCE_MESSAGE)
|
|
937
|
-
return CompositeSource(ordered, SourceCache(cache_root, logger))
|
|
991
|
+
return CompositeSource(ordered, SourceCache(cache_root, logger, ssl_revoke=git_ssl_revoke(*stores)))
|
|
@@ -324,7 +324,10 @@ def _resolve_from(
|
|
|
324
324
|
if cache_root is None:
|
|
325
325
|
cache_root = resolve_user_store(None) / ".cache" / "overlays"
|
|
326
326
|
try:
|
|
327
|
-
|
|
327
|
+
cache = _sources.SourceCache(
|
|
328
|
+
Path(cache_root), logger, ssl_revoke=_sources.git_ssl_revoke(resolve_user_store(None))
|
|
329
|
+
)
|
|
330
|
+
target = _sources.locate(spec, cache)
|
|
328
331
|
except SystemExit as exc:
|
|
329
332
|
message = str(exc.code if exc.code is not None else exc)
|
|
330
333
|
if message.startswith("error: "):
|
|
@@ -297,8 +297,8 @@ def test_bad_source_is_skipped_not_fatal(monkeypatch, tmp_path):
|
|
|
297
297
|
# at import time must not crash: discovery runs before EVERY invocation,
|
|
298
298
|
# including `env`/`context` inside the SessionStart hooks, so one typo in a
|
|
299
299
|
# personal cmds module used to take the whole session's env and context
|
|
300
|
-
# down
|
|
301
|
-
#
|
|
300
|
+
# down. duho propagates SyntaxError on purpose; the dotagents layer is
|
|
301
|
+
# where it must stop.
|
|
302
302
|
good = tmp_path / "good"
|
|
303
303
|
_write(good / "toy.py", TOY)
|
|
304
304
|
broken = tmp_path / "broken"
|
|
@@ -116,7 +116,7 @@ def test_harness_loads_subtracts_user_agents_md(layout, monkeypatch):
|
|
|
116
116
|
|
|
117
117
|
# Fresh install, no include yet: NOTHING loads the store's AGENTS.md, so
|
|
118
118
|
# `context` must emit it (the old static assumption dropped it here, and
|
|
119
|
-
# the base rules never reached a session
|
|
119
|
+
# the base rules never reached a session at all).
|
|
120
120
|
text = _context.assemble_context(claude, S(dotagents_dir, project_root, True))
|
|
121
121
|
assert "# User rules" in text
|
|
122
122
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
"""Env-assembly regressions
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"""Env-assembly regressions, each a repro turned into a test: malformed
|
|
2
|
+
env.py output, relative PATH entries, a session whose project root is the
|
|
3
|
+
home directory, and the PowerShell rendering of typographic quotes and
|
|
4
|
+
non-ASCII values through the real loader pipe.
|
|
5
5
|
|
|
6
|
-
A case the product still gets wrong is a strict xfail naming the
|
|
6
|
+
A case the product still gets wrong is a strict xfail naming the known
|
|
7
7
|
issue, so fixing it turns the test into an XPASS failure until the marker
|
|
8
8
|
goes. tmp dirs only, no network.
|
|
9
9
|
"""
|
|
@@ -22,7 +22,7 @@ from dotagents._scope import Scope
|
|
|
22
22
|
|
|
23
23
|
def _open(issue, why, condition=True):
|
|
24
24
|
return pytest.mark.xfail(
|
|
25
|
-
condition, strict=True, reason="open (
|
|
25
|
+
condition, strict=True, reason="open (%s): %s" % (issue, why)
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Env-chain safety: which files run, how a failed source is reported, and what
|
|
2
|
-
bash itself adds to a sourced file's output
|
|
3
|
-
the env/scope items).
|
|
2
|
+
bash itself adds to a sourced file's output.
|
|
4
3
|
|
|
5
4
|
tmp dirs only, no network. Run from repo root: ``python -m pytest tests/``.
|
|
6
5
|
"""
|
|
@@ -284,13 +284,13 @@ def test_subcommands_are_nested_not_module_level(findings_mod):
|
|
|
284
284
|
|
|
285
285
|
|
|
286
286
|
# --------------------------------------------------------------------------- #
|
|
287
|
-
# Encodings and reserved names
|
|
288
|
-
#
|
|
289
|
-
#
|
|
287
|
+
# Encodings and reserved names. Cases the module still gets wrong are strict
|
|
288
|
+
# xfails naming the issue: a fix flips them to XPASS failures until the
|
|
289
|
+
# marker goes.
|
|
290
290
|
# --------------------------------------------------------------------------- #
|
|
291
291
|
|
|
292
292
|
def _open(issue, why):
|
|
293
|
-
return pytest.mark.xfail(strict=True, reason="open (
|
|
293
|
+
return pytest.mark.xfail(strict=True, reason="open (%s): %s" % (issue, why))
|
|
294
294
|
|
|
295
295
|
|
|
296
296
|
def test_cmd_body_from_stdin_is_read_as_utf8_bytes(findings_mod, tmp_path, monkeypatch, capsys):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Adapter fixes
|
|
1
|
+
"""Adapter fixes: adapters are told the scope,
|
|
2
2
|
Claude honours CLAUDE_CONFIG_DIR, `init` writes the store's AGENTS.md itself,
|
|
3
3
|
global harness configs are left alone by a project init, Cursor's rule file,
|
|
4
4
|
Claude's import parsing, identity from runtime signals only, include dedupe.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Regression tests for
|
|
1
|
+
"""Regression tests for CLI fixes: command discovery that survives a
|
|
2
2
|
command whose parser cannot be built, the `findings` queue's encoding, naming
|
|
3
3
|
and move-never-delete rules, and `launch`'s exit code, context file and
|
|
4
4
|
cmd.exe argument handling.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Hook fixes
|
|
1
|
+
"""Hook fixes: the PowerShell handlers' Git Bash
|
|
2
2
|
gate, UTF-8 through the PowerShell pipe, the PreToolUse matcher, merging that
|
|
3
3
|
keeps the user's keys, symlink-safe and non-ASCII-safe wrappers.
|
|
4
4
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""Regression tests for
|
|
2
|
-
|
|
1
|
+
"""Regression tests for "Overlays, sources and skills" fixes: typed source
|
|
2
|
+
errors, registry parsing, credential handling, the spec
|
|
3
3
|
grammar, manifest validation, overlay names, install records, and the
|
|
4
4
|
add/remove/sync lifecycle.
|
|
5
5
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
"""Regressions for the scope-resolution and context-assembly
|
|
2
|
-
(2026-09-23 review, "Scope resolution and context assembly").
|
|
1
|
+
"""Regressions for the scope-resolution and context-assembly fixes.
|
|
3
2
|
|
|
4
3
|
tmp dirs only. The user store is redirected with `$AGENTS_HOME`, the system
|
|
5
4
|
store with `$AGENTS_SYSTEM_ROOT` (a path that does not exist), and the project
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""Regression tests for
|
|
2
|
-
|
|
1
|
+
"""Regression tests for skills, adapter and hook-cost fixes: Claude's
|
|
2
|
+
per-skill links are refreshed and pruned, `overlays
|
|
3
3
|
add/sync/remove` keep a wired Claude skills dir current, project-scope skill
|
|
4
4
|
links warn when git would commit them, pi gets a pointer to a project store,
|
|
5
5
|
and the env loaders reuse a cached diff while the inputs are unchanged.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""`git_ssl_revoke` in `<store>/dotagents/config.toml`: git's
|
|
2
|
+
certificate-revocation check for overlay clones and fetches. Git for Windows'
|
|
3
|
+
Schannel backend fails an https clone whose CRL cannot be fetched; git with
|
|
4
|
+
OpenSSL (Linux) checks no CRLs. dotagents passes the setting to every git call
|
|
5
|
+
as `http.schannelCheckRevoke`, `best-effort` unless the store says otherwise."""
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from dotagents import _sources
|
|
11
|
+
from dotagents.cli._common import STORE_CONFIG
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _config(store, text):
|
|
15
|
+
path = store / STORE_CONFIG
|
|
16
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
path.write_text(text, encoding="utf-8")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture()
|
|
21
|
+
def argv(monkeypatch):
|
|
22
|
+
seen = []
|
|
23
|
+
|
|
24
|
+
def run(cmd, **kwargs):
|
|
25
|
+
seen.append(cmd)
|
|
26
|
+
return subprocess.CompletedProcess(cmd, 0, "", "")
|
|
27
|
+
|
|
28
|
+
monkeypatch.setattr(_sources.subprocess, "run", run)
|
|
29
|
+
return seen
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_every_git_call_carries_the_setting_best_effort_by_default(tmp_path, argv):
|
|
33
|
+
cache = _sources.SourceCache(tmp_path / "cache")
|
|
34
|
+
cache._git(["ls-remote", "https://example.invalid/r.git"], None)
|
|
35
|
+
assert argv[-1][:3] == ["git", "-c", "http.schannelCheckRevoke=best-effort"]
|
|
36
|
+
_sources.SourceCache(tmp_path / "cache", ssl_revoke="false")._git(["fetch"], None)
|
|
37
|
+
assert argv[-1][:3] == ["git", "-c", "http.schannelCheckRevoke=false"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_the_store_config_chooses_project_before_user(tmp_path):
|
|
41
|
+
user, project = tmp_path / "user", tmp_path / "project"
|
|
42
|
+
assert _sources.git_ssl_revoke(project, user) == "best-effort"
|
|
43
|
+
_config(user, 'git_ssl_revoke = "false"\n')
|
|
44
|
+
assert _sources.git_ssl_revoke(None, user) == "false"
|
|
45
|
+
_config(project, 'git_ssl_revoke = "TRUE"\n')
|
|
46
|
+
assert _sources.git_ssl_revoke(project, user) == "true"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_an_unknown_value_names_the_file(tmp_path):
|
|
50
|
+
_config(tmp_path, 'git_ssl_revoke = "maybe"\n')
|
|
51
|
+
with pytest.raises(_sources.SourceError, match="git_ssl_revoke = 'maybe'.*best-effort, true, false"):
|
|
52
|
+
_sources.git_ssl_revoke(tmp_path)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_a_revocation_failure_names_the_setting(tmp_path, monkeypatch):
|
|
56
|
+
stderr = ("fatal: unable to access 'https://example.invalid/r.git/': schannel: next InitializeSecurityContext "
|
|
57
|
+
"failed: CRYPT_E_NO_REVOCATION_CHECK (0x80092012) - The revocation function was unable to check "
|
|
58
|
+
"revocation for the certificate.")
|
|
59
|
+
monkeypatch.setattr(_sources.subprocess, "run",
|
|
60
|
+
lambda cmd, **kw: subprocess.CompletedProcess(cmd, 128, "", stderr))
|
|
61
|
+
with pytest.raises(_sources.SourceError) as caught:
|
|
62
|
+
_sources.SourceCache(tmp_path)._git(["clone", "https://example.invalid/r.git"], None)
|
|
63
|
+
message = str(caught.value)
|
|
64
|
+
assert "CRYPT_E_NO_REVOCATION_CHECK" in message and 'git_ssl_revoke = "false"' in message
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_a_git_without_best_effort_runs_again_without_it(tmp_path, monkeypatch):
|
|
68
|
+
seen = []
|
|
69
|
+
|
|
70
|
+
def run(cmd, **kwargs):
|
|
71
|
+
seen.append(cmd)
|
|
72
|
+
if "http.schannelCheckRevoke=best-effort" in cmd:
|
|
73
|
+
return subprocess.CompletedProcess(cmd, 128, "", "fatal: bad boolean config value 'best-effort' for "
|
|
74
|
+
"'http.schannelcheckrevoke'\n")
|
|
75
|
+
return subprocess.CompletedProcess(cmd, 0, "ok", "")
|
|
76
|
+
|
|
77
|
+
monkeypatch.setattr(_sources.subprocess, "run", run)
|
|
78
|
+
proc = _sources.SourceCache(tmp_path)._git(["ls-remote", "https://example.invalid/r.git"], None)
|
|
79
|
+
assert proc.returncode == 0 and len(seen) == 2
|
|
80
|
+
assert seen[1] == ["git", "ls-remote", "https://example.invalid/r.git"]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_any_other_failure_has_no_hint(tmp_path, monkeypatch):
|
|
84
|
+
monkeypatch.setattr(_sources.subprocess, "run",
|
|
85
|
+
lambda cmd, **kw: subprocess.CompletedProcess(cmd, 128, "", "fatal: repository not found"))
|
|
86
|
+
with pytest.raises(_sources.SourceError) as caught:
|
|
87
|
+
_sources.SourceCache(tmp_path)._git(["clone", "https://example.invalid/r.git"], None)
|
|
88
|
+
assert "git_ssl_revoke" not in str(caught.value)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.skipif(not __import__("shutil").which("git"), reason="needs git on PATH")
|
|
92
|
+
def test_git_accepts_the_option_on_a_local_clone(tmp_path):
|
|
93
|
+
"""The option reaches a real git without breaking a clone (git ignores
|
|
94
|
+
it off Schannel; on Windows it is honoured)."""
|
|
95
|
+
work = tmp_path / "work"
|
|
96
|
+
work.mkdir()
|
|
97
|
+
for args in (["init", "-q", "-b", "main"], ["-c", "user.email=t@example.invalid", "-c", "user.name=t",
|
|
98
|
+
"commit", "-q", "--allow-empty", "-m", "x"]):
|
|
99
|
+
subprocess.run(["git", *args], cwd=str(work), check=True, capture_output=True)
|
|
100
|
+
cache = _sources.SourceCache(tmp_path / "cache", ssl_revoke="false")
|
|
101
|
+
cache._git(["clone", "-q", str(work), str(tmp_path / "copy")], None)
|
|
102
|
+
assert (tmp_path / "copy" / ".git").is_dir()
|