dotagents-cli 0.9.1__tar.gz → 0.9.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.9.1 → dotagents_cli-0.9.2}/CHANGELOG.md +41 -1
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/PKG-INFO +1 -1
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/guide/commands.md +2 -0
- dotagents_cli-0.9.2/docs/guide/overlay-hooks.md +213 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/guide/private-sync.md +14 -6
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/mkdocs.yml +1 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/AGENTS.md +150 -4
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/__init__.py +1 -1
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_activate.py +5 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_agents.py +179 -8
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_aos.py +483 -6
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_env.py +101 -8
- dotagents_cli-0.9.2/src/dotagents/_hook_dispatch.py +315 -0
- dotagents_cli-0.9.2/src/dotagents/_hook_main.py +55 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_hooks.py +7 -3
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/cmds/launch.py +20 -2
- dotagents_cli-0.9.2/src/dotagents/_overlay/dotagents/hooks/overlay_hook.py +289 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/hooks/preinvocation_antigravity_context.py +3 -3
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/hooks/pretooluse_codex_env.py +4 -5
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/hooks/sessionstart_codex_context.py +5 -5
- dotagents_cli-0.9.2/src/dotagents/_overlay_hooks.py +947 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_run.py +128 -7
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/cli/__init__.py +2 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/cli/env.py +4 -1
- dotagents_cli-0.9.2/src/dotagents/cli/hook.py +70 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/cli/overlays.py +208 -2
- dotagents_cli-0.9.2/tests/_hook_helpers.py +93 -0
- dotagents_cli-0.9.2/tests/aos_vectors/REVISION +2 -0
- dotagents_cli-0.9.2/tests/aos_vectors/hooks.json +6797 -0
- dotagents_cli-0.9.2/tests/aos_vectors/hooks_compose.json +436 -0
- dotagents_cli-0.9.2/tests/aos_vectors/hooks_validation.json +660 -0
- dotagents_cli-0.9.2/tests/test_aos_hook_vectors.py +147 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_aos_vectors.py +5 -1
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_audit_leak.py +1 -1
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_cmds_discovery.py +1 -1
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_launch_cmd.py +57 -10
- dotagents_cli-0.9.2/tests/test_overlay_hook_forwarder.py +286 -0
- dotagents_cli-0.9.2/tests/test_overlay_hook_wiring.py +181 -0
- dotagents_cli-0.9.2/tests/test_overlay_hooks.py +458 -0
- dotagents_cli-0.9.2/tests/test_overlay_hooks_cli.py +136 -0
- dotagents_cli-0.9.2/tests/test_overlay_hooks_probes.py +188 -0
- dotagents_cli-0.9.2/tests/test_overlay_hooks_security.py +284 -0
- dotagents_cli-0.9.2/tests/test_overlay_hooks_snapshot.py +278 -0
- dotagents_cli-0.9.2/tests/test_pre_env_hook.py +130 -0
- dotagents_cli-0.9.2/tests/test_run_hook.py +94 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_wire_hooks.py +56 -39
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tools/cloud-setup.sh +24 -31
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tools/pyz_smoke.sh +1 -1
- dotagents_cli-0.9.1/tests/aos_vectors/REVISION +0 -1
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/.gitignore +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/LICENSE +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/README.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/agents.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/cli.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/commands.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/context.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/env.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/fs.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/hooks.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/merge.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/overlays.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/reference.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/resources.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/scope.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/skills.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/sources.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/api/wrappers.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/changelog.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/guide/authoring.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/guide/install.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/guide/overlays.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/docs/index.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/install.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/pyproject.toml +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/__main__.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_context.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_deps.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_fs.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_launchers.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_mcp_gateway.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_mcp_gateway_clients.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_mcp_gateway_config.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_merge.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/cmds/findings.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/templates/AGENTS.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/templates/PROJECT.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_overlays.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_pipwheel.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_policy.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_pyvenv.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_resources.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_scope.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_skills.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_sources.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/_wrappers.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/cli/_common.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/cli/about.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/cli/build_pyz.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/cli/context.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/cli/init.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/cli/mcp.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/cli/path.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/src/dotagents/py.typed +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/_helpers.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/_shell.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/aos_vectors/README.md +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/aos_vectors/compose.json +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/aos_vectors/digest.json +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/aos_vectors/effective.json +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/aos_vectors/names.json +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/aos_vectors/reading.json +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/aos_vectors/source.json +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/aos_vectors/validation.json +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/conftest.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_about.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_agents.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_aos_consumer.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_api_header.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_build_pyz.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_cli_main.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_cloud_setup.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_context.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_deps.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_env.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_env_cli_scope.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_env_format.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_env_into.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_env_regressions.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_env_safety.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_findings_cmd.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_fix_adapters.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_fix_cli.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_fix_env.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_fix_hooks.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_fix_init.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_fix_overlays.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_fix_packaging.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_fix_scope_context.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_fix_skills_adapters.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_git_ssl_revoke.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_hooks.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_init_base.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_last_mile.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_launchers.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_local_additions.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_mcp.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_no_internal_labels.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_overlay_libs.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_overlays_cli.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_overlays_command.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_path_timeout_frontmatter.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_pipwheel.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_powershell_env_hook.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_pyvenv_core.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_quoting_injection_safety.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_scope.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_sources.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_sources_uri.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tests/test_wrappers.py +0 -0
- {dotagents_cli-0.9.1 → dotagents_cli-0.9.2}/tools/audit.py +0 -0
|
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.2] - 2026-11-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Overlay hooks, the AOS "Hooks" extension: an overlay's `hook.<ext>` and `[hooks]` table
|
|
15
|
+
run at a session's events (`pre-env`, `session-start`, `session-end`, `prompt`,
|
|
16
|
+
`turn-end`, `cd`, `pre-compact`, `pre-tool`, `post-tool`). dotagents fires `pre-env`
|
|
17
|
+
itself as it builds the environment, on every harness; `init` and
|
|
18
|
+
`overlays add/sync/remove` wire Claude Code's and Codex's own hooks to a new
|
|
19
|
+
`dotagents hook` command for the other events an installed overlay has a hook for (nothing
|
|
20
|
+
is wired, so no process starts, for a store without them). A hook is off until the user
|
|
21
|
+
allows it at the overlay's tree digest (`overlays hooks`, `overlays allow-hooks NAME
|
|
22
|
+
--digest D`, `overlays revoke-hooks`; kept in the user store, one digest per overlay), is
|
|
23
|
+
bounded (60 s a run, 120 s an event, 64 KiB of output, 1 MiB of payload, arguments as a
|
|
24
|
+
list, its own process group), and can only tighten: a `decision` other than
|
|
25
|
+
`block`, `deny` or `ask` is a refused attempt to grant, and the answer to the harness is
|
|
26
|
+
built by dotagents, never the hook's output. An overlay with a hook that refuses on failure
|
|
27
|
+
is not activated while its hooks are off. What a harness cannot fire or apply is reported
|
|
28
|
+
(`hook-unsupported`, `hook-partial`, `hook-not-applied`, `hook-ask-denied`,
|
|
29
|
+
`hook-not-allowed`, `hook-failed`), never dropped. Passes the specification's hook
|
|
30
|
+
vectors (the `OVERLAY.md` cases excepted: that form is not read yet). See
|
|
31
|
+
`docs/guide/overlay-hooks.md`.
|
|
32
|
+
- The sensitive-variable list holds `CLAUDE_CONFIG_DIR` and `CODEX_HOME`, which point a
|
|
33
|
+
harness at another configuration, with its own permissions and hooks.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- The session-start, PowerShell, Codex and Antigravity hook entries run the `dotagents` of the
|
|
38
|
+
store (`$AGENTS_HOME/bin`) or on PATH, never one in the project's own `.agents/bin`: the agent
|
|
39
|
+
can write there, and what it planted ran with the user's access.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- `private-sync`'s session start and sync-back are now overlay hooks (`session-start` and
|
|
44
|
+
`turn-end`) instead of scripts you register in `~/.claude/settings.json`; it no longer ships
|
|
45
|
+
`hooks/settings.snippet.json`. Allow them once (`dotagents overlays allow-hooks private-sync`)
|
|
46
|
+
and run `dotagents init -g`; remove the old entries from `settings.json` or they run twice.
|
|
47
|
+
`tools/cloud-setup.sh` does both steps itself.
|
|
48
|
+
|
|
10
49
|
## [0.9.1] - 2026-11-06
|
|
11
50
|
|
|
12
51
|
### Fixed
|
|
@@ -1754,7 +1793,8 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
|
|
|
1754
1793
|
affected module sources (`dotagents.cli`, `duho.presets`) to extracted temp files
|
|
1755
1794
|
before dispatch; a no-op for a plain install.
|
|
1756
1795
|
|
|
1757
|
-
[Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.9.
|
|
1796
|
+
[Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.9.2...HEAD
|
|
1797
|
+
[0.9.2]: https://github.com/jose-pr/dotagents/compare/v0.9.1...v0.9.2
|
|
1758
1798
|
[0.9.1]: https://github.com/jose-pr/dotagents/compare/v0.9.0...v0.9.1
|
|
1759
1799
|
[0.9.0]: https://github.com/jose-pr/dotagents/compare/v0.8.3...v0.9.0
|
|
1760
1800
|
[0.8.3]: https://github.com/jose-pr/dotagents/compare/v0.8.2...v0.8.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: dotagents-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -217,6 +217,8 @@ dotagents overlays list # installed (discovered) + available
|
|
|
217
217
|
dotagents overlays sync 'py*' # refresh installed overlays matching a glob
|
|
218
218
|
dotagents overlays remove python # delete the overlay dir, unpublish its skills, un-merge its rules
|
|
219
219
|
dotagents overlays show python # describe one: declarations, digest, pin, weakenings, source (--json)
|
|
220
|
+
dotagents overlays hooks private-sync # its hooks: what runs where, and whether they are allowed
|
|
221
|
+
dotagents overlays allow-hooks private-sync --digest sha256:... # allow them at that digest (see overlay-hooks)
|
|
220
222
|
```
|
|
221
223
|
|
|
222
224
|
- **`add`** takes names or globs: `'*'` is every overlay the repos offer, so with
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Overlay hooks
|
|
2
|
+
|
|
3
|
+
An overlay can run a program at a named moment of a session: before the environment is built,
|
|
4
|
+
when a session starts or ends, when you send a prompt, before a tool runs. This is the
|
|
5
|
+
[Agent Overlay Specification's hooks](https://boriken-dev.github.io/agent-overlay-spec/spec/v0/hooks/)
|
|
6
|
+
(an optional extension); this page is what dotagents does with them.
|
|
7
|
+
|
|
8
|
+
```toml
|
|
9
|
+
# overlay.cfg
|
|
10
|
+
[hooks]
|
|
11
|
+
session-start = "hooks/start.py"
|
|
12
|
+
pre-tool = { run = "hooks/guard.sh", match = ["execute"], timeout = 5, on_failure = "refuse" }
|
|
13
|
+
turn-end = false
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
A `hook.<ext>` at the overlay root (chosen by runtime, as `env.<ext>` is) receives every event
|
|
17
|
+
that carries no content by default. `prompt`, `pre-tool` and `post-tool` carry what you typed or
|
|
18
|
+
what a tool was given, so they fire only for an event `[hooks]` names. The hook is told its event
|
|
19
|
+
as its first argument and in `AOS_HOOK_EVENT`, and its payload as one JSON object on standard
|
|
20
|
+
input; it answers with nothing or one JSON object (`context`, `decision`, `reason`, `env`).
|
|
21
|
+
|
|
22
|
+
## A hook tightens and never grants
|
|
23
|
+
|
|
24
|
+
Nothing a hook prints reaches the harness. dotagents reads the output, accepts only what the
|
|
25
|
+
event takes, and answers the harness itself, in the harness's words. A `decision` other than
|
|
26
|
+
`block`, `deny` or `ask` (`allow`, `approve`, anything) is a refused attempt to grant: the run
|
|
27
|
+
fails (`grant`) and nothing of it applies. dotagents never states an approval and never rewrites
|
|
28
|
+
a tool's input.
|
|
29
|
+
|
|
30
|
+
## Off until you allow them
|
|
31
|
+
|
|
32
|
+
A hook is the overlay's code, running at every session with your access, outside the harness's
|
|
33
|
+
sandbox. An overlay's hooks do not run until you allow them at the overlay's digest:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
dotagents overlays hooks NAME # what runs, where, and whether it is allowed
|
|
37
|
+
dotagents overlays allow-hooks NAME # shows the same and the digest
|
|
38
|
+
dotagents overlays allow-hooks NAME --digest sha256:...
|
|
39
|
+
dotagents overlays revoke-hooks NAME
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The allowance is kept in your user store (`overlays/.dotagents-state/hooks-allowed.json`), never
|
|
43
|
+
in an overlay or a project, per overlay and store. There is one digest per overlay: any change to
|
|
44
|
+
the overlay turns its hooks off until you allow the new one, and going back to an earlier digest
|
|
45
|
+
needs a new yes. `allow-hooks` asks at the terminal itself (`/dev/tty`, `CONIN$`), refuses to run
|
|
46
|
+
inside an agent harness's session and is not an MCP tool; `--no-prompt` is for your own setup
|
|
47
|
+
script. The allowance is only as strong as the agent's lack of write access to your user store:
|
|
48
|
+
an agent that can write there can write an allowance.
|
|
49
|
+
|
|
50
|
+
An overlay with a hook that has `on_failure = "refuse"` (a guard) is **not activated** while its
|
|
51
|
+
hooks are off, and its guards **fail** meanwhile: a prompt is blocked, a tool call denied, as if
|
|
52
|
+
the hook had run and failed (`hook-not-allowed` at the next session start). A guard that is
|
|
53
|
+
missing must not let everything through.
|
|
54
|
+
|
|
55
|
+
## Which events fire where
|
|
56
|
+
|
|
57
|
+
| Event | Claude Code | Codex | Any other harness |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| `pre-env` | dotagents fires it as it builds the environment | same | same |
|
|
60
|
+
| `session-start` | `SessionStart`: context, and variables for Bash through the env file | `SessionStart`: context | cannot |
|
|
61
|
+
| `session-end` | `SessionEnd` | `SessionEnd` | cannot |
|
|
62
|
+
| `prompt` | `UserPromptSubmit`: context, block | same | cannot |
|
|
63
|
+
| `turn-end` | `Stop` (notification only) | `Stop` | cannot |
|
|
64
|
+
| `cd` | `CwdChanged`: variables | no such event: cannot | cannot |
|
|
65
|
+
| `pre-compact` | `PreCompact`: context, block | `PreCompact`: block (no context) | cannot |
|
|
66
|
+
| `pre-tool` | `PreToolUse`: deny, ask, context | `PreToolUse`: deny, context (`ask` is applied as `deny`) | cannot |
|
|
67
|
+
| `post-tool` | `PostToolUse`: context | `PostToolUse`: context | cannot |
|
|
68
|
+
|
|
69
|
+
What cannot fire is reported (`hook-unsupported`), as is an event that fires for only some
|
|
70
|
+
occurrences (`hook-partial`: Claude Code's `PostToolUse` is for calls that ended well; Codex's
|
|
71
|
+
tool hooks were measured on its shell tool), an output the harness cannot take
|
|
72
|
+
(`hook-not-applied`: variables from `prompt` or `pre-tool`, context from Codex's `pre-compact`,
|
|
73
|
+
refusing a session start), an `ask` turned into a `deny` (`hook-ask-denied`), a hook that is off
|
|
74
|
+
(`hook-not-allowed`), and a failed run (`hook-failed`, with its cause, never its output). The
|
|
75
|
+
reports reach you in the session-start message of the harness and in the log of `dotagents env`.
|
|
76
|
+
`dotagents overlays hooks` lists the table above per hook.
|
|
77
|
+
|
|
78
|
+
`init` and `overlays add/sync/remove` wire the harness's own hook for an event only when an
|
|
79
|
+
installed overlay has a hook for it (`dotagents hook` is the program they run, through a small
|
|
80
|
+
forwarder script `overlay_hook.py` deployed in your user config directory), so a store with no
|
|
81
|
+
hooks starts no extra process. A project's entries (`settings.local.json`) run the same forwarder
|
|
82
|
+
from your user config directory. The text of an entry holds no guard flag and no overlay name, so
|
|
83
|
+
it does not change when overlays do. Codex asks you to trust a hook by its hash; dotagents never
|
|
84
|
+
writes that trust.
|
|
85
|
+
|
|
86
|
+
## Limits and failures
|
|
87
|
+
|
|
88
|
+
Every hook runs with the overlay root as its working directory, outside the sandbox, in its own
|
|
89
|
+
process group (a job object on Windows) that ends with it, with arguments as a list (no shell
|
|
90
|
+
reads the overlay's strings) and the environment of that overlay's own processes. Defaults:
|
|
91
|
+
60 s a run (and the entry's `timeout`, default 30, if shorter), 120 s an event, 64 KiB of
|
|
92
|
+
output (more stops the run), 64 KiB of standard error kept for the log, 1 MiB of payload. A
|
|
93
|
+
failed hook applies nothing of its output; with `on_failure = "report"` the event goes on, with
|
|
94
|
+
`"refuse"` it is refused (a prompt blocked, a call denied, a session start reported as not
|
|
95
|
+
refusable). `pre-env` stops the session only under `dotagents launch`, where the harness does not
|
|
96
|
+
exist yet; under `dotagents env` (a harness already running, a setup) it is reported as
|
|
97
|
+
`hook-not-applied` and nothing of the hook applies. An oversize or unreadable payload is a failure
|
|
98
|
+
of the event, refused when it has a guard.
|
|
99
|
+
|
|
100
|
+
The limits are settings, read from your user store only
|
|
101
|
+
(`overlays/.dotagents-state/hooks-settings.json`; a project's file is never read), each in
|
|
102
|
+
bounds: `timeout`, `event_timeout`, `deadline`, `max_output`, `max_payload`. The deadline
|
|
103
|
+
(default 150 s, at most 580) covers everything a harness's hook entry does from the process's
|
|
104
|
+
start: building the scope, hashing, env scripts and hooks; at it the whole process tree ends. The
|
|
105
|
+
harness's own timeout for the entry is derived from it and written at wiring time, so run
|
|
106
|
+
`dotagents init -g` after changing it.
|
|
107
|
+
|
|
108
|
+
Context a hook adds is wrapped in a marker holding a random nonce chosen after the hooks ran, so
|
|
109
|
+
the text cannot imitate it. Variables for Bash are written to Claude Code's env file as
|
|
110
|
+
single-quoted values of names the hook declared in `sets`; a value holding a line separator of
|
|
111
|
+
any kind (CR, LF, VT, FF, U+0085, U+2028, U+2029, NUL) is not written (`hook-not-applied`), and a
|
|
112
|
+
sensitive name (`PATH`, `BASH_ENV`, `LD_*`, ...) that is set is reported (`env-sensitive`). What a
|
|
113
|
+
hook writes to standard error goes to `<user store>/.cache/hooks.log` (bounded), never to the
|
|
114
|
+
harness.
|
|
115
|
+
|
|
116
|
+
## What a hook cannot reach
|
|
117
|
+
|
|
118
|
+
This is about the **overlay-hook path**: a hook of an installed overlay, fired through the
|
|
119
|
+
forwarder (or by `pre-env`). It is the hardened path. It is not the whole of dotagents: see
|
|
120
|
+
"Where project code does run" below.
|
|
121
|
+
|
|
122
|
+
- **Bytes that were not hashed.** At each event the overlay's tree is read once, hashed, compared
|
|
123
|
+
with the allowed digest, and written to a snapshot under `<user store>/.cache/hook-snapshots/`
|
|
124
|
+
from those same bytes. The manifest the event is planned from, the env scripts that build the
|
|
125
|
+
hook's environment, the `lib/` and `bin/` paths they declare, the hook, and its working
|
|
126
|
+
directory are all the snapshot's; Python hooks run under `-I` with the snapshot's libraries on
|
|
127
|
+
the path explicitly. A file swapped after hashing, or a `node_modules`, `__pycache__` or
|
|
128
|
+
`.venv` (not part of a digest) planted beside the overlay, do not run. An existing snapshot is
|
|
129
|
+
hashed again before it is used, and copies of other digests are removed only when unused for a
|
|
130
|
+
week (a session started before an update still runs from its own). The overlay's state
|
|
131
|
+
directory is the live one, so a hook keeps what it wrote.
|
|
132
|
+
- **Anything the project holds.** The hook entries run an absolute interpreter, isolated (`-I`),
|
|
133
|
+
a forwarder script in your user config directory, and a fixed entry of the `dotagents` that
|
|
134
|
+
`init` recorded there (`dotagents._hook_main`). That entry is not the `dotagents` command line:
|
|
135
|
+
it discovers no command and adds no library, so `<project>/.agents/dotagents/cmds/`, a
|
|
136
|
+
project overlay's `cmds/` and `lib/` and the project's own modules never load in it. For a
|
|
137
|
+
user-level hook its scope has no project store at all, so a project folder cannot shadow a hook
|
|
138
|
+
of yours (a same-named one is reported, `hook-shadowed`, and your hook still runs), cannot add
|
|
139
|
+
anything to it, and cannot choose its Python.
|
|
140
|
+
- **A stand-in for dotagents.** The forwarder gives the entry a fresh nonce and accepts only an
|
|
141
|
+
answer that carries it back. A program that exits 0 with no output, or answers without the
|
|
142
|
+
nonce, is a failed guard (a refusal) and nothing at all for an event with no guard; an
|
|
143
|
+
"everything is fine" is never inferred from silence. When the forwarder's record lists a guard
|
|
144
|
+
for an event and the entry finds none, that is a refusal too. The forwarder passes the harness
|
|
145
|
+
only a refusal, context and a message, in the harness's own shape.
|
|
146
|
+
- **A project's interpreters.** `pwsh`, `node`, `bash` and the like, for a hook and for the env
|
|
147
|
+
scripts that build its environment, are found by dotagents itself (never the current
|
|
148
|
+
directory, which Windows searches first) on the `PATH` the session was started from, less every
|
|
149
|
+
relative entry, store or overlay `bin`, venv, and anything below the project or the user store
|
|
150
|
+
(`.venv/Scripts`, `node_modules/.bin`, `.agents/bin`). A Windows bash is looked for only beside
|
|
151
|
+
a `git` on that PATH and in the standard install directories.
|
|
152
|
+
- **A project's Python.** A hook, and the env scripts that build its environment, run with the
|
|
153
|
+
user store's Python chosen before any declaration runs, never a project's venv, and never the
|
|
154
|
+
session's `AGENTS_PYTHON`.
|
|
155
|
+
- **Other overlays' secrets.** `dotagents launch` records what it changed in
|
|
156
|
+
`AGENTS_SESSION_BASE`; a hook starts from the environment the session was started from, so a
|
|
157
|
+
secret another overlay was granted does not reach it. The harness's own variables
|
|
158
|
+
(`CLAUDE_ENV_FILE`, `CLAUDE_CODE_*`, `CODEX_*`) are dropped too. The project's own name for
|
|
159
|
+
itself (`AGENTS_PROJECT_ROOT`, `CLAUDE_PROJECT_DIR`) is kept.
|
|
160
|
+
- **The harness itself.** `dotagents launch` looks for a bare program name (`claude`) on the PATH
|
|
161
|
+
the shell had, less the same places, not on the session's PATH that starts with the project's
|
|
162
|
+
`.agents/bin`.
|
|
163
|
+
|
|
164
|
+
## Where project code does run
|
|
165
|
+
|
|
166
|
+
Not everything dotagents does is hardened this way, by design (the owner's decision, recorded in
|
|
167
|
+
dotagents' decisions):
|
|
168
|
+
|
|
169
|
+
- The session-start, Codex and Antigravity entries that `init` wrote before overlay hooks existed
|
|
170
|
+
run `dotagents env` and `dotagents context` **in the project**. Those are the full command line:
|
|
171
|
+
they load `<project>/.agents/dotagents/cmds/`, and `env` runs the env scripts of the overlays
|
|
172
|
+
active there, outside the harness's sandbox. They run the store's `dotagents` (never one found in
|
|
173
|
+
the project or on PATH), but what that program loads from the project, it runs.
|
|
174
|
+
- A project overlay's own `env`/env scripts run whenever the environment is built for a session
|
|
175
|
+
there (`env`, `launch`, `setup`), as any active overlay's do.
|
|
176
|
+
- **Project-level guards are advisory.** A project-level hook entry lives in
|
|
177
|
+
`<project>/.claude/settings.local.json`, which the agent can write: it can remove the entry. The
|
|
178
|
+
forwarder and the user-level entries in your own config are not affected, but a guard that only
|
|
179
|
+
a project overlay declares is only as strong as that file. Put a guard you rely on in an overlay
|
|
180
|
+
of your user store.
|
|
181
|
+
|
|
182
|
+
An agent that can write the project's `.agents`, or your user store, can run code through those
|
|
183
|
+
paths; the overlay-hook path is the one that does not depend on either being safe from the agent.
|
|
184
|
+
|
|
185
|
+
## The limit, generally
|
|
186
|
+
|
|
187
|
+
All of this assumes the places dotagents runs code *from* are not the agent's to write. A
|
|
188
|
+
deployment gets no barrier where:
|
|
189
|
+
|
|
190
|
+
- the recorded `code_root` is inside a checkout the agent works in (a `dist/dotagents.pyz` of a
|
|
191
|
+
repository, a source tree on `PYTHONPATH`), or a venv's `.pth` points dotagents into such a
|
|
192
|
+
place: what the agent writes there runs with your access. Install dotagents where the agent
|
|
193
|
+
cannot write, then run `dotagents init -g`;
|
|
194
|
+
- the agent can write your user store (`~/.agents`): it holds the allowances, the snapshots, the
|
|
195
|
+
settings and the wrappers, so it can allow and run anything.
|
|
196
|
+
|
|
197
|
+
`--no-prompt` and the harness-session check of `allow-hooks` stop accidents, not an agent: an
|
|
198
|
+
agent can clear its environment (`env -i`) and run the command. What keeps an agent from allowing
|
|
199
|
+
its own hooks is that `allow-hooks` is not an MCP tool and that the allowance is in a store it
|
|
200
|
+
cannot write.
|
|
201
|
+
|
|
202
|
+
Smaller limits: a hook that detaches into a new session (`setsid`) can outlive the tree kill on
|
|
203
|
+
POSIX; an overlay whose manifest is refused cannot show its guards; a hook can still read what
|
|
204
|
+
its overlay `requires` puts on its paths, live, and the packages an overlay's setup installed; an
|
|
205
|
+
entry whose interpreter has been uninstalled cannot run to say so (`dotagents overlays hooks` and
|
|
206
|
+
every `init` check the recorded one); and Codex asks you to trust an entry by its hash, which
|
|
207
|
+
dotagents cannot read: `overlays hooks` says the event fires natively once you have trusted it,
|
|
208
|
+
and a change of the entry text is warned about when it is wired.
|
|
209
|
+
|
|
210
|
+
## Not done yet
|
|
211
|
+
|
|
212
|
+
Hooks declared by your own configuration or a project's (outside overlays), a `pre-tool`
|
|
213
|
+
variable loader, `OVERLAY.md` front matter, and harnesses other than Claude Code and Codex.
|
|
@@ -77,12 +77,20 @@ leaves the machine is a perfectly valid setup.
|
|
|
77
77
|
|
|
78
78
|
## Cloud sessions
|
|
79
79
|
|
|
80
|
-
The `private-sync` overlay
|
|
81
|
-
the private repo and link/sync the project each session
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
The `private-sync` overlay declares two [overlay hooks](overlay-hooks.md) that clone or pull
|
|
81
|
+
the private repo and link/sync the project each session: `session-start` pulls and links,
|
|
82
|
+
`turn-end` pushes the session's changes back. They are off until you allow them at the
|
|
83
|
+
overlay's digest, and `dotagents init -g` wires Claude Code's and Codex's own hooks to run
|
|
84
|
+
them:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
dotagents overlays hooks private-sync # what runs, on which harness, and whether it is allowed
|
|
88
|
+
dotagents overlays allow-hooks private-sync # shows the same and the digest to allow
|
|
89
|
+
dotagents overlays allow-hooks private-sync --digest sha256:<the digest shown>
|
|
90
|
+
dotagents init -g
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The bootstrap below does all of that for you.
|
|
86
94
|
|
|
87
95
|
For a **fresh container** with no config yet, point the web environment's setup-script
|
|
88
96
|
field at the self-contained bootstrap shipped in this repo — it fetches the latest each
|
|
@@ -52,6 +52,10 @@ and an error saying a command is required, and exits 2.
|
|
|
52
52
|
| `overlays sync [GLOB] [-g] [--repo SPEC]... [--copy] [--no-setup] [--setup-timeout S] [--no-venv] [--dry-run]` | Refresh installed overlays from the repo recorded at install (`--repo` replaces it), checked as `add` checks a source: the install is made to hold exactly the overlay's members (new and changed files land, everything else goes, caches and setup output left inside it included); a file edited here is backed up first under `<store>/install_backup/<timestamp>/overlays/<name>/`. Installs new `requires`, then prepares each synced overlay in its `sync` phase (`install` when no preparation succeeded here yet), whether its digest changed or not: its environments rebuilt, then its setup. Exits 1 when an overlay's repo cannot be loaded, its source lies inside `<store>/overlays/` or is refused, or its dependencies or setup fail. |
|
|
53
53
|
| `overlays list [-g] [--repo SPEC]... [--json]` | Installed overlays per store (shadowed copies marked, unmet `requires` and refused overlays flagged) and what the repos offer. |
|
|
54
54
|
| `overlays show NAME [-g] [--repo SPEC]... [--json]` | One overlay: the copy a session would use, else the source's; its declarations, a refused manifest's problems, tree digest, pin, what this machine lacks for it, its weakenings, skills, member count, recorded source and commit. No name: exit 2. |
|
|
55
|
+
| `overlays hooks [NAME] [-g]` | An installed overlay's hooks (every overlay with some, without a name): per event the script and its settings, whether the hooks are allowed at the overlay's current digest, and per harness whether the event fires natively, is fired by dotagents itself (`pre-env`) or cannot fire. |
|
|
56
|
+
| `overlays allow-hooks NAME [--digest D] [--no-prompt] [-g]` | Show the same, then allow the overlay's hooks at digest `D`, which must be the current one: without `--digest` it prints the digest to pass and allows nothing (exit 0); a wrong digest exits 2. Then it asks at the terminal itself (`/dev/tty`, `CONIN$`): `allow NAME` to confirm; `--no-prompt` is for the user's own setup script. It refuses (exit 2) inside an agent harness's session and is not an MCP tool. The allowance is kept in the user store (`overlays/.dotagents-state/hooks-allowed.json`), one digest per overlay and store, and is only as strong as an agent's lack of write access to the user store. |
|
|
57
|
+
| `overlays revoke-hooks NAME [-g]` | Take back the allowance. |
|
|
58
|
+
| `hook EVENT --harness claude-code\|codex [--level user\|project] [--guard]` | Not for typing: what a harness's own hook entry runs (through `overlay_hook.py`). Reads the harness's payload on standard input, fires the overlays' hooks for the event (`user`: the user and system stores' overlays, `project`: the project store's), and answers in the harness's words; a hook's output never reaches the harness and the answer is never an approval. `--guard`: a failure of this program answers with a refusal where the event can be refused. Always exits 0 (2 for a bad argument). Not an MCP tool. |
|
|
55
59
|
| `context [OUT] [-g] [--agents a,b] [--format markdown\|system-reminder\|json] [--write-agent] [--inline]` | Print (or write to `OUT`) the assembled context. `--write-agent` merges it into each agent's `context_target` under the project root (not with `-g`). Exits 2 when `--agents` names no known agent. |
|
|
56
60
|
| `path [-g] [--lib] [--format list\|native\|posix\|json]` | Print the dirs `env` adds to `PATH` -- each store's `bin` and each active overlay's `PATH` additions (`--lib`: the existing agent library dirs, `PYTHONPATH`) -- highest precedence first; runs no env file. `posix` is `:`-joined, MSYS2 form on Windows. |
|
|
57
61
|
| `env [-g] [--format F] [--diff] [--cache] [--into FILE]` | Print the assembled environment (`--diff`: only what differs from the caller's; `--cache`: replay the previous output while `env_cache_key` holds, up to `ENV_CACHE_TTL`; `--into FILE`: write it into FILE as the one block this command owns there) in format `F`: `auto` (default: the calling shell), `export`/`posix`/`sh`/`bash`, `dotenv`/`env`, `powershell`/`pwsh`/`ps`, `cmd`/`bat`/`batch`, `fish`, `json`, `ini`, `yaml`. |
|
|
@@ -269,6 +273,24 @@ conformance vectors (`tests/aos_vectors/`, revision in `REVISION`).
|
|
|
269
273
|
`activates`; a failed sync keeps only the recorded digest active).
|
|
270
274
|
- Resolution: `class ResolveError(Exception)` (`chain`), `resolve(requires_of,
|
|
271
275
|
requested) -> (visiting order, missing)`, `order_key(priority, name, identity)`.
|
|
276
|
+
- Hooks (the hook vectors in `tests/aos_vectors/hooks*.json` pass): `HOOK_EVENTS`
|
|
277
|
+
(event → class, runs `hook.<ext>` by default, outputs, refusal, what `match` selects),
|
|
278
|
+
`SESSION_REASONS`, `TOOL_KINDS`; `[hooks]` and `[defaults] hooks` validate in
|
|
279
|
+
`validate`. `default_hook(files, os_name, interpreters)`, `hooks_of(manifest, files,
|
|
280
|
+
os_name, interpreters) -> (entries, script-not-run files)`, `hook_plan(manifest, facts)
|
|
281
|
+
-> {hooks, on, activates, needs, reports, non_defaults}` (`on` only while the digest is
|
|
282
|
+
the one allowed; `activates` false for a guard that is off), `hook_matches(event, match,
|
|
283
|
+
value)`, `hook_payload(event, known)` with `PAYLOAD_COMMON`, `PAYLOAD_KEYS`, `TOOL_KEYS`;
|
|
284
|
+
`read_hook_output(event, data, sets)` / `hook_result(event, run, limits)` raise
|
|
285
|
+
`HookFailure` (`.cause`: `not-run`, `payload-too-large`, `timeout`, `too-large`, `exit`,
|
|
286
|
+
`grant`, `output`) with `HOOK_OUTPUT_KEYS`, `MAX_HOOK_REASON`; `fire_event(event, runs,
|
|
287
|
+
applies, limits, windows) -> dict` combines hooks to the tightest (a run holding `start`, a
|
|
288
|
+
function, is started only when its turn comes); `apply_pre_env(env, identities, runs,
|
|
289
|
+
limits, reports, non_defaults)` applies `pre-env` output (raises `Refused("hook-refused")`);
|
|
290
|
+
`DEFAULT_HOOK_TIMEOUT = 30`, `DEFAULT_HOOK_LIMITS` (60 s a run, 120 s an event, 64 KiB
|
|
291
|
+
output and error, 1 MiB payload). `tree_entries(root, *, with_data=False)` hands back each
|
|
292
|
+
file's bytes (read once, at most `MAX_TREE_BYTES = 256 MiB`) instead of its digest, so a digest
|
|
293
|
+
and a copy can be made from the same bytes.
|
|
272
294
|
|
|
273
295
|
## `dotagents._activate`
|
|
274
296
|
|
|
@@ -342,6 +364,14 @@ started suspended until it is in the job) so nothing it started outlives it.
|
|
|
342
364
|
`run_step(argv, *, cwd, env, timeout, capture=False, max_output=1 MiB) ->
|
|
343
365
|
(status, stdout)`. `pycache_prefix(user_store) -> str` — where Python run for an
|
|
344
366
|
overlay keeps bytecode (`<user store>/.cache/pycache`), never inside an overlay.
|
|
367
|
+
`which_in(name, path) -> Optional[str]` finds a program on the absolute directories of `path`,
|
|
368
|
+
never the current directory (Windows searches it first with `shutil.which`); `interpreter_command(...,
|
|
369
|
+
trusted=False)` with `trusted` looks for a Windows bash only via `find_bash_on`.
|
|
370
|
+
`run_hook(argv, *, cwd, env, timeout, stdin, max_output, max_stderr) -> dict` runs a hook
|
|
371
|
+
bounded: `stdin` written without waiting on the child, stdout kept to `max_output` (a run
|
|
372
|
+
that writes more is stopped: `too_large`), stderr kept to `max_stderr` and the rest read and
|
|
373
|
+
dropped, ended at `timeout` (`timed_out`), every process it started ended with it; returns
|
|
374
|
+
`exit`, `stdout`, `stderr`, `timed_out`, `too_large`, `not_run`, `seconds`.
|
|
345
375
|
|
|
346
376
|
## `dotagents._resources`
|
|
347
377
|
|
|
@@ -364,13 +394,14 @@ The package's own data; library code, importing nothing from `dotagents.cli`.
|
|
|
364
394
|
install. The archive also holds the vendored duho and pathapi; `env`
|
|
365
395
|
exports it as `AGENTS_PYLIB`.
|
|
366
396
|
|
|
367
|
-
## Commands — `dotagents.cli.init`, `dotagents.cli.overlays`, `dotagents.cli.context`, `dotagents.cli.env`, `dotagents.cli.path`, `dotagents.cli.about`, `dotagents.cli.build_pyz`, `dotagents.cli.mcp`
|
|
397
|
+
## Commands — `dotagents.cli.init`, `dotagents.cli.overlays`, `dotagents.cli.context`, `dotagents.cli.env`, `dotagents.cli.path`, `dotagents.cli.about`, `dotagents.cli.build_pyz`, `dotagents.cli.mcp`, `dotagents.cli.hook`
|
|
368
398
|
|
|
369
399
|
One `duho` command class per command above; fields are the flags, `__call__()`
|
|
370
400
|
returns the exit code.
|
|
371
401
|
|
|
372
402
|
- `init.Init`; `overlays.Overlays` (umbrella) with `OverlayAdd`, `OverlayRemove`,
|
|
373
|
-
`OverlayList`, `OverlaySync`, `OverlayShow
|
|
403
|
+
`OverlayList`, `OverlaySync`, `OverlayShow`, `OverlayHooks`, `OverlayAllowHooks`,
|
|
404
|
+
`OverlayRevokeHooks`; `hook.Hook`; `context.Context` (`FORMATS =
|
|
374
405
|
("markdown", "system-reminder", "json")`); `env.Env`; `path.PathCmd`
|
|
375
406
|
(`PATH_FORMATS = ("list", "native", "posix", "json")`); `build_pyz.BuildPyz`;
|
|
376
407
|
`about.About`.
|
|
@@ -724,6 +755,9 @@ API:
|
|
|
724
755
|
included, from before/after snapshots taken in one bash process (MSYS2 `*PATH`
|
|
725
756
|
values converted back to Windows form). No bash, a failed `source` or an `exit`
|
|
726
757
|
in the file → empty, with a warning.
|
|
758
|
+
- `find_bash_on(path) -> Optional[str]` — a Windows bash for a hook, looked for only beside a `git` on `path`,
|
|
759
|
+
in the standard install dirs and as a `bash` on `path` (never `os.environ["PATH"]`, never the
|
|
760
|
+
working directory), proved MSYS2/Cygwin, not cached.
|
|
727
761
|
- `find_bash(logger=None) -> Optional[str]` — the bash for plain env files, resolved
|
|
728
762
|
once per process: `bash` on `PATH` on POSIX; on Windows Git's bash (beside `git`
|
|
729
763
|
on `PATH`, then the standard install dirs, then `PATH`), accepted only if it is
|
|
@@ -879,6 +913,15 @@ API:
|
|
|
879
913
|
derivable, for the runtime agent only (`{}` in a plain shell); never replaces a
|
|
880
914
|
value in `environ` unless `explicit` names a known agent. `root` is unused.
|
|
881
915
|
|
|
916
|
+
- `Agent.wire_overlay_hooks(dest, *, dry_run, logger, config_root=None)` — wire the
|
|
917
|
+
harness's own hooks to `dotagents hook` for the events the overlays installed in the
|
|
918
|
+
store `dest` have a hook for, and retract an entry no overlay uses any more; a no-op on
|
|
919
|
+
the base class. `ClaudeAgent` writes `settings.json` (user) or `settings.local.json`
|
|
920
|
+
(project) with a bash entry per event and, on Windows, a PowerShell one that selects itself
|
|
921
|
+
when there is no Git Bash; `CodexAgent` writes the user `hooks.json` (with
|
|
922
|
+
`commandWindows`). `init -g` and `overlays add/sync/remove` call it; the entries are
|
|
923
|
+
labelled `dotagents: Overlay hooks: <native event>` and run `overlay_hook.py`.
|
|
924
|
+
|
|
882
925
|
## `dotagents._merge`
|
|
883
926
|
|
|
884
927
|
Marker lines (a marker counts only on a line of its own, outside fenced code):
|
|
@@ -911,6 +954,109 @@ Marker lines (a marker counts only on a line of its own, outside fenced code):
|
|
|
911
954
|
- `timestamped_backup_root(dest) -> Path` — `<dest>/install_backup/<timestamp>`, one
|
|
912
955
|
per store per process.
|
|
913
956
|
|
|
957
|
+
## `dotagents._overlay_hooks`
|
|
958
|
+
|
|
959
|
+
Firing overlay hooks (the model is `dotagents._aos`; the user-facing page is
|
|
960
|
+
`docs/guide/overlay-hooks.md`). A hook is the overlay's code: it runs only while its
|
|
961
|
+
overlay's tree digest is the one the user allowed, as an argv list with the overlay root as
|
|
962
|
+
its directory, bounded by `LIMITS`.
|
|
963
|
+
|
|
964
|
+
- `LIMITS` (`DEFAULT_HOOK_LIMITS`, a copy tests may change), `NO_HARNESS = "dotagents"`,
|
|
965
|
+
`class Harness` (`id`, `native`: AOS event → native name, `applies`: event → outputs
|
|
966
|
+
dotagents can apply, `reasons`, `fires`), `CLAUDE`, `CODEX`, `ANYWHERE` (no
|
|
967
|
+
harness known: nothing reported as unfireable), `HARNESSES`, `harness_of(ident) -> Harness`
|
|
968
|
+
(an unknown id fires only `pre-env`), `partial_events(harness)`.
|
|
969
|
+
- Settings, read from the user's own store only (never an overlay or a project):
|
|
970
|
+
`SETTING_BOUNDS` (`timeout`, `event_timeout`, `deadline`, `max_output`, `max_payload`, each
|
|
971
|
+
with its bounds), `settings_path(user_store)` (`overlays/.dotagents-state/hooks-settings.json`),
|
|
972
|
+
`read_settings`, `limits_for(user_store)` (`LIMITS` under them), `deadline_for` (default
|
|
973
|
+
`DEFAULT_DEADLINE = 150` s for everything a hook entry does, from the process's start),
|
|
974
|
+
`entry_timeout(user_store)` (the harness's own timeout for the entry: deadline + 20 s, at
|
|
975
|
+
most `ENTRY_TIMEOUT_CAP = 600`).
|
|
976
|
+
- Allowances, in the user store only, keyed by the overlay's store and name:
|
|
977
|
+
`allowances_path(user_store)`, `read_allowances`, `allowed_digest(user_store, identity,
|
|
978
|
+
store=None)`, `allow(user_store, identity, digest, store=None)`, `revoke(...) -> bool`.
|
|
979
|
+
- `install_notice(overlay, user_store) -> Optional[str]` — the line `overlays add/sync` logs
|
|
980
|
+
when an overlay has hooks that are off;
|
|
981
|
+
- `read_tree(overlay, keep=False) -> (digest, files)` (the bytes the digest covers when `keep`),
|
|
982
|
+
`snapshot(user_store, overlay, digest, files) -> Path` — the copy a hook runs from, one directory
|
|
983
|
+
per digest under `<user store>/.cache/hook-snapshots/` made from exactly the bytes hashed (what
|
|
984
|
+
`AOS` excludes from a digest, `node_modules` and `__pycache__` included, is not in it) and
|
|
985
|
+
hashed again before an existing one is reused; others go after `SNAPSHOT_MAX_AGE` (7 days) unused;
|
|
986
|
+
`SnapshotOverlay(path, live)` — an overlay read from its snapshot (manifest, files, paths,
|
|
987
|
+
scripts), whose `state_dir`, `preparation_path` and `install_record_path` are the live overlay's;
|
|
988
|
+
`overlay_plan(..., keep_tree=True)` makes the snapshot and plans from its manifest
|
|
989
|
+
(`plan["snap"]`), so env scripts, paths and the cwd are the hashed bytes; `PYTHON_BOOT` runs a
|
|
990
|
+
Python hook under `-I` with the libraries explicit; `project_roots(environ, cwd)` — where the agent
|
|
991
|
+
works, for `clean_path(path, user_store, roots)`; `fresh_digest(overlay)` (never memoized),
|
|
992
|
+
`overlay_plan(overlay, harness, user_store) ->
|
|
993
|
+
plan` (`hook_plan` plus `digest`, `identity`), `declares_guard(overlay)`,
|
|
994
|
+
`refusal(overlay) -> Optional[str]` (a guard that is off keeps its overlay out of
|
|
995
|
+
`_activate.active`), `planned(overlays, harness, user_store)`, `has_live_pre_env(scope,
|
|
996
|
+
harness_id)` (`env --cache` is not replayed for it).
|
|
997
|
+
- `class Subject`, `execute(subject, event, payload, env, *, limits=None) -> run`,
|
|
998
|
+
`printable(data, bound=4096)` (stderr made fit for a log), `class Fired` (`result`, `reports`,
|
|
999
|
+
`errors`: what each hook wrote to standard error, for the log, `decision`), `session_id(harness, native_id)` (a hash,
|
|
1000
|
+
never the harness's id), `value_for_match`, `fire(event, facts, plans, harness, *,
|
|
1001
|
+
environment, limits=None) -> Fired`, `unsupported_reports(plans)`, `describe(report)`,
|
|
1002
|
+
`pre_env(plans, session, base, harness, *, cwd=None) -> (reports, non_defaults)`.
|
|
1003
|
+
- Wiring: `STATUS_PREFIX`, `FORWARDER = "overlay_hook.py"`, `wanted_events(store, harness) ->
|
|
1004
|
+
{event: guarded}`, `level_token(dest, user_store)` (`user` or `project-<hash>`: the only thing
|
|
1005
|
+
in an entry's text besides the harness and the event), `sh_quote` / `ps_quote` (single-quote a
|
|
1006
|
+
path for bash / PowerShell), `code_root()`, `forwarder_record(user_store, guards)`,
|
|
1007
|
+
`record_path`, `write_record`, `remove_record`, `interpreter_problem(config_dir, token)` (the
|
|
1008
|
+
recorded interpreter or dotagents is gone). The forwarder and its record live in the user's
|
|
1009
|
+
harness config directory only; the entry runs `<python> -I <forwarder> <harness> <event>
|
|
1010
|
+
<token>`.
|
|
1011
|
+
- `TOKEN_FORM` / `valid_token(token)` — a store token is `user` or `project-` and ten hex digits,
|
|
1012
|
+
checked before it is part of a path; `clean_path(path, user_store)` — a `PATH` without relative
|
|
1013
|
+
entries, any store's or overlay's `bin` or a venv, which is where hook and env-script
|
|
1014
|
+
interpreters (`pwsh`, `node`) are found; `shadowed_with_hooks(scope)` — the user's overlays
|
|
1015
|
+
with a hook that a nearer store's same-named folder hides (they still fire at `pre-env`).
|
|
1016
|
+
- Sessions: `KEEP_PROJECT` (the project's own names, `AGENTS_PROJECT_ROOT` and
|
|
1017
|
+
`CLAUDE_PROJECT_DIR`, that `session_base` leaves alone), `session_base(environ)` — the environment a hook starts from: what `dotagents
|
|
1018
|
+
launch` recorded in `AGENTS_SESSION_BASE` (each changed name's earlier value, null if unset)
|
|
1019
|
+
undone, and the harness's own variables (`CLAUDE_ENV_FILE`, `CLAUDE_CODE_*`, `CODEX_*`)
|
|
1020
|
+
dropped; `log_path`, `log_errors(user_store, errors)` and `LOG_LIMIT` — a hook's standard
|
|
1021
|
+
error goes to `<user store>/.cache/hooks.log`, never to the harness.
|
|
1022
|
+
|
|
1023
|
+
## `dotagents._hook_main`
|
|
1024
|
+
|
|
1025
|
+
The fixed entry the hook forwarder runs, in place of the `dotagents` command line: it discovers
|
|
1026
|
+
no command and adds no library (discovery reads what the project's `.agents` holds). `main(argv)`
|
|
1027
|
+
takes `EVENT --harness H --level user|project --agents-dir DIR --nonce N`, builds the scope
|
|
1028
|
+
(:func:`dotagents._hook_dispatch.scope_for`) and prints one JSON object, `{"nonce": N,
|
|
1029
|
+
"answer": <text for the harness>}`; the forwarder accepts nothing that does not carry its nonce.
|
|
1030
|
+
|
|
1031
|
+
## `dotagents._hook_dispatch`
|
|
1032
|
+
|
|
1033
|
+
The program a harness's own hook runs (`dotagents hook`). Nothing a hook printed reaches the
|
|
1034
|
+
harness: the answer is built from the output that was accepted, and can only deny, ask or
|
|
1035
|
+
block.
|
|
1036
|
+
|
|
1037
|
+
- `scope_for(level, user_store)` — the scope a hook of `level` is looked for in: `user` has no
|
|
1038
|
+
project store in it (nothing a project holds can shadow a user hook, add commands or
|
|
1039
|
+
libraries, or choose the Python); `dispatch(..., shadow_scope=None)` reports, at session start,
|
|
1040
|
+
the user hooks a project folder of the same name hides (`hook-shadowed`).
|
|
1041
|
+
- `MAX_NATIVE`, `tool_kind(harness, name) -> kind` (a tool not placed is `other`, which runs
|
|
1042
|
+
every hook of its event), `facts_of(harness, event, native) -> known`, `context_text(parts)`
|
|
1043
|
+
(each part inside a marker with a nonce chosen after the hooks ran),
|
|
1044
|
+
`write_env_block(path, label, changes)` (single-quoted values of validated names into
|
|
1045
|
+
Claude's env file; the file is split at line feeds only and a value holding any of
|
|
1046
|
+
`LINE_SEPARATORS` is refused, `env_file_safe(changes)`), `hook_environment(scope, overlay,
|
|
1047
|
+
environ)` (the overlay's process environment from `session_base`, with the user store's Python,
|
|
1048
|
+
never a project's venv), `answer(harness, event, fired, env_file) -> (stdout, stderr)`,
|
|
1049
|
+
`refusal_answer(harness, event, why)`, `plans_for(scope, harness, level, event=None)`,
|
|
1050
|
+
`dispatch(harness_id, event, stdin, scope, level, environ) ->
|
|
1051
|
+
(stdout, stderr)`.
|
|
1052
|
+
|
|
1053
|
+
## `dotagents.cli.hook`
|
|
1054
|
+
|
|
1055
|
+
`Hook` — the `hook` command (see the command table). Packaged beside the harness's hook
|
|
1056
|
+
configuration by `init`: `_overlay/dotagents/hooks/overlay_hook.py`, the forwarder that finds
|
|
1057
|
+
`dotagents`, runs `dotagents hook` with the payload, and answers a refusal itself when it
|
|
1058
|
+
cannot (`--guard`).
|
|
1059
|
+
|
|
914
1060
|
## `dotagents._hooks`
|
|
915
1061
|
|
|
916
1062
|
Settings schema (Claude Code and Codex): `hooks.<Event>` is a list of matcher
|
|
@@ -919,10 +1065,10 @@ objects `{"matcher"?: str, "hooks": [{"type": "command", "command": str, ...}]}`
|
|
|
919
1065
|
- `STATUS_PREFIX = "dotagents: "` — our hooks' `statusMessage` is
|
|
920
1066
|
`STATUS_PREFIX + label`; that label is the hook's identity.
|
|
921
1067
|
- `build_hook_entry(command, *, matcher=None, status_message=None, shell=None,
|
|
922
|
-
command_windows=None) -> dict` — one matcher object; `None` keys omitted;
|
|
1068
|
+
command_windows=None, timeout=None) -> dict` — one matcher object; `None` keys omitted;
|
|
923
1069
|
`command_windows` is written as `commandWindows`.
|
|
924
1070
|
- `merge_hook(existing, command, *, matcher=None, status_message=None, shell=None,
|
|
925
|
-
command_windows=None) -> tuple[list, bool]` — `(entries, changed)`. An identical
|
|
1071
|
+
command_windows=None, timeout=None) -> tuple[list, bool]` — `(entries, changed)`. An identical
|
|
926
1072
|
entry is kept; ours in another shape (same status label, or the bare label on a
|
|
927
1073
|
command running dotagents) is refreshed in place, keeping keys the user added;
|
|
928
1074
|
duplicates dropped; foreign and malformed entries kept verbatim; a foreign hook
|
|
@@ -111,6 +111,11 @@ def refusal(overlay: Any, *, prepared: bool = True) -> "Optional[str]":
|
|
|
111
111
|
foreign = _aos.foreign_entries(overlay.path)
|
|
112
112
|
if foreign:
|
|
113
113
|
return "its copy holds %s, which is not part of the overlay" % ", ".join(foreign[:5])
|
|
114
|
+
from dotagents import _overlay_hooks
|
|
115
|
+
|
|
116
|
+
held = _overlay_hooks.refusal(overlay)
|
|
117
|
+
if held is not None:
|
|
118
|
+
return held
|
|
114
119
|
try:
|
|
115
120
|
needs = needs_preparation(overlay)
|
|
116
121
|
except _aos.Refused as refused:
|