kanibako-cli 1.5.1__tar.gz → 1.6.0.dev23__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 (161) hide show
  1. {kanibako_cli-1.5.1/src/kanibako_cli.egg-info → kanibako_cli-1.6.0.dev23}/PKG-INFO +1 -1
  2. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/pyproject.toml +13 -1
  3. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/__init__.py +1 -1
  4. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/start.py +240 -54
  5. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/config.py +105 -19
  6. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/config_interface.py +21 -12
  7. kanibako_cli-1.6.0.dev23/src/kanibako/scripts/__init__.py +0 -0
  8. kanibako_cli-1.6.0.dev23/src/kanibako/targets/assembly.py +328 -0
  9. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/targets/base.py +206 -16
  10. kanibako_cli-1.6.0.dev23/src/kanibako/targets/credsync.py +165 -0
  11. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23/src/kanibako_cli.egg-info}/PKG-INFO +1 -1
  12. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako_cli.egg-info/SOURCES.txt +3 -0
  13. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_config.py +144 -13
  14. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_config_interface.py +5 -4
  15. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_resource_scoping.py +73 -5
  16. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/LICENSE.md +0 -0
  17. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/README.md +0 -0
  18. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/setup.cfg +0 -0
  19. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/__main__.py +0 -0
  20. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/_atomic.py +0 -0
  21. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/auth_browser.py +0 -0
  22. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/auth_parser.py +0 -0
  23. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/baseline.py +0 -0
  24. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/browser_sidecar.py +0 -0
  25. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/browser_state.py +0 -0
  26. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/bun_sea.py +0 -0
  27. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/cli.py +0 -0
  28. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/__init__.py +0 -0
  29. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/archive.py +0 -0
  30. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/baseline_cmd.py +0 -0
  31. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/box/__init__.py +0 -0
  32. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/box/_duplicate.py +0 -0
  33. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/box/_lifecycle.py +0 -0
  34. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/box/_parser.py +0 -0
  35. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/clean.py +0 -0
  36. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/crab_cmd.py +0 -0
  37. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/diagnose.py +0 -0
  38. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/fork_cmd.py +0 -0
  39. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/helper_cmd.py +0 -0
  40. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/image.py +0 -0
  41. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/install.py +0 -0
  42. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/refresh_credentials.py +0 -0
  43. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/restore.py +0 -0
  44. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/setup_cmd.py +0 -0
  45. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/stop.py +0 -0
  46. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/system_cmd.py +0 -0
  47. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/upgrade.py +0 -0
  48. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/vault_cmd.py +0 -0
  49. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/commands/workset_cmd.py +0 -0
  50. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/config_io.py +0 -0
  51. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/container.py +0 -0
  52. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/containerfiles.py +0 -0
  53. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/containers/Containerfile.template-android +0 -0
  54. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/containers/Containerfile.template-dotnet +0 -0
  55. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/containers/Containerfile.template-js +0 -0
  56. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/containers/Containerfile.template-jvm +0 -0
  57. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/containers/Containerfile.template-systems +0 -0
  58. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/containers/__init__.py +0 -0
  59. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/containers/tmux.conf +0 -0
  60. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/crabs.py +0 -0
  61. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/data/__init__.py +0 -0
  62. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/data/image-baseline.yaml +0 -0
  63. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/errors.py +0 -0
  64. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/freshness.py +0 -0
  65. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/git.py +0 -0
  66. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/helper_client.py +0 -0
  67. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/helper_listener.py +0 -0
  68. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/helpers.py +0 -0
  69. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/hygiene.py +0 -0
  70. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/image_sharing.py +0 -0
  71. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/instructions.py +0 -0
  72. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/log.py +0 -0
  73. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/names.py +0 -0
  74. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/paths.py +0 -0
  75. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/plugins/__init__.py +0 -0
  76. /kanibako_cli-1.5.1/src/kanibako/scripts/__init__.py → /kanibako_cli-1.6.0.dev23/src/kanibako/py.typed +0 -0
  77. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/registry.py +0 -0
  78. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/rig_bundle.py +0 -0
  79. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/rig_meta.py +0 -0
  80. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/rig_registry.py +0 -0
  81. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/rig_resolve.py +0 -0
  82. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/rig_source.py +0 -0
  83. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/scripts/helper-init.sh +0 -0
  84. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/scripts/kanibako-entry +0 -0
  85. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/settings_resolve.py +0 -0
  86. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/settings_seeds.py +0 -0
  87. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/settings_shares.py +0 -0
  88. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/shellenv.py +0 -0
  89. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/shells.py +0 -0
  90. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/snapshots.py +0 -0
  91. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/targets/__init__.py +0 -0
  92. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/targets/no_agent.py +0 -0
  93. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/templates.py +0 -0
  94. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/templates_image.py +0 -0
  95. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/tweakcc.py +0 -0
  96. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/tweakcc_cache.py +0 -0
  97. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/utils.py +0 -0
  98. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako/workset.py +0 -0
  99. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako_cli.egg-info/dependency_links.txt +0 -0
  100. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako_cli.egg-info/entry_points.txt +0 -0
  101. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako_cli.egg-info/requires.txt +0 -0
  102. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/src/kanibako_cli.egg-info/top_level.txt +0 -0
  103. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_archive_restore_integration.py +0 -0
  104. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_atomic.py +0 -0
  105. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_auth_browser.py +0 -0
  106. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_auth_parser.py +0 -0
  107. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_baseline.py +0 -0
  108. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_browser_sidecar.py +0 -0
  109. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_browser_state.py +0 -0
  110. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_bun_sea.py +0 -0
  111. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_cli.py +0 -0
  112. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_cli_exitcodes.py +0 -0
  113. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_comms.py +0 -0
  114. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_container.py +0 -0
  115. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_container_extended.py +0 -0
  116. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_container_integration.py +0 -0
  117. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_containerfiles.py +0 -0
  118. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_crabs.py +0 -0
  119. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_credentials.py +0 -0
  120. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_env_cmd.py +0 -0
  121. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_error_messages.py +0 -0
  122. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_fork.py +0 -0
  123. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_freshness.py +0 -0
  124. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_git.py +0 -0
  125. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_helper_client.py +0 -0
  126. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_helper_cmd.py +0 -0
  127. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_helper_listener.py +0 -0
  128. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_helpers.py +0 -0
  129. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_hygiene.py +0 -0
  130. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_image_sharing.py +0 -0
  131. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_init_cmd.py +0 -0
  132. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_install_integration.py +0 -0
  133. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_instructions.py +0 -0
  134. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_lifecycle_integration.py +0 -0
  135. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_log.py +0 -0
  136. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_names.py +0 -0
  137. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_paths.py +0 -0
  138. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_paths_extended.py +0 -0
  139. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_registry.py +0 -0
  140. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_rig_bundle.py +0 -0
  141. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_rig_meta.py +0 -0
  142. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_rig_registry.py +0 -0
  143. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_rig_resolve.py +0 -0
  144. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_rig_source.py +0 -0
  145. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_safety_invariants.py +0 -0
  146. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_settings_resolve.py +0 -0
  147. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_settings_seeds.py +0 -0
  148. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_settings_shares.py +0 -0
  149. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_shellenv.py +0 -0
  150. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_shells.py +0 -0
  151. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_snapshots.py +0 -0
  152. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_standalone_paths.py +0 -0
  153. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_start_integration.py +0 -0
  154. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_status.py +0 -0
  155. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_system_paths.py +0 -0
  156. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_templates.py +0 -0
  157. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_templates_image.py +0 -0
  158. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_tweakcc.py +0 -0
  159. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_utils.py +0 -0
  160. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_workset.py +0 -0
  161. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev23}/tests/test_workset_paths.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kanibako-cli
3
- Version: 1.5.1
3
+ Version: 1.6.0.dev23
4
4
  Summary: Run AI coding agents in rootless containers with per-project isolation
5
5
  Requires-Python: >=3.11
6
6
  License-File: LICENSE.md
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kanibako-cli"
7
- version = "1.5.1"
7
+ version = "1.6.0.dev23"
8
8
  description = "Run AI coding agents in rootless containers with per-project isolation"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = ["argcomplete>=3.0", "PyYAML>=6.0"]
@@ -22,6 +22,7 @@ dev = ["pytest>=7.0", "pytest-cov", "mypy", "ruff", "bump2version"]
22
22
  where = ["src"]
23
23
 
24
24
  [tool.setuptools.package-data]
25
+ "kanibako" = ["py.typed"]
25
26
  "kanibako.containers" = ["Containerfile.*", "tmux.conf"]
26
27
  "kanibako.scripts" = ["*.sh", "kanibako-entry"]
27
28
  "kanibako.data" = ["*.yaml"]
@@ -33,6 +34,17 @@ line-length = 99
33
34
  [tool.ruff.lint.per-file-ignores]
34
35
  "tests/conftest.py" = ["E402"]
35
36
 
37
+ [tool.mypy]
38
+ # kanibako is a namespace package split across distributions: the core ships
39
+ # kanibako/ (regular package) while each plugin contributes
40
+ # kanibako/plugins/<agent>/ with no __init__.py in kanibako/plugins/. To let
41
+ # `mypy packages/agent-<agent>/src` assign the full dotted module name (and
42
+ # thus resolve sibling imports like kanibako.plugins.claude.target), point
43
+ # mypy at every src base and enable explicit package bases.
44
+ namespace_packages = true
45
+ explicit_package_bases = true
46
+ mypy_path = "$MYPY_CONFIG_FILE_DIR/src:$MYPY_CONFIG_FILE_DIR/packages/agent-claude/src:$MYPY_CONFIG_FILE_DIR/packages/agent-goose/src:$MYPY_CONFIG_FILE_DIR/packages/agent-codex/src"
47
+
36
48
  [[tool.mypy.overrides]]
37
49
  module = ["yaml"]
38
50
  ignore_missing_imports = true
@@ -1,3 +1,3 @@
1
1
  """kanibako: Run AI coding agents in rootless containers with per-project isolation."""
2
2
 
3
- __version__ = "1.5.1"
3
+ __version__ = "1.6.0.dev23"
@@ -24,7 +24,8 @@ from kanibako.paths import (
24
24
  load_std_paths,
25
25
  resolve_any_project,
26
26
  )
27
- from kanibako.targets import resolve_target
27
+ from kanibako.targets import assembly, credsync, resolve_target
28
+ from kanibako.targets.assembly import BindingSourceError, descriptor_mounts
28
29
  from kanibako.utils import container_name_for, short_hash
29
30
 
30
31
 
@@ -192,6 +193,7 @@ def run_start(args: argparse.Namespace) -> int:
192
193
  # Map -A/-S to safe_mode: -A means autonomous (safe_mode=False),
193
194
  # -S means secure (safe_mode=True). Neither means autonomous (default).
194
195
  safe_mode = secure
196
+ autonomous = getattr(args, "autonomous", False)
195
197
 
196
198
  # Check for agent before launching container.
197
199
  # If no agent is detected, show a helpful message instead of silently
@@ -215,6 +217,7 @@ def run_start(args: argparse.Namespace) -> int:
215
217
  image_override=image_override,
216
218
  new_session=new_session,
217
219
  safe_mode=safe_mode,
220
+ autonomous=autonomous,
218
221
  resume_mode=resume_session,
219
222
  extra_args=agent_args,
220
223
  no_helpers=no_helpers,
@@ -268,6 +271,7 @@ def run_shell(args: argparse.Namespace) -> int:
268
271
  image_override=image_override,
269
272
  new_session=False,
270
273
  safe_mode=False,
274
+ autonomous=False,
271
275
  resume_mode=False,
272
276
  extra_args=shell_args,
273
277
  no_helpers=no_helpers,
@@ -543,6 +547,7 @@ def _run_container(
543
547
  image_override: str | None,
544
548
  new_session: bool,
545
549
  safe_mode: bool,
550
+ autonomous: bool = False,
546
551
  resume_mode: bool,
547
552
  extra_args: list[str],
548
553
  no_helpers: bool = False,
@@ -719,12 +724,25 @@ def _run_container(
719
724
  logger.debug("Image: %s", image)
720
725
  logger.debug("Container: %s", container_name)
721
726
 
727
+ # Plugin descriptor (None for legacy/no_agent targets). Hoisted here so the
728
+ # credential lifecycle sites below (init / refresh / writeback) and the
729
+ # launch-assembly block all branch off a single value: descriptor-bearing
730
+ # targets route their cred lifecycle through the credsync engine, legacy
731
+ # targets keep the per-plugin init_home/refresh/writeback hooks.
732
+ desc = target.descriptor if target else None
733
+
722
734
  # Persistent mode: reattach if already running, clean up stale containers
723
735
  if persistent:
724
736
  if runtime.is_running(container_name):
725
737
  # Refresh credentials before reattaching
726
738
  if target and proj.group_auth:
727
- target.refresh_credentials(proj.shell_path)
739
+ if desc is not None:
740
+ credsync.refresh_cred_files(
741
+ desc, target, host_home=Path.home(),
742
+ project_home=proj.shell_path, group_auth=proj.group_auth,
743
+ )
744
+ else:
745
+ target.refresh_credentials(proj.shell_path)
728
746
  return runtime.exec(
729
747
  container_name, _bootstrap_attach(bootstrap_program)
730
748
  )
@@ -804,7 +822,13 @@ def _run_container(
804
822
  # Ensure the agent-specific template variant directory exists.
805
823
  (templates_base / target.name / crab_cfg.shell).mkdir(parents=True, exist_ok=True)
806
824
  apply_shell_template(proj.shell_path, templates_base, target.name, crab_cfg.shell)
807
- target.init_home(proj.shell_path, group_auth=proj.group_auth)
825
+ if desc is not None:
826
+ credsync.seed_cred_files(
827
+ desc, target, host_home=Path.home(),
828
+ project_home=proj.shell_path, group_auth=proj.group_auth,
829
+ )
830
+ else:
831
+ target.init_home(proj.shell_path, group_auth=proj.group_auth)
808
832
 
809
833
  # Merge layered instruction files (base + template + user).
810
834
  instr_files = target.instruction_files()
@@ -898,7 +922,13 @@ def _run_container(
898
922
 
899
923
  # Credential refresh via target (skip for distinct auth)
900
924
  if target and proj.group_auth:
901
- target.refresh_credentials(proj.shell_path)
925
+ if desc is not None:
926
+ credsync.refresh_cred_files(
927
+ desc, target, host_home=Path.home(),
928
+ project_home=proj.shell_path, group_auth=proj.group_auth,
929
+ )
930
+ else:
931
+ target.refresh_credentials(proj.shell_path)
902
932
 
903
933
  # tweakcc: patch agent binary if enabled
904
934
  tweakcc_entry = None
@@ -922,16 +952,61 @@ def _run_container(
922
952
  # Apply model override from -M/--model flag
923
953
  if model_override:
924
954
  effective_state["model"] = model_override
925
- state_args, state_env = target.apply_state(effective_state)
926
955
  all_extra = list(crab_cfg.run_args) + list(extra_args)
927
- cli_args = target.build_cli_args(
928
- safe_mode=safe_mode,
929
- resume_mode=resume_mode,
930
- new_session=new_session,
931
- is_new_project=proj.is_new,
932
- extra_args=all_extra,
933
- )
934
- cli_args.extend(state_args)
956
+ if desc is not None:
957
+ # Descriptor path: assemble argv + container-env overlay
958
+ # declaratively from the plugin descriptor (replaces the legacy
959
+ # apply_state / build_cli_args hooks for descriptor-bearing
960
+ # targets).
961
+ #
962
+ # safe_off redeems the persisted `access` setting (claude:
963
+ # safe_bypass.setting_key="access", default "permissive") while
964
+ # the per-launch -A/-S flags still win (safe_mode IS the -S
965
+ # `secure` bool; autonomous IS -A). An agent whose descriptor
966
+ # declares no safe_bypass.setting_key (goose/codex) falls back to
967
+ # default-autonomous via effective_safe_mode_off.
968
+ sb = desc.safe_bypass
969
+ persisted_access = (
970
+ effective_state.get(sb.setting_key, "")
971
+ if sb is not None and sb.setting_key
972
+ else ""
973
+ )
974
+ safe_off = assembly.effective_safe_mode_off(
975
+ secure=safe_mode,
976
+ autonomous=autonomous,
977
+ persisted_access=persisted_access,
978
+ )
979
+ mode_key = assembly.resolve_mode(
980
+ resume_mode=resume_mode,
981
+ new_session=new_session,
982
+ is_new_project=proj.is_new,
983
+ extra_args=all_extra,
984
+ available_modes=desc.mode.keys(),
985
+ )
986
+ cli_args = assembly.assemble_argv(
987
+ desc,
988
+ mode_key=mode_key,
989
+ safe_mode_off=safe_off,
990
+ setting_values=effective_state,
991
+ op=None,
992
+ extra_args=all_extra,
993
+ )
994
+ state_env = assembly.assemble_env(
995
+ desc,
996
+ safe_mode_off=safe_off,
997
+ setting_values=effective_state,
998
+ )
999
+ else:
1000
+ # Legacy path (unchanged): apply_state + build_cli_args.
1001
+ state_args, state_env = target.apply_state(effective_state)
1002
+ cli_args = target.build_cli_args(
1003
+ safe_mode=safe_mode,
1004
+ resume_mode=resume_mode,
1005
+ new_session=new_session,
1006
+ is_new_project=proj.is_new,
1007
+ extra_args=all_extra,
1008
+ )
1009
+ cli_args.extend(state_args)
935
1010
  else:
936
1011
  state_env = {}
937
1012
  cli_args = list(extra_args)
@@ -944,38 +1019,99 @@ def _run_container(
944
1019
  # non-executable file fails fast with an actionable message before
945
1020
  # anything execs it. Here we only guard against missing mount
946
1021
  # sources.
947
- binary_mnts = target.binary_mounts(install)
948
- if not binary_mnts:
949
- print(
950
- f"Error: {target.display_name} binary detected at "
951
- f"{install.binary} but mount sources are missing.\n"
952
- f" binary: {install.binary} "
953
- f"({'exists' if install.binary.exists() else 'MISSING'})\n"
954
- f" install_dir: {install.install_dir} "
955
- f"({'exists' if install.install_dir.exists() else 'MISSING'})\n"
956
- f"The container would launch without the agent binary.",
957
- file=sys.stderr,
1022
+ if desc is not None:
1023
+ # Descriptor path: the AGENT_CRITICAL delivery binds (binary +
1024
+ # launcher) come from the descriptor. A missing/unresolvable
1025
+ # source raises BindingSourceError -> clean safe-fail (replaces
1026
+ # the legacy "mount source disappeared" check), not a crun crash.
1027
+ #
1028
+ # SHARED_STORE bindings (e.g. claude's AGENT-scope `plugins`
1029
+ # share) resolve under the agent-namespaced global shared store
1030
+ # `global_shared/<agent_id>/<src_rel>`, unifying with SHARED
1031
+ # resources. When the project has NO global shared store,
1032
+ # shared_store_root stays None and descriptor_mounts SKIPS those
1033
+ # bindings (the no-shared-store case, unchanged).
1034
+ shared_store_root = (
1035
+ proj.global_shared_path / agent_id
1036
+ if proj.global_shared_path
1037
+ else None
958
1038
  )
959
- return 1
960
- # Safe-fail: every agent bind source must still exist at mount
961
- # time. If the host churned (prune/repoint mid-flight) between
962
- # detection and here, fail with a clean, actionable kanibako error
963
- # instead of letting crun crash on a dangling bind source.
964
- missing = [m for m in binary_mnts if not m.source.exists()]
965
- if missing:
966
- detail = "\n".join(
967
- f" {m.source} → {m.destination}" for m in missing
1039
+ # Per-agent binding host-source overrides (crab.<name>.binding.<key>
1040
+ # layered over crab.default.binding) resolved across the config
1041
+ # cascade; an override redirects (and always wins for) a binding's
1042
+ # host source.
1043
+ binding_overrides = _build_binding_overrides(
1044
+ project_toml=project_toml,
1045
+ workset_config_path=workset_path,
1046
+ crab_config_path=crab_cfg_path,
1047
+ global_config_path=config_file,
1048
+ agent_name=agent_id,
968
1049
  )
969
- print(
970
- f"Error: {target.display_name} mount source disappeared "
971
- f"before launch:\n{detail}\n"
972
- f"The host agent install changed while starting (e.g. an "
973
- f"update pruned a version). Retry the launch.\n"
974
- f"Run 'kanibako system diagnose' for a full health check.",
975
- file=sys.stderr,
976
- )
977
- return 1
978
- extra_mounts.extend(binary_mnts)
1050
+ # The `plugins` binding is rw (ro=False) AGENT scope, and
1051
+ # descriptor_mounts SKIPS a MISSING agent source. Pre-create the
1052
+ # resolved host source dir best-effort (mirrors the legacy
1053
+ # rw-share behavior) so podman binds a real, PERSISTENT dir
1054
+ # instead of stubbing it. A bad source must never crash launch.
1055
+ plugins_src = binding_overrides.get("plugins")
1056
+ if not plugins_src and shared_store_root is not None:
1057
+ plugins_src = str(shared_store_root / "plugins")
1058
+ if plugins_src:
1059
+ try:
1060
+ Path(plugins_src).mkdir(parents=True, exist_ok=True)
1061
+ except OSError:
1062
+ pass
1063
+ try:
1064
+ binary_mnts = descriptor_mounts(
1065
+ desc, install,
1066
+ shared_store_root=shared_store_root,
1067
+ overrides=binding_overrides,
1068
+ )
1069
+ except BindingSourceError as exc:
1070
+ logger.error("Agent delivery binding unusable: %s", exc)
1071
+ print(
1072
+ f"Error: {target.display_name} mount source "
1073
+ f"disappeared before launch: {exc}\n"
1074
+ f"The host agent install changed while starting (e.g. "
1075
+ f"an update pruned a version). Retry the launch.\n"
1076
+ f"Run 'kanibako system diagnose' for a full health "
1077
+ f"check.",
1078
+ file=sys.stderr,
1079
+ )
1080
+ return 1
1081
+ extra_mounts.extend(binary_mnts)
1082
+ else:
1083
+ binary_mnts = target.binary_mounts(install)
1084
+ if not binary_mnts:
1085
+ print(
1086
+ f"Error: {target.display_name} binary detected at "
1087
+ f"{install.binary} but mount sources are missing.\n"
1088
+ f" binary: {install.binary} "
1089
+ f"({'exists' if install.binary.exists() else 'MISSING'})\n"
1090
+ f" install_dir: {install.install_dir} "
1091
+ f"({'exists' if install.install_dir.exists() else 'MISSING'})\n"
1092
+ f"The container would launch without the agent binary.",
1093
+ file=sys.stderr,
1094
+ )
1095
+ return 1
1096
+ # Safe-fail: every agent bind source must still exist at mount
1097
+ # time. If the host churned (prune/repoint mid-flight) between
1098
+ # detection and here, fail with a clean, actionable kanibako
1099
+ # error instead of letting crun crash on a dangling bind source.
1100
+ missing = [m for m in binary_mnts if not m.source.exists()]
1101
+ if missing:
1102
+ detail = "\n".join(
1103
+ f" {m.source} → {m.destination}" for m in missing
1104
+ )
1105
+ print(
1106
+ f"Error: {target.display_name} mount source disappeared "
1107
+ f"before launch:\n{detail}\n"
1108
+ f"The host agent install changed while starting (e.g. an "
1109
+ f"update pruned a version). Retry the launch.\n"
1110
+ f"Run 'kanibako system diagnose' for a full health check.",
1111
+ file=sys.stderr,
1112
+ )
1113
+ return 1
1114
+ extra_mounts.extend(binary_mnts)
979
1115
 
980
1116
  # kanibako CLI bind-mount (package + entry script)
981
1117
  kanibako_mnts = _kanibako_mounts()
@@ -1077,11 +1213,10 @@ def _run_container(
1077
1213
  # Merge per-run -e/--env KEY=VALUE vars (highest priority).
1078
1214
  container_env.update(_parse_cli_env(cli_env))
1079
1215
 
1080
- # Disable Claude Code telemetry inside containers.
1081
- if target and target.name == "claude":
1082
- container_env.setdefault(
1083
- "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1",
1084
- )
1216
+ # NOTE: Claude Code telemetry (CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC)
1217
+ # and the auto-updater disable (DISABLE_AUTOUPDATER) are now carried by
1218
+ # claude's descriptor.container_env, merged via state_env above. The
1219
+ # former core `target.name == "claude"` special-case has been removed.
1085
1220
 
1086
1221
  # Inject instance identity for peer communication.
1087
1222
  if proj.name:
@@ -1306,6 +1441,7 @@ def _run_container(
1306
1441
  image_override=image_override,
1307
1442
  new_session=True,
1308
1443
  safe_mode=safe_mode,
1444
+ autonomous=autonomous,
1309
1445
  resume_mode=False,
1310
1446
  extra_args=extra_args,
1311
1447
  no_helpers=no_helpers,
@@ -1372,6 +1508,7 @@ def _run_container(
1372
1508
  image_override=image_override,
1373
1509
  new_session=True,
1374
1510
  safe_mode=safe_mode,
1511
+ autonomous=autonomous,
1375
1512
  resume_mode=False,
1376
1513
  extra_args=extra_args,
1377
1514
  no_helpers=no_helpers,
@@ -1386,7 +1523,13 @@ def _run_container(
1386
1523
  else:
1387
1524
  # Write back refreshed credentials via target (skip for distinct auth)
1388
1525
  if target and proj.group_auth:
1389
- target.writeback_credentials(proj.shell_path)
1526
+ if desc is not None:
1527
+ credsync.writeback_cred_files(
1528
+ desc, target, host_home=Path.home(),
1529
+ project_home=proj.shell_path, group_auth=proj.group_auth,
1530
+ )
1531
+ else:
1532
+ target.writeback_credentials(proj.shell_path)
1390
1533
 
1391
1534
  # Hint when agent exits non-zero and --continue/--resume was used
1392
1535
  if rc != 0 and is_agent_mode and not new_session:
@@ -1445,12 +1588,17 @@ def _build_effective_state(
1445
1588
  with the target's declared defaults as a FLOOR (the system level's declared
1446
1589
  defaults). Sources for each level's ``[crab]`` table:
1447
1590
 
1448
- * **box** — ``[crab]`` in project.yaml
1449
- * **workset** — ``[crab]`` in the workset's config.yaml (if any)
1450
- * **crab** — the crab config's own state dict
1451
- * **system** — ``[crab]`` in the global kanibako.yaml
1591
+ * **box** — ``crab.<name>`` (over ``crab.default``) in project.yaml
1592
+ * **workset** — ``crab.<name>`` in the workset's config.yaml (if any)
1593
+ * **crab** — the crab config's own state dict (already per-agent)
1594
+ * **system** — ``crab.<name>`` in the global kanibako.yaml
1452
1595
  * **floor** — target ``setting_descriptors()`` defaults
1453
1596
 
1597
+ The box/workset/system/machine override sections are keyed per agent
1598
+ (``crab.<target.name>`` layered over the any-agent ``crab.default`` tier) so
1599
+ an override never bleeds across an agent switch; ``crab_cfg.state`` is
1600
+ already per-agent (loaded from ``crabs/<name>.yaml``).
1601
+
1454
1602
  Explicit set values beat all declared defaults; the most-specific level
1455
1603
  wins; an explicit ``""`` is terminal (no fall-through to the floor).
1456
1604
  Undeclared keys set anywhere (e.g. ``start_mode``) are passed through.
@@ -1480,7 +1628,9 @@ def _build_effective_state(
1480
1628
  try:
1481
1629
  if not path.exists():
1482
1630
  return {}
1483
- return read_crab_settings(path)
1631
+ # Agent-keyed: read crab.<agent>.* layered over crab.default.* so an
1632
+ # override set for one agent never bleeds onto another after a switch.
1633
+ return read_crab_settings(path, target.name)
1484
1634
  except Exception:
1485
1635
  return {}
1486
1636
 
@@ -1531,6 +1681,42 @@ def _build_effective_state(
1531
1681
  return effective
1532
1682
 
1533
1683
 
1684
+ def _build_binding_overrides(
1685
+ *,
1686
+ project_toml,
1687
+ workset_config_path,
1688
+ crab_config_path,
1689
+ global_config_path,
1690
+ agent_name: str,
1691
+ ) -> dict[str, str]:
1692
+ """Resolve descriptor binding host-source overrides across the config cascade.
1693
+
1694
+ Reads each config level's ``crab.<agent_name>.binding`` sub-table (layered
1695
+ over ``crab.default.binding`` within each file) via
1696
+ :func:`~kanibako.config.read_binding_overrides`, mirroring B3's agent-keying,
1697
+ then overlays the levels MOST-SPECIFIC-WINS:
1698
+
1699
+ box (project.yaml) > workset > crab (crabs/<name>.yaml) > system > machine
1700
+
1701
+ Returns ``{binding_key: host_src}`` (empty when nothing is configured, the
1702
+ common case). A bad/unreadable level contributes nothing (the reader
1703
+ swallows its own errors).
1704
+ """
1705
+ from kanibako.config import machine_config_path, read_binding_overrides
1706
+
1707
+ overrides: dict[str, str] = {}
1708
+ # Least-specific first so each more-specific level's .update() wins.
1709
+ for path in (
1710
+ machine_config_path(),
1711
+ global_config_path,
1712
+ crab_config_path,
1713
+ workset_config_path,
1714
+ project_toml,
1715
+ ):
1716
+ overrides.update(read_binding_overrides(path, agent_name))
1717
+ return overrides
1718
+
1719
+
1534
1720
  def _apply_init_seeds(
1535
1721
  *,
1536
1722
  std,
@@ -442,48 +442,134 @@ def load_project_overrides(path: Path) -> dict[str, str]:
442
442
  # Target settings overrides (per-project)
443
443
  # ---------------------------------------------------------------------------
444
444
 
445
- def read_crab_settings(path: Path) -> dict[str, str]:
446
- """Read crab-state overrides from a project.yaml ``crab`` section.
447
-
448
- project.yaml's ``crab`` holds box-level crab-state overrides (e.g.
449
- ``{"model": "sonnet"}``); identity keys live in ``box.crab``, not here.
450
- Returns an empty dict when the file or section is absent.
445
+ def read_crab_settings(path: Path, agent_name: str) -> dict[str, str]:
446
+ """Read agent-keyed crab-state overrides from a config file's ``crab`` table.
447
+
448
+ Override sections are keyed per agent under ``crab.<agent_name>``, layered
449
+ over the reserved any-agent ``crab.default`` tier (the agent-specific value
450
+ wins within a single file). This stops an override set while a box is on one
451
+ agent (e.g. ``model`` under ``crab.claude``) from bleeding onto another
452
+ agent after the box is switched (e.g. to ``goose``); identity keys live in
453
+ ``box.crab``, not here.
454
+
455
+ ``crab.default`` is RESERVED as the any-agent default tier; no real agent
456
+ may be named ``default``.
457
+
458
+ **No pass-1 migration.** A legacy FLAT ``[crab]`` table (scalar values
459
+ written directly under ``crab``, e.g. ``crab.model``) is treated as UNSET —
460
+ only nested per-agent dicts (``crab.default`` / ``crab.<agent_name>``) are
461
+ honored. Configs are hand-edited to the new shape. The common no-config case
462
+ (absent file, or absent/empty ``crab`` table) still returns ``{}`` unchanged.
451
463
  """
452
464
  if not path.exists():
453
465
  return {}
454
466
  data = load_doc(path)
455
- return {k: str(v) for k, v in data.get("crab", {}).items()}
467
+ crab = data.get("crab", {})
468
+ if not isinstance(crab, dict):
469
+ return {}
470
+ out: dict[str, str] = {}
471
+ default_sec = crab.get("default")
472
+ if isinstance(default_sec, dict):
473
+ out.update({k: str(v) for k, v in default_sec.items()})
474
+ agent_sec = crab.get(agent_name)
475
+ if isinstance(agent_sec, dict):
476
+ out.update({k: str(v) for k, v in agent_sec.items()})
477
+ return out
456
478
 
457
479
 
458
- def write_crab_setting(path: Path, key: str, value: str) -> None:
459
- """Write a single crab-state override to ``crab`` in project.yaml.
480
+ def write_crab_setting(path: Path, key: str, value: str, agent_name: str) -> None:
481
+ """Write a single crab-state override under ``crab.<agent_name>``.
460
482
 
461
- Preserves all other sections.
483
+ Preserves all other sections and other agents' crab subsections. Pass the
484
+ reserved ``"default"`` agent name to target the any-agent default tier.
462
485
  """
463
486
  existing = load_doc(path)
464
- existing.setdefault("crab", {})
465
- existing["crab"][key] = value
487
+ crab = existing.get("crab")
488
+ if not isinstance(crab, dict):
489
+ crab = {}
490
+ existing["crab"] = crab
491
+ agent_sec = crab.get(agent_name)
492
+ if not isinstance(agent_sec, dict):
493
+ agent_sec = {}
494
+ crab[agent_name] = agent_sec
495
+ agent_sec[key] = value
466
496
  dump_doc(path, existing)
467
497
 
468
498
 
469
- def remove_crab_setting(path: Path, key: str) -> bool:
470
- """Remove a single crab-state override from ``crab`` in project.yaml.
499
+ def remove_crab_setting(path: Path, key: str, agent_name: str) -> bool:
500
+ """Remove a single crab-state override from ``crab.<agent_name>``.
471
501
 
472
- Returns True if the setting was found and removed, False otherwise.
502
+ Returns True if the setting was found and removed, False otherwise. Prunes a
503
+ now-empty agent subsection and a now-empty ``crab`` table.
473
504
  """
474
505
  if not path.exists():
475
506
  return False
476
507
  existing = load_doc(path)
477
- settings = existing.get("crab", {})
478
- if key not in settings:
508
+ crab = existing.get("crab")
509
+ if not isinstance(crab, dict):
479
510
  return False
480
- del settings[key]
481
- if not settings:
511
+ agent_sec = crab.get(agent_name)
512
+ if not isinstance(agent_sec, dict) or key not in agent_sec:
513
+ return False
514
+ del agent_sec[key]
515
+ if not agent_sec:
516
+ del crab[agent_name]
517
+ if not crab:
482
518
  existing.pop("crab", None)
483
519
  dump_doc(path, existing)
484
520
  return True
485
521
 
486
522
 
523
+ def read_binding_overrides(path: Path | None, agent_name: str) -> dict[str, str]:
524
+ """Read agent-keyed binding host-source overrides from a config ``crab`` table.
525
+
526
+ Reads the ``binding`` sub-table under ``crab.<agent_name>`` layered over the
527
+ reserved any-agent ``crab.default.binding`` tier (the agent-specific value
528
+ wins within a single file) — the SAME agent-keying as
529
+ :func:`read_crab_settings`. These redirect the HOST SOURCE of a descriptor
530
+ :class:`~kanibako.targets.base.Binding` (e.g. ``crab.claude.binding.plugins``
531
+ points the claude ``plugins`` share at a custom host directory).
532
+
533
+ Returns ``{binding_key: host_src}``. Each binding VALUE may be either:
534
+
535
+ * a bare string ``host_src`` (``crab.claude.binding.plugins = "/path"``), or
536
+ * a sub-table carrying a ``host_src`` key
537
+ (``crab.claude.binding.plugins.host_src = "/path"``).
538
+
539
+ A sub-table without a string ``host_src`` (and any other non-string value)
540
+ is skipped. As with :func:`read_crab_settings`, a legacy FLAT ``[crab]``
541
+ table is treated as UNSET (no pass-1 migration); the common no-config case
542
+ (absent/None/unreadable path, or absent ``crab``/``binding`` table) returns
543
+ ``{}``.
544
+ """
545
+ if path is None or not path.exists():
546
+ return {}
547
+ try:
548
+ data = load_doc(path)
549
+ except Exception:
550
+ return {}
551
+ crab = data.get("crab", {})
552
+ if not isinstance(crab, dict):
553
+ return {}
554
+ out: dict[str, str] = {}
555
+ # Least-specific (default tier) first so the agent-specific tier wins.
556
+ for tier in ("default", agent_name):
557
+ section = crab.get(tier)
558
+ if not isinstance(section, dict):
559
+ continue
560
+ binding = section.get("binding")
561
+ if not isinstance(binding, dict):
562
+ continue
563
+ for key, val in binding.items():
564
+ if isinstance(val, str):
565
+ out[key] = val
566
+ elif isinstance(val, dict):
567
+ host_src = val.get("host_src")
568
+ if isinstance(host_src, str):
569
+ out[key] = host_src
570
+ return out
571
+
572
+
487
573
  # ---------------------------------------------------------------------------
488
574
  # Scoped shares (settings-framework {scope}.path.share_{ro,rw}.*)
489
575
  # ---------------------------------------------------------------------------