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.
- {kanibako_cli-1.5.1/src/kanibako_cli.egg-info → kanibako_cli-1.6.0.dev24}/PKG-INFO +1 -1
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/README.md +2 -2
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/pyproject.toml +19 -2
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/__init__.py +1 -1
- kanibako_cli-1.6.0.dev24/src/kanibako/agent_config.py +86 -0
- kanibako_cli-1.6.0.dev24/src/kanibako/box_identity.py +151 -0
- kanibako_cli-1.6.0.dev24/src/kanibako/channels.py +253 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/cli.py +50 -35
- 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
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/archive.py +3 -3
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/box/_duplicate.py +42 -16
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/box/_lifecycle.py +330 -116
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/box/_parser.py +74 -35
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/clean.py +78 -13
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/diagnose.py +12 -7
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/fork_cmd.py +5 -2
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/helper_cmd.py +15 -5
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/image.py +2 -2
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/install.py +30 -24
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/refresh_credentials.py +1 -1
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/start.py +949 -293
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/system_cmd.py +5 -2
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/workset_cmd.py +20 -19
- kanibako_cli-1.6.0.dev24/src/kanibako/config.py +920 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/config_interface.py +301 -59
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/config_io.py +2 -2
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/container.py +28 -19
- kanibako_cli-1.6.0.dev24/src/kanibako/data/templates/base/INSTRUCTIONS.md +67 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/helper_listener.py +7 -4
- kanibako_cli-1.6.0.dev24/src/kanibako/import_reconcile.py +314 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/names.py +28 -26
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/paths.py +621 -401
- kanibako_cli-1.6.0.dev24/src/kanibako/registry_store.py +226 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/rig_registry.py +26 -28
- kanibako_cli-1.6.0.dev24/src/kanibako/scripts/__init__.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/scripts/helper-init.sh +1 -1
- kanibako_cli-1.6.0.dev24/src/kanibako/settings_categories.py +508 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/settings_resolve.py +106 -8
- kanibako_cli-1.6.0.dev24/src/kanibako/settings_seeds.py +81 -0
- kanibako_cli-1.6.0.dev24/src/kanibako/settings_shares.py +77 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/shells.py +17 -12
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/snapshots.py +60 -23
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/targets/__init__.py +5 -3
- kanibako_cli-1.6.0.dev24/src/kanibako/targets/assembly.py +331 -0
- kanibako_cli-1.6.0.dev24/src/kanibako/targets/base.py +497 -0
- kanibako_cli-1.6.0.dev24/src/kanibako/targets/credsync.py +172 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/targets/no_agent.py +4 -7
- kanibako_cli-1.6.0.dev24/src/kanibako/templates.py +176 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/utils.py +2 -2
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/workset.py +44 -44
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24/src/kanibako_cli.egg-info}/PKG-INFO +1 -1
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako_cli.egg-info/SOURCES.txt +18 -5
- kanibako_cli-1.5.1/tests/test_crabs.py → kanibako_cli-1.6.0.dev24/tests/test_agent_config.py +67 -83
- kanibako_cli-1.6.0.dev24/tests/test_box_identity.py +226 -0
- kanibako_cli-1.6.0.dev24/tests/test_channels.py +213 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_cli.py +68 -80
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_cli_exitcodes.py +13 -12
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_comms.py +17 -55
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_config.py +246 -126
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_config_interface.py +255 -37
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_container.py +37 -15
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_container_extended.py +54 -14
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_credentials.py +2 -42
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_env_cmd.py +4 -4
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_error_messages.py +3 -3
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_fork.py +21 -27
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_helper_cmd.py +4 -4
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_helpers.py +15 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_image_sharing.py +1 -1
- kanibako_cli-1.6.0.dev24/tests/test_import.py +299 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_init_cmd.py +15 -15
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_install_integration.py +3 -29
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_lifecycle_integration.py +2 -2
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_names.py +5 -5
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_paths.py +125 -575
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_paths_extended.py +4 -4
- kanibako_cli-1.6.0.dev24/tests/test_registry_store.py +162 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_resource_scoping.py +110 -101
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_rig_registry.py +49 -33
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_safety_invariants.py +86 -26
- kanibako_cli-1.6.0.dev24/tests/test_settings_categories.py +640 -0
- kanibako_cli-1.6.0.dev24/tests/test_settings_loader.py +229 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_settings_resolve.py +14 -14
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_settings_seeds.py +53 -53
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_settings_shares.py +60 -60
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_shells.py +5 -3
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_snapshots.py +101 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_standalone_paths.py +93 -30
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_status.py +4 -4
- kanibako_cli-1.6.0.dev24/tests/test_system_paths.py +492 -0
- kanibako_cli-1.6.0.dev24/tests/test_templates.py +249 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_tweakcc.py +16 -16
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_utils.py +2 -2
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_workset.py +17 -1
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_workset_paths.py +20 -23
- kanibako_cli-1.5.1/src/kanibako/config.py +0 -584
- kanibako_cli-1.5.1/src/kanibako/crabs.py +0 -89
- kanibako_cli-1.5.1/src/kanibako/instructions.py +0 -160
- kanibako_cli-1.5.1/src/kanibako/settings_seeds.py +0 -154
- kanibako_cli-1.5.1/src/kanibako/settings_shares.py +0 -154
- kanibako_cli-1.5.1/src/kanibako/targets/base.py +0 -321
- kanibako_cli-1.5.1/src/kanibako/templates.py +0 -60
- kanibako_cli-1.5.1/tests/test_instructions.py +0 -542
- kanibako_cli-1.5.1/tests/test_system_paths.py +0 -157
- kanibako_cli-1.5.1/tests/test_templates.py +0 -172
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/LICENSE.md +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/setup.cfg +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/__main__.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/_atomic.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/auth_browser.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/auth_parser.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/baseline.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/browser_sidecar.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/browser_state.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/bun_sea.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/__init__.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/baseline_cmd.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/box/__init__.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/restore.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/setup_cmd.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/stop.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/upgrade.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/commands/vault_cmd.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containerfiles.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/Containerfile.template-android +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/Containerfile.template-dotnet +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/Containerfile.template-js +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/Containerfile.template-jvm +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/Containerfile.template-systems +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/__init__.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/containers/tmux.conf +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/data/__init__.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/data/image-baseline.yaml +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/errors.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/freshness.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/git.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/helper_client.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/helpers.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/hygiene.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/image_sharing.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/log.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/plugins/__init__.py +0 -0
- /kanibako_cli-1.5.1/src/kanibako/scripts/__init__.py → /kanibako_cli-1.6.0.dev24/src/kanibako/py.typed +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/registry.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/rig_bundle.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/rig_meta.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/rig_resolve.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/rig_source.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/scripts/kanibako-entry +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/shellenv.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/templates_image.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/tweakcc.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako/tweakcc_cache.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako_cli.egg-info/dependency_links.txt +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako_cli.egg-info/entry_points.txt +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako_cli.egg-info/requires.txt +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/src/kanibako_cli.egg-info/top_level.txt +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_archive_restore_integration.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_atomic.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_auth_browser.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_auth_parser.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_baseline.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_browser_sidecar.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_browser_state.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_bun_sea.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_container_integration.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_containerfiles.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_freshness.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_git.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_helper_client.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_helper_listener.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_hygiene.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_log.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_registry.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_rig_bundle.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_rig_meta.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_rig_resolve.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_rig_source.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_shellenv.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_start_integration.py +0 -0
- {kanibako_cli-1.5.1 → kanibako_cli-1.6.0.dev24}/tests/test_templates_image.py +0 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
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" = [
|
|
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
|
|
@@ -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
|
+
)
|