aicage 0.5.12__tar.gz → 0.7.0__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 (111) hide show
  1. {aicage-0.5.12 → aicage-0.7.0}/PKG-INFO +4 -3
  2. {aicage-0.5.12 → aicage-0.7.0}/README.md +3 -2
  3. {aicage-0.5.12 → aicage-0.7.0}/config/agent-build/Dockerfile +3 -2
  4. {aicage-0.5.12 → aicage-0.7.0}/config/agent-build/agents/claude/agent.yaml +2 -1
  5. aicage-0.7.0/config/agent-build/agents/claude/install.sh +18 -0
  6. aicage-0.7.0/config/agent-build/agents/claude/version.sh +18 -0
  7. aicage-0.7.0/config/agent-build/agents/droid/agent.yaml +5 -0
  8. {aicage-0.5.12 → aicage-0.7.0}/config/config.yaml +1 -0
  9. aicage-0.7.0/config/extension-build/Dockerfile +12 -0
  10. {aicage-0.5.12 → aicage-0.7.0}/config/images-metadata.yaml +26 -24
  11. aicage-0.7.0/config/validation/agent.schema.json +38 -0
  12. {aicage-0.5.12 → aicage-0.7.0}/pyproject.toml +6 -0
  13. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/_version.py +2 -2
  14. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/cli/_parse.py +11 -5
  15. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/cli/entrypoint.py +8 -4
  16. aicage-0.7.0/src/aicage/config/__init__.py +4 -0
  17. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/config/config_store.py +3 -2
  18. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/config/context.py +2 -4
  19. aicage-0.7.0/src/aicage/config/global_config.py +67 -0
  20. aicage-0.7.0/src/aicage/config/project_config.py +116 -0
  21. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/config/runtime_config.py +13 -18
  22. aicage-0.7.0/src/aicage/paths.py +25 -0
  23. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/_agent_discovery.py +2 -1
  24. aicage-0.7.0/src/aicage/registry/_extended_images.py +128 -0
  25. aicage-0.7.0/src/aicage/registry/_extensions.py +115 -0
  26. aicage-0.7.0/src/aicage/registry/_hashing.py +16 -0
  27. aicage-0.7.0/src/aicage/registry/_image_refs.py +6 -0
  28. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/_pull_decision.py +5 -5
  29. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/_remote_api.py +1 -1
  30. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/_remote_query.py +12 -4
  31. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/agent_version/__init__.py +1 -1
  32. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/agent_version/checker.py +1 -1
  33. aicage-0.5.12/src/aicage/registry/agent_version/_store.py → aicage-0.7.0/src/aicage/registry/agent_version/store.py +9 -6
  34. aicage-0.7.0/src/aicage/registry/custom_agent/_validation.py +107 -0
  35. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/custom_agent/loader.py +36 -31
  36. aicage-0.7.0/src/aicage/registry/image_selection/__init__.py +2 -0
  37. aicage-0.7.0/src/aicage/registry/image_selection/extensions/context.py +16 -0
  38. aicage-0.7.0/src/aicage/registry/image_selection/extensions/extended_images.py +65 -0
  39. aicage-0.7.0/src/aicage/registry/image_selection/extensions/handler.py +74 -0
  40. aicage-0.7.0/src/aicage/registry/image_selection/extensions/missing_extensions.py +70 -0
  41. aicage-0.7.0/src/aicage/registry/image_selection/extensions/refs.py +14 -0
  42. aicage-0.7.0/src/aicage/registry/image_selection/models.py +9 -0
  43. aicage-0.7.0/src/aicage/registry/image_selection/selection.py +149 -0
  44. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/images_metadata/loader.py +4 -3
  45. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/images_metadata/models.py +96 -49
  46. aicage-0.7.0/src/aicage/registry/local_build/__init__.py +0 -0
  47. aicage-0.7.0/src/aicage/registry/local_build/_extended_plan.py +45 -0
  48. aicage-0.7.0/src/aicage/registry/local_build/_extended_runner.py +90 -0
  49. aicage-0.7.0/src/aicage/registry/local_build/_extended_store.py +76 -0
  50. aicage-0.7.0/src/aicage/registry/local_build/_layers.py +24 -0
  51. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/local_build/_logs.py +5 -0
  52. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/local_build/_plan.py +22 -4
  53. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/local_build/_runner.py +15 -2
  54. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/local_build/_store.py +22 -20
  55. aicage-0.7.0/src/aicage/registry/local_build/ensure_extended_image.py +94 -0
  56. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/local_build/ensure_local_image.py +4 -7
  57. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/mounts/_docker_socket.py +2 -2
  58. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/mounts/_entrypoint.py +2 -2
  59. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/mounts/_git_config.py +2 -4
  60. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/mounts/_gpg.py +2 -4
  61. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/mounts/_ssh_keys.py +2 -4
  62. aicage-0.7.0/src/aicage/runtime/prompts/__init__.py +20 -0
  63. aicage-0.7.0/src/aicage/runtime/prompts/_tty.py +8 -0
  64. aicage-0.5.12/src/aicage/runtime/prompts.py → aicage-0.7.0/src/aicage/runtime/prompts/base.py +12 -28
  65. aicage-0.7.0/src/aicage/runtime/prompts/confirm.py +48 -0
  66. aicage-0.7.0/src/aicage/runtime/prompts/extensions.py +46 -0
  67. aicage-0.7.0/src/aicage/runtime/prompts/image_choice.py +115 -0
  68. aicage-0.7.0/src/aicage/runtime/prompts/image_ref.py +11 -0
  69. aicage-0.7.0/src/aicage/runtime/prompts/missing_extensions.py +24 -0
  70. aicage-0.5.12/config/agent-build/agents/claude/install.sh +0 -4
  71. aicage-0.5.12/config/agent-build/agents/claude/version.sh +0 -4
  72. aicage-0.5.12/config/agent-build/agents/droid/agent.yaml +0 -4
  73. aicage-0.5.12/src/aicage/config/__init__.py +0 -8
  74. aicage-0.5.12/src/aicage/config/global_config.py +0 -53
  75. aicage-0.5.12/src/aicage/config/project_config.py +0 -87
  76. aicage-0.5.12/src/aicage/registry/_agent_definition.py +0 -28
  77. aicage-0.5.12/src/aicage/registry/custom_agent/_validation.py +0 -44
  78. aicage-0.5.12/src/aicage/registry/image_selection.py +0 -66
  79. {aicage-0.5.12 → aicage-0.7.0}/.gitignore +0 -0
  80. {aicage-0.5.12 → aicage-0.7.0}/LICENSE +0 -0
  81. {aicage-0.5.12 → aicage-0.7.0}/config/agent-build/agents/droid/install.sh +0 -0
  82. {aicage-0.5.12 → aicage-0.7.0}/config/agent-build/agents/droid/version.sh +0 -0
  83. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/__init__.py +0 -0
  84. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/__main__.py +0 -0
  85. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/_logging.py +0 -0
  86. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/cli/__init__.py +0 -0
  87. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/cli/_print_config.py +0 -0
  88. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/cli_types.py +0 -0
  89. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/config/_file_locking.py +0 -0
  90. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/config/errors.py +0 -0
  91. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/config/resources.py +0 -0
  92. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/docker_client.py +0 -0
  93. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/errors.py +0 -0
  94. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/__init__.py +0 -0
  95. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/_local_query.py +0 -0
  96. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/_logs.py +0 -0
  97. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/_pull_runner.py +0 -0
  98. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/custom_agent/__init__.py +0 -0
  99. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/image_pull.py +0 -0
  100. {aicage-0.5.12/src/aicage/registry/images_metadata → aicage-0.7.0/src/aicage/registry/image_selection/extensions}/__init__.py +0 -0
  101. {aicage-0.5.12/src/aicage/registry/local_build → aicage-0.7.0/src/aicage/registry/images_metadata}/__init__.py +0 -0
  102. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/registry/local_build/_digest.py +0 -0
  103. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/__init__.py +0 -0
  104. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/_env_vars.py +0 -0
  105. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/agent_config.py +0 -0
  106. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/mounts/__init__.py +0 -0
  107. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/mounts/_exec.py +0 -0
  108. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/mounts/_signing.py +0 -0
  109. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/mounts/resolver.py +0 -0
  110. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/run_args.py +0 -0
  111. {aicage-0.5.12 → aicage-0.7.0}/src/aicage/runtime/run_plan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aicage
3
- Version: 0.5.12
3
+ Version: 0.7.0
4
4
  Summary: Runs agentic coding assistants in docker containers
5
5
  Author: Stefan Kuhn
6
6
  License: Apache License
@@ -278,11 +278,12 @@ preferences and credentials.
278
278
  ## aicage options
279
279
 
280
280
  - `--dry-run` prints the composed `docker run` command without executing it.
281
- - `--entrypoint PATH` mounts a custom entrypoint script to `/usr/local/bin/entrypoint.sh`.
281
+ - `--aicage-entrypoint PATH` mounts a custom entrypoint script to `/usr/local/bin/entrypoint.sh`.
282
282
  - `--docker` mounts `/run/docker.sock` into the container to enable Docker-in-Docker workflows.
283
283
  - `--config print` prints the project config path and its contents.
284
284
 
285
- Configuration file formats are documented in [CONFIG.md](CONFIG.md).
285
+ Configuration file formats are documented in [CONFIG.md](CONFIG.md). Extension authoring is documented in
286
+ [doc/extensions.md](doc/extensions.md).
286
287
 
287
288
  ## Why cage agents?
288
289
 
@@ -65,11 +65,12 @@ preferences and credentials.
65
65
  ## aicage options
66
66
 
67
67
  - `--dry-run` prints the composed `docker run` command without executing it.
68
- - `--entrypoint PATH` mounts a custom entrypoint script to `/usr/local/bin/entrypoint.sh`.
68
+ - `--aicage-entrypoint PATH` mounts a custom entrypoint script to `/usr/local/bin/entrypoint.sh`.
69
69
  - `--docker` mounts `/run/docker.sock` into the container to enable Docker-in-Docker workflows.
70
70
  - `--config print` prints the project config path and its contents.
71
71
 
72
- Configuration file formats are documented in [CONFIG.md](CONFIG.md).
72
+ Configuration file formats are documented in [CONFIG.md](CONFIG.md). Extension authoring is documented in
73
+ [doc/extensions.md](doc/extensions.md).
73
74
 
74
75
  ## Why cage agents?
75
76
 
@@ -1,4 +1,3 @@
1
- # syntax=docker/dockerfile:1.7-labs
2
1
  ARG BASE_IMAGE=base
3
2
  ARG AGENT=codex
4
3
 
@@ -18,4 +17,6 @@ RUN --mount=type=bind,source=agents/,target=/tmp/agents,readonly \
18
17
  /tmp/agents/${AGENT}/install.sh
19
18
 
20
19
  ENV AGENT=${AGENT}
21
- CMD ["sh", "-c", "$AGENT"]
20
+
21
+ # entrypoint.sh uses this variable
22
+ ENV AICAGE_ENTRYPOINT_CMD=${AGENT}
@@ -1,4 +1,5 @@
1
1
  agent_path: ~/.claude
2
2
  agent_full_name: Claude Code
3
3
  agent_homepage: https://claude.com/product/claude-code
4
- redistributable: false
4
+ # Build locally because the license does not allow redistributing prebuilt images.
5
+ build_local: true
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Install Claude using the official installer.
5
+ curl -fsSL https://claude.ai/install.sh | bash
6
+
7
+ # Ensure the binary is on the global PATH for the runtime user.
8
+ if [[ -x "/root/.local/bin/claude" ]]; then
9
+ install -m 0755 /root/.local/bin/claude /usr/local/bin/claude
10
+ elif command -v claude >/dev/null 2>&1; then
11
+ # Fallback: copy whatever the installer placed on PATH.
12
+ install -m 0755 "$(command -v claude)" /usr/local/bin/claude
13
+ fi
14
+
15
+ if ! command -v claude >/dev/null 2>&1; then
16
+ echo "[install_claude] 'claude' executable not found after installation." >&2
17
+ exit 1
18
+ fi
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Use npm to determine version despite installing with non-rpm
5
+ # Reason: Claude infrastructure reports wrong/old version
6
+ #
7
+ # The official installer way leads to this url:
8
+ # https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/stable
9
+ # but there this version is reported:
10
+ # 2.0.67
11
+ # while actually this version is currently correct:
12
+ # 2.0.76
13
+ #
14
+ # See bug: https://github.com/anthropics/claude-code/issues/13888
15
+ #
16
+ # The official installer is preferred as only then does Claude support syntax-highlighting
17
+
18
+ npm view @anthropic-ai/claude-code version
@@ -0,0 +1,5 @@
1
+ agent_path: ~/.factory
2
+ agent_full_name: Factory CLI
3
+ agent_homepage: https://factory.ai/product/cli
4
+ # Build locally because the license does not allow redistributing prebuilt images.
5
+ build_local: true
@@ -5,3 +5,4 @@ image_repository: aicage/aicage
5
5
  image_base_repository: aicage/aicage-image-base
6
6
  default_image_base: ubuntu
7
7
  version_check_image: ghcr.io/aicage/aicage-image-util:agent-version
8
+ local_image_repository: aicage
@@ -0,0 +1,12 @@
1
+ ARG BASE_IMAGE=base
2
+ FROM ${BASE_IMAGE} AS runtime
3
+
4
+ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
5
+
6
+ RUN --mount=type=bind,source=scripts,target=/tmp/aicage/scripts,readonly \
7
+ set -e; \
8
+ shopt -s nullglob; \
9
+ mkdir -p /tmp/aicage/scripts-run; \
10
+ for script in /tmp/aicage/scripts/*.sh; do cp "$script" /tmp/aicage/scripts-run/; done; \
11
+ for script in /tmp/aicage/scripts-run/*.sh; do chmod +x "$script"; "$script"; done; \
12
+ rm -rf /tmp/aicage/scripts-run
@@ -1,7 +1,7 @@
1
1
  aicage-image:
2
- version: 0.5.10
2
+ version: 0.7.0
3
3
  aicage-image-base:
4
- version: 0.5.7
4
+ version: 0.7.0
5
5
  bases:
6
6
  act:
7
7
  root_image: ghcr.io/catthehacker/ubuntu:act-latest
@@ -50,20 +50,21 @@ agent:
50
50
  agent_path: ~/.claude
51
51
  agent_full_name: Claude Code
52
52
  agent_homepage: https://claude.com/product/claude-code
53
- redistributable: false
53
+ # Build locally because the license does not allow redistributing prebuilt images.
54
+ build_local: true
54
55
  valid_bases:
55
- act: ghcr.io/aicage/aicage:claude-act
56
- alpine: ghcr.io/aicage/aicage:claude-alpine
57
- debian: ghcr.io/aicage/aicage:claude-debian
58
- fedora: ghcr.io/aicage/aicage:claude-fedora
59
- minimal: ghcr.io/aicage/aicage:claude-minimal
60
- node: ghcr.io/aicage/aicage:claude-node
61
- ubuntu: ghcr.io/aicage/aicage:claude-ubuntu
56
+ act: aicage:claude-act
57
+ alpine: aicage:claude-alpine
58
+ debian: aicage:claude-debian
59
+ fedora: aicage:claude-fedora
60
+ minimal: aicage:claude-minimal
61
+ node: aicage:claude-node
62
+ ubuntu: aicage:claude-ubuntu
62
63
  codex:
63
64
  agent_path: ~/.codex
64
65
  agent_full_name: Codex CLI
65
66
  agent_homepage: https://developers.openai.com/codex/cli
66
- redistributable: true
67
+ build_local: false
67
68
  valid_bases:
68
69
  act: ghcr.io/aicage/aicage:codex-act
69
70
  alpine: ghcr.io/aicage/aicage:codex-alpine
@@ -76,7 +77,7 @@ agent:
76
77
  agent_path: ~/.copilot
77
78
  agent_full_name: GitHub Copilot CLI
78
79
  agent_homepage: https://github.com/features/copilot/cli
79
- redistributable: true
80
+ build_local: false
80
81
  base_exclude:
81
82
  - alpine
82
83
  - minimal
@@ -92,7 +93,7 @@ agent:
92
93
  agent_path: ~/.local/share/crush
93
94
  agent_full_name: Crush
94
95
  agent_homepage: https://github.com/charmbracelet/crush
95
- redistributable: true
96
+ build_local: false
96
97
  valid_bases:
97
98
  act: ghcr.io/aicage/aicage:crush-act
98
99
  alpine: ghcr.io/aicage/aicage:crush-alpine
@@ -105,20 +106,21 @@ agent:
105
106
  agent_path: ~/.factory
106
107
  agent_full_name: Factory CLI
107
108
  agent_homepage: https://factory.ai/product/cli
108
- redistributable: false
109
+ # Build locally because the license does not allow redistributing prebuilt images.
110
+ build_local: true
109
111
  valid_bases:
110
- act: ghcr.io/aicage/aicage:droid-act
111
- alpine: ghcr.io/aicage/aicage:droid-alpine
112
- debian: ghcr.io/aicage/aicage:droid-debian
113
- fedora: ghcr.io/aicage/aicage:droid-fedora
114
- minimal: ghcr.io/aicage/aicage:droid-minimal
115
- node: ghcr.io/aicage/aicage:droid-node
116
- ubuntu: ghcr.io/aicage/aicage:droid-ubuntu
112
+ act: aicage:droid-act
113
+ alpine: aicage:droid-alpine
114
+ debian: aicage:droid-debian
115
+ fedora: aicage:droid-fedora
116
+ minimal: aicage:droid-minimal
117
+ node: aicage:droid-node
118
+ ubuntu: aicage:droid-ubuntu
117
119
  gemini:
118
120
  agent_path: ~/.gemini
119
121
  agent_full_name: Gemini CLI
120
122
  agent_homepage: https://geminicli.com
121
- redistributable: true
123
+ build_local: false
122
124
  valid_bases:
123
125
  act: ghcr.io/aicage/aicage:gemini-act
124
126
  alpine: ghcr.io/aicage/aicage:gemini-alpine
@@ -131,7 +133,7 @@ agent:
131
133
  agent_path: ~/.config/goose
132
134
  agent_full_name: Goose CLI
133
135
  agent_homepage: https://block.github.io/goose
134
- redistributable: true
136
+ build_local: false
135
137
  valid_bases:
136
138
  act: ghcr.io/aicage/aicage:goose-act
137
139
  alpine: ghcr.io/aicage/aicage:goose-alpine
@@ -144,7 +146,7 @@ agent:
144
146
  agent_path: ~/.qwen
145
147
  agent_full_name: Qwen Code
146
148
  agent_homepage: https://github.com/QwenLM/qwen-code
147
- redistributable: true
149
+ build_local: false
148
150
  valid_bases:
149
151
  act: ghcr.io/aicage/aicage:qwen-act
150
152
  alpine: ghcr.io/aicage/aicage:qwen-alpine
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "AICAGE agent definition",
4
+ "type": "object",
5
+ "required": [
6
+ "agent_path",
7
+ "agent_full_name",
8
+ "agent_homepage"
9
+ ],
10
+ "properties": {
11
+ "agent_path": {
12
+ "type": "string"
13
+ },
14
+ "agent_full_name": {
15
+ "type": "string"
16
+ },
17
+ "agent_homepage": {
18
+ "type": "string"
19
+ },
20
+ "build_local": {
21
+ "type": "boolean",
22
+ "default": true
23
+ },
24
+ "base_exclude": {
25
+ "type": "array",
26
+ "items": {
27
+ "type": "string"
28
+ }
29
+ },
30
+ "base_distro_exclude": {
31
+ "type": "array",
32
+ "items": {
33
+ "type": "string"
34
+ }
35
+ }
36
+ },
37
+ "additionalProperties": false
38
+ }
@@ -26,12 +26,16 @@ packages = ["src/aicage"]
26
26
  "config/config.yaml" = "config/config.yaml"
27
27
  "config/images-metadata.yaml" = "config/images-metadata.yaml"
28
28
  "config/agent-build" = "config/agent-build"
29
+ "config/extension-build" = "config/extension-build"
30
+ "config/validation/agent.schema.json" = "config/validation/agent.schema.json"
29
31
 
30
32
  [tool.hatch.build]
31
33
  include = [
32
34
  "config/config.yaml",
33
35
  "config/images-metadata.yaml",
34
36
  "config/agent-build/**",
37
+ "config/extension-build/**",
38
+ "config/validation/agent.schema.json",
35
39
  ]
36
40
 
37
41
  [tool.hatch.build.targets.sdist]
@@ -40,6 +44,8 @@ include = [
40
44
  "config/config.yaml",
41
45
  "config/images-metadata.yaml",
42
46
  "config/agent-build/**",
47
+ "config/extension-build/**",
48
+ "config/validation/agent.schema.json",
43
49
  "LICENSE",
44
50
  "README.md",
45
51
  "pyproject.toml",
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.5.12'
32
- __version_tuple__ = version_tuple = (0, 5, 12)
31
+ __version__ = version = '0.7.0'
32
+ __version_tuple__ = version_tuple = (0, 7, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -6,7 +6,7 @@ from aicage._logging import get_logger
6
6
  from aicage.cli_types import ParsedArgs
7
7
  from aicage.errors import CliError
8
8
 
9
- MIN_REMAINING_FOR_DOCKER_ARGS = 2
9
+ _MIN_REMAINING_WITH_AGENT = 2
10
10
 
11
11
 
12
12
  def parse_cli(argv: Sequence[str]) -> ParsedArgs:
@@ -16,7 +16,11 @@ def parse_cli(argv: Sequence[str]) -> ParsedArgs:
16
16
  """
17
17
  parser = argparse.ArgumentParser(add_help=False)
18
18
  parser.add_argument("--dry-run", action="store_true", help="Print docker run command without executing.")
19
- parser.add_argument("--entrypoint", help="Override the container entrypoint with a host path.")
19
+ parser.add_argument(
20
+ "--aicage-entrypoint",
21
+ dest="entrypoint",
22
+ help="Override the container entrypoint with a host path.",
23
+ )
20
24
  parser.add_argument("--docker", action="store_true", help="Mount the host Docker socket into the container.")
21
25
  parser.add_argument("--config", help="Perform config actions such as 'print'.")
22
26
  parser.add_argument("-h", "--help", action="store_true", help="Show help message and exit.")
@@ -30,8 +34,8 @@ def parse_cli(argv: Sequence[str]) -> ParsedArgs:
30
34
  usage: str = (
31
35
  "Usage:\n"
32
36
  " aicage <agent>\n"
33
- " aicage [--dry-run] [--docker] [--entrypoint PATH] -- <agent> [<agent-args>]\n"
34
- " aicage [--dry-run] [--docker] [--entrypoint PATH] <docker-args> -- <agent> [<agent-args>]\n"
37
+ " aicage [--dry-run] [--docker] [--aicage-entrypoint PATH] -- <agent> [<agent-args>]\n"
38
+ " aicage [--dry-run] [--docker] [--aicage-entrypoint PATH] <docker-args> -- <agent> [<agent-args>]\n"
35
39
  " aicage --config print\n\n"
36
40
  "Any arguments between aicage and the agent require a '--' separator before the agent.\n"
37
41
  "<docker-args> are any arguments not recognized by aicage.\n"
@@ -102,6 +106,8 @@ def _parse_agent_section(
102
106
  if not remaining:
103
107
  raise CliError("Missing arguments. Provide an agent name (and optional docker args).")
104
108
  first: str = remaining[0]
105
- if len(remaining) >= MIN_REMAINING_FOR_DOCKER_ARGS and (first.startswith("-") or "=" in first):
109
+ if first.startswith("-") or "=" in first:
110
+ if len(remaining) < _MIN_REMAINING_WITH_AGENT:
111
+ raise CliError("Missing agent name after docker args. Use '--' before the agent.")
106
112
  return first, remaining[1], remaining[2:]
107
113
  return "", first, remaining[1:]
@@ -7,9 +7,11 @@ from aicage._logging import get_logger
7
7
  from aicage.cli._parse import parse_cli
8
8
  from aicage.cli._print_config import print_project_config
9
9
  from aicage.cli_types import ParsedArgs
10
- from aicage.config import ConfigError, RunConfig, load_run_config
10
+ from aicage.config import ConfigError
11
+ from aicage.config.runtime_config import RunConfig, load_run_config
11
12
  from aicage.errors import CliError
12
13
  from aicage.registry.image_pull import pull_image
14
+ from aicage.registry.local_build.ensure_extended_image import ensure_extended_image
13
15
  from aicage.registry.local_build.ensure_local_image import ensure_local_image
14
16
  from aicage.runtime.run_args import DockerRunArgs, assemble_docker_run
15
17
  from aicage.runtime.run_plan import build_run_args
@@ -26,10 +28,12 @@ def main(argv: Sequence[str] | None = None) -> int:
26
28
  run_config: RunConfig = load_run_config(parsed.agent, parsed)
27
29
  logger.info("Resolved run config for agent %s", run_config.agent)
28
30
  agent_metadata = run_config.images_metadata.agents[run_config.agent]
29
- if not agent_metadata.redistributable and not agent_metadata.is_custom:
30
- ensure_local_image(run_config)
31
- else:
31
+ if run_config.extensions:
32
+ ensure_extended_image(run_config)
33
+ elif agent_metadata.local_definition_dir is None:
32
34
  pull_image(run_config)
35
+ else:
36
+ ensure_local_image(run_config)
33
37
  run_args: DockerRunArgs = build_run_args(config=run_config, parsed=parsed)
34
38
 
35
39
  run_cmd: list[str] = assemble_docker_run(run_args)
@@ -0,0 +1,4 @@
1
+ from .config_store import SettingsStore as SettingsStore
2
+ from .errors import ConfigError as ConfigError
3
+ from .global_config import GlobalConfig as GlobalConfig
4
+ from .project_config import ProjectConfig as ProjectConfig
@@ -7,12 +7,13 @@ from typing import Any
7
7
 
8
8
  import yaml
9
9
 
10
+ from aicage.paths import CONFIG_FILENAME
11
+
10
12
  from .errors import ConfigError
11
13
  from .global_config import GlobalConfig
12
14
  from .project_config import ProjectConfig
13
15
  from .resources import find_packaged_path
14
16
 
15
- _CONFIG_FILENAME = "config.yaml"
16
17
  _DEFAULT_BASE_DIR = "~/.aicage"
17
18
  _PROJECTS_SUBDIR = "projects"
18
19
 
@@ -66,7 +67,7 @@ class SettingsStore:
66
67
  """
67
68
  Returns the path to the packaged global config file.
68
69
  """
69
- return find_packaged_path(_CONFIG_FILENAME)
70
+ return find_packaged_path(CONFIG_FILENAME)
70
71
 
71
72
  def project_config_path(self, project_realpath: Path) -> Path:
72
73
  """
@@ -20,11 +20,11 @@ class ConfigContext:
20
20
  return f"{self.global_cfg.image_registry}/{self.global_cfg.image_repository}"
21
21
 
22
22
 
23
- def build_config_context() -> ConfigContext:
23
+ def _build_config_context() -> ConfigContext:
24
24
  store = SettingsStore()
25
25
  project_path = Path.cwd().resolve()
26
26
  global_cfg = store.load_global()
27
- images_metadata = load_images_metadata()
27
+ images_metadata = load_images_metadata(global_cfg.local_image_repository)
28
28
  project_cfg = store.load_project(project_path)
29
29
  return ConfigContext(
30
30
  store=store,
@@ -32,5 +32,3 @@ def build_config_context() -> ConfigContext:
32
32
  global_cfg=global_cfg,
33
33
  images_metadata=images_metadata,
34
34
  )
35
-
36
-
@@ -0,0 +1,67 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any
3
+
4
+ from .errors import ConfigError
5
+
6
+ _IMAGE_REGISTRY_KEY: str = "image_registry"
7
+ _IMAGE_REGISTRY_API_URL_KEY: str = "image_registry_api_url"
8
+ _IMAGE_REGISTRY_API_TOKEN_URL_KEY: str = "image_registry_api_token_url"
9
+ _IMAGE_REPOSITORY_KEY: str = "image_repository"
10
+ _IMAGE_BASE_REPOSITORY_KEY: str = "image_base_repository"
11
+ _DEFAULT_IMAGE_BASE_KEY: str = "default_image_base"
12
+ _VERSION_CHECK_IMAGE_KEY: str = "version_check_image"
13
+ _LOCAL_IMAGE_REPOSITORY_KEY: str = "local_image_repository"
14
+ _GLOBAL_AGENTS_KEY: str = "agents"
15
+
16
+
17
+ @dataclass
18
+ class GlobalConfig:
19
+ image_registry: str
20
+ image_registry_api_url: str
21
+ image_registry_api_token_url: str
22
+ image_repository: str
23
+ image_base_repository: str
24
+ default_image_base: str
25
+ version_check_image: str
26
+ local_image_repository: str
27
+ agents: dict[str, dict[str, Any]] = field(default_factory=dict)
28
+
29
+ @classmethod
30
+ def from_mapping(cls, data: dict[str, Any]) -> "GlobalConfig":
31
+ required = (
32
+ _IMAGE_REGISTRY_KEY,
33
+ _IMAGE_REGISTRY_API_URL_KEY,
34
+ _IMAGE_REGISTRY_API_TOKEN_URL_KEY,
35
+ _IMAGE_REPOSITORY_KEY,
36
+ _IMAGE_BASE_REPOSITORY_KEY,
37
+ _DEFAULT_IMAGE_BASE_KEY,
38
+ _VERSION_CHECK_IMAGE_KEY,
39
+ _LOCAL_IMAGE_REPOSITORY_KEY,
40
+ )
41
+ missing = [key for key in required if key not in data]
42
+ if missing:
43
+ raise ConfigError(f"Missing required config values: {', '.join(missing)}.")
44
+ return cls(
45
+ image_registry=data[_IMAGE_REGISTRY_KEY],
46
+ image_registry_api_url=data[_IMAGE_REGISTRY_API_URL_KEY],
47
+ image_registry_api_token_url=data[_IMAGE_REGISTRY_API_TOKEN_URL_KEY],
48
+ image_repository=data[_IMAGE_REPOSITORY_KEY],
49
+ image_base_repository=data[_IMAGE_BASE_REPOSITORY_KEY],
50
+ default_image_base=data[_DEFAULT_IMAGE_BASE_KEY],
51
+ version_check_image=data[_VERSION_CHECK_IMAGE_KEY],
52
+ local_image_repository=data[_LOCAL_IMAGE_REPOSITORY_KEY],
53
+ agents=data.get(_GLOBAL_AGENTS_KEY, {}) or {},
54
+ )
55
+
56
+ def to_mapping(self) -> dict[str, Any]:
57
+ return {
58
+ _IMAGE_REGISTRY_KEY: self.image_registry,
59
+ _IMAGE_REGISTRY_API_URL_KEY: self.image_registry_api_url,
60
+ _IMAGE_REGISTRY_API_TOKEN_URL_KEY: self.image_registry_api_token_url,
61
+ _IMAGE_REPOSITORY_KEY: self.image_repository,
62
+ _IMAGE_BASE_REPOSITORY_KEY: self.image_base_repository,
63
+ _DEFAULT_IMAGE_BASE_KEY: self.default_image_base,
64
+ _VERSION_CHECK_IMAGE_KEY: self.version_check_image,
65
+ _LOCAL_IMAGE_REPOSITORY_KEY: self.local_image_repository,
66
+ _GLOBAL_AGENTS_KEY: self.agents,
67
+ }
@@ -0,0 +1,116 @@
1
+ from dataclasses import dataclass, field
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ _PROJECT_PATH_KEY: str = "path"
6
+ _PROJECT_AGENTS_KEY: str = "agents"
7
+ _DOCKER_ARGS_KEY: str = "docker_args"
8
+
9
+ AGENT_BASE_KEY: str = "base"
10
+ _AGENT_ENTRYPOINT_KEY: str = "entrypoint"
11
+ _AGENT_MOUNTS_KEY: str = "mounts"
12
+ _AGENT_IMAGE_REF_KEY: str = "image_ref"
13
+ _AGENT_EXTENSIONS_KEY: str = "extensions"
14
+
15
+ _MOUNT_GITCONFIG_KEY: str = "gitconfig"
16
+ _MOUNT_GNUPG_KEY: str = "gnupg"
17
+ _MOUNT_SSH_KEY: str = "ssh"
18
+ _MOUNT_DOCKER_KEY: str = "docker"
19
+
20
+
21
+ @dataclass
22
+ class _AgentMounts:
23
+ gitconfig: bool | None = None
24
+ gnupg: bool | None = None
25
+ ssh: bool | None = None
26
+ docker: bool | None = None
27
+
28
+ @classmethod
29
+ def from_mapping(cls, data: dict[str, Any]) -> "_AgentMounts":
30
+ return cls(
31
+ gitconfig=data.get(_MOUNT_GITCONFIG_KEY),
32
+ gnupg=data.get(_MOUNT_GNUPG_KEY),
33
+ ssh=data.get(_MOUNT_SSH_KEY),
34
+ docker=data.get(_MOUNT_DOCKER_KEY),
35
+ )
36
+
37
+ def to_mapping(self) -> dict[str, bool]:
38
+ payload: dict[str, bool] = {}
39
+ if self.gitconfig is not None:
40
+ payload[_MOUNT_GITCONFIG_KEY] = self.gitconfig
41
+ if self.gnupg is not None:
42
+ payload[_MOUNT_GNUPG_KEY] = self.gnupg
43
+ if self.ssh is not None:
44
+ payload[_MOUNT_SSH_KEY] = self.ssh
45
+ if self.docker is not None:
46
+ payload[_MOUNT_DOCKER_KEY] = self.docker
47
+ return payload
48
+
49
+
50
+ @dataclass
51
+ class AgentConfig:
52
+ base: str | None = None
53
+ docker_args: str = ""
54
+ entrypoint: str | None = None
55
+ mounts: _AgentMounts = field(default_factory=_AgentMounts)
56
+ image_ref: str | None = None
57
+ extensions: list[str] = field(default_factory=list)
58
+
59
+ @classmethod
60
+ def from_mapping(cls, data: dict[str, Any]) -> "AgentConfig":
61
+ mounts = _AgentMounts.from_mapping(data.get(_AGENT_MOUNTS_KEY, {}) or {})
62
+ return cls(
63
+ base=data.get(AGENT_BASE_KEY),
64
+ docker_args=data.get(_DOCKER_ARGS_KEY, "") or "",
65
+ entrypoint=data.get(_AGENT_ENTRYPOINT_KEY),
66
+ mounts=mounts,
67
+ image_ref=data.get(_AGENT_IMAGE_REF_KEY),
68
+ extensions=_read_str_list(data.get(_AGENT_EXTENSIONS_KEY)),
69
+ )
70
+
71
+ def to_mapping(self) -> dict[str, Any]:
72
+ payload: dict[str, Any] = {}
73
+ if self.base:
74
+ payload[AGENT_BASE_KEY] = self.base
75
+ if self.docker_args:
76
+ payload[_DOCKER_ARGS_KEY] = self.docker_args
77
+ if self.entrypoint:
78
+ payload[_AGENT_ENTRYPOINT_KEY] = self.entrypoint
79
+ mounts = self.mounts.to_mapping()
80
+ if mounts:
81
+ payload[_AGENT_MOUNTS_KEY] = mounts
82
+ if self.image_ref:
83
+ payload[_AGENT_IMAGE_REF_KEY] = self.image_ref
84
+ if self.extensions:
85
+ payload[_AGENT_EXTENSIONS_KEY] = list(self.extensions)
86
+ return payload
87
+
88
+
89
+ @dataclass
90
+ class ProjectConfig:
91
+ path: str
92
+ agents: dict[str, AgentConfig] = field(default_factory=dict)
93
+
94
+ @classmethod
95
+ def from_mapping(cls, project_path: Path, data: dict[str, Any]) -> "ProjectConfig":
96
+ raw_agents = data.get(_PROJECT_AGENTS_KEY, {}) or {}
97
+ agents = {name: AgentConfig.from_mapping(cfg) for name, cfg in raw_agents.items()}
98
+ legacy_docker_args = data.get(_DOCKER_ARGS_KEY, "")
99
+ if legacy_docker_args:
100
+ for agent_cfg in agents.values():
101
+ if not agent_cfg.docker_args:
102
+ agent_cfg.docker_args = legacy_docker_args
103
+ return cls(
104
+ path=data.get(_PROJECT_PATH_KEY, str(project_path)),
105
+ agents=agents,
106
+ )
107
+
108
+ def to_mapping(self) -> dict[str, Any]:
109
+ agents_payload = {name: cfg.to_mapping() for name, cfg in self.agents.items()}
110
+ return {_PROJECT_PATH_KEY: self.path, _PROJECT_AGENTS_KEY: agents_payload}
111
+
112
+
113
+ def _read_str_list(value: Any) -> list[str]:
114
+ if not isinstance(value, list):
115
+ return []
116
+ return [item for item in value if isinstance(item, str) and item]