agentbundle 0.12.1__tar.gz → 0.17.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. {agentbundle-0.12.1 → agentbundle-0.17.0}/PKG-INFO +118 -2
  2. {agentbundle-0.12.1 → agentbundle-0.17.0}/README.md +115 -1
  3. agentbundle-0.17.0/agentbundle/_data/catalogue.schema.json +170 -0
  4. agentbundle-0.17.0/agentbundle/_data/install-defaults.toml +9 -0
  5. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/__init__.py +43 -4
  6. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/adapters/claude_code.py +16 -10
  7. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/adapters/codex.py +20 -15
  8. agentbundle-0.17.0/agentbundle/build/recipes/composite-agents-md.toml +8 -0
  9. agentbundle-0.17.0/agentbundle/build/recipes/composite-marketplace.toml +9 -0
  10. agentbundle-0.17.0/agentbundle/build/recipes/marketplace.toml +8 -0
  11. agentbundle-0.17.0/agentbundle/build/recipes/per-pack-apm-package.toml +7 -0
  12. agentbundle-0.17.0/agentbundle/build/recipes/per-pack-claude-plugin.toml +7 -0
  13. agentbundle-0.17.0/agentbundle/build/recipes/per-pack-overlay.toml +7 -0
  14. agentbundle-0.17.0/agentbundle/build/recipes/self-host.toml +54 -0
  15. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_security.py +10 -7
  16. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_shipped_packs_v08_declarations.py +1 -0
  17. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/catalogue.py +13 -0
  18. agentbundle-0.17.0/agentbundle/catalogue_tooling/__init__.py +12 -0
  19. agentbundle-0.17.0/agentbundle/catalogue_tooling/archive.py +357 -0
  20. agentbundle-0.17.0/agentbundle/catalogue_tooling/build.py +125 -0
  21. agentbundle-0.17.0/agentbundle/catalogue_tooling/config.py +459 -0
  22. agentbundle-0.17.0/agentbundle/catalogue_tooling/defaults.py +199 -0
  23. agentbundle-0.17.0/agentbundle/catalogue_tooling/diagnostics.py +38 -0
  24. agentbundle-0.17.0/agentbundle/catalogue_tooling/lint.py +648 -0
  25. agentbundle-0.17.0/agentbundle/catalogue_tooling/package.py +623 -0
  26. agentbundle-0.17.0/agentbundle/catalogue_tooling/results.py +69 -0
  27. agentbundle-0.17.0/agentbundle/catalogue_tooling/self_host.py +73 -0
  28. agentbundle-0.17.0/agentbundle/catalogue_tooling/skill_spec_lint.py +665 -0
  29. agentbundle-0.17.0/agentbundle/catalogue_tooling/verify.py +440 -0
  30. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/cli.py +237 -10
  31. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/adapt.py +1 -1
  32. agentbundle-0.17.0/agentbundle/commands/catalogue_build.py +46 -0
  33. agentbundle-0.17.0/agentbundle/commands/catalogue_lint.py +34 -0
  34. agentbundle-0.17.0/agentbundle/commands/catalogue_package.py +47 -0
  35. agentbundle-0.17.0/agentbundle/commands/catalogue_self_host.py +48 -0
  36. agentbundle-0.17.0/agentbundle/commands/catalogue_sync_defaults.py +43 -0
  37. agentbundle-0.17.0/agentbundle/commands/catalogue_tooling_stub.py +22 -0
  38. agentbundle-0.17.0/agentbundle/commands/catalogue_verify.py +47 -0
  39. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/install.py +124 -1
  40. agentbundle-0.17.0/agentbundle/commands/list_installed.py +540 -0
  41. agentbundle-0.17.0/agentbundle/commands/pack_evals.py +1242 -0
  42. agentbundle-0.17.0/agentbundle/commands/package_catalogue.py +66 -0
  43. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/upgrade.py +851 -194
  44. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/config.py +88 -5
  45. agentbundle-0.17.0/agentbundle/https_catalogue.py +518 -0
  46. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/scope.py +10 -0
  47. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/source_defaults.py +271 -20
  48. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/version.py +1 -1
  49. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle.egg-info/PKG-INFO +118 -2
  50. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle.egg-info/SOURCES.txt +31 -0
  51. agentbundle-0.17.0/agentbundle.egg-info/requires.txt +3 -0
  52. {agentbundle-0.12.1 → agentbundle-0.17.0}/pyproject.toml +5 -2
  53. {agentbundle-0.12.1 → agentbundle-0.17.0}/tests/test_linear_primitive.py +2 -1
  54. agentbundle-0.12.1/agentbundle/_data/install-defaults.toml +0 -10
  55. agentbundle-0.12.1/agentbundle/commands/list_installed.py +0 -223
  56. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/__init__.py +0 -0
  57. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/__main__.py +0 -0
  58. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/_data/adapter.schema.json +0 -0
  59. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/_data/adapter.toml +0 -0
  60. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/_data/install-marker.py +0 -0
  61. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/_data/pack.schema.json +0 -0
  62. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/_data/plugin-manifest.derived.schema.json +0 -0
  63. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/_data/plugin-manifest.schema.json +0 -0
  64. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/_data/profile.schema.json +0 -0
  65. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/__main__.py +0 -0
  66. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/adapter_root_bins.py +0 -0
  67. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/adapters/__init__.py +0 -0
  68. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/adapters/copilot.py +0 -0
  69. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/adapters/cursor.py +0 -0
  70. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/adapters/gemini.py +0 -0
  71. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/adapters/kiro.py +0 -0
  72. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/adapters/kiro_cli.py +0 -0
  73. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/adapters/kiro_ide.py +0 -0
  74. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/contract.py +0 -0
  75. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/lint_packs.py +0 -0
  76. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/main.py +0 -0
  77. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/phase_order.py +0 -0
  78. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/projections/__init__.py +0 -0
  79. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/projections/codex_agent_toml.py +0 -0
  80. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/projections/copilot_agent_md.py +0 -0
  81. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/projections/copilot_hooks_json.py +0 -0
  82. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/projections/direct_directory.py +0 -0
  83. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/projections/gemini_command_toml.py +0 -0
  84. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/projections/hook_id.py +0 -0
  85. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/projections/kiro_ide_hook.py +0 -0
  86. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/projections/merge_into_agent_json.py +0 -0
  87. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/projections/merge_json.py +0 -0
  88. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/projections/user_merge_json.py +0 -0
  89. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/scope_rails.py +0 -0
  90. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/self_host.py +0 -0
  91. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/shared_libs.py +0 -0
  92. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/target_resolver.py +0 -0
  93. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/__init__.py +0 -0
  94. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_adapter_claude_code.py +0 -0
  95. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_adapter_codex.py +0 -0
  96. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_adapter_copilot.py +0 -0
  97. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_adapter_cursor.py +0 -0
  98. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_adapter_gemini.py +0 -0
  99. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_adapter_kiro.py +0 -0
  100. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_adapter_kiro_alias.py +0 -0
  101. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_adapter_kiro_cli.py +0 -0
  102. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_adapter_kiro_ide.py +0 -0
  103. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_adapter_root_bins_projection.py +0 -0
  104. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_architect_design_reviewer_projection.py +0 -0
  105. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_architect_design_reviewer_rubric_parity.py +0 -0
  106. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_build_ships_seeds.py +0 -0
  107. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_contract.py +0 -0
  108. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_contract_scope.py +0 -0
  109. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_contract_v07.py +0 -0
  110. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_contract_v08.py +0 -0
  111. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_direct_directory_cleanup.py +0 -0
  112. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_end_to_end_build.py +0 -0
  113. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_lint_agents_md_diataxis_block.py +0 -0
  114. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_lint_agents_md_legacy_block.py +0 -0
  115. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_lint_agents_md_risk_block.py +0 -0
  116. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_lint_packs.py +0 -0
  117. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +0 -0
  118. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_pack_schema.py +0 -0
  119. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_pack_schema_allowed_adapters.py +0 -0
  120. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_pack_schema_install.py +0 -0
  121. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_pipeline.py +0 -0
  122. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_plugin_manifest_schema.py +0 -0
  123. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_projectable_subset.py +0 -0
  124. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_projections_merge_json.py +0 -0
  125. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_scope_rails.py +0 -0
  126. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_self_host_check.py +0 -0
  127. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_self_host_fixture_guard.py +0 -0
  128. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_self_host_recipe_config.py +0 -0
  129. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_shared_libs_projection.py +0 -0
  130. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_shared_prefix_contract.py +0 -0
  131. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_shipped_packs_v07_declarations.py +0 -0
  132. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_user_libs_projection.py +0 -0
  133. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_validate.py +0 -0
  134. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/tests/test_writers_emit_lf.py +0 -0
  135. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/user_libs.py +0 -0
  136. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/build/validate.py +0 -0
  137. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/categories.py +0 -0
  138. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/__init__.py +0 -0
  139. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/_common.py +0 -0
  140. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/_drop_warning.py +0 -0
  141. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/config.py +0 -0
  142. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/diff.py +0 -0
  143. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/init_state.py +0 -0
  144. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/list_packs.py +0 -0
  145. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/list_profiles.py +0 -0
  146. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/list_targets.py +0 -0
  147. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/profile.py +0 -0
  148. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/reconcile.py +0 -0
  149. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/render.py +0 -0
  150. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/scaffold.py +0 -0
  151. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/show.py +0 -0
  152. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/uninstall.py +0 -0
  153. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/commands/validate.py +0 -0
  154. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/pack_inventory.py +0 -0
  155. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/render.py +0 -0
  156. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/safety.py +0 -0
  157. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/statelock.py +0 -0
  158. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle/user_config.py +0 -0
  159. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle.egg-info/dependency_links.txt +0 -0
  160. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle.egg-info/entry_points.txt +0 -0
  161. {agentbundle-0.12.1 → agentbundle-0.17.0}/agentbundle.egg-info/top_level.txt +0 -0
  162. {agentbundle-0.12.1 → agentbundle-0.17.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentbundle
3
- Version: 0.12.1
3
+ Version: 0.17.0
4
4
  Summary: npm for your coding agent. Install packs of skills, subagents, and hooks into any repo, for every major agent.
5
5
  Author-email: eugenelim <eugenelim@users.noreply.github.com>
6
6
  License: Apache-2.0 OR MIT
@@ -15,6 +15,8 @@ Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Requires-Python: >=3.11
17
17
  Description-Content-Type: text/markdown
18
+ Provides-Extra: lint
19
+ Requires-Dist: pyyaml>=6.0; extra == "lint"
18
20
 
19
21
  # agentbundle
20
22
 
@@ -64,6 +66,8 @@ agentbundle show core --format json # stable object for scripts/agents
64
66
  agentbundle list-installed
65
67
  agentbundle list-installed --no-check # skip the catalogue check (offline, fast)
66
68
  agentbundle list-installed --check-drift # also count locally edited files
69
+ agentbundle list-installed --format json # machine-readable JSON (schema_version 1)
70
+ agentbundle list-installed --updates-only # show only rows needing attention
67
71
 
68
72
  # Install a whole curated profile — a single-scope set of packs — in one command
69
73
  agentbundle install --profile inception
@@ -80,7 +84,7 @@ agentbundle uninstall --pack core --dry-run
80
84
  agentbundle uninstall --pack core --yes
81
85
  ```
82
86
 
83
- **`list-installed`** reads your state files (not the catalogue) and reports every installed `(pack, adapter)` at each scope with its version and an `up-to-date` / `upgrade-available` / `unknown` status; it degrades to `unknown` (never an error) when the catalogue can't be resolved, and `--no-check` skips the check entirely.
87
+ **`list-installed`** reads your state files (not the catalogue) and reports every installed `(pack, adapter)` at each scope with its version and a four-value status — `up-to-date`, `upgrade-available`, `ahead` (installed version is newer than catalogue), or `unknown`; it degrades to `unknown` (never an error) when the catalogue can't be resolved, and `--no-check` skips the check entirely. `--format json` emits a stable JSON contract (`schema_version: 1`) to stdout — useful for CI automation of upgrade decisions. `--updates-only` hides `up-to-date` rows.
84
88
 
85
89
  **`show <pack>`** answers "what skills and agents does this pack contain?" by walking the pack's source tree live on each call — so the answer can't drift, and nothing is persisted. `--format json` emits a stable object (`name`, `version`, `description`, `skills`, `agents`, `source`) for scripts and agents. When the catalogue can't be resolved, an *installed* pack still reports its inventory from your state files (marked `source: installed-state`); a not-installed pack errors.
86
90
 
@@ -88,6 +92,90 @@ A **profile** is a catalogue-curated, single-scope set of packs you install in o
88
92
 
89
93
  **Mutating commands ask first.** `uninstall`, the `--force` cleanup, and the upgrade offer all preview what they'll do and confirm before touching anything; `--dry-run` previews without writing, and `--yes` skips the prompt for non-interactive / CI use (where, without it, they refuse rather than hang).
90
94
 
95
+ ## Enterprise distribution
96
+
97
+ For organizations running an internal Artifactory mirror or any static HTTPS server,
98
+ agentbundle's enterprise distribution capabilities handle the full adoption loop —
99
+ from org-wide channel configuration to CI-driven bulk upgrades.
100
+
101
+ **Install from an internal Artifactory channel:**
102
+
103
+ ```bash
104
+ # Point agentbundle at your org's channel descriptor (one-time per machine,
105
+ # or pre-configured in your org fork — see Org bootstrap below)
106
+ agentbundle config set source catalogue+https://artifactory.example.test/agentbundle/catalogues/core/channels/stable.json
107
+
108
+ agentbundle install --pack core
109
+ ```
110
+
111
+ The channel descriptor points to an immutable versioned archive; agentbundle
112
+ fetches, verifies its SHA-256 digest, and installs. Pass a bearer token via
113
+ `AGENTBUNDLE_HTTP_BEARER_TOKEN` — it is never stored in state, never printed, and
114
+ never forwarded to a different host.
115
+
116
+ **JSON output for CI pipelines:**
117
+
118
+ ```bash
119
+ # See what's installed and what needs upgrading — machine-readable
120
+ agentbundle list-installed --format json
121
+ agentbundle list-installed --format json --updates-only
122
+ ```
123
+
124
+ Returns a stable JSON contract (`schema_version` 1) with per-row status
125
+ (`up-to-date` / `upgrade-available` / `ahead` / `unknown`) and machine-readable
126
+ reason codes for unknown rows. Pipe into `jq` or your CI annotation step.
127
+
128
+ **Bulk upgrade in one scoped command:**
129
+
130
+ ```bash
131
+ # Upgrade all installed packs in a scope — preflights before any write
132
+ agentbundle upgrade --all --scope repo --yes
133
+ agentbundle upgrade --all --scope user --format json --yes
134
+ ```
135
+
136
+ Preflights all rows before writing anything; a blocked row stops the run before the
137
+ filesystem is touched. Partial failure is reported honestly — not described as a
138
+ rollback. Never silently downgrades an `ahead` row.
139
+
140
+ **Package your catalogue for Artifactory:**
141
+
142
+ ```bash
143
+ agentbundle package-catalogue \
144
+ --root /path/to/catalogue \
145
+ --bundle my-packs \
146
+ --release 1.0.0 \
147
+ --channel stable \
148
+ --output dist/
149
+ ```
150
+
151
+ Produces a deterministic, reproducible gzip archive (versioned) and a mutable channel
152
+ descriptor JSON (`stable.json`), ready to upload to Artifactory. Identical inputs
153
+ produce byte-identical archives (honors `SOURCE_DATE_EPOCH`).
154
+
155
+ **Org bootstrap — ship the default channel in your fork:**
156
+
157
+ Add an `[organization.artifactory]` block to
158
+ `agentbundle/_data/install-defaults.toml` in your org's agentbundle fork:
159
+
160
+ ```toml
161
+ [organization.artifactory]
162
+ enabled = true
163
+ base-url = "https://artifactory.example.test"
164
+ repository = "agentbundle"
165
+ bundle = "core"
166
+ channel = "stable"
167
+ ```
168
+
169
+ Developers installing from your fork get the internal channel without a manual
170
+ `config set source` step. The block ships `enabled = false` in the public package.
171
+ A malformed `enabled = true` config fails closed — no silent fallback to the public
172
+ source.
173
+
174
+ See the full enterprise adoption guide at
175
+ `docs/guides/_shared/how-to/use-an-artifactory-catalogue.md` for all six flows
176
+ (org bootstrap, repo-scope CI upgrade, user-scope MDM, source-conflict remediation,
177
+ disconnected hosts, and security controls).
178
+
91
179
  ## Build your own catalogue
92
180
 
93
181
  `agentbundle` isn't tied to the agent-ready-repo catalogue. Any repo that lays its packs out the same way can use it. A pack is a directory:
@@ -120,6 +208,34 @@ is deliberately lossy per tool.
120
208
 
121
209
  Point a catalogue URI (a git URL or a local path) at the repo that holds your packs. Then `validate` a pack against the adapter contract, `render` it to preview the projection, and `install` it into a target repo. `scaffold` drops a pack's seeds into a fresh directory to start from. The build pipeline (`agentbundle.build`) is the same engine `make build` runs.
122
210
 
211
+ **Org adapter default:** If your org ships a private `agentbundle` wheel (or a fork pinned to your internal catalogue), you can set a default adapter for all developers without requiring them to run `agentbundle config set` or pass `--adapter` on every install. Add an `[organization]` table to `_data/install-defaults.toml` in your fork:
212
+
213
+ ```toml
214
+ [organization]
215
+ preferred_adapter = "cursor"
216
+ ```
217
+
218
+ The org hint fires after the user-config but before the on-disk IDE probe — so `--adapter`, user-config, and upgrade state-hints all take priority. An invalid value exits 1 before writing anything. See the [`agentbundle` reference](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/guides/_shared/reference/agentbundle.md#org-adapter-default) for the full cascade.
219
+
220
+ **Lint your catalogue** — shallow structural checks run without extra dependencies:
221
+
222
+ ```bash
223
+ agentbundle catalogue lint --root .
224
+ ```
225
+
226
+ For full [agentskills.io spec](https://agentskills.io/specification) compliance (frontmatter key set, description policy, encoding, evals schema), install the `lint` extra and run with `--deep`:
227
+
228
+ ```bash
229
+ pip install 'agentbundle[lint]'
230
+ agentbundle catalogue lint --root . --deep
231
+ ```
232
+
233
+ **Run Tier-A activation evals** to measure whether each covered skill fires on the prompts it should:
234
+
235
+ ```bash
236
+ agentbundle pack evals run --pack <pack-name> --catalogue-root .
237
+ ```
238
+
123
239
  See the [pack layout reference](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/architecture/pack-layout.md) and [authoring a skill](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/guides/_shared/how-to/author-a-skill.md).
124
240
 
125
241
  ## Credentials
@@ -46,6 +46,8 @@ agentbundle show core --format json # stable object for scripts/agents
46
46
  agentbundle list-installed
47
47
  agentbundle list-installed --no-check # skip the catalogue check (offline, fast)
48
48
  agentbundle list-installed --check-drift # also count locally edited files
49
+ agentbundle list-installed --format json # machine-readable JSON (schema_version 1)
50
+ agentbundle list-installed --updates-only # show only rows needing attention
49
51
 
50
52
  # Install a whole curated profile — a single-scope set of packs — in one command
51
53
  agentbundle install --profile inception
@@ -62,7 +64,7 @@ agentbundle uninstall --pack core --dry-run
62
64
  agentbundle uninstall --pack core --yes
63
65
  ```
64
66
 
65
- **`list-installed`** reads your state files (not the catalogue) and reports every installed `(pack, adapter)` at each scope with its version and an `up-to-date` / `upgrade-available` / `unknown` status; it degrades to `unknown` (never an error) when the catalogue can't be resolved, and `--no-check` skips the check entirely.
67
+ **`list-installed`** reads your state files (not the catalogue) and reports every installed `(pack, adapter)` at each scope with its version and a four-value status — `up-to-date`, `upgrade-available`, `ahead` (installed version is newer than catalogue), or `unknown`; it degrades to `unknown` (never an error) when the catalogue can't be resolved, and `--no-check` skips the check entirely. `--format json` emits a stable JSON contract (`schema_version: 1`) to stdout — useful for CI automation of upgrade decisions. `--updates-only` hides `up-to-date` rows.
66
68
 
67
69
  **`show <pack>`** answers "what skills and agents does this pack contain?" by walking the pack's source tree live on each call — so the answer can't drift, and nothing is persisted. `--format json` emits a stable object (`name`, `version`, `description`, `skills`, `agents`, `source`) for scripts and agents. When the catalogue can't be resolved, an *installed* pack still reports its inventory from your state files (marked `source: installed-state`); a not-installed pack errors.
68
70
 
@@ -70,6 +72,90 @@ A **profile** is a catalogue-curated, single-scope set of packs you install in o
70
72
 
71
73
  **Mutating commands ask first.** `uninstall`, the `--force` cleanup, and the upgrade offer all preview what they'll do and confirm before touching anything; `--dry-run` previews without writing, and `--yes` skips the prompt for non-interactive / CI use (where, without it, they refuse rather than hang).
72
74
 
75
+ ## Enterprise distribution
76
+
77
+ For organizations running an internal Artifactory mirror or any static HTTPS server,
78
+ agentbundle's enterprise distribution capabilities handle the full adoption loop —
79
+ from org-wide channel configuration to CI-driven bulk upgrades.
80
+
81
+ **Install from an internal Artifactory channel:**
82
+
83
+ ```bash
84
+ # Point agentbundle at your org's channel descriptor (one-time per machine,
85
+ # or pre-configured in your org fork — see Org bootstrap below)
86
+ agentbundle config set source catalogue+https://artifactory.example.test/agentbundle/catalogues/core/channels/stable.json
87
+
88
+ agentbundle install --pack core
89
+ ```
90
+
91
+ The channel descriptor points to an immutable versioned archive; agentbundle
92
+ fetches, verifies its SHA-256 digest, and installs. Pass a bearer token via
93
+ `AGENTBUNDLE_HTTP_BEARER_TOKEN` — it is never stored in state, never printed, and
94
+ never forwarded to a different host.
95
+
96
+ **JSON output for CI pipelines:**
97
+
98
+ ```bash
99
+ # See what's installed and what needs upgrading — machine-readable
100
+ agentbundle list-installed --format json
101
+ agentbundle list-installed --format json --updates-only
102
+ ```
103
+
104
+ Returns a stable JSON contract (`schema_version` 1) with per-row status
105
+ (`up-to-date` / `upgrade-available` / `ahead` / `unknown`) and machine-readable
106
+ reason codes for unknown rows. Pipe into `jq` or your CI annotation step.
107
+
108
+ **Bulk upgrade in one scoped command:**
109
+
110
+ ```bash
111
+ # Upgrade all installed packs in a scope — preflights before any write
112
+ agentbundle upgrade --all --scope repo --yes
113
+ agentbundle upgrade --all --scope user --format json --yes
114
+ ```
115
+
116
+ Preflights all rows before writing anything; a blocked row stops the run before the
117
+ filesystem is touched. Partial failure is reported honestly — not described as a
118
+ rollback. Never silently downgrades an `ahead` row.
119
+
120
+ **Package your catalogue for Artifactory:**
121
+
122
+ ```bash
123
+ agentbundle package-catalogue \
124
+ --root /path/to/catalogue \
125
+ --bundle my-packs \
126
+ --release 1.0.0 \
127
+ --channel stable \
128
+ --output dist/
129
+ ```
130
+
131
+ Produces a deterministic, reproducible gzip archive (versioned) and a mutable channel
132
+ descriptor JSON (`stable.json`), ready to upload to Artifactory. Identical inputs
133
+ produce byte-identical archives (honors `SOURCE_DATE_EPOCH`).
134
+
135
+ **Org bootstrap — ship the default channel in your fork:**
136
+
137
+ Add an `[organization.artifactory]` block to
138
+ `agentbundle/_data/install-defaults.toml` in your org's agentbundle fork:
139
+
140
+ ```toml
141
+ [organization.artifactory]
142
+ enabled = true
143
+ base-url = "https://artifactory.example.test"
144
+ repository = "agentbundle"
145
+ bundle = "core"
146
+ channel = "stable"
147
+ ```
148
+
149
+ Developers installing from your fork get the internal channel without a manual
150
+ `config set source` step. The block ships `enabled = false` in the public package.
151
+ A malformed `enabled = true` config fails closed — no silent fallback to the public
152
+ source.
153
+
154
+ See the full enterprise adoption guide at
155
+ `docs/guides/_shared/how-to/use-an-artifactory-catalogue.md` for all six flows
156
+ (org bootstrap, repo-scope CI upgrade, user-scope MDM, source-conflict remediation,
157
+ disconnected hosts, and security controls).
158
+
73
159
  ## Build your own catalogue
74
160
 
75
161
  `agentbundle` isn't tied to the agent-ready-repo catalogue. Any repo that lays its packs out the same way can use it. A pack is a directory:
@@ -102,6 +188,34 @@ is deliberately lossy per tool.
102
188
 
103
189
  Point a catalogue URI (a git URL or a local path) at the repo that holds your packs. Then `validate` a pack against the adapter contract, `render` it to preview the projection, and `install` it into a target repo. `scaffold` drops a pack's seeds into a fresh directory to start from. The build pipeline (`agentbundle.build`) is the same engine `make build` runs.
104
190
 
191
+ **Org adapter default:** If your org ships a private `agentbundle` wheel (or a fork pinned to your internal catalogue), you can set a default adapter for all developers without requiring them to run `agentbundle config set` or pass `--adapter` on every install. Add an `[organization]` table to `_data/install-defaults.toml` in your fork:
192
+
193
+ ```toml
194
+ [organization]
195
+ preferred_adapter = "cursor"
196
+ ```
197
+
198
+ The org hint fires after the user-config but before the on-disk IDE probe — so `--adapter`, user-config, and upgrade state-hints all take priority. An invalid value exits 1 before writing anything. See the [`agentbundle` reference](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/guides/_shared/reference/agentbundle.md#org-adapter-default) for the full cascade.
199
+
200
+ **Lint your catalogue** — shallow structural checks run without extra dependencies:
201
+
202
+ ```bash
203
+ agentbundle catalogue lint --root .
204
+ ```
205
+
206
+ For full [agentskills.io spec](https://agentskills.io/specification) compliance (frontmatter key set, description policy, encoding, evals schema), install the `lint` extra and run with `--deep`:
207
+
208
+ ```bash
209
+ pip install 'agentbundle[lint]'
210
+ agentbundle catalogue lint --root . --deep
211
+ ```
212
+
213
+ **Run Tier-A activation evals** to measure whether each covered skill fires on the prompts it should:
214
+
215
+ ```bash
216
+ agentbundle pack evals run --pack <pack-name> --catalogue-root .
217
+ ```
218
+
105
219
  See the [pack layout reference](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/architecture/pack-layout.md) and [authoring a skill](https://github.com/eugenelim/agent-ready-repo/blob/main/docs/guides/_shared/how-to/author-a-skill.md).
106
220
 
107
221
  ## Credentials
@@ -0,0 +1,170 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "catalogue.schema.json",
4
+ "title": "CatalogueConfig",
5
+ "description": "JSON Schema for catalogue.toml — ini-005 Bucket 3 configuration layer.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema", "catalogue", "distribution"],
9
+ "properties": {
10
+ "schema": {
11
+ "type": "integer",
12
+ "description": "Schema version — must be 1.",
13
+ "enum": [1]
14
+ },
15
+ "catalogue": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "required": [
19
+ "name",
20
+ "display-name",
21
+ "description",
22
+ "minimum-agentbundle-version",
23
+ "paths",
24
+ "build",
25
+ "package"
26
+ ],
27
+ "properties": {
28
+ "name": {
29
+ "type": "string",
30
+ "description": "Safe identifier — [A-Za-z0-9][A-Za-z0-9_-]*.",
31
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9_\\-]*$"
32
+ },
33
+ "display-name": {
34
+ "type": "string",
35
+ "description": "Human-readable catalogue name."
36
+ },
37
+ "description": {
38
+ "type": "string",
39
+ "description": "One-line description of the catalogue."
40
+ },
41
+ "minimum-agentbundle-version": {
42
+ "type": "string",
43
+ "description": "Minimum agentbundle version required to use this catalogue."
44
+ },
45
+ "paths": {
46
+ "type": "object",
47
+ "additionalProperties": false,
48
+ "required": ["packs", "profiles", "contracts", "marketplace", "build-output"],
49
+ "properties": {
50
+ "packs": {
51
+ "type": "string",
52
+ "description": "Relative path to the packs directory."
53
+ },
54
+ "profiles": {
55
+ "type": "string",
56
+ "description": "Relative path to the profiles directory."
57
+ },
58
+ "contracts": {
59
+ "type": "string",
60
+ "description": "Relative path to the contracts directory."
61
+ },
62
+ "marketplace": {
63
+ "type": "string",
64
+ "description": "Relative path to the marketplace.json file."
65
+ },
66
+ "build-output": {
67
+ "type": "string",
68
+ "description": "Relative path for build output."
69
+ }
70
+ }
71
+ },
72
+ "build": {
73
+ "type": "object",
74
+ "additionalProperties": false,
75
+ "required": ["recipes", "self-host", "claude-plugin-branch", "marketplace-description"],
76
+ "properties": {
77
+ "recipes": {
78
+ "type": "array",
79
+ "items": { "type": "string" },
80
+ "description": "Build recipe IDs or relative .toml paths."
81
+ },
82
+ "self-host": {
83
+ "type": "boolean",
84
+ "description": "Whether to generate self-host install-defaults.toml."
85
+ },
86
+ "claude-plugin-branch": {
87
+ "type": "string",
88
+ "description": "Branch name for claude-plugin publishing."
89
+ },
90
+ "marketplace-description": {
91
+ "type": "string",
92
+ "description": "Description for marketplace.json."
93
+ }
94
+ }
95
+ },
96
+ "package": {
97
+ "type": "object",
98
+ "additionalProperties": false,
99
+ "required": ["include", "required"],
100
+ "properties": {
101
+ "include": {
102
+ "type": "array",
103
+ "items": { "type": "string" },
104
+ "description": "Pack paths to include in a packaged archive."
105
+ },
106
+ "required": {
107
+ "type": "array",
108
+ "items": { "type": "string" },
109
+ "description": "Pack paths that must be present (subset of include)."
110
+ }
111
+ }
112
+ }
113
+ }
114
+ },
115
+ "distribution": {
116
+ "type": "object",
117
+ "additionalProperties": false,
118
+ "required": ["agentbundle"],
119
+ "properties": {
120
+ "agentbundle": {
121
+ "type": "object",
122
+ "additionalProperties": false,
123
+ "required": [
124
+ "install-defaults-output",
125
+ "preferred-adapter",
126
+ "default-source",
127
+ "artifactory"
128
+ ],
129
+ "properties": {
130
+ "install-defaults-output": {
131
+ "type": "string",
132
+ "description": "Relative path for generated install-defaults.toml."
133
+ },
134
+ "preferred-adapter": {
135
+ "type": "string",
136
+ "description": "Default adapter hint written to install-defaults.toml."
137
+ },
138
+ "default-source": {
139
+ "type": "string",
140
+ "description": "Default catalogue source URI (git+https:// or local path)."
141
+ },
142
+ "artifactory": {
143
+ "type": "object",
144
+ "additionalProperties": false,
145
+ "required": ["enabled"],
146
+ "properties": {
147
+ "enabled": {
148
+ "type": "boolean",
149
+ "description": "Whether the Artifactory bootstrap is active."
150
+ },
151
+ "base-url": {
152
+ "type": "string",
153
+ "description": "Artifactory base URL (https:// only, required when enabled)."
154
+ },
155
+ "repository": {
156
+ "type": "string",
157
+ "description": "Artifactory repository name (required when enabled)."
158
+ },
159
+ "bundle": {
160
+ "type": "string",
161
+ "description": "Catalogue bundle name (required when enabled)."
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
@@ -0,0 +1,9 @@
1
+ # Generated by agentbundle catalogue sync-defaults — source of truth: catalogue.toml
2
+ [organization]
3
+ preferred_adapter = "claude-code"
4
+
5
+ [organization.artifactory]
6
+ enabled = false
7
+
8
+ [defaults]
9
+ source = "git+https://github.com/eugenelim/agent-ready-repo"
@@ -111,7 +111,7 @@ def _build_parser() -> argparse.ArgumentParser:
111
111
  default="dist",
112
112
  help="Where to write build artefacts (default: dist/).",
113
113
  )
114
- build_parser.set_defaults(func=cmd_build)
114
+ build_parser.set_defaults(func=_cmd_build_shim)
115
115
 
116
116
  self_parser = subparsers.add_parser(
117
117
  "self",
@@ -130,7 +130,7 @@ def _build_parser() -> argparse.ArgumentParser:
130
130
  "OS for filesystems or workflows that disallow symlinks."
131
131
  ),
132
132
  )
133
- self_parser.set_defaults(func=cmd_self)
133
+ self_parser.set_defaults(func=_cmd_self_shim)
134
134
 
135
135
  check_parser = subparsers.add_parser(
136
136
  "check",
@@ -146,7 +146,7 @@ def _build_parser() -> argparse.ArgumentParser:
146
146
  "regular-file CLAUDE.md copy rather than a symlink."
147
147
  ),
148
148
  )
149
- check_parser.set_defaults(func=cmd_check)
149
+ check_parser.set_defaults(func=_cmd_check_shim)
150
150
 
151
151
  scaffold_parser = subparsers.add_parser(
152
152
  "scaffold",
@@ -162,14 +162,53 @@ def _build_parser() -> argparse.ArgumentParser:
162
162
  help=(
163
163
  "Windows-portability lint: reject packs that ship symlinks "
164
164
  "or Windows-poisonous names under seeds/ or .apm/."
165
+ " [deprecated: use 'agentbundle catalogue lint']"
165
166
  ),
166
167
  )
167
168
  lint_packs_parser.add_argument("--packs-dir", default="packs")
168
- lint_packs_parser.set_defaults(func=cmd_lint_packs)
169
+ lint_packs_parser.set_defaults(func=_cmd_lint_packs_shim)
169
170
 
170
171
  return parser
171
172
 
172
173
 
174
+ def _cmd_build_shim(args) -> int:
175
+ print(
176
+ "agentbundle.build build is deprecated; use 'agentbundle catalogue build --root .' instead",
177
+ file=sys.stderr,
178
+ )
179
+ return int(cmd_build(args))
180
+
181
+
182
+ def _cmd_self_shim(args) -> int:
183
+ print(
184
+ "agentbundle.build self is deprecated; use 'agentbundle catalogue self-host --write --root .' instead",
185
+ file=sys.stderr,
186
+ )
187
+ return int(cmd_self(args))
188
+
189
+
190
+ def _cmd_check_shim(args) -> int:
191
+ print(
192
+ "agentbundle.build check is deprecated; use 'agentbundle catalogue self-host --check --root .' instead",
193
+ file=sys.stderr,
194
+ )
195
+ return int(cmd_check(args))
196
+
197
+
198
+ def _cmd_lint_packs_shim(args) -> int:
199
+ print(
200
+ "agentbundle.build lint-packs is deprecated; use 'agentbundle catalogue lint --root .' instead",
201
+ file=sys.stderr,
202
+ )
203
+ from agentbundle.catalogue_tooling.lint import lint_catalogue, render_table
204
+
205
+ packs_dir = Path(getattr(args, "packs_dir", "packs")).resolve()
206
+ root = packs_dir.parent
207
+ result = lint_catalogue(root)
208
+ print(render_table(result), file=sys.stderr)
209
+ return 0 if result.ok else 1
210
+
211
+
173
212
  def _cmd_scaffold(args) -> int:
174
213
  pack_seeds = Path(args.packs_dir) / args.pack / "seeds"
175
214
  output = Path(args.output)
@@ -14,6 +14,7 @@ deep-merging the incoming TOML payload.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import os
17
18
  import shutil
18
19
  from pathlib import Path
19
20
  from typing import Iterator
@@ -107,15 +108,17 @@ def _project_single(pack_path: Path, contract: dict, output_root: Path) -> None:
107
108
  raise ValueError(f"claude-code: unhandled mode {mode!r} for {primitive_name}")
108
109
 
109
110
 
110
- def _ignore_symlinks(directory: str, names: list[str]) -> set[str]:
111
- """`shutil.copytree` ignore callback: skip every symlink member.
111
+ def _ignore_absolute_symlinks(directory: str, names: list[str]) -> set[str]:
112
+ """`shutil.copytree` ignore callback: drop symlinks with absolute targets.
112
113
 
113
- Drops nested symlinks so they are never reproduced in the output
114
- tree. The top-level `is_symlink()` skip in `_project_direct_directory`
115
- covers the skill root; this covers the subtree.
114
+ Relative symlinks (intra-skill cross-references) are preserved as
115
+ symlinks. Absolute symlinks always escape the tree and are a path-escape vector.
116
116
  """
117
117
  base = Path(directory)
118
- return {name for name in names if (base / name).is_symlink()}
118
+ return {
119
+ name for name in names
120
+ if (base / name).is_symlink() and os.path.isabs(os.readlink(base / name))
121
+ }
119
122
 
120
123
 
121
124
  def _project_direct_directory(source_dir: Path, target_dir: Path) -> None:
@@ -136,10 +139,13 @@ def _project_direct_directory(source_dir: Path, target_dir: Path) -> None:
136
139
  destination.unlink()
137
140
  elif destination.exists():
138
141
  shutil.rmtree(destination)
139
- # `ignore=_ignore_symlinks` drops nested symlinks so they are
140
- # never reproduced in the output tree. A malicious pack with a
141
- # symlink to /etc/passwd cannot exfiltrate the target.
142
- shutil.copytree(entry, destination, ignore=_ignore_symlinks)
142
+ # symlinks=True preserves relative nested symlinks; absolute
143
+ # targets are filtered out by _ignore_absolute_symlinks.
144
+ shutil.copytree(
145
+ entry, destination,
146
+ symlinks=True,
147
+ ignore=_ignore_absolute_symlinks,
148
+ )
143
149
 
144
150
 
145
151
  def _project_direct_file(source_dir: Path, output_root: Path, target_prefix: str) -> None:
@@ -29,16 +29,6 @@ from agentbundle.build.projections.codex_agent_toml import (
29
29
  )
30
30
 
31
31
 
32
- def _ignore_symlinks(directory: str, names: list[str]) -> set[str]:
33
- """`shutil.copytree` ignore callback: skip every symlink member.
34
-
35
- Drops nested symlinks so they are never reproduced in the output
36
- tree. The top-level `is_symlink()` skip in `project_packs` covers
37
- the skill root; this covers the subtree.
38
- """
39
- base = Path(directory)
40
- return {name for name in names if (base / name).is_symlink()}
41
-
42
32
 
43
33
  def _iter_primitives(contract: dict) -> Iterator[str]:
44
34
  """Yield Codex's projected primitive names in phase order."""
@@ -142,11 +132,13 @@ def project_packs(pack_paths: list[Path], contract: dict, output_root: Path) ->
142
132
  destination.unlink()
143
133
  elif destination.exists():
144
134
  shutil.rmtree(destination)
145
- # `ignore=_ignore_symlinks` drops nested symlinks
146
- # entirely never reproduced in the output tree.
147
- # A malicious pack with a symlink to /etc/passwd
148
- # cannot exfiltrate.
149
- shutil.copytree(entry, destination, ignore=_ignore_symlinks)
135
+ # symlinks=True preserves relative nested symlinks;
136
+ # absolute targets are filtered by _ignore_absolute_symlinks.
137
+ shutil.copytree(
138
+ entry, destination,
139
+ symlinks=True,
140
+ ignore=_ignore_absolute_symlinks,
141
+ )
150
142
  # Bound to `skill` only per spec § Never do. Other
151
143
  # direct-directory primitives opt in explicitly.
152
144
  if primitive_name == "skill":
@@ -166,6 +158,19 @@ def project_packs(pack_paths: list[Path], contract: dict, output_root: Path) ->
166
158
  raise ValueError(f"codex: unhandled mode {mode!r} for {primitive_name}")
167
159
 
168
160
 
161
+ def _ignore_absolute_symlinks(directory: str, names: list[str]) -> set[str]:
162
+ """`shutil.copytree` ignore callback: drop symlinks with absolute targets.
163
+
164
+ Relative symlinks (intra-skill cross-references) are preserved (AC5).
165
+ Absolute symlinks always escape the tree and are a path-escape vector.
166
+ """
167
+ base = Path(directory)
168
+ return {
169
+ name for name in names
170
+ if (base / name).is_symlink() and os.path.isabs(os.readlink(base / name))
171
+ }
172
+
173
+
169
174
  def _project_direct_file(source_dir: Path, output_root: Path, target_prefix: str) -> None:
170
175
  target_dir = output_root / target_prefix.rstrip("/")
171
176
  target_dir.mkdir(parents=True, exist_ok=True)