agentbundle 0.12.0__tar.gz → 0.12.1__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 (129) hide show
  1. {agentbundle-0.12.0 → agentbundle-0.12.1}/PKG-INFO +1 -1
  2. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/_common.py +49 -33
  3. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/diff.py +3 -3
  4. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/install.py +24 -24
  5. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/uninstall.py +3 -2
  6. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/upgrade.py +25 -21
  7. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/safety.py +38 -22
  8. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/version.py +1 -1
  9. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle.egg-info/PKG-INFO +1 -1
  10. {agentbundle-0.12.0 → agentbundle-0.12.1}/pyproject.toml +1 -1
  11. {agentbundle-0.12.0 → agentbundle-0.12.1}/README.md +0 -0
  12. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/__init__.py +0 -0
  13. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/__main__.py +0 -0
  14. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/_data/adapter.schema.json +0 -0
  15. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/_data/adapter.toml +0 -0
  16. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/_data/install-defaults.toml +0 -0
  17. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/_data/install-marker.py +0 -0
  18. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/_data/pack.schema.json +0 -0
  19. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/_data/plugin-manifest.derived.schema.json +0 -0
  20. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/_data/plugin-manifest.schema.json +0 -0
  21. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/_data/profile.schema.json +0 -0
  22. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/__init__.py +0 -0
  23. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/__main__.py +0 -0
  24. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/adapter_root_bins.py +0 -0
  25. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/adapters/__init__.py +0 -0
  26. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/adapters/claude_code.py +0 -0
  27. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/adapters/codex.py +0 -0
  28. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/adapters/copilot.py +0 -0
  29. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/adapters/cursor.py +0 -0
  30. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/adapters/gemini.py +0 -0
  31. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/adapters/kiro.py +0 -0
  32. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/adapters/kiro_cli.py +0 -0
  33. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/adapters/kiro_ide.py +0 -0
  34. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/contract.py +0 -0
  35. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/lint_packs.py +0 -0
  36. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/main.py +0 -0
  37. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/phase_order.py +0 -0
  38. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/projections/__init__.py +0 -0
  39. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/projections/codex_agent_toml.py +0 -0
  40. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/projections/copilot_agent_md.py +0 -0
  41. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/projections/copilot_hooks_json.py +0 -0
  42. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/projections/direct_directory.py +0 -0
  43. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/projections/gemini_command_toml.py +0 -0
  44. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/projections/hook_id.py +0 -0
  45. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/projections/kiro_ide_hook.py +0 -0
  46. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/projections/merge_into_agent_json.py +0 -0
  47. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/projections/merge_json.py +0 -0
  48. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/projections/user_merge_json.py +0 -0
  49. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/scope_rails.py +0 -0
  50. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/self_host.py +0 -0
  51. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/shared_libs.py +0 -0
  52. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/target_resolver.py +0 -0
  53. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/__init__.py +0 -0
  54. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_adapter_claude_code.py +0 -0
  55. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_adapter_codex.py +0 -0
  56. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_adapter_copilot.py +0 -0
  57. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_adapter_cursor.py +0 -0
  58. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_adapter_gemini.py +0 -0
  59. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_adapter_kiro.py +0 -0
  60. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_adapter_kiro_alias.py +0 -0
  61. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_adapter_kiro_cli.py +0 -0
  62. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_adapter_kiro_ide.py +0 -0
  63. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_adapter_root_bins_projection.py +0 -0
  64. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_architect_design_reviewer_projection.py +0 -0
  65. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_architect_design_reviewer_rubric_parity.py +0 -0
  66. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_build_ships_seeds.py +0 -0
  67. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_contract.py +0 -0
  68. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_contract_scope.py +0 -0
  69. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_contract_v07.py +0 -0
  70. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_contract_v08.py +0 -0
  71. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_direct_directory_cleanup.py +0 -0
  72. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_end_to_end_build.py +0 -0
  73. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_lint_agents_md_diataxis_block.py +0 -0
  74. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_lint_agents_md_legacy_block.py +0 -0
  75. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_lint_agents_md_risk_block.py +0 -0
  76. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_lint_packs.py +0 -0
  77. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +0 -0
  78. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_pack_schema.py +0 -0
  79. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_pack_schema_allowed_adapters.py +0 -0
  80. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_pack_schema_install.py +0 -0
  81. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_pipeline.py +0 -0
  82. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_plugin_manifest_schema.py +0 -0
  83. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_projectable_subset.py +0 -0
  84. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_projections_merge_json.py +0 -0
  85. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_scope_rails.py +0 -0
  86. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_security.py +0 -0
  87. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_self_host_check.py +0 -0
  88. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_self_host_fixture_guard.py +0 -0
  89. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_self_host_recipe_config.py +0 -0
  90. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_shared_libs_projection.py +0 -0
  91. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_shared_prefix_contract.py +0 -0
  92. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_shipped_packs_v07_declarations.py +0 -0
  93. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_shipped_packs_v08_declarations.py +0 -0
  94. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_user_libs_projection.py +0 -0
  95. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_validate.py +0 -0
  96. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/tests/test_writers_emit_lf.py +0 -0
  97. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/user_libs.py +0 -0
  98. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/build/validate.py +0 -0
  99. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/catalogue.py +0 -0
  100. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/categories.py +0 -0
  101. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/cli.py +0 -0
  102. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/__init__.py +0 -0
  103. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/_drop_warning.py +0 -0
  104. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/adapt.py +0 -0
  105. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/config.py +0 -0
  106. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/init_state.py +0 -0
  107. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/list_installed.py +0 -0
  108. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/list_packs.py +0 -0
  109. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/list_profiles.py +0 -0
  110. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/list_targets.py +0 -0
  111. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/profile.py +0 -0
  112. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/reconcile.py +0 -0
  113. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/render.py +0 -0
  114. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/scaffold.py +0 -0
  115. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/show.py +0 -0
  116. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/commands/validate.py +0 -0
  117. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/config.py +0 -0
  118. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/pack_inventory.py +0 -0
  119. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/render.py +0 -0
  120. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/scope.py +0 -0
  121. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/source_defaults.py +0 -0
  122. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/statelock.py +0 -0
  123. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle/user_config.py +0 -0
  124. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle.egg-info/SOURCES.txt +0 -0
  125. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle.egg-info/dependency_links.txt +0 -0
  126. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle.egg-info/entry_points.txt +0 -0
  127. {agentbundle-0.12.0 → agentbundle-0.12.1}/agentbundle.egg-info/top_level.txt +0 -0
  128. {agentbundle-0.12.0 → agentbundle-0.12.1}/setup.cfg +0 -0
  129. {agentbundle-0.12.0 → agentbundle-0.12.1}/tests/test_linear_primitive.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentbundle
3
- Version: 0.12.0
3
+ Version: 0.12.1
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
@@ -40,6 +40,17 @@ def resolve_catalogue_uri(args: "argparse.Namespace") -> str:
40
40
  )
41
41
 
42
42
 
43
+ def resolve_state_path(scope: str, root: Path) -> Path:
44
+ """Return the state-file path for *scope* under *root*.
45
+
46
+ ``scope="repo"`` → ``<root>/.agentbundle-state.toml``
47
+ ``scope="user"`` → ``<root>/.agentbundle/state.toml``
48
+ """
49
+ if scope == "user":
50
+ return root / ".agentbundle" / "state.toml"
51
+ return root / ".agentbundle-state.toml"
52
+
53
+
43
54
  class SeedDelivery(NamedTuple):
44
55
  """One seed file's delivery outcome, returned by ``deliver_seeds``.
45
56
 
@@ -77,44 +88,20 @@ def _compose_agents_md_bytes(body: bytes, footer_path: Path) -> bytes:
77
88
  return (text + footer).encode("utf-8")
78
89
 
79
90
 
80
- def deliver_seeds(seeds_dir: Path, output: Path) -> list[SeedDelivery]:
81
- """Deliver a pack's ``seeds/`` into ``output`` with Tier-1/2/3 safety.
82
-
83
- For each file under ``seeds_dir`` (recursively):
84
- - **Composition fragments** (name starts with ``_``, e.g.
85
- ``_agents-footer.md``) are *not* delivered standalone — they are folded
86
- into ``AGENTS.md`` instead (per ``CONVENTIONS.md`` §Pack source-of-truth split).
87
- - **Absent on disk** → write the seed (Tier-1).
88
- - **Present, content matches** → no-op (already in sync).
89
- - **Present, content differs** → write a ``*.upstream.<ext>`` companion
90
- next to the original; leave the original untouched (Tier-2).
91
-
92
- Every write routes through ``safety.write_jailed`` / ``safety.write_companion``
93
- with the **bare under-root jail** (no ``allowed_prefixes`` — seeds land at the
94
- repo root and ``docs/``, outside the adapter projection prefixes). The caller
95
- decides whether to record state; this helper never writes ``.agentbundle-state.toml``.
91
+ def _classify_seeds(seeds_dir: Path, root: Path) -> list[SeedDelivery]:
92
+ """Classify seeds without writing pure read-only; same walk logic as deliver_seeds.
96
93
 
97
- Raises ``safety.PathJailError`` if any seed relpath would escape ``output``;
98
- the caller is expected to catch it, print to stderr, and exit 1.
94
+ Returns a list of SeedDelivery records with action 'wrote' (absent on disk),
95
+ 'skipped' (byte-identical), or 'companion' (differs Tier-2). No filesystem
96
+ writes occur under root.
99
97
  """
100
98
  import os
101
99
 
102
100
  from agentbundle import safety
103
101
 
104
102
  footer_path = seeds_dir / "_agents-footer.md"
105
- # Guard the footer read too — ``_compose_agents_md_bytes`` reads
106
- # ``footer_path`` directly, so a symlinked footer would be read through.
107
103
  footer_ok = footer_path.is_file() and not footer_path.is_symlink()
108
104
 
109
- # Defence-in-depth against a malicious pack exfiltrating a host file
110
- # (``/etc/passwd``, ``~/.ssh/id_rsa``) into the adopter tree by symlinking
111
- # a seed — never read *through* a pack-shipped symlink. We must not rely on
112
- # ``Path.rglob``'s symlink posture: on Python 3.11/3.12 ``rglob`` recurses
113
- # *into* symlinked directories (3.13 changed the default to
114
- # ``recurse_symlinks=False``), so ``seeds/x -> /`` would surface real host
115
- # files as non-symlink entries. ``os.walk(followlinks=False)`` never
116
- # descends into a symlinked directory on any supported Python, and we also
117
- # skip symlinked files — closing both the file and directory cases.
118
105
  seed_files: list[Path] = []
119
106
  for dirpath, _dirnames, filenames in os.walk(seeds_dir, followlinks=False):
120
107
  for fname in filenames:
@@ -125,7 +112,6 @@ def deliver_seeds(seeds_dir: Path, output: Path) -> list[SeedDelivery]:
125
112
 
126
113
  results: list[SeedDelivery] = []
127
114
  for seed_file in sorted(seed_files):
128
- # Composition fragments are folded in, never delivered standalone.
129
115
  if seed_file.name.startswith("_"):
130
116
  continue
131
117
  relpath = seed_file.relative_to(seeds_dir).as_posix()
@@ -133,19 +119,49 @@ def deliver_seeds(seeds_dir: Path, output: Path) -> list[SeedDelivery]:
133
119
  if relpath == "AGENTS.md" and footer_ok:
134
120
  content = _compose_agents_md_bytes(content, footer_path)
135
121
 
136
- on_disk = output / relpath
122
+ on_disk = root / relpath
137
123
  if not on_disk.exists():
138
- safety.write_jailed(output, relpath, content)
139
124
  results.append(SeedDelivery(relpath, content, "wrote", None))
140
125
  elif on_disk.read_bytes() == content:
141
126
  results.append(SeedDelivery(relpath, content, "skipped", None))
142
127
  else:
143
- safety.write_companion(output, relpath, content)
144
128
  companion = safety.companion_path(Path(relpath)).as_posix()
145
129
  results.append(SeedDelivery(relpath, content, "companion", companion))
146
130
  return results
147
131
 
148
132
 
133
+ def deliver_seeds(seeds_dir: Path, output: Path) -> list[SeedDelivery]:
134
+ """Deliver a pack's ``seeds/`` into ``output`` with Tier-1/2/3 safety.
135
+
136
+ For each file under ``seeds_dir`` (recursively):
137
+ - **Composition fragments** (name starts with ``_``, e.g.
138
+ ``_agents-footer.md``) are *not* delivered standalone — they are folded
139
+ into ``AGENTS.md`` instead (per ``CONVENTIONS.md`` §Pack source-of-truth split).
140
+ - **Absent on disk** → write the seed (Tier-1).
141
+ - **Present, content matches** → no-op (already in sync).
142
+ - **Present, content differs** → write a ``*.upstream.<ext>`` companion
143
+ next to the original; leave the original untouched (Tier-2).
144
+
145
+ Every write routes through ``safety.write_jailed`` / ``safety.write_companion``
146
+ with the **bare under-root jail** (no ``allowed_prefixes`` — seeds land at the
147
+ repo root and ``docs/``, outside the adapter projection prefixes). The caller
148
+ decides whether to record state; this helper never writes ``.agentbundle-state.toml``.
149
+
150
+ Raises ``safety.PathJailError`` if any seed relpath would escape ``output``;
151
+ the caller is expected to catch it, print to stderr, and exit 1.
152
+ """
153
+ from agentbundle import safety
154
+
155
+ records = _classify_seeds(seeds_dir, output)
156
+ for record in records:
157
+ if record.action == "wrote":
158
+ safety.write_jailed(output, record.relpath, record.content)
159
+ elif record.action == "companion":
160
+ safety.write_companion(output, record.relpath, record.content)
161
+ # "skipped" → no-op
162
+ return records
163
+
164
+
149
165
  def check_spec_version_gate(pack_toml: dict[str, Any]) -> int | None:
150
166
  """Refuse if the pack's declared spec major version differs from ours.
151
167
 
@@ -22,7 +22,7 @@ import sys
22
22
  from pathlib import Path
23
23
 
24
24
  from agentbundle import render, safety
25
- from agentbundle.commands._common import check_spec_version_gate
25
+ from agentbundle.commands._common import check_spec_version_gate, resolve_state_path
26
26
  from agentbundle.config import ConfigError, load_pack_toml
27
27
 
28
28
 
@@ -57,7 +57,7 @@ def run(args: argparse.Namespace) -> int:
57
57
  # hard cross-version refusal); surface it as a clean refuse rather
58
58
  # than a traceback.
59
59
  try:
60
- repo_state_for_check = load_state(root / ".agentbundle-state.toml")
60
+ repo_state_for_check = load_state(resolve_state_path("repo", root))
61
61
  except ConfigError as exc:
62
62
  print(f"diff: {exc}", file=sys.stderr)
63
63
  return 1
@@ -65,7 +65,7 @@ def run(args: argparse.Namespace) -> int:
65
65
  try:
66
66
  user_root_resolved = scope_mod.resolve_user_root()
67
67
  user_state_for_check = load_state(
68
- user_root_resolved / ".agentbundle" / "state.toml"
68
+ resolve_state_path("user", user_root_resolved)
69
69
  )
70
70
  installed_at_user = user_state_for_check.has_pack(pack_name)
71
71
  except scope_mod.UserScopeUnresolvable:
@@ -892,30 +892,13 @@ def run(args: "argparse.Namespace") -> int:
892
892
  projection = repo_projection if plan.scope == "repo" else user_projection
893
893
  if projection is None:
894
894
  continue
895
- for relpath in projection.keys():
896
- target = plan.root / relpath
897
- try:
898
- safety.assert_under(plan.root, target)
899
- except safety.PathJailError as exc:
900
- print(f"install: {exc}", file=sys.stderr)
901
- return 1
902
- # Per-prefix probe fires whenever the plan has an
903
- # allowed_prefixes list (user scope always; repo scope when
904
- # the per-IDE path is in use — RFC-0012). With
905
- # `allowed_prefixes=None` the probe is skipped (legacy
906
- # dist-tree producer at repo scope under
907
- # --emit-install-routes).
908
- if plan.allowed_prefixes is not None:
909
- target_relpath = target.resolve().relative_to(plan.root.resolve()).as_posix()
910
- prefixes = plan.allowed_prefixes or []
911
- # Directory-boundary matching only — see safety.py.
912
- if not any(target_relpath.startswith(p) for p in prefixes):
913
- print(
914
- f"install: refusing to write outside allowed prefixes "
915
- f"for scope {plan.scope!r}: {target.resolve()}",
916
- file=sys.stderr,
917
- )
918
- return 1
895
+ try:
896
+ safety.assert_projection_jailed(
897
+ plan.root, projection.keys(), plan.allowed_prefixes, command="install"
898
+ )
899
+ except safety.PathJailError as exc:
900
+ print(str(exc), file=sys.stderr)
901
+ return 1
919
902
 
920
903
  # ── Dry-run: all read-only pre-flight passed — preview and stop ───────────
921
904
  # At the top of Step 9 (after Step 8's path-jail probe) so every pre-flight
@@ -927,6 +910,8 @@ def run(args: "argparse.Namespace") -> int:
927
910
  # recap). `--force` is refused up front, so there is exactly one writing
928
911
  # plan here (dual-scope writes arise only under --force).
929
912
  if dry_run:
913
+ from agentbundle.commands._common import _classify_seeds
914
+
930
915
  actions: list[str] = []
931
916
  for plan in plans:
932
917
  if plan.already_installed:
@@ -946,6 +931,21 @@ def run(args: "argparse.Namespace") -> int:
946
931
  )
947
932
  print(format_plan_line(action, tier.value, relpath, companion))
948
933
  actions.append(action)
934
+ if plan.scope == "repo":
935
+ seeds_dir = pack_dir / "seeds"
936
+ if seeds_dir.is_dir():
937
+ for record in _classify_seeds(seeds_dir, plan.root):
938
+ if record.action == "skipped":
939
+ continue
940
+ verb = "create" if record.action == "wrote" else "companion"
941
+ tier_str = "tier-1" if record.action == "wrote" else "tier-2"
942
+ path_str = (
943
+ record.relpath
944
+ if record.action == "wrote"
945
+ else f"{record.relpath} -> {record.companion_relpath}"
946
+ )
947
+ print(format_plan_line(verb, tier_str, path_str))
948
+ actions.append(verb)
949
949
  print(summarize_plan(actions))
950
950
  return 0
951
951
 
@@ -46,12 +46,13 @@ def run(args: "argparse.Namespace") -> int:
46
46
  """
47
47
  from agentbundle.config import ConfigError, dump_state, load_state
48
48
  from agentbundle import safety
49
+ from agentbundle.commands._common import resolve_state_path
49
50
 
50
51
  pack_name: str = args.pack
51
52
  cli_scope: str | None = getattr(args, "scope", None)
52
53
  cli_adapter: str | None = getattr(args, "adapter", None)
53
54
  root = Path(args.root).resolve()
54
- state_path = root / ".agentbundle-state.toml"
55
+ state_path = resolve_state_path("repo", root)
55
56
 
56
57
  # ── Step 1: Multi-scope disambiguator (RFC-0004) ──────────────────────────
57
58
  # If the pack is at both repo and user scopes, --scope is required.
@@ -72,7 +73,7 @@ def run(args: "argparse.Namespace") -> int:
72
73
  user_state_for_check = None
73
74
  try:
74
75
  user_root = scope_mod.resolve_user_root()
75
- user_state_path = user_root / ".agentbundle" / "state.toml"
76
+ user_state_path = resolve_state_path("user", user_root)
76
77
  user_state_for_check = load_state(user_state_path)
77
78
  installed_at_user = user_state_for_check.has_pack(pack_name)
78
79
  except scope_mod.UserScopeUnresolvable:
@@ -79,6 +79,7 @@ def run(args: "argparse.Namespace") -> int:
79
79
  format_plan_line,
80
80
  plan_action,
81
81
  resolve_catalogue_uri,
82
+ resolve_state_path,
82
83
  summarize_plan,
83
84
  )
84
85
  from agentbundle.config import (
@@ -115,7 +116,7 @@ def run(args: "argparse.Namespace") -> int:
115
116
  # If the pack is at both scopes, --scope is required; at one scope, infer.
116
117
  from agentbundle import scope as scope_mod
117
118
 
118
- repo_state_path = root / ".agentbundle-state.toml"
119
+ repo_state_path = resolve_state_path("repo", root)
119
120
  # A legacy (non-v0.4) state file is refused on read too (RFC-0052);
120
121
  # surface it as a clean refuse rather than a traceback.
121
122
  try:
@@ -129,7 +130,7 @@ def run(args: "argparse.Namespace") -> int:
129
130
  user_state_for_check = None
130
131
  try:
131
132
  user_root_resolved = scope_mod.resolve_user_root()
132
- user_state_path = user_root_resolved / ".agentbundle" / "state.toml"
133
+ user_state_path = resolve_state_path("user", user_root_resolved)
133
134
  user_state_for_check = load_state(user_state_path)
134
135
  installed_at_user = user_state_for_check.has_pack(pack_name)
135
136
  except scope_mod.UserScopeUnresolvable:
@@ -248,7 +249,7 @@ def run(args: "argparse.Namespace") -> int:
248
249
  if effective_scope == "user":
249
250
  state_path = user_state_path # already resolved above
250
251
  else:
251
- state_path = root / ".agentbundle-state.toml"
252
+ state_path = resolve_state_path("repo", root)
252
253
  try:
253
254
  state = load_state(state_path, for_write=True)
254
255
  except ConfigError as exc:
@@ -541,24 +542,13 @@ def run(args: "argparse.Namespace") -> int:
541
542
  # the print loop (probe-all-then-print): a jail violation on a late file
542
543
  # aborts with a clean stderr refusal instead of a partial plan already
543
544
  # on stdout.
544
- for relpath in sorted(work_projection):
545
- target = root / relpath
546
- try:
547
- safety.assert_under(root, target)
548
- except safety.PathJailError as exc:
549
- print(f"upgrade: {exc}", file=sys.stderr)
550
- return 1
551
- if allowed_prefixes is not None:
552
- target_relpath = (
553
- target.resolve().relative_to(root.resolve()).as_posix()
554
- )
555
- if not any(target_relpath.startswith(p) for p in allowed_prefixes):
556
- print(
557
- f"upgrade: refusing to write outside allowed prefixes "
558
- f"for scope {effective_scope!r}: {target.resolve()}",
559
- file=sys.stderr,
560
- )
561
- return 1
545
+ try:
546
+ safety.assert_projection_jailed(
547
+ root, sorted(work_projection), allowed_prefixes, command="upgrade"
548
+ )
549
+ except safety.PathJailError as exc:
550
+ print(str(exc), file=sys.stderr)
551
+ return 1
562
552
  actions: list[str] = []
563
553
  for relpath in sorted(work_projection):
564
554
  tier = safety.classify(relpath, root, state)
@@ -575,6 +565,20 @@ def run(args: "argparse.Namespace") -> int:
575
565
  print(summarize_plan(actions))
576
566
  return 0
577
567
 
568
+ # ── Path-jail pre-flight (AC4) — probe all before any write ──────────────
569
+ # Mirror the dry-run probe: refuse if any projected path escapes root or
570
+ # violates allowed_prefixes, before touching disk. Without this gate a
571
+ # prefix violation surfaces mid-loop via write_jailed, after earlier files
572
+ # have already been written — the probe-all-before-write contract means a
573
+ # violation is always a clean abort.
574
+ try:
575
+ safety.assert_projection_jailed(
576
+ root, sorted(work_projection), allowed_prefixes, command="upgrade"
577
+ )
578
+ except safety.PathJailError as exc:
579
+ print(str(exc), file=sys.stderr)
580
+ return 1
581
+
578
582
  # ── Walk projection; apply Tier contract ──────────────────────────────────
579
583
  # Collect `.upstream.<ext>` companions dropped this run so we can surface
580
584
  # them after the walk. Without this notice the upgrade is silent on a
@@ -162,6 +162,33 @@ def assert_under(root: Path, target: Path) -> None:
162
162
  ) from exc
163
163
 
164
164
 
165
+ def assert_projection_jailed(
166
+ root: Path,
167
+ relpaths: "Iterable[str]",
168
+ allowed_prefixes: "list[str] | None",
169
+ *,
170
+ command: str,
171
+ ) -> None:
172
+ """Raise PathJailError if any relpath escapes root or violates allowed_prefixes.
173
+
174
+ When allowed_prefixes is None, only root-escape is checked.
175
+ root must be an absolute resolved Path.
176
+ The command parameter is prefixed on all PathJailError messages raised.
177
+ """
178
+ for relpath in relpaths:
179
+ target = root / relpath
180
+ try:
181
+ assert_under(root, target)
182
+ except PathJailError as exc:
183
+ raise PathJailError(f"{command}: {exc}") from exc
184
+ if allowed_prefixes is not None:
185
+ target_relpath = target.resolve().relative_to(root.resolve()).as_posix()
186
+ if not any(target_relpath.startswith(p) for p in allowed_prefixes):
187
+ raise PathJailError(
188
+ f"{command}: path {relpath!r} is not within any declared prefix zone"
189
+ )
190
+
191
+
165
192
  # ---------------------------------------------------------------------------
166
193
  # Windows-portability guard
167
194
  # ---------------------------------------------------------------------------
@@ -320,31 +347,20 @@ def write_jailed(
320
347
  assert_under(root, target)
321
348
 
322
349
  if allowed_prefixes is not None:
323
- # Check the resolved target is under one of the declared
324
- # prefixes relative to root. Use directory-boundary matching:
325
- # the prefix's trailing slash is mandatory, so ``.claude/``
326
- # admits ``.claude/skills/foo`` but rejects a top-level
327
- # ``.claude`` file (which would otherwise let a pack replace
328
- # the directory with a file).
329
- prefixes = allowed_prefixes
330
- # Defense-in-depth: the adapter contract schema enforces a
331
- # trailing slash on every `allowed-prefixes` entry; assert it
332
- # at runtime so a future caller that bypasses the schema
333
- # (e.g. constructing the list in code) cannot silently widen
334
- # the jail. A `.claude` (no slash) prefix would otherwise
335
- # admit `.claudefoo` — exactly the bug the equality-clause
336
- # removal was meant to fix.
337
- if not all(p.endswith("/") for p in prefixes):
350
+ # Defense-in-depth: the adapter contract schema enforces a trailing
351
+ # slash on every `allowed-prefixes` entry; assert it at runtime so a
352
+ # future caller that bypasses the schema (e.g. constructing the list in
353
+ # code) cannot silently widen the jail. A `.claude` (no slash) prefix
354
+ # would otherwise admit `.claudefoo` exactly the bug the
355
+ # equality-clause removal was meant to fix.
356
+ if not all(p.endswith("/") for p in allowed_prefixes):
338
357
  raise PathJailError(
339
358
  f"refusing to write at scope {scope!r}: allowed_prefixes "
340
- f"must each end with '/'; got {prefixes!r}"
341
- )
342
- target_relpath = target.resolve().relative_to(root.resolve()).as_posix()
343
- if not any(target_relpath.startswith(p) for p in prefixes):
344
- raise PathJailError(
345
- f"refusing to write outside allowed prefixes for scope "
346
- f"{scope!r}: {target.resolve()}"
359
+ f"must each end with '/'; got {allowed_prefixes!r}"
347
360
  )
361
+ assert_projection_jailed(
362
+ root, [relpath], allowed_prefixes, command=f"scope={scope}"
363
+ )
348
364
  try:
349
365
  target.parent.mkdir(parents=True, exist_ok=True)
350
366
  except OSError as exc:
@@ -14,7 +14,7 @@ import tomllib
14
14
  from importlib.resources import files
15
15
  from pathlib import Path
16
16
 
17
- CLI_VERSION = "0.12.0"
17
+ CLI_VERSION = "0.12.1"
18
18
 
19
19
  _HERE = Path(__file__).resolve().parent
20
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentbundle
3
- Version: 0.12.0
3
+ Version: 0.12.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentbundle"
7
- version = "0.12.0"
7
+ version = "0.12.1"
8
8
  description = "npm for your coding agent. Install packs of skills, subagents, and hooks into any repo, for every major agent."
9
9
  requires-python = ">=3.11"
10
10
  dependencies = []
File without changes
File without changes