kanibako-cli 1.6.0.dev30__tar.gz → 1.6.0.dev31__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 (179) hide show
  1. {kanibako_cli-1.6.0.dev30/src/kanibako_cli.egg-info → kanibako_cli-1.6.0.dev31}/PKG-INFO +1 -1
  2. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/pyproject.toml +1 -1
  3. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/__init__.py +1 -1
  4. kanibako_cli-1.6.0.dev31/src/kanibako/box_seed.py +84 -0
  5. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/start.py +57 -12
  6. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/registry_store.py +60 -1
  7. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/templates.py +18 -48
  8. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/workset.py +24 -1
  9. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31/src/kanibako_cli.egg-info}/PKG-INFO +1 -1
  10. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako_cli.egg-info/SOURCES.txt +2 -0
  11. kanibako_cli-1.6.0.dev31/tests/test_box_seed.py +137 -0
  12. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_registry_store.py +76 -0
  13. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_templates.py +0 -73
  14. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_workset.py +71 -0
  15. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/LICENSE.md +0 -0
  16. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/README.md +0 -0
  17. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/setup.cfg +0 -0
  18. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/__main__.py +0 -0
  19. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/_atomic.py +0 -0
  20. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/agent_config.py +0 -0
  21. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/auth_browser.py +0 -0
  22. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/auth_parser.py +0 -0
  23. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/baseline.py +0 -0
  24. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/box_identity.py +0 -0
  25. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/browser_sidecar.py +0 -0
  26. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/browser_state.py +0 -0
  27. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/bun_sea.py +0 -0
  28. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/channels.py +0 -0
  29. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/cli.py +0 -0
  30. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/__init__.py +0 -0
  31. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/agent_cmd.py +0 -0
  32. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/archive.py +0 -0
  33. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/baseline_cmd.py +0 -0
  34. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/box/__init__.py +0 -0
  35. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/box/_duplicate.py +0 -0
  36. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/box/_lifecycle.py +0 -0
  37. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/box/_parser.py +0 -0
  38. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/clean.py +0 -0
  39. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/diagnose.py +0 -0
  40. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/flags.py +0 -0
  41. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/fork_cmd.py +0 -0
  42. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/helper_cmd.py +0 -0
  43. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/image.py +0 -0
  44. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/install.py +0 -0
  45. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/restore.py +0 -0
  46. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/setup_cmd.py +0 -0
  47. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/stop.py +0 -0
  48. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/system_cmd.py +0 -0
  49. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/upgrade.py +0 -0
  50. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/vault_cmd.py +0 -0
  51. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/workset_cmd.py +0 -0
  52. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/config.py +0 -0
  53. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/config_interface.py +0 -0
  54. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/config_io.py +0 -0
  55. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/container.py +0 -0
  56. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containerfiles.py +0 -0
  57. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/Containerfile.template-android +0 -0
  58. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/Containerfile.template-dotnet +0 -0
  59. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/Containerfile.template-js +0 -0
  60. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/Containerfile.template-jvm +0 -0
  61. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/Containerfile.template-systems +0 -0
  62. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/__init__.py +0 -0
  63. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/tmux.conf +0 -0
  64. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/data/__init__.py +0 -0
  65. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/data/image-baseline.yaml +0 -0
  66. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/data/templates/base/INSTRUCTIONS.md +0 -0
  67. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/deprecation.py +0 -0
  68. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/errors.py +0 -0
  69. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/freshness.py +0 -0
  70. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/git.py +0 -0
  71. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/helper_client.py +0 -0
  72. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/helper_listener.py +0 -0
  73. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/helpers.py +0 -0
  74. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/hygiene.py +0 -0
  75. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/image_sharing.py +0 -0
  76. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/import_reconcile.py +0 -0
  77. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/install_method.py +0 -0
  78. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/log.py +0 -0
  79. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/names.py +0 -0
  80. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/paths.py +0 -0
  81. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/plugins/__init__.py +0 -0
  82. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/py.typed +0 -0
  83. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/registry.py +0 -0
  84. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/rig_bundle.py +0 -0
  85. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/rig_meta.py +0 -0
  86. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/rig_registry.py +0 -0
  87. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/rig_resolve.py +0 -0
  88. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/rig_source.py +0 -0
  89. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/scripts/__init__.py +0 -0
  90. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/scripts/helper-init.sh +0 -0
  91. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/scripts/kanibako-entry +0 -0
  92. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/settings_categories.py +0 -0
  93. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/settings_resolve.py +0 -0
  94. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/settings_seeds.py +0 -0
  95. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/settings_shares.py +0 -0
  96. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/shellenv.py +0 -0
  97. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/shells.py +0 -0
  98. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/snapshots.py +0 -0
  99. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/targets/__init__.py +0 -0
  100. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/targets/assembly.py +0 -0
  101. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/targets/base.py +0 -0
  102. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/targets/credsync.py +0 -0
  103. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/targets/no_agent.py +0 -0
  104. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/templates_image.py +0 -0
  105. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/tweakcc.py +0 -0
  106. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/tweakcc_cache.py +0 -0
  107. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/utils.py +0 -0
  108. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako_cli.egg-info/dependency_links.txt +0 -0
  109. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako_cli.egg-info/entry_points.txt +0 -0
  110. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako_cli.egg-info/requires.txt +0 -0
  111. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako_cli.egg-info/top_level.txt +0 -0
  112. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_agent_config.py +0 -0
  113. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_agent_resolution.py +0 -0
  114. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_archive_restore_integration.py +0 -0
  115. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_atomic.py +0 -0
  116. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_auth_browser.py +0 -0
  117. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_auth_parser.py +0 -0
  118. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_baseline.py +0 -0
  119. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_box_identity.py +0 -0
  120. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_browser_sidecar.py +0 -0
  121. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_browser_state.py +0 -0
  122. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_bun_sea.py +0 -0
  123. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_channels.py +0 -0
  124. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_cli.py +0 -0
  125. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_cli_exitcodes.py +0 -0
  126. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_comms.py +0 -0
  127. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_config.py +0 -0
  128. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_config_interface.py +0 -0
  129. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_container.py +0 -0
  130. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_container_extended.py +0 -0
  131. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_container_integration.py +0 -0
  132. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_containerfiles.py +0 -0
  133. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_credentials.py +0 -0
  134. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_deprecations.py +0 -0
  135. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_env_cmd.py +0 -0
  136. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_error_messages.py +0 -0
  137. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_fork.py +0 -0
  138. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_freshness.py +0 -0
  139. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_git.py +0 -0
  140. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_helper_client.py +0 -0
  141. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_helper_cmd.py +0 -0
  142. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_helper_listener.py +0 -0
  143. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_helpers.py +0 -0
  144. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_hygiene.py +0 -0
  145. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_image_sharing.py +0 -0
  146. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_import.py +0 -0
  147. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_init_cmd.py +0 -0
  148. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_install_integration.py +0 -0
  149. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_lifecycle_integration.py +0 -0
  150. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_log.py +0 -0
  151. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_names.py +0 -0
  152. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_paths.py +0 -0
  153. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_paths_extended.py +0 -0
  154. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_registry.py +0 -0
  155. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_resolve_box_target.py +0 -0
  156. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_resource_scoping.py +0 -0
  157. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_rig_bundle.py +0 -0
  158. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_rig_meta.py +0 -0
  159. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_rig_registry.py +0 -0
  160. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_rig_resolve.py +0 -0
  161. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_rig_source.py +0 -0
  162. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_safety_invariants.py +0 -0
  163. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_settings_categories.py +0 -0
  164. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_settings_loader.py +0 -0
  165. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_settings_resolve.py +0 -0
  166. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_settings_seeds.py +0 -0
  167. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_settings_shares.py +0 -0
  168. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_setup_cmd.py +0 -0
  169. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_shellenv.py +0 -0
  170. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_shells.py +0 -0
  171. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_snapshots.py +0 -0
  172. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_standalone_paths.py +0 -0
  173. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_start_integration.py +0 -0
  174. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_status.py +0 -0
  175. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_system_paths.py +0 -0
  176. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_templates_image.py +0 -0
  177. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_tweakcc.py +0 -0
  178. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_utils.py +0 -0
  179. {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/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.6.0.dev30
3
+ Version: 1.6.0.dev31
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.6.0.dev30"
7
+ version = "1.6.0.dev31"
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", "packaging>=20.0"]
@@ -1,6 +1,6 @@
1
1
  """kanibako: Run AI coding agents in rootless containers with per-project isolation."""
2
2
 
3
- __version__ = "1.6.0.dev30"
3
+ __version__ = "1.6.0.dev31"
4
4
 
5
5
  # The oldest build version whose ``setup`` run is still considered current. The
6
6
  # setup-completion gate (W1) re-nudges the user to re-run ``kanibako setup`` when
@@ -0,0 +1,84 @@
1
+ """Uniform per-box seed-once query/record API.
2
+
3
+ This is the single read/write surface the launch path (Step 3) will call to
4
+ ask "has this box already been seeded?" and to record "this box is now
5
+ seeded". It hides where each box's seeded flag lives by dispatching on
6
+ ``proj.mode`` (a :class:`kanibako.paths.BoxMode`) to whichever registry owns
7
+ the box:
8
+
9
+ * ``BoxMode.primary`` → the ``seeded`` section's ``projects`` domain in
10
+ ``registry.yaml`` (keyed by ``proj.name``).
11
+ * ``BoxMode.standalone`` → the ``seeded`` section's ``standalone`` domain in
12
+ ``registry.yaml`` (keyed by ``proj.name``).
13
+ * ``BoxMode.named`` → ``WorksetProject.seeded`` inside the owning
14
+ workset's ``settings.yaml`` (located via ``proj.group.root``).
15
+
16
+ It is the explicit, registry-resident successor to the brittle ``.seeded``
17
+ sentinel file (now REMOVED): Step 3 wired launch-time detection over to here,
18
+ ORing this registry flag with an existing-inbox backstop for legacy boxes.
19
+ Layering: this module
20
+ depends on ``paths`` (types only), ``registry_store``, and ``workset``; none of
21
+ those import ``box_seed``, so there is no import cycle.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from kanibako import registry_store
27
+ from kanibako.paths import BoxMode, ProjectPaths, StandardPaths
28
+
29
+
30
+ def box_is_seeded(proj: ProjectPaths, std: StandardPaths) -> bool:
31
+ """Return whether *proj*'s box has completed its one-time seed.
32
+
33
+ Dispatches on ``proj.mode`` to the owning registry. Returns False for a
34
+ box that has never been seeded (or, defensively, a ``named`` box whose
35
+ group/workset-project cannot be resolved).
36
+ """
37
+ if proj.mode is BoxMode.primary:
38
+ return registry_store.is_box_seeded(std.data_path, "projects", proj.name)
39
+ if proj.mode is BoxMode.standalone:
40
+ return registry_store.is_box_seeded(std.data_path, "standalone", proj.name)
41
+ # BoxMode.named: the flag lives on the WorksetProject.
42
+ if proj.group is None:
43
+ return False
44
+ from kanibako.errors import WorksetError
45
+ from kanibako.workset import load_workset
46
+
47
+ try:
48
+ ws = load_workset(proj.group.root)
49
+ except WorksetError:
50
+ # Defensive: a named box whose workset cannot be loaded (e.g. its
51
+ # settings.yaml is absent or not yet materialised) reads as unseeded.
52
+ return False
53
+ for p in ws.projects:
54
+ if p.name == proj.name:
55
+ return p.seeded
56
+ return False
57
+
58
+
59
+ def mark_box_seeded(proj: ProjectPaths, std: StandardPaths) -> None:
60
+ """Record that *proj*'s box has completed its one-time seed.
61
+
62
+ Dispatches on ``proj.mode`` to the owning registry. Idempotent; a no-op
63
+ (defensive) for a ``named`` box whose group/workset-project cannot be
64
+ resolved.
65
+ """
66
+ if proj.mode is BoxMode.primary:
67
+ registry_store.mark_box_seeded_entry(std.data_path, "projects", proj.name)
68
+ return
69
+ if proj.mode is BoxMode.standalone:
70
+ registry_store.mark_box_seeded_entry(std.data_path, "standalone", proj.name)
71
+ return
72
+ # BoxMode.named: persist on the WorksetProject via the workset's public API.
73
+ if proj.group is None:
74
+ return
75
+ from kanibako.errors import WorksetError
76
+ from kanibako.workset import load_workset, set_project_seeded
77
+
78
+ try:
79
+ ws = load_workset(proj.group.root)
80
+ set_project_seeded(ws, proj.name)
81
+ except WorksetError:
82
+ # Defensive: a named box whose workset cannot be loaded/updated is a
83
+ # no-op (mirrors the missing-group guard above).
84
+ return
@@ -37,6 +37,31 @@ from kanibako.targets import assembly, credsync, resolve_target
37
37
  from kanibako.targets.assembly import BindingSourceError, descriptor_mounts
38
38
  from kanibako.utils import container_name_for, project_hash, short_hash
39
39
 
40
+ from typing import TYPE_CHECKING
41
+
42
+ if TYPE_CHECKING:
43
+ from kanibako.paths import ProjectPaths, StandardPaths
44
+
45
+
46
+ def _box_already_seeded(proj: ProjectPaths, std: StandardPaths) -> bool:
47
+ """True when *proj*'s box has already had its one-time home seed applied.
48
+
49
+ Two ORed signals decide this:
50
+
51
+ * the authoritative per-box ``seeded`` registry flag
52
+ (:func:`kanibako.box_seed.box_is_seeded`); and
53
+ * the backstop that the box's own mailbox (inbox) dir already exists
54
+ — that dir is created during launch AFTER the seed gate, so on a first
55
+ launch it is absent (→ seed) and on every later launch it is present
56
+ (→ skip). The inbox backstop also covers LEGACY boxes created before
57
+ the registry flag existed.
58
+ """
59
+ from kanibako import box_seed, channels
60
+
61
+ if box_seed.box_is_seeded(proj, std):
62
+ return True
63
+ return channels.box_channel_addresses(proj, std).inbox.exists()
64
+
40
65
 
41
66
  def add_start_parser(subparsers: argparse._SubParsersAction) -> None:
42
67
  p = subparsers.add_parser(
@@ -902,13 +927,28 @@ def _run_container(
902
927
  logger.info(action)
903
928
 
904
929
  # Seed-once gate (BUG-D): seed the box home exactly once, on its FIRST
905
- # start — whether the box was created by this very `start` (proj.is_new)
906
- # or earlier by `box create` (which makes the metadata dir, leaving
907
- # is_new False at start time). The `.seeded` sentinel under the metadata
908
- # dir records completion so subsequent launches never re-seed (user edits
909
- # to seeded files survive, D-B6).
910
- from kanibako.templates import mark_seeded, needs_seed
911
- seed_box = needs_seed(proj.metadata_path, is_new=proj.is_new)
930
+ # start — whether the box was created by this very `start` or earlier by
931
+ # `box create`. Detection ORs two signals (see `_box_already_seeded`):
932
+ # the authoritative per-box `seeded` registry flag, plus a backstop that
933
+ # the box's mailbox (inbox) already exists. The inbox dir is created
934
+ # later in launch (after this gate), so it is absent on a first start
935
+ # (→ seed) and present on every later start (→ skip), and it also covers
936
+ # LEGACY boxes that predate the registry flag. This replaces the brittle
937
+ # `.seeded` sentinel file (removed); the non-destructive create-if-absent
938
+ # template/seed application (S1) is the failsafe so a mis-detection can
939
+ # never clobber user edits.
940
+ from kanibako.box_seed import box_is_seeded, mark_box_seeded
941
+ flag_seeded = box_is_seeded(proj, std)
942
+ already_seeded = _box_already_seeded(proj, std)
943
+ seed_box = not already_seeded
944
+ # ADOPT-ON-DETECT: a legacy box detected seeded only via its existing
945
+ # inbox (flag still False) gets the authoritative flag stamped once, so
946
+ # future detection uses the flag. Cheap + idempotent; never fatal.
947
+ if not flag_seeded and already_seeded:
948
+ try:
949
+ mark_box_seeded(proj, std)
950
+ except Exception: # pragma: no cover - adopt is best-effort
951
+ logger.debug("adopt-on-detect: could not stamp seeded flag", exc_info=True)
912
952
 
913
953
  # Template application + agent init for new projects.
914
954
  if seed_box:
@@ -948,10 +988,11 @@ def _run_container(
948
988
  logger=logger, group_auth=proj.group_auth,
949
989
  )
950
990
 
951
- # Record seed-once completion so subsequent launches skip the blocks
952
- # above (idempotent; only the first start writes meaningful content).
991
+ # Record seed-once completion via the authoritative per-box registry
992
+ # flag so subsequent launches skip the blocks above (idempotent; only the
993
+ # first start writes meaningful content).
953
994
  if seed_box:
954
- mark_seeded(proj.metadata_path)
995
+ mark_box_seeded(proj, std)
955
996
 
956
997
  # Synced copies (the `<scope>.synced.<name>` category) — applied on
957
998
  # EVERY launch (mtime-gated), unlike copy-once seeds. Distinct from the
@@ -2010,6 +2051,7 @@ def _apply_init_seeds(
2010
2051
  import shutil
2011
2052
 
2012
2053
  from kanibako.settings_resolve import GUEST_HOME
2054
+ from kanibako.templates import copy_resource_tree_if_absent
2013
2055
 
2014
2056
  default_seeds = target.default_seeds() if target is not None else {}
2015
2057
 
@@ -2050,8 +2092,11 @@ def _apply_init_seeds(
2050
2092
  continue
2051
2093
  if src.is_dir():
2052
2094
  dest.mkdir(parents=True, exist_ok=True)
2053
- shutil.copytree(str(src), str(dest), dirs_exist_ok=True)
2054
- else:
2095
+ # Create-if-absent: a seed delivers content ONCE; existing home
2096
+ # content is owned by the box and must never be overwritten by a
2097
+ # re-seed (the playbook-clobber bug).
2098
+ copy_resource_tree_if_absent(src, dest)
2099
+ elif not dest.exists():
2055
2100
  dest.parent.mkdir(parents=True, exist_ok=True)
2056
2101
  shutil.copy2(str(src), str(dest))
2057
2102
 
@@ -66,6 +66,7 @@ _SECTIONS: tuple[str, ...] = (
66
66
  "workset_roots",
67
67
  "connected",
68
68
  "standalone",
69
+ "seeded",
69
70
  "rigs",
70
71
  "image_shells",
71
72
  )
@@ -95,6 +96,9 @@ def load_registry(data_path: Path) -> dict[str, dict]:
95
96
  """
96
97
  path = registry_path(data_path)
97
98
  data = load_doc(path) if path.is_file() else {}
99
+ seeded_raw = data.get("seeded", {})
100
+ if not isinstance(seeded_raw, dict):
101
+ seeded_raw = {}
98
102
  return {
99
103
  "projects": {
100
104
  k: str(v) for k, v in dict(data.get("projects", {})).items()
@@ -107,6 +111,16 @@ def load_registry(data_path: Path) -> dict[str, dict]:
107
111
  },
108
112
  "connected": dict(data.get("connected", {})),
109
113
  "standalone": dict(data.get("standalone", {})),
114
+ "seeded": {
115
+ "projects": {
116
+ k: bool(v)
117
+ for k, v in dict(seeded_raw.get("projects", {})).items()
118
+ },
119
+ "standalone": {
120
+ k: bool(v)
121
+ for k, v in dict(seeded_raw.get("standalone", {})).items()
122
+ },
123
+ },
110
124
  "rigs": dict(data.get("rigs", {})),
111
125
  "image_shells": dict(data.get("image_shells", {})),
112
126
  }
@@ -122,7 +136,17 @@ def save_registry(data_path: Path, registry: dict[str, dict]) -> None:
122
136
  data: dict = {}
123
137
  for section in _SECTIONS:
124
138
  entries = registry.get(section, {}) or {}
125
- if section in _NAME_SECTIONS:
139
+ if section == "seeded":
140
+ # Nested ``{domain: {name: bool}}`` shape (NOT a name->path map):
141
+ # persist each domain with its inner keys sorted for stable diffs.
142
+ data[section] = {
143
+ domain: {
144
+ name: bool(entries.get(domain, {})[name])
145
+ for name in sorted(entries.get(domain, {}))
146
+ }
147
+ for domain in ("projects", "standalone")
148
+ }
149
+ elif section in _NAME_SECTIONS:
126
150
  data[section] = {name: entries[name] for name in sorted(entries)}
127
151
  else:
128
152
  data[section] = dict(entries)
@@ -224,3 +248,38 @@ def standalone_name_for_root(data_path: Path, root: Path) -> str | None:
224
248
  if root_str == target:
225
249
  return name
226
250
  return None
251
+
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # Seeded-flag helpers (``seeded`` section: {domain: {box_name: bool}})
255
+ # ---------------------------------------------------------------------------
256
+ #
257
+ # These are the per-box seed-once read/write primitives for the PRIMARY
258
+ # (``domain == "projects"``) and STANDALONE (``domain == "standalone"``)
259
+ # registries, the explicit successor to the brittle ``.seeded`` sentinel file.
260
+ # The named-workset seeded flag lives on ``WorksetProject.seeded`` instead.
261
+ # The uniform launch-path API that dispatches across all three lives in
262
+ # :mod:`kanibako.box_seed`.
263
+
264
+
265
+ def is_box_seeded(data_path: Path, domain: str, box_name: str) -> bool:
266
+ """Return whether *box_name* in *domain* has completed its one-time seed.
267
+
268
+ *domain* is ``"projects"`` (PRIMARY) or ``"standalone"``. False when the
269
+ section/domain/name is absent (a fresh or legacy registry).
270
+ """
271
+ return bool(
272
+ load_registry(data_path)["seeded"].get(domain, {}).get(box_name, False)
273
+ )
274
+
275
+
276
+ def mark_box_seeded_entry(data_path: Path, domain: str, box_name: str) -> None:
277
+ """Record that *box_name* in *domain* has completed its one-time seed.
278
+
279
+ Idempotent; preserves every sibling section (whole-file rewrite via
280
+ ``save_registry``). Reuses ``load_registry``/``save_registry`` — no extra
281
+ YAML I/O.
282
+ """
283
+ registry = load_registry(data_path)
284
+ registry["seeded"].setdefault(domain, {})[box_name] = True
285
+ save_registry(data_path, registry)
@@ -62,44 +62,6 @@ def workset_template_dir(proj: ProjectPaths, std: StandardPaths) -> Path | None:
62
62
  return workset_root(proj, std) / "template"
63
63
 
64
64
 
65
- #: Sentinel filename (under the box metadata dir) recording that the seed-once
66
- #: home content (templates + credential seeds + init seeds) has been applied.
67
- #: Its presence — NOT ``proj.is_new`` — is what gates seed-once application, so a
68
- #: box made via the two-step ``box create`` then ``start`` flow (which creates
69
- #: the metadata dir at CREATE time, leaving ``is_new`` False at START) is still
70
- #: seeded exactly once on its first ``start`` (BUG-D).
71
- SEED_MARKER = ".seeded"
72
-
73
-
74
- def seed_marker_path(metadata_path: Path) -> Path:
75
- """Return the seed-once sentinel path under *metadata_path*."""
76
- return metadata_path / SEED_MARKER
77
-
78
-
79
- def needs_seed(metadata_path: Path, *, is_new: bool) -> bool:
80
- """True when the box still needs its one-time home seeding.
81
-
82
- A box needs seeding when it is freshly created (*is_new*) OR when it was
83
- created earlier (e.g. via ``box create``) but never seeded — detected by the
84
- absence of the :data:`SEED_MARKER` sentinel under *metadata_path*. Once the
85
- sentinel exists the box is never re-seeded, so user edits to seeded files
86
- survive subsequent launches (D-B6).
87
- """
88
- if is_new:
89
- return True
90
- return not seed_marker_path(metadata_path).exists()
91
-
92
-
93
- def mark_seeded(metadata_path: Path) -> None:
94
- """Record that the box's one-time home seeding has completed.
95
-
96
- Writes the :data:`SEED_MARKER` sentinel under *metadata_path* so a later
97
- launch's :func:`needs_seed` returns False. Idempotent.
98
- """
99
- metadata_path.mkdir(parents=True, exist_ok=True)
100
- seed_marker_path(metadata_path).touch()
101
-
102
-
103
65
  def apply_template_layers(
104
66
  home: Path,
105
67
  layers: list[Path | None],
@@ -109,10 +71,11 @@ def apply_template_layers(
109
71
  The 1.6.0 home-seed model layers ordered template sources into the box home
110
72
  at creation (base -> agent -> workset; later overlays earlier). The copy is
111
73
  NON-DESTRUCTIVE toward files that ALREADY EXIST in *home* before this seed
112
- pass begins: such a pre-existing file (a user-edited home file on a
113
- marker-less migrated box) is NEVER clobbered. This is the guard against
114
- re-seed DATA LOSS — a box whose home is already populated but lacks the
115
- ``.seeded`` marker would otherwise have its files overwritten.
74
+ pass begins: such a pre-existing file (a user-edited home file on a box that
75
+ detection mis-flags as un-seeded) is NEVER clobbered. This is the guard
76
+ against re-seed DATA LOSS — a box whose home is already populated but which
77
+ detection (the per-box registry flag + inbox backstop) fails to recognise as
78
+ seeded would otherwise have its files overwritten.
116
79
 
117
80
  Within a SINGLE seed pass, cross-layer LAST-WINS is still honoured: a file
118
81
  written by an earlier layer during this pass IS overwritten by a later
@@ -122,12 +85,14 @@ def apply_template_layers(
122
85
  are skipped (a ``<None>`` / absent layer contributes nothing — e.g.
123
86
  STANDALONE boxes have no workset layer).
124
87
 
125
- This is SEED-ONCE: callers invoke it only when the box still
126
- :func:`needs_seed`. It does NOT special-case or merge any file every file
127
- is a plain ordered copy (the CLAUDE.md merge special-case is gone, D-B5).
128
- The caller is responsible for never re-running it once the box is marked
129
- seeded so user edits made inside the box survive (D-B6); the non-destructive
130
- copy here is an additional safety net for the marker-less re-seed case.
88
+ This is SEED-ONCE: callers invoke it only when the box still needs seeding
89
+ (the launch gate detects an already-seeded box via the per-box registry flag
90
+ OR the existing-inbox backstop). It does NOT special-case or merge any file
91
+ every file is a plain ordered copy (the CLAUDE.md merge special-case is
92
+ gone, D-B5). The caller is responsible for never re-running it once the box
93
+ is recorded seeded so user edits made inside the box survive (D-B6); the
94
+ non-destructive copy here is an additional safety net against a re-seed
95
+ caused by mis-detection.
131
96
  """
132
97
  written: set[Path] = set()
133
98
  for layer in layers:
@@ -189,6 +154,11 @@ def _copy_resource_tree_if_absent(src: Path, dest: Path) -> None:
189
154
  shutil.copy2(str(entry), str(target))
190
155
 
191
156
 
157
+ # Public alias so other modules (e.g. the seed-once apply in commands.start)
158
+ # can reuse the create-if-absent tree copy without reaching for a private name.
159
+ copy_resource_tree_if_absent = _copy_resource_tree_if_absent
160
+
161
+
192
162
  def _packaged_base_template() -> Path | None:
193
163
  """Locate the packaged base-template content (``kanibako.data/templates/base``)."""
194
164
  try:
@@ -106,6 +106,7 @@ class WorksetProject:
106
106
 
107
107
  name: str
108
108
  source_path: Path # original project path (for reference / cloning)
109
+ seeded: bool = False # True once the box's one-time seed has completed
109
110
 
110
111
 
111
112
  @dataclass
@@ -174,7 +175,11 @@ def _write_workset_toml(ws: Workset) -> None:
174
175
  "created": ws.created,
175
176
  "group_auth": ws.group_auth,
176
177
  "projects": [
177
- {"name": proj.name, "source_path": str(proj.source_path)}
178
+ {
179
+ "name": proj.name,
180
+ "source_path": str(proj.source_path),
181
+ "seeded": proj.seeded,
182
+ }
178
183
  for proj in ws.projects
179
184
  ],
180
185
  }
@@ -204,6 +209,7 @@ def _load_workset_toml(root: Path) -> Workset:
204
209
  WorksetProject(
205
210
  name=entry["name"],
206
211
  source_path=Path(entry["source_path"]),
212
+ seeded=bool(entry.get("seeded", False)),
207
213
  )
208
214
  )
209
215
  return Workset(name=name, root=root, created=created, projects=projects, group_auth=group_auth)
@@ -673,6 +679,23 @@ def _detach_project(ws: Workset, name: str) -> None:
673
679
  ws.projects[:] = [p for p in ws.projects if p.name != name]
674
680
 
675
681
 
682
+ def set_project_seeded(ws: Workset, name: str) -> bool:
683
+ """Mark the project *name* in *ws* as seeded and persist the change.
684
+
685
+ Sets ``WorksetProject.seeded`` True for the matching project and rewrites
686
+ the workset identity (preserving cascade settings). Idempotent; a no-op
687
+ that returns False when *name* is not a project in *ws*, True otherwise.
688
+ This is the public persist entry point used by :mod:`kanibako.box_seed` so
689
+ callers do not reach for the module-private ``_write_workset_toml``.
690
+ """
691
+ for proj in ws.projects:
692
+ if proj.name == name:
693
+ proj.seeded = True
694
+ _write_workset_toml(ws)
695
+ return True
696
+ return False
697
+
698
+
676
699
  def remove_project(
677
700
  ws: Workset, name: str, *, remove_files: bool = False,
678
701
  std: StandardPaths | None = None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kanibako-cli
3
- Version: 1.6.0.dev30
3
+ Version: 1.6.0.dev31
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
@@ -9,6 +9,7 @@ src/kanibako/auth_browser.py
9
9
  src/kanibako/auth_parser.py
10
10
  src/kanibako/baseline.py
11
11
  src/kanibako/box_identity.py
12
+ src/kanibako/box_seed.py
12
13
  src/kanibako/browser_sidecar.py
13
14
  src/kanibako/browser_state.py
14
15
  src/kanibako/bun_sea.py
@@ -110,6 +111,7 @@ tests/test_auth_browser.py
110
111
  tests/test_auth_parser.py
111
112
  tests/test_baseline.py
112
113
  tests/test_box_identity.py
114
+ tests/test_box_seed.py
113
115
  tests/test_browser_sidecar.py
114
116
  tests/test_browser_state.py
115
117
  tests/test_bun_sea.py
@@ -0,0 +1,137 @@
1
+ """Tests for kanibako.box_seed -- the uniform per-box seed-once dispatcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from kanibako import box_seed, registry_store
8
+ from kanibako.paths import BoxMode, ProjectGroup, ProjectPaths
9
+ from kanibako.workset import add_project, create_workset, load_workset
10
+
11
+
12
+ def _make_proj(mode: BoxMode, name: str, group: ProjectGroup | None) -> ProjectPaths:
13
+ """Build a minimal ProjectPaths carrying only the dispatch-relevant fields."""
14
+ dummy = Path("/nonexistent")
15
+ return ProjectPaths(
16
+ project_path=dummy,
17
+ project_hash="0" * 16,
18
+ metadata_path=dummy,
19
+ shell_path=dummy,
20
+ vault_ro_path=dummy,
21
+ vault_rw_path=dummy,
22
+ mode=mode,
23
+ name=name,
24
+ group=group,
25
+ )
26
+
27
+
28
+ def _primary_group(std) -> ProjectGroup:
29
+ return ProjectGroup(
30
+ name="default",
31
+ root=std.data_path,
32
+ is_default=True,
33
+ local_shared_base=std.data_path,
34
+ )
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # PRIMARY → registry seeded.projects
39
+ # ---------------------------------------------------------------------------
40
+
41
+
42
+ def test_primary_routes_to_projects_domain(std, tmp_home):
43
+ proj = _make_proj(BoxMode.primary, "myapp", _primary_group(std))
44
+
45
+ assert box_seed.box_is_seeded(proj, std) is False
46
+ box_seed.mark_box_seeded(proj, std)
47
+ assert box_seed.box_is_seeded(proj, std) is True
48
+
49
+ # Landed in projects domain ONLY.
50
+ assert registry_store.is_box_seeded(std.data_path, "projects", "myapp") is True
51
+ assert registry_store.is_box_seeded(std.data_path, "standalone", "myapp") is False
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # STANDALONE → registry seeded.standalone
56
+ # ---------------------------------------------------------------------------
57
+
58
+
59
+ def test_standalone_routes_to_standalone_domain(std, tmp_home):
60
+ proj = _make_proj(BoxMode.standalone, "abc_box", None)
61
+
62
+ assert box_seed.box_is_seeded(proj, std) is False
63
+ box_seed.mark_box_seeded(proj, std)
64
+ assert box_seed.box_is_seeded(proj, std) is True
65
+
66
+ # Landed in standalone domain ONLY.
67
+ assert registry_store.is_box_seeded(std.data_path, "standalone", "abc_box") is True
68
+ assert registry_store.is_box_seeded(std.data_path, "projects", "abc_box") is False
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # NAMED → WorksetProject.seeded
73
+ # ---------------------------------------------------------------------------
74
+
75
+
76
+ def test_named_routes_to_workset_project(std, tmp_home):
77
+ ws = create_workset("myset", tmp_home / "worksets" / "myset", std)
78
+ add_project(ws, "projname", tmp_home / "project")
79
+
80
+ group = ProjectGroup(
81
+ name=ws.name,
82
+ root=ws.root,
83
+ is_default=False,
84
+ local_shared_base=ws.root,
85
+ )
86
+ proj = _make_proj(BoxMode.named, "projname", group)
87
+
88
+ assert box_seed.box_is_seeded(proj, std) is False
89
+ box_seed.mark_box_seeded(proj, std)
90
+ assert box_seed.box_is_seeded(proj, std) is True
91
+
92
+ # Landed on the WorksetProject, NOT in either registry domain.
93
+ assert load_workset(ws.root).projects[0].seeded is True
94
+ assert registry_store.is_box_seeded(std.data_path, "projects", "projname") is False
95
+ assert (
96
+ registry_store.is_box_seeded(std.data_path, "standalone", "projname") is False
97
+ )
98
+
99
+
100
+ def test_named_missing_group_is_defensive(std, tmp_home):
101
+ """A named box with no group reads unseeded and mark is a no-op."""
102
+ proj = _make_proj(BoxMode.named, "orphan", None)
103
+ assert box_seed.box_is_seeded(proj, std) is False
104
+ box_seed.mark_box_seeded(proj, std) # must not raise
105
+ assert box_seed.box_is_seeded(proj, std) is False
106
+
107
+
108
+ def test_named_unloadable_workset_is_defensive(std, tmp_home):
109
+ """A named box whose workset root has no settings.yaml reads unseeded.
110
+
111
+ The seed gate calls ``box_is_seeded``/``mark_box_seeded`` early in launch; a
112
+ named box whose workset cannot be loaded (e.g. a not-yet-materialised root)
113
+ must NOT raise — it reads as unseeded and the mark is a no-op.
114
+ """
115
+ bare_root = tmp_home / "no-such-workset"
116
+ bare_root.mkdir()
117
+ group = ProjectGroup(
118
+ name="ghostset", root=bare_root, is_default=False, local_shared_base=bare_root,
119
+ )
120
+ proj = _make_proj(BoxMode.named, "projname", group)
121
+
122
+ assert box_seed.box_is_seeded(proj, std) is False # must not raise
123
+ box_seed.mark_box_seeded(proj, std) # must not raise
124
+ assert box_seed.box_is_seeded(proj, std) is False
125
+
126
+
127
+ def test_marking_one_mode_leaves_others_empty(std, tmp_home):
128
+ """Marking the primary box does not touch the standalone domain or worksets."""
129
+ ws = create_workset("myset", tmp_home / "worksets" / "myset", std)
130
+ add_project(ws, "shared", tmp_home / "project")
131
+
132
+ primary = _make_proj(BoxMode.primary, "shared", _primary_group(std))
133
+ box_seed.mark_box_seeded(primary, std)
134
+
135
+ # Standalone domain untouched; the same-named workset project unseeded.
136
+ assert registry_store.is_box_seeded(std.data_path, "standalone", "shared") is False
137
+ assert load_workset(ws.root).projects[0].seeded is False