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