agentbundle 0.2.0__py3-none-any.whl
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.
- agentbundle/__init__.py +14 -0
- agentbundle/__main__.py +5 -0
- agentbundle/_data/adapter.schema.json +270 -0
- agentbundle/_data/adapter.toml +584 -0
- agentbundle/_data/install-marker.py +1099 -0
- agentbundle/_data/pack.schema.json +152 -0
- agentbundle/_data/plugin-manifest.derived.schema.json +33 -0
- agentbundle/_data/plugin-manifest.schema.json +18 -0
- agentbundle/build/__init__.py +206 -0
- agentbundle/build/__main__.py +8 -0
- agentbundle/build/adapter_root_bins.py +336 -0
- agentbundle/build/adapters/__init__.py +46 -0
- agentbundle/build/adapters/claude_code.py +142 -0
- agentbundle/build/adapters/codex.py +227 -0
- agentbundle/build/adapters/copilot.py +149 -0
- agentbundle/build/adapters/kiro.py +608 -0
- agentbundle/build/adapters/kiro_cli.py +53 -0
- agentbundle/build/adapters/kiro_ide.py +275 -0
- agentbundle/build/contract.py +20 -0
- agentbundle/build/lint_packs.py +555 -0
- agentbundle/build/main.py +596 -0
- agentbundle/build/phase_order.py +40 -0
- agentbundle/build/projections/__init__.py +13 -0
- agentbundle/build/projections/codex_agent_toml.py +232 -0
- agentbundle/build/projections/copilot_agent_md.py +206 -0
- agentbundle/build/projections/copilot_hooks_json.py +142 -0
- agentbundle/build/projections/direct_directory.py +41 -0
- agentbundle/build/projections/hook_id.py +27 -0
- agentbundle/build/projections/kiro_ide_hook.py +256 -0
- agentbundle/build/projections/merge_into_agent_json.py +264 -0
- agentbundle/build/projections/merge_json.py +58 -0
- agentbundle/build/projections/user_merge_json.py +324 -0
- agentbundle/build/scope_rails.py +728 -0
- agentbundle/build/self_host.py +1486 -0
- agentbundle/build/shared_libs.py +309 -0
- agentbundle/build/target_resolver.py +85 -0
- agentbundle/build/tests/__init__.py +0 -0
- agentbundle/build/tests/test_adapter_claude_code.py +275 -0
- agentbundle/build/tests/test_adapter_codex.py +699 -0
- agentbundle/build/tests/test_adapter_copilot.py +91 -0
- agentbundle/build/tests/test_adapter_kiro.py +449 -0
- agentbundle/build/tests/test_adapter_kiro_alias.py +105 -0
- agentbundle/build/tests/test_adapter_kiro_cli.py +102 -0
- agentbundle/build/tests/test_adapter_kiro_ide.py +173 -0
- agentbundle/build/tests/test_adapter_root_bins_projection.py +429 -0
- agentbundle/build/tests/test_build_ships_seeds.py +78 -0
- agentbundle/build/tests/test_contract.py +582 -0
- agentbundle/build/tests/test_contract_scope.py +224 -0
- agentbundle/build/tests/test_contract_v07.py +191 -0
- agentbundle/build/tests/test_contract_v08.py +230 -0
- agentbundle/build/tests/test_direct_directory_cleanup.py +65 -0
- agentbundle/build/tests/test_end_to_end_build.py +227 -0
- agentbundle/build/tests/test_lint_agents_md_legacy_block.py +135 -0
- agentbundle/build/tests/test_lint_agents_md_risk_block.py +116 -0
- agentbundle/build/tests/test_lint_packs.py +703 -0
- agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +176 -0
- agentbundle/build/tests/test_pack_schema.py +265 -0
- agentbundle/build/tests/test_pack_schema_allowed_adapters.py +258 -0
- agentbundle/build/tests/test_pack_schema_install.py +305 -0
- agentbundle/build/tests/test_pipeline.py +272 -0
- agentbundle/build/tests/test_plugin_manifest_schema.py +327 -0
- agentbundle/build/tests/test_projections_merge_json.py +148 -0
- agentbundle/build/tests/test_scope_rails.py +398 -0
- agentbundle/build/tests/test_security.py +97 -0
- agentbundle/build/tests/test_self_host_check.py +2100 -0
- agentbundle/build/tests/test_shared_libs_projection.py +415 -0
- agentbundle/build/tests/test_shipped_packs_v07_declarations.py +100 -0
- agentbundle/build/tests/test_shipped_packs_v08_declarations.py +80 -0
- agentbundle/build/tests/test_validate.py +250 -0
- agentbundle/build/validate.py +141 -0
- agentbundle/catalogue.py +164 -0
- agentbundle/cli.py +486 -0
- agentbundle/commands/__init__.py +5 -0
- agentbundle/commands/_common.py +174 -0
- agentbundle/commands/_drop_warning.py +329 -0
- agentbundle/commands/adapt.py +343 -0
- agentbundle/commands/config.py +125 -0
- agentbundle/commands/diff.py +211 -0
- agentbundle/commands/init_state.py +279 -0
- agentbundle/commands/install.py +3026 -0
- agentbundle/commands/list_packs.py +170 -0
- agentbundle/commands/list_targets.py +23 -0
- agentbundle/commands/reconcile.py +161 -0
- agentbundle/commands/render.py +165 -0
- agentbundle/commands/scaffold.py +69 -0
- agentbundle/commands/uninstall.py +294 -0
- agentbundle/commands/upgrade.py +699 -0
- agentbundle/commands/validate.py +688 -0
- agentbundle/config.py +747 -0
- agentbundle/render.py +123 -0
- agentbundle/safety.py +633 -0
- agentbundle/scope.py +319 -0
- agentbundle/user_config.py +284 -0
- agentbundle/version.py +49 -0
- agentbundle-0.2.0.dist-info/METADATA +37 -0
- agentbundle-0.2.0.dist-info/RECORD +99 -0
- agentbundle-0.2.0.dist-info/WHEEL +5 -0
- agentbundle-0.2.0.dist-info/entry_points.txt +2 -0
- agentbundle-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"""T6 (credential-broker-contract): adapter-root-bins/ build-pipeline
|
|
2
|
+
primitive class — AC22 / AC23.
|
|
3
|
+
|
|
4
|
+
Verifies:
|
|
5
|
+
- AC22: source-to-target projection at `<working_tree>/.agentbundle/bin/<basename>.py`
|
|
6
|
+
- AC22: POSIX mode 0o755
|
|
7
|
+
- AC22: path-jail compliance — the target falls under the v0.7
|
|
8
|
+
contract's `allowed-prefixes.repo` for `.agentbundle/`
|
|
9
|
+
- AC22: no PATH manipulation — `os.environ["PATH"]` unchanged
|
|
10
|
+
- AC23: drift gate distinguishes modified / missing / orphaned;
|
|
11
|
+
build-self resolves all three; inter-pack basename collision is hard-error
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import stat
|
|
18
|
+
import tempfile
|
|
19
|
+
import tomllib
|
|
20
|
+
import unittest
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from agentbundle.build import adapter_root_bins as arb
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _make_fixture_pack(
|
|
27
|
+
packs_dir: Path,
|
|
28
|
+
name: str,
|
|
29
|
+
bins: dict[str, bytes],
|
|
30
|
+
shared_libs: dict[str, bytes] | None = None,
|
|
31
|
+
) -> Path:
|
|
32
|
+
pack = packs_dir / name
|
|
33
|
+
pack.mkdir(parents=True)
|
|
34
|
+
(pack / "pack.toml").write_text(
|
|
35
|
+
f'[pack]\nname = "{name}"\nversion = "0.1.0"\ndescription = "fixture"\n'
|
|
36
|
+
f'[pack.adapter-contract]\nversion = "0.7"\n'
|
|
37
|
+
f'[pack.install]\ndefault-scope = "user"\n'
|
|
38
|
+
f'allowed-scopes = ["user", "repo"]\n',
|
|
39
|
+
encoding="utf-8",
|
|
40
|
+
)
|
|
41
|
+
if bins:
|
|
42
|
+
bins_dir = pack / ".apm" / "adapter-root-bins"
|
|
43
|
+
bins_dir.mkdir(parents=True)
|
|
44
|
+
for basename, content in bins.items():
|
|
45
|
+
(bins_dir / basename).write_bytes(content)
|
|
46
|
+
if shared_libs:
|
|
47
|
+
sl_dir = pack / ".apm" / "shared-libs"
|
|
48
|
+
sl_dir.mkdir(parents=True)
|
|
49
|
+
for basename, content in shared_libs.items():
|
|
50
|
+
(sl_dir / basename).write_bytes(content)
|
|
51
|
+
return pack
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AdapterRootBinsTests(unittest.TestCase):
|
|
55
|
+
def setUp(self) -> None:
|
|
56
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
57
|
+
self.tmp_path = Path(self._tmp.name)
|
|
58
|
+
|
|
59
|
+
def tearDown(self) -> None:
|
|
60
|
+
self._tmp.cleanup()
|
|
61
|
+
|
|
62
|
+
def _packs(self) -> Path:
|
|
63
|
+
packs = self.tmp_path / "packs"
|
|
64
|
+
packs.mkdir(exist_ok=True)
|
|
65
|
+
return packs
|
|
66
|
+
|
|
67
|
+
def _wt(self) -> Path:
|
|
68
|
+
wt = self.tmp_path / "wt"
|
|
69
|
+
wt.mkdir(exist_ok=True)
|
|
70
|
+
return wt
|
|
71
|
+
|
|
72
|
+
def test_collect_sources_returns_basename_map(self) -> None:
|
|
73
|
+
packs = self._packs()
|
|
74
|
+
_make_fixture_pack(packs, "p1", {"sso-broker.py": b"# stub\n"})
|
|
75
|
+
sources = arb.collect_sources(packs)
|
|
76
|
+
self.assertEqual(set(sources.keys()), {"sso-broker.py"})
|
|
77
|
+
self.assertEqual(sources["sso-broker.py"].read_bytes(), b"# stub\n")
|
|
78
|
+
|
|
79
|
+
def test_collect_sources_collision_hard_errors(self) -> None:
|
|
80
|
+
packs = self._packs()
|
|
81
|
+
_make_fixture_pack(packs, "p1", {"sso-broker.py": b"# p1\n"})
|
|
82
|
+
_make_fixture_pack(packs, "p2", {"sso-broker.py": b"# p2\n"})
|
|
83
|
+
with self.assertRaisesRegex(ValueError, "adapter-root-bins collision"):
|
|
84
|
+
arb.collect_sources(packs)
|
|
85
|
+
|
|
86
|
+
def test_apply_projection_writes_target_with_0755(self) -> None:
|
|
87
|
+
packs = self._packs()
|
|
88
|
+
_make_fixture_pack(packs, "p1", {"sso-broker.py": b"# real broker\n"})
|
|
89
|
+
wt = self._wt()
|
|
90
|
+
|
|
91
|
+
arb.apply_projection(wt, packs)
|
|
92
|
+
|
|
93
|
+
target = wt / ".agentbundle" / "bin" / "sso-broker.py"
|
|
94
|
+
self.assertTrue(target.is_file())
|
|
95
|
+
self.assertEqual(target.read_bytes(), b"# real broker\n")
|
|
96
|
+
if os.name == "posix":
|
|
97
|
+
mode = stat.S_IMODE(target.stat().st_mode)
|
|
98
|
+
self.assertEqual(
|
|
99
|
+
mode & 0o777, 0o755, f"expected mode 0755, got {oct(mode)}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def test_apply_projection_creates_target_dir(self) -> None:
|
|
103
|
+
packs = self._packs()
|
|
104
|
+
_make_fixture_pack(packs, "p1", {"sso-broker.py": b"# stub\n"})
|
|
105
|
+
wt = self._wt()
|
|
106
|
+
arb.apply_projection(wt, packs)
|
|
107
|
+
self.assertTrue((wt / ".agentbundle" / "bin").is_dir())
|
|
108
|
+
|
|
109
|
+
def test_apply_projection_overwrites_modified_target(self) -> None:
|
|
110
|
+
packs = self._packs()
|
|
111
|
+
_make_fixture_pack(packs, "p1", {"sso-broker.py": b"# source\n"})
|
|
112
|
+
wt = self._wt()
|
|
113
|
+
bin_dir = wt / ".agentbundle" / "bin"
|
|
114
|
+
bin_dir.mkdir(parents=True)
|
|
115
|
+
(bin_dir / "sso-broker.py").write_bytes(b"# stale\n")
|
|
116
|
+
|
|
117
|
+
arb.apply_projection(wt, packs)
|
|
118
|
+
|
|
119
|
+
self.assertEqual(
|
|
120
|
+
(bin_dir / "sso-broker.py").read_bytes(), b"# source\n"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def test_apply_projection_removes_orphan(self) -> None:
|
|
124
|
+
packs = self._packs()
|
|
125
|
+
_make_fixture_pack(packs, "p1", {"sso-broker.py": b"# real\n"})
|
|
126
|
+
wt = self._wt()
|
|
127
|
+
bin_dir = wt / ".agentbundle" / "bin"
|
|
128
|
+
bin_dir.mkdir(parents=True)
|
|
129
|
+
(bin_dir / "stale.py").write_bytes(b"# orphan\n")
|
|
130
|
+
|
|
131
|
+
arb.apply_projection(wt, packs)
|
|
132
|
+
|
|
133
|
+
self.assertFalse((bin_dir / "stale.py").exists())
|
|
134
|
+
self.assertTrue((bin_dir / "sso-broker.py").is_file())
|
|
135
|
+
|
|
136
|
+
def test_check_drift_clean_after_apply(self) -> None:
|
|
137
|
+
packs = self._packs()
|
|
138
|
+
_make_fixture_pack(packs, "p1", {"sso-broker.py": b"# real\n"})
|
|
139
|
+
wt = self._wt()
|
|
140
|
+
arb.apply_projection(wt, packs)
|
|
141
|
+
self.assertEqual(arb.check_drift(wt, packs), [])
|
|
142
|
+
|
|
143
|
+
def test_check_drift_modified_outcome(self) -> None:
|
|
144
|
+
packs = self._packs()
|
|
145
|
+
_make_fixture_pack(packs, "p1", {"sso-broker.py": b"# source\n"})
|
|
146
|
+
wt = self._wt()
|
|
147
|
+
arb.apply_projection(wt, packs)
|
|
148
|
+
target = wt / ".agentbundle" / "bin" / "sso-broker.py"
|
|
149
|
+
target.write_bytes(b"# tampered\n")
|
|
150
|
+
|
|
151
|
+
drifts = arb.check_drift(wt, packs)
|
|
152
|
+
self.assertEqual(len(drifts), 1)
|
|
153
|
+
self.assertIn("modified", drifts[0])
|
|
154
|
+
self.assertIn("make build-self", drifts[0])
|
|
155
|
+
|
|
156
|
+
def test_check_drift_missing_outcome(self) -> None:
|
|
157
|
+
packs = self._packs()
|
|
158
|
+
_make_fixture_pack(packs, "p1", {"sso-broker.py": b"# source\n"})
|
|
159
|
+
wt = self._wt()
|
|
160
|
+
# No apply_projection — target missing.
|
|
161
|
+
|
|
162
|
+
drifts = arb.check_drift(wt, packs)
|
|
163
|
+
self.assertEqual(len(drifts), 1)
|
|
164
|
+
self.assertIn("missing", drifts[0])
|
|
165
|
+
self.assertIn("make build-self", drifts[0])
|
|
166
|
+
|
|
167
|
+
def test_check_drift_orphaned_outcome(self) -> None:
|
|
168
|
+
packs = self._packs()
|
|
169
|
+
_make_fixture_pack(packs, "p1", {"sso-broker.py": b"# real\n"})
|
|
170
|
+
wt = self._wt()
|
|
171
|
+
arb.apply_projection(wt, packs)
|
|
172
|
+
orphan = wt / ".agentbundle" / "bin" / "phantom.py"
|
|
173
|
+
orphan.write_bytes(b"# orphan\n")
|
|
174
|
+
|
|
175
|
+
drifts = arb.check_drift(wt, packs)
|
|
176
|
+
self.assertTrue(any("orphaned" in d for d in drifts))
|
|
177
|
+
|
|
178
|
+
def test_check_drift_collision_short_circuits(self) -> None:
|
|
179
|
+
packs = self._packs()
|
|
180
|
+
_make_fixture_pack(packs, "p1", {"sso-broker.py": b"# p1\n"})
|
|
181
|
+
_make_fixture_pack(packs, "p2", {"sso-broker.py": b"# p2\n"})
|
|
182
|
+
wt = self._wt()
|
|
183
|
+
drifts = arb.check_drift(wt, packs)
|
|
184
|
+
self.assertEqual(len(drifts), 1)
|
|
185
|
+
self.assertIn("collision", drifts[0])
|
|
186
|
+
|
|
187
|
+
def test_no_path_manipulation(self) -> None:
|
|
188
|
+
"""AC22: os.environ['PATH'] is unchanged before/after apply_projection."""
|
|
189
|
+
packs = self._packs()
|
|
190
|
+
_make_fixture_pack(packs, "p1", {"sso-broker.py": b"# real\n"})
|
|
191
|
+
wt = self._wt()
|
|
192
|
+
path_before = os.environ.get("PATH", "")
|
|
193
|
+
arb.apply_projection(wt, packs)
|
|
194
|
+
path_after = os.environ.get("PATH", "")
|
|
195
|
+
self.assertEqual(path_before, path_after)
|
|
196
|
+
|
|
197
|
+
def test_path_jail_compliance_against_contract(self) -> None:
|
|
198
|
+
"""AC22 path-jail: `.agentbundle/` is in `allowed-prefixes.repo`
|
|
199
|
+
for the named user-scope adapters in the v0.7 contract."""
|
|
200
|
+
contract_path = (
|
|
201
|
+
Path(__file__).resolve().parents[2] / "_data" / "adapter.toml"
|
|
202
|
+
)
|
|
203
|
+
with contract_path.open("rb") as fh:
|
|
204
|
+
contract = tomllib.load(fh)
|
|
205
|
+
for adapter_name in ("claude-code", "kiro"):
|
|
206
|
+
prefixes = contract["adapter"][adapter_name]["scope"][
|
|
207
|
+
"allowed-prefixes"
|
|
208
|
+
]["repo"]
|
|
209
|
+
target_prefix = str(arb.TARGET_SUBDIR.parts[0]) + "/"
|
|
210
|
+
self.assertIn(
|
|
211
|
+
target_prefix, prefixes,
|
|
212
|
+
f"adapter {adapter_name!r}: {target_prefix!r} not in {prefixes!r}",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def test_real_pack_projection_against_credential_brokers(self) -> None:
|
|
216
|
+
"""Smoke test: the real credential-brokers pack source projects
|
|
217
|
+
into <tmp>/.agentbundle/bin/sso-broker.py with the real bytes."""
|
|
218
|
+
real_packs = Path(__file__).resolve().parents[5] / "packs"
|
|
219
|
+
if not (real_packs / "credential-brokers").is_dir():
|
|
220
|
+
self.skipTest("credential-brokers pack not present")
|
|
221
|
+
wt = self.tmp_path / "real-wt"
|
|
222
|
+
wt.mkdir()
|
|
223
|
+
arb.apply_projection(wt, real_packs)
|
|
224
|
+
target = wt / ".agentbundle" / "bin" / "sso-broker.py"
|
|
225
|
+
source = (
|
|
226
|
+
real_packs / "credential-brokers" / ".apm"
|
|
227
|
+
/ "adapter-root-bins" / "sso-broker.py"
|
|
228
|
+
)
|
|
229
|
+
self.assertEqual(target.read_bytes(), source.read_bytes())
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class AdapterRootBinsShimCompanionTests(unittest.TestCase):
|
|
233
|
+
"""AC22b: shim-companion projection alongside adapter-root-bins/.
|
|
234
|
+
|
|
235
|
+
Closes the deferred-projection gap from the credential
|
|
236
|
+
user-install fix — under bare user-scope
|
|
237
|
+
install, `_sso_*` modules' `from .credentials_shim import
|
|
238
|
+
Tier2HardFailError` previously failed and `sso-broker.py`'s
|
|
239
|
+
try/except cascade silently degraded `_tier2_backend` to `None`
|
|
240
|
+
on macOS / Windows.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def setUp(self) -> None:
|
|
244
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
245
|
+
self.tmp_path = Path(self._tmp.name)
|
|
246
|
+
|
|
247
|
+
def tearDown(self) -> None:
|
|
248
|
+
self._tmp.cleanup()
|
|
249
|
+
|
|
250
|
+
def _packs(self) -> Path:
|
|
251
|
+
packs = self.tmp_path / "packs"
|
|
252
|
+
packs.mkdir(exist_ok=True)
|
|
253
|
+
return packs
|
|
254
|
+
|
|
255
|
+
def _wt(self) -> Path:
|
|
256
|
+
wt = self.tmp_path / "wt"
|
|
257
|
+
wt.mkdir(exist_ok=True)
|
|
258
|
+
return wt
|
|
259
|
+
|
|
260
|
+
def test_apply_projection_writes_shim_companion(self) -> None:
|
|
261
|
+
"""AC22b: pack ships both adapter-root-bins/ and
|
|
262
|
+
shared-libs/credentials_shim.py — companion projected as a
|
|
263
|
+
sibling under bin/."""
|
|
264
|
+
packs = self._packs()
|
|
265
|
+
_make_fixture_pack(
|
|
266
|
+
packs,
|
|
267
|
+
"p1",
|
|
268
|
+
bins={"sso-broker.py": b"# broker\n"},
|
|
269
|
+
shared_libs={"credentials_shim.py": b"# shim\n"},
|
|
270
|
+
)
|
|
271
|
+
wt = self._wt()
|
|
272
|
+
arb.apply_projection(wt, packs)
|
|
273
|
+
bin_dir = wt / ".agentbundle" / "bin"
|
|
274
|
+
self.assertEqual((bin_dir / "sso-broker.py").read_bytes(), b"# broker\n")
|
|
275
|
+
self.assertEqual(
|
|
276
|
+
(bin_dir / "credentials_shim.py").read_bytes(), b"# shim\n"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def test_apply_projection_omits_companion_when_adapter_root_bins_absent(
|
|
280
|
+
self,
|
|
281
|
+
) -> None:
|
|
282
|
+
"""Opt-in by ship-both. A pack that ships only shared-libs/ —
|
|
283
|
+
no adapter-root-bins/ — does NOT trigger the bin/ companion."""
|
|
284
|
+
packs = self._packs()
|
|
285
|
+
# Use ``bins={}`` then strip the empty dir so the fixture
|
|
286
|
+
# really only has shared-libs/.
|
|
287
|
+
_make_fixture_pack(
|
|
288
|
+
packs,
|
|
289
|
+
"p1",
|
|
290
|
+
bins={},
|
|
291
|
+
shared_libs={"credentials_shim.py": b"# shim\n"},
|
|
292
|
+
)
|
|
293
|
+
wt = self._wt()
|
|
294
|
+
arb.apply_projection(wt, packs)
|
|
295
|
+
bin_dir = wt / ".agentbundle" / "bin"
|
|
296
|
+
# No adapter-root-bins source → no bin/ at all.
|
|
297
|
+
self.assertFalse(
|
|
298
|
+
(bin_dir / "credentials_shim.py").exists(),
|
|
299
|
+
"companion projected without an adapter-root-bins/ trigger",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def test_apply_projection_hard_errors_on_shim_import_without_companion(
|
|
303
|
+
self,
|
|
304
|
+
) -> None:
|
|
305
|
+
"""AC22b content-grep rail. A pack ships an adapter-root-bins
|
|
306
|
+
module that imports the shim, but does NOT ship the shim
|
|
307
|
+
source — refuse the build with the broker-agnostic message.
|
|
308
|
+
Uses a non-`_sso_*` basename to exercise the generalised
|
|
309
|
+
trigger (the rail must not be coupled to `_sso_*`)."""
|
|
310
|
+
packs = self._packs()
|
|
311
|
+
_make_fixture_pack(
|
|
312
|
+
packs,
|
|
313
|
+
"p1",
|
|
314
|
+
bins={
|
|
315
|
+
"oauth-broker.py": b"# stub\n",
|
|
316
|
+
"_oauth_macos.py": (
|
|
317
|
+
b"from .credentials_shim import Tier2HardFailError\n"
|
|
318
|
+
),
|
|
319
|
+
},
|
|
320
|
+
shared_libs=None, # NB: no credentials_shim.py in pack.
|
|
321
|
+
)
|
|
322
|
+
wt = self._wt()
|
|
323
|
+
with self.assertRaises(ValueError) as cm:
|
|
324
|
+
arb.apply_projection(wt, packs)
|
|
325
|
+
msg = str(cm.exception)
|
|
326
|
+
self.assertIn("_oauth_macos.py", msg)
|
|
327
|
+
self.assertIn("credentials_shim.py is missing", msg)
|
|
328
|
+
self.assertIn(
|
|
329
|
+
"Tier-2 dispatch would degrade silently on macOS/Windows", msg,
|
|
330
|
+
f"hard-error message must be broker-agnostic; got: {msg!r}",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def test_check_drift_modified_shim_companion_carries_prefix(self) -> None:
|
|
334
|
+
"""AC22b: companion drift descriptions use the
|
|
335
|
+
`[adapter-root-bins:shim-companion]` prefix so the source-side
|
|
336
|
+
reference (under `shared-libs/`) reads coherently."""
|
|
337
|
+
packs = self._packs()
|
|
338
|
+
_make_fixture_pack(
|
|
339
|
+
packs,
|
|
340
|
+
"p1",
|
|
341
|
+
bins={"sso-broker.py": b"# broker\n"},
|
|
342
|
+
shared_libs={"credentials_shim.py": b"# shim\n"},
|
|
343
|
+
)
|
|
344
|
+
wt = self._wt()
|
|
345
|
+
arb.apply_projection(wt, packs)
|
|
346
|
+
# Tamper the companion target.
|
|
347
|
+
(wt / ".agentbundle" / "bin" / "credentials_shim.py").write_bytes(
|
|
348
|
+
b"# tampered\n"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
drifts = arb.check_drift(wt, packs)
|
|
352
|
+
companion_drifts = [
|
|
353
|
+
d for d in drifts if "[adapter-root-bins:shim-companion]" in d
|
|
354
|
+
]
|
|
355
|
+
self.assertEqual(len(companion_drifts), 1, drifts)
|
|
356
|
+
self.assertIn("modified", companion_drifts[0])
|
|
357
|
+
# The companion source is rooted in shared-libs/ — the
|
|
358
|
+
# diagnostic reference must name that.
|
|
359
|
+
self.assertIn("shared-libs/credentials_shim.py", companion_drifts[0])
|
|
360
|
+
|
|
361
|
+
def test_check_drift_missing_shim_companion_carries_prefix(self) -> None:
|
|
362
|
+
"""Companion target absent → missing drift with the
|
|
363
|
+
shim-companion prefix."""
|
|
364
|
+
packs = self._packs()
|
|
365
|
+
_make_fixture_pack(
|
|
366
|
+
packs,
|
|
367
|
+
"p1",
|
|
368
|
+
bins={"sso-broker.py": b"# broker\n"},
|
|
369
|
+
shared_libs={"credentials_shim.py": b"# shim\n"},
|
|
370
|
+
)
|
|
371
|
+
wt = self._wt()
|
|
372
|
+
# No apply_projection — every target is missing. We isolate
|
|
373
|
+
# the companion's diagnostic shape.
|
|
374
|
+
drifts = arb.check_drift(wt, packs)
|
|
375
|
+
companion_missing = [
|
|
376
|
+
d for d in drifts
|
|
377
|
+
if "[adapter-root-bins:shim-companion]" in d and "missing" in d
|
|
378
|
+
]
|
|
379
|
+
self.assertEqual(len(companion_missing), 1, drifts)
|
|
380
|
+
|
|
381
|
+
def test_check_drift_orphaned_companion_not_misfiring(self) -> None:
|
|
382
|
+
"""The companion target must land in `expected_targets` so
|
|
383
|
+
the orphan rail does not flag it. After `apply_projection`,
|
|
384
|
+
`check_drift` returns no entries — and in particular no
|
|
385
|
+
`orphaned` entry referencing `credentials_shim.py`."""
|
|
386
|
+
packs = self._packs()
|
|
387
|
+
_make_fixture_pack(
|
|
388
|
+
packs,
|
|
389
|
+
"p1",
|
|
390
|
+
bins={"sso-broker.py": b"# broker\n"},
|
|
391
|
+
shared_libs={"credentials_shim.py": b"# shim\n"},
|
|
392
|
+
)
|
|
393
|
+
wt = self._wt()
|
|
394
|
+
arb.apply_projection(wt, packs)
|
|
395
|
+
drifts = arb.check_drift(wt, packs)
|
|
396
|
+
self.assertEqual(drifts, [])
|
|
397
|
+
# Explicit invariant: even if a future contributor relaxes the
|
|
398
|
+
# equality check above, the companion must never be reported
|
|
399
|
+
# as orphaned.
|
|
400
|
+
self.assertFalse(
|
|
401
|
+
any("orphaned" in d and "credentials_shim.py" in d for d in drifts),
|
|
402
|
+
f"orphan rail misfired on the shim companion: {drifts}",
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def test_real_pack_projection_includes_shim_companion(self) -> None:
|
|
406
|
+
"""Smoke test: the real `credential-brokers` pack projects
|
|
407
|
+
`credentials_shim.py` into `<wt>/.agentbundle/bin/` with the
|
|
408
|
+
real shared-libs source bytes."""
|
|
409
|
+
real_packs = Path(__file__).resolve().parents[5] / "packs"
|
|
410
|
+
if not (real_packs / "credential-brokers").is_dir():
|
|
411
|
+
self.skipTest("credential-brokers pack not present")
|
|
412
|
+
wt = self.tmp_path / "real-wt"
|
|
413
|
+
wt.mkdir()
|
|
414
|
+
arb.apply_projection(wt, real_packs)
|
|
415
|
+
target = wt / ".agentbundle" / "bin" / "credentials_shim.py"
|
|
416
|
+
source = (
|
|
417
|
+
real_packs / "credential-brokers" / ".apm"
|
|
418
|
+
/ "shared-libs" / "credentials_shim.py"
|
|
419
|
+
)
|
|
420
|
+
self.assertTrue(
|
|
421
|
+
target.is_file(),
|
|
422
|
+
"AC22b companion projection did not write "
|
|
423
|
+
"credentials_shim.py into bin/ from the real pack",
|
|
424
|
+
)
|
|
425
|
+
self.assertEqual(target.read_bytes(), source.read_bytes())
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
if __name__ == "__main__": # pragma: no cover
|
|
429
|
+
unittest.main()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""T5 (issue #190 Finding 1, dist half): the per-pack APM and Claude-plugin
|
|
2
|
+
build recipes ship the pack's seeds/ inside the artifact.
|
|
3
|
+
|
|
4
|
+
RFC-0001 §595 (APM) + §281-284 (both routes) require a pack's governance seeds
|
|
5
|
+
to travel inside the published artifact so the content is available on every
|
|
6
|
+
install route. The build never copied seeds/; this test drives the real build
|
|
7
|
+
pipeline against a seed-bearing temp pack and asserts seeds land in both
|
|
8
|
+
per-pack outputs. (`dist/` is gitignored, so this is verified by test, not by a
|
|
9
|
+
committed snapshot.)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
import unittest
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
REPO_ROOT = Path(__file__).resolve().parents[5]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _make_seed_pack(packs_dir: Path) -> None:
|
|
25
|
+
pack = packs_dir / "seedpack"
|
|
26
|
+
skill = pack / ".apm" / "skills" / "demo"
|
|
27
|
+
skill.mkdir(parents=True)
|
|
28
|
+
(skill / "SKILL.md").write_text(
|
|
29
|
+
"---\nname: demo\ndescription: A demo skill for the seed-ship build test.\n---\n\nBody.\n",
|
|
30
|
+
encoding="utf-8",
|
|
31
|
+
)
|
|
32
|
+
(pack / "pack.toml").write_text(
|
|
33
|
+
'[pack]\nname = "seedpack"\nversion = "0.1.0"\ndescription = "seed-ship test pack."\n',
|
|
34
|
+
encoding="utf-8",
|
|
35
|
+
)
|
|
36
|
+
(pack / ".claude-plugin").mkdir()
|
|
37
|
+
(pack / ".claude-plugin" / "plugin.json").write_text(
|
|
38
|
+
json.dumps({"name": "seedpack", "version": "0.1.0", "description": "seed-ship test pack."}) + "\n",
|
|
39
|
+
encoding="utf-8",
|
|
40
|
+
)
|
|
41
|
+
docs = pack / "seeds" / "docs"
|
|
42
|
+
docs.mkdir(parents=True)
|
|
43
|
+
(pack / "seeds" / "AGENTS.md").write_text("# seedpack agents\n", encoding="utf-8")
|
|
44
|
+
(docs / "CHARTER.md").write_text("# seedpack charter\n", encoding="utf-8")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BuildShipsSeedsTests(unittest.TestCase):
|
|
48
|
+
def test_seeds_shipped_in_both_artifacts(self) -> None:
|
|
49
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
50
|
+
tmp_path = Path(tmp)
|
|
51
|
+
packs_dir = tmp_path / "packs"
|
|
52
|
+
packs_dir.mkdir()
|
|
53
|
+
_make_seed_pack(packs_dir)
|
|
54
|
+
out = tmp_path / "dist"
|
|
55
|
+
|
|
56
|
+
result = subprocess.run(
|
|
57
|
+
[
|
|
58
|
+
sys.executable, "-m", "agentbundle.build", "build",
|
|
59
|
+
"--packs-dir", str(packs_dir),
|
|
60
|
+
"--output-dir", str(out),
|
|
61
|
+
],
|
|
62
|
+
capture_output=True, text=True, cwd=REPO_ROOT,
|
|
63
|
+
)
|
|
64
|
+
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
|
65
|
+
|
|
66
|
+
# Claude-plugin artifact carries seeds/.
|
|
67
|
+
plugin = out / "claude-plugins" / "seedpack" / "seeds"
|
|
68
|
+
self.assertTrue((plugin / "AGENTS.md").exists(), msg=result.stderr)
|
|
69
|
+
self.assertTrue((plugin / "docs" / "CHARTER.md").exists())
|
|
70
|
+
|
|
71
|
+
# APM artifact carries seeds/.
|
|
72
|
+
apm = out / "apm" / "seedpack" / "seeds"
|
|
73
|
+
self.assertTrue((apm / "AGENTS.md").exists())
|
|
74
|
+
self.assertTrue((apm / "docs" / "CHARTER.md").exists())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
unittest.main()
|