ai-kit-python 1.4.2__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.
Files changed (278) hide show
  1. ai_kit_python-1.4.2.dist-info/METADATA +132 -0
  2. ai_kit_python-1.4.2.dist-info/RECORD +278 -0
  3. ai_kit_python-1.4.2.dist-info/WHEEL +5 -0
  4. ai_kit_python-1.4.2.dist-info/entry_points.txt +2 -0
  5. ai_kit_python-1.4.2.dist-info/licenses/LICENSE +21 -0
  6. ai_kit_python-1.4.2.dist-info/licenses/template/ai-kit/vendor/security-audit/LICENSE +21 -0
  7. ai_kit_python-1.4.2.dist-info/licenses/template/ai-kit/vendor/strix/LICENSE +201 -0
  8. ai_kit_python-1.4.2.dist-info/top_level.txt +1 -0
  9. aikit/.agents/skills/go-work/SKILL.md +8 -0
  10. aikit/.agents/skills/laravel-work/SKILL.md +8 -0
  11. aikit/.agents/skills/moodle-work/SKILL.md +8 -0
  12. aikit/.agents/skills/project-continuity/SKILL.md +34 -0
  13. aikit/.agents/skills/python-work/SKILL.md +8 -0
  14. aikit/.dockerignore +90 -0
  15. aikit/.gitattributes +3 -0
  16. aikit/.github/workflows/check-kit.yml +70 -0
  17. aikit/.github/workflows/publish-pypi.yml +108 -0
  18. aikit/.gitignore +60 -0
  19. aikit/BOOTSTRAP_PROMPT.md +18 -0
  20. aikit/CHANGELOG.md +85 -0
  21. aikit/CLAUDE.md +3 -0
  22. aikit/CONTRIBUTING.md +46 -0
  23. aikit/Dockerfile +44 -0
  24. aikit/KIMI.md +3 -0
  25. aikit/LICENSE +21 -0
  26. aikit/MANIFEST.in +22 -0
  27. aikit/MANUS.md +3 -0
  28. aikit/Makefile +70 -0
  29. aikit/README.md +1161 -0
  30. aikit/SECURITY.md +8 -0
  31. aikit/SKILL.md +105 -0
  32. aikit/VERSION +1 -0
  33. aikit/__init__.py +1 -0
  34. aikit/ai-kit/ADAPTERS.md +5 -0
  35. aikit/ai-kit/CONTAINERS.md +5 -0
  36. aikit/ai-kit/ENGINEERING.md +5 -0
  37. aikit/ai-kit/INTENT.md +5 -0
  38. aikit/ai-kit/MODELS.md +5 -0
  39. aikit/ai-kit/SKILLS.md +5 -0
  40. aikit/ai-kit/stacks/GO.md +5 -0
  41. aikit/ai-kit/stacks/LARAVEL.md +5 -0
  42. aikit/ai-kit/stacks/MOODLE.md +5 -0
  43. aikit/ai-kit/stacks/PYTHON.md +5 -0
  44. aikit/aikit_cli.py +59 -0
  45. aikit/deploy/k8s/install-preview-job.yaml +56 -0
  46. aikit/docker/build-requirements.txt +4 -0
  47. aikit/docker/check_version.py +29 -0
  48. aikit/docker/pypi-requirements.txt +3 -0
  49. aikit/docs/ADAPTER_PATH_FIX.md +23 -0
  50. aikit/docs/BENCHMARK.md +106 -0
  51. aikit/docs/CONTEXT7.md +33 -0
  52. aikit/docs/DECISIONS.md +22 -0
  53. aikit/docs/DEVELOPMENT_PLAN.md +16 -0
  54. aikit/docs/DIALOGUE_MEMORY.md +70 -0
  55. aikit/docs/DOCKER.md +74 -0
  56. aikit/docs/DOCKER_HUB_OVERVIEW.md +311 -0
  57. aikit/docs/EVALUATION.md +30 -0
  58. aikit/docs/IMPLEMENTATION.md +134 -0
  59. aikit/docs/MANUS_REVIEW.md +46 -0
  60. aikit/docs/PYPI.md +87 -0
  61. aikit/docs/PYPI_DESCRIPTION.md +103 -0
  62. aikit/docs/SECURITY_AUDIT.md +26 -0
  63. aikit/docs/STRIX.md +28 -0
  64. aikit/docs/V0_5_0.md +27 -0
  65. aikit/docs/V0_6_4.md +25 -0
  66. aikit/docs/V1_4_0.md +73 -0
  67. aikit/docs/V1_4_1.md +55 -0
  68. aikit/docs/V1_4_2.md +33 -0
  69. aikit/docs/WEB_DESIGN.md +72 -0
  70. aikit/docs/WINDOWS_CI_FIX.md +39 -0
  71. aikit/docs/adr/0000-template.md +24 -0
  72. aikit/docs/adr/0001-adaptive-model-routing.md +30 -0
  73. aikit/docs/adr/0002-template-separation.md +24 -0
  74. aikit/docs/adr/0003-agent-registry-and-router.md +24 -0
  75. aikit/docs/adr/0004-onboarding-and-project-facts.md +24 -0
  76. aikit/docs/adr/0005-client-settings-ownership.md +30 -0
  77. aikit/docs/adr/0006-routing-evidence-and-local-measurements.md +26 -0
  78. aikit/docs/adr/0007-generated-client-configuration-and-presets.md +34 -0
  79. aikit/docs/adr/0008-unified-cli-and-fact-drift.md +20 -0
  80. aikit/docs/adr/0009-provider-review-doctor-fix-and-ci-self-install.md +20 -0
  81. aikit/docs/adr/0010-metrics-analysis-and-gemini-verification.md +20 -0
  82. aikit/docs/adr/0011-evidence-lifecycle-and-evaluation.md +20 -0
  83. aikit/docs/adr/0012-measurement-led-routing.md +31 -0
  84. aikit/docs/adr/0013-docker-cli-image.md +24 -0
  85. aikit/docs/adr/0014-local-dialogue-memory.md +22 -0
  86. aikit/docs/history/original-bundle-checks.md +11 -0
  87. aikit/docs/history/v0.1-changelog.txt +37 -0
  88. aikit/docs/history/v0.10-changelog.md +12 -0
  89. aikit/docs/history/v0.2-changelog.md +35 -0
  90. aikit/docs/history/v0.3-changelog.md +25 -0
  91. aikit/docs/history/v0.4-changelog.md +31 -0
  92. aikit/docs/history/v0.5-changelog.md +14 -0
  93. aikit/docs/history/v0.6.4-changelog.md +12 -0
  94. aikit/docs/history/v0.6.5-v0.9-changelog.md +35 -0
  95. aikit/docs/history/v1.1.0-changelog.md +12 -0
  96. aikit/docs/history/v1.2.0-changelog.md +11 -0
  97. aikit/docs/history/verification-before-measurements.md +53 -0
  98. aikit/integrations/CONVENTIONS.md +3 -0
  99. aikit/integrations/README.md +7 -0
  100. aikit/integrations/agents.json +136 -0
  101. aikit/integrations/ai-kit-check.yml +66 -0
  102. aikit/integrations/ai-kit-cline.md +4 -0
  103. aikit/integrations/ai-kit-roo.md +4 -0
  104. aikit/integrations/ai-kit-windsurf.md +4 -0
  105. aikit/integrations/ai-kit.mdc +6 -0
  106. aikit/integrations/claude-settings.data-ask.json +23 -0
  107. aikit/integrations/claude-settings.git-ask.json +10 -0
  108. aikit/integrations/copilot-instructions.md +3 -0
  109. aikit/integrations/security-audit.json +135 -0
  110. aikit/integrations/strix.json +69 -0
  111. aikit/pyproject.toml +60 -0
  112. aikit/scripts/__init__.py +1 -0
  113. aikit/scripts/adapters.py +184 -0
  114. aikit/scripts/adaptive.py +167 -0
  115. aikit/scripts/adr.py +61 -0
  116. aikit/scripts/benchmark.py +543 -0
  117. aikit/scripts/changelog.py +55 -0
  118. aikit/scripts/check_distribution.py +178 -0
  119. aikit/scripts/check_kit.py +406 -0
  120. aikit/scripts/context.py +184 -0
  121. aikit/scripts/docker_test_usage.py +55 -0
  122. aikit/scripts/doctor.py +313 -0
  123. aikit/scripts/evaluate.py +125 -0
  124. aikit/scripts/install.py +1584 -0
  125. aikit/scripts/learning.py +260 -0
  126. aikit/scripts/memory.py +344 -0
  127. aikit/scripts/memory_import.py +177 -0
  128. aikit/scripts/memory_store.py +125 -0
  129. aikit/scripts/metrics.py +351 -0
  130. aikit/scripts/router.py +399 -0
  131. aikit/template/.agents/hooks/session-start.md +3 -0
  132. aikit/template/.agents/hooks/session-start.sh +21 -0
  133. aikit/template/.agents/skills/ai-kit-bootstrap/SKILL.md +14 -0
  134. aikit/template/.agents/skills/ai-kit-review/SKILL.md +11 -0
  135. aikit/template/.agents/skills/ai-kit-sync-context/SKILL.md +12 -0
  136. aikit/template/.agents/skills/context7-docs/SKILL.md +11 -0
  137. aikit/template/.agents/skills/design-extract/SKILL.md +9 -0
  138. aikit/template/.agents/skills/dotnet-work/SKILL.md +9 -0
  139. aikit/template/.agents/skills/filament-work/SKILL.md +9 -0
  140. aikit/template/.agents/skills/frontend-work/SKILL.md +9 -0
  141. aikit/template/.agents/skills/go-work/SKILL.md +9 -0
  142. aikit/template/.agents/skills/infra-work/SKILL.md +9 -0
  143. aikit/template/.agents/skills/java-work/SKILL.md +9 -0
  144. aikit/template/.agents/skills/laravel-blade-ui/SKILL.md +9 -0
  145. aikit/template/.agents/skills/laravel-work/SKILL.md +10 -0
  146. aikit/template/.agents/skills/library-work/SKILL.md +9 -0
  147. aikit/template/.agents/skills/moodle-work/SKILL.md +9 -0
  148. aikit/template/.agents/skills/php-work/SKILL.md +9 -0
  149. aikit/template/.agents/skills/project-continuity/SKILL.md +21 -0
  150. aikit/template/.agents/skills/python-work/SKILL.md +9 -0
  151. aikit/template/.agents/skills/rust-work/SKILL.md +9 -0
  152. aikit/template/.agents/skills/security-audit/SKILL.md +16 -0
  153. aikit/template/.agents/skills/security-audit/agents/openai.yaml +2 -0
  154. aikit/template/.agents/skills/strix/SKILL.md +16 -0
  155. aikit/template/.agents/skills/strix/agents/openai.yaml +2 -0
  156. aikit/template/.agents/skills/ui-review/SKILL.md +9 -0
  157. aikit/template/.agents/skills/web-design/SKILL.md +9 -0
  158. aikit/template/AGENTS.md +16 -0
  159. aikit/template/CHANGELOG.md +6 -0
  160. aikit/template/CLAUDE.md +5 -0
  161. aikit/template/GEMINI.md +4 -0
  162. aikit/template/KIMI.md +4 -0
  163. aikit/template/MANUS.md +4 -0
  164. aikit/template/PROJECT_CONTEXT.md +35 -0
  165. aikit/template/README.md +6 -0
  166. aikit/template/WIKI.md +37 -0
  167. aikit/template/ai-kit/ADAPTERS.md +100 -0
  168. aikit/template/ai-kit/BOOTSTRAP.md +15 -0
  169. aikit/template/ai-kit/CONTAINERS.md +63 -0
  170. aikit/template/ai-kit/CONTEXT7.md +96 -0
  171. aikit/template/ai-kit/CORE.md +23 -0
  172. aikit/template/ai-kit/ENGINEERING.md +34 -0
  173. aikit/template/ai-kit/MCP.md +17 -0
  174. aikit/template/ai-kit/METRICS.md +83 -0
  175. aikit/template/ai-kit/SECURITY-AUDIT.md +21 -0
  176. aikit/template/ai-kit/SECURITY.md +34 -0
  177. aikit/template/ai-kit/SKILLS.md +37 -0
  178. aikit/template/ai-kit/STRIX.md +44 -0
  179. aikit/template/ai-kit/UPSTREAM.md +38 -0
  180. aikit/template/ai-kit/WEB-DESIGN.md +37 -0
  181. aikit/template/ai-kit/profiles/OWNER.md +19 -0
  182. aikit/template/ai-kit/project.json +14 -0
  183. aikit/template/ai-kit/router/POLICY.md +79 -0
  184. aikit/template/ai-kit/router/providers/anthropic.json +44 -0
  185. aikit/template/ai-kit/router/providers/anthropic.md +14 -0
  186. aikit/template/ai-kit/router/providers/gemini.json +13 -0
  187. aikit/template/ai-kit/router/providers/gemini.md +10 -0
  188. aikit/template/ai-kit/router/providers/kimi.json +26 -0
  189. aikit/template/ai-kit/router/providers/kimi.md +13 -0
  190. aikit/template/ai-kit/router/providers/local.json +18 -0
  191. aikit/template/ai-kit/router/providers/local.md +10 -0
  192. aikit/template/ai-kit/router/providers/openai.json +48 -0
  193. aikit/template/ai-kit/router/providers/openai.md +14 -0
  194. aikit/template/ai-kit/router/routing.json +11 -0
  195. aikit/template/ai-kit/router/selection.json +14 -0
  196. aikit/template/ai-kit/settings.json +11 -0
  197. aikit/template/ai-kit/stacks/DOTNET.md +13 -0
  198. aikit/template/ai-kit/stacks/FILAMENT.md +18 -0
  199. aikit/template/ai-kit/stacks/FRONTEND.md +9 -0
  200. aikit/template/ai-kit/stacks/GO.md +19 -0
  201. aikit/template/ai-kit/stacks/INFRA.md +12 -0
  202. aikit/template/ai-kit/stacks/JAVA.md +13 -0
  203. aikit/template/ai-kit/stacks/LARAVEL.md +15 -0
  204. aikit/template/ai-kit/stacks/LIBRARY.md +9 -0
  205. aikit/template/ai-kit/stacks/MOODLE.md +12 -0
  206. aikit/template/ai-kit/stacks/PHP.md +11 -0
  207. aikit/template/ai-kit/stacks/PYTHON.md +11 -0
  208. aikit/template/ai-kit/stacks/RUST.md +18 -0
  209. aikit/template/ai-kit/vendor/security-audit/AI-AND-LLM.md +83 -0
  210. aikit/template/ai-kit/vendor/security-audit/ATTACK-CLASSES.md +130 -0
  211. aikit/template/ai-kit/vendor/security-audit/CLIENT-SIDE.md +83 -0
  212. aikit/template/ai-kit/vendor/security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  213. aikit/template/ai-kit/vendor/security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  214. aikit/template/ai-kit/vendor/security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  215. aikit/template/ai-kit/vendor/security-audit/HUNTING.md +251 -0
  216. aikit/template/ai-kit/vendor/security-audit/LICENSE +21 -0
  217. aikit/template/ai-kit/vendor/security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  218. aikit/template/ai-kit/vendor/security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  219. aikit/template/ai-kit/vendor/security-audit/RECONNAISSANCE.md +156 -0
  220. aikit/template/ai-kit/vendor/security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  221. aikit/template/ai-kit/vendor/security-audit/SKILL.md +192 -0
  222. aikit/template/ai-kit/vendor/security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  223. aikit/template/ai-kit/vendor/security-audit/VALIDATION-AND-REPORTING.md +186 -0
  224. aikit/template/ai-kit/vendor/security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  225. aikit/template/ai-kit/vendor/security-audit/report-schema.json +461 -0
  226. aikit/template/ai-kit/vendor/security-audit/validate-coverage-ledger.cjs +872 -0
  227. aikit/template/ai-kit/vendor/security-audit/validate-coverage-ledger.test.cjs +740 -0
  228. aikit/template/ai-kit/vendor/security-audit/validate-findings.cjs +773 -0
  229. aikit/template/ai-kit/vendor/security-audit/validate-findings.test.cjs +652 -0
  230. aikit/template/ai-kit/vendor/strix/LICENSE +201 -0
  231. aikit/template/ai-kit/vendor/strix/api-security-testing/SKILL.md +61 -0
  232. aikit/template/ai-kit/vendor/strix/application-security-testing/SKILL.md +66 -0
  233. aikit/template/ai-kit/vendor/strix/ci-security-scanning-with-strix/SKILL.md +149 -0
  234. aikit/template/ai-kit/vendor/strix/find-security-vulnerabilities-in-code/SKILL.md +62 -0
  235. aikit/template/ai-kit/vendor/strix/fix-security-vulnerabilities-with-strix/SKILL.md +77 -0
  236. aikit/template/ai-kit/vendor/strix/managed-pentesting-with-strix/SKILL.md +321 -0
  237. aikit/template/ai-kit/vendor/strix/owasp-top-10-testing/SKILL.md +64 -0
  238. aikit/template/ai-kit/vendor/strix/penetration-testing-with-strix/SKILL.md +162 -0
  239. aikit/template/ai-kit/vendor/strix/web-app-penetration-testing/SKILL.md +54 -0
  240. aikit/template/ai-kit/web-design/LARAVEL.md +51 -0
  241. aikit/template/ai-kit/web-design/LIBRARIES.md +39 -0
  242. aikit/template/ai-kit/web-design/REVIEW.md +37 -0
  243. aikit/template/ai-kit/web-design/TOOLS.md +43 -0
  244. aikit/template/docs/DECISIONS.md +7 -0
  245. aikit/template/docs/adr/0000-template.md +20 -0
  246. aikit/template/docs/design/ACCEPTANCE.md +24 -0
  247. aikit/template/docs/design/BRIEF.md +26 -0
  248. aikit/template/docs/design/SYSTEM.md +30 -0
  249. aikit/templates/agent-secrets.ignore +19 -0
  250. aikit/templates/gitignore.private +160 -0
  251. aikit/templates/gitignore.team +139 -0
  252. aikit/tests/build_distribution.py +73 -0
  253. aikit/tests/docker_smoke.py +172 -0
  254. aikit/tests/fixtures/create-junction.ps1 +18 -0
  255. aikit/tests/fixtures/golden/claude-settings-local.json +58 -0
  256. aikit/tests/test_adapters.py +105 -0
  257. aikit/tests/test_aikit.py +79 -0
  258. aikit/tests/test_benchmark.py +241 -0
  259. aikit/tests/test_check_kit.py +203 -0
  260. aikit/tests/test_context.py +76 -0
  261. aikit/tests/test_context7.py +153 -0
  262. aikit/tests/test_context_selection.py +70 -0
  263. aikit/tests/test_distribution.py +181 -0
  264. aikit/tests/test_docker_image.py +63 -0
  265. aikit/tests/test_docker_test_usage.py +54 -0
  266. aikit/tests/test_doctor.py +148 -0
  267. aikit/tests/test_evaluate.py +66 -0
  268. aikit/tests/test_install.py +939 -0
  269. aikit/tests/test_learning.py +247 -0
  270. aikit/tests/test_memory.py +371 -0
  271. aikit/tests/test_metrics.py +209 -0
  272. aikit/tests/test_native.py +342 -0
  273. aikit/tests/test_onboarding.py +301 -0
  274. aikit/tests/test_router.py +336 -0
  275. aikit/tests/test_security_audit.py +237 -0
  276. aikit/tests/test_strix.py +245 -0
  277. aikit/tests/test_web_design.py +219 -0
  278. aikit/tests/wheel_smoke.py +104 -0
@@ -0,0 +1,153 @@
1
+ """Context7 workflows remain optional, offline and separate from native MCP activation."""
2
+ from __future__ import annotations
3
+
4
+ import contextlib
5
+ import io
6
+ import json
7
+ from pathlib import Path
8
+ import shutil
9
+ import sys
10
+ import tempfile
11
+ import unittest
12
+ from unittest.mock import patch
13
+
14
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
15
+ import doctor
16
+ import install
17
+
18
+ ENTRY = ".agents/skills/context7-docs/SKILL.md"
19
+ NATIVE = ".claude/skills/context7-docs/SKILL.md"
20
+
21
+
22
+ class Context7Tests(unittest.TestCase):
23
+ def setUp(self):
24
+ self.temp = tempfile.TemporaryDirectory(prefix="ai-kit-context7-")
25
+ self.addCleanup(self.temp.cleanup)
26
+ base = Path(self.temp.name).resolve()
27
+ self.source = base / "reference"
28
+ self.target = base / "project with spaces"
29
+ shutil.copytree(install.ROOT, self.source, ignore=shutil.ignore_patterns("__pycache__", ".git"))
30
+
31
+ def plan(self, **options):
32
+ return install.build_plan(self.target, root=self.source, **options)
33
+
34
+ def apply(self, **options):
35
+ plan = self.plan(**options)
36
+ self.assertFalse(install.needs_review(plan), plan)
37
+ self.assertTrue(install.apply_plan(plan))
38
+ return plan
39
+
40
+ def test_default_and_presets_omit_skill_and_registry_entry(self):
41
+ for preset in (None, *install.PRESETS):
42
+ with self.subTest(preset=preset):
43
+ plan = self.plan(preset=preset, agents=["codex", "claude"])
44
+ self.assertNotIn("context7", plan["extras"])
45
+ self.assertNotIn(ENTRY, plan["actions"])
46
+ self.assertNotIn(NATIVE, plan["actions"])
47
+ self.assertNotIn(b"context7-docs/SKILL.md", plan["actions"]["ai-kit/SKILLS.md"]["data"])
48
+ self.apply()
49
+ self.assertEqual(doctor.link_warnings(self.target), [])
50
+
51
+ def test_apply_is_offline_and_preserves_native_mcp_configuration(self):
52
+ paths = (".codex/config.toml", ".mcp.json", ".cursor/mcp.json", ".gemini/settings.json")
53
+ for path in paths:
54
+ existing = self.target / path
55
+ existing.parent.mkdir(parents=True, exist_ok=True)
56
+ existing.write_bytes(b"Existing client-owned configuration.\n")
57
+ with patch("subprocess.Popen", side_effect=AssertionError("No client or package process")), \
58
+ patch("socket.socket", side_effect=AssertionError("No network connection")):
59
+ plan = self.apply(extras=["context7"], agents=["codex", "claude", "cursor"])
60
+ for path in paths:
61
+ self.assertNotIn(path, plan["actions"])
62
+ self.assertEqual((self.target / path).read_bytes(), b"Existing client-owned configuration.\n")
63
+ original = (self.source / "template" / ENTRY).read_bytes()
64
+ self.assertEqual((self.target / ENTRY).read_bytes(), original)
65
+ self.assertEqual((self.target / NATIVE).read_bytes(), original)
66
+ self.assertNotIn(b"disable-model-invocation", original)
67
+ self.assertEqual(doctor.examine(self.target, root=self.source)["errors"], [])
68
+ self.assertEqual(doctor.link_warnings(self.target), [])
69
+ self.assertEqual(self.plan()["actions"], {})
70
+
71
+ def test_selection_persists_independently_of_other_integrations_and_presets(self):
72
+ self.apply(extras=["context7"])
73
+ self.assertFalse((self.target / "ai-kit/vendor/security-audit").exists())
74
+ self.assertFalse((self.target / "ai-kit/vendor/strix").exists())
75
+ self.apply(preset="minimal")
76
+ self.assertEqual(self.plan()["extras"], ["context7"])
77
+ self.apply(extras=["security-audit", "strix"], agents=["codex", "claude"])
78
+ self.assertEqual(self.plan()["extras"], ["context7", "security-audit", "strix"])
79
+ settings = json.loads((self.target / "ai-kit/settings.json").read_text(encoding="utf-8"))
80
+ self.assertIs(settings["context7"], True)
81
+ self.assertEqual(self.plan()["actions"], {})
82
+
83
+ def test_local_edits_and_source_upgrade_preserve_conflicting_files(self):
84
+ self.apply(extras=["context7"])
85
+ path = self.target / ENTRY
86
+ local = path.read_bytes() + b"\nProject-specific documentation scope.\n"
87
+ path.write_bytes(local)
88
+ self.assertEqual(self.plan()["actions"], {})
89
+ source = self.source / "template" / ENTRY
90
+ incoming = source.read_bytes() + b"\nUpdated documentation workflow.\n"
91
+ source.write_bytes(incoming)
92
+ old_state = (self.target / "ai-kit/.install-state.json").read_bytes()
93
+ plan = self.plan()
94
+ self.assertIn(ENTRY, plan["conflicts"])
95
+ self.assertFalse(install.apply_plan(plan))
96
+ self.assertEqual(path.read_bytes(), local)
97
+ self.assertEqual((self.target / "ai-kit/.install-state.json").read_bytes(), old_state)
98
+ self.assertEqual((self.target / plan["candidates"][ENTRY]["path"]).read_bytes(), incoming)
99
+
100
+ def test_untracked_skill_conflict_blocks_install_before_managed_writes(self):
101
+ path = self.target / ENTRY
102
+ path.parent.mkdir(parents=True)
103
+ path.write_bytes(b"Existing skill.\n")
104
+ plan = self.plan(extras=["context7"])
105
+ self.assertIn(ENTRY, plan["conflicts"])
106
+ self.assertFalse(install.apply_plan(plan))
107
+ self.assertEqual(path.read_bytes(), b"Existing skill.\n")
108
+ self.assertFalse((self.target / "ai-kit/settings.json").exists())
109
+
110
+ def test_deselection_retains_files_and_native_retirement_requires_review(self):
111
+ self.apply(extras=["context7"], agents=["codex", "claude"])
112
+ plan = self.plan(agents=["codex"])
113
+ self.assertIn(NATIVE, plan["adapter_conflicts"])
114
+ self.assertFalse(install.apply_plan(plan))
115
+ settings_path = self.target / "ai-kit/settings.json"
116
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
117
+ settings["context7"] = False
118
+ settings_path.write_text(json.dumps(settings) + "\n", encoding="utf-8")
119
+ old = (self.target / ENTRY).read_bytes()
120
+ plan = self.apply()
121
+ self.assertNotIn("context7", plan["extras"])
122
+ self.assertEqual((self.target / ENTRY).read_bytes(), old)
123
+ self.assertTrue(any("Previously managed path retained" in warning and "context7-docs" in warning
124
+ for warning in plan["warnings"]))
125
+ self.assertEqual(doctor.link_warnings(self.target), [])
126
+
127
+ def test_invalid_choice_and_blocked_skill_parent_are_refused(self):
128
+ defaults_path = self.source / "template/ai-kit/settings.json"
129
+ defaults = json.loads(defaults_path.read_text(encoding="utf-8"))
130
+ defaults["context7"] = "true"
131
+ defaults_path.write_text(json.dumps(defaults) + "\n", encoding="utf-8")
132
+ with self.assertRaisesRegex(ValueError, "context7"):
133
+ self.plan(extras=["context7"])
134
+ defaults.pop("context7")
135
+ defaults_path.write_text(json.dumps(defaults) + "\n", encoding="utf-8")
136
+ parent = (self.target / ENTRY).parent
137
+ parent.parent.mkdir(parents=True)
138
+ parent.write_bytes(b"Blocked directory.\n")
139
+ with self.assertRaises(NotADirectoryError):
140
+ self.plan(extras=["context7"])
141
+ self.assertFalse((self.target / "ai-kit/settings.json").exists())
142
+ self.assertEqual(parent.read_bytes(), b"Blocked directory.\n")
143
+
144
+ def test_cli_flag_only_previews_until_apply_is_requested(self):
145
+ output = io.StringIO()
146
+ with contextlib.redirect_stdout(output):
147
+ self.assertEqual(install.main([str(self.target), "--with-context7"]), 0)
148
+ self.assertFalse(self.target.exists())
149
+ self.assertIn("context7", json.loads(output.getvalue())["extras"])
150
+
151
+
152
+ if __name__ == "__main__":
153
+ unittest.main()
@@ -0,0 +1,70 @@
1
+ """Module-scoped instruction selection, immutable critical rules and byte-budget transparency."""
2
+ import json
3
+ from pathlib import Path
4
+ import sys
5
+ import tempfile
6
+ import unittest
7
+ from unittest import mock
8
+
9
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
10
+ import context
11
+ import install
12
+
13
+
14
+ class SelectionTests(unittest.TestCase):
15
+ def setUp(self):
16
+ self.temp = tempfile.TemporaryDirectory()
17
+ self.addCleanup(self.temp.cleanup)
18
+ self.root = Path(self.temp.name).resolve()
19
+ for name in ("AGENTS.md", "ai-kit/CORE.md", "ai-kit/settings.json", "PROJECT_CONTEXT.md",
20
+ "ai-kit/ENGINEERING.md", "ai-kit/SECURITY.md", "docs/DECISIONS.md",
21
+ "docs/adr/0001-module-boundary.md", "ai-kit/stacks/PYTHON.md", "ai-kit/stacks/GO.md"):
22
+ path = self.root / name
23
+ path.parent.mkdir(parents=True, exist_ok=True)
24
+ path.write_text(json.dumps({"conventions": "owner"}) if name.endswith(".json") else "Rules: " + name, encoding="utf-8")
25
+ (self.root / "ai-kit/project.json").write_text(json.dumps({"schema": 1, "modules": [
26
+ {"path": "/api/", "profiles": ["PYTHON"]}, {"path": "/worker/", "profiles": ["GO"]}]}), encoding="utf-8")
27
+
28
+ def test_only_matching_profiles_are_selected_and_no_file_is_changed(self):
29
+ before = {p.relative_to(self.root): p.read_bytes() for p in self.root.rglob("*") if p.is_file()}
30
+ report = context.select_context(self.root, path="api/services")
31
+ names = {item["path"] for item in report["selected"]}
32
+ self.assertIn("ai-kit/stacks/PYTHON.md", names)
33
+ self.assertNotIn("ai-kit/stacks/GO.md", names)
34
+ self.assertIn("docs/adr/0001-module-boundary.md", names)
35
+ self.assertEqual(report["modules"], ["/api/"])
36
+ self.assertEqual(before, {p.relative_to(self.root): p.read_bytes() for p in self.root.rglob("*") if p.is_file()})
37
+
38
+ def test_critical_context_survives_budget_and_duplicates_are_reported(self):
39
+ (self.root / "ai-kit/ENGINEERING.md").write_bytes((self.root / "ai-kit/SECURITY.md").read_bytes())
40
+ report = context.select_context(self.root, path="worker", budget=1)
41
+ self.assertTrue(report["over_budget"])
42
+ self.assertTrue(any(r["path"] == "ai-kit/CORE.md" for r in report["selected"]))
43
+ self.assertEqual(len(report["duplicates"]), 1)
44
+ self.assertEqual(report["total_bytes"], sum(r["bytes"] for r in report["selected"]))
45
+
46
+ def test_unknown_scope_is_not_given_sibling_stack_facts(self):
47
+ report = context.select_context(self.root, path="unknown")
48
+ self.assertFalse(report["selection_complete"])
49
+ self.assertEqual(report["profiles"], [])
50
+
51
+ def test_nested_module_overrides_parent_stack_but_preserves_shared_requirements(self):
52
+ path = self.root / "ai-kit/project.json"
53
+ data = json.loads(path.read_text())
54
+ data["modules"].append({"path": "/", "profiles": ["LARAVEL"]})
55
+ path.write_text(json.dumps(data), encoding="utf-8")
56
+ report = context.select_context(self.root, path="worker")
57
+ self.assertEqual(report["profiles"], ["GO"])
58
+ self.assertTrue(any(r["path"] == "ai-kit/SECURITY.md" for r in report["selected"]))
59
+
60
+ def test_traversal_link_and_missing_profile_are_refused(self):
61
+ for scope in ("../outside", "/api", "C:/outside"):
62
+ with self.subTest(scope=scope), self.assertRaises(ValueError):
63
+ context.select_context(self.root, path=scope)
64
+ original = install.is_link
65
+ with mock.patch.object(install, "is_link", side_effect=lambda p: p.name == "CORE.md" or original(p)):
66
+ with self.assertRaises(ValueError):
67
+ context.select_context(self.root, path="api")
68
+ (self.root / "ai-kit/stacks/PYTHON.md").unlink()
69
+ with self.assertRaisesRegex(ValueError, "Missing required"):
70
+ context.select_context(self.root, path="api")
@@ -0,0 +1,181 @@
1
+ """Exercise publication guards and built-artifact privacy/resource contracts offline."""
2
+ import io
3
+ from pathlib import Path
4
+ import sys
5
+ import tarfile
6
+ import tempfile
7
+ import unittest
8
+ import zipfile
9
+
10
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
11
+ import check_distribution as release
12
+ import build_distribution
13
+
14
+ VERSION = "1.4.1"
15
+ DESCRIPTION = b"# AI-KIT\n\nPortable project instructions.\n"
16
+
17
+
18
+ class DistributionTests(unittest.TestCase):
19
+ def setUp(self):
20
+ self.temporary = tempfile.TemporaryDirectory(prefix="aikit-package-")
21
+ self.addCleanup(self.temporary.cleanup)
22
+ self.directory = Path(self.temporary.name)
23
+
24
+ def metadata(self, *, version=VERSION, extra="", description=DESCRIPTION):
25
+ headers = ("Metadata-Version: 2.4\nName: ai-kit-python\nVersion: " + version + "\n"
26
+ "Requires-Python: >=3.10\nLicense-Expression: MIT AND Apache-2.0\n"
27
+ "Author: Igor Sazonov\nDescription-Content-Type: text/markdown\n")
28
+ headers += "".join("License-File: " + name + "\n" for name in release.LICENSES)
29
+ headers += "".join("Project-URL: " + name + ", https://example.invalid/\n"
30
+ for name in ("Homepage", "Documentation", "Source", "Issues", "Changelog"))
31
+ return (headers + extra + "\n").encode() + description
32
+
33
+ def payload(self, *, description=DESCRIPTION):
34
+ result = {name: b"Public resource.\n" for name in release.REQUIRED}
35
+ result["VERSION"] = (VERSION + "\n").encode()
36
+ result["docs/PYPI_DESCRIPTION.md"] = description
37
+ return result
38
+
39
+ def wheel(self, *, payload=None, metadata=None, mutate=None):
40
+ payload = self.payload() if payload is None else payload
41
+ files = {"aikit/" + name: data for name, data in payload.items()}
42
+ prefix = "ai_kit_python-" + VERSION + ".dist-info/"
43
+ files[prefix + "METADATA"] = self.metadata() if metadata is None else metadata
44
+ files[prefix + "entry_points.txt"] = b"[console_scripts]\naikit = aikit.aikit_cli:main\n"
45
+ files[prefix + "WHEEL"] = b"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n"
46
+ for name in release.LICENSES:
47
+ files[prefix + "licenses/" + name] = payload.get(name, b"")
48
+ if mutate:
49
+ mutate(files, prefix)
50
+ path = self.directory / ("ai_kit_python-" + VERSION + "-py3-none-any.whl")
51
+ with zipfile.ZipFile(path, "w") as archive:
52
+ for name, data in files.items():
53
+ archive.writestr(name, data)
54
+ return path
55
+
56
+ def sdist(self, *, metadata=None):
57
+ path = self.directory / ("ai_kit_python-" + VERSION + ".tar.gz")
58
+ files = {**self.payload(), "PKG-INFO": self.metadata() if metadata is None else metadata}
59
+ with tarfile.open(path, "w:gz") as archive:
60
+ for name, data in files.items():
61
+ item = tarfile.TarInfo("ai_kit_python-" + VERSION + "/" + name)
62
+ item.size = len(data)
63
+ archive.addfile(item, io.BytesIO(data))
64
+ return path
65
+
66
+ def test_valid_wheel_and_sdist_keep_templates_licenses_and_same_version(self):
67
+ self.wheel()
68
+ self.sdist()
69
+ reports = release.check_archives(self.directory, VERSION)
70
+ self.assertEqual(len(reports), 2)
71
+ self.assertTrue(all(report["version"] == VERSION and len(report["sha256"]) == 64 for report in reports))
72
+
73
+ def test_archive_version_mismatch_refuses(self):
74
+ path = self.wheel(metadata=self.metadata(version="1.3.0"))
75
+ with self.assertRaisesRegex(ValueError, "name/version"):
76
+ release.inspect_distribution(path, VERSION)
77
+
78
+ def test_legacy_distribution_name_refuses(self):
79
+ metadata = self.metadata().replace(b"Name: ai-kit-python\n", b"Name: ai-kit\n")
80
+ for path in (self.wheel(metadata=metadata), self.sdist(metadata=metadata)):
81
+ with self.assertRaisesRegex(ValueError, "name/version"):
82
+ release.inspect_distribution(path, VERSION)
83
+
84
+ def test_unexpected_runtime_dependency_refuses(self):
85
+ path = self.wheel(metadata=self.metadata(extra="Requires-Dist: unexpected-package\n"))
86
+ with self.assertRaisesRegex(ValueError, "dependency contract"):
87
+ release.inspect_distribution(path, VERSION)
88
+
89
+ def test_private_root_documents_and_memory_refuse_but_templates_remain_required(self):
90
+ for name in ("AGENTS.md", "PROJECT_CONTEXT.md", "WIKI.md", "ai-kit/.memory/raw.json",
91
+ "ai-kit/.design/raw.md", "docs/.private/raw.md", ".env", "auth.json", "docs/private.pem"):
92
+ with self.subTest(name=name):
93
+ path = self.wheel(payload={**self.payload(), name: b"Synthetic private content."})
94
+ with self.assertRaisesRegex(ValueError, "Private local|Credential"):
95
+ release.inspect_distribution(path, VERSION)
96
+ payload = self.payload()
97
+ del payload["template/AGENTS.md"]
98
+ with self.assertRaisesRegex(ValueError, "required distribution resources"):
99
+ release.inspect_distribution(self.wheel(payload=payload), VERSION)
100
+
101
+ def test_missing_cli_and_changed_license_payload_refuse(self):
102
+ for suffix in ("entry_points.txt", "licenses/LICENSE"):
103
+ with self.subTest(suffix=suffix):
104
+ path = self.wheel(mutate=lambda files, prefix: files.__setitem__(prefix + suffix, b"changed\n"))
105
+ with self.assertRaises((ValueError, release.configparser.Error)):
106
+ release.inspect_distribution(path, VERSION)
107
+
108
+ def test_source_description_and_portable_links_are_checked(self):
109
+ with self.assertRaisesRegex(ValueError, "differs from"):
110
+ release.inspect_distribution(self.wheel(metadata=self.metadata(description=b"Other description.\n")), VERSION)
111
+ for text in (b"[Guide](../README.md)\n", b"```mermaid\ngraph LR\n```\n"):
112
+ with self.subTest(text=text):
113
+ path = self.wheel(payload=self.payload(description=text), metadata=self.metadata(description=text))
114
+ with self.assertRaisesRegex(ValueError, "nonportable"):
115
+ release.inspect_distribution(path, VERSION)
116
+
117
+ def test_missing_or_multiple_archives_refuse(self):
118
+ self.wheel()
119
+ with self.assertRaisesRegex(ValueError, "exactly one"):
120
+ release.check_archives(self.directory, VERSION)
121
+ self.sdist()
122
+ (self.directory / "extra.whl").write_bytes(b"not a wheel")
123
+ with self.assertRaisesRegex(ValueError, "exactly one"):
124
+ release.check_archives(self.directory, VERSION)
125
+
126
+ def test_traversal_and_linked_members_refuse_without_extraction(self):
127
+ path = self.wheel()
128
+ with zipfile.ZipFile(path, "a") as archive:
129
+ archive.writestr("../outside", b"synthetic")
130
+ with self.assertRaisesRegex(ValueError, "Unsafe"):
131
+ release.inspect_distribution(path, VERSION)
132
+ self.assertFalse((self.directory.parent / "outside").exists())
133
+ path = self.wheel()
134
+ with zipfile.ZipFile(path, "a") as archive:
135
+ item = zipfile.ZipInfo("aikit/linked")
136
+ item.create_system = 3
137
+ item.external_attr = 0o120777 << 16
138
+ archive.writestr(item, "outside")
139
+ with self.assertRaisesRegex(ValueError, "Linked"):
140
+ release.inspect_distribution(path, VERSION)
141
+ path = self.sdist()
142
+ with tarfile.open(path, "w:gz") as archive:
143
+ item = tarfile.TarInfo("source/linked")
144
+ item.type = tarfile.SYMTYPE
145
+ item.linkname = "outside"
146
+ archive.addfile(item)
147
+ with self.assertRaisesRegex(ValueError, "Linked"):
148
+ release.inspect_distribution(path, VERSION)
149
+
150
+ def test_default_validation_is_local_and_allows_review_branches(self):
151
+ result = release.release_identity(VERSION, "", "validate", "refs/heads/review", "example/fork")
152
+ self.assertFalse(result["upload_performed"])
153
+
154
+ def test_build_output_inside_source_refuses_before_writing(self):
155
+ with self.assertRaisesRegex(ValueError, "outside the source"):
156
+ build_distribution.prepare(build_distribution.ROOT / "dist")
157
+
158
+ def test_registry_upload_requires_exact_version_tag_and_canonical_repository(self):
159
+ for destination in ("testpypi", "pypi"):
160
+ for expected, ref, repository in (
161
+ ("", "refs/tags/v" + VERSION, "tigusigalpa/ai-kit"),
162
+ ("1.3.0", "refs/tags/v" + VERSION, "tigusigalpa/ai-kit"),
163
+ (VERSION, "refs/heads/main", "tigusigalpa/ai-kit"),
164
+ (VERSION, "refs/tags/v1.3.0", "tigusigalpa/ai-kit"),
165
+ (VERSION, "refs/tags/v" + VERSION, "example/fork"),
166
+ ):
167
+ with self.subTest(destination=destination, expected=expected, ref=ref, repository=repository):
168
+ with self.assertRaises(ValueError):
169
+ release.release_identity(VERSION, expected, destination, ref, repository)
170
+ result = release.release_identity(VERSION, VERSION, destination, "refs/tags/v" + VERSION, "tigusigalpa/ai-kit")
171
+ self.assertFalse(result["upload_performed"])
172
+
173
+ def test_invalid_versions_destinations_and_mismatched_validation_refuse(self):
174
+ for version, expected, destination in (("1.4", "", "validate"), (VERSION, "1.3.0", "validate"), (VERSION, "", "other")):
175
+ with self.subTest(version=version, destination=destination), self.assertRaises(ValueError):
176
+ release.release_identity(version, expected, destination, "", "")
177
+ self.assertEqual(release.release_identity("1.5.0rc1", "", "validate", "", "")["version"], "1.5.0rc1")
178
+
179
+
180
+ if __name__ == "__main__":
181
+ unittest.main()
@@ -0,0 +1,63 @@
1
+ """Version binding and safe CLI image smoke argument construction."""
2
+ import importlib.util
3
+ import os
4
+ from pathlib import Path
5
+ import tempfile
6
+ import unittest
7
+ from unittest.mock import patch
8
+
9
+
10
+ def module(path, name):
11
+ spec = importlib.util.spec_from_file_location(name, path)
12
+ result = importlib.util.module_from_spec(spec)
13
+ spec.loader.exec_module(result)
14
+ return result
15
+
16
+
17
+ ROOT = Path(__file__).resolve().parents[1]
18
+ version_check = module(ROOT / "docker/check_version.py", "aikit_image_version_check")
19
+ smoke = module(ROOT / "tests/docker_smoke.py", "aikit_image_smoke")
20
+
21
+
22
+ class DockerImageTests(unittest.TestCase):
23
+ def test_matching_version_returns_the_bundled_value(self):
24
+ with tempfile.TemporaryDirectory() as folder:
25
+ path = Path(folder) / "VERSION"
26
+ path.write_bytes(b"1.3.0\r\n")
27
+ self.assertEqual(version_check.check_version(path, "1.3.0"), "1.3.0")
28
+
29
+ def test_missing_or_wrong_image_label_is_refused(self):
30
+ with tempfile.TemporaryDirectory() as folder:
31
+ path = Path(folder) / "VERSION"
32
+ path.write_text("1.3.0\n", encoding="utf-8")
33
+ for expected in (None, "", "1.2.0"):
34
+ with self.subTest(expected=expected), self.assertRaises(ValueError):
35
+ version_check.check_version(path, expected)
36
+
37
+ def test_mount_paths_with_spaces_are_one_argument_and_runtime_is_restricted(self):
38
+ with tempfile.TemporaryDirectory(prefix="aikit image ") as folder:
39
+ path = Path(folder).resolve()
40
+ arguments = smoke.runtime_arguments("aikit:test", path, readonly=True)
41
+ self.assertIn(f"type=bind,source={path},target=/workspace,readonly", arguments)
42
+ self.assertIn("--read-only", arguments)
43
+ self.assertEqual(arguments[arguments.index("--network") + 1], "none")
44
+ self.assertEqual(arguments[arguments.index("--cap-drop") + 1], "ALL")
45
+ self.assertEqual(arguments[-1], "aikit:test")
46
+ self.assertNotIn("--privileged", arguments)
47
+
48
+ def test_nonexistent_mount_is_refused(self):
49
+ with tempfile.TemporaryDirectory() as folder:
50
+ with self.assertRaises(ValueError):
51
+ smoke.runtime_arguments("aikit:test", Path(folder) / "missing")
52
+
53
+ def test_linux_host_identity_is_used_without_switching_to_root(self):
54
+ with patch.object(os, "getuid", return_value=1002, create=True), \
55
+ patch.object(os, "getgid", return_value=1003, create=True):
56
+ arguments = smoke.runtime_arguments("aikit:test")
57
+ self.assertEqual(arguments[arguments.index("--user") + 1], "1002:1003")
58
+ with patch.object(os, "getuid", return_value=0, create=True):
59
+ self.assertNotIn("--user", smoke.runtime_arguments("aikit:test"))
60
+
61
+
62
+ if __name__ == "__main__":
63
+ unittest.main()
@@ -0,0 +1,54 @@
1
+ """Evidence collection never authorizes deletion or infers exact storage ownership."""
2
+ import json
3
+ from pathlib import Path
4
+ import subprocess
5
+ import sys
6
+ import unittest
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
9
+ import docker_test_usage
10
+
11
+
12
+ class DockerEvidenceTests(unittest.TestCase):
13
+ def test_inspection_only_and_referenced_volume_is_protected(self):
14
+ calls = []
15
+
16
+ def run(argv):
17
+ calls.append(argv)
18
+ args = argv[1:]
19
+ if args == ["context", "show"]:
20
+ return "desktop-linux\n"
21
+ if args[:1] == ["info"]:
22
+ return "test-version\n"
23
+ if args[:2] == ["volume", "ls"]:
24
+ return "exact test volume\n"
25
+ if args[:2] == ["volume", "inspect"]:
26
+ self.assertEqual(args[2], "exact test volume")
27
+ return json.dumps([{"Labels": {"com.docker.compose.project": "demo",
28
+ "ai-kit.disposable": "true"}}])
29
+ if args[:1] == ["ps"]:
30
+ return "stopped-container\n"
31
+ if args == ["system", "df", "-v"]:
32
+ return "Human-readable storage report\n"
33
+ if args[:2] == ["buildx", "du"]:
34
+ return "{}\n"
35
+ self.fail("Unexpected command: " + repr(argv))
36
+
37
+ report = docker_test_usage.collect("demo", run=run, builder="dedicated-test")
38
+ self.assertIsNone(report["measured_project_bytes"])
39
+ self.assertEqual(report["builder_project_ownership"], "unverified")
40
+ self.assertTrue(report["volumes"][0]["declared_disposable"])
41
+ self.assertEqual(report["volumes"][0]["container_references"], ["stopped-container"])
42
+ self.assertFalse(report["volumes"][0]["eligible_without_more_evidence"])
43
+ self.assertFalse(any(token in {"rm", "prune", "remove", "stop"} for argv in calls for token in argv))
44
+
45
+ def test_stopped_daemon_is_not_a_successful_evidence_report(self):
46
+ def unavailable(argv):
47
+ raise subprocess.CalledProcessError(1, argv, stderr="Daemon unavailable")
48
+
49
+ with self.assertRaises(subprocess.CalledProcessError):
50
+ docker_test_usage.collect("demo", run=unavailable)
51
+
52
+
53
+ if __name__ == "__main__":
54
+ unittest.main()