kanibako-cli 1.5.1__tar.gz → 1.6.0.dev24__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 (181) hide show
  1. {kanibako_cli-1.5.1/src/kanibako_cli.egg-info → kanibako_cli-1.6.0.dev24}/PKG-INFO +1 -1
  2. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/README.md +2 -2
  3. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/pyproject.toml +19 -2
  4. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/__init__.py +1 -1
  5. kanibako_cli-1.6.0.dev24/src/kanibako/agent_config.py +86 -0
  6. kanibako_cli-1.6.0.dev24/src/kanibako/box_identity.py +151 -0
  7. kanibako_cli-1.6.0.dev24/src/kanibako/channels.py +253 -0
  8. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/cli.py +50 -35
  9. kanibako_cli-1.5.1/src/kanibako/commands/crab_cmd.py → kanibako_cli-1.6.0.dev24/src/kanibako/commands/agent_cmd.py +72 -119
  10. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/archive.py +3 -3
  11. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/box/_duplicate.py +42 -16
  12. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/box/_lifecycle.py +330 -116
  13. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/box/_parser.py +74 -35
  14. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/clean.py +78 -13
  15. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/diagnose.py +12 -7
  16. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/fork_cmd.py +5 -2
  17. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/helper_cmd.py +15 -5
  18. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/image.py +2 -2
  19. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/install.py +30 -24
  20. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/refresh_credentials.py +1 -1
  21. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/start.py +949 -293
  22. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/system_cmd.py +5 -2
  23. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/workset_cmd.py +20 -19
  24. kanibako_cli-1.6.0.dev24/src/kanibako/config.py +920 -0
  25. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/config_interface.py +301 -59
  26. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/config_io.py +2 -2
  27. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/container.py +28 -19
  28. kanibako_cli-1.6.0.dev24/src/kanibako/data/templates/base/INSTRUCTIONS.md +67 -0
  29. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/helper_listener.py +7 -4
  30. kanibako_cli-1.6.0.dev24/src/kanibako/import_reconcile.py +314 -0
  31. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/names.py +28 -26
  32. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/paths.py +621 -401
  33. kanibako_cli-1.6.0.dev24/src/kanibako/registry_store.py +226 -0
  34. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/rig_registry.py +26 -28
  35. kanibako_cli-1.6.0.dev24/src/kanibako/scripts/__init__.py +0 -0
  36. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/scripts/helper-init.sh +1 -1
  37. kanibako_cli-1.6.0.dev24/src/kanibako/settings_categories.py +508 -0
  38. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/settings_resolve.py +106 -8
  39. kanibako_cli-1.6.0.dev24/src/kanibako/settings_seeds.py +81 -0
  40. kanibako_cli-1.6.0.dev24/src/kanibako/settings_shares.py +77 -0
  41. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/shells.py +17 -12
  42. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/snapshots.py +60 -23
  43. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/targets/__init__.py +5 -3
  44. kanibako_cli-1.6.0.dev24/src/kanibako/targets/assembly.py +331 -0
  45. kanibako_cli-1.6.0.dev24/src/kanibako/targets/base.py +497 -0
  46. kanibako_cli-1.6.0.dev24/src/kanibako/targets/credsync.py +172 -0
  47. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/targets/no_agent.py +4 -7
  48. kanibako_cli-1.6.0.dev24/src/kanibako/templates.py +176 -0
  49. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/utils.py +2 -2
  50. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/workset.py +44 -44
  51. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24/src/kanibako_cli.egg-info}/PKG-INFO +1 -1
  52. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako_cli.egg-info/SOURCES.txt +18 -5
  53. kanibako_cli-1.5.1/tests/test_crabs.py → kanibako_cli-1.6.0.dev24/tests/test_agent_config.py +67 -83
  54. kanibako_cli-1.6.0.dev24/tests/test_box_identity.py +226 -0
  55. kanibako_cli-1.6.0.dev24/tests/test_channels.py +213 -0
  56. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_cli.py +68 -80
  57. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_cli_exitcodes.py +13 -12
  58. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_comms.py +17 -55
  59. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_config.py +246 -126
  60. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_config_interface.py +255 -37
  61. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_container.py +37 -15
  62. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_container_extended.py +54 -14
  63. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_credentials.py +2 -42
  64. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_env_cmd.py +4 -4
  65. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_error_messages.py +3 -3
  66. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_fork.py +21 -27
  67. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_helper_cmd.py +4 -4
  68. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_helpers.py +15 -0
  69. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_image_sharing.py +1 -1
  70. kanibako_cli-1.6.0.dev24/tests/test_import.py +299 -0
  71. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_init_cmd.py +15 -15
  72. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_install_integration.py +3 -29
  73. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_lifecycle_integration.py +2 -2
  74. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_names.py +5 -5
  75. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_paths.py +125 -575
  76. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_paths_extended.py +4 -4
  77. kanibako_cli-1.6.0.dev24/tests/test_registry_store.py +162 -0
  78. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_resource_scoping.py +110 -101
  79. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_rig_registry.py +49 -33
  80. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_safety_invariants.py +86 -26
  81. kanibako_cli-1.6.0.dev24/tests/test_settings_categories.py +640 -0
  82. kanibako_cli-1.6.0.dev24/tests/test_settings_loader.py +229 -0
  83. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_settings_resolve.py +14 -14
  84. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_settings_seeds.py +53 -53
  85. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_settings_shares.py +60 -60
  86. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_shells.py +5 -3
  87. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_snapshots.py +101 -0
  88. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_standalone_paths.py +93 -30
  89. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_status.py +4 -4
  90. kanibako_cli-1.6.0.dev24/tests/test_system_paths.py +492 -0
  91. kanibako_cli-1.6.0.dev24/tests/test_templates.py +249 -0
  92. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_tweakcc.py +16 -16
  93. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_utils.py +2 -2
  94. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_workset.py +17 -1
  95. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_workset_paths.py +20 -23
  96. kanibako_cli-1.5.1/src/kanibako/config.py +0 -584
  97. kanibako_cli-1.5.1/src/kanibako/crabs.py +0 -89
  98. kanibako_cli-1.5.1/src/kanibako/instructions.py +0 -160
  99. kanibako_cli-1.5.1/src/kanibako/settings_seeds.py +0 -154
  100. kanibako_cli-1.5.1/src/kanibako/settings_shares.py +0 -154
  101. kanibako_cli-1.5.1/src/kanibako/targets/base.py +0 -321
  102. kanibako_cli-1.5.1/src/kanibako/templates.py +0 -60
  103. kanibako_cli-1.5.1/tests/test_instructions.py +0 -542
  104. kanibako_cli-1.5.1/tests/test_system_paths.py +0 -157
  105. kanibako_cli-1.5.1/tests/test_templates.py +0 -172
  106. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/LICENSE.md +0 -0
  107. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/setup.cfg +0 -0
  108. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/__main__.py +0 -0
  109. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/_atomic.py +0 -0
  110. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/auth_browser.py +0 -0
  111. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/auth_parser.py +0 -0
  112. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/baseline.py +0 -0
  113. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/browser_sidecar.py +0 -0
  114. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/browser_state.py +0 -0
  115. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/bun_sea.py +0 -0
  116. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/__init__.py +0 -0
  117. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/baseline_cmd.py +0 -0
  118. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/box/__init__.py +0 -0
  119. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/restore.py +0 -0
  120. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/setup_cmd.py +0 -0
  121. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/stop.py +0 -0
  122. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/upgrade.py +0 -0
  123. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/vault_cmd.py +0 -0
  124. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containerfiles.py +0 -0
  125. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/Containerfile.template-android +0 -0
  126. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/Containerfile.template-dotnet +0 -0
  127. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/Containerfile.template-js +0 -0
  128. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/Containerfile.template-jvm +0 -0
  129. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/Containerfile.template-systems +0 -0
  130. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/__init__.py +0 -0
  131. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/tmux.conf +0 -0
  132. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/data/__init__.py +0 -0
  133. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/data/image-baseline.yaml +0 -0
  134. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/errors.py +0 -0
  135. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/freshness.py +0 -0
  136. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/git.py +0 -0
  137. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/helper_client.py +0 -0
  138. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/helpers.py +0 -0
  139. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/hygiene.py +0 -0
  140. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/image_sharing.py +0 -0
  141. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/log.py +0 -0
  142. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/plugins/__init__.py +0 -0
  143. /kanibako_cli-1.5.1/src/kanibako/scripts/__init__.py → /kanibako_cli-1.6.0.dev24/src/kanibako/py.typed +0 -0
  144. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/registry.py +0 -0
  145. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/rig_bundle.py +0 -0
  146. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/rig_meta.py +0 -0
  147. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/rig_resolve.py +0 -0
  148. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/rig_source.py +0 -0
  149. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/scripts/kanibako-entry +0 -0
  150. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/shellenv.py +0 -0
  151. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/templates_image.py +0 -0
  152. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/tweakcc.py +0 -0
  153. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/tweakcc_cache.py +0 -0
  154. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako_cli.egg-info/dependency_links.txt +0 -0
  155. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako_cli.egg-info/entry_points.txt +0 -0
  156. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako_cli.egg-info/requires.txt +0 -0
  157. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako_cli.egg-info/top_level.txt +0 -0
  158. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_archive_restore_integration.py +0 -0
  159. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_atomic.py +0 -0
  160. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_auth_browser.py +0 -0
  161. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_auth_parser.py +0 -0
  162. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_baseline.py +0 -0
  163. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_browser_sidecar.py +0 -0
  164. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_browser_state.py +0 -0
  165. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_bun_sea.py +0 -0
  166. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_container_integration.py +0 -0
  167. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_containerfiles.py +0 -0
  168. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_freshness.py +0 -0
  169. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_git.py +0 -0
  170. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_helper_client.py +0 -0
  171. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_helper_listener.py +0 -0
  172. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_hygiene.py +0 -0
  173. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_log.py +0 -0
  174. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_registry.py +0 -0
  175. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_rig_bundle.py +0 -0
  176. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_rig_meta.py +0 -0
  177. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_rig_resolve.py +0 -0
  178. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_rig_source.py +0 -0
  179. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_shellenv.py +0 -0
  180. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_start_integration.py +0 -0
  181. {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_templates_image.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.dev24
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
@@ -751,7 +751,7 @@ Codex CLI, Goose).
751
751
  pip install kanibako-target-aider
752
752
 
753
753
  # Use a specific target
754
- kanibako box config box.crab=aider
754
+ kanibako box config box.agent=aider
755
755
  kanibako start
756
756
  ```
757
757
 
@@ -809,7 +809,7 @@ All kanibako config files are YAML.
809
809
  | `persistence` | `persistent` | Session type (persistent/ephemeral) |
810
810
  | `box.image` | `kanibako-oci:latest` | Container rig |
811
811
  | `box.shell` | `$KANIBAKO_SHELL` | Shell launched for a no-agent box (`kanibako start` with no agent, `kanibako shell`); resolved `box.shell` → `$KANIBAKO_SHELL` → the image's recorded login shell → `sh` |
812
- | `box.crab` | (auto-detect) | Agent target plugin |
812
+ | `box.agent` | (auto-detect) | Agent target plugin |
813
813
  | `box.share_images` | | Share host images into the box |
814
814
  | `group_auth` | `true` | Shared credentials across the group (`true`) vs. per-project (`false`) |
815
815
  | `enable_vault` | `true` | Enable vault directories |
@@ -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.dev24"
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,9 +22,15 @@ 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
- "kanibako.data" = ["*.yaml"]
28
+ "kanibako.data" = [
29
+ "*.yaml",
30
+ "templates/**/*", "templates/**/.*",
31
+ "templates/.*/**/*", "templates/.*/*",
32
+ "templates/**/.*/**/*", "templates/**/.*/*",
33
+ ]
28
34
 
29
35
  [tool.ruff]
30
36
  target-version = "py311"
@@ -33,6 +39,17 @@ line-length = 99
33
39
  [tool.ruff.lint.per-file-ignores]
34
40
  "tests/conftest.py" = ["E402"]
35
41
 
42
+ [tool.mypy]
43
+ # kanibako is a namespace package split across distributions: the core ships
44
+ # kanibako/ (regular package) while each plugin contributes
45
+ # kanibako/plugins/<agent>/ with no __init__.py in kanibako/plugins/. To let
46
+ # `mypy packages/agent-<agent>/src` assign the full dotted module name (and
47
+ # thus resolve sibling imports like kanibako.plugins.claude.target), point
48
+ # mypy at every src base and enable explicit package bases.
49
+ namespace_packages = true
50
+ explicit_package_bases = true
51
+ 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"
52
+
36
53
  [[tool.mypy.overrides]]
37
54
  module = ["yaml"]
38
55
  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.dev24"
@@ -0,0 +1,86 @@
1
+ """Agent YAML configuration: load, write, and resolve per-agent settings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+ from kanibako.config_io import dump_doc, load_doc
9
+
10
+ # Keys that live directly in the [agent] section as agent identity (not state).
11
+ IDENTITY_KEYS = frozenset({"name", "run_args"})
12
+
13
+
14
+ @dataclass
15
+ class AgentConfig:
16
+ """Per-agent configuration loaded from an agent YAML file.
17
+
18
+ Sections:
19
+ agent — identity (name, run_args) plus agent-state knobs
20
+ (model, access, start_mode, autonomous, …)
21
+ env — raw env vars injected into container
22
+ shared — agent-level shared cache paths
23
+ """
24
+
25
+ name: str = ""
26
+ run_args: list[str] = field(default_factory=list)
27
+ state: dict[str, str] = field(default_factory=dict)
28
+ env: dict[str, str] = field(default_factory=dict)
29
+ shared_caches: dict[str, str] = field(default_factory=dict)
30
+ tweakcc: dict = field(default_factory=dict)
31
+
32
+
33
+ def agents_dir(data_path: Path, paths_agents: str = "agents") -> Path:
34
+ """Return the agents directory under *data_path*."""
35
+ return data_path / (paths_agents or "agents")
36
+
37
+
38
+ def agent_config_path(
39
+ data_path: Path, agent_id: str, paths_agents: str = "agents",
40
+ ) -> Path:
41
+ """Return the path to an agent's config file."""
42
+ return agents_dir(data_path, paths_agents) / f"{agent_id}.yaml"
43
+
44
+
45
+ def load_agent_config(path: Path) -> AgentConfig:
46
+ """Read an agent config file and return an AgentConfig.
47
+
48
+ Returns defaults if the file does not exist.
49
+ """
50
+ cfg = AgentConfig()
51
+ if not path.exists():
52
+ return cfg
53
+
54
+ data = load_doc(path)
55
+
56
+ agent_sec = data.get("agent", {})
57
+ cfg.name = str(agent_sec.get("name", ""))
58
+ raw_args = agent_sec.get("run_args", [])
59
+ cfg.run_args = [str(a) for a in raw_args] if isinstance(raw_args, list) else []
60
+
61
+ cfg.state = {
62
+ k: str(v) for k, v in agent_sec.items() if k not in IDENTITY_KEYS
63
+ }
64
+ cfg.env = {k: str(v) for k, v in data.get("env", {}).items()}
65
+ cfg.shared_caches = {k: str(v) for k, v in data.get("shared", {}).items()}
66
+ cfg.tweakcc = dict(data.get("tweakcc", {}))
67
+
68
+ return cfg
69
+
70
+
71
+ def write_agent_config(path: Path, cfg: AgentConfig) -> None:
72
+ """Write an AgentConfig to a YAML file."""
73
+ agent_sec: dict = {
74
+ "name": cfg.name,
75
+ "run_args": list(cfg.run_args),
76
+ }
77
+ for k, v in cfg.state.items():
78
+ agent_sec[k] = v
79
+
80
+ data: dict = {
81
+ "agent": agent_sec,
82
+ "env": dict(cfg.env),
83
+ "shared": dict(cfg.shared_caches),
84
+ "tweakcc": dict(cfg.tweakcc),
85
+ }
86
+ dump_doc(path, data)
@@ -0,0 +1,151 @@
1
+ """STANDALONE box identity generation (``<random24>_<sanitized,capped leaf>``).
2
+
3
+ A standalone box is named ``<random24>_<leaf>`` where:
4
+
5
+ * ``random24`` is 24 bits of randomness (``os.urandom(3)``) encoded as a
6
+ fixed-width, lowercased base32 token (no padding) — always 5 characters.
7
+ * ``leaf`` is the project-root basename, sanitized so only portable filename
8
+ characters survive (``[^A-Za-z0-9._-]`` → ``_``), capped at 32 characters,
9
+ with an empty leaf falling back to ``"box"``.
10
+
11
+ The pieces are joined with ``_``. Because the random prefix can collide with
12
+ an already-registered standalone box name (design-review D-M13), the generator
13
+ regenerates the random component (bounded retries) until the *whole* name is
14
+ unique within a caller-supplied ``existing`` set.
15
+
16
+ This module is pure (modulo ``os.urandom``) and side-effect free so the
17
+ sanitize/cap/collision-regen logic is directly unit-testable.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import base64
23
+ import os
24
+ import re
25
+ from pathlib import Path
26
+
27
+ from kanibako.errors import ProjectError
28
+
29
+ # Maximum length of the sanitized leaf component.
30
+ _LEAF_CAP = 32
31
+ # Fallback leaf when the project basename sanitizes to empty.
32
+ _EMPTY_LEAF_FALLBACK = "box"
33
+ # Characters that are NOT replaced during sanitization.
34
+ _SAFE_CHAR_RE = re.compile(r"[^A-Za-z0-9._-]")
35
+ # Bound on collision-regeneration attempts before giving up.
36
+ _MAX_REGEN_ATTEMPTS = 1000
37
+
38
+ # A canonical standalone box name is ``<random24>_<leaf>`` where the random
39
+ # prefix is exactly 5 lowercase base32 chars and the leaf is 1-32 chars drawn
40
+ # from the sanitized, *lowercased* alphabet (see :func:`sanitize_cap`). This
41
+ # is the verbatim shape the generator emits, so it is also what a user may
42
+ # assert by passing a fully-formed ``--name``.
43
+ _CANONICAL_NAME_RE = re.compile(r"^[a-z2-7]{5}_[a-z0-9._-]{1,32}$")
44
+
45
+
46
+ def random24() -> str:
47
+ """Return a 24-bit random token as a fixed-width lowercase base32 string.
48
+
49
+ 24 bits encode to exactly 5 base32 characters (with the trailing ``=``
50
+ padding stripped), so every token is the same width. Lowercased for
51
+ container/path friendliness.
52
+ """
53
+ raw = os.urandom(3) # 3 bytes == 24 bits
54
+ return base64.b32encode(raw).decode("ascii").rstrip("=").lower()
55
+
56
+
57
+ def sanitize_cap(leaf: str) -> str:
58
+ """Sanitize, lowercase, and cap a project-basename *leaf* for a box name.
59
+
60
+ Non-portable characters (anything outside ``[A-Za-z0-9._-]``) become ``_``;
61
+ the result is lowercased (every box name is lowercase) and capped at
62
+ :data:`_LEAF_CAP` characters. An empty result (e.g. an empty or all-illegal
63
+ basename) falls back to ``"box"``.
64
+ """
65
+ sanitized = _SAFE_CHAR_RE.sub("_", leaf).lower()[:_LEAF_CAP]
66
+ return sanitized or _EMPTY_LEAF_FALLBACK
67
+
68
+
69
+ def is_canonical_standalone_name(name: str) -> bool:
70
+ """True when *name* matches the canonical ``<random24>_<leaf>`` shape.
71
+
72
+ The prefix must be exactly 5 lowercase base32 chars (``[a-z2-7]``) and the
73
+ leaf 1-32 chars of ``[a-z0-9._-]`` (lowercase, matching :func:`sanitize_cap`).
74
+ An over-long or illegal-char leaf, or a wrong-width prefix, fails the match.
75
+ The check is case-sensitive: callers lowercase a supplied name first.
76
+ """
77
+ return _CANONICAL_NAME_RE.match(name) is not None
78
+
79
+
80
+ def _generate_with_leaf(leaf: str, existing: set[str]) -> str:
81
+ """Build ``<random24>_<leaf>`` with whole-name collision regen.
82
+
83
+ *leaf* must already be sanitized (see :func:`sanitize_cap`). Regenerates
84
+ the random prefix (bounded retries) until the whole name is unique within
85
+ *existing*. Shared by :func:`make_standalone_box_name` (leaf from the root
86
+ basename) and :func:`resolve_standalone_name` (leaf from a supplied string).
87
+
88
+ Raises :class:`RuntimeError` if a unique name cannot be found within
89
+ :data:`_MAX_REGEN_ATTEMPTS` attempts.
90
+ """
91
+ for _ in range(_MAX_REGEN_ATTEMPTS):
92
+ name = f"{random24()}_{leaf}"
93
+ if name not in existing:
94
+ return name
95
+ raise RuntimeError(
96
+ "Could not generate a unique standalone box name after "
97
+ f"{_MAX_REGEN_ATTEMPTS} attempts."
98
+ )
99
+
100
+
101
+ def make_standalone_box_name(root: Path, existing: set[str]) -> str:
102
+ """Generate a unique standalone box name for project *root*.
103
+
104
+ Builds ``<random24>_<sanitize_cap(root.name)>`` and, if that whole name is
105
+ already in *existing*, regenerates the random prefix (bounded retries) until
106
+ the name is unique. *existing* is the set of standalone box names currently
107
+ registered (see ``registry.standalone``).
108
+
109
+ Raises :class:`RuntimeError` if a unique name cannot be found within
110
+ :data:`_MAX_REGEN_ATTEMPTS` attempts (effectively impossible with a sane
111
+ ``existing`` set; the bound guards against a degenerate caller).
112
+ """
113
+ return _generate_with_leaf(sanitize_cap(root.name), existing)
114
+
115
+
116
+ def resolve_standalone_name(
117
+ root: Path, supplied: str, existing: set[str],
118
+ ) -> str:
119
+ """Resolve the standalone box name for *root* given a user *supplied* name.
120
+
121
+ Pure (modulo ``os.urandom``) so it is directly unit-testable. Branches:
122
+
123
+ 1. *supplied* empty → :func:`make_standalone_box_name` (fresh prefix +
124
+ sanitized ``root.name`` leaf, whole-name collision regen).
125
+ 2. Otherwise lowercase *supplied*, then:
126
+
127
+ * If it does NOT match the canonical ``<random24>_<leaf>`` shape (see
128
+ :func:`is_canonical_standalone_name`) → treat the WHOLE supplied string
129
+ as a raw leaf: ``<fresh-random>_<sanitize_cap(supplied)>`` with
130
+ collision regen. (An over-long / illegal name lands here.)
131
+ * If it DOES match → the user is asserting a full canonical id verbatim.
132
+ If it is free in *existing* → return it as-is; if taken → raise
133
+ :class:`~kanibako.errors.ProjectError`.
134
+ """
135
+ if not supplied:
136
+ return make_standalone_box_name(root, existing)
137
+
138
+ supplied = supplied.lower()
139
+
140
+ if not is_canonical_standalone_name(supplied):
141
+ # Not a canonical id: use the whole supplied string as the leaf source.
142
+ return _generate_with_leaf(sanitize_cap(supplied), existing)
143
+
144
+ # A verbatim canonical id: honor it if free, else refuse with guidance.
145
+ if supplied in existing:
146
+ prefix = supplied.partition("_")[0]
147
+ raise ProjectError(
148
+ f"already a box with that name '{supplied}' — try without the "
149
+ f"'{prefix}_' prefix to generate a fresh one"
150
+ )
151
+ return supplied
@@ -0,0 +1,253 @@
1
+ """Channel path resolution + per-instance partition addressing (PURE helpers).
2
+
3
+ Phase 6 (sub-step 6a) of the 1.6.0 config/settings revamp. This module derives
4
+ the host-side channel paths from an already-resolved :class:`~kanibako.paths.
5
+ ProjectPaths` (``proj``) + :class:`~kanibako.paths.StandardPaths` (``std``). It
6
+ is **pure derivation only** — it computes paths, it does NOT create directories,
7
+ touch launch mounts, or change any behavior. The bind production / mkdir /
8
+ file-seeding live in sub-step 6b.
9
+
10
+ Two channel scopes (TARGET §1, §2c, §2f):
11
+
12
+ * **system scope** — five type roots under ``@system.channels`` (commons, chat,
13
+ broadcast, mailboxes, share). The instance-owned types (mailboxes, share) are
14
+ *partitioned* by the workset-name token: ``mailboxes/<ws>`` and ``share/<ws>``
15
+ where ``<ws>`` is ``__PRIMARY__`` | ``<named>`` | ``__STANDALONE__``. These
16
+ partition roots apply to EVERY mode (standalone included).
17
+ * **workset scope** — three type roots under ``@workset.meta.root/channels``
18
+ (commons, chat, share). These exist for the PRIMARY and NAMED modes ONLY;
19
+ standalone has no workset-local channels (its ``workset.channels`` is
20
+ ``<None>`` per the TARGET).
21
+
22
+ The per-instance partition ADDRESSES (``box.meta.{inbox,share_global,
23
+ share_workset}``) are this box's own mailbox/share dirs *within* the partitioned
24
+ roots — derived from the workset-name token + the box name.
25
+
26
+ ⚑ The workset-name token and the workset root are NOT carried verbatim on
27
+ ``ProjectPaths`` (the legacy comms block used only ``proj.name``). Per the
28
+ design-review A8 resolution we derive them HERE from ``proj.mode`` + ``proj.
29
+ group`` rather than widening the resolver's public shape: PRIMARY roots at
30
+ ``@system.primary_workset`` with token ``__PRIMARY__``; NAMED roots at
31
+ ``proj.group.root`` with token ``proj.group.name``; STANDALONE roots at
32
+ ``proj.project_path`` with token ``__STANDALONE__``.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from dataclasses import dataclass
38
+ from pathlib import Path
39
+ from typing import TYPE_CHECKING
40
+
41
+ if TYPE_CHECKING: # avoid an import cycle (paths.py would import this later)
42
+ from kanibako.paths import ProjectPaths, StandardPaths
43
+
44
+
45
+ # Reserved workset-name tokens for the system-scope partition key. A named
46
+ # workset may not use either (Phase 5 reserves them at create time, 5e); they
47
+ # stand for the PRIMARY and STANDALONE pseudo-worksets.
48
+ WS_TOKEN_PRIMARY = "__PRIMARY__"
49
+ WS_TOKEN_STANDALONE = "__STANDALONE__"
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class SystemPartition:
54
+ """The per-workset SYSTEM-scope partition roots (keyed by the ws token).
55
+
56
+ ``mailboxes`` == ``@system.channels.mailboxes/<ws>`` and ``share`` ==
57
+ ``@system.channels.share/<ws>``. These are the *parents* under which each
58
+ box gets its own ``<box>`` subdir (see :func:`box_channel_addresses`).
59
+ """
60
+
61
+ ws_token: str
62
+ mailboxes: Path
63
+ share: Path
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class WorksetChannels:
68
+ """The workset-local channel roots (PRIMARY/NAMED only).
69
+
70
+ ``@workset.channels = @workset.meta.root/channels`` with ``commons``,
71
+ ``chat`` (+ the reserved ``broadcast.md`` / default ``general.md`` files
72
+ inside it), and ``share`` (per-box subdirs are ``box.meta.share_workset``).
73
+ """
74
+
75
+ root: Path
76
+ commons: Path
77
+ chat: Path
78
+ chat_general: Path
79
+ chat_broadcast: Path
80
+ share: Path
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class BoxChannelAddresses:
85
+ """This box's own partition ADDRESSES (TARGET §2c ``box.meta.*``).
86
+
87
+ * ``inbox`` == ``@system.channels.mailboxes/<ws>/<box>`` — this box's own
88
+ mailbox dir (also surfaced in-box at ``~/channels/inbox``).
89
+ * ``share_global`` == ``@system.channels.share/<ws>/<box>`` — this box's own
90
+ system-scope publication dir.
91
+ * ``share_workset`` == ``@workset.channels.share/<box>`` — this box's own
92
+ workset-scope publication dir; ``None`` for standalone (no workset-local
93
+ channels).
94
+ """
95
+
96
+ ws_token: str
97
+ box_name: str
98
+ inbox: Path
99
+ share_global: Path
100
+ share_workset: Path | None
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class OwnPartition:
105
+ """This box's OWN system-scope partition dirs (mailbox + share_global).
106
+
107
+ ``mailbox`` == ``@system.channels.mailboxes/<ws>/<box>`` (this box's inbox
108
+ source dir) and ``share_global`` == ``@system.channels.share/<ws>/<box>``
109
+ (this box's system-scope publication dir). Used by the move/convert
110
+ relocation (6d), which addresses the partition by raw ``(ws_token, box)``
111
+ rather than a fully-resolved :class:`ProjectPaths`.
112
+ """
113
+
114
+ ws_token: str
115
+ box_name: str
116
+ mailbox: Path
117
+ share_global: Path
118
+
119
+
120
+ def own_partition_dirs(
121
+ std: StandardPaths, ws_token: str, box_name: str
122
+ ) -> OwnPartition:
123
+ """Derive a box's OWN system-scope partition dirs from ``(ws_token, box)``.
124
+
125
+ The lower-level primitive behind :func:`box_channel_addresses` — it takes the
126
+ workset-name token + box name directly (no ``ProjectPaths``), so the lifecycle
127
+ relocation can compute BOTH the OLD and the NEW partition for a box being
128
+ moved/converted. Mirrors :func:`system_partition` + the ``box.meta.{inbox,
129
+ share_global}`` joins (TARGET §2c).
130
+ """
131
+ part = system_partition(std, ws_token)
132
+ return OwnPartition(
133
+ ws_token=ws_token,
134
+ box_name=box_name,
135
+ mailbox=part.mailboxes / box_name,
136
+ share_global=part.share / box_name,
137
+ )
138
+
139
+
140
+ def workset_name_token(proj: ProjectPaths) -> str:
141
+ """Return the workset-name token for *proj* (the system partition key).
142
+
143
+ ``__PRIMARY__`` for PRIMARY mode, the named workset's name for NAMED mode,
144
+ ``__STANDALONE__`` for STANDALONE mode. Derived from ``proj.mode`` +
145
+ ``proj.group`` (A8) rather than read off a dedicated field.
146
+ """
147
+ # Lazy import keeps this module free of an import cycle with paths.py.
148
+ from kanibako.paths import BoxMode
149
+
150
+ if proj.mode is BoxMode.primary:
151
+ return WS_TOKEN_PRIMARY
152
+ if proj.mode is BoxMode.standalone:
153
+ return WS_TOKEN_STANDALONE
154
+ # NAMED: the partition key is the named workset's name.
155
+ if proj.group is None or not proj.group.name:
156
+ raise ValueError(
157
+ "NAMED box is missing its workset group/name; cannot derive the "
158
+ "channel partition token."
159
+ )
160
+ return proj.group.name
161
+
162
+
163
+ def workset_root(proj: ProjectPaths, std: StandardPaths) -> Path:
164
+ """Return ``@workset.meta.root`` for *proj*.
165
+
166
+ PRIMARY → ``@system.primary_workset``; NAMED → the named workset root
167
+ (``proj.group.root``); STANDALONE → the project dir itself
168
+ (``proj.project_path``).
169
+ """
170
+ from kanibako.paths import BoxMode
171
+
172
+ if proj.mode is BoxMode.primary:
173
+ return std.primary_workset
174
+ if proj.mode is BoxMode.standalone:
175
+ return proj.project_path
176
+ if proj.group is None:
177
+ raise ValueError(
178
+ "NAMED box is missing its workset group; cannot derive the workset "
179
+ "root."
180
+ )
181
+ return proj.group.root
182
+
183
+
184
+ def has_workset_channels(proj: ProjectPaths) -> bool:
185
+ """True iff *proj* gets workset-local channels (PRIMARY/NAMED, not standalone).
186
+
187
+ Standalone omits ``~/channels/workset/*`` (no workset-local channels); its
188
+ system-scope partition (mailboxes/share_global) still applies (A10).
189
+ """
190
+ from kanibako.paths import BoxMode
191
+
192
+ return proj.mode is not BoxMode.standalone
193
+
194
+
195
+ def system_partition(std: StandardPaths, ws_token: str) -> SystemPartition:
196
+ """Derive the SYSTEM-scope per-workset partition roots for *ws_token*.
197
+
198
+ ``mailboxes/<ws>`` and ``share/<ws>`` under the resolved system channels
199
+ skeleton. Applies to EVERY mode — do NOT gate this off the workset-local
200
+ channels (D-M9): standalone still has a ``__STANDALONE__`` partition.
201
+ """
202
+ return SystemPartition(
203
+ ws_token=ws_token,
204
+ mailboxes=std.channels_mailboxes / ws_token,
205
+ share=std.channels_share / ws_token,
206
+ )
207
+
208
+
209
+ def workset_channel_paths(
210
+ proj: ProjectPaths, std: StandardPaths
211
+ ) -> WorksetChannels | None:
212
+ """Derive the WORKSET-local channel roots for *proj* (PRIMARY/NAMED only).
213
+
214
+ Returns ``None`` for standalone (no workset-local channels). Rooted at
215
+ ``@workset.meta.root/channels`` (A3 — a derived helper, NOT new fields on
216
+ ``ProjectPaths``).
217
+ """
218
+ if not has_workset_channels(proj):
219
+ return None
220
+ root = workset_root(proj, std) / "channels"
221
+ chat = root / "chat"
222
+ return WorksetChannels(
223
+ root=root,
224
+ commons=root / "commons",
225
+ chat=chat,
226
+ chat_general=chat / "general.md",
227
+ chat_broadcast=chat / "broadcast.md",
228
+ share=root / "share",
229
+ )
230
+
231
+
232
+ def box_channel_addresses(
233
+ proj: ProjectPaths, std: StandardPaths
234
+ ) -> BoxChannelAddresses:
235
+ """Derive this box's own partition addresses (``box.meta.*``) for *proj*.
236
+
237
+ ``inbox`` / ``share_global`` always resolve (system-scope, every mode);
238
+ ``share_workset`` is ``None`` for standalone. *proj.name* is the box name.
239
+ """
240
+ if not proj.name:
241
+ raise ValueError(
242
+ "box has no name; cannot derive its channel partition addresses."
243
+ )
244
+ ws_token = workset_name_token(proj)
245
+ part = system_partition(std, ws_token)
246
+ wch = workset_channel_paths(proj, std)
247
+ return BoxChannelAddresses(
248
+ ws_token=ws_token,
249
+ box_name=proj.name,
250
+ inbox=part.mailboxes / proj.name,
251
+ share_global=part.share / proj.name,
252
+ share_workset=(wch.share / proj.name) if wch is not None else None,
253
+ )